테스트 주도 개발 실전 강의 교재¶
1장 — 다중 통화를 지원하는 Money 객체¶
원서: 켄트 벡 『Test-Driven Development: By Example』 1부(화폐 예제) 대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 → 비유 → 따라하기(Red-Green-Refactor) → 핵심 교훈 → 현업 예제 → 함정 → 체크리스트 → 퀴즈(정답 분리) 전제 환경: Java 17+, JUnit 5
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- TDD의 기본 리듬 Red → Green → Refactor를 첫 사이클로 체감한다.
- 할 일 목록으로 작업을 잘게 쪼개 한 번에 하나씩 처리한다.
- "테스트를 먼저, 구현은 나중"이 왜 설계를 이끄는지 이해한다.
0.2 큰 그림 — TDD의 세 박자¶
┌──────────────────────────────────────────────┐
│ ① RED 실패하는 작은 테스트를 먼저 쓴다 │
│ ② GREEN 가장 빠른 방법으로 통과시킨다(반칙OK)│
│ ③ REFACTOR 중복을 제거하고 깨끗이 정리한다 │
└──────────────────────────────────────────────┘
└──────────── 반복 ───────────┘
비유 — "등산": 먼저 정상에 깃발을 꽂고(RED: 도달하고 싶은 목표를 테스트로 선언), 일단 가장 빠른 길로 올라간 뒤(GREEN: 수단 안 가리고 통과), 다음 사람을 위해 길을 닦습니다(REFACTOR: 정리). 길부터 닦고 오르는 게 아니라, 오르고 나서 닦습니다.
0.3 현업에서 왜 중요한가¶
- 리팩터링·클린코드에서 줄곧 강조한 "테스트라는 안전망"을, TDD는 아예 개발의 출발점으로 삼습니다.
- 테스트를 먼저 쓰면 "이 코드를 어떻게 쓰고 싶은가"를 사용자 관점에서 먼저 설계하게 됩니다(= API 설계가 좋아짐).
1. 할 일 목록부터 만들기¶
비유 — "장보기 목록"¶
머릿속에 모든 할 일을 담아두면 불안하고 빠뜨립니다. 종이에 적어 부담을 덜고, 하나씩 지웁니다. TDD는 이 목록으로 시작합니다.
이번 목표는 "여러 통화를 더할 수 있는 Money"입니다. 최종 요구를 적고, 거기서 작은 첫 걸음을 골라냅니다.
원칙(거꾸로 설계): 최종 목표에서 출발해, 지금 당장 통과시킬 수 있는 가장 작은 테스트로 쪼갭니다.
2. 첫 번째 테스트 — 곱셈 (RED)¶
비유 — "손님 입장에서 메뉴를 먼저 그리기"¶
구현을 떠올리기 전에, "이 객체를 어떻게 쓰고 싶은가"를 테스트로 먼저 적습니다. 테스트가 곧 첫 사용 예시(첫 고객)입니다.
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class DollarTest {
@Test
void 곱셈() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}
}
이 시점에 Dollar 클래스도, times도, amount도 없습니다. 컴파일조차 안 됩니다. 이것이 정상입니다 — 지금은 "빨강(RED)" 단계입니다.
TDD에서 "컴파일 실패"도 일종의 실패한 테스트입니다. 먼저 컴파일되게 만들고, 그다음 통과시킵니다.
3. 가장 빠른 길로 통과시키기 (GREEN)¶
1단계: 컴파일되게 만든다 (껍데기)¶
이제 컴파일은 되지만 테스트는 실패합니다(amount가 0). 빨강을 눈으로 확인하는 것이 중요합니다 — 테스트가 진짜로 실패하는지 봐야, 나중에 통과가 의미를 가집니다.
2단계: 가장 빠르게 초록으로¶
"반칙"이라도 좋으니 일단 통과시킵니다. 지금은 정답을 그냥 박아 넣어도 됩니다(가짜로 구현하기, Fake It).
class Dollar {
int amount = 10; // 일단 통과만 시킨다(곧 제거할 반칙)
Dollar(int amount) { }
void times(int multiplier) { }
}
테스트 초록! 하지만 우리는 이게 가짜임을 압니다. 다음 단계에서 진짜로 만들 겁니다.
3단계: 진짜 구현으로 (가짜 제거)¶
상수 10은 입력에서 유도할 수 있습니다. 점진적으로 진짜 계산으로 바꿉니다.
class Dollar {
int amount;
Dollar(int amount) { this.amount = amount; }
void times(int multiplier) { this.amount = amount * multiplier; }
}
테스트는 여전히 초록입니다. 첫 사이클 완료. 목록을 지웁니다.
핵심 리듬: 컴파일되게 → 빠르게 초록(가짜라도) → 가짜를 진짜로. "초록을 유지한 채" 조금씩 전진합니다.
4. 통과했지만, 냄새가 남았다 (다음 장으로)¶
첫 테스트는 통과했지만 설계 문제가 보입니다.
Dollar five = new Dollar(5);
five.times(2); // five 자신이 10으로 변해버린다!
// five는 이제 5가 아니다 → '값'이 변질됨(부작용)
금액 같은 값은 한 번 만들면 변하지 않아야 자연스럽습니다(이펙티브 자바 아이템 17 불변과 연결). times가 자기 자신을 바꾸는 이 문제를 다음 장(2장 타락한 객체)에서 해결합니다 — 새 Dollar를 반환하도록.
TDD는 이렇게 테스트가 통과한 뒤에 드러나는 설계 문제를, 다음 작은 테스트/리팩터로 이어가며 풉니다. 한 번에 완벽하게가 아니라, 작은 걸음의 연속입니다.
핵심 교훈¶
- Red → Green → Refactor가 한 사이클이다. 실패를 먼저 본 뒤 통과시킨다.
- 할 일 목록으로 작업을 잘게 쪼개 한 번에 하나씩.
- 테스트를 먼저 쓰면 "어떻게 쓰고 싶은가"(API)를 사용자 관점에서 설계하게 된다.
- 초록을 만들 땐 수단을 가리지 않는다(가짜로 구현 OK). 정리는 그다음.
- 통과 뒤 드러난 문제는 다음 작은 걸음으로 이어 푼다.
현업 예제 — JUnit/Spring에서의 TDD¶
- 새 기능을 도메인 서비스에 추가할 때, 실패하는 테스트부터 작성합니다.
@Test
void 계약_금액은_음수일_수_없다() {
assertThrows(IllegalArgumentException.class, () -> new Money(-1, "KRW"));
}
- 이 테스트가 빨강 →
Money생성자에 검증 추가(이펙티브 자바 아이템 49) → 초록 → 정리. - 컨트롤러/리포지토리는 슬라이스 테스트(
@WebMvcTest,@DataJpaTest)로, 순수 도메인 로직은 POJO 단위 테스트로 빠르게 돌립니다.
교차 연결: 클린코드 9장(F.I.R.S.T. — 빠르고, 독립적이고, 반복 가능하고, 자가 검증되고, 적시에). 리팩터링 4장(자가 테스트가 리팩터링의 전제). TDD는 이 둘을 "개발의 시작점"으로 끌어올린 것입니다.
함정 / 주의¶
- 빨강을 건너뛰지 말 것. 실패를 눈으로 확인하지 않으면, 통과가 우연인지 알 수 없습니다.
- 큰 걸음 금지. 한 사이클에 너무 많은 걸 담으면 디버깅이 어려워집니다. 막히면 더 작은 테스트로 쪼갭니다.
- 가짜 구현은 임시. Fake It은 초록을 빨리 보기 위한 디딤돌일 뿐, 곧 진짜로 바꿉니다.
- 테스트도 코드다. 테스트가 지저분하면 유지보수가 무너집니다(클린코드 9장).
체크리스트 (TDD 사이클)¶
- 할 일 목록에서 가장 작은 다음 항목을 골랐는가
- 구현 전에 실패하는 테스트를 먼저 썼는가
- 빨강(실패)을 눈으로 확인했는가
- 가장 빠른 방법으로 초록을 만들었는가
- 초록을 유지한 채 중복 제거/정리(리팩터)를 했는가
- 끝난 항목을 목록에서 지웠는가
퀴즈¶
- TDD의 세 박자와 각 단계의 목적을 말하라.
- "컴파일 실패"를 TDD에서 어떻게 취급하는가?
- 초록 단계에서 정답을 그냥 박아 넣는 "가짜로 구현하기"가 정당한 이유는?
- 1장 마지막에 드러난
five.times(2)의 설계 문제는 무엇이고, 어떤 개념과 연결되는가? - "빨강을 눈으로 확인하라"가 중요한 이유는?
정답·해설¶
- Red(실패하는 작은 테스트를 먼저 써서 목표를 선언) → Green(가장 빠른 방법으로 통과) → Refactor(중복 제거·정리). 각각 "목표 고정 / 빠른 도달 / 길 닦기"입니다.
- 일종의 실패한 테스트로 봅니다. 먼저 컴파일되게(껍데기) 만든 뒤, 그다음 통과시킵니다.
- 초록을 빨리 보기 위한 디딤돌이기 때문입니다. 일단 통과를 확인하면, 박아 넣은 상수를 입력에서 유도되는 진짜 계산으로 점진 교체합니다. 항상 동작하는 상태를 유지하는 게 핵심입니다.
times가 자기 자신(five)의 상태를 바꿔 값이 변질됩니다. 금액 같은 값은 불변이어야 자연스럽다는 점에서 이펙티브 자바 아이템 17(불변)과 연결되며, 2장에서 새 객체를 반환하도록 고칩니다.- 테스트가 진짜로 실패하는지 확인해야, 이후의 통과가 우연이 아니라 우리가 만든 결과임을 신뢰할 수 있기 때문입니다.
다음 장 예고 — 2장: 타락한 객체¶
1장에서 드러난 "값이 변질되는" 문제를 해결합니다. times가 자기 자신을 바꾸는 대신 새 Dollar를 반환하게 만들어, Money를 값 객체(불변)로 한 걸음 진화시킵니다. 작은 테스트 하나로 설계를 어떻게 밀어붙이는지 이어서 봅니다.