TDD 입문 가이드 — 10년째 벽 앞에 선 당신을 위해 - k82022603/k82022603.github.io GitHub Wiki
🔴🟢♻️ TDD 입문 가이드 — 10년째 벽 앞에 선 당신을 위해 #
"실패하는 테스트가 없으면 프로덕션 코드를 한 줄도 쓰지 않는다."
— Robert C. Martin (Uncle Bob)
- TDD란 무엇인가 — 5분 요약
- Uncle Bob의 3가지 법칙
- Red → Green → Refactor 사이클
- 처음 해보는 TDD 실전 시뮬레이션
- TDD 연습 사이트 & 도구
- 입문자가 가장 많이 하는 실수
- 오늘 당장 시작하는 방법
대부분의 개발자는 이렇게 코딩합니다:
코드 작성 → 실행 → 버그 발견 → 수정 → 반복
TDD는 순서를 뒤집습니다:
테스트 먼저 작성 → 실패 확인 → 코드 작성 → 테스트 통과 → 코드 정리
왜 순서를 뒤집을까요?
테스트를 먼저 쓰면 "내가 만들 기능이 정확히 무엇인지"를 코드를 짜기 전에 명확히 정의하게 됩니다. 청사진 없이 집을 짓지 않듯, 기능 명세(테스트) 없이 코드를 짜지 않는 것이 TDD의 핵심입니다.
💡 비유: 시험 문제지(테스트)를 먼저 받고 답안지(코드)를 쓰는 것과 같습니다.
시험 범위를 미리 알면 불필요한 공부를 하지 않아도 됩니다.
TDD의 규칙은 딱 세 가지입니다. 단순하지만 강력합니다.
| 법칙 | 내용 |
|---|---|
| 법칙 1 | 실패하는 단위 테스트가 없으면 프로덕션 코드를 한 줄도 작성하지 않는다 |
| 법칙 2 | 컴파일에 실패할 정도 이상의 테스트 코드는 작성하지 않는다 |
| 법칙 3 | 현재 실패하는 테스트를 통과시킬 만큼만 프로덕션 코드를 작성한다 |
- 법칙 1: 항상 목표(테스트)를 먼저 정합니다. 방향 없이 달리지 않습니다.
- 법칙 2: 테스트를 한 번에 너무 많이 쓰지 않습니다. 작은 걸음으로 나아갑니다.
- 법칙 3: 지금 당장 필요 없는 코드를 만들지 않습니다 (YAGNI 원칙).
세 법칙을 따르면 테스트와 프로덕션 코드가 거의 줄 단위로 번갈아 작성됩니다. 결과적으로 코드 전체가 항상 테스트로 보호받는 상태가 됩니다.
TDD는 이 세 단계를 반복하는 리듬입니다. 보통 한 사이클은 2~5분 안에 끝납니다.
┌─────────────────────────────────────────┐
│ │
▼ │
🔴 RED │
실패하는 테스트 작성 │
(아직 코드가 없으니 당연히 실패) │
│ │
▼ │
🟢 GREEN │
테스트를 통과시키는 최소한의 코드 작성 │
(우아하지 않아도 괜찮다, 일단 통과!) │
│ │
▼ │
♻️ REFACTOR │
테스트는 유지한 채, 코드 구조를 정리 │
(중복 제거, 네이밍 개선, 구조 개선) │
│ │
└─────────────────────────────────────────┘
반복
- 🔴 Red: "이 기능은 아직 없다"는 것을 증명합니다
- 🟢 Green: "일단 동작하게" 만듭니다. 코드가 더럽고 하드코딩이어도 OK
- ♻️ Refactor: "깨끗하게" 만듭니다. 테스트가 안전망 역할을 합니다
FizzBuzz 문제로 TDD를 경험해 봅시다.
문제 정의: 1부터 N까지 숫자를 출력하되,
- 3의 배수이면
"Fizz"- 5의 배수이면
"Buzz"- 15의 배수이면
"FizzBuzz"- 나머지는 숫자 그대로 출력
- pom.xml에 아래 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>아직 FizzBuzz 클래스는 존재하지 않습니다. 컴파일조차 안 됩니다. 이것이 정상입니다.
// src/test/java/com/example/FizzBuzzTest.java
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class FizzBuzzTest {
@Test
void 숫자_1은_1을_반환한다() {
FizzBuzz fizzBuzz = new FizzBuzz();
assertThat(fizzBuzz.convert(1)).isEqualTo("1");
}
}테스트를 실행하면? → 컴파일 에러 (FizzBuzz 클래스가 없으니까)
이것이 RED 상태입니다. ✅ 정상입니다.
테스트를 통과시킬 가장 단순한 코드를 만듭니다.
하드코딩이어도 괜찮습니다. 일단 초록불이 목표입니다.
// src/main/java/com/example/FizzBuzz.java
public class FizzBuzz {
public String convert(int number) {
return "1"; // 😂 하드코딩! 하지만 지금은 이것으로 충분합니다
}
}테스트 실행 → GREEN 🟢
하드코딩을 깨뜨리는 새 테스트를 추가합니다.
@Test
void 숫자_2는_2를_반환한다() {
FizzBuzz fizzBuzz = new FizzBuzz();
assertThat(fizzBuzz.convert(2)).isEqualTo("2");
}실행 → RED 🔴 (
"1"하드코딩이2를 처리 못 함)
public String convert(int number) {
return String.valueOf(number); // 이제 숫자를 문자로 변환
}실행 → GREEN 🟢 (두 테스트 모두 통과)
@Test
void 숫자_3은_Fizz를_반환한다() {
FizzBuzz fizzBuzz = new FizzBuzz();
assertThat(fizzBuzz.convert(3)).isEqualTo("Fizz");
}실행 → RED 🔴
public String convert(int number) {
if (number % 3 == 0) return "Fizz";
return String.valueOf(number);
}실행 → GREEN 🟢
@Test
void 숫자_5는_Buzz를_반환한다() {
FizzBuzz fizzBuzz = new FizzBuzz();
assertThat(fizzBuzz.convert(5)).isEqualTo("Buzz");
}실행 → RED 🔴
public String convert(int number) {
if (number % 15 == 0) return "FizzBuzz"; // 15의 배수 먼저 체크!
if (number % 3 == 0) return "Fizz";
if (number % 5 == 0) return "Buzz";
return String.valueOf(number);
}@Test
void 숫자_15는_FizzBuzz를_반환한다() {
FizzBuzz fizzBuzz = new FizzBuzz();
assertThat(fizzBuzz.convert(15)).isEqualTo("FizzBuzz");
}
@Test
void 숫자_9는_Fizz를_반환한다() {
FizzBuzz fizzBuzz = new FizzBuzz();
assertThat(fizzBuzz.convert(9)).isEqualTo("Fizz");
}
@Test
void 숫자_10은_Buzz를_반환한다() {
FizzBuzz fizzBuzz = new FizzBuzz();
assertThat(fizzBuzz.convert(10)).isEqualTo("Buzz");
}모두 GREEN 🟢
중복된 new FizzBuzz()를 정리합니다. 테스트는 바뀌지 않습니다.
// src/test/java/com/example/FizzBuzzTest.java
class FizzBuzzTest {
private final FizzBuzz fizzBuzz = new FizzBuzz(); // 중복 제거
@Test
void 숫자_1은_1을_반환한다() {
assertThat(fizzBuzz.convert(1)).isEqualTo("1");
}
@Test
void 숫자_3은_Fizz를_반환한다() {
assertThat(fizzBuzz.convert(3)).isEqualTo("Fizz");
}
@Test
void 숫자_5는_Buzz를_반환한다() {
assertThat(fizzBuzz.convert(5)).isEqualTo("Buzz");
}
@Test
void 숫자_15는_FizzBuzz를_반환한다() {
assertThat(fizzBuzz.convert(15)).isEqualTo("FizzBuzz");
}
@Test
void 숫자_9는_Fizz를_반환한다() {
assertThat(fizzBuzz.convert(9)).isEqualTo("Fizz");
}
@Test
void 숫자_10은_Buzz를_반환한다() {
assertThat(fizzBuzz.convert(10)).isEqualTo("Buzz");
}
}// src/main/java/com/example/FizzBuzz.java (최종)
public class FizzBuzz {
public String convert(int number) {
if (number % 15 == 0) return "FizzBuzz";
if (number % 3 == 0) return "Fizz";
if (number % 5 == 0) return "Buzz";
return String.valueOf(number);
}
}리팩터링 후 전체 테스트 실행 → 모두 GREEN 🟢 ✅
Go는 별도 라이브러리 없이 표준 testing 패키지만으로 TDD를 실천할 수 있습니다.
mkdir fizzbuzz && cd fizzbuzz
go mod init fizzbuzz// fizzbuzz_test.go
package fizzbuzz
import "testing"
func TestConvert_숫자_1은_1을_반환한다(t *testing.T) {
result := Convert(1)
if result != "1" {
t.Errorf("Convert(1) = %q, want %q", result, "1")
}
}go test ./...
# FAIL: undefined: Convert ← 정상입니다. RED 상태// fizzbuzz.go
package fizzbuzz
func Convert(number int) string {
return "1" // 하드코딩, 일단 통과!
}go test ./...
# PASS ← GREEN 🟢// fizzbuzz_test.go 에 테스트 추가
func TestConvert_숫자_2는_2를_반환한다(t *testing.T) {
result := Convert(2)
if result != "2" {
t.Errorf("Convert(2) = %q, want %q", result, "2")
}
}
func TestConvert_숫자_3은_Fizz를_반환한다(t *testing.T) {
result := Convert(3)
if result != "Fizz" {
t.Errorf("Convert(3) = %q, want %q", result, "Fizz")
}
}
func TestConvert_숫자_5는_Buzz를_반환한다(t *testing.T) {
result := Convert(5)
if result != "Buzz" {
t.Errorf("Convert(5) = %q, want %q", result, "Buzz")
}
}
func TestConvert_숫자_15는_FizzBuzz를_반환한다(t *testing.T) {
result := Convert(15)
if result != "FizzBuzz" {
t.Errorf("Convert(15) = %q, want %q", result, "FizzBuzz")
}
}// fizzbuzz.go (최종)
package fizzbuzz
import "fmt"
func Convert(number int) string {
if number%15 == 0 {
return "FizzBuzz"
}
if number%3 == 0 {
return "Fizz"
}
if number%5 == 0 {
return "Buzz"
}
return fmt.Sprintf("%d", number)
}go test ./... -v
# === RUN TestConvert_숫자_1은_1을_반환한다
# --- PASS: TestConvert_숫자_1은_1을_반환한다 (0.00s)
# === RUN TestConvert_숫자_3은_Fizz를_반환한다
# --- PASS: TestConvert_숫자_3은_Fizz를_반환한다 (0.00s)
# ...
# PASS ← 모두 GREEN 🟢Go에서는 테이블 드리븐 테스트(Table-Driven Test)가 관용적인 리팩터링 방법입니다.
// fizzbuzz_test.go (리팩터링 후)
package fizzbuzz
import "testing"
func TestConvert(t *testing.T) {
tests := []struct {
input int
expected string
}{
{1, "1"},
{2, "2"},
{3, "Fizz"},
{5, "Buzz"},
{9, "Fizz"},
{10, "Buzz"},
{15, "FizzBuzz"},
{30, "FizzBuzz"},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("Convert(%d)", tt.input), func(t *testing.T) {
result := Convert(tt.input)
if result != tt.expected {
t.Errorf("got %q, want %q", result, tt.expected)
}
})
}
}go test ./... -v
# 모든 케이스 PASS 🟢TDD는 혼자 책으로 읽는 것보다 직접 반복 연습이 훨씬 효과적입니다.
아래 사이트들은 설치 없이 브라우저에서 바로 TDD를 연습할 수 있습니다.
- 무료, 브라우저에서 바로 사용 (설치 불필요)
- Java, Go, Python, Ruby 등 수십 가지 언어 지원
- Red 🔴 / Green 🟢 / Yellow 트래픽 라이트로 내 TDD 리듬을 시각적으로 추적
- FizzBuzz, String Calculator 등 유명 카타(Kata) 내장
- 그룹 연습도 지원 (최대 64명 동시)
사용 방법:
- https://cyber-dojo.org 접속
-
[I practice]버튼 클릭 - 언어(Java 또는 Go) + 테스트 프레임워크 선택
- 문제 선택 (FizzBuzz 권장) →
[start] - 테스트 파일을 먼저 수정 →
[test]버튼으로 실행 - 상단의 트래픽 라이트 색상으로 Red/Green 확인
사이트: https://exercism.org
- 무료, Java와 Go 트랙 모두 존재
- 멘토(실제 개발자)가 코드 리뷰를 해줍니다
- 문제가 난이도 순으로 구성되어 있어 단계적으로 성장 가능
- 로컬 개발 환경에서도, 브라우저에서도 사용 가능
- TDD에 적합한 카타 문제 목록 제공
- FizzBuzz, String Calculator, Bowling Game, Roman Numerals 등 수십 가지
- 문제만 제공하므로 내 로컬 환경에서 직접 TDD 적용 연습에 적합
| 사이트 | URL | 특징 |
|---|---|---|
| TDD Buddy | https://www.tddbuddy.com | 가이드 + 카타 모음, 초보자 친화적 |
| Kata-log | https://kata-log.rocks/tdd | 난이도·태그별 카타 분류 |
| Roy Osherove String Calculator | https://osherove.com/tdd-kata-1 | 세계적으로 유명한 TDD 입문 카타 |
1단계 (시작): FizzBuzz ← 30분 내 완성 가능
2단계 (기초): String Calculator ← 하루 15분씩 1주일
3단계 (중급): Roman Numerals ← 알고리즘 + TDD 결합
4단계 (심화): Bowling Game ← 규칙이 복잡한 현실적 문제
5단계 (팀): Tennis Game ← 상태 기계 설계 연습
// 나쁜 예 — 한 번에 모든 경우를 테스트
@Test
void FizzBuzz_전체_동작_테스트() {
assertThat(fizzBuzz.convert(1)).isEqualTo("1");
assertThat(fizzBuzz.convert(3)).isEqualTo("Fizz");
assertThat(fizzBuzz.convert(5)).isEqualTo("Buzz");
assertThat(fizzBuzz.convert(15)).isEqualTo("FizzBuzz");
// ... 10줄 더
}// 좋은 예 — 하나의 테스트, 하나의 동작
@Test
void 숫자_3은_Fizz를_반환한다() {
assertThat(fizzBuzz.convert(3)).isEqualTo("Fizz");
}TDD에서 GREEN 단계는 "동작하게"만 하면 됩니다.
하드코딩도 괜찮습니다. 정리는 REFACTOR에서 합니다.
Red → Green만 반복하고 Refactor를 건너뛰면 코드가 점점 지저분해집니다.
Green이 되면 반드시 코드를 정리하는 습관을 들이세요.
Green이 됐으면 충분합니다. 다음 테스트로 넘어가세요.
"어차피 나중에 필요할 것 같아서..." 라는 생각으로 미리 코드를 추가하지 마세요.
TDD 근육을 키우려면 반복 훈련이 필요합니다.
FizzBuzz, String Calculator 같은 쉬운 문제를 하루 15분씩 한 달간 반복하는 것이
어려운 문제 하나를 한 번 하는 것보다 훨씬 효과적입니다.
- https://cyber-dojo.org 접속
-
[I practice]클릭 - Java 또는 Go 선택
-
FizzBuzz선택 - 테스트 파일 열기 → 첫 번째 테스트 작성
-
[test]버튼 → 빨간불(RED) 확인 - 구현 파일 열기 → 최소 코드 작성
-
[test]버튼 → 초록불(GREEN) 확인 - 다음 테스트 작성...
| 주차 | 목표 | 카타 |
|---|---|---|
| 1주차 | Red → Green 리듬 익히기 | FizzBuzz 매일 반복 |
| 2주차 | 작게 쪼개는 습관 | String Calculator (Step 1~3) |
| 3주차 | Refactor 습관화 | String Calculator (Step 4~7) |
| 4주차 | 실제 프로젝트 적용 시도 | 본인 프로젝트의 유틸 함수 1개 |
💡 핵심: 매일 15분이 한 달에 한 번 4시간보다 훨씬 효과적입니다.
뇌는 짧은 반복을 통해 새로운 습관을 만들어 냅니다.
10년간 TDD를 이해하려 했지만 실천하지 못한 건 의지의 문제가 아닙니다.
TDD는 읽어서 이해하는 것이 아니라, 손으로 반복해야 몸이 기억하는 기술입니다.
오늘 FizzBuzz 하나만 Cyber-Dojo에서 Red → Green → Refactor 사이클로 완성해 보세요.
그 15분이 지난 10년의 벽을 무너뜨리는 첫 번째 벽돌이 될 것입니다.
참고: Uncle Bob (Robert C. Martin)의 TDD 3법칙, Clean Code, Cyber-Dojo, Exercism, Coding Dojo