테스트 주도 개발 실전 강의 교재¶
5장 — 솔직히 말하자면¶
대상: Java/Spring 백엔드 입문~중급 수강생 형식: 할 일 → RED → GREEN → REFACTOR → 함정 → 체크리스트 → 퀴즈 전제 환경: Java 17+, JUnit 5
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 두 번째 통화
Franc을 도입하고,Dollar코드를 통째로 복붙 해서라도 가장 빠르게 초록을 만든다. - "복붙은 죄가 아니라 신호" — 두 클래스의 닮음이 명확해진 후에 좋은 추상화를 발견할 수 있음을 손에 익힌다.
- 의도된 중복 (intentional duplication) 을 할 일 목록에 적어 다음 사이클(6장)에서 정리.
- 리팩터링 의 "3의 법칙" 과 Clean Code 의 보이스카우트 규칙이 어떻게 TDD 사이클 안에서 연결되는지 본다.
0.2 큰 그림 — "패턴은 두 번째에 보인다"¶
[ 처음 ] [ 두 번째 ] [ 세 번째 ]
한 가지 사례만 있음 → 추상화 미루기 비슷한 사례 등장 → 닮음 발견 같은 닮음 또 등장 → 추출
(Dollar) (Dollar + Franc — 5장) (6장 슈퍼클래스로)
비유 — "원피스 vs 정장 라인"
디자이너가 첫 옷 (원피스) 한 벌을 만들 때는 "원피스" 라는 카테고리를 의식 안 함. 두 번째 디자인 (정장) 이 나오고 나서야 "이 둘은 모두 '상의'" 라는 공통점이 보임. 처음부터 "상의" 라는 카테고리부터 만들면 잘못된 추상화 위험.
0.3 현업에서 왜 중요한가¶
- 신입이 가장 자주 하는 실수 — 처음부터 인터페이스·슈퍼클래스·제네릭 도입. 사용 사례 1개로는 잘못된 추상화.
- 시니어가 가장 자주 미루는 것 — 사용 사례 3개 모인 후의 정리. 그 사이 코드는 부패.
- TDD 가 두 시기를 모두 짚어줌 — 5장에서 복붙, 6장에서 추출.
1. 할 일 목록 갱신¶
[x] $5 * 2 = $10
[x] Dollar 부작용 제거
[x] equals()
[x] amount 를 private 으로
[ ] hashCode()
[ ] $5 + 10 CHF = $10
[ ] CHF * 2 = 10 CHF ← 이번 (Franc 추가)
[ ] Dollar 와 Franc 중복 제거 ← 6장 빚
통화 추가 = 하나, 중복 제거 = 다른 사이클. 두 모자 분리.
2. RED — Franc 첫 테스트¶
비유 — "두 번째 손님, 같은 메뉴 다른 음료"¶
첫 손님 (Dollar) 이 만족했으니, 두 번째 손님 (Franc) 도 같은 메뉴 (times) 를 다른 통화로 주문합니다.
class FrancTest {
@Test
void Franc_곱셈() {
Franc five = new Franc(5);
assertEquals(new Franc(10), five.times(2));
}
}
→ 컴파일 실패. Franc 가 아직 없음. 정상 — 1·2장처럼 컴파일 실패도 빨강의 한 종류.
3. GREEN — 가장 빠른 길 (복붙)¶
단계 1: 컴파일되게 만든다 (껍데기)¶
Dollar 와 거의 같은 모양 — 복붙해서 만든다.
public class Franc {
private final int amount;
public Franc(int amount) { this.amount = amount; }
public Franc times(int multiplier) { return new Franc(0); } // 일단 가짜
@Override public boolean equals(Object o) { return false; } // 일단 가짜
}
→ 컴파일은 되지만 테스트 실패 (가짜 반환). 빨강 확인.
단계 2: 빠르게 초록으로¶
public class Franc {
private final int amount;
public Franc(int amount) { this.amount = 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;
}
}
→ 초록. Dollar 와 글자만 다르고 구조는 똑같음.
단계 3: 복붙 인정¶
여기서 멈춥니다. 중복 제거는 6장 빚. 한 사이클에 한 변경만.
왜 5장에서 추출 안 하나? 추출을 하려면 두 클래스의 정확한 공통 부분을 알아야 함. 지금은 막 만든 직후 — 한 박자 쉬고 패턴을 본 후 6장에서 추출. 너무 빠른 추상화 = 잘못된 추상화.
4. 통과했지만 — 닮음의 명확화¶
Dollar 와 Franc 를 나란히 두면 거의 데칼코마니:
// Dollar // Franc
public class Dollar { public class Franc {
private final int amount; private final int amount;
public Dollar(int amount) { ... } public Franc(int amount) { ... }
public Dollar times(int m) { public Franc times(int m) {
return new Dollar(amount * m); return new Franc(amount * m);
} }
@Override public boolean equals(...) @Override public boolean equals(...)
... ...
} }
차이:
1. 클래스 이름 (Dollar / Franc)
2. 생성자 호출 (new Dollar / new Franc)
3. instanceof 대상
같은 점:
1. 필드 (amount)
2. 생성자 본문
3. times 본문 구조
4. equals 본문 구조
→ 공통 부모 가 보이기 시작. 6장에서 추출.
5. 현업 예제 — Spring·도메인에서의 "의도된 중복"¶
사례: 결제 수단 추가¶
// 기존
public class CardPayment {
public PaymentResult process(Order o, Card c) { ... }
}
// 새 요구사항: 가상계좌 결제 추가
public class VirtualAccountPayment {
public PaymentResult process(Order o, VirtualAccount va) { ... }
}
처음에는 복붙해서 빠르게 동작 확인. 세 번째 결제 수단 (간편결제) 추가 시점에 공통 인터페이스 PaymentProcessor 추출이 자연스러움.
리팩터링 의 "3의 법칙"¶
첫 번째: 그냥 한다. 두 번째: 중복을 인지하지만 그대로 둔다. 세 번째: 추출한다.
TDD 5장은 정확히 두 번째 단계 — 인지하되 미룸.
6. 함정 / 주의¶
- 추상화를 너무 빨리 도입: 사용 사례 1개로 인터페이스 만들면 잘못된 인터페이스. 결과적으로 더 큰 리팩토링 비용.
- 복붙을 영구로 방치: 6장으로 넘어가지 않고 그대로 두면 코드 부패. 할 일 목록에 적어두는 게 핵심.
- "의도된 중복" 을 핑계로 모든 복붙 정당화: 6장 단계로 넘어가는 의지가 있어야 정당함.
- 5장만 보고 멈추면 사실 나쁜 코드. 6·7장이 완결.
7. 체크리스트 (5장 완료 기준)¶
-
Franc클래스를 만들고 곱셈 테스트가 초록인가 -
Dollar와Franc의 공통 구조를 머리로 인지했는가 - 중복 제거 항목이 할 일 목록에 적혔는가 (다음 장 빚)
- 5장 PR 이 통화 추가 의도만 담고 있는가 (중복 제거 섞이지 않음)
8. 퀴즈¶
- 5장에서
Franc를Dollar코드를 복붙해서 만든 이유는? - "처음부터 슈퍼클래스를 만들면 안 되는" 이유?
- 리팩터링 의 "3의 법칙" 과 5장이 연결되는 지점은?
- 의도된 중복을 6장에서 정리하지 않고 그대로 두면 어떻게 되나?
- 도메인 (결제 수단·할인 정책 등) 에 이 원칙을 어떻게 적용?
정답·해설¶
- 빠른 초록. 새 통화 (Franc) 동작 자체 검증이 이번 사이클 목표. 추상화는 다음 사이클 목표 (두 모자 분리). 복붙은 가장 빠른 길이고, 같은 PR 에 두 일을 섞으면 리뷰·롤백 어려움.
- 잘못된 추상화 위험. 사용 사례 1개 (Dollar) 만으로는 공통 부분이 무엇인지·어떻게 다를지 모름. 두 번째 사례 (Franc) 가 나타나야 진짜 공통점·차이가 보임. 빨리 추상화한 인터페이스는 두 번째 사례에 안 맞아 다시 리팩토링하는 비용 폭증.
- 두 번째 단계. 첫 번째는 그냥, 두 번째는 인지하되 그대로, 세 번째에 추출. 5장이 정확히 두 번째 — 인지 + 적어두기 + 미루기. 6장에서 세 번째 단계의 추출.
- 코드 부패의 시작. 의도된 중복을 영구 방치하면 두 클래스가 서로 다른 방향으로 진화 → 미래 추출이 더 어려워짐. 할 일 목록 + 다음 사이클의 약속이 있어야 정당.
- 결제 수단 첫 추가 시 복붙 → 세 번째 결제 수단 추가 시
PaymentProcessor인터페이스 추출. 첫 인터페이스가 두 사례를 모두 안고, 세 번째 사례에서 확신을 얻은 후 추상화. 할인 정책·검색 전략 모두 같은 패턴.
다음 장 예고 — 6장: 돌아온 '모두를 위한 평등'¶
5장에서 만든 Franc.equals 는 Dollar.equals 와 거의 똑같습니다. 슈퍼클래스 Money 를 추출 해서 equals 를 부모로 끌어올립니다. 동시에 새로운 빨강이 나타납니다 — Franc(5).equals(Dollar(5)) 가 어떻게 동작해야 할까? 7장의 "사과와 오렌지" 로 이어집니다.