← 프로젝트 목록

주문(Order)

주문 라이프사이클 전체 + 도메인 이벤트 기반 10개 후처리

DDDDomain EventPessimistic LockState MachineE2E Test

배경 및 제약조건

  • 주문은 카트, 결제, 쿠폰, 포인트, 배송, 제휴 등 거의 모든 도메인과 연계되는 핵심 Aggregate
  • 레거시 주문 테이블(g5_shop_order)과 카트 테이블이 od_id로 연결 — 테이블 변경 불가
  • 주문 상태가 레거시 한글 값('쇼핑','주문','입금','배송')으로 저장 — 호환 필수
  • 결제 완료 후 8개 이상의 후처리(쿠폰, 포인트, 등급, 제휴, K-POP, 메일, 백업 등)에 걸쳐 발생
  • PG사(Eximbay, PayPal) 장애/지연 시에도 주문 정합성 유지 필요

설계 구조

Order Status State Machine (7개 상태)
SHOPPING → PENDING → PAID → PREPARING → SHIPPED → DELIVERED
    │         │        │
    └─ cancel ┴─ cancel┘ → CANCELLED

* 즉시결제(포인트/빌링키): SHOPPING → PAID (PENDING 건너뜀)
* 클라이언트 결제: SHOPPING → PENDING → PAID

PlaceOrder 흐름:
├── Cart 조회 + 프로모션/쿠폰 적용
├── 배송사 유효성 검증 (ShippingCarrierResolver)
├── PriceCalculator.calculate() → PricingResult
├── OrderFactory.createWithPricing() → Order Aggregate
├── Order.validateOrder() → 불변 조건 검증
├── OrderRepository.save() + saveOrderItems()
├── couponRepository.updateCouponUseOrderId()
└── cartRepository.backupToTemp()

OrderPaid 이벤트 후처리 (10개 독립 핸들러):
├── CouponConfirmEventHandler      → 쿠폰 사용 확정
├── PointDeductEventHandler        → 포인트 차감
├── RankUpgradeEventHandler        → 등급 업그레이드 + 쿠폰
├── AffiliateOrderRegistration     → 제휴 커미션 계산/저장
├── CjOrderEventHandler            → CJ GraphQL 전송
├── KpopOrderSyncEventHandler      → K-POP 주문 동기화
├── OrderBackupEventHandler        → 주문 백업 생성
├── OrderCompleteMailEventHandler   → 주문 확인 메일
├── VaultHistoryEventHandler       → 결제 이력 저장
└── RecordPaymentPreference        → 최근 결제 수단 기록

Problem & Solution

Problem

PG 콜백 지연 중 사용자가 취소 → 결제는 됐는데 주문이 삭제되는 '결제-주문 불일치'

Solution

SELECT FOR UPDATE 비관락으로 동시 접근 직렬화. 취소 전 PaymentStatusQueryRouter로 PG 실제 상태 조회 — 이미 결제됐으면 주문 보호(취소하지 않음). od_transaction 플래그로 결제 완료 영구 기록

Problem

결제 후 10개 후처리가 하나의 트랜잭션 — 하나 실패하면 결제까지 롤백

Solution

OrderPaid 도메인 이벤트 발행 → @TransactionalEventListener(AFTER_COMMIT) + @Async + @Transactional(REQUIRES_NEW)로 각 핸들러 독립 격리. 후처리 실패가 원본 결제에 영향 없음

Problem

20분 미결제 주문 만료 시, PG에서는 이미 결제 완료된 경우 — 무조건 삭제하면 결제만 남는 사고

Solution

만료 전 PaymentStatusQueryRouter로 PG(PayPal/Eximbay) 결제 상태 직접 조회. 결제 완료면 주문 복구 처리, 미완료면 안전하게 만료

Problem

카트-주문 공유 테이블에서 PlaceOrder와 ViewCart가 동시 실행 시 주문 데이터 유실 (Lost Update)

Solution

g5_shop_order_item 테이블 도입. 주문 생성 시 카트 아이템 ID를 order_item에 INSERT하여 카트 merge 대상에서 원천 제거. '상태값 판별'에서 '존재 여부 판별'로 전환

Problem

PG notify_url과 return_url이 동시에 도착하여 결제 완료 처리가 2번 실행

Solution

order.pay()에서 이미 PAID면 예외 → 멱등성 보장. 비관락으로 동시 접근 직렬화하여 정확히 1회만 처리

기술적 챌린지

레거시 PHP 동시 운영 마이그레이션

PC 버전(PHP)은 그대로 운영하면서 동시에 Spring Boot로 전환. 양쪽이 같은 DB를 공유하므로 주문 라이프사이클(상태 전이, 한글 값 호환)을 정확히 맞춰야 함

카트 테이블을 주문 아이템으로 겸용 → Lost Update

g5_shop_cart가 카트+주문을 겸하면서 PlaceOrder와 ViewCart가 같은 행을 동시 조작. 테이블 구조 변경 불가 제약 속에서 order_item 분리 전략으로 해결

성과

  • Checkout 이탈률 84.3% 감소 (3.52% → 0.55%) — 주문-결제 정합성 강화 기여
  • 결제-주문 불일치 원천 차단 (비관락 + PG 상태 조회)
  • 10개 후처리 독립 격리 — 후처리 실패가 결제에 영향 없음
  • 카트-주문 동시성 근본 해결 — order_item 테이블로 경합 제거
  • 7개 상태 전이를 Aggregate 내부에서 안전하게 관리
  • 9가지 변이축 E2E 테스트로 주문 흐름 체계적 검증
  • 레거시 100% 호환 — 한글 상태값, od_id 체계, 카트 백업 모두 유지