← 프로젝트 목록

프로모션(Promotion)

7종 프로모션 통합 엔진 — sealed class + Effect 패턴 + 5단계 파이프라인

Sealed ClassEffect PatternPipelineStrategy Pattern레거시 PHP 분석

배경 및 제약조건

  • 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
  • 사은품 재고 안전 — 여러 프로모션 동시 증정 시 재고 초과 방지