테스트 주도 개발 실전 강의 교재¶
6장 — 돌아온 '모두를 위한 평등'¶
대상: Java/Spring 백엔드 입문~중급 수강생 형식: 할 일 → RED → GREEN → REFACTOR → 함정 → 체크리스트 → 퀴즈 전제 환경: Java 17+, JUnit 5
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 슈퍼클래스
Money를 추출 해Dollar와Franc의 중복을 제거한다. equals를 슈퍼클래스로 끌어올리고, 숨겨진 문제 (통화 다른데 같다고 판정) 를 새 빨강으로 드러낸다.- 리팩터링 의 12.8 슈퍼클래스 추출 절차를 손에 익힌다.
- "하나의 추출이 새 문제를 드러낸다" — TDD 가 진화의 다음 단계를 자연스럽게 안내함을 본다.
0.2 큰 그림 — "추출 + 새 빨강"¶
[ 5장 끝 ] [ 6장: 추출 ] [ 6장 끝: 새 빨강 ]
Dollar / Franc 평행 코드 → Money 슈퍼클래스 추출 → equals 가 Money 비교
(equals 두 곳에 중복) (equals 부모로 끌어올림) → Franc(5) == Dollar(5) ❌
(7장에서 해결)
비유 — "두 가게의 공통 주방 만들기"
두 카페가 (Dollar·Franc) 각자 같은 음료 (
equals) 를 만들고 있었음. 공동 주방 (Money) 으로 합치면 작업 효율 ↑. 그런데 합친 주방에서 두 카페의 손님이 섞이는 새 문제 발생 (통화 무시 비교) — 7장에서 해결.
0.3 현업에서 왜 중요한가¶
- 슈퍼클래스 추출은 리팩터링 카탈로그에서 가장 자주 쓰는 기법 중 하나.
- 추출 후 새로 드러난 문제 (LSP·equals 대칭성) 는 OO 설계의 단골 함정.
- TDD 사이클이 "한 단계 → 새 문제 → 다음 단계" 로 안내하는 살아있는 사례.
1. 할 일 목록 갱신¶
[x] $5 * 2 = $10
[x] Dollar 부작용 제거
[x] equals()
[x] amount 를 private 으로
[x] CHF * 2 = 10 CHF
[ ] Dollar 와 Franc 중복 제거 ← 이번 (슈퍼클래스 추출)
[ ] 통화가 다르면 equals false ← 6장 끝의 새 빨강 → 7장
[ ] hashCode()
[ ] $5 + 10 CHF = $10
2. RED — 추출 전제 테스트¶
추출 전에, 추출 후 동작 보장 을 위한 테스트를 명시적으로 적어둡니다.
@Test
void Dollar_equals() {
assertEquals(new Dollar(5), new Dollar(5));
assertNotEquals(new Dollar(5), new Dollar(6));
}
@Test
void Franc_equals() {
assertEquals(new Franc(5), new Franc(5));
assertNotEquals(new Franc(5), new Franc(6));
}
→ 이미 통과. 안전망 확인.
3. GREEN/REFACTOR — 슈퍼클래스 추출¶
단계 1: 빈 Money 슈퍼클래스 도입¶
public class Money {
// 일단 비워둠
}
public class Dollar extends Money { ... } // 기존 그대로
public class Franc extends Money { ... } // 기존 그대로
→ 테스트 통과 (변경 없음). 컴파일도 OK.
단계 2: amount 필드 부모로¶
public class Money {
protected final int amount;
protected Money(int amount) { this.amount = amount; }
}
public class Dollar extends Money {
public Dollar(int amount) { super(amount); }
public Dollar times(int multiplier) { return new Dollar(amount * multiplier); }
@Override public boolean equals(Object o) {
if (!(o instanceof Dollar d)) return false;
return amount == d.amount;
}
}
public class Franc extends Money {
public Franc(int amount) { super(amount); }
public Franc times(int multiplier) { return new Franc(amount * multiplier); }
@Override public boolean equals(Object o) {
if (!(o instanceof Franc f)) return false;
return amount == f.amount;
}
}
→ 테스트 그대로 통과. 작은 한 걸음.
단계 3: equals 부모로 — 그러나 instanceof 를 Money 로¶
여기가 핵심 단계. equals 두 개의 본문이 사실상 동일 (타입 체크 빼고). 부모로 끌어올리면:
public class Money {
protected final int amount;
protected Money(int amount) { this.amount = amount; }
@Override
public boolean equals(Object o) {
if (!(o instanceof Money m)) return false;
return amount == m.amount; // ← 통화 무시
}
}
public class Dollar extends Money {
public Dollar(int amount) { super(amount); }
public Dollar times(int multiplier) { return new Dollar(amount * multiplier); }
// equals 제거
}
public class Franc extends Money {
public Franc(int amount) { super(amount); }
public Franc times(int multiplier) { return new Franc(amount * multiplier); }
// equals 제거
}
→ 테스트 모두 통과. 중복 제거 완료.
단계 4: 진짜 빨강 발견¶
현재 equals 가 Money 타입만 보고 amount 만 비교 → Franc(5) == Dollar(5) true. 도메인적으로 틀림 (5달러 ≠ 5프랑).
진짜 문제 발견. 추출이 가져온 새 통찰. 6장은 여기서 일단 멈추고, 빨강을 할 일 목록에 적습니다. 7장에서 해결.
4. 통과했지만 — 다음 장의 빚¶
추출은 끝났고, 새 빨강이 등장. 6장의 성과:
| 전 | 후 |
|---|---|
equals 가 Dollar·Franc 두 곳에 중복 |
Money.equals 한 곳 |
새 통화 추가 시 equals 도 또 복붙 |
새 통화 = extends Money 만 |
| 통화 무시 문제 잠재 | 통화 무시 문제 명시적 빨강 |
TDD 의 안내: 추출이 끝나면 다음에 풀 문제가 빨강으로 모습을 드러냄. 한 단계 → 새 문제 사이클.
5. 현업 예제 — Spring 에서의 슈퍼클래스 추출¶
사례: 결제 처리기 공통화¶
// Before — 중복
public class CardPaymentProcessor {
public PaymentResult process(Order o, PaymentInfo i) {
log("결제 시작: " + o.id());
// 카드 특수 로직
log("결제 종료");
}
}
public class VirtualAccountPaymentProcessor {
public PaymentResult process(Order o, PaymentInfo i) {
log("결제 시작: " + o.id()); // 중복
// 가상계좌 특수 로직
log("결제 종료"); // 중복
}
}
// After — 슈퍼클래스 추출
public abstract class PaymentProcessor {
public final PaymentResult process(Order o, PaymentInfo i) {
log("결제 시작: " + o.id());
PaymentResult result = doProcess(o, i);
log("결제 종료");
return result;
}
protected abstract PaymentResult doProcess(Order o, PaymentInfo i);
}
→ Template Method 패턴. 리팩터링 12.8 슈퍼클래스 추출 + concept-design-patterns Template Method.
6. 함정 / 주의¶
- 추출 시 한 단계씩. 빈 슈퍼클래스 → 필드 올리기 → 메서드 올리기. 한 번에 다 옮기면 실패 시 원인 추적 어려움.
equals슈퍼클래스 추출 = LSP 함정 가능성. 자식 타입 차이가 의미 있다면 (DollarvsFranc) 부모equals가 그 차이를 무시하면 위험.- 상속을 가볍게. Effective Java Item 18 — 상속보다 컴포지션. 5·6·7장은 상속을 다루지만 11장에서 자식 클래스 자체를 제거하는 방향으로 진화.
- 자동 IDE 리팩토링 도구 (IntelliJ
Pull Members Up) 가 안전. 수동 추출은 사고 위험.
7. 체크리스트 (6장 완료 기준)¶
- 추출 전 테스트가 모두 통과하는가 (안전망)
- 빈 슈퍼클래스 → 필드 → 메서드 순으로 단계별 추출했는가
- 매 단계마다 테스트가 초록인가
- 추출 후 새로 드러난 빨강 (통화 비교) 을 할 일 목록에 적었는가
- 슈퍼클래스가 도메인적으로 의미 있는 이름인가 (
Money)
8. 퀴즈¶
- 슈퍼클래스 추출의 절차를 한 줄로?
- 6장에서
equals를 부모로 끌어올린 직후 새 빨강이 등장한 이유? - 한 단계씩 추출하는 게 안전한 이유?
- 슈퍼클래스 추출이 entity-refactoring 의 어떤 기법?
- Template Method 패턴이 슈퍼클래스 추출과 어떻게 연결되나?
정답·해설¶
- 빈 슈퍼클래스 → 필드 올리기 → 메서드 올리기 — 각 단계마다 테스트 초록 유지. 한 번에 다 옮기지 않음.
equals가Money타입까지만 보고amount비교 — 자식 타입 (Dollar/Franc) 차이를 무시. 도메인적으로 5달러 ≠ 5프랑인데 같다고 판정. 7장에서 통화 필드 또는 클래스 비교 추가로 해결.- 실패 시 원인 추적. 한 단계 = 한 변경 = 실패 시 원인 명확. 여러 단계 동시 = 실패 시 어느 단계 책임인지 모름. TDD 의 핵심 리듬.
- 12.8 슈퍼클래스 추출 (Extract Superclass). 형제 클래스의 공통 부분을 새 부모로. 12.1 메서드 올리기·12.2 필드 올리기와 짝.
- 슈퍼클래스가 알고리즘 골격 을 정의 (process 메서드의 log 시작/종료), 자식이 단계 구현 (doProcess). 슈퍼클래스 추출의 정형화된 형태. 6장의
Money.equals도 유사 — 공통 비교 알고리즘을 부모가, 자식이 차이만.
다음 장 예고 — 7장: 사과와 오렌지¶
6장 끝의 빨강 (Franc(5) == Dollar(5)) 을 해결합니다. 통화가 다르면 다른 객체 — equals 에 클래스 비교를 추가. getClass() vs instanceof 의 트레이드오프와 LSP 문제까지 다룹니다.