번호 배정 조정 방침 (공통, 잠정): 현재 배정 ② partner=V38/V39/V40 · ③ limited_drops=V41 · ⑥ alerts=V42는 세 과제가 같은 db/migration/ 네임스페이스를 공유해 발생한 잠정 배정이다. 먼저 머지되는 쪽이 V38부터 순차 점유하고, 나중 쪽은 origin/dev 기준 최신 번호로 재배정한다. 구현 시 origin/dev 기준 워크트리에서 최신 버전을 실측 후 확정한다.
신규 limited_drops는 goods 도메인 테이블 → goods 컨벤션(soft-delete 6컬럼)을 준용한다.
저장소 선택
데이터 단위
저장소
채택 사유
limited_drops (회차 메타·상태)
MySQL
정형·트랜잭션·상태 머신. goods 도메인 Aggregate. 기본 저장소
재고 슬롯 카운터 (입장 게이트)
Redis (신규 테이블 아님)
20000TPS 원자 DECR 게이트 — private-redis-convention 담당. 본 문서 범위 밖
최종 재고 SSOT
MySQL stocks (기존, 무변경)
@Version 낙관락으로 오버셀 물리 봉쇄 (FR-3 재사용)
FR-9 집계 (성공/거부 수)
전용 테이블 없음 — Redis 카운터 + goods_orders 파생
아래 “FR-9 집계” 참조. 단순함 우선(P2)
MongoDB 미채택: limited_drops는 goods 도메인의 정형 관계형 데이터(product_id 참조·상태 전이·기존 stocks/goods_orders와 조인). 문서형 채택 근거 없음. DB 신규분은 MySQL 1테이블.
테이블 정의
limited_drops — 한정판 판매 회차 (V41)
컬럼
타입
NULL
기본값
COMMENT / 근거
id
BIGINT AUTO_INCREMENT
NOT NULL
—
PK
product_id
BIGINT
NOT NULL
—
대상 goods Product id (products.id, 물리 FK 없음)
open_at
DATETIME(6)
NOT NULL
—
판매 시작 시각 (판매 시작 게이트 기준, FR-2)
close_at
DATETIME(6)
NOT NULL
—
판매 종료 시각
limited_quantity
INT
NOT NULL
—
한정 수량 (Redis 카운터 시드 값)
per_user_limit
INT
NOT NULL
1
1인 구매 한도 (기본 1, FR-6). 회차별 조정
status
VARCHAR(20)
NOT NULL
—
SCHEDULED | OPEN | SOLD_OUT | CLOSED (ENUM 금지 → VARCHAR)
version
BIGINT
NOT NULL
0
낙관락(@Version) — 상태 전이 동시성
created_at
DATETIME(6)
NOT NULL
—
생성 시각 (UTC)
created_by
BIGINT
NULL
—
개설 판매자 user_id
updated_at
DATETIME(6)
NOT NULL
—
마지막 수정 시각
updated_by
BIGINT
NULL
—
마지막 수정자 user_id
deleted_at
DATETIME(6)
NULL
—
소프트 삭제 시각 (goods 도메인 컨벤션 준수)
deleted_by
BIGINT
NULL
—
삭제자 user_id
영속 status는 조회·집계 표기용. 게이트 판정은 validatePurchasable()가 now·remaining으로 실시간 결정(TDD 상태 전이 표 각주) — 컬럼은 표시·필터용.
BOOLEAN 없음, JSON 없음. limited_quantity/per_user_limit는 INT(수량 상한은 상품 재고 규모 내 → INT 충분).
쿼리 패턴 → 인덱스 매핑
#
쿼리 패턴
WHERE / 조건
인덱스
근거
L1
findById(id) (구매·조회 진입, purchase마다)
PK
(PK)
클러스터 PK. 20000TPS 핫 리드 — 아래 캐싱 주의 참조
L2
findOpenByProductId(productId) (상품의 활성 회차 1건)
product_id = ? AND status IN ('SCHEDULED','OPEN') AND deleted_at IS NULL
INDEX idx_limited_drops_product_id (product_id)
상품당 활성 회차 수가 극소(대개 1)라 product_id 단독으로 충분히 선택적. status를 뒤에 붙일 실익 미미
L3
리컨실리에이션·개시 스케줄 스캔
status = ? AND open_at <= :now / status='OPEN' 활성 회차 순회
INDEX idx_limited_drops_status_open_at (status, open_at)
복합 순서 근거: status(등가, 저카디널리티지만 활성 회차만 좁힘) → open_at(범위). DropReconciliationWorker의 OPEN 회차 순회 + 개시 대상(SCHEDULED AND open_at<=now) 조회를 커버. senior-be가 요청한 open_at 인덱스를 쿼리 근거 있는 복합형으로 구체화
위 3개 외 인덱스는 만들지 않는다. close_at 단독 인덱스는 대응 쿼리 없음(마감 판정도 실시간 now 비교) → 미생성.
카디널리티 주의(L3): status 선두는 저카디널리티지만 회차 테이블 전체 행 수가 극소(수백)라 인덱스 유효. 테이블이 작아 인덱스 효과 자체가 부차적 — 정확성·정렬 목적.
핫 리드 캐싱 주의 (스키마 외 — BE 설계 인계)
L1은 구매 요청마다 발생 → 스파이크 시 단일 row에 20000 reads/sec. MySQL 버퍼풀로 감당 가능하나, 회차 메타(open_at/close_at/status/limited_quantity/per_user_limit)는 애플리케이션/Redis 캐시 권고. 이는 스키마가 아니라 domain/infra 캐싱 설계 사항 — private-redis-convention·private-senior-be 인계.
FR-9 집계 — 전용 테이블 미채택 (파생 권고)
항목
설계
successCount (성공 주문 수)
limited_quantity - Redis.remaining (즉시) 또는 goods_orders ⨝ goods_order_itemsWHERE product_id = drop.product_id AND created_at BETWEEN open_at AND close_at로 파생
Redis 카운터(휘발성) — DB 비영속. 정상 거부는 5xx가 아니므로 지표성으로 충분
전용 테이블 미채택 사유
거부 수는 휘발성 운영 지표(관측용)라 durable 저장 불요. 성공 수는 goods_orders에서 파생 가능. 별도 집계 테이블은 쓰기 경합(20000TPS 시 집계 row 자체가 핫스팟)을 신설 → 오히려 병목. 단순함 우선(TDD Open Q P2)
파생 정밀도 caveat
goods_orders에 drop_id가 없어 상품+기간 창으로 귀속. 회차 창 밖 동일 상품 판매가 있으면 오차 → 현재는 회차 창=상품 판매창 전제로 허용. 정밀 durable 집계가 필요해지면 후속 과제에서 limited_drop_id 컬럼을 goods_orders에 가산(expand-contract) 검토
용량 추정 · 보존 정책
테이블
예상 행 수
증가율
1년 후
보존 정책
limited_drops
마케팅 이벤트당 수 개
회차 개설 시 +1
< 1,000 행
영구 (무한 성장 아님, 극소). 보관·아카이빙 불요
20000TPS 부하는 limited_drops에 쓰기를 유발하지 않는다 — 쓰기는 Redis 카운터·stocks·goods_orders로 흡수. limited_drops는 개설 시 1 write + 상태 전이 시 소수 update뿐. 테이블 규모·성장 모두 무시 가능.
무중단 마이그레이션 (expand-contract)
limited_drops는 신규 가산 1테이블. 기존 goods 구매 경로(createPendingOrder·stocks·ADR-003 UseCase) 전부 무변경. 백필 없음 → NOT NULL 컬럼도 단일 마이그레이션 안전(신규 테이블, 채울 기존 데이터 없음).
단계
작업
배포 순서
락 영향
롤백 지점
1. 스키마
V41__create_limited_drops.sql 생성
스키마 먼저
CREATE TABLE = 신규 객체, 기존 테이블 무락. 온라인 안전
미배포 상태에서 역방향 DROP TABLE limited_drops; (참조 코드 없어 안전)
2. 코드 (플래그 OFF)
LimitedDrop 전 코드 배포, limited-drop.enabled=false
스키마 후 코드
없음
플래그 이미 OFF — 무영향
3. 회차 시드
판매자 회차 개설 API → drop 생성 + Redis seedIfAbsent
—
limited_drops INSERT 1건
drop soft-delete + Redis 키 삭제
4. 플래그 ON
limited-drop.enabled=true
설정
없음
플래그 OFF → 즉시 비활성 (기존 goods 무영향)
5. Redis 게이트 서브플래그
limited-drop.redis-gate.enabled — 게이트 오작동 시 OFF → DB @Version 폴백(방안 B)
설정
없음
서브플래그 OFF → 폴백(오버셀 0 유지, 처리량만 감소)
배포 순서 근거: 테이블 부재 시 부팅 스키마 검증 실패 방지 → 스키마 먼저, 코드는 플래그 OFF 배포.