try anything chris!

자바 개발자를 위한 97가지 제안 본문

review

자바 개발자를 위한 97가지 제안

뭐든창하 2025. 9. 22. 22:30
728x90

이 책은 학습을 하기 위해서라기 보다는 그래도 자바개발자로 밥먹고 사는데 97지의 제안중에 내가 어느정도를 알고 지키고 있는지가 궁금해서 보게 된 책이다.
참고로 5년정도는 지난 책이라, 그 이후로 바뀐 경향도 있기도 하고, 무엇보다 업무환경이나 동료들의 성향이 모두 다른 개발자들이 그 상황속에서만 겪은 경험으로 얘기하는 제안이기 때문에, 이것이 꼭 정답이라고는 할 수 없음을 감안하고 보았고 그래도 그 중에서 보편적인 제안들은 동의하기도 하고 배울점이라고 생각이 들었다.



01. 자바만으로도 충분하다
많은 개발자가 재사용성을 극대화하기 위해 범용 비즈니스 로직 프레임워크의 개발에 착수했다. 하지만 범용 비즈니스 문제랑 사실상 존재하지 않았으므로 대부분 실패로 돌아갔다. 뭔가 특별한 것을 특별한 방법으로 실행하는 것이야말로 어떤 한 비즈니스를 다른 비즈니스와 차별화하는 것 아닌가. 그러므로 프로젝트마다 새로운 비즈니스 로직을 작성할 일이 있는 것이다.

02. 확인 테스트
JUnit 같은 단위 테스트 프레임워크를 사용하면 함수의 리턴 문자열이 변경될 때 테스트를 수정하기가 어려울 수 있다. 결국 소스 코드 여기저기서 변경된 기댓값을 복사해 붙여넣게 된다. 하지만 확인 테스트 도구를 사용하면 확인한 문자열이 파일에 대신 저장된다. 이 방법은 새로운 가능성을 열어준다. 예를 들면 비교 도구를 열어 변경 사항을 확인한 후 하나씩 머지하면 된다. JSON 문자열 같은 것을 다를 때는 문법 강조 지원도 받을 수 있다. 게다가 각기 다른 클래스에 대한 여러 테스트를 찾아 바꾸기 기능으로 한 번에 수정할 수도 있다.

04. 컨테이너를 제대로 이해하자
오래된 버전의 JVM 까지 컨테이너에 욱여넣게 되면 JVM 어거노믹스(ergonomics)로 인한 여러 위험 요소에 맞닥뜨리게 된다.
JVM 어거노믹스(ergonomics) : CPU의 개수와 가용 메모릴라는 두 가지 핵심 요소를 기준으로 JVM 을 직접 튜닝. JVM 은 이 두 가지 지표를 이용해 어떤 garbage collector 를 이용할지, 어떻게 설정할지, 힙 메모리 크기는 얼마로 할지, ForkJoinPool 의 크기는 얼마로 결정할지 등 중요한 매개변수를 결정한다.
JVM 1.8.191 이전의 JVM 은 자신이 컨테이너 안에서 실행 중이라는 점을 인지하지 못해서 컨테이너가 아닌 호스트OS 의 지표를 측정하려 하고, JVM 이 잘못된 지표를 사용해 스스로 튜닝하려고 시도한다.
최신버전이면서 안전한 런타임을 제공하는 버전의 JVM 을 이용하자.

05. 행위를 구현하는 것은 쉽지만 상태를 관리하는 것은 어렵다
유효성 검사(validation) 프레임워크를 이용해 사용자가 제공하는 입력값은 확인하지만 모든 코드가 '순전히 내부 상태의 값만 변경하는' setter 를 호출할 수 있다.
코드에 setter 가 정말 필요한가? setter 를 자동으로 생성하지 말자.

07. 아키텍처의 품질을 체계화하고 검증하는 방법의 장점
ArchUnit(https://www.archunit.org)은 JUnit 이나 TestNG 같은 자바 단위 테스트 프레임워크를 사용해 자바 코드의 아키텍처를 확인하는 확장 가능한 오픈로스 라이브러리다. ArchUnit 은 순환 의존성은 물론 패키지 및 클래스, 계층과 코드 조각 사이의 의존성을 검사한다.

08. 문제와 업무를 더 작은 단위로 나누기
큰 문제는 해결하는 데 더 오래 걸린다.
좋은 방법은 문제를 더 작은 조각으로 나누는 것이다. 더 작게 나눌수록 더 좋다. 일단 작은 문제를 하나 해결하면 더는 그 문제를 고민할 필요 없이 다른 문제로 넘어가면 된다.

09. 다양성을 인정하는 팀 만들기
협업의 핵심은 팀 내 심리적 안정성과 신뢰를 쌓는 것이다.
심리적으로 안정된 팀에서는 사람들이 커뮤니케이션은 그 비용보다 장점이 더 크다고 생각하는 경향이 있다. 참여를 통해 변화에 대한 저항이 줄어들고, 사람들이 더 자주 참여할수록 더 참신한 아이디어가 떠오른다.
개인의 성향도 무시할 수 없다. 개인의 성향은 다른 성향의 사람을 신뢰할 수 있는 환경을 만드는 것만큼이나 중요하다.
새로운 프로세스, 코딩 스타일, 커밋 메시지 형식을 정립하길 좋아하고 적절한 절차를 따르지 않으면 한 번 더 설명해 주는 사람도 있다. 함께 일하는 팀원 중에는 약속은 보수적으로 하면서 기대를 웃도는 결과물을 내놓는 사람도 있고, 의존성 갱신, 패치 설치, 보안 위험 등 뭐든지 잘못될 수 있다고 생각하는 사람도 있다. 모두의 다양성을 존중하고 너무 심하게 몰아붙이진 말자.

13. 코드 복원전문가
최고의 코드는 나중에 그 코드를 보게 될 프로그래머를 생각하며 작성한 코드다.
기업은 그저 또 하루, 다음 전력질주, 그 다음 주기를 살아가기에 급급하다. 더 안타까운 점은 누구도 이런 현상에 대해 우려하지 않는 것 같다는 점이다.
처음부터 영원히 지속될 것을 만들려면 비용이 크게 증가하므로 그럴 가치가 없지만 반대로 단기 이익만을 생각하고 코드를 만들면 결국 그 자체의 무게를 견지지 못하고 무너질 것이다. 그렇기에 '(모두가 바라지만 거의 항상 실패하는)같은 것을 더 나은 방법으로 다시 만드는' 일이 아니라 기존 코드를 천천히 다음어 다시 관리할 수 있는 상태로 재창조하는 코드 복원전문가가 필요하다. 여기에 테스트를 조금 더 추가하고, 말도 안되는 클래스를 잘게 나누고, 사용하지 않는 기능은 과감히 제거해서 더 나아진 코드를 다시 내놓는 그런 사람이 필요하다.

14. JVM의 동시성
자바는 분산 메모리를 지원하지 않으므로 멀티스레드 프로그램을 여러 머신에 수평적으로 확장하는 것이 불가능하다.
공유 메모리 제한을 극복하는 가장 간단한 방법은 Lock 대신 분산 큐(distributed queue)를 이용해 스레드를 조율하는 것이다.

16. 선언전 표현식은 병렬성으로 가는 지름길이다
선언적(declarative)프로그래밍은 목적을 달성하기 위한 방법에 대한 목표 추상화를 표현하는 코드를 작성하는 것이다.

  // 명령형 코드
  List<Integer> squareImperative(final List<Intenger> datum) {
    var result = new ArrayList<Integer>();
    for (var i = 0; i < datum.size(); i++) {
      result.add(i.datum.get(i) * datum.get(i));
    }
    return result;
  }
  
  // 선언적 코드
  List<Integer> squareDeclarative(final List<Intenger> datum) {
  	return datum.stream().map(i -> i * i).collect(Collectors.toList());
  }
  
  // 선언적 코드(병렬)
  List<Integer> squareDeclarativeParallel(final List<Intenger> datum) {
    return datum.parallelStream().map(i -> i * i).collect(Collectors.toList());
  }


17. 더 나은 소프트웨어를 더 빨리 전달하기
여러분은 코드를 작성하고 대가를 받는 것이 아니라 사용자가 뭔가 가치 있는 것을 더 쉽게 할 수 있도록 만듦으로써 대가를 받는 것이며, 여러분이 작성한 코드가 프로덕션 환경에서 동작하기 전까지는 아무리 열심히 일한들 소용이 없다.
실제 구현에 앞서 모호한 요구 사항을 명확하게 이해하려 하지 않고 자의적인 해석으로 업무를 수행하는 등 절차를 무시하지 않는다.
여러분이 작성한 코드가 요구 사항에 부합하는지를 확인하는 자동화된 테스트를 작성하고 실행하는 등 절차를 더 빠르게 수행하기 위해 노력한다.
더 나은 소프트웨어란 '올바른 기능을 구현'하는 것과 '올바르게 기능을 구현'하는 두 가지 개념을 짧게 표현한 것이다.
'올바른 기능을 구현'하는 것은 항상 요구 사항과 수용 조건을 만족하는 코드를 작성한다는 뜻.
'올바르게 기능을 구현'하는 것은 다른 프로그래머도 버그를 성공적으로 수정하거나 새로운 기능을 추가할 수 있도록 이해하기 쉬운 코드를 작성한다는 뜻.
문제가 발생할 확률을 낮추고 사용자들이 더 신속하게 여러분이 업데이트한 시스템을 사용할 수 있도록 더 작은 변경 내역을 프로덕션 환경에 더 자주 배포하는 것을 권장한다.

18. 지금 몇 시에요?
LocalDateTime 은 2019년 10월 13일 오후 1시 15분의 개념을 의미한다. 이 시간은 여러 시간선(timeline)에 걸쳐 존재할 수 있다. Instant 는 이 시간선의 특정 지점을 가리킨다 이 지점은 보스턴이든 베이징이든 모두 같다. LocalDateTime 부터 Instant 를 얻으려면 특정 시간에 대한 협정 세계시(UTC) 오프셋과 일광 절약 시간(DST) 규칙을 가진 TimeZone 이 필요하다. ZonedDateTime 은 TimeZone 정보를 가진 LocalDataTime 이다.

19. 기본 도구의 사용에 충실하자
진저어한 기술을 손에 넣으려면 기본 도구를 이해하고 충분히 활용하자.

22. 자바 컴포넌트 간의 이벤트
자바의 모든 클래스를 컴포넌트로 간주할 수 있다. 자반의 이벤트는 컴포넌트의 상태를 변경하는 행위다.
Oven 컴포넌트와 Person 컴포넌트를 가지고 있다고 가정해보자. 이 두 컴포넌트는 병렬로 존재하며 각자 독립적으로 동작한다. Person 을 Oven 의 일부로 선언해서도 안 되고 반대로 Oven 을 Person 의 일부로 만들어서도 안된다. Person 컴포넌트가 배고픔을 느끼면 Oven 컴포넌트가 음식을 준비하도록 하는 기능을 구현해보자.
1. Oven 컴포넌트에서 짧은 간격으로 Person 컴포넌트를 확인한다. 이 방법은 Person 컴포넌트를 귀찮게 할 뿐 아니라 여러 Person 인스턴스를 확인해야 한다면 Oven 컴포넌트 자체에도 부담이 된다.
2. Person 컴포넌트가 공개 이벤트 Hungry 를 발생하고 Oven 컴포넌트가 이를 구독해서, 이벤트가 발생하면 이를 알아채고 음식을 준비한다. 이 방법은 두 컴포넌트 간에 직접 결합 없이도 서로를 리스닝(listening)하고 통신할 수 있는 이벤트 아키텍처를 사용하는 방법이다.

23. 피드백 루프
제대로 만드는 것을 고민하지 말고 잘못된 것을 어떻게 알아낼지, 잘못된 것을 찾았을 때 얼마나 쉽게 수정할 수 있는지를 고민하자. 왜냐하면 분명 뭔가 잘못될 것이기 때문이다.

24. 불꽃 그래프를 이용한 성능 확인
보편적인 자바 프로파일러(profiler)는 바이트 코드 계측(byte code instrumemtation)이나 샘플링 기법(짧은 주기로 stack trace 를 수집하는 방법)을 이용해 실행 시간이 긴 구간을 찾는다.
거의 모든 시스템으로부터 stack tace 를 수집해 기발한 형태의 다이어그램으로 보여주는 불꽃 그래프(flame graphs, https://oreil.ly/2kCDd)
trace 를 각 stack 수준으로 정렬해 집계한 것, 각 스택 수준별 카운트는 코드의 각 부분의 총 실행 시간의 백분율을 의미.
불꽃은 아래에서 위로 향하며 프로그램이나 thread(main 또는 event loop)의 진입점부터 코드가 실행되는 과정을 거쳐, 불꽃의 끝에서는 코드 실행이 완료되는 지점을 표시한다.
중요한 것은 각 블록의 너비와 스택의 깊이(depth). 그래프의 높이가 높을수록 더 많은 시간을 허비한 것. 특히 꼭대기 부분에서 매우 넓은 블록을 본다는 것은 해당 부분에서 병목이 발생하고 있다는 뜻.

26. 자주 릴리스하면 위험을 줄일 수 있다
자주 릴리스하면 위험을 줄일 수 있다.
위험이란 장애가 발생했을 때 받을 수 있는 최악의 영향과 장애가 발생할 가능성을 결합한 요소.
실패할 시나리오에 대한 자동화 테스트를 구축하자. 그리고 새로운 실패를 발견할 때마다 테스트에 추가하자. 회귀 테스트를 계속 늘리되, 가볍고 빠르며 반복할 수 있게 유지하다.

27. 퍼즐에서 제품까지
'내 직업은 코드를 바꾸는 일이 아니라 변화를 설계하는 일'이라고 생각한다. 코드는 그저 세부 사항일 뿐이다.
기능 플래그, 사용이 금지된 deprecated 메서드, 하위 호환성을 처리하기 위해 if 문으로 도배한다고 해서 잘못된 코드라고 볼 수 없다. 이런 if 문은 변화를 표현하는 것뿐이다. 그리고 중요한 것은 변화이지 코드의 어떤 특정 상태가 아니다.
변화를 디자인한다는 것은 관측성(observability)을 구축해서 누가 금지된 기능을 여전히 사용하는지, 누가 새로운 기능에서 값을 얻어가는지 확인할 수 있음을 의미한다.
'올바른' 제품은 한 가지로 정의할 수 없다. 분명 많은 것이 올바르지 않기에 '오동작'하지 않도록 주의해야 한다. 그 외에는 '더 나은 것'에 초점을 맞춘다.

29. 가비지 컬렉션은 나의 친구
현대의 가비지 컬렉션은 메모리 할당/해제보다 더 빠르게 동작하며 GC가 실행되는 시간에도 속도를 높일 수 있다. 가비지 컬렉터는 메모리 해제 외에 다른 작업도 수행하기 때문이다. 즉 메모리의 할당과 메모리상의 객체 재정렬도 실행한다.
어째서 메모리상의 객체 위치가 애플리케이션 성능에 영향을 주는 걸까? 프로세서의 캐시에서 객체를 불러오면(fetch) 이웃한 데이터도 함께 가져오게 된다. 그래서 다음 작업에서 이웃한 데이터에 접근하면 빠르게 실행된다. 이렇게 동시에 사용하는 객체를 메모리상에 서로 가깝게 배치하는 것을 객체 지역성(object locality)라고 하며 성능 향상에 도움이 된다.
힙 메모리가 파편화되어 있으면 프로그램이 객체를 생성할 때 충분한 크기의 빈 메모리 공간을 찾는 시간이 오래 걸린다. 실험 삼아 강제로 GC가 더 자주 실행되게 하면 GC 오버헤드는 많이 증가하지만 애플리케이션 성능이 향상된다.
성능을 측정할 때는 비즈니스 가치와 관련 있어야 한다. 초당 트랜잭션, 평균 서비스 시간 또는 최악의 응답 지연을 최적화하자. GC가 소비하는 시간을 너무 세세히 최적화할 필요는 없다. GC가 소비하는 시간은 실질적으로 프로그램의 속도에 도움이 되기 때문이다.

30. 이름 짖기를 잘 하자
먼저 무의미한 이름(foo), 너무 추상적인 이름(data), 중복된 이름(data2), 모호한 이름(DataManager), 약자나 줄인 이름(dat), 한 글자(d) 등은 피하는 것이 좋다. 이렇게 모호한 이름을 사용하면 프로그래머가 시간을 들여 코드를 읽은 후에야 코드를 작성할 수 있으므로 모든 사람의 업무 속도가 저하된다.
이름을 지을 때 단어를 최대 4개 사용(id 나 문제 도메인에 적용된 것을 제외하고)하고 약어는 사용하지 말자. 한 단어만으로 충분한 경우는 드물다. 네 단어 이상 사용하는 것은 어설프며 더는 의미를 부여하지 않는다.
복수형은 집합 명사로(예를 들면 appointment_list 대신 calendar로) 대체하자.
엔티티 쌍의 이름은 관계의 이름으로(예를 들면 company_person 은 employee, owner, shareholder 등으로) 대체하자.
클래스와 객체 이름을 혼합하지 말자. 날짜 필드인 dateCreated 는 created 로 바꾸고 Boolean 타입의 필드 isValid 는 valid 로 바꾸자. 그러면 객체 이름에 타입이 중복되는 상황을 방지할 수 있다.
결국, 이름 짓기를 마스터한다는 것은 구시대의 규칙 중 어떤 것을 지키지 않을지 선택하는 것이다.

32. null 을 피하는 방법
어떤 값을 저장할지 결정하기 전에 변수를 선언하는 것은 대부분 좋은 생각이 아니다. 초기화가 복잡하다면 초기화 로직을 메서드로 옮기자.
변수를 null 값으로 초기화하면 에러 처리 코드를 제대로 작성하지 않을 경우 의도치 않게 널 값을 노출하게 된다.
null 값을 리턴하기 보다 Optional 를 리턴하는 것이 코드를 더욱 명확하게 만드는 방법이다.
null 값 매개변수를 전달하거나 받지 말자. T 객체가 필요하다면 이 객체를 전달해 달라고 요구하자. 이 객체가 없어도 된다면 아예 요구하지 말자.

38. 일은 끝났어요. 그런데...
첫 번째 작업을 완전히 끝내지 못한 상태에서 다른 작업을 시작했다고 생각해 보자. 바로 이 시점에 기술 부채가 생겨나는 것이다! 경우에 따라서는 선택에 의해 기술 부채를 남기기도 한다. 하지만 여러분이 끝내지 않은 일을 끝냈다고 말하는 바람에 기술 부채가 생기는 것보다는 논의를 거쳐 기술 부채를 남기도록 결정하는 것이 훨씬 올바른 선택이다.
반드시 기억하자. 실제로 끝내기 전까지는 절대 끝냈다고 말하지 말자.

45. 최신 동향을 파악하자
최신 동향을 파악하는 것은 시장에 있는 모든 기술을 알아야 한다는 의미가 아니다. 그저 계속 동향을 파악하면서 공통 키워드에 주목하고 기술 트렌드를 이해하면 된다. 더 깊이 공부하는 것은 현재 수행 중인 업무와 관련이 있거나 개인적으로 흥미가 있을 때(이상적으로는 둘 다 해당할 때) 해도 늦지 않다.

46. 주석의 종류
자바독(Java Doc) 주석(/** ... */ 형식)은 클래스, 인터페이스, 필드, 메서드에만 사용하며 이들을 선언한 코드 바로 위에 작성한다. 자바독 주석은 계약(contract) 이다. 이 주석으로 API 사용자에게 구현의 상세는 숨기고 타입이 추상화하는 동작을 설명한다. 그와 동시에 이 메서드를 실제로 구현할 개발자에게도 어떤 동작을 구현해야 하는지를 설명한다.
블록(Block) 주석은 /* ... */ 로 둘러싼다. 이 주석은 어느 자리에 작성ㅎ래도 무관하며 개발 도구는 거의 이 주석을 무시한다. 이 주석은 주로 클래스나 메서드의 시작 지점에서 그 구현 내용을 설명할 때 사용한다. 기술적인 상세 내용을 작성할 수도 있지만 코드를 작성하게 된 시점의 주변 상황(코드는 어떤 동작을 하는지 설명한다면 주석은 왜 이 동작이 필요한지를 설명한다)을 설명하거나 또는 현재 코드가 고려하지 않는 코드 실행 경로 등을 설명하기 위한 용도로도 사용한다. 보편적으로는 여러분이 처음 구현한 솔루션이 완전히 마무리되지 않았을 때, 상황에 의해 어떤 절충안을 채택했을 때, 요구 사항이 이상하거나 의존하는 API 가 형편없어서 코드가 볼썽사나울 때 당시 상황을 주석으로 남기는 용도로 사용하자. 나중에 본인이나 다른 동료가 이 코드를 읽어보면 주석에 고마움을 느낄 것이다.
줄 단위 주석은 코드 동작을 설명하기 위해 주로 사용하는데 이는 사실 좋은 방법이 아니다. 하지만 코드가 굉장히 특이한 언어의 기능을 사용하거나 조금만 바꿔도 코드에 문제가 생기는(예를 들면 동시성 문제) 등 몇몇 상황에서는 여전히 유용한다.

48. 컬렉션을 제대로 이해하자
컬렉션은 순서가 있는(ordered) 것과 순서가 없는(unordered) 것으로 구분할 수 있다.
또 다른 방법은 정렬된(sorted) 것과 정렬되지 않은(unsorted) 것으로 나눌 수 있다.
가장 중요한 차이점은 순서가 있는 컬렉션에는 아이템이 항상 같은 순서로 저장되지만 정렬되지는 않는다. 정렬된 컬렉션은 정렬 순서를 예측할 수 있으므로 아이템의 순서 역시 예측이 가능하다. 모든 정렬된 컬렉션은 순서가 있는 컬렉션이지만, 순서가 있는 컬렉션이라고 해서 모두 정렬된 컬렉션은 아니라는 점을 기억하자. JDK 는 순서가 있는 컬렉션, 순서가 없는 컬렉션, 정렬된 컬렉션, 정렬되지 않은 컬렉션을 구현한 여러 클래스를 제공한다.
List 는 안정적인 인덱스를 기반으로 순서가 있는 컬렉션을 구현한 인터페이스다. 구현체(ArrayList, LinkedList)
Map 은 키와 값의 관계를 유지하며 유일한 키 값만 보관하는 인터페이스다. 구현체(HashMap, LinkedHashMap, TreeMap)
Set 은 유일한 아이템의 컬렉션을 정의하는 인터페이스다. 구현체(HashSet, LinkedHashSet, TreeSet). 내부적으로는 Map 을 사용한다.

51. 카타를 하기 위해 학습하고 카타를 이용해 학습하자
코드 카타는 실습으로 특정한 기술을 연마하는 실천적인 프로그래밍 기법이다.
코드 카타를 직접 만들어보고 싶다면 다음 절차를 따라 해보자.
1. 학습하려는 주제를 선정한다.
2. 원하는 지식을 설명할 수 있으며 성공하는 단위 테스트를 작성한다.
3. 최종 솔루션에 만족할 때까지 반복해서 코드를 리팩토링한다. 리팩토링 과정에서 단위 테스트가 실패하지 않는지 확인한다.
4. 테스트가 실패하도록 실습한 솔루션을 삭제한다.
5. 실패하는 테스트와 관련 코드 그리고 빌드 결과물을 버전 관리 시스템에 커밋한다.

52. 레거시 코드를 사랑하는 방법
레거시 시스템을 유지보수 해야 한다면 어떨까?
첫 번째 해결책은 테이프로 감는 것이다. 숨을 꾹 참고 결함을 수정한다. 오랜 시간이 지나도 별로 훼손되지 않았을 수도 있지만 깨진 유리 하나만 있어도 순식간에 나머지 유리도 모조리 박살이 날 것이다.
두 번째 해결책은 새로운 시스템을 바닥부터 다시 개발하는 것이다. 대부분 재개발은 제대로 진행되지 않거나 끝이 나지 않는다. 예전 코드가 형편없이 보이겠지만 그 코드는 이미 수많은 전투에서 살아남은 코드다. 시스템을 새로 개발하기 시작하는 여러분은 그동안 어떤 전투가 있었는지 모를뿐더러 그 비즈니스 도메인에 대한 지식의 상당 부분을 잃는 셈이다.
그럼 어떻게 해야 할까? 제대로 된 방법으로 수정하는 방법을 배워야 한다. 교살자 패턴(https://oreil.ly/SWJFc)을 이용하면 간단한다. 좋지 않은 코드를 깔끔하고 철저히 테스트한 새 코드로 교체하는 것부터 시작하고 이 작업을 반복해서 예전 애플리케이션 위에 새로운 애플리케이션을 만들어 가는 것이다. 설령 완전히 끝내지는 못하더라도 오래된 코드가 계속 썩어가도록 두는 것보다는 새로운 코드와 오래된 코드가 섞여 있는 편이 낫다.

55. 자바 API를 디자인하는 기술
우리는 모두 API 디자이너다. 소프트웨어는 그 자체로는 아무 의미가 없다. 다른 개발자가 작성한 다른 소프트웨어와 함께 동작할 때 비로소 의미를 가질 수 있다.
처음부터 제대로 된 API를 작성할 수 있다고 생각하지 말자. API를 디자인하는 것은 반복 작업이며, API를 개선할 유일한 방법은 개밥 먹기(dogfooding, 자신이 만든 제품이나 서비스를 직접 사용해 보며 문제나 버그를 찾는 방법)뿐이다. API 에 대한 테스트와 예제를 작성하고 동료 및 사용자와 끊임없이 소통하자. 몇 버너이고 반복해서 불분명한 의도, 불필요한 코드, 미처 추상화되지 않은 부분을 개선하자.

56. 간결하고 가독성이 좋은 코드
코드는 한 줄 한 줄 모두 최대한 명확해야 한다. 모든 코드는 필요한 것이어야 한다. 가독성이 좋고 간결한 코드를 작성하려면 형식(format)과 내용(content)에 주의를 기울여야 한다.
* 들여쓰기를 이용해 코드를 정리하자.
* 변수와 메서드에 의미 있는 이름을 채택하자.
* 필요한 경우에는 코드에 주석을 작성하자.
* 주석처리한 코드는 커밋하지 말자. 머뭇거리지 말고 그냥 지우자.
* 혹시 나중에 필요할지도 모를 코드까지 작성하는 오버엔지니어링의 우를 범하지 말자. 필요 이상의 코드는 버그나 유지보수 오버헤드를 야기할 뿐이다.
* 장황한 코드를 작성하지 말자. 개발자가 얼마나 많은 양의 코드를 작성했는지보다 얼마나 깔끔하게 가독성이 좋은 코드를 작성했는지로 평가하자.
* 아직 시도해 본 적이 없다면 함수형 프로그래밍을 공부하자.
* 짝 프로그래밍(pair programming)을 도입하자. 코드를 작성하는 동안 다른 사람에게 스스로의 선택과 그 이유를 설명해야 하므로 더욱 의미 있는 코드를 작성하는 방법이기도 한다.
코드가 복잡할수록 버그도 늘어난다. 쉽게 이해할 수 없는 코드는 더 많은 버그를 내포하고 있다.

60. 업계의 발전에 기여하는 기술의 필요성
현실 세계의 문제를 해결하기 위한 차세대 장난감 따위는 필요하지 않다.
자바는 이런 며에서 훌륭한 기술이다. 현대식 언어 기능을 아우를 정도로 새로운 언어지만 신뢰할 수 있을 정도로 성숙해 있다. 대규모 코드도 잘 정리할 수 있으며, 순수한 기술적인 문제에서 벗어나 실질적인 비즈니스 문제에 집중하는 데 도움을 주는 제품, 도구, 생태계의 지원도 훌륭하다. 환경과 시스템을 분리할 수 있는 강력한 스택이며 숙력된 직원을 찾을 수 있을 정도로 표준화되었다.
자바는 적어도 수십 년은 지속할 수 있는 시스템을 구현할 정도로 신뢰할 수 있으며 안정적인 플랫폼이다.
엔지니어링은 유행을 따라서는 안 된다. 소프트웨어 개발은 지식과 조직이 습득한 것이다. 어떤 부품이 어떻게 동작하는지 모른다면 전체가 어떻게 동작할지 모른다는 뜻이다. 어느 정도 동작하는 코드를 서로 주고받는 것이 재미있을지는 모르지만, 어려운 현식을 여유롭게 감당할 수 있는 뭔가를 개발하는 것이 바로 프로의 자세다.

64. 기본 접근 한정자를 가진 기능 단위 패키지

  // 계층별 패키지 구조
  tld.domain.project.model.Company
  tld.domain.project.model.User
  tld.domain.project.controllers.CompanyController
  tld.domain.project.controllers.UserController
  tld.domain.project.storage.CompanyRepository
  tld.domain.project.storage.UserRepository
  tld.domain.project.service.CompanyService
  tld.domain.project.service.UserService
이러허게 클래스를 계층별 패키지 구조로 정리하면 너무 많은 메서드를 공개(public) 메서드로 선언해야 한다. UserService 는 User 클래스를 저장소로 부터 읽고 쓸 수 있어야 한다. 그런데 UserRepository 가 다른 패키지에 있으므로 UserRepository 의 거의 모든 메서드를 공개해야 한다.

  // 기능 단위 패키지 구조
  tld.domain.project.company.Company
  tld.domain.project.company.CompanyController
  tld.domain.project.company.CompanyService
  tld.domain.project.company.CompanyRepository
  tld.domain.project.user.User
  tld.domain.project.user.UserController
  tld.domain.project.user.UserService
  tld.domain.project.user.UserRepository
클래스를 이렇게 정리하면 UserRepository 의 어떤 메서드도 공개할 필요가 없다. 모든 매서드를 패키지 비공개로 선언해도 UserService 클래스가 얼마든지 사용할 수 있다. 그래서 필요에 따라 UserService 클래스의 메서드만 공개하면 된다.

66. 좋은 단위 테스트에 기반한 프로그래밍
@Test 애노테이션이 이미 이 코드가 테스트 코드임을 말해 주고 있다. 코드를 읽는 사람에게 지금 보는 코드가 테스트 코드임을 알려주는 것보다는 정확히 뭘 테스트하는지 알려줘야 한다.
테스트 중인 메서드 이름을 따서 테스트의 이름을 붙이라는 것이 아니다. 어떤 동작, 속성, 기능 등을 테스트하는지 알려주라는 뜻이다.
테스트의 목적이 무엇이냐고 묻고 싶다. '이 코드가 동작하는지' 테스트하는 것이 목적인가? 그렇다면 테스트의 목적 중 절반만 달성한 것이다. 코드를 작성하는 데 어려움은 '이 코드가 동작하는지'를 결정하는 것이 아니라 '이 코드가 동작한다'의 의미가 무엇인지를 결정하는 것이다.
예를 들면, 언더스코어(_) 문자를 이용해 가독성을 개선해서 Addition_of_item_with_unique_key_is_retained() 가 적합하다. @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscored) 를 사용하면 'Addition of item with unique key is retained' 처럼 예쁘게 출력된다.
테스트가 성공한다고 해서 그 코드가 동작한다는 점을 보장하지는 않는다. 하지만 좋은 단위 테스트를 작성하려면 실패의 의미가 명확해야 한다. 즉 코드가 동작하지 않음을 의미해야 한다. '프로그램의 테스트는 버그가 있음은 보여줄 수 있지만 버그가 없음은 절대 보여주지 못한다!'
단위 테스트는 테스트가 제어할 수 없는 것에 의존해서는 안 된다는 것을 의미한다. 파일시스템, 네트워크, 데이터베이스, 비동기 코드의 실행 순서 등에 영향을 받을 수는 있지만 제어할 수 없다. 테스트 중인 단위는 올바른 코드가 실패를 유발할 수 있는 것에 의존해서는 안 된다.

69. 자바의 재탄생
자바 9 이후부터는 매년 3월과 9월에 자바의 새로운 메이저 버전이 릴리스된다.

72. 속독을 위한 리팩토링
코드의 구조는 코드를 읽는 사람을 도와줘야 한다.
코드를 작성할 때는 스스로 빨리 읽을 수 있는지 확인해 보자. 시각적 고정과 메타 가이딩을 염두에 두고 코드를 작성하자. 관련 정보를 눈으로 확인할 수 있는 논리적인 느낌을 줄 수 있도록 코드 구조를 갖추자. 그러면 코드를 빨리 읽을 수 있음 뿐만 아니라 흐름도 더 잘 파악할 수 있다.

78. 테스트 주도 개발
보편적으로 고품질의 소프트웨어란 다음과 같은 속서어을 갖는 코드를 의미한다.
* 모듈성(Modularity)
* 낮은 결합(Loose coupliing)
* 응집력(Cohesion)
* 적절한 관심사 분리(Good separation of concerns)
* 정보 은폐(Information hiding)
테스트 가능한 코드는 이런 속성을 가지고 있다. 코드 커버리지는 그다지 좋은 지표가 아니다.
TDD는 형편없는 개발자를 훌륭한 개발자로 바꾸지는 않지만 더 나은 프로그래머를 양산한다.
TDD는 매우 간단해서 '레드, 그린, 리팩터(Red, Green, Refactor)'의 순서만 지키면 된다.
* Red : 이 단계에서는 코드의 행위적 의도를 표현하는 것에 집중한다.
* Green : 테스트를 통과하기 위한 최소한의 작업만 수행한다. 그 방법이 설령 너무 소박해 보이더라도 그렇게 해야 한다.
* Refactor : 일단 Green 상태로 돌아왔다면 안전하게 리팩토링이 가능하다. 그러면 잘못된 방향으로 빠지지 않고 올곧게 작업을 진행할 수 있다. 작고 간단한 단계를 만들고 테스트를 다시 실행해서 모든 것이 제대로 동작하는지 확인한다.

79. bin 디렉터리에는 좋은 도구가 너무나 많다
* jps : 실행 중인 모든 JVM 의 목록
ex) jps -m : main 메서드에 전달된 매개변수
ex) jps -v : JVM 자체에 전달된 모든 매개변수
* jmap : JVM 프로세스의 메모리 공간에 대한 요약 정보
ex) jmap -heap {processId}
* jhat : jmap 명령으로 생성한 파일을 이용해 로컬 웹서버를 실행하여 정보를 확인
ex) jhat {heap dump file}
* jinfo : JVM 명령줄 플래그와 JVM이 로드한 모든 시스템 속성 확인
ex) jinfo {processId}
* jstack : JVM 안의 모든 자바 thread 의 stack trace 출력
ex) jstack {processId}

82. 스레드는 인프라스트럭처로 취급해야 한다
만일 코드가 동기화 구문, lock, mutex 등(모두 운영체제를 위한 것이다)을 사용한다면 뭔가 잘못하고 있을 가능성이 크다.

83. 정말 좋은 개발자의 세 가지 특징
첫 번째이자 가장 중요한 특징은 호기심이다.
두번째와 세 번째 특징은 공감과 상상력이다.

84. 마이크로서비스 아키텍처의 트레이드 오프
측정 가능한 트레이드오프를 설명하는 CAP 이론, 이 이론은 실행 중인 데이터베이스는 일관성(consistency), 가용성(availability), 파티션 내구서어(partition tolerance) 등 세 가지 특성 중 두 가지만 보장할 수 있다는 이론이다. 애플리케이션이 네트워크 경계를 넘어서 상태를 공유한다면 일관성과 가용성 중 하나를 선택해야 하며 둘 다 보장할 수는 없다고 주장한다.
소프트웨어 개발의 세계에 발을 들이다 보면, 결국 올바른 선택 따위는 없다는 것을 알게 된다. 최선의 선택이 있다고 믿으며 그 선택에 엄청난 논쟁을 벌인다.

85. 예외를 확인하지 말자
확인된 예외(Checked Exception)의 의도는 메서드가 성공적으로 실행될 때의 입력과 출력의 타입이 중요한 만큼, 메서드가 실패하는 경우도 예외의 타입으로 표현해서 두 가지 시나리오 모두 같은 수준의 타입 중요성을 갖도록 하자는 것이다. 기반 코드가 작고 폐쇄적이라면 일부 예외를 간과하지 않도록 하는 이런 타입 중요성은 달성하기 쉬운 목표다. 그리고 일단 달성하면 코드의 완성도에 어느 정도(아주) 기본적인 보상도 얻을 수 있다. 하지만 이는 기반 코드의 크기가 작을 때나 동작할 법한 사례로, 규모가 커지면 제대로 동작하지 않는다.
원래 의도가 무엇이든 확인된 예외는 이제 일상에서 그저 장애물로 취급되고 있다. 확인된 예외는 문법적으로 부담이 될 뿐이고 실질적인 문제는 그보다 더 크다. 단순히 프로그래머의 학습을 요구하거나 구문이 장황해지는 문제가 아니다. 프레임워크 개발이나 확장 가능한 코드의 측면에서 볼 때 확인된 예외는 애당초 결함이었던 것이다.
인터페이스를 정의한다는 것은 메서드 시그니처를 이용해 계약을 표현한다는 것이다. 인터페이스는 안정적으로 정의하기도 어렵고 나중에 개선하기도 어렵다. 여기에 throws 까지 추가하면 일은 더 어려워진다.
확인되지 않은 예외(Unchecked Exception)를 사용하는 방법이 그나마 가장 가볍고, 가장 안정적이며, 가장 열린 접근법이다.

87. 퍼즈 테스트의 어마무시한 효과
프로그래머는 대부분 잘못된 입력이 주어졌을 때 소프트웨어가 얼마나 견고하게 동작하는지를 테스트하는 것보다 유효한 입력이 주어졌을 때 소프트웨어가 올바르게 동작하는지를 테스트한다는 뜻이다.
퍼즈 테스팅(Fuzz Testing)은 이미 존재하는 테스트 슈트(suite)에 쉽게 부정적 테스트(negative testing)를 추가할 수 있는 엄청나게 효율적인 기법이다.
* 변형 기반 퍼저(mutation-based fuzzer)는 유효한 입력의 예시를 변경해서 유효하지 않은 테스트 입력을 생성한다.
* 생성 기반 퍼저(generation-based fuzzer)는 문법 같은 형식화된 입력을 생성한다. 이 입력은 유효한 이렵의 구조를 정의한다.

88. 커버리지를 이용해 단위 테스트 개선하기
만일 커버리지를 높이기 위한 목표만으로 테스트를 작성한다면 테스트와 실제 구현 코드의 결합이 높아질 수 있다.
전체 코드베이스의 테스트 커버리지를 주기적으로 측정한다면 절대적 숫자보다는 트렌드에 집중하라고 권하고 싶다. 커버리지 대상이 일관적이지 않다면 대부분 테스트하기 쉬운것만 테스트하려 한다는 사실을 목격했기 때문이다.

89. 사용자 정의 아이덴티티 애노테이션을 자유롭게 사용하자
아이덴티티 애노테이션(identity annotation)의 차이점은 어떤 기능도 갖지 않는다는 점이다. 이 애노테이션은 단지 코드나 아키텍처의 어떤 관점을 다루고 분석하거나 문서화하기 위해 사용하는 프로그래밍적 정보를 제공할 뿐이다. 아이텐티티 애노테이션을 사용하면 트랜잭션 경계나 도메인, 서브도메인을 특정하고 서비스의 분류, 프레임워크 토드의 표시 등 여러 방법으로 활용할 수 있다.

90. 테스트를 이용해 더 나은 소프트웨어를 더 빨리 개발하자
테스트는 안정적이어야 하며 자신감을 향상할 수 있어야 한다. 테스트를 믿을 수 없다면 수정하거나 차라리 지워버리자. 절대 무시해서는 안 된다. 테스트를 무시하면 나중에 그 이유를 알기 위해 더 많은 시간을 낭비하게 될 것이다. 더는 가치가 없다면 테스트를 지워버리자.

91. 커뮤니티의 힘을 빌려 경력을 개발하자
여러분의 경력과 관련한 결정을 내리는 사람은 여러분이 작성한 코드를 보지 않는 경우가 태반이기 때문이다. 그러므로 그런 사람이 여러분의 이름을 듣고 볼 수 있도록 해야 한다.

95. 주석은 한 문장으로 작성하라
대규모 애플리케이션에서 주석을 올바르게 작성하는 방법은 다음 절차에 따라 한 문장으로 작성하는 것이다.
1. 최선의 코드를 작성한다.
2. 모든 공개(public) 클래스와 메서드/함수에 한 문장 주석을 작성한다.
3. 코드를 리팩토링한다.
4. 필요 없는 주석을 제거한다.
5. 코드를 잘 설명하지 못하는 주석은 다시 작성한다.
6. 정말 필요한 곳에만 상세 주석을 추가한다.
이 방법은 코드가 스스로의 존재 이유를 설명하지 못해서든 아니면 미처 코드를 리팩토링할 시간이 어벗어서든 어떤 주석을 남겨둬야 하는지 확인하는 데 도움이 된다. 직접 한 문장 주석을 작성해 보면 알게 될 것이다. 만약, 좋은 주석을 작성하는데 몇 분이 걸린다면 그 주석은 정말 필요한 주석이며, 나중에는 이 주석 덕분에 코드를 파악하는 시간을 절약할 수 있다. 그 이후에 그 코드가 주석이 필요하지 않는 '명확한' 코드임을 알았다면 그 자리에서 주석을 삭제해야 한다.
항상 코드만으로 설명할 수 없는 것만 언급하라. 왜라는 질문에 코드로 답할 수 없는 것만 설명하기 위한 주석을 최소한으로 유지해야 한다.

728x90
Comments