Dollar와 Franc은 이제 거의 아무 일도 하지 않는다. 남은 차이는 times가 각자 다른 팩터리를 부른다는 것뿐이다. 이 장의 목표는 그 마지막 차이를 없애 times를 Money로 끌어올리는 것이다. 그러면 다음 장에서 서브클래스를 통째로 지울 수 있다.

문제는 순서다. times를 위로 올리려면 두 구현이 완전히 같아야 하고, 같아지려면 new Money(...)를 반환해야 하며, 그러려면 Money가 추상 클래스가 아니어야 한다. 그런데 Money를 구상화하면 equalsgetClass() 비교가 발목을 잡는다. 그러니 equals부터 손본다.

사이클: 통화 비교 → 구상화 → 끌어올리기

Refactoring Step 출발점 — 9장이 남긴 계약 상태
1/3
abstract class Money {    protected int amount;    protected String currency;    Money(int amount, String currency) { this.amount = amount; this.currency = currency; }    static Money dollar(int amount) { return new Dollar(amount, "USD"); }    static Money franc(int amount) { return new Franc(amount, "CHF"); }    abstract Money times(int multiplier);    public boolean equals(Object object) {        Money money = (Money) object;        return amount == money.amount && getClass().equals(money.getClass());    }}class Dollar extends Money {    Dollar(int amount, String currency) { super(amount, currency); }    Money times(int multiplier) { return Money.dollar(amount * multiplier); }}class Franc extends Money {    Franc(int amount, String currency) { super(amount, currency); }    Money times(int multiplier) { return Money.franc(amount * multiplier); }}// 클래스로 같음을 판단하고, times는 서브클래스마다 다른 팩터리를 부른다.

equals를 클래스 비교에서 통화 비교로 바꾼 것이 이 장의 핵심 수술이다. 이 한 줄 덕분에 new Money(10, "USD")new Dollar(10, "USD")가 같아져, 다음 장에서 서브클래스를 지워도 테스트가 깨지지 않는다. 표현 방식(어떤 클래스인가)이 아니라 값(어느 통화 얼마인가)으로 동치를 정의한 것 — 값 객체의 본질에 다가섰다.

판단 기준: 리팩터링의 순서는 컴파일 의존성이 정한다. “equals → 구상화 → 끌어올리기”는 임의의 순서가 아니라, 각 단계가 다음 단계의 전제 조건을 만들어 주는 사슬이다. 함정: getClass() 비교를 지우면서 “혹시 필요할까” 남겨두면 두 기준이 공존해 혼란이 생긴다. 대체하는 리팩터링은 옛것을 확실히 지워야 완결된다.

이 장이 남긴 할 일 목록

  • 10
  • equals() — currency 비교로 완성
  • 공통 times() 를 Money로 — 이제 완료, 서브클래스 제거만 남음
  • Dollar/Franc 서브클래스 제거 → 11장
  • 10 (환율 2:1)

다음장으로 11장