[TIL] Spring @Transactional 전파로 인한 포인트 차감 누락 버그

김재진·2026년 2월 19일

내일배움캠프

목록 보기
50/70

문제 상황

결제 확인(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 엔티티 상태 + 포인트 차감


    배운 점

  1. PROPAGATION_REQUIRED에서 내부 예외는 전체 트랜잭션을 오염시킨다

    같은 트랜잭션을 공유하는 메서드 내부에서 예외가 발생하면
    rollback-only가 마킹된다. 외부에서 catch해도 이미 늦다.

  2. 엔티티 메서드를 여러 레이어에서 직접 호출하면 상태 충돌이 생긴다

    엔티티 내부에 상태 검증 로직이 있다면,
    그 메서드를 호출하는 진입점은 Service 레이어 하나로 고정해야 한다.
    Service를 우회해서 엔티티를 직접 조작하는 패턴은 위험하다.

  3. 서비스 간 역할 경계를 명확히 해야 한다

    PaymentService가 Order 엔티티를 직접 다루는 것 자체가
    단일 책임 원칙(SRP) 위반이었다. 각 Service는 자신의 도메인 엔티티만
    직접 조작하고, 다른 도메인은 해당 Service를 통해서만 접근해야 한다.

profile
개발공부 처음해보는 사람

0개의 댓글