TDD 입문 가이드 — 10년째 벽 앞에 선 당신을 위해 - k82022603/k82022603.github.io GitHub Wiki

🔴🟢♻️ TDD 입문 가이드 — 10년째 벽 앞에 선 당신을 위해 #

"실패하는 테스트가 없으면 프로덕션 코드를 한 줄도 쓰지 않는다."
— Robert C. Martin (Uncle Bob)


📋 목차

  1. TDD란 무엇인가 — 5분 요약
  2. Uncle Bob의 3가지 법칙
  3. Red → Green → Refactor 사이클
  4. 처음 해보는 TDD 실전 시뮬레이션
  5. TDD 연습 사이트 & 도구
  6. 입문자가 가장 많이 하는 실수
  7. 오늘 당장 시작하는 방법

1. TDD란 무엇인가 — 5분 요약

대부분의 개발자는 이렇게 코딩합니다:

코드 작성 → 실행 → 버그 발견 → 수정 → 반복

TDD는 순서를 뒤집습니다:

테스트 먼저 작성 → 실패 확인 → 코드 작성 → 테스트 통과 → 코드 정리

왜 순서를 뒤집을까요?

테스트를 먼저 쓰면 "내가 만들 기능이 정확히 무엇인지"를 코드를 짜기 전에 명확히 정의하게 됩니다. 청사진 없이 집을 짓지 않듯, 기능 명세(테스트) 없이 코드를 짜지 않는 것이 TDD의 핵심입니다.

💡 비유: 시험 문제지(테스트)를 먼저 받고 답안지(코드)를 쓰는 것과 같습니다.
시험 범위를 미리 알면 불필요한 공부를 하지 않아도 됩니다.


2. Uncle Bob의 3가지 법칙

TDD의 규칙은 딱 세 가지입니다. 단순하지만 강력합니다.

법칙 내용
법칙 1 실패하는 단위 테스트가 없으면 프로덕션 코드를 한 줄도 작성하지 않는다
법칙 2 컴파일에 실패할 정도 이상의 테스트 코드는 작성하지 않는다
법칙 3 현재 실패하는 테스트를 통과시킬 만큼만 프로덕션 코드를 작성한다

왜 이 규칙들이 중요한가?

  • 법칙 1: 항상 목표(테스트)를 먼저 정합니다. 방향 없이 달리지 않습니다.
  • 법칙 2: 테스트를 한 번에 너무 많이 쓰지 않습니다. 작은 걸음으로 나아갑니다.
  • 법칙 3: 지금 당장 필요 없는 코드를 만들지 않습니다 (YAGNI 원칙).

세 법칙을 따르면 테스트와 프로덕션 코드가 거의 줄 단위로 번갈아 작성됩니다. 결과적으로 코드 전체가 항상 테스트로 보호받는 상태가 됩니다.


3. Red → Green → Refactor 사이클

TDD는 이 세 단계를 반복하는 리듬입니다. 보통 한 사이클은 2~5분 안에 끝납니다.

        ┌─────────────────────────────────────────┐
        │                                         │
        ▼                                         │
   🔴 RED                                         │
   실패하는 테스트 작성                            │
   (아직 코드가 없으니 당연히 실패)               │
        │                                         │
        ▼                                         │
   🟢 GREEN                                       │
   테스트를 통과시키는 최소한의 코드 작성          │
   (우아하지 않아도 괜찮다, 일단 통과!)           │
        │                                         │
        ▼                                         │
   ♻️ REFACTOR                                    │
   테스트는 유지한 채, 코드 구조를 정리           │
   (중복 제거, 네이밍 개선, 구조 개선)            │
        │                                         │
        └─────────────────────────────────────────┘
                     반복

핵심 마음가짐

  • 🔴 Red: "이 기능은 아직 없다"는 것을 증명합니다
  • 🟢 Green: "일단 동작하게" 만듭니다. 코드가 더럽고 하드코딩이어도 OK
  • ♻️ Refactor: "깨끗하게" 만듭니다. 테스트가 안전망 역할을 합니다

4. 처음 해보는 TDD 실전 시뮬레이션

FizzBuzz 문제로 TDD를 경험해 봅시다.

문제 정의: 1부터 N까지 숫자를 출력하되,

  • 3의 배수이면 "Fizz"
  • 5의 배수이면 "Buzz"
  • 15의 배수이면 "FizzBuzz"
  • 나머지는 숫자 그대로 출력

4.1 SpringBoot + JUnit 5 예제

환경 준비

  • pom.xml에 아래 의존성 추가
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

🔴 STEP 1: 첫 번째 테스트 작성 (RED)

아직 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 상태입니다. ✅ 정상입니다.


🟢 STEP 2: 최소한의 코드로 통과 (GREEN)

테스트를 통과시킬 가장 단순한 코드를 만듭니다.
하드코딩이어도 괜찮습니다. 일단 초록불이 목표입니다.

// src/main/java/com/example/FizzBuzz.java

public class FizzBuzz {

    public String convert(int number) {
        return "1"; // 😂 하드코딩! 하지만 지금은 이것으로 충분합니다
    }
}

테스트 실행 → GREEN 🟢


🔴 STEP 3: 두 번째 테스트 추가 (RED)

하드코딩을 깨뜨리는 새 테스트를 추가합니다.

@Test
void 숫자_2는_2를_반환한다() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    assertThat(fizzBuzz.convert(2)).isEqualTo("2");
}

실행 → RED 🔴 ("1" 하드코딩이 2를 처리 못 함)


🟢 STEP 4: 두 테스트 모두 통과 (GREEN)

public String convert(int number) {
    return String.valueOf(number); // 이제 숫자를 문자로 변환
}

실행 → GREEN 🟢 (두 테스트 모두 통과)


🔴 STEP 5: 3의 배수 테스트 추가 (RED)

@Test
void 숫자_3은_Fizz를_반환한다() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    assertThat(fizzBuzz.convert(3)).isEqualTo("Fizz");
}

실행 → RED 🔴


🟢 STEP 6: 3의 배수 처리 (GREEN)

public String convert(int number) {
    if (number % 3 == 0) return "Fizz";
    return String.valueOf(number);
}

실행 → GREEN 🟢


🔴 STEP 7: 5의 배수 테스트 추가 (RED)

@Test
void 숫자_5는_Buzz를_반환한다() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    assertThat(fizzBuzz.convert(5)).isEqualTo("Buzz");
}

실행 → RED 🔴


🟢 STEP 8: 5의 배수 처리 (GREEN)

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);
}

🔴 STEP 9: 15의 배수 테스트 추가 후 GREEN 확인

@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 🟢


♻️ STEP 10: 리팩터링 (REFACTOR)

중복된 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 🟢 ✅


4.2 Go + testing 패키지 예제

Go는 별도 라이브러리 없이 표준 testing 패키지만으로 TDD를 실천할 수 있습니다.

환경 준비

mkdir fizzbuzz && cd fizzbuzz
go mod init fizzbuzz

🔴 STEP 1: 테스트 파일 먼저 생성 (RED)

// 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 상태

🟢 STEP 2: 최소 코드 작성 (GREEN)

// fizzbuzz.go

package fizzbuzz

func Convert(number int) string {
    return "1" // 하드코딩, 일단 통과!
}
go test ./...
# PASS ← GREEN 🟢

🔴 STEP 3 → 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")
    }
}

🟢 최종 구현 (GREEN)

// 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 테이블 기반 테스트로 리팩터링 (REFACTOR)

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 🟢

5. TDD 연습 사이트 & 도구

TDD는 혼자 책으로 읽는 것보다 직접 반복 연습이 훨씬 효과적입니다.
아래 사이트들은 설치 없이 브라우저에서 바로 TDD를 연습할 수 있습니다.


🥇 Cyber-Dojo (강력 추천 — 입문자에게 최적)

사이트: https://cyber-dojo.org

  • 무료, 브라우저에서 바로 사용 (설치 불필요)
  • Java, Go, Python, Ruby 등 수십 가지 언어 지원
  • Red 🔴 / Green 🟢 / Yellow 트래픽 라이트로 내 TDD 리듬을 시각적으로 추적
  • FizzBuzz, String Calculator 등 유명 카타(Kata) 내장
  • 그룹 연습도 지원 (최대 64명 동시)

사용 방법:

  1. https://cyber-dojo.org 접속
  2. [I practice] 버튼 클릭
  3. 언어(Java 또는 Go) + 테스트 프레임워크 선택
  4. 문제 선택 (FizzBuzz 권장) → [start]
  5. 테스트 파일을 먼저 수정 → [test] 버튼으로 실행
  6. 상단의 트래픽 라이트 색상으로 Red/Green 확인

🥈 Exercism (언어별 단계별 학습)

사이트: https://exercism.org

  • 무료, Java와 Go 트랙 모두 존재
  • 멘토(실제 개발자)가 코드 리뷰를 해줍니다
  • 문제가 난이도 순으로 구성되어 있어 단계적으로 성장 가능
  • 로컬 개발 환경에서도, 브라우저에서도 사용 가능

🥉 Coding Dojo Kata (카타 문제 모음)

사이트: https://codingdojo.org/kata

  • 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       ← 상태 기계 설계 연습

6. 입문자가 가장 많이 하는 실수

❌ 실수 1: 테스트를 너무 크게 쓴다

// 나쁜 예 — 한 번에 모든 경우를 테스트
@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");
}

❌ 실수 2: GREEN을 건너뛰고 완벽한 코드를 쓰려 한다

TDD에서 GREEN 단계는 "동작하게"만 하면 됩니다.
하드코딩도 괜찮습니다. 정리는 REFACTOR에서 합니다.


❌ 실수 3: REFACTOR를 생략한다

Red → Green만 반복하고 Refactor를 건너뛰면 코드가 점점 지저분해집니다.
Green이 되면 반드시 코드를 정리하는 습관을 들이세요.


❌ 실수 4: 테스트가 통과한 뒤 프로덕션 코드를 더 건드린다

Green이 됐으면 충분합니다. 다음 테스트로 넘어가세요.
"어차피 나중에 필요할 것 같아서..." 라는 생각으로 미리 코드를 추가하지 마세요.


❌ 실수 5: 처음부터 어려운 문제에 도전한다

TDD 근육을 키우려면 반복 훈련이 필요합니다.
FizzBuzz, String Calculator 같은 쉬운 문제를 하루 15분씩 한 달간 반복하는 것이
어려운 문제 하나를 한 번 하는 것보다 훨씬 효과적입니다.


7. 오늘 당장 시작하는 방법

🚀 5분 안에 시작하기

  1. https://cyber-dojo.org 접속
  2. [I practice] 클릭
  3. Java 또는 Go 선택
  4. FizzBuzz 선택
  5. 테스트 파일 열기 → 첫 번째 테스트 작성
  6. [test] 버튼 → 빨간불(RED) 확인
  7. 구현 파일 열기 → 최소 코드 작성
  8. [test] 버튼 → 초록불(GREEN) 확인
  9. 다음 테스트 작성...

📅 한 달 연습 계획

주차 목표 카타
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

⚠️ **GitHub.com Fallback** ⚠️