테스트 주도 개발 TDD(Test-DrivenDevelopment)이해하기

1. 테스트 주도 개발이 필요한 이유
일반적인 개발 방식에서는 보통 기능 코드를 먼저 작성한 뒤 테스트를 진행합니다.
흐름은 대략 다음과 같습니다.
요구사항 확인
↓
기능 구현
↓
테스트 작성 또는 수동 테스트
↓
버그 수정
이 방식은 익숙하고 빠르게 시작할 수 있다는 장점이 있습니다.
하지만 코드가 커지고 기능이 복잡해질수록 다음과 같은 문제가 발생하기 쉽습니다.
✔️ 일반적인 개발 방식에서 자주 발생하는 문제
▸ 구현이 끝난 뒤에야 요구사항을 잘못 이해했다는 사실을 알게 됩니다.
▸ 테스트 코드가 구현 코드에 맞춰 작성되어 검증력이 약해질 수 있습니다.
▸ 리팩토링 시 기존 동작이 깨졌는지 확인하기 어렵습니다.
▸ 수동 테스트에 의존하면 반복 검증 비용이 커집니다.
▸ 버그 수정 과정에서 또 다른 버그가 발생할 수 있습니다.
TDD는 이러한 문제를 줄이기 위해 테스트를 먼저 작성하는 방식을 사용합니다.
즉, 기능을 구현하기 전에 먼저 다음 질문에 답합니다.
“이 기능은 어떤 입력을 받았을 때 어떤 결과를 반환해야 하는가?”
이 질문에 대한 답을 테스트 코드로 작성한 뒤, 해당 테스트를 통과하도록 실제 코드를 구현합니다.
✔️ TDD가 중요한 이유
▸ 요구사항을 코드로 명확하게 표현할 수 있습니다.
▸ 구현 전에 기대 동작을 먼저 정의할 수 있습니다.
▸ 변경에 강한 코드를 만들 수 있습니다.
▸ 리팩토링을 더 안전하게 수행할 수 있습니다.
▸ 테스트 가능한 구조로 설계하게 됩니다.
결국 TDD의 핵심은 테스트 코드 작성 그 자체가 아니라, 테스트를 통해 설계를 점진적으로 개선하는 것입니다.
✔️ 일반 개발 방식과 TDD 비교
| 구분 | 일반개발방식 | TDD |
| 개발 순서 | 기능 구현 후 테스트합니다. | 테스트를 먼저 작성한 뒤 구현합니다. |
| 요구사항 확인 | 구현 과정에서 구체화되는 경우가 많습니다. | 테스트 코드로 기대 동작을 먼저 정의합니다. |
| 테스트 목적 | 구현 결과를 검증합니다. | 구현 방향을 이끌어갑니다. |
| 리팩토링 안정성 | 변경 영향 범위를 확인하기 어렵습니다. | 테스트를 통해 기존 동작을 검증할 수 있습니다. |
| 설계 방식 | 구현 중심으로 설계가 진행됩니다. | 테스트 가능한 구조를 중심으로 설계가 개선됩니다. |
| 적합한 경우 | 단순한 기능이나 빠른 프로토타입에 적합합니다. | 유지보수성과 안정성이 중요한 코드에 적합합니다. |
2. TDD 개발 프로세스 이해하기
TDD는 일반적으로 다음 3단계 사이클로 설명합니다.
Red → Green → Refactor
🔷 Red: 실패하는 테스트 작성
구현할 기능의 기대 동작을 명확히 정의합니다.
▸ 테스트가 실제로 실패하는지 확인합니다.
▸ 잘못된 테스트가 아닌지 검증합니다.
▸ 구현 전에 요구사항을 구체화합니다.
예를 들어 다음과 같은 요구사항이 있다고 가정해 보겠습니다.
두 숫자를 더하면 합계를 반환해야 한다.
이 요구사항을 먼저 테스트 코드로 작성합니다.
test('두 숫자를 더하면 합계를 반환한다', () => {
expect(add(1, 2)).toBe(3);
});
아직 add 함수가 없거나 구현되지 않았다면 이 테스트는 실패합니다.
이 실패는 나쁜 것이 아닙니다.
오히려 TDD에서는 이 실패가 중요한 출발점입니다.
🔷 Green: 테스트를 통과하는 최소 코드 작성
다음 단계는 실패한 테스트를 통과시키는 것입니다.
▸ 실패한 테스트를 빠르게 통과시킵니다.
▸ 과도한 설계를 피합니다.
▸ 현재 요구사항에 필요한 만큼만 구현합니다.
▸ 작고 안정적인 단위로 기능을 완성합니다.
우선 테스트를 통과하는 가장 단순한 코드를 작성합니다.
function add(a, b) {
return a + b;
}
이제 테스트를 실행하면 통과합니다.
PASS 두 숫자를 더하면 합계를 반환한다
Green 단계에서는 코드 품질보다 동작을 만족시키는 것에 집중합니다.
🔷 Refactor: 코드 구조 개선
▸ 중복 코드를 제거합니다.
▸ 가독성을 개선합니다.
▸ 함수와 클래스의 책임을 명확히 합니다.
▸ 유지보수하기 쉬운 구조로 변경합니다.
▸ 테스트를 통해 기존 동작이 유지되는지 확인합니다.
예를 들어 중복된 코드가 있다면 제거하고, 변수명을 더 명확히 바꾸고, 함수의 책임을 분리할 수 있습니다.
동작은 그대로 유지
↓
내부 구조 개선
↓
테스트 재실행
↓
기존 동작 보장 확인
TDD에서 리팩토링이 중요한 이유는 테스트가 안전망 역할을 하기 때문입니다.
리팩토링 후에도 테스트가 통과한다면, 최소한 기존에 정의한 동작은 유지되고 있다고 판단할 수 있습니다.
✔️ TDD 프로세스 정리
| 단계 | 설명 | 핵심질문 |
| Red | 실패하는 테스트를 먼저 작성합니다. | 이 기능은 어떻게 동작해야 하는가? |
| Green | 테스트를 통과하는 최소 구현을 작성합니다. | 테스트를 통과하려면 무엇이 필요한가? |
| Refactor | 동작은 유지하면서 코드 구조를 개선합니다. | 더 읽기 쉽고 유지보수하기 좋은 구조인가? |
3. 간단한 코드 예제로 보는 TDD 흐름
예제 요구사항은 다음과 같습니다.
비밀번호가 8자 이상이면 유효한 비밀번호로 판단한다.
이 기능을 TDD 방식으로 구현해 보겠습니다.
1단계: 실패하는 테스트 작성: Red 단계
test('비밀번호가 8자 이상이면 유효하다', () => {
expect(isValidPassword('12345678')).toBe(true);
});
아직 isValidPassword 함수가 없기 때문에 테스트는 실패합니다.
2단계: 테스트를 통과하는 최소 구현 작성: Green 단계
이제 테스트를 통과하도록 함수를 구현합니다.
function isValidPassword(password) {
return password.length >= 8;
}
테스트를 다시 실행하면 통과합니다.
3단계: 예외 케이스 추가
하지만 실제 비밀번호 검증에서는 8자 미만인 경우도 확인해야 합니다.
새로운 테스트를 추가합니다.
test('비밀번호가 8자 미만이면 유효하지 않다', () => {
expect(isValidPassword('1234567')).toBe(false);
});
이 경우에는 추가 구현이 필요하지 않습니다.
하지만 여기서 중요한 점은 요구사항이 테스트 코드로 더 명확해졌다는 것입니다.
4단계: null 또는 빈 값 처리 추가
이번에는 다음 요구사항을 추가해 보겠습니다.
비밀번호가 없으면 유효하지 않다.
테스트를 먼저 작성합니다.
test('비밀번호가 null이면 유효하지 않다', () => {
expect(isValidPassword(null)).toBe(false);
});
현재 코드는 password.length를 호출하기 때문에 null이 들어오면 에러가 발생합니다.
이제 테스트를 통과하도록 코드를 수정합니다.
function isValidPassword(password) {
if (!password) {
return false;
}
return password.length >= 8;
}
다시 테스트를 실행하면 통과합니다.
5단계: 리팩토링
현재 코드는 충분히 단순하지만, 조건을 변수로 분리하면 의미를 더 명확하게 표현할 수 있습니다.
function isValidPassword(password) {
if (!password) {
return false;
}
const MIN_PASSWORD_LENGTH = 8;
return password.length >= MIN_PASSWORD_LENGTH;
}
동작은 동일하지만 코드의 의도가 더 명확해졌습니다.
그리고 테스트가 있기 때문에 리팩토링 후에도 기존 동작이 깨지지 않았는지 확인할 수 있습니다.
✔️ 최종 테스트 코드 예시
describe('isValidPassword', () => {
test('비밀번호가 8자 이상이면 유효하다', () => {
expect(isValidPassword('12345678')).toBe(true);
});
test('비밀번호가 8자 미만이면 유효하지 않다', () => {
expect(isValidPassword('1234567')).toBe(false);
});
test('비밀번호가 null이면 유효하지 않다', () => {
expect(isValidPassword(null)).toBe(false);
});
});
✔️ 최종 구현 코드 예시
function isValidPassword(password) {
if (!password) {
return false;
}
const MIN_PASSWORD_LENGTH = 8;
return password.length >= MIN_PASSWORD_LENGTH;
}
이 예제에서 볼 수 있듯이 TDD는 큰 기능을 한 번에 완성하는 방식이 아닙니다.
작은 요구사항을 테스트로 정의하고,
그 테스트를 통과하는 구현을 작성하고,
필요할 때 구조를 개선하는 과정을 반복합니다.
4. TDD와 리팩토링, 단위 테스트의 차이
TDD를 처음 접할 때 자주 헷갈리는 개념이 있습니다.
바로 TDD, 단위 테스트(Unit Test), 리팩토링(Refactoring) 입니다.
세 개념은 서로 밀접하게 연결되어 있지만 같은 의미는 아닙니다.
🔷 TDD와 단위 테스트의 차이
단위 테스트는 함수, 메서드, 클래스처럼 작은 단위의 코드가 기대한 대로 동작하는지 검증하는 테스트입니다.
반면 TDD는 테스트를 활용하는 개발 방법론입니다.
즉, 단위 테스트는 테스트의 한 종류이고, TDD는 그 테스트를 작성하고 활용하는 개발 프로세스입니다.
| 구분 | 단위 테스트 | TDD |
| 의미 | 작은 코드 단위를 검증하는 테스트입니다. | 테스트를 먼저 작성하고 구현하는 개발 방식입니다. |
| 작성 시점 | 구현 전 또는 구현 후 모두 가능합니다. | 구현 전에 작성합니다. |
| 목적 | 코드가 정상 동작하는지 확인합니다. | 테스트를 통해 설계와 구현을 이끌어갑니다. |
| 범위 | 테스트 기법에 가깝습니다. | 개발 프로세스에 가깝습니다. |
| 관계 | TDD에서 자주 사용됩니다. | 단위 테스트를 기반으로 진행되는 경우가 많습니다. |
단위 테스트는 검증 도구입니다.
TDD는 테스트를 먼저 작성하는 개발 방식입니다.
🔷 TDD와 리팩토링의 관계
리팩토링은 코드의 외부 동작은 바꾸지 않고 내부 구조를 개선하는 작업입니다.
예를 들어 다음과 같은 작업이 리팩토링에 해당합니다.
▸ 중복 코드 제거
▸ 변수명과 함수명 개선
▸ 긴 함수 분리
▸ 클래스 책임 분리
▸ 조건문 단순화
▸ 의존성 구조 개선
TDD에서 리팩토링이 중요한 이유는 테스트가 있기 때문입니다.
테스트가 없는 상태에서 리팩토링을 하면 기존 기능이 깨졌는지 확인하기 어렵습니다.
반면 테스트가 있으면 리팩토링 후 테스트를 실행하여 기존 동작이 유지되는지 확인할 수 있습니다.
테스트 작성
↓
기능 구현
↓
테스트 통과
↓
리팩토링
↓
테스트 재실행
이 흐름 덕분에 개발자는 더 자신 있게 코드를 개선할 수 있습니다.
✔️ TDD를 적용하기 좋은 경우
▸ 비즈니스 규칙이 명확한 도메인 로직
▸ 계산, 검증, 변환 로직
▸ 예외 케이스가 많은 기능
▸ 리팩토링이 자주 필요한 코드
▸ 장애 발생 시 영향이 큰 핵심 로직
예를 들어 다음과 같은 코드는 TDD 적용 효과가 큽니다.
결제 금액 계산
쿠폰 할인 정책
비밀번호 검증
주문 상태 변경
권한 체크
정산 로직
반대로 다음과 같은 경우에는 TDD의 효율이 낮을 수 있습니다.
✔️ TDD 적용이 부담스러운 경우
▸ UI 디자인이 자주 바뀌는 화면
▸ 단순 CRUD 코드
▸ 빠르게 검증해야 하는 프로토타입
▸ 외부 API 의존성이 강한 코드
▸ 요구사항이 아직 불명확한 기능
이런 경우에는 모든 코드를 TDD로 작성하려고 하기보다, 핵심 로직을 분리한 뒤 해당 로직에 대해 테스트를 작성하는 방식이 더 현실적입니다.
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'5. IT기술노트 > 인프라&개발' 카테고리의 다른 글
| 행위 주도 개발 BDD(Behavior-Driven Development) 이해하기 (0) | 2026.05.07 |
|---|---|
| Latency Numbers로 설계하는 백엔드 아키텍처 (0) | 2026.05.05 |
| CQRS 개념과 Read-Your-Own-Writes 해결 전략 (0) | 2026.02.09 |
| PWA(Progressive Web App)의 개념과 실행 프로세스 이해 (0) | 2026.01.08 |
| 개발 표기법(Naming Convention)정리: camelCase, PascalCase, snake_case (0) | 2026.01.02 |