๑ `⌃´ ๑
이미 사용된 쿠폰은 사용할 수 없다.

 

이런 요구사항이 있다고 예를 들자.

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 / 담당자에게 질문 → 규칙 확정 → 테스트와 구현 의 과정을 거쳐야 한다.

따라서 요구사항 분석은 단순히 문서를 읽고 코딩하는 과정이 아니라, 문서에서 빠진 의사결정을 찾아내는 과정이기도 하다.

 

🎵 Playlist
loading...
00:00 / 00:00