마케팅 이벤트 고부하 대응 DB 설계 (design-db)

Background

  • 근거 TDD: /Users/biuea/Desktop/dpdpdndn/프로젝트/스포츠앱/마케팅 이벤트 고부하 대응/TDD.md
  • 대상 레포: /Users/biuea/sports-application (backend/src/main/resources/db/migration/)
  • 이 문서는 설계 산출물이며 DDL 전문 작성·실행은 private-mysql-implementer가 담당한다.
  • 재고 경합 봉쇄는 기존 stocks.@Version을 재사용하며(무변경), 신규 스키마는 limited_drops 1개다.

실측한 기존 마이그레이션 컨벤션 (SSOT)

항목실측 결과근거
파일 명명순차 정수 V{정수}__{snake_case}.sql (타임스탬프 아님)레포 db/migration/ 전수
최신 버전V37실측
goods 도메인 audit 컬럼created_at/created_by/updated_at/updated_by/deleted_at/deleted_by 6종 (soft-delete)V6__create_products_orders.sql, V17__create_goods_orders...
인덱스 명명INDEX idx_{table}_{cols}V6:18, V17:17
statusVARCHAR(20~32) (ENUM 금지)V6:10, V17:7
시간 컬럼DATETIME(6)전수
낙관락stocks.version BIGINT NOT NULL (재사용 대상)V6:26
멱등goods_orders.idempotency_key VARCHAR(255) + UNIQUEV23

번호 배정 조정 방침 (공통, 잠정): 현재 배정 ② 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 담당. 본 문서 범위 밖
최종 재고 SSOTMySQL 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 / 근거
idBIGINT AUTO_INCREMENTNOT NULLPK
product_idBIGINTNOT NULL대상 goods Product id (products.id, 물리 FK 없음)
open_atDATETIME(6)NOT NULL판매 시작 시각 (판매 시작 게이트 기준, FR-2)
close_atDATETIME(6)NOT NULL판매 종료 시각
limited_quantityINTNOT NULL한정 수량 (Redis 카운터 시드 값)
per_user_limitINTNOT NULL11인 구매 한도 (기본 1, FR-6). 회차별 조정
statusVARCHAR(20)NOT NULLSCHEDULED | OPEN | SOLD_OUT | CLOSED (ENUM 금지 → VARCHAR)
versionBIGINTNOT NULL0낙관락(@Version) — 상태 전이 동시성
created_atDATETIME(6)NOT NULL생성 시각 (UTC)
created_byBIGINTNULL개설 판매자 user_id
updated_atDATETIME(6)NOT NULL마지막 수정 시각
updated_byBIGINTNULL마지막 수정자 user_id
deleted_atDATETIME(6)NULL소프트 삭제 시각 (goods 도메인 컨벤션 준수)
deleted_byBIGINTNULL삭제자 user_id
  • 영속 status는 조회·집계 표기용. 게이트 판정은 validatePurchasable()가 now·remaining으로 실시간 결정(TDD 상태 전이 표 각주) — 컬럼은 표시·필터용.
  • BOOLEAN 없음, JSON 없음. limited_quantity/per_user_limit는 INT(수량 상한은 상품 재고 규모 내 → INT 충분).

쿼리 패턴 → 인덱스 매핑

#쿼리 패턴WHERE / 조건인덱스근거
L1findById(id) (구매·조회 진입, purchase마다)PK(PK)클러스터 PK. 20000TPS 핫 리드 — 아래 캐싱 주의 참조
L2findOpenByProductId(productId) (상품의 활성 회차 1건)product_id = ? AND status IN ('SCHEDULED','OPEN') AND deleted_at IS NULLINDEX 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_ordersgoods_order_items WHERE product_id = drop.product_id AND created_at BETWEEN open_at AND close_at로 파생
soldOutRejectCount / tooEarlyRejectCount / throttledCountRedis 카운터(휘발성) — DB 비영속. 정상 거부는 5xx가 아니므로 지표성으로 충분
전용 테이블 미채택 사유거부 수는 휘발성 운영 지표(관측용)라 durable 저장 불요. 성공 수는 goods_orders에서 파생 가능. 별도 집계 테이블은 쓰기 경합(20000TPS 시 집계 row 자체가 핫스팟)을 신설 → 오히려 병목. 단순함 우선(TDD Open Q P2)
파생 정밀도 caveatgoods_ordersdrop_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 seedIfAbsentlimited_drops INSERT 1건drop soft-delete + Redis 키 삭제
4. 플래그 ONlimited-drop.enabled=true설정없음플래그 OFF → 즉시 비활성 (기존 goods 무영향)
5. Redis 게이트 서브플래그limited-drop.redis-gate.enabled — 게이트 오작동 시 OFF → DB @Version 폴백(방안 B)설정없음서브플래그 OFF → 폴백(오버셀 0 유지, 처리량만 감소)
  • 배포 순서 근거: 테이블 부재 시 부팅 스키마 검증 실패 방지 → 스키마 먼저, 코드는 플래그 OFF 배포.
  • 스키마 변경 없음(가산만) → API 버저닝 불요, 하위 호환.

자가 점검 (TDD 도메인 모델 ↔ 컬럼 대응)

TDD 도메인 필드대응 컬럼확인
LimitedDrop(productId, openAt, closeAt, limitedQuantity, perUserLimit, status)limited_drops 전 컬럼OK
validatePurchasable() — now vs open_at/close_at, statusopen_at/close_at/statusOK
생성 검증(openAt<closeAt, quantity>0, perUserLimit>0)INT/DATETIME(6) 컬럼 (검증은 Entity, 스키마는 타입만 보장)OK
findOpenByProductIdL2 인덱스OK
상태 전이(SCHEDULED/OPEN/SOLD_OUT/CLOSED)status VARCHAR(20)OK
재고 SSOT / 오버셀 봉쇄기존 stocks.@Version 재사용 (신규 스키마 아님)OK
  • 컨벤션 위반 점검: FK 없음 OK / ENUM 없음(status VARCHAR) OK / JSON 없음 OK / BOOLEAN 없음 OK / DATETIME(6) OK / 전 컬럼·테이블 COMMENT 대상 OK / PK=id, 참조=product_id OK.

Document History

날짜변경 내용
2026-07-03최초 작성 — 기존 컨벤션 실측(최신 V37, goods soft-delete 6컬럼), limited_drops 단일 테이블 정의, 인덱스 3종(L1 PK/L2 product_id/L3 status·open_at 복합), FR-9 파생 권고(전용 테이블 미채택), 핫 리드 캐싱 주의, expand-contract 배포 순서