콘텐츠로 이동

테스트 주도 개발 실전 강의 교재

1장 — 다중 통화를 지원하는 Money 객체

원서: 켄트 벡 『Test-Driven Development: By Example』 1부(화폐 예제) 대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 → 비유 → 따라하기(Red-Green-Refactor) → 핵심 교훈 → 현업 예제 → 함정 → 체크리스트 → 퀴즈(정답 분리) 전제 환경: Java 17+, JUnit 5


0. 이 장을 시작하기 전에

0.1 학습 목표

  • TDD의 기본 리듬 Red → Green → Refactor를 첫 사이클로 체감한다.
  • 할 일 목록으로 작업을 잘게 쪼개 한 번에 하나씩 처리한다.
  • "테스트를 먼저, 구현은 나중"이 왜 설계를 이끄는지 이해한다.

0.2 큰 그림 — TDD의 세 박자

   ┌──────────────────────────────────────────────┐
   │  ① RED      실패하는 작은 테스트를 먼저 쓴다     │
   │  ② GREEN    가장 빠른 방법으로 통과시킨다(반칙OK)│
   │  ③ REFACTOR 중복을 제거하고 깨끗이 정리한다       │
   └──────────────────────────────────────────────┘
        └──────────── 반복 ───────────┘

비유 — "등산": 먼저 정상에 깃발을 꽂고(RED: 도달하고 싶은 목표를 테스트로 선언), 일단 가장 빠른 길로 올라간 뒤(GREEN: 수단 안 가리고 통과), 다음 사람을 위해 길을 닦습니다(REFACTOR: 정리). 길부터 닦고 오르는 게 아니라, 오르고 나서 닦습니다.

0.3 현업에서 왜 중요한가

  • 리팩터링·클린코드에서 줄곧 강조한 "테스트라는 안전망"을, TDD는 아예 개발의 출발점으로 삼습니다.
  • 테스트를 먼저 쓰면 "이 코드를 어떻게 쓰고 싶은가"를 사용자 관점에서 먼저 설계하게 됩니다(= API 설계가 좋아짐).

1. 할 일 목록부터 만들기

비유 — "장보기 목록"

머릿속에 모든 할 일을 담아두면 불안하고 빠뜨립니다. 종이에 적어 부담을 덜고, 하나씩 지웁니다. TDD는 이 목록으로 시작합니다.

이번 목표는 "여러 통화를 더할 수 있는 Money"입니다. 최종 요구를 적고, 거기서 작은 첫 걸음을 골라냅니다.

[ ] $5 + 10 CHF = $10  (환율이 2:1일 때)   ← 최종 목표(아직 크다)
[ ] $5 * 2 = $10                          ← 여기서 시작!  (가장 작고 쉬운 것)

원칙(거꾸로 설계): 최종 목표에서 출발해, 지금 당장 통과시킬 수 있는 가장 작은 테스트로 쪼갭니다.


2. 첫 번째 테스트 — 곱셈 (RED)

비유 — "손님 입장에서 메뉴를 먼저 그리기"

구현을 떠올리기 전에, "이 객체를 어떻게 쓰고 싶은가"를 테스트로 먼저 적습니다. 테스트가 곧 첫 사용 예시(첫 고객)입니다.

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

class DollarTest {
    @Test
    void 곱셈() {
        Dollar five = new Dollar(5);
        five.times(2);
        assertEquals(10, five.amount);
    }
}

이 시점에 Dollar 클래스도, times도, amount도 없습니다. 컴파일조차 안 됩니다. 이것이 정상입니다 — 지금은 "빨강(RED)" 단계입니다.

TDD에서 "컴파일 실패"도 일종의 실패한 테스트입니다. 먼저 컴파일되게 만들고, 그다음 통과시킵니다.


3. 가장 빠른 길로 통과시키기 (GREEN)

1단계: 컴파일되게 만든다 (껍데기)

class Dollar {
    int amount;
    Dollar(int amount) { }
    void times(int multiplier) { }
}

이제 컴파일은 되지만 테스트는 실패합니다(amount가 0). 빨강을 눈으로 확인하는 것이 중요합니다 — 테스트가 진짜로 실패하는지 봐야, 나중에 통과가 의미를 가집니다.

2단계: 가장 빠르게 초록으로

"반칙"이라도 좋으니 일단 통과시킵니다. 지금은 정답을 그냥 박아 넣어도 됩니다(가짜로 구현하기, Fake It).

class Dollar {
    int amount = 10;          // 일단 통과만 시킨다(곧 제거할 반칙)
    Dollar(int amount) { }
    void times(int multiplier) { }
}

테스트 초록! 하지만 우리는 이게 가짜임을 압니다. 다음 단계에서 진짜로 만들 겁니다.

3단계: 진짜 구현으로 (가짜 제거)

상수 10은 입력에서 유도할 수 있습니다. 점진적으로 진짜 계산으로 바꿉니다.

class Dollar {
    int amount;
    Dollar(int amount) { this.amount = amount; }
    void times(int multiplier) { this.amount = amount * multiplier; }
}

테스트는 여전히 초록입니다. 첫 사이클 완료. 목록을 지웁니다.

[x] $5 * 2 = $10
[ ] $5 + 10 CHF = $10

핵심 리듬: 컴파일되게 → 빠르게 초록(가짜라도) → 가짜를 진짜로. "초록을 유지한 채" 조금씩 전진합니다.


4. 통과했지만, 냄새가 남았다 (다음 장으로)

첫 테스트는 통과했지만 설계 문제가 보입니다.

Dollar five = new Dollar(5);
five.times(2);      // five 자신이 10으로 변해버린다!
// five는 이제 5가 아니다 → '값'이 변질됨(부작용)

금액 같은 은 한 번 만들면 변하지 않아야 자연스럽습니다(이펙티브 자바 아이템 17 불변과 연결). times가 자기 자신을 바꾸는 이 문제를 다음 장(2장 타락한 객체)에서 해결합니다 — 새 Dollar를 반환하도록.

TDD는 이렇게 테스트가 통과한 뒤에 드러나는 설계 문제를, 다음 작은 테스트/리팩터로 이어가며 풉니다. 한 번에 완벽하게가 아니라, 작은 걸음의 연속입니다.


핵심 교훈

  1. Red → Green → Refactor가 한 사이클이다. 실패를 먼저 본 뒤 통과시킨다.
  2. 할 일 목록으로 작업을 잘게 쪼개 한 번에 하나씩.
  3. 테스트를 먼저 쓰면 "어떻게 쓰고 싶은가"(API)를 사용자 관점에서 설계하게 된다.
  4. 초록을 만들 땐 수단을 가리지 않는다(가짜로 구현 OK). 정리는 그다음.
  5. 통과 뒤 드러난 문제는 다음 작은 걸음으로 이어 푼다.

현업 예제 — JUnit/Spring에서의 TDD

  • 새 기능을 도메인 서비스에 추가할 때, 실패하는 테스트부터 작성합니다.
@Test
void 계약_금액은_음수일_수_없다() {
    assertThrows(IllegalArgumentException.class, () -> new Money(-1, "KRW"));
}
  • 이 테스트가 빨강 → Money 생성자에 검증 추가(이펙티브 자바 아이템 49) → 초록 → 정리.
  • 컨트롤러/리포지토리는 슬라이스 테스트(@WebMvcTest, @DataJpaTest)로, 순수 도메인 로직은 POJO 단위 테스트로 빠르게 돌립니다.

교차 연결: 클린코드 9장(F.I.R.S.T. — 빠르고, 독립적이고, 반복 가능하고, 자가 검증되고, 적시에). 리팩터링 4장(자가 테스트가 리팩터링의 전제). TDD는 이 둘을 "개발의 시작점"으로 끌어올린 것입니다.


함정 / 주의

  • 빨강을 건너뛰지 말 것. 실패를 눈으로 확인하지 않으면, 통과가 우연인지 알 수 없습니다.
  • 큰 걸음 금지. 한 사이클에 너무 많은 걸 담으면 디버깅이 어려워집니다. 막히면 더 작은 테스트로 쪼갭니다.
  • 가짜 구현은 임시. Fake It은 초록을 빨리 보기 위한 디딤돌일 뿐, 곧 진짜로 바꿉니다.
  • 테스트도 코드다. 테스트가 지저분하면 유지보수가 무너집니다(클린코드 9장).

체크리스트 (TDD 사이클)

  • 할 일 목록에서 가장 작은 다음 항목을 골랐는가
  • 구현 전에 실패하는 테스트를 먼저 썼는가
  • 빨강(실패)을 눈으로 확인했는가
  • 가장 빠른 방법으로 초록을 만들었는가
  • 초록을 유지한 채 중복 제거/정리(리팩터)를 했는가
  • 끝난 항목을 목록에서 지웠는가

퀴즈

  1. TDD의 세 박자와 각 단계의 목적을 말하라.
  2. "컴파일 실패"를 TDD에서 어떻게 취급하는가?
  3. 초록 단계에서 정답을 그냥 박아 넣는 "가짜로 구현하기"가 정당한 이유는?
  4. 1장 마지막에 드러난 five.times(2)의 설계 문제는 무엇이고, 어떤 개념과 연결되는가?
  5. "빨강을 눈으로 확인하라"가 중요한 이유는?

정답·해설

  1. Red(실패하는 작은 테스트를 먼저 써서 목표를 선언) → Green(가장 빠른 방법으로 통과) → Refactor(중복 제거·정리). 각각 "목표 고정 / 빠른 도달 / 길 닦기"입니다.
  2. 일종의 실패한 테스트로 봅니다. 먼저 컴파일되게(껍데기) 만든 뒤, 그다음 통과시킵니다.
  3. 초록을 빨리 보기 위한 디딤돌이기 때문입니다. 일단 통과를 확인하면, 박아 넣은 상수를 입력에서 유도되는 진짜 계산으로 점진 교체합니다. 항상 동작하는 상태를 유지하는 게 핵심입니다.
  4. times자기 자신(five)의 상태를 바꿔 값이 변질됩니다. 금액 같은 값은 불변이어야 자연스럽다는 점에서 이펙티브 자바 아이템 17(불변)과 연결되며, 2장에서 새 객체를 반환하도록 고칩니다.
  5. 테스트가 진짜로 실패하는지 확인해야, 이후의 통과가 우연이 아니라 우리가 만든 결과임을 신뢰할 수 있기 때문입니다.

다음 장 예고 — 2장: 타락한 객체

1장에서 드러난 "값이 변질되는" 문제를 해결합니다. times가 자기 자신을 바꾸는 대신 새 Dollar를 반환하게 만들어, Money를 값 객체(불변)로 한 걸음 진화시킵니다. 작은 테스트 하나로 설계를 어떻게 밀어붙이는지 이어서 봅니다.