마지막 장은 책 전체의 압축이다. 마틴 파울러의 코드 냄새 개념을 이어받아, 저자가 코드를 검토할 때 실제로 쓰는 냄새와 휴리스틱을 범주별 목록으로 정리했다. 전부 외울 필요는 없다 — 리뷰할 때 옆에 두는 점검표로 쓰는 게 맞다. 이 장의 가치는 완전성이 아니라 “가치 체계”에 있다. 규칙 암기가 아니라 냄새를 맡는 감각을 기르는 것이 목적이다.
원서는 각 항목에 접두어를 붙여 참조하기 쉽게 했다. 주석 C, 환경 E, 함수 F, 일반 G, 이름 N, 테스트 T. 이 노트도 그 지도를 그대로 따른다.
flowchart TD ROOT["냄새와 휴리스틱"] ROOT --> C["C 주석<br/>부정확·죽은 주석"] ROOT --> E["E 환경<br/>빌드·테스트 절차"] ROOT --> F["F 함수<br/>인수·부수효과"] ROOT --> G["G 일반<br/>가장 큰 범주"] ROOT --> N["N 이름<br/>서술성·명명법"] ROOT --> T["T 테스트<br/>커버리지·경계"] G --> G1["중복 (G5)"] G --> G2["추상화 수준 (G6·G7)"] G --> G3["죽은 코드 (G9)"] G --> G4["매직 넘버 (G25)"]
C — 주석
- C1 부적절한 정보. 변경 이력, 작성자, 최종 수정일처럼 소스 관리 도구가 대신 기억할 정보는 주석에 두지 않는다.
- C2 쓸모없는 주석. 당연한 내용을 반복하는 주석은 잡음이다. 읽는 눈이 미끄러진다.
- C3 중복된 주석. 코드가 이미 말하는 것을 주석이 다시 말하면 유지 대상만 둘로 늘어난다.
- C5 주석 처리된 코드. 남겨진 죽은 코드는 아무도 못 지운다. “누군가 필요할지 몰라서” — 그 누군가는 오지 않는다. 그냥 지운다.
// C3 — 코드가 곧 주석인데 주석이 중복됨
i++; // i를 1 증가시킨다
// C5 — 주석 처리된 죽은 코드, 지워야 한다
// InputStream is = new FileInputStream(f);
// response = new Response(is);판단 기준: “이 주석이 사라지면 정보가 정말 소실되는가?” 소실되지 않으면(git이 답해 주거나 코드가 이미 말하면) 지운다. 함정: 좋은 주석과 나쁜 주석을 뭉뚱그려 “주석은 다 나쁘다”로 오해하면 안 된다. 의도·경고·법적 고지 같은 주석은 남긴다.
E — 환경
- E1 빌드는 한 단계로. 저장소를 받아 명령 하나로 빌드가 끝나야 한다. 여러 스크립트와 수동 설정을 요구하면 그 자체가 냄새다.
- E2 테스트도 한 단계로. 버튼 하나, 명령 하나로 전체 테스트가 돌아야 한다. 돌리기 번거로운 테스트는 결국 안 돌리는 테스트가 된다.
# E1·E2 — 이상적인 형태
git clone <repo> && cd <repo>
./build.sh # 의존성 설치·컴파일·패키징까지 한 번에
./test.sh # 전체 테스트 한 번에판단 기준: 신규 입사자가 반나절 안에 빌드와 테스트를 돌릴 수 있는가로 가늠한다. 함정: “우리 팀은 다 아니까 괜찮다”는 착각이다. 절차가 사람 머릿속에만 있으면 그 사람이 병목이자 단일 장애점이 된다.
F — 함수
- F1 너무 많은 인수. 인수는 적을수록 좋다. 0개가 가장 좋고, 셋을 넘으면 의심 대상이다.
- F2 출력 인수. 인수로 결과를 되돌리는 건 직관을 배반한다. 상태 변경은 객체 자신에게 시킨다.
- F3 플래그 인수. boolean 인수는 함수가 두 가지 일을 한다는 자백이다. 함수를 둘로 나눈다.
- F4 죽은 함수. 아무도 호출하지 않는 함수는 지운다.
// F2·F3 — 출력 인수 + 플래그 인수
public void process(Report r, boolean isDraft) { // 나쁨
appendFooter(r); // r을 수정해 되돌림
}
// 개선 — 상태 변경은 객체에게, 플래그는 분리
public class Report {
public void appendFooter() { ... }
}
report.appendDraftFooter(); // 초안용
report.appendFinalFooter(); // 최종용판단 기준: 인수 목록에 boolean이 보이면 거의 항상 함수를 나눌 신호다. 함정: 출력 인수를 없애려 무리하게 반환 타입을 부풀리면(여러 값을 배열/맵으로 반환) 오히려 응집도가 떨어진다. 그럴 땐 값 객체를 하나 만든다.
G — 일반 (가장 자주 걸리는 범주)
가장 크고 자주 걸리는 범주다. 리뷰 지적의 대부분이 여기에 이름을 두고 있다.
- G5 중복. 어떤 형태든(복붙, 반복되는 switch, 유사 알고리즘) 추상화로 승격할 기회를 놓친 것이다. DRY 원칙의 반대말.
- G6 잘못된 추상화 수준. 고차원 개념과 저차원 세부사항을 한 곳에 섞지 않는다.
- G7 기초 클래스가 파생 클래스에 의존. 상위가 하위를 알면 의존 방향이 거꾸로 선 것이다(16장 SerialDate 사례).
- G9 죽은 코드. 실행되지 않는 분기, 도달 불가능한 코드는 유지비만 낸다.
- G23 다형성을 쓰라. 타입에 따라 갈리는 switch가 여러 곳에 반복되면 다형성으로 옮긴다.
- G25 매직 넘버. 의미를 못 말하는 리터럴은 이름 있는 상수 뒤로 숨긴다.
- G28 조건을 캡슐화하라. 복잡한 불리언 식은 의도를 말하는 함수로 뽑는다.
// G5 중복 + G25 매직 넘버 + G28 캡슐화 안 됨
if (employee.flags == 0xE && employee.age > 65) grantFullBenefits();
if (employee.flags == 0xE && employee.age > 65) sendRetirementNotice();
// 개선 — 조건 캡슐화 + 이름 있는 상수로 중복·매직 넘버 동시 해소
private static final int RETIREMENT_AGE = 65;
private boolean isEligibleForFullBenefits(Employee e) {
return e.flags == HOURLY_FLAG && e.age > RETIREMENT_AGE;
}
if (isEligibleForFullBenefits(employee)) grantFullBenefits();
if (isEligibleForFullBenefits(employee)) sendRetirementNotice();판단 기준: 같은 조건식이 두 번째로 등장하는 순간이 추출 시점이다. 세 번을 기다리지 않아도 된다. 함정: 매직 넘버라고 0이나 1까지 무조건 상수화하면 잡음이다. 문맥에서 이미 자명한 리터럴은 그대로 둔다 — 상수화는 “숫자가 의미를 못 말할 때만”.
N — 이름
- N1 서술적인 이름. 이름이 의도를 설명하면 코드의 절반은 문서가 된다.
- N2 추상화 수준에 맞는 이름. 구현 방식이 아니라 무엇을 하는지를 이름에 담는다.
- N4 명확한 명명법. 표준 패턴 이름 등 널리 통하는 어휘를 활용한다.
- N7 부수 효과를 이름에 드러내라. 없으면 만들어 반환하는 메서드를
getX()로 부르면 거짓말이다.
// N7 — 이름은 조회처럼 생겼지만 실제로는 생성이라는 부수 효과가 있다
public ObjectOutputStream getOos() throws IOException { // 나쁨
if (oos == null) oos = new ObjectOutputStream(...); // 없으면 만든다
return oos;
}
// 개선 — 이름이 부수 효과를 정직하게 말한다
public ObjectOutputStream createOrReturnOos() throws IOException { ... }판단 기준: 이름과 실제 동작 사이에 놀라움이 있으면 새는 이름이다. 특히 조회를 가장한 생성·수정이 흔한 함정이다. 함정: 지나치게 긴 이름을 두려워해 축약하면(calcTot) 오히려 해독 비용이 든다. 길어도 정확한 이름이 짧고 모호한 이름을 이긴다.
T — 테스트
- T1 불충분한 테스트. 잠재적으로 깨질 수 있는 모든 조건을 테스트한다. 통과하는 것만이 아니라 실패해야 하는 것도.
- T2 커버리지 도구를 써라. 어느 줄이 안 밟혔는지는 눈이 아니라 도구가 안다.
- T3 사소한 테스트를 건너뛰지 마라. 사소해 보여도 문서로서의 가치가 크다.
- T5 경계 조건을 테스트하라. 버그는 서로 모이는 경향이 있어, 경계와 버그 주변을 파고들면 이웃 버그가 딸려 나온다.
- T9 느린 테스트를 방치 마라. 느린 테스트는 결국 안 돌리는 테스트가 된다.
// T5 — 경계와 예외 경로를 함께 찌른다
@Test void handlesEmptyAndBoundary() {
assertEquals(0, sum(new int[]{})); // 빈 입력
assertEquals(Integer.MAX_VALUE, sum(...)); // 오버플로 경계
assertThrows(NullPointerException.class,
() -> sum(null)); // 실패해야 하는 경로
}판단 기준: “이 함수가 틀린다면 어디서 틀릴까”를 상상하고 그 지점을 테스트한다. 대개 경계값·빈 입력·널·오버플로다. 함정: 커버리지 100%가 정확성 100%는 아니다. 모든 줄을 밟아도 검증(assert)이 약하면 버그를 못 잡는다. 커버리지는 필요조건이지 충분조건이 아니다.
정리
- 이 목록의 가치는 완전성이 아니라 “가치 체계”다. 규칙 암기가 아니라 냄새를 맡는 감각을 기르는 것이 목적이다.
- 접두어(C·E·F·G·N·T)는 리뷰에서 근거를 빠르게 가리키는 좌표다. 지적할 때 “이건 G5(중복)“라고 이름을 붙이면 논의가 감정이 아닌 원칙 위에서 진행된다.
- 대부분의 지적은 여기 어딘가에 이미 이름이 있다. 리뷰 근거가 궁하면 이 장으로 돌아온다.
- 클린 코드 한 권의 결론: 깨끗한 코드는 규칙의 산물이 아니라, 작은 규율을 반복하는 장인 정신의 산물이다.
다음장으로 후기