[BE-03] LimitedDropDomainService 구매 오케스트레이션

작업 내용 (설계 의도)

변경 사항

한정판 구매 도메인 흐름을 조립한다(근거 TDD: ../TDD.md “Sequence Diagram”, ADR-001·ADR-003). 기존 GoodsDomainService.createPendingOrder(같은 도메인)를 무변경 재사용한다(FR-3). 롤백: 피처 플래그 OFF로 즉시 비활성(기존 goods 흐름 무영향).

  • LimitedDropDomainService.purchase(command):
    1. findOpenByProductId → 없으면 NotFound.
    2. drop.validatePurchasable() (시각 게이트 FR-2, DB·Redis 미접근 거부).
    3. reservationStore.reserve(...) 결과 when 분기: Admitted/AlreadyReserved 진행, SoldOut→예외(409), Throttled→예외(429), PerUserLimitExceeded→예외(403).
    4. try goodsDomainService.createPendingOrder(userId, items, key)confirmSuccess(). catch → cancel() 후 재전파(슬롯 반환, 언더셀만 허용).
  • openDrop(dropId): 상태 OPEN 전이 + seedIfAbsent로 Redis 카운터 시드(BE-08에서 호출).
  • Redis 장애(DataAccessException/RedisLockException) 시 fail-open to DB: 게이트 우회하고 createPendingOrder 진행(오버셀은 Stock.@Version이 봉쇄), redis-degraded 신호.
  • 메서드 15줄 초과 시 validateGate·admit·persistOrRelease 헬퍼로 분해.
  • 오버셀 감지 지점(리컨실리에이션 입력)에서 LimitedDropOversoldEvent 발행(DomainEventPublisher).

의존

  • BE-01 (LimitedDrop·Repository·예외·이벤트), BE-02 (DropReservationStore·ReservationResult)

다이어그램

처리 흐름

sequenceDiagram
    participant D as LimitedDropDomainService
    participant R as DropReservationStore
    participant G as GoodsDomainService
    D->>D: validatePurchasable()
    D->>R: reserve(...)
    R-->>D: Admitted
    D->>G: createPendingOrder(재사용)
    G-->>D: GoodsOrder
    D->>R: confirmSuccess()

클래스 의존

flowchart LR
    LimitedDropDomainService --> LimitedDropRepository
    LimitedDropDomainService --> DropReservationStore
    LimitedDropDomainService --> GoodsDomainService
    LimitedDropDomainService --> DomainEventPublisher

테스트 케이스

  • Admitted면 createPendingOrder를 호출하고 confirmSuccess로 확정한다
  • SoldOut이면 createPendingOrder를 호출하지 않고 SoldOut 예외를 던진다
  • Throttled/PerUserLimitExceeded는 각각 429/403 매핑 예외를 던지고 DB에 도달하지 않는다
  • reserve는 Admitted였으나 createPendingOrder가 예외를 던지면 cancel로 슬롯을 복원한 뒤 재전파한다
  • 동일 idempotencyKey 재요청 시 AlreadyReserved로 재-DECR 없이 기존 주문을 반환한다
  • now<openAt이면 reserve를 호출하지 않고 TooEarly 예외를 던진다
  • Redis가 DataAccessException을 던지면 fail-open으로 createPendingOrder를 진행한다