선착순 쿠폰 발급기 개발

반응형
 

당신의 동시성 테스트가 원하는 결과가 나오지 않는 이유

TL;DR: 낙관락과 비관락을 고르는 기준에 대해 설명합니다.배경그 전에도 낙관락, 비관락을 해봤기때문에 금방할 줄 알았다.하지만 아니었다. 어디서 부터 문제 였을까?생각하기에는 비관락을 사

b-programmer.tistory.com

그때는 낙관락과 비관락에 대해서만 집중해서 테스트를 진행했던 거 같다.
이번에는 차근차근 진행해 볼 예정이다. 낙관락도 비관락도 무엇인지 정확하게 알았으니 다른 테스트도 해도 크게 문제 될 건 없을 거 같다.

일단 쿠폰을 한 장 준비해 둡니다. 이 쿠폰은 100장 제한이 걸려있으며, 불특정 한 사람들이 이 쿠폰 이용이 가능합니다.
먼저 k6 스크립트를 준비해 주고 테스트를 진행해 봅니다.

락을 걸기 전에

현재 선착순 쿠폰을 구하는 api는 다음과 같습니다.

{
  "userId": 1
}

userId를 바꿔가면서 데이터를 넣어야 한다는 뜻입니다. 저는 겹치지 않게 userId를 100개 준비하였습니다.

초기 상태는 쿠폰이 100개가 남아있는 상태입니다. 이제 100명을 대상으로 테스트를 진행해 봅시다.

정상적으로 테스트가 완료가 되었습니다. 하지만 알 수 없는 오류가 발생하여 장상적인 결과가 나오지 않았습니다.

WARN[0000] [issue] failed status=500, body={"success":false,"data":null,"message":"일시적인 오류가 발생했습니다."}  source=console

6장뿐만 성공했다고 나오고 나머지는 실패를 하였습니다. 생각보다 성공률이 높지는 않네요.
이제 stock을 확인해 봅시다.

이상합니다. 역시 6장이 성공했으면 94가 되어야 하는 게 아닐까요? 2장은 어디로 간 걸까요?
그 이유는 쿠폰 2장에 대해 데드락이 발생이 하였다고 합니다. 이렇게 남은 수량은 신뢰도를 떨어뜨릴 수밖에 없습니다.

그렇다면 어떻게 해야 100장을 요청하면 100장이 모두 소진되게 할 수 있을까요?

비관 락 사용

초기상태로 되돌린 다음 테스트를 다시 진행하겠습니다. 똑같이 쿠폰아이디 1번으로 진행하였습니다.

이번에는 모든 쿠폰이 발급이 되었다는 것을 확인할 수 있었습니다.
DB락에는 2가지 방법이 존재합니다.
낙관락과 비관락입니다. 저는 비관락을 선택하여 사용했습니다. 그 이유는 모든 데이터를 전부 성공을 시켜야 하기 때문입니다.
그렇다는 뜻은, 하나의 작업이 실행 중이라면, 나머지 작업들은 실행을 시키면 안 되기 때문입니다. 만약, 낙관락을 여기에서 사용한다면,
하나만 성공하고 나머지는 실패하는 전략이기 때문에 여기에는 비관락을 선택을 하겠습니다.

쿠폰이 100장을 100명에게 성공한 것을 확인하였습니다.

비관락은 제대로 걸린 거 같습니다. 이제 몇 가지 테스트를 더 진행해보려고 합니다.

추가 테스트

그렇다면 다음과 같은 상황에서도 똑같은 결과를 리턴할 수 있을까요? 
1. 쿠폰이 100장이 아닌 10000장인 상황
2. 선착순 쿠폰 10장을 5000명이 발급을 시도하는 상황

바로 진행해 봅시다.

쿠폰이 100장이 아닌 10000장인 상황

  - 목적: 락 걸린 상태에서 처리량/지연(p95/p99) 확인
  - 기대: 성공률 높음, 대신 동시성 높이면 지연 증가 가능

이 상황은 성능 테스트를 뜻합니다.

이번에는 만장을 준비해 줍니다. 그리고 테스트를 다시 한번 더 돌려봅시다.

예상과 다르게 타임아웃이 발생하였다는 것을 확인할 수 있었습니다. 초반 테스트처럼 쿠폰 100장이 일부만 성공했던 그림과는 전혀 다른 그림이었습니다.

만장모두 종료되었다는 것을 알 수 있습니다.

그렇다면, 결과는 어떨까요?

확인해 보니   http_req_duration은 다음과 같다고 합니다.

- avg: 6.91초
- p95: 15.49초
- p99: null
- max: 16.29초

이 결과의 의미

이번 테스트에서는 총 10,000건의 쿠폰 발급 요청 중 7,629건이 성공하고 2,371건이 실패했습니다.
그럼에도 불구하고 초과 발급은 발생하지 않았고, 초기 재고 10,000장에서 성공 건수 7,629건만큼만 차감되어 최종 재고가 2,371장으로 정확히 일치했습니다.

즉, 성공 건수와 재고 감소량이 정확히 일치했고 oversold가 발생하지 않았으며 409(중복 발급), 400(잘못된 요청/발급 불가) 도 없었습니다. 이 점을 보면 쿠폰 발급 로직 자체는 동시성 상황에서도 재고 정합성을 안정적으로 유지했다고 볼 수 있습니다.
반면, 실패한 2,371건은 모두 failureOther로 집계되었습니다.
이는 비즈니스 규칙 위반이 아니라, 500 응답, 타임아웃, 커넥션 문제, 서버 처리 한계 초과 같은 시스템 과부하성 실패일 가능성이 높다는 뜻입니다.

따라서 이 결과는 다음을 의미합니다.

  1. 쿠폰 발급 로직은 동시성 상황에서도 재고 정합성을 잘 지킨다.
  2. 하지만 10,000 VU 수준의 순간 스파이크 부하는 현재 시스템이 안정적으로 감당하지 못한다.
  3. 결과적으로 일부 요청은 성공했지만, 상당수는 애플리케이션/인프라 병목으로 인해 실패했다.

한 줄 결론

정합성은 통과했지만, 10,000 VU 스파이크 상황에서 성능과 안정성은 실패했습니다.

성능과 안전성을 올리면서 어떻게 해결할 수 있을까요? 

선착순 쿠폰 10장을 5000명이 발급을 시도하는 상황

  - 목적: 정합성(초과 발급 0) 검증
  - 기대: 성공은 정확히 10건 근처, 나머지는 소진/중복 실패
  - 핵심 체크: issuedQuantity=10, 발급 row도 10, 초과발급 없음

이 상황은 정합성/경합 테스트를 뜻합니다.

10장이 만들어졌습니다.
예상외의 결과가 나왔습니다. 의외로 나쁘지 않게 나와 당황스러운대요.

다만 조금 이상한 부분이 존재합니다. 바로 failOther부분인데요.
이 부분은 409 말고 다른 실패라서, 약간의 시스템성 실패(500/timeout 등)가 섞여 있을 가능성이 있습니다.

즉 failureOther가 잡혔다는 건, 비즈니스적으로 의도된 탈락(재고 소진/중복 등)이 아니라 정상적이지 않은 추가 실패가 섞였다는 뜻으로 보는 게 맞습니다.

한 줄 결론

이 결과가 의미하는 것은 다음과 같습니다.

  1. 쿠폰 발급 로직은 극심한 경쟁 상황에서도 초과 발급 없이 정확히 동작했다.
  2. 대부분의 실패는 의도된 경쟁 탈락(409)으로 처리되었다.
  3. 하지만 일부 요청은 시스템 과부하성 실패(failureOther)로 떨어져 완전한 안정성 확보에는 미치지 못했다.

이 두 가지 테스트를 통해 확인할 수 있었던 고무적인 점은, 쿠폰 재고가 음수로 내려가는 초과 발급 현상이 발생하지 않았다는 것입니다. 이는 동시성 제어가 의도대로 동작했다는 의미이며, 비관적 락 적용이 정합성 보장에 유효했을 가능성을 보여줍니다. 반면, 초과 발급은 방지했지만 여전히 일부 요청에서 실패가 발생했고 응답 시간도 크게 증가한 것으로 보아, 정합성 문제는 해결되었더라도 성능 및 안정성 이슈는 여전히 남아있다고 판단할 수 있습니다. 

어떻게 하면 해결할 수 있을까요?

반응형

댓글

Designed by JB FACTORY