재고가 0인데도 대기열을 닫지 못했다: 매진 판정의 조건을 다시 세운 이유
목차
한정 상품 구매 API 앞에 대기열을 두면 매진된 뒤에도 결제 단계까지 들어가는 요청을 줄일 수 있습니다. 그래서 재고가 0이 되면 입장을 닫도록 만들었습니다.
그런데 헛걸음은 140건 그대로였고, 매진 판정 300번 중 대기열을 닫은 횟수는 0번이었습니다. 재고가 0이어도 결제 결과를 기다리는 예약이 남아 있으면 나중에 재고가 돌아올 수 있기 때문입니다. 이번 부하에서는 그 조건이 한 번도 동시에 성립하지 않았습니다.
이 글은 대기열 구현기가 아니라, 매진을 언제 확정할 수 있는지 다시 정의한 기록입니다. 안전하게 닫을 수 있는 조건은 찾았지만, 그 조건이 실제 부하에서 얼마나 자주 성립하는지는 별도의 문제였습니다.
대기열이 풀어야 할 문제
재고 100개인 한정 상품에 구매자 300명이 몰리는 부하를 기준으로 삼았습니다. 구매자의 20%는 중간에 떠나고 결제의 약 10%는 PG 결과를 모르는 UNKNOWN이 됩니다. 대기열은 결제 API에 들어갈 수 있는 인원을 제한하고, 입장 칸이 만료되면 다음 구매자에게 자리를 넘깁니다.
입장 인원과 입장 칸 만료 시간을 둔 대기열을 넣자 먼저 온 100명 중 산 사람이 50명에서 88명(입장 50명 · 만료 30초)으로 늘었습니다. 하지만 판매 성공률이 좋아진 것과 매진 뒤 헛걸음을 줄인 것은 같은 효과가 아니었습니다.
헛걸음은 결제 단계까지 갔다가 매진 응답을 받은 경우로 정의했습니다. 대기열이 재고 상태를 모르면 매진 뒤에도 사람을 계속 들여보낼 수 있습니다. 이 조건에서 헛걸음은 대기열이 없을 때와 같은 140건이었습니다.
따라서 측정 질문을 둘로 나눴습니다.
- 대기열이 판매 성공률을 높이는가?
- 매진이 확정된 뒤 입장을 닫아 헛걸음을 줄이는가?
매진을 언제 확정할 수 있는가
재고만 0이면 닫는 것이 가장 단순합니다. 그런데 UNKNOWN 결제가 잡아 둔 예약은 복구 배치가 결과를 확인한 뒤 풀립니다. 결제가 실패였으면 재고가 돌아옵니다. 재고만 보고 닫으면 돌아올 재고를 살 수 있던 사람까지 매진이라고 돌려보냅니다. 측정 전에 “팔지 못한 재고가 늘지 않는다”를 기준으로 적었으니 이 방식은 기준을 어길 수 있었습니다.
그래서 재고가 0이고 결과를 기다리는 예약도 0일 때 매진으로 봤습니다. 이 조건이면 나중에 되돌아올 재고를 기다리는 구매자를 성급하게 막지 않으면서, 더 이상 들어올 수량이 없다는 판단도 내릴 수 있습니다.
한 번 닫은 상태를 저장하는 방식도 검토했지만 재고가 돌아왔을 때 다시 여는 로직이 필요합니다. 순번을 묻는 요청마다 실제 재고를 확인하면 다음 요청이 그대로 다시 열지 확인하는 비용도 생기므로, 아직 입장 차례가 오지 않은 구매자에게는 매진 판정을 묻지 않고 입장 인원 규모 안에서만 확인했습니다.
모듈 경계도 하나 걸렸습니다. 매진 판정에 필요한 재고와 예약은 주문 모듈에 있고 대기열 모듈은 어떤 모듈에도 기대지 않게 정해 뒀습니다. 대기열이 주문을 부르면 의존이 거꾸로 서서 순환이 생깁니다. 대기열 모듈이 매진 판정 인터페이스를 갖고 주문 모듈이 구현해 넣게 했습니다. 의존 방향은 원래대로 주문에서 대기열로 향합니다.
측정 전에 정한 성공 기준
같은 부하에서 세 가지를 봤습니다.
- 헛걸음이 대기열 없음(140건)의 절반 이하
- 팔지 못한 재고가 늘지 않음
- 초과 판매 0
판매 성공률은 올랐지만 헛걸음은 줄지 않았다
대기열의 입장 인원과 만료 시간을 바꾼 네 조건을 매진 닫기를 넣기 전과 뒤로 쟀습니다.
| 조건 | 헛걸음 넣기 전 | 헛걸음 넣은 뒤 | 판매 넣은 뒤 | 팔지 못한 재고 전 · 뒤 |
|---|---|---|---|---|
| 대기열 없음 | 140 | 140 | 93 | 7 · 7 |
| 입장 20 · 만료 10초 | 68 | 64 | 98 | 6 · 2 |
| 입장 20 · 만료 30초 | 135 | 135 | 99 | 0 · 1 |
| 입장 50 · 만료 30초 | 140 | 140 | 95 | 5 · 5 |
기준 1은 어느 조건에서도 채우지 못했습니다. 입장 20 · 만료 10초가 낮은 것은 넣기 전에도 그랬습니다. 넣기 전 측정에서 입장 칸을 10초 만에 잃어 결제까지 가지 못한 사람이 72명이었습니다. 기준 2와 3은 통과했습니다.
이번 측정은 같은 노트북에서 다른 부하 스크립트가 도는 동안 진행됐습니다. 판정은 CPU 경합에 둔감한 건수로 했습니다. 대기 시간 p95는 넣기 전 측정과 1초 안에서 같게 나왔습니다(입장 50 · 만료 30초에서 57.4초 대 58.2초).
대기열이 한 번도 닫히지 않은 이유
헛걸음이 그대로인 이유는 둘로 갈릴 수 있었습니다. 판정은 섰는데 닫지 못한 것인지, 애초에 재고가 0이 된 적이 없는 것인지입니다. 결과 건수만으로는 둘이 같은 모양이라, 매진 판정에 카운터 셋을 달고 입장 50 · 만료 30초로 다시 돌렸습니다.
| 카운터 | 값 |
|---|---|
| 입장 차례가 온 사람이 매진 여부를 물은 횟수 | 300 |
| 재고는 0이지만 결과를 기다리는 예약이 남아 닫지 못한 횟수 | 151 |
| 매진으로 닫은 횟수 | 0 |
재고는 0이 됐지만 “예약도 0”이 한 번도 서지 않았습니다. 이 부하의 구매자는 한 번만 시도하고 UNKNOWN 결제는 복구 배치가 나중에 확정합니다. 재고가 0이 되는 순간에는 방금 그 재고를 잡은 예약이 거의 항상 아직 결과를 기다리고 있었습니다. 그 예약이 나중에 풀려 재고가 돌아와도 그때는 다시 시도할 사람이 대기열에 없었습니다. 팔지 못한 재고 5개가 이 경로입니다. 헛걸음은 재고가 “0이지만 풀리지 않은 예약이 있는” 동안 동시에 몰린 결제가 즉시 매진 응답을 받으며 다 소진됐습니다.
재고를 20개로 줄여 수요를 공급의 12배로 만들어 다시 재도 같았습니다. 대기열 없음과 입장 50 · 만료 30초가 판매 19 · 헛걸음 220으로 똑같았습니다.
이 조건을 채택한 이유와 확인하지 못한 범위
안전 기준 둘은 통과했고 기존 테스트도 모두 통과했습니다. 그래서 초과 판매를 막는 조건은 적용했지만, 헛걸음 감소 효과를 확인했다고 쓰지는 않았습니다. 구매자가 다시 시도하거나 UNKNOWN 비율이 낮은 환경에서는 “재고 0 + 예약 0”이 더 자주 성립할 수 있지만, 이번 부하에서는 그 조건을 별도로 만들지 않았습니다.
- 예약이 남아 있어도 재고만 0이면 닫는 방식으로 바꾸지 않았습니다. 헛걸음은 줄겠지만 돌아올 재고를 살 사람을 돌려보내 기준 2를 어길 수 있습니다
- 다시 시도하는 구매자로는 재지 않았습니다. 이 기능이 효과를 낼 조건을 따로 만들어 확인하는 일은 다음으로 남겼습니다
- 복구 배치가 예약을 푸는 주기를 줄이는 방향은 범위 밖으로 뒀습니다
이 측정에서 얻은 결론
- 대기열은 먼저 결제에 들어갈 구매자를 조절해 판매 성공률을 50명에서 88명으로 높였지만, 매진 뒤 헛걸음 140건은 줄이지 못했습니다.
재고 0만으로는 매진을 확정할 수 없습니다.UNKNOWN결제가 잡은 예약이 나중에 풀리면 재고가 돌아올 수 있기 때문입니다.- 이번 부하에서는
재고 0 + 예약 0이 한 번도 성립하지 않았습니다. 매진 판정 요청 수와 닫지 못한 이유를 함께 기록해야 “기능이 작동하지 않았다”와 “안전 조건이 만들어지지 않았다”를 구분할 수 있습니다.
댓글
댓글 수정/삭제는 GitHub Discussions에서 가능합니다.