열여섯 장에 걸쳐 다중 통화 Money를 처음부터 만들었다. 이 장에는 새 코드가 없다. 대신 우리가 어떻게 여기까지 왔는지를 되돌아본다 — TDD가 실제로 설계를 어떻게 이끌었는가.

최종 설계 지도

classDiagram
    class Expression {
        <<interface>>
        +reduce(Bank, String) Money
        +plus(Expression) Expression
        +times(int) Expression
    }
    class Money {
        -int amount
        -String currency
        +reduce(Bank, String) Money
        +plus(Expression) Expression
        +times(int) Expression
    }
    class Sum {
        +Expression augend
        +Expression addend
        +reduce(Bank, String) Money
        +plus(Expression) Expression
        +times(int) Expression
    }
    class Bank {
        -Hashtable rates
        +reduce(Expression, String) Money
        +addRate(String, String, int)
        +rate(String, String) int
    }
    class Pair {
        -String from
        -String to
    }
    Expression <|.. Money
    Expression <|.. Sum
    Sum o-- Expression : augend, addend
    Bank ..> Expression : reduce
    Bank o-- Pair : rates key

시작은 int를 반환하던 Dollar.times 하나였다. 끝은 Money와 Sum이 대칭으로 구현하는 Expression 추상화다. 이 구조를 미리 설계해 그린 것이 아니라, 테스트가 하나씩 요구할 때마다 한 조각씩 자라난 결과라는 점이 핵심이다.

메트릭이 말해주는 것

숫자로 보면 리듬이 드러난다. 매 장은 대체로 테스트 하나를 추가하고, 억지로 통과시킨 뒤, 중복을 제거하는 한 사이클이었다. 테스트 코드와 제품 코드의 양은 엇비슷하게 자랐다 — TDD에서는 이게 정상이다. 테스트가 제품 코드의 절반도 안 된다면 충분히 촘촘하지 않다는 신호이고, 몇 배로 많다면 테스트끼리 중복됐을 가능성이 크다.

주목할 것은 되돌린 걸음이 거의 없다는 사실이다. 매 걸음 끝이 초록이었기에, 방향이 틀렸다면 바로 다음 걸음에서 알아챘다. 디버거를 붙들고 몇 시간을 헤매는 일이 없었던 이유는 실패의 범위가 언제나 “방금 바꾼 몇 줄”로 좁았기 때문이다.

걸음의 크기를 어떻게 정했나

Money 예제는 세 가지 통과 전략을 오갔다. 가짜 구현(상수를 박고 시작 — 1장의 amount = 10, 12장의 reduceMoney.dollar(10)), 삼각측량(예제 둘로 일반화를 강제), 명백한 구현(답이 뻔하면 곧장 작성 — 16장의 times). 어느 하나가 옳은 게 아니라, 확신의 크기에 따라 걸음의 보폭을 바꾼 것이다.

확신이 서면 성큼 걷고, 불안하면 잘게 쪼갠다. 11장에서 서브클래스를 지울 때 두 팩터리를 한 번에 바꾸지 않고 하나씩 초록을 확인한 것이 후자다. TDD의 기술은 테스트를 쓰는 것 자체가 아니라, 지금 내 확신에 맞는 보폭을 고르는 감각이다.

할 일 목록이 설계를 대신했다

1장부터 17장까지, 설계 문서는 없었다. 대신 할 일 목록이 그 자리를 채웠다. 사이클 도중 발견한 문제(amount 노출, Pair.hashCode, 서브클래스 중복)는 그 자리에서 고치지 않고 목록에 적어 미뤘다. 덕분에 지금 걸음과 다음 걸음이 섞이지 않았다. 목록이 비어갈수록 설계가 드러났다.

남은 부채도 정직하게 남긴다. Pair.hashCode는 여전히 return 0이다. 성능이 문제가 되는 날 프로파일러가 그곳을 가리킬 것이고, 그때 고치면 된다. 완벽을 미루는 것은 게으름이 아니라 우선순위다.

판단 기준: 회고는 “무엇을 만들었나”가 아니라 “어떤 리듬으로 만들었나”를 본다. 되돌린 걸음의 수, 초록에서 초록까지의 간격, 미뤄둔 부채의 목록 — 이것이 다음 프로젝트의 보폭을 정하는 자료다. 함정: 예제가 끝났다고 목록의 남은 항목을 지우고 잊는 것. 부채는 갚지 않아도 기록은 남겨야 다음 사람이 안다.

1부가 남긴 것

  • 10 — 책의 첫 목표 달성
  • Expression 추상화 완성
  • Pair.hashCode 개선 (의도적으로 남긴 부채)
  • 다음: 테스트 프레임워크 자체를 TDD로 — 부트스트래핑

다음장으로 18장