마케팅 이벤트 고부하 대응 PRD

Background

ticketing 도메인은 Seat 락(SeatLockStore, SET NX PX 기반 분산락으로 추정되는 좌석 단위 선점)을 통한 동시성 제어를 이미 갖추고 있으며, qa/load/k6/ticket-seat-select-spike.js 스파이크 부하 시나리오도 존재한다. goods 도메인도 이미 상당한 동시성 안전장치를 갖추고 있다 — GoodsDomainService.createPendingOrder()idempotencyKey로 중복 주문을 멱등 처리하고, 주문 생성 시점에 validateAndDeductStock()을 호출해 Stock.deduct()로 재고를 차감한다. Stock 엔티티는 @Version 낙관적 락(optimistic lock)을 가지고 있어 동시 갱신 충돌을 감지·재시도할 수 있는 구조가 이미 존재한다. 다만 이 메커니즘은 판매 시작 시각 게이트(등록 즉시 누구나 구매 가능하며 “특정 시각부터 선착순”이라는 개념이 없음)가 없고, 20000TPS 수준의 고부하 상황에서 검증된 적이 없다는 두 가지가 실제 공백이다. 최근 커밋(a4fd4958)으로 장바구니(Cart) 동시 추가 경합은 멱등 병합으로 개선됐으나, 이는 “카트에 담기” 단계의 문제이며 주문 생성 단계의 재고 차감과는 별개다.

요구사항의 “마케팅 이벤트(티케팅, 한정판 상품) 시 20000TPS 수준 트래픽”을 실제로 감당하려면, 기존 Stock.@Version 낙관적 락·idempotencyKey 멱등 처리 위에 판매 시작 시각 게이트를 추가하고, 고부하 상황에서 오버셀 0건을 실증해야 한다.

Problem Definition

  • goods에는 판매 시작 시각 게이트가 없어 상품 등록 즉시 누구나 구매할 수 있다 — “특정 시각부터 선착순 판매”라는 마케팅 이벤트 요구를 표현할 방법이 없다.
  • 재고 차감 자체는 Stock.@Version 낙관적 락으로 원자성이 구조적으로 보장되나, 20000TPS 규모의 동시 다발 요청에서 낙관적 락 충돌·재시도가 감당 가능한 수준인지(재시도 폭주로 인한 지연 증가, 최종 실패율)가 검증된 적이 없다.
  • “구매 성공”의 판정 시점이 명시적으로 정의된 적이 없다 — createPendingOrder()는 주문을 PENDING 상태로 생성하면서 재고를 이미 차감하고(선점), 결제 확정(markPaid())은 이후 별도 단계다. 오버셀 판정·응답시간 측정 대상 구간이 “주문 생성(선점)“인지 “결제 완료”인지에 따라 요구사항이 달라진다.
  • 재고 소진 이후 몰리는 초과 요청에 대한 완충 전략이 없어, 모든 초과 요청이 즉시 DB 쓰기 시도로 이어지며 순간 부하가 그대로 전달된다.

Goals / Non-Goals

Goals

  • goods에 한정판 판매 회차(LimitedDrop) 개념을 도입한다 — 대상 상품, 판매 시작 시각, 한정 수량을 갖는다.
  • 판매 시작 시각 이전 구매 요청은 거부하고, 시작 시각 이후에는 기존 createPendingOrder()(idempotencyKey 멱등 + Stock.@Version 낙관적 락) 흐름을 그대로 재사용해 선착순으로 재고를 차감한다 — 새로운 주문/재고 파이프라인을 별도로 만들지 않는다.
  • “구매 성공”을 재고 차감·주문 생성(PENDING) 성공 시점으로 정의한다. 결제 확정(markPaid())은 별개 단계이며, 오버셀 판정과 P95 응답시간 측정은 모두 “주문 생성 API” 구간을 대상으로 한다.
  • 20000TPS 스파이크 상황에서 기존 낙관적 락 메커니즘이 오버셀 0건을 유지하는지 실증하고, 목표 응답 시간·에러율을 수치로 정의한다.

Non-Goals

  • 가상 대기실 등 프론트엔드 대기열 UI — 필요 시 별도 FE 과제로 분리한다(Open Questions).
  • 추첨(Draw) 방식 판매 — 1차 범위는 선착순(Line)만 다루며, 추첨은 Open Questions에서 결정한다.
  • 동적 프로모션 가격 정책(할인 로직) — 가격은 고정, 재고·시각 게이트만 다룬다.
  • 신규 재고 차감 파이프라인 개발 — 기존 Stock.@Version 낙관적 락 메커니즘을 재사용하는 것이 기본 방향이며, ticketing의 좌석 단위 SET NX PX 분산락을 그대로 이식하지 않는다(아래 Benchmarking·FR-4 참조).

User Scenarios

페르소나: 일반 사용자(B2C, 모바일 앱 한정판 상품 구매자).

  1. 해피 패스: 판매 시작 시각에 정확히 구매(주문 생성) 요청을 보내고, 재고가 남아 있으면 주문이 PENDING으로 생성되며 재고가 차감된다 — 이 시점이 “구매 성공”이다.
  2. 예외 — 시작 전 요청: 판매 시작 시각 이전에 구매를 요청하면 거부되고, 정확한 시작 시각을 안내받는다.
  3. 예외 — 재고 소진: 재고가 모두 소진된 이후 요청하면 “재고 소진” 응답을 받는다.
  4. 엣지 — 동시 폭주: 재고 수량과 동일하거나 그 이상의 요청이 동시에 몰려도 오버셀은 0건이다(낙관적 락 충돌 시 재시도 또는 실패 응답으로 수렴).
  5. 엣지 — 중복 요청: 네트워크 재시도로 동일 사용자가 같은 요청을 중복 전송해도 기존 idempotencyKey 메커니즘으로 멱등하게 처리된다(1인당 구매 수량 제한 정책은 Open Questions).

Benchmarking

제품명카테고리참조 패턴URL
Nike SNKRS스니커즈 한정판 발매 플랫폼추첨(Draw)과 선착순(Line) 두 방식을 병행 운영하며, 선착순 방식은 “사이즈 선택 → 결제정보 입력 → 짧은 대기 → 구매 성공/실패 확정”의 대기 단계를 명시적으로 둔다. “결제 실패 시 카드 승인이 진행되지 않는다”는 원칙을 참조나이키 SNKRS 발매 방식 업데이트
럭키드로우(LUCK-D)한정판 스니커즈 발매 정보 플랫폼여러 플랫폼(무신사 등)의 한정판 발매 시각·재고를 추적해 사용자에게 안내하는 구조 — “판매 시작 시각”이 마케팅 이벤트의 핵심 트리거임을 재확인럭키드로우

참고: ticketingSeatLockStore는 좌석이라는 개별 식별자 단위 락(좌석 1개=락 키 1개)으로, SET NX PX 분산락이 자연스럽다. 반면 한정판 재고는 수량 집계형 카운터이므로 좌석 락을 그대로 이식하기보다 이미 존재하는 Stock.@Version 낙관적 락(또는 원자적 UPDATE ... WHERE quantity >= N 방식)이 구조적으로 더 적합한 후보다. 최종 전략은 TDD에서 두 방식(낙관적 락 재시도 vs 원자적 UPDATE)의 처리량 비교로 확정한다.

Functional Requirements

ID요구사항우선순위
FR-1goodsLimitedDrop(한정판 판매 회차: 대상 상품, 판매 시작 시각, 한정 수량) 개념을 도입한다P0
FR-2판매 시작 시각 이전 구매 요청은 거부하고, 정확한 시작 시각을 응답에 포함한다P0
FR-3한정판 구매는 기존 createPendingOrder()(idempotencyKey 멱등 + Stock.@Version 낙관적 락) 흐름을 재사용한다 — 별도 재고 차감 파이프라인을 신설하지 않는다P0
FR-420000TPS 규모에서 Stock.@Version 낙관적 락의 재시도 폭주 여부를 검증하고, 재시도로 처리량이 감당되지 않으면 원자적 UPDATE 방식으로의 전환 여부를 TDD에서 결정한다(전략 확정은 TDD, 목표는 오버셀 0건)P0
FR-5재고 소진 이후 요청에는 명확한 “재고 소진” 응답을 반환한다P0
FR-61인당 구매 수량 제한을 적용한다(기본값·정책은 Open Questions에서 확정)P1
FR-7소진 전 완충: 판매 시작 직후 폭주하는 요청이 DB에 그대로 전달되지 않도록 애플리케이션 계층에서 스로틀링(예: 세마포어·대기열)을 적용해 처리 속도를 제어한다P1
FR-8소진 후 즉시 거부: 재고가 이미 소진된 이후의 요청은 대기열에 넣지 않고 즉시 “재고 소진” 응답으로 실패시킨다(FR-7의 완충 대상이 아니다)P1
FR-9한정판 판매 결과(성공 건수, 재고소진 거부 건수, 시작전 거부 건수)를 집계하는 API를 제공한다P2

Non-Functional Requirements

  • 20000TPS 피크 구간에서 오버셀(재고 수량 초과 판매) 0건을 유지한다.
  • 판매 시작 시각 도달 직후 구간의 P95 응답 시간은 800ms 이내다(평시 일반 상품 조회·구매는 300ms 이내를 유지). 측정 대상은 “주문 생성 API” 구간이다.
  • 재고 차감 연산(낙관적 락 재시도든 원자적 UPDATE든 — 구체 전략 중립적으로 서술)의 최종 실패(재시도 한도 초과·타임아웃) 비율은 5% 미만이다. 구체 전략은 FR-4에서 TDD로 확정하며, 이 NFR은 채택된 전략과 무관하게 적용되는 목표치다.
  • 정상 실패 응답(재고소진 409, 시작전 거부)은 에러율 계산에서 제외하고 5xx 에러율만 별도로 측정한다.

Operations

  • 판매 시작 시각 전후의 QPS, 오버셀 카운터, 재고 차감 재시도율(또는 락 대기시간) P95를 [옵저버빌리티 스택 도입](../옵저버빌리티 스택 도입/PRD.md) 대시보드에 노출한다.
  • 오버셀이 1건이라도 발생하면 [지능형 장애 알림](../지능형 장애 알림/PRD.md) 채널로 즉시 알림을 발송한다(알림 소스 태그: oversell, 심각도: critical).

Success Metrics

  • 오버셀 0건 유지(모든 한정판 판매 회차에서).
  • 20000TPS를 목표치로 도전한다. 목표 미달 시에는 실패로 간주하지 않고, 실제 도달 TPS와 병목 구간(애플리케이션 스로틀링/DB 락 경합/네트워크 등)을 기록한다. ([상시 트래픽 시뮬레이터](../상시 트래픽 시뮬레이터/PRD.md)와 동일 기준.)
  • 20000TPS 스파이크 구간(또는 실제 도달 TPS 구간)의 5xx 에러율 1% 미만.
  • [상시 트래픽 시뮬레이터](../상시 트래픽 시뮬레이터/PRD.md) 마케팅 이벤트 스파이크 시나리오로 재현 검증 완료.

Milestones

  • M1: LimitedDrop 도메인 모델·판매 시작 게이트(FR-1, FR-2).
  • M2: 기존 흐름 재사용·재고 차감 전략 검증(FR-3, FR-4, FR-5).
  • M3: 구매 수량 제한·소진 전 완충·소진 후 거부(FR-6~FR-8).
  • M4: 20000TPS 스파이크 검증 — [상시 트래픽 시뮬레이터](../상시 트래픽 시뮬레이터/PRD.md)의 마케팅 이벤트 스파이크 시나리오와 연계.

의존: [도메인 경계 재설계](../도메인 경계 재설계/PRD.md)의 goods 도메인 소유 데이터 확정이 선행되면 LimitedDrop의 배치 위치가 명확해진다.

Open Questions

  • 선착순(Line) 외 추첨(Draw) 방식 지원 여부를 결정해야 한다.
  • 1인당 구매 수량 제한의 기본값(예: 1개)을 결정해야 한다.
  • 프론트엔드 가상 대기실 UI 필요 여부와, 필요 시 별도 FE 과제로 분리할지 결정해야 한다.

Document History

날짜변경 내용
2026-07-03최초 작성
2026-07-03재검수 1차 반영: AS-IS 정정(GoodsOrder+Stock.@Version+idempotencyKey 멱등이 이미 존재), “구매 성공” 정의(주문 생성=선점 시점) 명시, LimitedDrop이 기존 주문/재고 흐름을 재사용하도록 FR-3 재정의, FR-7을 소진 전 완충(FR-7)/소진 후 거부(FR-8)로 분리, NFR 락 대기 타임아웃 지표를 전략 중립적으로 재기술, ticketing 좌석락(개별 식별자)과 재고(수량 집계)의 락 단위 차이를 Benchmarking에 명시, 20000TPS Success Metrics 문구를 ⑦과 통일