5장에서 저지른 죄를 회개할 차례다. Dollar와 Franc은 amount 필드, 생성자, equals가 사실상 같다. 공통 상위 클래스 Money를 만들고, 중복된 코드를 한 걸음씩 위로 끌어올린다(pull up). 큰 리팩터링을 한 번에 하지 않고, 매 걸음마다 테스트를 돌려 초록을 유지하는 것이 요령이다.

작은 걸음으로 공통 부모를 뽑는다

먼저 amount 필드부터 Money로 올린다. 그다음 equals를 올린다. equals 안의 캐스팅 타입도 Dollar/Franc에서 Money로 바꿔야 위로 옮길 수 있다. 한 번에 하나씩만 옮기고, 옮길 때마다 테스트로 안전을 확인한다.

Refactoring Step amount를 Money로 끌어올린다
1/3
class Money {    protected int amount;                 // private → protected, 자식이 물려받는다}class Dollar extends Money {    Dollar(int amount) {        this.amount = amount;    }    Dollar times(int multiplier) {        return new Dollar(amount * multiplier);    }    public boolean equals(Object object) {        Dollar dollar = (Dollar) object;  // 아직 Dollar로 캐스팅        return amount == dollar.amount;    }}

세 걸음 모두 테스트는 계속 초록이다. equals를 곧바로 Money로 옮기지 않고, 먼저 캐스팅 타입을 Money로 넓혀 “위로 옮겨도 안전한 상태”를 만든 다음 옮겼다. Franc의 equals도 동일한 절차로 지우면, 동치성 로직이 Money 한 곳에만 남는다.

판단 기준: 리팩터링은 테스트가 초록인 상태를 한순간도 벗어나지 않도록 잘게 쪼갠다. “캐스팅 넓히기 → 끌어올리기 → 자식에서 삭제”처럼 각 걸음이 독립적으로 안전해야 한다. 함정: equals를 Money로 올리면 Dollar와 Franc이 값만 같으면 서로 같다고 판정된다. $5 == 5CHF이 참이 되어 버린다. 명백한 버그지만, 지금은 목록에 적고 다음 장에서 테스트로 잡는다.

이 장이 남긴 할 일 목록

  • 공통 equals
  • $5 == 5CHF 인가? — Dollar와 Franc이 같다고 나온다 → 7장
  • times 중복 (아직 Dollar/Franc에 따로 있다)
  • equals(null), equals(다른 객체) 처리
  • 10 (환율 2:1)

다음장으로 7장