이미 사용된 쿠폰은 사용할 수 없다.
이런 요구사항이 있다고 예를 들자.
if (coupon.isUsed()) {
...
}
바로 위와 같은 코드를 생각하는 것이 아니라,
먼저 이렇게 생각해야 한다.
누가 사용하는가?
어떤 상태여야 사용할 수 있는가?
사용하면 상태가 어떻게 바뀌는가?
어떤 경우에 실패해야 하는가?
실패하면 기존 상태는 어떻게 되어야 하는가?
왜 요구사항을 바로 코드로 옮기면 안될까?
자연어 요구사항은 생각보다 모호하다.
`쿠폰을 사용할 수 있다.` 이 문장만 보고 구현하면 여러 질문이 빠진다.
누가 사용할 수 있지?
본인 쿠폰만 가능한가?
이미 사용한 쿠폰은?
존재하지 않는 쿠폰은?
유효기간은?
사용한 뒤에는 어떤 상태가 되지?
실패했을 때 상태는?
개발자가 이런 것을 정리하지 않고 바로 구현하면 코드 안에서 규칙이 뒤늦게 발견된다.
그러면 코드가 길어지고, 지저분해지며 복잡해진다.
구현 시작
↓
"어? 이미 사용된 쿠폰은?"
↓
if 추가
↓
"다른 사람 쿠폰은?"
↓
if 추가
↓
"쿠폰이 없으면?"
↓
예외 추가
↓
테스트 작성하다 보니 또 빠진 조건 발견
코드가 요구사항 분석 도구가 되어버린다.
TDD에서는 정확히 반대로 흘러간다.
자연어 요구사항 → 규칙 정리 → 테스트 케이스 → 그 다음 구현
요구사항에서 무엇을 찾아야 할까?
요구사항에서 크게 다섯 가지를 찾아보자.
① 주체
② 시작 상태
③ 행동
④ 성공 결과
⑤ 실패 조건
예를 들어 `사용자는 자신이 보유한 미사용 쿠폰을 사용할 수 있다.` 를 분해하면..
① 주체
사용자
② 시작 상태
사용자가 쿠폰을 가지고 있다.
쿠폰은 아직 사용되지 않았다.
③ 행동
쿠폰을 사용한다.
④ 성공 결과
쿠폰이 사용 상태로 변경된다.
⑤ 실패 조건
문장에 직접 모두 적혀 있지는 않지만 요구사항의 반대 조건을 생각할 수 있다.
쿠폰이 존재하지 않는다.
다른 사용자의 쿠폰이다.
이미 사용된 쿠폰이다.
이제 테스트를 만들기 좋은 형태가 됐다.
Given / When / Then 으로 옮겨보자
정상 케이스
Given
- 사용자 A가 쿠폰을 소유하고 있다.
- 쿠폰은 미사용 상태다.
When
- 사용자 A가 쿠폰을 사용한다.
Then
- 쿠폰은 사용 상태가 된다.
이것이 Happy Path, 즉 정상 흐름이다. 하지만 정상 테스트 하나만으로는 부족하다.
실패 조건을 찾는 것이 중요하다
실패 1 - 이미 사용한 쿠폰
Given
- 사용자 A가 쿠폰을 소유하고 있다.
- 쿠폰은 이미 사용된 상태다.
When
- 사용자 A가 다시 쿠폰을 사용하려 한다.
Then
- 쿠폰 사용에 실패한다.
- 쿠폰은 계속 사용 상태다.
실패 2 - 다른 사람의 쿠폰
Given
- 쿠폰의 소유자는 사용자 A다.
When
- 사용자 B가 해당 쿠폰을 사용하려 한다.
Then
- 쿠폰 사용에 실패한다.
- 쿠폰 상태는 변경되지 않는다.
실패 3 - 존재하지 않는 쿠폰
Given
- 요청한 쿠폰이 존재하지 않는다.
When
- 사용자가 쿠폰을 사용하려 한다.
Then
- 쿠폰 사용에 실패한다.
이렇게 되면 코드를 한 줄도 쓰지 않았는데 벌써 꽤 중요한 규칙이 보인다.
모든 실패 조건을 무조건 테스트 해야하나?
상상할 수 있는 모든 조건을 테스트하는 것이 목표는 아니다.
쿠폰 시스템이라고 해서 갑자기 `쿠폰 유효기간`, `쿠폰 최소 주문 금액`, `카테고리 제한`, `브랜드 제한`, `중복 할인`, `등급 제한`까지 만들어내면 안 된다.
재고 예제 분석해보기
`재고가 있으면 1개를 차감할 수 있고, 재고가 없으면 차감할 수 없다.` 라는 요구사항을 생각해보자.
비즈니스 규칙 1
재고가 1개 이상이면 재고 한 개를 차감할 수 있다.
Given
재고가 10개다.
When
재고를 한 개 차감한다.
Then
재고는 9개다.
비즈니스 규칙 2
재고가 0개라면 차감할 수 없다.
Given
재고가 0개다.
When
재고를 한 개 차감하려 한다.
Then
차감에 실패한다.
그리고 재고는 0개로 유지된다.
비즈니스 규칙과 구현 방법을 구분해야 한다
A
재고가 없으면 상품을 판매할 수 없다.
B
`stock <= 0` 이면 `IllegalStateException` 을 던진다.
A는 비즈니스 규칙이고, B는 현재 선택한 구현 방법이다. 나중에는 구현 방법이 바뀔 수도 있기 때문에, 둘을 섞지 않는 게 중요하다.
구현 방법만 바뀌고 비즈니스 요구사항은 바뀌지 않을 수 있기 때문에,
테스트를 만들 때도 먼저 `어떤 행동이 보장되어야 하는가?`를 생각하고
그 다음에 필요한 수준에서 예외 타입 같은 구현 세부사항을 정한다.
요구사항에서 "상태 변화"를 찾아라
`사용자가 쿠폰을 사용한다`라고 읽지 말고, 아래와 같이 생각해보자.
Before
Coupon
used = false
↓
use()
↓
After
Coupon
used = true
즉 업무 시스템에서 자연어 요구사항을 만나면, "이 행동 전후에 무엇이 달라지는가?"를 찾는 습관이 좋다.
실패했을 때는 무엇이 달라지지 않아야 하는가?
쿠폰이 이미 사용된 경우
Before
used = true
↓
다시 사용 시도
↓
실패
↓
After
used = true
재고가 0인 경우
Before
stock = 0
↓
차감 시도
↓
실패
↓
After
stock = 0
예제 1
요구사항 : 사용자는 자신이 보유한 쿠폰을 한 번만 사용할 수 있다.
[정상 케이스]
Given
- 사용자 A는 쿠폰을 가지고 있다.
- 쿠폰은 미사용 상태이다.
When
- 사용자 A는 쿠폰을 사용한다.
Then
- 쿠폰이 사용 상태로 바뀐다.
[실패 케이스 1]
Given
- 사용자는 쿠폰을 가지고 있다.
- 쿠폰은 사용된 상태이다.
When
- 사용자가 쿠폰을 사용한다.
Then
- 쿠폰 사용이 실패한다.
- 쿠폰은 계속 사용 상태로 유지된다.
[실패 케이스 2]
Given
- 쿠폰의 소유자는 사용자 A이다.
- 쿠폰은 미사용 상태이다.
When
- 사용자 B가 쿠폰을 사용한다.
Then
- 쿠폰 사용이 실패한다.
- 쿠폰은 미사용 상태로 유지된다.
예제2
요구사항 : 상품 재고가 있는 경우에만 주문할 수 있다.
비즈니스 규칙: 상품 재고가 1개 이상인 경우에만 주문할 수 있다.
정상 조건: 상품 재고가 1개 이상이다.
실패 조건: 상품 재고가 0개이다.
성공하면 바뀌는 상태: 재고가 1 감소한다.
실패하면 유지되어야 하는 상태: 재고가 기존 값 그대로 유지된다.
[Given / When / Then]
[정상 케이스]
Given
- 상품의 재고가 10개이다.
When
- 상품 1개를 주문한다.
Then
- 주문이 성공한다.
- 상품 재고는 9개가 된다.
[실패 케이스]
Given
- 상품의 재고가 0개이다.
When
- 상품 1개를 주문한다.
Then
- 주문이 실패한다.
- 상품 재고는 0개로 유지된다.
좋은 요구사항 분석 vs 좋지 않은 요구사항 분석
요구사항이 `사용자는 쿠폰을 한 번만 사용할 수 있다`일 경우,
좋지 않은 접근
CouponService 만들고
if문 넣고
Repository 조회하고
예외 클래스 만들고...
더 좋은 접근
"한 번만"이라는 게 무슨 뜻이지?
↓
미사용이면 성공
↓
이미 사용했다면 실패
↓
성공하면 used가 true
↓
실패하면 used 상태는 그대로
실제 개발에서 요구사항이 애매하면?
실무에서는 요구사항 확인 → 기획자 / PO / 담당자에게 질문 → 규칙 확정 → 테스트와 구현 의 과정을 거쳐야 한다.
따라서 요구사항 분석은 단순히 문서를 읽고 코딩하는 과정이 아니라, 문서에서 빠진 의사결정을 찾아내는 과정이기도 하다.
'Spring' 카테고리의 다른 글
| [설계] 도메인 모델링 / 객체의 책임 / 응집도 (0) | 2026.08.24 |
|---|---|
| [TDD] Sequence Diagram / 주문 처리 실행 흐름 (0) | 2026.08.21 |
| [TDD] TDD 맛보기 (0) | 2026.08.20 |
| [TDD] 좋은 테스트란 무엇인가 (0) | 2026.08.19 |
| AOP(Aspect Oriented Programming) (0) | 2022.09.05 |