외부활동/우아한테크코스 [프리코스]

테스트주도 개발 시작하기 - 챕터 2. TDD 시작

softmoca__ 2025. 11. 4. 22:34
목차

https://product.kyobobook.co.kr/detail/S000001248962

 

테스트 주도 개발 시작하기 | 최범균 - 교보문고

테스트 주도 개발 시작하기 | 작동하는 깔끔한 코드를 만드는 데 필요한 습관 - JUnit 5를 이용한 테스트 주도 개발 안내 - 테스트 작성과 설계를 위한 대역 - 테스트 가능한 설계 방법 안내 - 유지

product.kyobobook.co.kr

 

1. TDD 이전의 개발 방식과 문제점

1-1. 전통적인 개발 흐름

  1. 기능 설계부터 시작
    • “어떤 클래스가 필요하지?”, “어떤 인터페이스를 만들까?”, “어떤 메서드를 넣지?” 같은 걸 머릿속/노트에서 한참 고민.
  2. 대략 구현을 떠올린 뒤 한 번에 쭉 코딩
    • 머릿속에서 디자인이 어느 정도 그려지면 코드를 왕창 작성.
  3. 기능 테스트 & 디버깅
    • 구현이 끝났다고 느끼면 실행해 보고, 안 되면 디버깅, 로그 찍고, 브레이크포인트 걸고, 수정 반복.

1-2. 이 방식의 문제점

  • 코드량이 많을수록 디버깅 지옥
    • 한 번에 너무 많은 코드를 작성하면,어디서 잘못됐는지 찾는 데 시간이 오래 걸림.
  • 기능 단위 검증이 아니라 ‘전체 동작’만 보는 테스트
    • 주로 UI나 전체 기능을 통해서만 테스트해서,세부 로직 별로 정확히 검증하기 힘듦.
  • “문제가 나와야 고치는” 후행형 개발
    • 문제는 항상 나중에 발견됨 → 수정 비용, 멘탈 데미지 증가 그래서 “이제는 다른 흐름이 필요하다”고 이야기하고, 그 대안으로 TDD를 소개한다.

2. TDD란? – 정의와 기본 철학

2-1. TDD의 한 줄 정의 “테스트부터 시작하는 개발 방식”

  • 구현을 먼저 작성하고 나중에 테스트하는 게 아니라,
  • 기능을 검증하는 테스트 코드를 먼저 작성하고,
  • 그 테스트를 통과시키기 위해 **필요한 만큼만 구현**한다.

2-2. “테스트가 개발을 이끈다”

  • TDD에서는 테스트가 ‘다음에 무엇을 만들지’ 결정하는 역할을 한다고 강조
  • 테스트 코드가 “이제 이런 상황에서도 이렇게 동작해야 해”라고 규칙을 못 박으면,
    • 우리는 그 테스트를 통과시키는 최소한의 코드만 작성.
    • 이렇게 테스트가 늘어나면서, 구현도 점점 완성되어 감.

3. TDD 기본 흐름 – RED / GREEN / REFACTOR

3-1. 전형적인 TDD 사이클

  1. RED – 실패하는 테스트 작성
    • 아직 구현이 없거나 불완전하니까, 테스트를 먼저 작성하면 당연히 실패하거나 컴파일 에러가 난다.
  2. GREEN – 테스트를 통과시키는 최소 구현
    • 테스트를 통과시키기 위해 필요한 만큼만 코드 작성.
    • “정답스럽게” 짜려고 욕심내지 말고, 일단 초록색(성공) 먼저.
  3. REFACTOR – 설계 개선
    • 테스트가 모두 초록인 상태에서 중복/구조를 정리.
    • 이름 개선, 중복 제거, 메서드 추출, 책임 분리 등.
    • 리팩토링 후에는 테스트를 다시 실행해서 기존 기능이 안 깨졌는지 확인.

이 사이클을 계속 돌리면서 기능을 점진적으로 완성해 가는 것이 TDD

4. 예제: 암호 검사기

“문자열을 검사해서 암호를 약함/보통/강함으로 구분하는 기능을 TDD로 개발하기”.

4-1. 요구사항 정의

암호를 아래 세 가지 규칙으로 검사

  1. 길이가 8글자 이상
  2. 0–9 사이 숫자를 하나 이상 포함
  3. 대문자를 하나 이상 포함

그리고 규칙 충족 개수에 따라 등급을 나눔:

  • 세 가지 규칙을 모두 충족 → “강함(strong)”
  • 두 가지 규칙만 충족 → “보통(normal)”
  • 한 가지 이하 충족 → “약함(weak)”

4-2. TDD 첫 스텝: 가장 쉬운 테스트부터

“첫 테스트는 가장 쉽거나 가장 예외적인 케이스로 시작하라”

  • “모든 규칙을 다 만족하는 암호 → 강함” 같은 직관적인 케이스를 첫 테스트로 잡는 식.
  1. 테스트 클래스부터 만든다.
    • 예: PasswordStrengthMeterTest
  2. 아직 존재하지 않는 클래스/메서드 이름을 고민하면서 바로 테스트 코드 작성
    • new PasswordStrengthMeter().meter("ab12!ABCD") 같은 식으로, 아직 없는 타입/메서드를 테스트에서 먼저 호출
  3. 그러면 당연히 컴파일 에러 발생 →에러를 없앨 만큼만 최소한의 클래스/메서드 뼈대를 만든다.

이 시점에서는 구현이 아니라 “타입과 메서드 모양”만 잡는다는 게 포인트.

4-3. GREEN: 테스트를 통과시키는 최소 구현

첫 테스트를 만들고 나면, 이제 실패/컴파일 에러 상태를 GREEN으로 바꾸는 단계로 넘어간다.

  • 예를 들어, ab12!ABCD가 강함으로 평가돼야 한다면,
    • 테스트에서 assertEquals(PasswordStrength.STRONG, meter.meter("ab12!ABCD"));
    • 구현에서는 아주 단순하게 강함만 반환하는 코드로 시작

이때 중요한 건

  • 처음부터 모든 규칙을 구현하지 않아도 된다.
  • “테스트를 통과하기 위해 필요한 최소한”만 구현하고, 나머지 요구사항은 다음 테스트들에서 조금씩 채워나간다.

4-4. 테스트 추가 & 구현 확장

첫 테스트를 통과시키면, 다음 흐름으로 반복

  1. 두 번째 테스트 추가
    • 예: 한 규칙만 만족하는 암호는 약함이어야 한다.
  2. 테스트 실행 → 실패
    • 기존 구현이 첫 테스트만 통과하게 억지로 짜놨다면, 다른 케이스에서 당연히 깨짐.
  3. 구현을 확장
    • 이제는 “규칙 몇 개를 만족하는지 세는 로직”을 조금 더 일반화.
  4. GREEN 상태가 되면, 필요 시 리팩토링:
    • 규칙 검사 메서드 분리 (길이 검사, 숫자 포함 여부, 대문자 포함 여부 등)
    • 중복 제거, 이름 개선, 조건문 단순화 등.

이 과정을 여러 번 반복하면서, 암호 검사기가 점점 “진짜 요구사항을 만족하는 함수”로 완성된다.

4-5. 예외/에러 케이스도 테스트부터

“에러 케이스도 TDD의 일부로 먼저 다룬다”

  • null이 들어왔을 때 어떻게 할지?
  • 너무 짧은 문자열일 때?
  • 숫자/대문자가 전혀 없는 경우?

이런 케이스들을 테스트로 먼저 명시해 두면 구현에서 그 상황을 처리할지/예외를 던질지 명확해지고, 나중에 코드가 복잡해져도 테스트가 안전망 역할을 해줌.

5. 강조하는 TDD 실천 원칙들

2장 끝부분과 각종 요약 글에서 반복해서 등장하는 핵심 원칙들을 정리해 보면

5-1. 테스트부터 작성하라

  • “Test Case 먼저”
    • 프로덕션 코드(구현 코드)는 테스트가 요구하는 만큼만 작성.
  • 구현 아이디어가 떠올라도 테스트로 먼저 눌러 담고 시작하는 패턴을 몸에 익히는 게 중요.

5-2. 쉬운 케이스부터

  • 첫 테스트는 가장 단순하거나 대표적인 케이스로.
  • 너무 많은 경우를 한 번에 커버하려고 하지 않는다.
  • 이렇게 해야 한 스텝에서 변경하는 코드량이 적어져서 디버깅이 쉬움.

5-3. 한 번에 개발하는 양을 줄여라

  • “한 번에 큰 기능을 완성하겠다”는 생각 대신, 테스트 하나를 만족시키는 최소 단위 변경”에 집중.
  • 작은 스텝일수록:
    • 실패 지점이 명확해지고,
    • 회귀 버그를 찾기가 쉬워지고,
    • 설계 개선도 반복적으로 할 수 있음.

5-4. 에러 케이스를 충분히 고려하라

  • 성공 케이스뿐 아니라 실패/경계 상황을 테스트로 명시.
  • 암호 검사기처럼 규칙이 있는 기능에서는 특히
    • 경계값(길이 7 vs 8),
    • 한 규칙만 충족 / 두 규칙만 충족 같은 경우를 테스트로 꼭 넣자.

5-5. 테스트 코드도 코드다 (가독성 중요)

  • 테스트도 결국 유지보수 대상이기 때문에 중복 제거, 의미 있는 메서드 이름, 명확한Arrange/Act/Assert 구조를 갖추는 게 좋다.
  • “테스트가 복잡해서 이해가 안 되면, 그 테스트로 보호받는 느낌이 안 난다”는 메시지도 담겨 있다.

5-6. src/test/java에서 먼저 작업하고, 나중에 main으로 옮기기

  • 처음에는 테스트 소스 폴더에서만 클래스/메서드를 만들고, 나중에 충분히 안정화되면 main으로 옮긴다
  • 이렇게 하면 “미완성 프로덕션 코드가 빌드/배포에 섞이는 것”을 피할 수 있고, 실험적인 설계를 테스트 영역에서 마음껏 해볼 수 있음.

핵심 정리

  1. 빠른 피드백
    • 코드를 조금 바꾸고 바로 테스트를 돌려 확인 →잘못된 코드가 배포 이전에 바로 걸러짐.
  2. 회귀 버그 방지
    • 새로운 기능을 추가할 때마다 이전 테스트들이 안전망 역할.
  3. 설계 개선 유도
    • 테스트하기 어렵게 설계된 코드는 자연스럽게 리팩토링하게 됨.
    • 규칙 검사 로직 분리, 책임 나누기 같은 것들이 자연스럽게 따라옴.
  4. 심리적 안정감
    • “내가 한 수정이 다른 부분을 깨뜨릴까?” 하는 불안이 줄어듦.

TDD 이전 방식의 문제 → 한 번에 많은 코드 작성, 늦은 피드백, 디버깅 지옥

  • TDD 정의 → 테스트 먼저, 구현은 “테스트를 통과할 정도로만”, 그 뒤 리팩토링
  • RED – GREEN – REFACTOR → 실패하는 테스트 → 최소 구현으로 통과 → 설계 개선
  • 암호 검사기 예제 → 규칙 3개(길이·숫자·대문자) + 강/보통/약 구분을 TDD로 점진 구현
  • 실천 원칙
    • 테스트 케이스부터
    • 쉬운 케이스부터
    • 작은 단위로 개발
    • 에러/경계 케이스까지 테스트
    • 테스트 코드도 가독성/중복 제거 신경