주문-결제 시스템에서 강한 결합을 이벤트로 풀어낸 이야기
현재 이커머스 프로젝트에 다양한 기능이 추가되고, 외부 PG 연동도 하면서 주문과 결제의 트랜잭션은 분리되었지만 여전히 주문 파사드에서는 다양한 연관 도메인들의 작업이 하나의 트랜잭션으로 묶여있는 상황이었다.
@Transactional
public void completeOrder(OrderCriteria.Complete criteria) {
// 포인트 차감 - 핵심 기능
pointService.deduct(criteria.toPointDeductCommand(orderInfo.pointAmount()));
// 쿠폰 사용 - 외부 시스템 호출로 인한 지연 발생
couponService.useCoupons(criteria.userId(), orderInfo.couponIds());
// 재고 차감 - 핵심 기능
stockService.validateAndReduceStocks(criteria.toStockReduceCommands(orderInfo.items()));
// 주문 상태 변경 - 핵심 기능
orderService.completeOrder(criteria.orderId());
// 외부 데이터 플랫폼 전송 - 부가 기능인데 실패하면?
dataPlatformService.sendOrderData(orderInfo);
}
특히 PaymentSyncFacade의 코드에서 OrderFacade를 직접 호출하는 구조는 도메인 분리 관점에서도 수정이 필요했다.
// PaymentSyncFacade.java
@Transactional
public void processCompletedPayment(String paymentId) {
Payment payment = paymentRepository.findByPaymentUuid(paymentId)
.orElseThrow(() -> new IllegalArgumentException("Payment not found: " + paymentId));
// Payment 도메인에서 Order 도메인을 직접 호출
OrderCriteria.Complete criteria = new OrderCriteria.Complete(
payment.getOrderId(),
payment.getUserId(),
orderInfo.couponIds()
);
orderFacade.completeOrder(criteria); // 강한 결합!
}
이번 주차의 과제에서 주어진 것은 쿠폰의 이벤트를 분리하라는 것이었다.
기왕 이벤트로 분리한다면 다 이벤트 기반으로 분리하는 것이 좋지만 그 중에서도 하나의 도메인을 선정했는데, 왜 하필 쿠폰이었을지 궁금했다.
다른 모든 도메인을 제치고 쿠폰이 선택된 이유를 나름 분석해봤다.
⇒ '주문 트랜잭션 내에서 포인트 사용을 함께 처리'하는 것이 효율적
복잡성을 늘리지 않으면서도 즉각적인 데이터 정합성을 보장
⇒ 커맨드성
⇒ 분리시 주문 로직의 안정성을 확보 가능
쿠폰 시스템의 장애가 발생해도 주문 자체는 유효한 점진적 실패(graceful degradation)가 가능하도록!
주문 트랜잭션과 쿠폰 사용 처리를 분리 - 쿠폰 실패가 주문을 막으면 안 됨
결제 결과에 따른 주문 처리를 분리 - 도메인 간 직접 호출 제거
데이터 플랫폼 전송을 후속처리로 - 분석용 데이터는 비동기로
SpringEventConfig로 config를 설정하는 방식도 있었지만, 현재 프로젝트의 규모상 기본 설정으로도 충분할 것 같아서 도입하지 않았다.
EventPublisher는 공통으로, DomainEvent라는 베이스 클래스를 만들고 각 이벤트 클래스가 확장할 수 있도록 했다. 또한 각 도메인 이벤트와 리스너는 도메인 레이어에 위치하도록 구현했다.
각 이벤트는 트랜잭션 커밋 후 실행되도록 @TransactionalEventListener
를 적용했다.
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
주문 완료 요청
↓
[OrderFacade.completeOrder]
├─ 포인트 차감 (동기)
├─ 재고 차감 (동기)
├─ 주문 상태 변경 (동기)
└─ OrderCompletedEvent 발행
↓
트랜잭션 커밋
↓
┌───────┴────────────────┐
↓ ↓
[CouponEventListener] [DataPlatformListener]
(비동기, AFTER_COMMIT) (비동기, AFTER_COMMIT)