TDD 개요
정의
Test-Driven Development(TDD)는 동작하는 코드를 작성하기 전에 그 코드가 통과해야 할 자동화된 테스트를 먼저 작성하는 개발 방식이다. 2002년 Kent Beck의 Test-Driven Development: By Example로 정리됐고, Extreme Programming(XP)의 실천법 중 하나에서 출발했다.
이름에 “Test"가 들어 있지만 TDD는 테스트 기법이 아니라 설계 기법으로 분류된다. 테스트를 먼저 쓰려면 그 코드를 어떻게 호출할지, 무엇을 주고 무엇을 받을지를 먼저 정해야 하고, 그 결정이 곧 인터페이스 설계이기 때문이다. 테스트 스위트는 그 과정에서 부수적으로 남는 산출물에 가깝다.
Beck이 내건 목표는 한 문장이다.
Clean code that works. (동작하는 깨끗한 코드)
두 요구는 보통 충돌한다. 동작하게 만들다 보면 지저분해지고, 깨끗하게 만들다 보면 동작이 깨진다. TDD는 이 둘을 동시에 추구하는 대신 시간축에서 분리한다. 먼저 동작하게 만들고(Green), 그다음 깨끗하게 만든다(Refactor). 그리고 그 사이를 테스트가 붙잡아 준다.
두 가지 규칙
Beck이 제시한 TDD의 규칙은 두 개뿐이다.
- 실패하는 자동화된 테스트가 있을 때만 새 코드를 작성한다.
- 중복을 제거한다.
이 두 규칙에서 작업 순서가 따라 나온다. 테스트를 먼저 써야 하고, 테스트가 실패하는 것을 눈으로 확인해야 하며, 통과시킨 뒤에는 반드시 정리 단계를 거쳐야 한다. 나머지 관행은 대부분 이 두 규칙의 파생물이다.
사이클
TDD의 단위 작업은 Red - Green - Refactor 세 단계의 짧은 반복이다.
실패하는 테스트 작성"] --> G["Green
최소한의 코드로 통과"] G --> F["Refactor
중복 제거·정리"] F --> R
| 단계 | 하는 일 | 끝나는 조건 |
|---|---|---|
| Red | 아직 없는 기능에 대한 테스트를 하나 작성한다 | 테스트가 의도한 이유로 실패한다 |
| Green | 그 테스트를 통과시키는 가장 빠른 코드를 작성한다 | 모든 테스트가 통과한다 |
| Refactor | 방금 만든 중복과 지저분함을 제거한다 | 모든 테스트가 여전히 통과한다 |
한 사이클은 보통 몇 분 단위다. 사이클이 길어진다는 것은 테스트 단위가 너무 크다는 신호로 읽는다. 각 단계의 세부 규칙과 Green을 만드는 전략은 Red-Green-Refactor에서 다룬다.
무엇을 얻는가
설계 피드백이 즉시 온다
테스트를 먼저 쓸 때 준비 코드가 스무 줄씩 필요하거나, 객체 하나를 만들려고 협력 객체 다섯 개를 조립해야 하거나, private 메서드를 호출하고 싶어진다면 그것은 테스트의 문제가 아니라 설계의 문제다. 결합도가 높거나 책임이 뭉쳐 있다는 사실이 구현을 끝내기 전에 드러난다.
Michael Feathers의 표현대로, 테스트하기 어려운 코드는 대개 잘못 설계된 코드다. TDD는 그 판정을 코드가 굳기 전에 받게 한다.
회귀 안전망이 리팩터링을 가능하게 한다
리팩터링의 전제 조건은 “동작이 바뀌지 않았음을 확인할 수단"이다. TDD로 쌓인 테스트 스위트가 그 수단이 된다. 안전망이 없으면 구조를 바꾸는 일 자체가 도박이 되므로, 코드는 손대지 않는 방향으로 방치되고 점점 나빠진다.
이 관계는 단방향이 아니다. 리팩터링 단계를 건너뛰면 테스트만 쌓이고 설계는 나아지지 않는다. TDD에서 실제로 가장 자주 생략되는 단계가 Refactor다.
작업 범위가 좁아진다
“이 기능을 어떻게 구현하지"라는 큰 질문이 “다음 이 테스트 하나를 어떻게 통과시키지"라는 작은 질문으로 바뀐다. 한 번에 한 가지만 판단하므로 디버깅 범위도 마지막 몇 분간 작성한 코드로 한정된다.
실행되는 명세가 남는다
테스트 이름과 본문은 “이 코드가 어떤 입력에 어떻게 반응하는가"를 기술한다. 주석이나 문서와 달리 이 명세는 거짓이 되면 빌드가 깨지므로 코드와 동기화가 강제된다.
예제로 보는 한 사이클
주문 금액에 따라 할인율을 매기는 요구사항이 있다고 하자. 첫 사이클은 다음과 같이 시작한다.
Red - 아직 DiscountPolicy 클래스는 존재하지 않는다. 컴파일 에러도 실패로 친다.
class DiscountPolicyTest {
@Test
void 오만원_미만은_할인이_없다() {
DiscountPolicy policy = new DiscountPolicy();
assertThat(policy.discountFor(49_000)).isEqualTo(0);
}
}
Green - 통과시키는 가장 짧은 코드를 쓴다. 이 시점에서 일반화는 하지 않는다.
public class DiscountPolicy {
public int discountFor(int amount) {
return 0;
}
}
Refactor - 중복이 없으므로 넘어간다. 다음 테스트에서 5만원 이상 케이스를 추가하면 그때 return 0이 깨지고, 그 시점에 비로소 조건 분기가 등장한다. 구현이 테스트에 의해 한 단계씩 끌려나오는 이 방식을 삼각측량(triangulation)이라고 부른다.
흔한 오해
커버리지를 올리는 활동이 아니다
TDD의 산출물로 커버리지가 높아지는 것은 사실이지만, 커버리지를 목표로 삼으면 방향이 반대가 된다. 구현을 먼저 하고 나중에 테스트를 채워 넣는 것은 TDD가 아니라 테스트 후행(test-after) 이고, 이 경우 테스트는 이미 존재하는 구현을 그대로 따라 쓰게 되어 설계 피드백이라는 핵심 효과가 사라진다.
테스트를 먼저 쓴다고 설계가 저절로 되지는 않는다
TDD는 나쁜 설계를 알려주는 장치이지 좋은 설계를 만들어 주는 장치가 아니다. 어떤 객체가 어떤 책임을 가져야 하는지에 대한 판단은 여전히 사람의 몫이고, 리팩터링 단계에서 그 판단을 반영하지 않으면 테스트만 많은 절차적 코드가 남는다.
모든 코드에 적용하는 것이 아니다
Beck 본인도 전부에 적용하지는 않는다고 말한다. 로직이 거의 없는 getter/setter, 프레임워크 설정, UI 픽셀 조정, 외부 시스템과의 연동처럼 검증할 규칙이 없거나 실행 비용이 큰 영역에서는 이득이 비용을 넘지 못한다. 판단 기준은 “여기에 틀릴 수 있는 결정이 들어 있는가"다.
통합 테스트를 대체하지 않는다
TDD로 만들어지는 테스트는 대개 빠른 단위 테스트다. 단위가 각각 맞아도 조립하면 틀릴 수 있으므로, 경계를 넘는 동작은 별도의 통합 테스트로 검증해야 한다. 어떤 종류의 테스트를 어떤 비율로 둘 것인가는 테스트 피라미드와 단위의 범위에서 다룬다.
TDD와 Self-testing Code는 같은 말이 아니다
TDD는 테스트가 갖춰진 상태를 만드는 하나의 경로이지 그 상태 자체가 아니다. 코드를 먼저 쓰고 테스트를 나중에 붙여도 같은 상태에 도달할 수 있다. 이 구분을 흐리면 “TDD가 필요한가"라는 논쟁과 “자동화된 회귀 테스트가 필요한가"라는 논쟁이 뒤섞인다. 자세한 내용은 Self-testing Code에 있다.
TDD에 대한 비판
2014년 David Heinemeier Hansson(DHH)의 “TDD is dead. Long live testing.” 글로 촉발된 논쟁이 이 주제의 대표적 참고 자료다. Martin Fowler, Kent Beck, DHH가 진행한 연속 대담(Is TDD Dead?)에서 정리된 쟁점은 다음과 같다.
| 쟁점 | 비판 측 주장 | 반론 |
|---|---|---|
| 설계 왜곡 | 테스트 가능성을 위해 인터페이스와 계층이 불필요하게 늘어난다 | 그 지점이 실제로 결합도가 높은 지점인 경우가 많다. 다만 과도한 간접층은 실제 위험으로 인정됨 |
| Mock 남용 | 목으로 격리한 테스트는 구현 세부에 결합되어 리팩터링을 방해한다 | 학파의 문제로, Classicist 스타일에서는 발생하지 않는다 |
| 속도만을 위한 격리 | 단위 테스트를 빠르게 만들려고 DB를 걷어내면 진짜 위험은 검증되지 않는다 | 테스트 종류를 계층으로 나누어 각각의 목적에 맞게 배치해야 한다 |
논쟁의 결론은 “TDD를 하느냐 마느냐"가 아니라 어떤 종류의 테스트를 어느 비율로 두느냐로 수렴했다. 이 비판의 상당 부분은 TDD 자체보다 테스트 더블의 오용에서 비롯되므로, 테스트 더블 문서와 함께 읽는 것이 낫다.
관련 문서
- Red-Green-Refactor - 사이클 각 단계의 규칙과 Green을 만드는 세 가지 전략
- Classicist vs Mockist - 격리 방식에 따라 갈리는 두 학파와 선택 기준
- 테스트 더블 - Dummy·Fake·Stub·Spy·Mock의 구분과 사용 기준
- 테스트 피라미드와 단위의 범위 - 테스트 종류의 배치 비율, “단위"의 정의, solitary vs sociable
- Self-testing Code - TDD가 만들어내는 결과 상태와 리팩터링·CI와의 관계
- DDD 개요 - 도메인 모델을 코드의 중심에 두는 접근. TDD의 설계 피드백과 상호 보완적
참고 자료
- Kent Beck, Test-Driven Development: By Example (2002)
- Martin Fowler, Test Driven Development
- Martin Fowler, Is TDD Dead? - Beck·DHH·Fowler 대담 정리
- David Heinemeier Hansson, TDD is dead. Long live testing.
- Michael Feathers, Working Effectively with Legacy Code (2004)