TDD 3법칙과 F.I.R.S.T.¶
정의¶
Clean Code 9장(단위 테스트)이 압축해 놓은 테스트 규율 두 벌을 다루는 페이지입니다. TDD 3법칙은 테스트와 프로덕션 코드를 어떤 순서로 쓰는가에 대한 초 단위 규율이고, F.I.R.S.T. 는 그렇게 쌓인 테스트가 갖춰야 할 5가지 속성입니다. 둘을 관통하는 결론은 하나입니다 — 테스트 코드는 프로덕션 코드만큼 깨끗해야 하며, 더러운 테스트는 없는 테스트보다 나쁩니다.
귀속을 정확히 해 두면, 3법칙의 정식화는 Kent Beck이 아니라 Robert C. Martin(Uncle Bob) 의 것입니다. Martin은 1999년 Kent Beck과 페어 프로그래밍하며 "테스트 한 줄 → 그걸 통과시키는 프로덕션 한 줄"의 잘게 썬 사이클을 배웠고, 그 경험을 3법칙으로 명문화해 Clean Code 9장에 실었습니다. Beck의 원전 규칙은 두 개(실패하는 자동 테스트 없이는 새 코드를 쓰지 않는다, 중복을 제거한다)로 더 굵습니다.
TDD 3법칙 — 무엇을 금지하는 규율인가¶
3법칙은 "무엇을 하라"보다 "무엇을 하지 말라"로 읽을 때 명확해집니다. 각 법칙이 실무에서 실제로 차단하는 습관을 함께 정리합니다.
| # | 법칙 | 금지하는 습관 |
|---|---|---|
| 1 | 실패하는 단위 테스트를 작성하기 전에는 프로덕션 코드를 작성하지 않는다 | "일단 구현부터 하고 테스트는 나중에" (Test-After) |
| 2 | 컴파일 실패도 실패로 쳐서, 실패하는 정도까지만 테스트를 작성한다 | 테스트를 한꺼번에 왕창 써 두고 구현으로 넘어가기 |
| 3 | 현재 실패하는 테스트를 통과시킬 만큼만 프로덕션 코드를 작성한다 | 미래 요구사항을 예견한 과잉 구현 (YAGNI 위반) |
세 법칙을 지키면 사이클 한 바퀴가 30초~몇 분 안에 끝나고, 테스트와 프로덕션 코드가 사실상 같은 속도로 함께 자랍니다. 테스트 안 된 코드가 생길 틈 자체가 없어서, 높은 커버리지는 목표가 아니라 부산물이 됩니다.
사이클의 실제 장면¶
편의점 계산대의 신입 직원을 떠올려 봅니다. 옆에 선 선배가 먼저 "삼각김밥 하나 찍어 보세요"라고 딱 하나만 주문합니다. 신입은 바코드를 찍고, 선배는 화면 금액이 맞는지 그 자리에서 확인합니다. 맞으면 다음 주문이 하나 더 나오고, 틀리면 그 자리에서 바로잡습니다. 선배는 절대로 "오늘 올 손님 전부의 계산법을 먼저 다 외우세요"라고 하지 않습니다.
TDD 3법칙도 같은 순서로 돕니다. 실패하는 테스트 하나가 "삼각김밥 하나 찍어 보세요"라는 다음 주문이고(1·2법칙), 그 주문을 처리할 만큼만 프로덕션 코드를 쓰는 것이 바코드 한 번 찍기이며(3법칙), 테스트 막대가 초록으로 바뀌는 순간이 선배의 그 자리 확인입니다. 주문을 몰아서 받지 않는 것처럼 테스트도 몰아서 쓰지 않고, 계산법을 미리 다 외우지 않는 것처럼 구현도 미리 다 하지 않습니다.
흐름을 단계로 펼치면 다음과 같습니다.
1. Red — 실패하는 테스트 작성 (컴파일 실패도 Red)
2. 골조 — 인터페이스·클래스 뼈대만 만들어 컴파일 통과, 여전히 Red
3. Green — 통과시킬 최소 구현 (상수를 박아도 좋음)
4. Refactor — 테스트가 안전망인 상태에서 중복 제거·이름 정리
5. 다음 실패 테스트로
여기서 4단계 Refactor는 3법칙 자체에는 들어 있지 않다는 점이 중요합니다. 3법칙은 Red→Green 구간을 초 단위로 썬 규율이고, Refactor까지 포함한 전체 리듬은 아래에서 다루는 Kent Beck의 원전 사이클이 제공합니다.
F.I.R.S.T. — 좋은 테스트의 5속성¶
F.I.R.S.T.는 Object Mentor의 Tim Ottinger와 Brett Schuchert가 교육 자료를 다듬다가 만든 두문자로, Clean Code 9장 말미에 실리면서 널리 퍼졌습니다. 각 속성은 위반했을 때 겪는 증상으로 기억하는 편이 오래갑니다.
| 속성 | 요구 | 위반 시 실제로 겪는 증상 |
|---|---|---|
| Fast (빠르게) | 단위 테스트는 ms 단위, DB·HTTP·파일 접근 없음 | 저장할 때마다 돌리기가 부담스러워 "CI에서만 확인"으로 밀리고, 피드백이 커밋 뒤로 늦어져 결국 아무도 안 돌리는 테스트가 됨 |
| Independent (독립적으로) | 테스트 간 실행 순서·공유 상태 의존 금지 | 단독 실행은 통과인데 전체 실행은 실패(또는 그 반대), 테스트 하나 추가했더니 무관한 테스트가 깨짐, 병렬 실행 불가 |
| Repeatable (반복 가능하게) | 네트워크·현재 시각·랜덤·환경에 의존 금지 | "제 컴퓨터에서는 되는데요", 자정을 넘기면 깨지는 날짜 테스트, 며칠 방치하면 썩는 flaky 테스트로 신뢰 붕괴 |
| Self-validating (자가 검증하는) | 결과가 boolean — assert가 통과/실패를 스스로 판정 | 콘솔 로그를 사람이 눈으로 대조해야 판정 가능, 실패가 나도 초록불이라 회귀를 놓침 |
| Timely (적시에) | 프로덕션 코드 직전에 작성 (TDD의 1법칙과 동전의 양면) | 이미 짠 코드가 정적 호출·new 남발로 테스트하기 어려운 구조로 굳어, 테스트 작성 비용이 폭증하고 "나중에"가 영원히 계속됨 |
Timely가 3법칙과 만나는 지점입니다. 1법칙(실패 테스트 먼저)을 지키면 Timely는 자동으로 충족되고, 테스트하기 쉬운 구조가 설계 단계에서 강제됩니다. 반대로 Timely를 어기면 나머지 네 속성을 지키고 싶어도 구조가 허락하지 않는 경우가 많습니다.
깨끗한 테스트 — 가독성이 전부¶
Clean Code 9장이 제시하는 깨끗한 테스트의 기준은 단 하나, 가독성입니다. 그것도 프로덕션 코드보다 더 높은 수준의 가독성을 요구합니다. 이를 위한 골격이 BUILD-OPERATE-CHECK 패턴입니다 — 테스트를 자료 만들기(BUILD), 그 자료 조작하기(OPERATE), 결과 확인하기(CHECK)의 세 단으로 나누는 구조로, BDD 진영의 Given-When-Then과 같은 뼈대입니다.
셋업 잡음에 의도가 파묻힌 테스트와, 세 단이 눈에 보이는 테스트를 나란히 놓고 비교합니다. 아래는 실행용 실습이 아닌 구조 비교용 참고 코드입니다 (JUnit 5 + AssertJ).
// Before — 셋업 잡음에 검증 의도가 파묻힌 테스트
@Test
void test1() {
Member member = new Member();
member.setId(1L);
member.setName("won");
member.setGrade(Grade.BASIC);
memberRepository.save(member);
Order order = new Order();
order.setMemberId(1L);
order.addItem(new OrderItem("keyboard", 30_000, 1));
order.addItem(new OrderItem("mouse", 20_000, 1));
orderService.place(order);
Order found = orderRepository.findById(order.getId()).orElseThrow();
assertThat(found.payAmount()).isEqualTo(47_500);
assertThat(found.status()).isEqualTo(OrderStatus.PLACED);
}
// After — BUILD-OPERATE-CHECK 3단 + 도메인 헬퍼
@Test
void 기본_등급_회원은_5퍼센트_할인가로_주문된다() {
// BUILD — 도메인 언어로 재료 준비
Member won = aMember().withGrade(BASIC).saved(memberRepository);
Order order = anOrder().by(won)
.withItem("keyboard", 30_000)
.withItem("mouse", 20_000)
.build();
// OPERATE — 검증할 동작 하나만 실행
orderService.place(order);
// CHECK — 한 개념(할인 적용가)만 확인
assertThat(findSaved(order).payAmount()).isEqualTo(47_500);
}
After에서 달라진 것은 세 가지입니다. 첫째, aMember()·anOrder() 같은 빌더 헬퍼가 셋업 세부를 감춰 테스트 본문이 도메인의 단어로 읽힙니다(도메인 특화 테스트 언어). 둘째, 테스트 이름이 검증하는 사실을 한국어 문장으로 말합니다. 셋째, CHECK 단이 "할인 적용가"라는 개념 하나만 다룹니다 — assert 개수가 아니라 개념 개수가 기준이므로, 같은 개념의 두 면을 assert 두 개로 확인하는 것은 괜찮습니다.
여기에 이중 표준(dual standard) 이 더해집니다. 테스트 환경은 프로덕션 환경만큼 자원이 제약되지 않으므로, 메모리·CPU 효율을 가독성과 맞바꾸는 선택이 테스트에서는 허용됩니다. 단, 이름·구조·중복 제거의 기준은 프로덕션과 동일하게 지킵니다 — 낮춰도 되는 것은 효율이지 품질이 아닙니다.
Kent Beck 원전 사이클과의 관계¶
entity-tdd에서 정리한, 화폐 예제 하나로 32개 장 내내 빨강→초록→리팩터를 반복하는 Kent Beck의 원전(Test-Driven Development: By Example, 2002)과 이 페이지의 3법칙은 층위가 다릅니다. 역할 분담을 표로 정리합니다.
| 구분 | Kent Beck 원전 사이클 | Martin의 3법칙 |
|---|---|---|
| 단위 | 사이클 1회 (분 단위) — 빨강→초록→리팩터 | 빨강→초록 구간의 행동 규칙 (초 단위) |
| 형태 | 리듬·패턴 (Fake It, 삼각측량, 두 모자) | 금지 규칙 3개 (하지 말 것의 목록) |
| Refactor | 사이클의 정식 3단계 | 법칙 밖 — 초록 이후 별도 행동 |
| 배우는 곳 | entity-tdd 1부 화폐 예제 | Clean Code 9장, 이 페이지 |
즉 3법칙만 지키면 초록까지는 가지만 코드는 점점 지저분해지고, Beck의 사이클이 "초록 다음에 반드시 리팩터"를 붙여야 규율이 완성됩니다. 두 벌을 겹쳐 쓰는 것이 정답이고, 어느 한쪽만 아는 상태가 반쪽입니다.
같은 인사이트 패턴 — "거부 신호를 사이클 안에 내장"¶
1법칙이 하는 일을 한 발 떨어져 보면, 거부 신호(빨강)를 사이클의 첫 자리에 강제로 배치하는 것입니다. 시스템이 "그 기능은 아직 없다"고 먼저 거부하게 만들고, 그 거부를 지우는 것만이 다음 행동으로 허용됩니다. concept-loop-engineering이 "거부 신호 없는 루프는 메아리방"이라는 문장으로, entity-tdd가 "사이클 안의 거부 신호" 비교표로 이미 누적해 온 패턴의 원형이 바로 이것입니다.
| 영역 | 거부 신호 | 신호의 위치 | 참조 |
|---|---|---|---|
| TDD 3법칙 | 실패 테스트 (빨강) | 사이클의 첫 자리 — 프로덕션 코드보다 먼저 | (이 페이지) |
| Kent Beck TDD | 빨간 막대 | 빨강→초록→리팩터의 출발점 | entity-tdd |
| AI 루프 | 테스트·타입체크·실제 에러 | 루프 본체 안 (없으면 메아리방) | concept-loop-engineering |
| Hooks | guard.sh 종료 코드 1 |
도구 실행 직전 차단 | concept-claude-hooks |
| 멀티 에이전트 | Critic의 REJECT | 산출물 제출 직후 | concept-multi-agent-pattern |
공통 원리는 이렇습니다 — 자동으로 도는 사이클은 밀어내는 신호가 사이클 안에 붙박이로 있어야 폭주하지 않으며, TDD의 빨강은 그 신호를 사이클의 맨 앞에 둔 가장 오래된 사례입니다. F.I.R.S.T.는 그 신호가 계속 신호로 기능하기 위한 유지 조건으로 읽을 수 있습니다. 느리면(F 위반) 신호를 안 보게 되고, flaky하면(R 위반) 신호를 안 믿게 되고, 자가 검증이 안 되면(S 위반) 신호 자체가 사라집니다.
빠른 진단¶
아래 항목에 2개 이상 해당하면 이 페이지의 규율이 무너진 상태이므로, 해당 절부터 다시 점검합니다.
- 프로덕션 코드를 먼저 쓰고 테스트를 나중에 붙이는가 (1법칙·Timely 위반)
- 단위 테스트 전체가 1분 안에 도는가 (Fast)
- 단독 실행과 전체 실행의 결과가 다른 테스트가 있는가 (Independent)
- 현재 시각·랜덤·네트워크에 의존하는 단위 테스트가 있는가 (Repeatable)
- 통과 여부를 로그를 눈으로 보고 판정하는 테스트가 있는가 (Self-validating)
- 테스트 한 개가 서로 다른 개념 여러 개를 검증하는가
- 셋업 코드가 검증 코드보다 훨씬 길어 의도가 안 보이는가 (BUILD 단 헬퍼 부재)
- flaky 테스트를 원인 추적·고정·삭제 없이 방치하고 있는가
원본 출처¶
raw/clean-code/클린 코드 실전 강의 교재 9장.md— 단위 테스트 장 강의 교재 (위키 페이지: lecture-clean-code-ch9)- Robert C. Martin, "The Cycles of TDD" (blog.cleancoder.com, 2014-12-17) — 3법칙의 유래(1999년 Beck과의 페어링)와 정식화 경위
- Tim Ottinger·Jeff Langr, "Agile in a Flash: F.I.R.S.T." (2009) — F.I.R.S.T. 두문자의 탄생 경위 (Ottinger·Schuchert, Object Mentor)
- 주의: 강의 교재(raw)는 3법칙을 "Kent Beck의 3법칙"으로 표기하지만, 검증 결과 정식화의 주체는 Robert C. Martin입니다. 이 페이지는 검증된 귀속을 따릅니다.
관련 페이지¶
- lecture-clean-code-ch9 — 이 페이지의 바탕이 된 9장 강의 교재 (assert 하나·DSL·퀴즈 포함)
- entity-clean-code — Clean Code 책 전체 지도 (9장의 자리)
- entity-tdd — Kent Beck 원전 (빨강→초록→리팩터의 본가)
- src-tdd-lecture — TDD 실전 강의 교재 통합 인덱스
- java-study-ch09 — 테스트 피라미드·Spring Boot 테스트 전략 (이 규율의 실습 무대)
- src-spring-testing-ref — JUnit 5·MockMvc 실무 레퍼런스
- concept-loop-engineering — "거부 신호를 사이클 안에" 패턴의 AI 영역 확장