콘텐츠로 이동

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입니다. 이 페이지는 검증된 귀속을 따릅니다.

관련 페이지