https://product.kyobobook.co.kr/detail/S000001248962
테스트 주도 개발 시작하기 | 최범균 - 교보문고
테스트 주도 개발 시작하기 | 작동하는 깔끔한 코드를 만드는 데 필요한 습관 - JUnit 5를 이용한 테스트 주도 개발 안내 - 테스트 작성과 설계를 위한 대역 - 테스트 가능한 설계 방법 안내 - 유지
product.kyobobook.co.kr
1. 기능 명세 – 기능은 “입력 + 결과”
1-1. 설계는 기능 명세에서 시작된다
- 기능이 뭔지 모호한 상태에서 설계를 하기 시작하면,
- 클래스/메서드가 엉뚱한 방향으로 생겨나기 쉽고,
- 나중에 요구사항이 구체화되면 설계를 다시 갈아엎어야 함.
그래서 설계 전에 먼저 해야 할 일
- 이 기능의 입력은 무엇인가?
- 이 기능이 끝나면 결과는 어떻게 표현되는가?
를 명확히 하는 것.
1-2. 기능 = 입력(Input) + 결과(Result)
- 입력: 기능을 실행하는 데 필요한 값들
- 결과: 기능 실행 후 시스템이 보여주는 변화
- 로그인 기능
- 입력: 아이디, 암호
- 결과: 성공/실패 여부 (리턴 값, 상태 코드 등)
- 만료일 계산 기능
- 입력: 첫 납부일, 이번 납부일, 납부액
- 결과: 서비스 만료일(LocalDate 같은 것)
이렇게 “입력과 결과”가 분명해질수록 메서드 파라미터가 자연스럽게 정의되고, 리턴 타입/예외/상태 변경이 어떻게 되어야 할지 설계
1-3. 결과는 세 가지 방식으로 드러난다
- 리턴 값
- 예: 로그인 성공 시 LoginResult.SUCCESS, 실패 시 FAILURE
- 예외 발생 (Exception)
- 예: 회원 가입 시 이미 같은 ID가 있으면 DuplicateIdException 던지기
- 상태 변경 (변경된 외부 상태)
- 예: 회원 가입 시 DB에 회원 정보가 추가됨
- 예: 주문 기능 호출 후 주문 테이블에 row 생성
TDD 관점에서 이 세 가지는 전부 테스트로 검증 가능한 “결과”
- 리턴 값 → assertEquals, assertThat 등
- 예외 → assertThrows
- 상태 변경 → DB 조회, 리포지토리 대역 사용 등으로 검증
2. 설계 과정을 지원하는 TDD
2-1. 테스트를 쓰면서 “자연스럽게” 설계를 한다
TDD 자체가 설계는 아니지만, 테스트 코드를 작성하는 과정에서 일부 설계를 진행하게 된다.
테스트를 작성하려면 최소한 이런 것들이 필요
- 무엇을 호출할지 (실행)
- 클래스/함수 이름
- 메서드 이름
- 파라미터(타입, 개수)
- 무엇을 검증할지 (결과)
- 리턴 타입
- 던져지는 예외의 타입
- 변경된 상태(객체의 필드값, DB 상태 등)
이걸 정하는 행위 자체가 곧 설계 활동
@Test
void 만원_납부하면_한달_뒤가_만료일() {
LocalDate billingDate = LocalDate.of(2024, 3, 1);
int payAmount = 10_000;
LocalDate expiryDate = calculator.calculateExpiryDate(billingDate, payAmount);
assertEquals(LocalDate.of(2024, 4, 1), expiryDate);
}
이 테스트를 쓰는 순간 이미 설계의 일부가 결정
- 클래스 이름: ExpiryDateCalculator
- 메서드 이름: calculateExpiryDate
- 파라미터: (LocalDate billingDate, int payAmount)
- 리턴 타입: LocalDate
이건 “고민 없이 아무렇게나” 결정하는 게 아니라, 기능 명세(입력/결과)에 기반해 설계된 인터페이스.
2-2. 좋은 설계의 핵심: 이름
설계 과정에서 기능을 정확하게 표현하는 이름을 사용하는 것이 매우 중요하다. 이름 고민하는 시간을 아까워하지 말자.
TDD를 하면:
- 테스트 메서드 이름 → 기능의 의도가 드러나야 하고,
- 테스트 대상 클래스/메서드 이름 → 기능이 무엇을 하는지 바로 보여야 하며,
- 파라미터 이름/타입 → 어떤 입력이 필요한지 분명히 드러나야 한다.
그래서 테스트를 쓰는 행위 =
- “이 기능을 뭐라고 부를지”
- “무슨 인자를 받아야 자연스러운지”
- “무엇을 돌려줘야 이해하기 쉬운지”
를 계속 다듬는 과정 → 곧 설계.
3. 필요한 만큼 설계하기 (YAGNI 느낌)
3-1. “미리” 설계를 다 해두지 않는다
- TDD는 “테스트를 통과시킬 만큼만 코드와 설계를 만든다.”
- 미래 요구사항을 상상해서 미리 인터페이스/추상화/계층을 마구 쌓아놓지 않는다.
- 나중에 실제로 필요해졌을 때, 테스트를 기반으로 리팩토링하면서 설계를 확장한다.
3-2. 만료일 계산 예제에서의 설계 진화
- 첫 번째 테스트
- 파라미터: 납부일(LocalDate billingDate), 납부액(int payAmount)
- 아주 간단한 시그니처로 시작
- 두 번째 테스트를 추가하면서
- “첫 납부일(firstBillingDate)”도 만료일 계산에 영향을 준다는 요구가 발견
- 이때 새로운 타입(예: PayData)을 도입해서 납부일/첫 납부일/납부액을 한 번에 전달하도록 변경
즉, 처음부터 PayData 같은 복잡한 타입을 만들지 않고
- 우선 “현재 테스트”를 만족하는 최소 설계를 쓰고,
- 추후 추가 테스트에서 진짜 필요해질 때 리팩토링으로 타입을 도입하는 흐름
이런 방식으로 설계가 “필요 이상으로 복잡해지는 것”을 막고,실제 요구사항이 드러나는 순서에 맞춰 설계를 확장
3-3. 예외 타입도 필요한 시점에 도입
- “언젠가 쓸 것 같으니” DuplicateIdException, InvalidPasswordException 같은 걸 미리 다 정의하지 않는다.
- 정말로 그 예외를 검증하는 테스트를 추가하는 시점에 그때 실제 예외 타입을 정의하고,테스트로 그 예외 발생을 검증한다.
즉 “혹시 필요할지도 몰라서 미리 만들어두는 클래스/예외/계층”을 줄이고,테스트가 필요성을 증명할 때 설계를 추가하는 방식.
4. 기능 명세 구체화 – 애매함을 예시로 정리하기
4-1. 요구사항 문서 → 기능 명세로 바꾸기
현실 세계에서 요구사항은 보통 이렇게 전달
- 기획서(문서)
- 스토리보드
- 와이어프레임
- 화면 스케치, 회의 노트 등
이 문서들은 대부분 구체적인 ‘예’가 부족하고, 언어적으로도 모호한 표현이 많다.
그래서 개발자는 TDD를 하기 전에
- 이 문서들을 읽고,
- 각 기능마다 입력 / 결과를 뽑아내고,
- 거기서 테스트에 넣을 수 있는 구체적인 예를 만들어야 함
이게 4장이 말하는 “기능 명세 구체화”의 출발점
4-2. 애매한 요구사항을 “예”로 바꾸기
예를 들어, 만료일 계산 기능 요구사항이 이렇게 되어 있다
“납부일 기준으로 한 달 뒤가 서비스 만료일이다.”
이 문장만 보면 애매한 점
- 4월 1일에 납부 → 만료일은 4월 30일인가, 5월 1일인가?
- 1월 31일에 납부 → 2월은 28일/29일까지밖에 없는데 만료일은?
- 윤년(2월 29일) 케이스는 어떻게 할 건가?
4장은 이런 애매함을 만나면
- 기획자/PO와 대화해서 규칙을 구체화한다.
- 결정된 규칙을 테스트용 예시로 만든다.
- 그 예시를 테스트 코드에 넣는다.
이 과정을 거치면
- 문장으로 표현된 기능 명세가 실행 가능한 함수 호출 + 기대 리턴 값으로 바뀌고, 더 이상 애매하지 않은 “구체적인 규칙”이 된다.
4-3. 테스트 코드는 “예를 이용한 구체적인 명세”
- 테스트 코드는 예시(example)를 이용한 구체적인 명세다.
- 모호한 표현(“한 달 뒤”, “약간 할인”, “적당히 강력한 비밀번호”)을 정확한 입력 값과 기대 결과로 바꿔서 코드로 적어둔 것.
- 이 예시 기반 명세 덕분에 개발자는 오해 없이 기능을 구현할 수 있고, 나중에 누군가 코드나 기능 동작을 이해하고 싶을 때 해당 상황을 검증하는 테스트를 실행해 보면 된다
5. 개발자에게 기능 명세가 필수인 이유
- 복잡한 로직을 구현해야 하는 건 결국 개발자
- 기획 문서가 아무리 잘 되어 있어도, 경계 조건, 예외 상황, 조합 케이스 등은 개발자가 직접 캐치해서 테스트로 만들어야 한다.
- 예외적인 상황에 대한 예를 적극적으로 찾아야 한다
- 단순 성공 케이스뿐 아니라 “이상한 입력 / 극단 값 / 경계 값”에 대한 예를 찾아 테스트로 명세해두는 습관이 중요.
- 테스트 코드는 유지보수에 큰 도움
- 특정 상황에서 코드가 어떻게 동작하는지 궁금하면 해당 상황을 다루는 테스트를 찾아 실행해 보고 필요하면 테스트를 추가로 작성해서 원하는 동작을 못 박을 수 있음.
핵심요약
- 기능 = 입력 + 결과
- 입력 → 메서드 파라미터로 전달되는 값들
- 결과 → 리턴 값 / 예외 / 상태 변경(예: DB 변경)
- 설계는 기능 명세에서 시작
- 기능의 입력/결과가 정리되어야 클래스/메서드/파라미터/리턴 타입 같은 설계가 가능해진다.
- TDD는 설계를 ‘지원’한다
- 테스트 코드를 작성하려면:
- 무엇을 호출할지(클래스/메서드 이름, 파라미터)
- 무엇을 검증할지(리턴 타입, 예외, 상태 변경)
- 를 먼저 정해야 하고, 이 결정 과정이 곧 설계 활동이다.
- 테스트 코드를 작성하려면:
- 필요한 만큼만 설계
- TDD는 “테스트를 통과할 만큼”만 코드/설계를 만든다.
- 미래를 상상해서 미리 추상화/확장해 두지 않는다.
- 실제 필요 지점(새 테스트 추가 시점)에 맞춰 타입/예외/구조를 도입한다.
- 기능 명세 구체화 = 애매함을 예시로 치환
- 요구사항 문서의 애매한 문장을
- 기획자/실무자와 대화 → 구체 예시 도출 → 테스트 코드로 기록
- 이렇게 테스트 코드는 “예를 이용한 실행 가능한 명세”가 된다.
'외부활동 > 우아한테크코스 [프리코스]' 카테고리의 다른 글
| 테스트주도 개발 시작하기 - 챕터 6. 테스트 코드 구성 (0) | 2025.11.05 |
|---|---|
| 테스트주도 개발 시작하기 - 챕터 5. JUnit (0) | 2025.11.05 |
| 테스트주도 개발 시작하기 - 챕터 3. 테스트 코드 작성 순서 (0) | 2025.11.04 |
| 테스트주도 개발 시작하기 - 챕터 2. TDD 시작 (0) | 2025.11.04 |
| [우아한테크코스 8기] 프리코스 3주차 회고 (0) | 2025.11.04 |