결제 확인(confirm) API를 호출했을 때, 포인트가 차감되어야 함에도
차감되지 않는 현상이 발생했다.
흐름:
PaymentService.confirmPayment()
└─► OrderService.completePayment() ← 포인트 차감 포함
└─► order.completePayment() ← Order 엔티티 직접 호출 (중복!)
기대: 주문 상태 변경 + 포인트 차감 정상 처리
실제: 포인트 차감 없이 조용히 실패
구조 문제
PaymentService.confirmPayment() 내부에서
OrderService.completePayment()를 호출하면서,
Order 엔티티의 completePayment()도 중복으로 직접 호출하고 있었다.
// ❌ 문제가 된 코드
payment.completePayment(dbPaymentId); // Payment 상태 PAID
orderService.completePayment(orderId); // 내부에서 order.completePayment() 호출
order.completePayment(); // 또 호출 → 예외 발생!
OrderService.completePayment() 안에서 이미 order.completePayment()를
호출해 주문 상태가 PENDING_CONFIRMATION으로 바뀐 상태인데,
PaymentService에서 order.completePayment()를 한 번 더 호출하면서
엔티티 내부의 상태 검증이 실패했다.
// Order 엔티티
public void completePayment() {
if (this.orderStatus != OrderStatus.PENDING_PAYMENT) {
// 이미 PENDING_CONFIRMATION 상태이므로 여기서 예외 발생
throw new IllegalStateException("결제 대기 상태의 주문만 결제 완료 처리할 수 있습니다.");
}
this.orderStatus = OrderStatus.PENDING_CONFIRMATION;
}
Spring의 @Transactional 기본 전파 전략은 PROPAGATION_REQUIRED다.
PaymentService와 OrderService는 같은 트랜잭션을 공유한다.
PaymentService.confirmPayment() — @Transactional (트랜잭션 시작)
└─► OrderService.completePayment() — @Transactional (기존 트랜잭션 합류)
├─ order.completePayment() → 상태 정상 변경
└─ pointService.usePoints() → 포인트 차감 성공 (DB 반영은 아직)
└─► order.completePayment() 재호출 → IllegalStateException 발생
└─ Spring이 트랜잭션을 rollback-only로 마킹
→ 커밋 시 UnexpectedRollbackException
→ 포인트 차감 포함 모든 변경 사항 롤백
예외를 catch해도 이미 트랜잭션이 rollback-only 상태이므로
커밋 자체가 불가능해진다.
PaymentService에서 Order 엔티티를 직접 조작하는 코드를 제거하고,
OrderService.completePayment()만 호출하도록 단일화했다.
// ✅ 수정 후
payment.completePayment(dbPaymentId); // Payment 상태만 처리
orderService.completePayment(payment.getOrder().getId()); // Order 상태 + 포인트 차감
책임을 명확히 분리한다.
PaymentService → Payment 엔티티 상태 관리
OrderService → Order 엔티티 상태 + 포인트 차감
PROPAGATION_REQUIRED에서 내부 예외는 전체 트랜잭션을 오염시킨다
같은 트랜잭션을 공유하는 메서드 내부에서 예외가 발생하면
rollback-only가 마킹된다. 외부에서 catch해도 이미 늦다.
엔티티 메서드를 여러 레이어에서 직접 호출하면 상태 충돌이 생긴다
엔티티 내부에 상태 검증 로직이 있다면,
그 메서드를 호출하는 진입점은 Service 레이어 하나로 고정해야 한다.
Service를 우회해서 엔티티를 직접 조작하는 패턴은 위험하다.
서비스 간 역할 경계를 명확히 해야 한다
PaymentService가 Order 엔티티를 직접 다루는 것 자체가
단일 책임 원칙(SRP) 위반이었다. 각 Service는 자신의 도메인 엔티티만
직접 조작하고, 다른 도메인은 해당 Service를 통해서만 접근해야 한다.