TDD
contents
테스트 주도 개발(Test-Driven Development, TDD) 은 실제(운영) 코드를 작성하기 전에 실패하는 자동화된 테스트를 먼저 작성하는 소프트웨어 개발 프로세스입니다.
이 프로세스는 전통적인 개발 방식의 역순입니다. 이는 단순한 테스트 기법이 아니라 강력한 설계 방법론입니다. 테스트가 코드 설계를 주도 하며, 코드가 처음부터 모듈화되고, 단순하며, 테스트 가능하도록 강제합니다.
TDD 순환: 레드-그린-리팩토
TDD는 짧고 반복적인 순환(loop)을 기반으로 합니다. 각 순환은 몇 분 정도만 소요되어야 합니다.
1. 🔴 RED: 실패하는 테스트 작성하기
먼저, 아직 존재하지 않는 작은 기능 조각에 대한 자동화된 테스트를 작성합니다.
- 목표: 코드가 무엇을 하기를 원하는지 명확하게 정의하는 것입니다. 테스트는 정확한 명세서 역할을 합니다.
- 행동: 존재하지 않는 메서드나 구현되지 않은 기능에 대한 단위 테스트를 작성합니다.
- 결과: 테스트를 실행하면 반드시 실패해야 합니다. 컴파일이 되지 않는 것도 실패로 간주합니다. 이 실패는 테스트가 (오탐(false positive)이 아니라) 올바르게 작동하고 있으며, 해당 기능이 우연히 이미 존재하지 않는다는 것을 증명하기 때문에 중요합니다.
예시 (Java):
// 간단한 계산기 테스트
@Test
void testAddTwoNumbers() {
Calculator calc = new Calculator();
int result = calc.add(2, 3);
assertEquals(5, result);
}
이 시점에는 Calculator 클래스나 .add() 메서드가 존재하지 않으므로, 이 테스트는 컴파일에 실패합니다 (RED).
2. 🟢 GREEN: 테스트를 통과하는 최소한의 코드 작성하기
다음으로, 테스트를 통과시키기 위해 필요한 최소한의, 가장 간단한 코드를 작성합니다.
- 목표: 오직 테스트를 통과하는 것, 그 이상도 이하도 아닙니다.
- 행동: 좋거나, 깨끗하거나, 효율적인 코드를 작성하려 노력하는 것이 아닙니다. 오직 테스트 막대를 녹색으로 만드는 데만 집중합니다. 이것이 핵심 규칙입니다. 테스트가
true를 기대한다면, 그냥return true;를 합니다. - 결과: 테스트 스위트를 실행합니다. 이제 모든 테스트가 통과해야 합니다.
예시 (Java):
이전 테스트를 통과시키기 위해 다음과 같이 작성할 수 있습니다.
public class Calculator {
public int add(int a, int b) {
return 5; // 테스트를 통과하기 위한 최소한의 코드!
}
}
이것은 어리석어 보일 수 있지만, calc.add(3, 4)와 같은 다음 테스트를 작성하도록 강제합니다. 이 새 테스트는 실패할 것이고(RED), 이로 인해 return 5;를 return a + b;라는 실제 해결책으로 변경하게 됩니다(GREEN). 이 과정을 삼각측량(triangulation) 이라고 부릅니다.
3. ♻️ REFACTOR: 코드 정리하기
이제 테스트가 통과하므로, 당신은 안전망을 갖게 되었습니다. 기능이 명세대로 작동한다는 것을 압니다. 이제 자신감을 가지고 코드를 정리할 수 있습니다.
- 목표: 방금 작성한 코드의 동작을 변경하지 않으면서 설계를 개선하는 것입니다.
- 행동: 코드를 리팩토링합니다. 중복을 제거하고, 변수 이름을 개선하며, 큰 메서드를 분리하고, 설계 원칙을 준수하는 것을 의미합니다.
- 결과: 테스트 스위트를 다시 실행합니다. 테스트는 여전히 모두 통과해야 합니다. 만약 통과하지 못한다면, 리팩토링 중에 무언가를 망가뜨렸다는 것을 즉시 알 수 있고 고칠 수 있습니다.
이 단계가 끝나면, 즉시 새로운 RED 테스트를 작성하며 이 순환을 반복합니다.
💡 TDD의 이점
- 회귀 방지 안전망: 애플리케이션을 구축하면서 포괄적인 단위 테스트 스위트를 만들게 됩니다. 이는 실수로 무언가를 망가뜨리면 테스트가 즉시 실패할 것을 알기 때문에, 자신감을 가지고 리팩토링하거나 새 기능을 추가할 수 있게 해줍니다.
- 좋은 설계를 강제함: 테스트 불가능한 코드(거대하고 복잡한 "갓 클래스(God classes)" 등)를 작성할 수 없습니다. TDD는 자연스럽게 작고, 모듈화되며, 느슨하게 결합된 코드를 작성하도록 강제합니다. 이런 코드는 테스트하기 쉽고, 따라서 유지보수하기도 쉽습니다.
- 살아있는 문서: 테스트 스위트 자체가 상세하고 최신 상태인 문서 역할을 합니다. 누구나 테스트를 읽어보고 코드가 모든 시나리오에서 어떻게 동작해야 하는지 정확히 이해할 수 있습니다.
- 버그 감소: 버그가 QA 단계에서 몇 주, 몇 달 뒤에 발견되는 것이 아니라, 생성되는 순간에 바로 잡힙니다. 이로 인해 수정이 훨씬 쉽고 저렴해집니다.
🚧 TDD의 어려움
- 가파른 학습 곡선: 처음에는 "느리고" 부자연스럽게 느껴집니다. 연습이 필요한 훈련입니다.
- 모든 곳에 적용하기 어려움: 비즈니스 로직에는 훌륭하지만, TDD는 GUI(그래픽 사용자 인터페이스)나 데이터베이스 또는 네트워크 API와 같이 외부 시스템과 많이 상호작용하는 코드에는 적용하기 더 어렵습니다. (불가능하지는 않지만, 해당 시스템 _주변_의 로직을 테스트하게 됩니다.)
- 나쁜 테스트의 위험: 만약 부실하고 사소한 테스트만 작성한다면, 잘못된 안정감을 얻게 됩니다. 테스트가 너무 "깨지기 쉽다면"(구현에 너무 밀접하게 결합되어 있다면), 리팩토링할 때마다 실패하여 좌절감을 유발할 것입니다.
TDD vs. 전통적인 테스트
| 전통적인 테스트 | 테스트 주도 개발 (TDD) |
|---|---|
| 1. 실제 코드를 작성한다. | 1. 실패하는 테스트를 작성한다. |
| 2. 코드를 검증하기 위해 테스트를 작성한다. | 2. 테스트를 통과시키기 위한 최소한의 실제 코드를 작성한다. |
| 3. 테스트를 실행하고 버그를 고친다. | 3. 코드를 리팩토링한다. |
| 목표: 테스트는 코드를 검증하기 위한 사후 활동이다. | 목표: 테스트는 개발을 주도하는 설계 활동이다. |
references