콘텐츠로 이동

테스트 주도 개발 실전 강의 교재

10장 — 흥미로운 시간(times)

대상: Java/Spring 백엔드 입문~중급 수강생 형식: 할 일 → REFACTOR → 함정 → 체크리스트 → 퀴즈 전제 환경: Java 17+, JUnit 5


0. 이 장을 시작하기 전에

0.1 학습 목표

  • 자식 times본문이 사실상 동일 함을 확인하고 통합.
  • Money추상 → 구체 전환.
  • 단계마다 모든 테스트 초록 유지.

0.2 큰 그림

[ 9장 끝 ]                              [ 10장 ]
 자식 times 본문 동일 (currency 활용)    부모 times 하나로 통합
 Money 추상 클래스                      Money 구체 클래스 (instantiable)

0.3 현업에서 왜 중요한가

  • 자식의 메서드 본문이 동일 = 부모로 끌어올릴 신호 (entity-refactoring 12.1).
  • 추상→구체 전환은 신중 — 추상이 의도 명시였다면 그대로 두는 게 나을 수도.

1. 할 일 목록 갱신

[x] currency 필드
[ ] times 를 부모로                    ← 이번
[ ] 자식 클래스 제거                    ← 11장
[ ] $5 + 10 CHF = $10
[ ] hashCode()

2. REFACTOR — times 통합

단계 1: 자식 times 본문 같게 만들기

9장 끝의 자식 times:

// Dollar
public Money times(int multiplier) { return Money.dollar(amount * multiplier); }
// Franc
public Money times(int multiplier) { return Money.franc(amount * multiplier); }

Money.dollarMoney.franc 가 결국 new Dollar(amount) / new Franc(amount). 그런데 currency 필드를 부모가 알고 있으니, 직접 new Money(amount, currency) 가능 (단, Money 가 추상 아니어야).

단계 2: Money 를 추상 → 구체

public class Money {   // ← abstract 제거
    protected final int amount;
    protected final String currency;

    public Money(int amount, String currency) {   // public 으로
        this.amount = amount;
        this.currency = currency;
    }

    public Money times(int multiplier) {   // ← 부모로 끌어올림
        return new Money(amount * multiplier, currency);
    }

    // 정적 팩터리는 그대로
    public static Money dollar(int amount) { return new Money(amount, "USD"); }
    public static Money franc(int amount)  { return new Money(amount, "CHF"); }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Money m)) return false;
        return amount == m.amount && currency.equals(m.currency);   // ← getClass → currency 비교
    }
}

public class Dollar extends Money {
    public Dollar(int amount) { super(amount, "USD"); }
    // times 제거 (부모 사용)
}

public class Franc extends Money {
    public Franc(int amount) { super(amount, "CHF"); }
    // times 제거
}

→ 자식 클래스가 거의 빈 껍데기. 생성자 한 줄만.

단계 3: equals 도 통화 비교로

getClass() 비교는 자식 클래스를 위한 것이었지만, 이제 currency 필드가 있으니:

public boolean equals(Object o) {
    if (!(o instanceof Money m)) return false;
    return amount == m.amount && currency.equals(m.currency);
}

Franc(5).equals(Dollar(5)) 는 currency 가 달라 false. 7장의 의도 유지.

단계 4: 테스트 모두 통과

@Test void Dollar_곱셈() { ... }   // OK
@Test void Franc_곱셈() { ... }    // OK
@Test void 통화() { ... }          // OK
@Test void Franc_와_Dollar_는_같지_않다() { ... }   // OK

전부 초록.


3. 통과했지만 — 자식 클래스가 진짜 빈약

public class Dollar extends Money {
    public Dollar(int amount) { super(amount, "USD"); }
}

생성자 한 줄. 이것 말고는 부모와 차이 없음. 존재 가치 0 — 11장에서 제거.


4. 현업 예제 — 추상 → 구체 전환의 함정

사례: 추상이 의도 명시였던 경우

public abstract class PaymentProcessor {
    public abstract PaymentResult process(Order o);
}

// "PaymentProcessor 는 추상 — 직접 인스턴스화 X, 자식 구현 강제"

이 추상은 사용자에게 메시지: "이 클래스는 직접 쓰지 말고 자식 (CardProcessor 등) 구현해서 써". 추상을 제거하면 메시지 사라짐.

→ TDD 책의 Money 처럼 자식이 사실상 사라지는 경우만 추상 → 구체 전환 정당.


5. 함정 / 주의

  • 추상이 사용 의도를 명시하는 경우 (사용자에게 "직접 쓰지 마세요") 는 그대로 두는 게 나음.
  • 자식 차이가 사라진 후에만 추상 → 구체 전환. 차이가 있는 채로 전환하면 자식 메서드가 오버라이드 안 됨 위험.
  • equals 가 getClass → currency 로 바뀐 게 도메인 의미 변화 — 일관성 검증.

6. 체크리스트 (10장 완료 기준)

  • 자식 times 가 모두 제거됐는가
  • 부모 times 하나로 통합됐는가
  • Money 가 instantiable 인가
  • equals 가 currency 까지 비교하는가
  • 자식 클래스가 거의 빈 껍데기인가 (11장 제거 신호)

7. 퀴즈

  1. 자식 times 본문이 같아진 이유?
  2. Money 를 추상 → 구체로 바꾼 직접 이유?
  3. equals 의 getClass() → currency 비교 변경이 의미하는 바?
  4. 추상 클래스를 구체로 바꾸면 안 되는 경우는?
  5. 9·10·11장의 변환을 한 줄로 요약?

정답·해설

  1. 9장에서 currency 필드 도입 → 차이가 데이터로. new Money(amount * m, currency) 로 두 자식 모두 표현 가능. 자식 메서드 본문이 100% 동일해짐.
  2. 자식의 times 가 사라지면서 새 인스턴스 생성을 부모가 해야 함. 부모가 추상이면 인스턴스화 불가. 더 이상 추상일 이유 없음 (자식이 빈 껍데기).
  3. 타입 (Java 클래스) 차이가 의미 없어지고 데이터 (currency) 가 도메인 의미를 운반. Java 타입 시스템 대신 도메인 데이터로 동일성 결정. 11장의 자식 제거 후엔 타입이 다 Money 라 currency 만 의미 있음.
  4. (1) 추상이 사용자에게 "직접 쓰지 마세요" 메시지, (2) 추상 메서드가 진짜 다양한 구현 을 강제 — 결제·알림 같은 정책 다형성. Money 는 둘 다 해당 안 됨 (자식 차이가 데이터로 환원).
  5. 타입으로 표현하던 차이를 데이터로 — 9장 필드 도입, 10장 메서드 통합, 11장 자식 제거. entity-refactoring 12.7 서브클래스 제거의 정형화된 흐름.

다음 장 예고 — 11장: 모든 악의 근원

자식 클래스 Dollar·Franc 자체를 완전히 제거. Money 한 클래스 + currency 필드로 단순화. Effective Java Item 18 (상속 대신 컴포지션) + 리팩터링 12.7 의 살아있는 사례.