1장 끝에 남긴 첫 번째 할 일은 부작용이다. five.times(2)를 부르면 five 자체의 amount가 바뀐다. 한 번 곱한 five를 다시 쓰면 엉뚱한 값이 나온다. 이건 화폐라는 개념에 어울리지 않는다. 5여야 한다. 벡은 이 문제를 값 객체(Value Object) 패턴으로 푼다.

부작용이 드러나는 테스트

값 객체의 규칙은 단순하다. 객체의 인스턴스 변수는 생성될 때 한 번 정해지고 그 뒤로 절대 바뀌지 않는다. 곱셈처럼 새 값이 필요한 연산은 원본을 건드리지 않고 새 객체를 반환한다. 테스트를 이 계약에 맞게 다시 쓰는 것부터 시작한다.

Refactoring Step 빨강 — 반환값을 기대하도록 테스트를 고친다
1/2
public class MoneyTest {    @Test    public void testMultiplication() {        Dollar five = new Dollar(5);        Dollar product = five.times(2);        assertEquals(10, product.amount);        product = five.times(3);        assertEquals(15, product.amount);    }}class Dollar {    int amount;    Dollar(int amount) {        this.amount = amount;    }    void times(int multiplier) {          // 아직 void — 컴파일 실패        amount = amount * multiplier;    }}

product = five.times(3)를 같은 five로 두 번 곱해도 15가 나온다. five는 여전히 $5다. 부작용이 사라졌다. 반환값을 받는 형태로 테스트를 먼저 고쳤기 때문에, 구현이 계약을 어기면 컴파일 단계에서 곧바로 막힌다.

판단 기준: 화폐, 좌표, 날짜처럼 “같은 값이면 같은 것”으로 다루고 싶은 개념은 값 객체로 만든다. 함정: 값 객체의 세터를 열어두면 곱셈 결과가 원본을 오염시켜 공유된 인스턴스가 조용히 깨진다. 인스턴스 변수는 생성자에서만 정하고 잠근다.

이 장이 남긴 할 일 목록

  • 10
  • Dollar 부작용?
  • amount를 public으로 노출 중
  • equals() — Dollar 두 개를 값으로 비교하고 싶다 → 3장
  • 10 (환율 2:1)

값 객체가 되고 나니 다음 질문이 자연스럽게 따라온다. new Dollar(10)five.times(2)는 같은 값인데, 지금은 ==로 비교되지 않는다. 값이 같으면 같다고 말할 수 있어야 한다.

다음장으로 3장