테스트 주도 개발 실전 강의 교재¶
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. 할 일 목록 갱신¶
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.dollar 와 Money.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. 통과했지만 — 자식 클래스가 진짜 빈약¶
생성자 한 줄. 이것 말고는 부모와 차이 없음. 존재 가치 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. 퀴즈¶
- 자식
times본문이 같아진 이유? - Money 를 추상 → 구체로 바꾼 직접 이유?
- equals 의
getClass()→ currency 비교 변경이 의미하는 바? - 추상 클래스를 구체로 바꾸면 안 되는 경우는?
- 9·10·11장의 변환을 한 줄로 요약?
정답·해설¶
- 9장에서 currency 필드 도입 → 차이가 데이터로.
new Money(amount * m, currency)로 두 자식 모두 표현 가능. 자식 메서드 본문이 100% 동일해짐. - 자식의
times가 사라지면서 새 인스턴스 생성을 부모가 해야 함. 부모가 추상이면 인스턴스화 불가. 더 이상 추상일 이유 없음 (자식이 빈 껍데기). - 타입 (Java 클래스) 차이가 의미 없어지고 데이터 (currency) 가 도메인 의미를 운반. Java 타입 시스템 대신 도메인 데이터로 동일성 결정. 11장의 자식 제거 후엔 타입이 다 Money 라 currency 만 의미 있음.
- (1) 추상이 사용자에게 "직접 쓰지 마세요" 메시지, (2) 추상 메서드가 진짜 다양한 구현 을 강제 — 결제·알림 같은 정책 다형성. Money 는 둘 다 해당 안 됨 (자식 차이가 데이터로 환원).
- 타입으로 표현하던 차이를 데이터로 — 9장 필드 도입, 10장 메서드 통합, 11장 자식 제거. entity-refactoring 12.7 서브클래스 제거의 정형화된 흐름.
다음 장 예고 — 11장: 모든 악의 근원¶
자식 클래스 Dollar·Franc 자체를 완전히 제거. Money 한 클래스 + currency 필드로 단순화. Effective Java Item 18 (상속 대신 컴포지션) + 리팩터링 12.7 의 살아있는 사례.