6장에서 equals를 Money로 끌어올린 대가로 버그가 생겼다. Dollar(5)와 Franc(5)가 amount만 보고 같다고 나온다. 사과와 오렌지를 같다고 우기는 셈이다. 값 객체의 동치성은 값뿐 아니라 타입까지 같아야 한다. 먼저 이 버그를 드러내는 테스트를 쓰고, 그다음 고친다.

버그를 테스트로 고정한 뒤 고친다

머릿속으로만 “이건 버그야”라고 생각하는 대신, 실패하는 테스트로 못 박는다. assertFalse(new Franc(5).equals(new Dollar(5)))가 지금은 빨강이다. 이 테스트가 초록이 되게 만들면 버그는 사라지고, 다시는 조용히 돌아오지 못한다.

Refactoring Step 빨강 — 통화가 다르면 달라야 한다는 테스트
1/2
@Testpublic void testEquality() {    assertTrue(new Dollar(5).equals(new Dollar(5)));    assertFalse(new Dollar(5).equals(new Dollar(6)));    assertTrue(new Franc(5).equals(new Franc(5)));    assertFalse(new Franc(5).equals(new Dollar(5)));   // 지금은 실패 — 5 == 5라 true}class Money {    protected int amount;    public boolean equals(Object object) {        Money money = (Money) object;        return amount == money.amount;                 // 타입을 무시한다    }}

getClass().equals(money.getClass())는 두 객체의 실제 런타임 클래스를 비교한다. Franc(5)와 Dollar(5)는 amount가 같아도 클래스가 달라 이제 다르다고 판정된다. 사과와 오렌지가 분리됐다.

판단 기준: 값 객체의 동치성은 “값이 같고 타입도 같을 때”다. getClass 비교는 이 정의를 코드로 옮긴 것이다. 함정: getClass 비교는 상속 계층에서 서브클래스를 다르게 취급하는 문제가 있어 도메인 클래스에는 보통 부적절하다. 다만 벡은 여기서 통화를 클래스로 표현했기 때문에 자연스럽다. 사실 통화를 클래스가 아니라 데이터(문자열)로 표현하면 이 비교 자체가 필요 없어진다 — 그 방향이 9장의 복선이다.

이 장이 남긴 할 일 목록

  • $5 == 5CHF 인가?
  • Dollar/Franc을 굳이 클래스로 나눌 필요가 있나 → 통화를 데이터로
  • times 중복 — Dollar와 Franc에 거의 같은 times가 있다 → 8장
  • equals(null), equals(다른 객체) 처리
  • 10 (환율 2:1)

다음장으로 8장