프로모션(Promotion)
7종 프로모션 통합 엔진 — sealed class + Effect 패턴 + 5단계 파이프라인
배경 및 제약조건
- •7종 프로모션(타임딜, 더블딜, 스페셜오퍼, 사은품, 뉴멤버딜, 타임어택, 클리어런스) 동시 운영
- •레거시 PHP에서 각 프로모션이 독립 코드로 산재, 적용 순서/중복 규칙 미명시
- •1상품 1할인 프로모션 원칙 — 단, 사은품(FreeGift)은 할인 프로모션과 중복 적용 가능
- •프로모션 종료 시 카트에서 해당 아이템 자동 제거 (스페셜오퍼, 뉴멤버딜, 사은품)
- •장바구니 조회마다 프로모션 상태를 리셋하고 현재 활성 프로모션만 재적용 (실시간 반영)
설계 구조
Promotion (sealed abstract class — Aggregate Root) │ 공통 계약: isActive() → verify() → apply() ├── TimeDeal ← 한정 시간 할인가 ├── DoubleDeal ← 1+1 (무게 × 2) ├── SpecialOffer ← 조건 충족 시 특정 상품 무료 ├── FreeGift ← 사은품 (4가지 QualificationStrategy) ├── NewMemberDeal ← 신규 회원 전용 ├── TimeAttack ← 시간 한정 특가 └── Clearance ← 재고 소진 할인 PromotionApplier 5단계 파이프라인: ├── Phase 1: Cleanup — 종료된 프로모션 아이템 제거 + 플래그 리셋 ├── Phase 2: Mark Flags — 혜택 대상 표시 (verify 무관) ├── Phase 3: Qualify — verify 통과 선별 (1상품 1할인 규칙) ├── Phase 4: Apply Benefits — Effect 적용 (사은품 추가 등) └── Phase 5: Collect Result — 프론트 노출 목록 수집 PromotionEffect (sealed interface — Cart를 모르는 순수 값): ├── MarkDouble, MarkAsTimeDeal, MarkAsFreeGift, ... ├── AddGift(productId, qty) ← 사은품 카트 추가 ├── SetMaxBuyQuantity, SetPromotionPeriod, RenameItem └── RemoveSpecialOfferItems, RemoveFreeGiftItems ← 자격 미달 시 삭제 FreeGift QualificationStrategy (sealed): ├── TotalAmount(minimum, beautyOnly, kpopOnly) ├── SpecificItem(targetId, minimum) ├── SpecificBrand(brandId, minimum) └── SpecificPromotion(skuList, minCount, minimum)
Problem & Solution
Problem
7종 프로모션이 각각 별도 테이블 + 별도 로직으로 산재 — 새 타입 추가마다 전혀 다른 코드 경로 필요
Solution
sealed Promotion 추상 클래스로 통합. 3단계 프로토콜(isActive → verify → apply)로 모든 타입을 일관된 흐름으로 처리. 새 타입 추가 시 sealed class 구현체만 작성
Problem
Promotion이 Cart를 직접 조작하면 결합도 증가 + 프로모션 로직 독립 테스트 불가
Solution
Effect 패턴: Promotion.apply()는 PromotionEffect(sealed interface)만 반환. PromotionApplier가 Effect를 Cart에 반영하는 Mediator 역할. Promotion은 Cart를 모르고, Cart는 Promotion을 모른다
Problem
타임딜 상품에 사은품은 동시 적용 가능하지만, 타임딜 + 스페셜오퍼는 중복 불가 — 모든 조합을 하드코딩하면 확장 불가
Solution
Qualify 단계에서 appliedItemIds로 이미 할인 프로모션 적용된 상품 추적. FreeGift는 isIndependent 플래그로 추적에서 제외되어 할인과 독립 적용
Problem
프로모션 종료 시 카트에 남아있는 해당 아이템(스페셜오퍼, 사은품)이 자동 제거되어야 함
Solution
Phase 1(Cleanup)에서 현재 활성 프로모션 목록과 카트 아이템 플래그 비교. 활성 프로모션 없는데 해당 플래그 아이템 있으면 자동 삭제. 매 조회마다 clean→reapply로 항상 최신 상태
Problem
체크아웃/주문 시 프로모션 재적용하면 사은품이 추가/삭제되어 DB와 메모리 불일치
Solution
applyFlagsOnly()를 분리하여 AddGift/Remove 계열 Effect를 필터링. 플래그만 적용(검증 용도)하되 카트 아이템 구성은 변경하지 않음
기술적 챌린지
사은품 재고 기반 수량 조정
동일 사은품이 여러 프로모션에서 동시 증정될 수 있음. PromotionApplier에서 상품별 재고 맵 관리, 프로모션 우선순위 순 할당, 전체 지급 수량이 재고 초과하지 않도록 조정
카트-프로모션 연동 시점 분리
ViewCart: 전체 5단계 파이프라인 (사은품 추가/삭제 포함). 체크아웃/주문: applyFlagsOnly() (플래그만, 아이템 변경 없음). PriceCalculator: CartItemFlags 참조 (쿠폰 제외, 더블딜 무게×2)
성과
- ✓Home 이탈률 77.9% 감소 + 체류 시간 33.7% 증가에 기여 — 프로모션 정확 적용으로 상품 탐색 경험 개선
- ✓7종 프로모션을 단일 5단계 파이프라인으로 통합 처리
- ✓새 프로모션 타입 추가 시 sealed class 구현체 + Effect만 추가
- ✓Promotion↔Cart 결합도 0 — Effect 패턴으로 독립 테스트 가능
- ✓실시간 프로모션 반영 — 매 조회마다 clean→reapply
- ✓사은품 재고 안전 — 여러 프로모션 동시 증정 시 재고 초과 방지