Chapter 2

화폐 예제 완성

  • 2.1 공통 상위 클래스와 점진적 일반화
  • 2.2 동치성 비교의 빈틈
  • 2.3 팩토리 메서드로 하위 클래스 숨기기
  • 2.4 통화 개념 도입
  • 2.5 times() 일반화와 하위 클래스 제거

복붙으로 만든 Dollar와 Franc, 이 중복을 어떻게 없앨까? 한 번에 확 바꾸는 게 아니라 작은 단계를 밟아가면서 점진적으로 합치는 거야. 이게 TDD 리팩토링의 핵심이지.

공통 상위 클래스 Money를 먼저 만들어. Dollar와 Franc이 Money를 상속받게 하고, amount 필드를 Money로 올리고, equals() 메서드도 Money로 올리지. 중요한 건 한 번에 다 바꾸지 않는다는 거야. amount를 올리고 -- 테스트 돌리고 -- 초록 막대. equals()를 올리고 -- 테스트 돌리고 -- 초록 막대. 작은 단계를 밟아라가 이 과정의 메시지야. 한 번에 크게 바꾸면 뭐가 깨졌는지 찾기 어렵거든. 한 번에 조금씩 바꾸면 문제가 생겨도 바로 전 단계로 돌아가면 돼.

근데 equals()를 Money 클래스로 올렸더니 문제가 생겨. new Dollar(5).equals(new Franc(5))가 true를 반환하는 거야. 5달러와 5프랑은 같은 돈이 아닌데! amount만 비교하고 있었기 때문이지. 클래스가 같은지도 비교해야 해. getClass()를 사용해서 두 객체의 클래스가 같을 때만 동등하다고 판단하도록 수정하지. 영어 관용구로 "comparing apples and oranges(전혀 다른 걸 비교하다)"라는 표현이 있는데, Dollar와 Franc은 사과와 오렌지처럼 다른 종류니까 같다고 하면 안 되는 거야. 리팩토링이 새로운 버그를 만들 수 있어. 그래서 테스트가 있어야 하고, 리팩토링할 때마다 돌려야 하지.

다음 단계로 테스트 코드에 new Dollar(5)가 직접 등장하면, 나중에 Dollar 클래스를 없앨 수 없잖아. 그래서 팩토리 메서드를 도입해. Money.dollar(5), Money.franc(5)를 통해 객체를 생성하면 반환 타입은 Money야. 클라이언트는 Dollar인지 Franc인지 신경 안 써도 돼. 이게 나중에 하위 클래스를 제거할 수 있는 발판이 되지. TDD의 작은 단계가 점점 큰 설계 변화를 가능하게 하는 걸 보여주는 대목이야.

이제 **통화(currency)**라는 개념을 명시적으로 도입해. 지금까지 Dollar와 Franc을 클래스로 구분했는데, 이걸 "USD", "CHF" 같은 통화 문자열로 구분하는 방향으로 바꾸기 시작하지. currency() 메서드를 만들어서 각 Money 객체가 자신의 통화를 알려줄 수 있게 해. 이 통화 문자열을 생성자에서 받아서 저장하도록 바꾸면, Dollar와 Franc의 차이가 점점 줄어들어. 클래스 대신 데이터로 구분하는 거야. 타입으로 구분하던 것을 문자열로 바꾸면서, 하위 클래스를 제거할 준비를 하지.

Dollar.times()와 Franc.times()의 유일한 차이는 생성하는 객체의 타입뿐이야. 팩토리 메서드를 사용하도록 바꾸면 두 메서드가 완전히 동일해져. 똑같아졌으니 Money 클래스로 올릴 수 있지. 이 과정이 쉽지만은 않아서, 켄트 벡도 중간에 **"실험을 해보고, 안 되면 되돌리자"**는 태도를 보여. TDD는 항상 순탄하게 진행되는 게 아니야. 막히면 한 발 물러서서 다른 접근을 시도하는 것도 TDD의 일부거든.

times()도 올리고, equals()도 올리고, currency도 올리고 나니까, Dollar와 Franc 클래스에 남은 코드가 거의 없어. 생성자뿐이지. 이 정도면 하위 클래스가 존재할 이유가 없어. 팩토리 메서드에서 new Dollar(5, "USD") 대신 new Money(5, "USD")를 반환하게 바꿔. 테스트를 돌려봐. 초록 막대. Dollar와 Franc 클래스를 삭제하지. 이게 1장에서 복사해서 만든 두 클래스가 점진적 리팩토링을 거쳐 결국 하나의 Money 클래스로 합쳐진 여정의 끝이야.


정리

2장 읽고 기억할 거 세 가지:

  1. 중복 제거는 작은 단계로 해. 공통 상위 클래스를 만들고, 필드와 메서드를 하나씩 올리면서 매번 테스트를 돌리지. 리팩토링이 버그를 만들 수 있으니까 테스트가 안전망이야
  2. 팩토리 메서드와 통화 문자열로 하위 클래스를 점진적으로 불필요하게 만들어. 클래스 대신 데이터로 구분하면 구조가 단순해지거든
  3. 막히면 되돌려. TDD에서는 실험하고, 안 되면 전 단계로 돌아가는 게 자연스러워. 테스트가 있으니까 안전하게 되돌릴 수 있지