테스트 주도 개발 실전 강의 교재¶
13장 — 진짜로 만들기¶
대상: Java/Spring 백엔드 입문~중급 수강생 형식: 할 일 → RED → GREEN → REFACTOR → 함정 → 체크리스트 → 퀴즈 전제 환경: Java 17+, JUnit 5
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 12장에서 만든
Bank.reduce골조를 진짜 동작 으로 채운다. Money.reduce가 환율 1:1 (같은 통화) 일 때부터 시작 — 가장 단순한 케이스.Sum.reduce가 augend·addend 각각 reduce 후 합산.- 삼각측량 으로 일반화 — 같은 통화 + 다른 통화 둘 다 통과.
0.2 큰 그림 — "reduce 는 환율 적용 + 합산"¶
[ Money.reduce ] [ Sum.reduce ]
amount / rate (환율 1 이면 그대로) augend.reduce + addend.reduce
(재귀 — 깊은 표현도 처리)
0.3 현업에서 왜 중요한가¶
- 같은 인터페이스 (Expression) + 다른 구현 (Money·Sum) 의 다형성 dispatch.
- 환율 정책이 외부 (Bank) 에 있어 도메인 객체 (Money) 가 환율을 모름 — 관심사 분리.
1. 할 일 목록 갱신¶
[x] $5 + $5 = $10 (같은 통화)
[ ] $5 + 10 CHF = $10 (환율 2:1) ← 이번 (Sum.reduce 본격)
[ ] Pair 클래스 (Bank 의 환율 키) ← 14장
[ ] hashCode()
2. RED — 같은 통화 reduce¶
@Test
void reduce_같은_통화() {
Bank bank = new Bank();
Money result = bank.reduce(Money.dollar(1), "USD");
assertEquals(Money.dollar(1), result); // 환율 1:1
}
→ 통과 (12장에서 이미 rate(from, to) 가 같으면 1 반환).
3. RED — 다른 통화 reduce¶
@Test
void reduce_다른_통화() {
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2); // 2 CHF = 1 USD
Money result = bank.reduce(Money.franc(2), "USD");
assertEquals(Money.dollar(1), result); // 2 / 2 = 1
}
→ 통과 (12장 코드로 충분).
4. RED — Sum reduce (핵심)¶
@Test
void Sum_reduce() {
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Expression sum = Money.dollar(5).plus(Money.franc(10));
Money result = bank.reduce(sum, "USD");
assertEquals(Money.dollar(10), result); // 5 + 10/2 = 10
}
→ 12장의 Sum.reduce 가 이미 처리 — 통과.
5. REFACTOR — 도메인 의미 명시¶
Money.reduce 의 의미 보강¶
public class Money implements Expression {
@Override
public Money reduce(Bank bank, String to) {
if (this.currency.equals(to)) return this; // 같은 통화면 자기 자신
int rate = bank.rate(currency, to);
return new Money(amount / rate, to);
}
}
→ 같은 통화면 환산 불필요. 의도 명시 + 성능 최적화.
Sum.reduce 의 의미 보강¶
public class Sum implements Expression {
@Override
public Money reduce(Bank bank, String to) {
int amount = augend.reduce(bank, to).amount() + addend.reduce(bank, to).amount();
return new Money(amount, to);
}
}
augend·addend 가 또 Sum 일 수 있어 재귀.
6. 통과한 시점에서의 인사이트¶
- Bank 가 도메인의 환율 정책 단일 책임 — 환율 추가·조회·환산 진입점.
- Money·Sum 이 모두 reduce 구현 — Bank 는 둘을 구분 안 함 (다형성).
- 표현이 깊어져도 (
Sum.augend = Sum) reduce 가 재귀로 처리.
OO 의 보상: 새 표현 추가 (예:
Times implements Expression) 시 Bank 코드 변경 0. OCP 충족.
7. 현업 예제 — 정책의 외부 분리¶
Spring @Value + 환경 설정¶
@Component
public class CurrencyConverter {
private final ExchangeRateProvider provider; // 외부에서 환율 가져오기
public Money convert(Money source, String to) {
BigDecimal rate = provider.rate(source.currency(), to);
return source.times(rate);
}
}
// 테스트 시 가짜 provider 주입
new CurrencyConverter(new FixedRateProvider(Map.of(
Pair.of("USD", "KRW"), new BigDecimal("1300")
)));
→ 환율 정책이 외부 (provider) 에 있고 도메인 (Money) 은 모름. 13장 Bank 와 같은 분리.
8. 함정 / 주의¶
amount / rate정수 나눗셈 — 책 예제는 정수라 OK, 실무는BigDecimal+RoundingMode신중.- 환율이 1 인 경우 (같은 통화) 처리 안 하면 환산 비용 낭비 — 위 REFACTOR 단계.
- Bank 가 환율을 메모리에만 보관 — 운영은 외부 API + 캐싱 + TTL 필요.
9. 체크리스트 (13장 완료 기준)¶
-
Money.reduce가 같은 통화 빠른 경로 처리 -
Sum.reduce가 augend·addend 재귀 reduce 후 합산 - Bank 가 환율 추가·조회·환산 진입점
- 모든 테스트 (같은 통화 + 다른 통화 + Sum) 초록
10. 퀴즈¶
- Money.reduce 가 환율 1 인 경우 빠른 경로를 두는 이유?
- Sum.reduce 가 재귀인 이유?
- Bank 의 단일 책임은 무엇인가?
- 새 표현 (Times) 추가 시 Bank 변경이 필요한가?
- 정수 나눗셈의 함정과 실무 처방?
정답·해설¶
- 의도 명시 + 성능. 같은 통화면 환산 불필요 → 객체 자기 자신 반환. 환율 조회·계산·새 객체 생성 모두 생략. 도메인 의미 ("같은 통화는 환산 X") 도 코드에 보임.
- 표현이 깊어질 수 있음. Sum.augend 가 또 Sum 일 가능성 (예:
(a + b) + c). 재귀로 트리 끝까지 reduce. 컴포지트 패턴의 표준 처리. - 환율 정책. 환율 보관·조회·환산 진입점. Money·Sum 같은 도메인 객체는 환율을 모름 — Bank 에 위임. 관심사 분리.
- 변경 0. Times 가 Expression 구현 + reduce 만 정의하면 Bank 의
source.reduce(this, to)가 다형성 dispatch. OCP 충족. 새 표현 = 새 클래스 1개. - 정수 손실 —
5/2 = 2(소수점 버림). 실무는BigDecimal+RoundingMode.HALF_UP등 명시. 책 예제는 학습 단순화. Effective Java Item 60 (BigDecimal 권장) 직결.
다음 장 예고 — 14장: 바꾸기¶
Bank 가 환율을 Map<Pair, Integer> 로 보관. Pair 클래스 가 필요 (from·to 쌍). equals·hashCode 가 HashMap 키로 작동하게. Effective Java Item 10·11 의 살아있는 사례.