TDD(Test-Driven Development)

Theo·2025년 8월 19일
post-thumbnail

TDD란 무엇인가?

TDD(Test-Driven Development)는 테스트를 먼저 작성하고, 그 테스트를 통과하는 최소한의 코드를 작성한 후, 리팩토링을 통해 코드를 개선하는 개발 방법론입니다.

TDD의 핵심 사이클: Red-Green-Refactor

TDD는 다음 3단계를 반복하는 사이클로 진행됩니다:

🔴 Red: 실패하는 테스트 작성

  • 아직 구현되지 않은 기능에 대한 테스트를 먼저 작성
  • 당연히 테스트는 실패함

🟢 Green: 테스트를 통과하는 최소한의 코드 작성

  • 테스트를 통과시키기 위한 가장 간단한 코드 구현
  • 완벽하지 않아도 됨, 일단 동작하기만 하면 됨

🔵 Refactor: 코드 개선

  • 중복 제거, 가독성 향상, 성능 최적화 등
  • 테스트가 여전히 통과하는지 확인

실습 예제: 간단한 계산기 (Java)

TDD의 Red-Green-Refactor 사이클을 실제로 체험해보겠습니다.

1단계: 🔴 Red - 실패하는 테스트 작성

먼저 덧셈 기능을 테스트하는 코드를 작성합니다.

// CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    
    @Test
    void 두_수를_더한다() {
        // Given
        Calculator calculator = new Calculator();
        
        // When
        int result = calculator.add(2, 3);
        
        // Then
        assertEquals(5, result);
    }
}

이 시점에서는 Calculator 클래스가 없어서 컴파일 에러가 발생합니다. 이것이 바로 🔴 Red 단계입니다!

2단계: 🟢 Green - 테스트를 통과하는 최소 코드

이제 테스트를 통과시키기 위한 최소한의 코드를 작성합니다.

// Calculator.java
public class Calculator {
    
    public int add(int a, int b) {
        return a + b;
    }
}

테스트가 통과합니다! 🟢 Green 단계 완료!

3단계: 🔵 Refactor - 코드 개선

현재는 코드가 간단해서 리팩토링할 부분이 없습니다. 다음 기능으로 넘어가겠습니다.

TDD 사이클 반복: 빼기 기능 추가

🔴 Red: 빼기 테스트 추가

@Test
void 두_수를_뺀다() {
    // Given
    Calculator calculator = new Calculator();
    
    // When
    int result = calculator.subtract(5, 2);
    
    // Then
    assertEquals(3, result);
}

subtract 메서드가 없어서 컴파일 에러 발생! 🔴 Red 단계

🟢 Green: 빼기 기능 구현

public class Calculator {
    
    public int add(int a, int b) {
        return a + b;
    }
    
    public int subtract(int a, int b) {
        return a - b;
    }
}

테스트 통과! 🟢 Green 단계 완료

🔵 Refactor: 코드 개선

여전히 간단해서 리팩토링할 부분이 없습니다.

TDD의 장점

1. 높은 코드 품질

  • 테스트가 보장되어 있어 버그 발생률 감소
  • 리팩토링 시 안전성 확보

2. 명확한 요구사항 정의

  • 테스트를 통해 기능의 명세를 명확히 정의
  • 개발자가 무엇을 구현해야 하는지 정확히 파악

3. 설계 개선

  • 테스트하기 쉬운 코드는 대부분 잘 설계된 코드
  • 의존성 분리, 단일 책임 원칙 등이 자연스럽게 적용

4. 문서화 효과

  • 테스트 코드가 살아있는 문서 역할
  • 코드의 사용법과 예상 동작을 명확히 보여줌

TDD 모범 사례

1. 작은 단위로 시작하기

TDD에서 가장 중요한 원칙 중 하나는 작은 단위로 나누어 개발하는 것입니다.

❌ 잘못된 예: 한 번에 복잡한 기능 테스트

@Test
void 복잡한_계산을_한번에_테스트() {
    Calculator calc = new Calculator();
    // 여러 기능을 한 번에 테스트하려고 시도
    double result = calc.calculate("(10 + 5) * 2 - 3 / 1.5");
    assertEquals(27.0, result);
}

✅ 올바른 예: 작은 단위로 나누어서 테스트

@Test
void 덧셈_테스트() {
    Calculator calc = new Calculator();
    assertEquals(15, calc.add(10, 5));
}

@Test
void 곱셈_테스트() {
    Calculator calc = new Calculator();
    assertEquals(30, calc.multiply(15, 2));
}

@Test
void 뺄셈_테스트() {
    Calculator calc = new Calculator();
    assertEquals(27, calc.subtract(30, 3));
}

왜 작은 단위가 중요한가?

  • 어떤 부분에서 문제가 발생했는지 정확히 파악 가능
  • 테스트 실패 시 빠른 디버깅 가능
  • 각 기능별로 독립적인 개발 가능
  • 단순한 구현부터 시작해서 점진적으로 복잡도 증가

2. 의미 있는 테스트 이름 작성

// ❌ Bad
@Test
void test1() {}

// ✅ Good
@Test
void 음수를_입력하면_예외가_발생한다() {}

3. AAA 패턴 활용

  • Arrange: 테스트 준비 (Given)
  • Act: 실행 (When)
  • Assert: 검증 (Then)

4. 테스트 독립성 유지

  • 각 테스트는 서로 독립적이어야 함
  • 테스트 순서에 의존하지 않도록 작성

TDD의 한계와 주의점

한계점

  • 학습 곡선: 처음에는 개발 속도가 느려질 수 있음
  • 과도한 테스트: 단순한 getter/setter까지 테스트하는 오버엔지니어링
  • UI 테스트의 어려움: 사용자 인터페이스는 TDD 적용이 까다로움

주의사항

  • 테스트를 위한 테스트는 피하기
  • 구현 세부사항이 아닌 동작을 테스트하기
  • 테스트 코드도 유지보수 대상임을 인식하기

실무에서 TDD 적용하기

점진적 도입

  1. 새로운 기능부터 TDD 적용
  2. 버그 수정 시 테스트 먼저 작성
  3. 리팩토링 전 테스트 작성하여 안전망 구축

팀 차원의 도입

  • 코드 리뷰 시 테스트 코드도 함께 검토
  • TDD 교육 및 페어 프로그래밍 활용
  • 테스트 커버리지 목표 설정 (100%보다는 핵심 로직 위주)

마무리

TDD는 단순히 테스트를 먼저 작성하는 것이 아니라, 더 나은 설계와 안정적인 코드를 만들기 위한 개발 방법론입니다. 처음에는 번거로울 수 있지만, 익숙해지면 개발 속도와 코드 품질 모두를 향상시킬 수 있는 강력한 도구가 됩니다.

중요한 것은 완벽한 TDD보다는 점진적인 적용과 지속적인 개선입니다. 작은 기능부터 시작해서 천천히 TDD 문화를 만들어가시길 추천드립니다!


참고자료

0개의 댓글