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

테스트주도 개발 시작하기 - 챕터 3. 테스트 코드 작성 순서

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

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

 

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

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

product.kyobobook.co.kr

 

1. 테스트 코드 작성 순서 – 기본 원칙

1-1. 암호 강도 측정 예제에서의 실제 순서

  1. 모든 규칙을 충족하는 암호 → 강함
  2. 길이만 8 미만이고 나머지는 만족 → 보통
  3. 숫자를 포함하지 않고 나머지는 만족 → 보통
  4. 값이 없는 암호(null/빈 문자열) → 유효하지 않음
  5. 대문자를 포함하지 않고 나머지는 만족하는 경우
  6. ‘길이 규칙만’ 충족하는 경우
  7. ‘숫자 규칙만’ 충족하는 경우
  8. ‘대문자 규칙만’ 충족하는 경우
  9. 아무 규칙도 충족하지 않는 경우

이 순서는 임의로 만든 게 아니라, 두 가지 기준을 따른 결과

1-2. 두 가지 핵심 규칙

  1. 쉬운 경우 → 어려운 경우 순서
  2. 예외적인 경우 → 정상적인 경우 순서
  • 너무 꼬인 조합은 나중에, 처음엔 “명확하고 직관적인 케이스”부터,
  • 예외적인 상황(널, 빈 값 등)을 비교적 앞쪽에 가져와서 다룬다는 전략

2. 초반에 복잡한 테스트부터 시작하면 안 되는 이유

2-1. 복잡한 테스트의 문제점

예를 들어 암호 강도 측정에서 이렇게 시작한다고 생각해 보자

  1. “대문자 규칙만 충족하는 경우”
  2. “모든 규칙을 충족하는 경우”
  3. “숫자를 제외하고 나머지는 충족하는 경우”

이 순서로 테스트를 만들면:

  • 첫 테스트를 통과시키기 위해 규칙 일부만 구현했다가,
  • 두 번째 테스트에서 한 번에 큰 범위의 구현을 해버리게 되고,
  • 세 번째 테스트가 오면 이미 꼬여 있는 로직에 조건을 덕지덕지 붙이게 된다.

결과:

  • 한 번에 구현해야 하는 코드가 많아짐
  • 테스트 한 개를 통과시키려 할 때마다 바꿔야 할 코드 양이 커짐
  • 그 과정에서 버그가 숨어들기 쉬워짐
  • 디버깅 시간이 늘어 TDD의 장점을 못 느끼게 됨

“초반 테스트가 복잡하면, TDD 사이클이 길어지고 지쳐버린다.”

3. 구현하기 쉬운 테스트부터 시작하기

3-1. 쉬운 테스트의 기준

암호 예제에서 쉬운 테스트는 예를 들면:

  • “모든 조건을 충족하는 경우”
  • “모든 조건을 하나도 충족하지 않는 경우”

왜 쉬울까?

  • 로직이 단순한 방향으로만 흘러간다 (조건문 가지치기가 적음)
  • 구현도 단순:
    • 세 규칙을 모두 만족 → count==3 → STRONG
    • 세 규칙 모두 불만족 → count==0 → WEAK

3-2. 점진적 난이도 상승

  • 한 규칙만 충족하는 경우
  • 두 규칙만 충족하는 경우

핵심

  • 한 테스트를 통과시킨 다음, 그다음으로 “조금 더 어려운” 테스트를 고른다.
  • 그러면 한 번에 바꿔야 하는 코드 양이 늘지 않고, 디버깅 단위가 작아지면서 TDD가 훨씬 수월해진다.

4. 예외 상황을 먼저 테스트해야 하는 이유

4-1. 예외를 나중에 넣으면 생기는 일

예외 처리(널, 경계값, 이상한 입력)는 보통 이런 걸 유발

  • 복잡한 if-else 블록 추가
  • 기존 로직 중간중간에 조건문 끼워 넣기
  • 코드 구조가 뒤집히거나, 같은 조건을 여러 번 검사하는 중복

예외를 나중에 붙이면코드 구조 자체를 갈아엎어야 할 가능성이 커지고, 이미 짜놓은 테스트/구현이 같이 흔들리게 된다.

4-2. 예외를 앞에서 테스트하면 좋은 점

  • 예외 상황을 먼저 테스트해두면, 구현할 때 애초에 예외를 처리하는 구조를 갖고 출발.
  • 그래서 나중에 기능 확장을 할 때 구조를 덜 깨고도 로직을 넣을 수 있다.

5. 완급 조절 – 한 번에 얼마나 구현할 것인가

“한 번에 구현하는 양을 얼마나 가져가야 하는가”에 대한 가이드

5-1. 초보 TDD 단계에서의 3스텝

  1. 정해진 값을 리턴
    • 일단 테스트를 통과시키기 위해, 하드코딩된 값으로만 구현.
  2. 값 비교를 이용해서 정해진 값을 리턴
    • 입력 값이 어떤 조건을 만족하면 그 값을 리턴, 아직 일반화는 불완전해도 좋음.
  3. 테스트를 추가하면서 점점 일반화
    • 규칙 케이스(1개 만족, 2개 만족, 0개 만족…)를 늘려가며 조건문/계산 로직을 점진적으로 일반화.

이 3단계는 “너무 완벽하려고 하지 말고, 작게 쪼개서 가자” 라는 훈련

6. 지속적인 리팩토링 – 언제, 어디까지?

리팩토링은 항상 따라붙어야 한다 !

6-1. 리팩토링의 목적

  • 소프트웨어는 생명 주기가 길수록 변경을 많이 겪는다.
  • 변경이 쉬운 구조를 위해서는 지속적인 구조 개선(리팩토링) 이 필수.
  • TDD는 이 리팩토링을 테스트라는 안전망과 함께 하도록 돕는다.

6-2. 어떤 리팩토링을 언제 할까?

  • 작은 리팩토링 (상수→변수, 변수 이름, 간단한 중복 제거) → 보이자마자 바로 해도 된다.
  • 구조에 영향을 주는 리팩토링 (메서드 추출, 책임 분리 등) → 기능의 큰 흐름이 어느 정도 보이기 시작한 뒤에 한다.
    • 구현 초기에는 전체 흐름이 안 보이기 때문에,너무 일찍 구조를 확정해 버리면 틀린 구조를 고집하게 될 수 있음.
  • 리팩토링은 항상 GREEN 상태에서만.(테스트 전부 통과한 상태에서 구조를 바꾸고, 다시 테스트 돌려 확인)

7. 테스트 작성 순서 연습 – [정기 유료 서비스 만료일 예제]

7-1. 요구사항 요약

예제에서 주어진 규칙은 세 가지:

  1. 서비스를 사용하려면 매달 1만 원을 선불로 납부한다.
  2. 2개월 이상 요금을 한 번에 납부할 수 있다 (2만 원 → 2개월, 3만 원 → 3개월 …).
  3. 10만 원을 납부하면 1년(12개월) 제공한다.

즉, 입력(납부 금액 / 납부일)으로부터 서비스 만료일(LocalDate 등)을 계산하는 기능을 TDD로 만듬

7-2. 테스트 클래스부터 만들기

  • 먼저 “만료일 계산”이라는 맥락이 잘 드러나는 테스트 클래스 이름을 정하고, 테스트 파일만 만들기

7-3. 첫 테스트 – 가장 쉬운 경우

  • “1만 원을 납부하면 정확히 한 달 뒤가 만료일이다.”

이 테스트로부터 자연스럽게:

  • 만료일은 LocalDate 같은 타입으로 표현하는 게 좋은가?
  • 계산할 때 필요한 정보는 최소한 무엇인가? (납부일, 납부액)
  • 계산을 담당할 객체/메서드 시그니처는 어떻게 잡을까?

를 하나씩 결정하게 된다.

7-4. 테스트를 늘려가며 일반화

  1. 1만 원 납부 → 한 달 뒤 만료
  2. 2만 원 납부 → 두 달 뒤 만료
  3. 3만 원 납부 → 세 달 뒤 만료
  4. 10만 원 납부 → 1년 뒤 만료

달이 바뀔 때의 경계 조건도 추가

  • 달의 마지막 날에 납부하면 → 다음 달 마지막 날이 만료일
  • 예: 1/31 + 1개월 = 2월 마지막 날(28/29일)

테스트를 추가할 때마다 구현을 조금씩 확장/일반화하고, 필요시 리팩토링을 수행해서 “돈 → 개월 수 → 만료일” 로직을 깨끗하게 나눈다.

8. 테스트할 목록 정리하기 – 미리 써두기

8-1. 만료일 예제에서의 목록 예시

  • 1만 원 납부하면 한 달 뒤가 만료일
  • 달의 마지막 날에 납부하면 다음 달 마지막 날이 만료일
  • 2만 원 납부하면 2개월 뒤가 만료일
  • 3만 원 납부하면 3개월 뒤가 만료일
  • 10만 원 납부하면 1년 뒤가 만료일

이 목록은 두 가지 용도

  1. 시야 확보 – 앞으로 어떤 케이스를 다뤄야 하는지 감 잡기
  2. 다음 테스트 선택 – “이 중에서 지금 가장 구현하기 쉬운 건 무엇인가?”를 고를 기준

8-2. 하지만 한 번에 다 쓰지 않는다

  • 목록을 적었다고 해서 테스트를 한 번에 다 작성하면 안 됨.
    • 테스트가 동시에 여러 개 깨진 상태가 되면,리팩토링/구현 단위가 커져서 리듬이 망가지기 때문.
  • 올바른 패턴:
    1. 테스트 한 개 작성
    2. 해당 테스트를 통과시키는 최소 구현
    3. 필요하면 리팩토링
    4. 다시 새로운 테스트 선택

이 짧은 사이클을 계속 반복

9. 시작이 안 될 때 / 구현이 막힐 때 대처법

멘탈 관리 팁에 가까운 내용

9-1. 시작이 안 될 때는 “단언부터” 작성

테스트를 쓰려는데, 막상 어디서부터 시작해야 할지 막힐 때 검증 코드(assert)부터 쓰자

  • 아직 기대만료일/실제만료일 변수가 뭔지도 모르지만, “무엇을 비교해야 하는지”부터 적어두면,
  • 만료일을 어떻게 표현할지,
  • 실제 만료일을 계산하는 함수/객체는 어떤 모양이어야 할지
  • 어떤 파라미터가 필요한지를 역으로 떠올리기 쉬워진다

9-2. 구현이 막히면?

구현 중간에 완전히 꼬였다 싶으면 과감하게 코드를 지우고 다시 시작하는 것도 옵션.

  • 다만 “어디서 꼬였는지”를 돌아보기
    • 테스트 작성 순서가 너무 복잡하지 않았는지
    • 쉬운 케이스/예외 케이스부터 갔는지
    • 한 번에 너무 많은 걸 구현하려 한 건 아닌지

이때 되새겨야 할 키워드

  • 쉬운 테스트부터
  • 예외적인 테스트 먼저
  • 완급 조절

핵심정리

  • 테스트 작성 순서
    • 쉬운 → 어려운
    • 예외적인 → 정상적인
  • 초반에 복잡한 테스트 금지
    • 한 번에 많은 코드 구현 → 버그/디버깅 폭증
  • 구현하기 쉬운 테스트부터
    • 대표 케이스, 극단 케이스(모두 만족 / 아무도 만족 X)부터
  • 예외 상황을 초기에
    • 예외를 나중에 붙이면 구조를 뒤집어야 함
  • 완급 조절
    • 정해진 값 리턴 → 조건 비교 → 점진적 일반화
  • 지속적인 리팩토링
    • 작은 건 바로, 구조 바꾸는 건 흐름이 잡힌 뒤
  • 테스트 작성 순서 연습
    • 유료 서비스 만료일 예제로 테스트 목록 만들기 + 순서 조율 연습
  • 막힐 때의 팁
    • 단언부터 작성
    • 순서가 꼬였으면 “쉬운/예외 테스트 + 완급 조절”을 떠올리며 다시 설계