상황: 결제 승인 비즈니스 로직 중, 외부 API 호출과 내부 DB 반영을 분리하고 DB 작업에만 트랜잭션을 걸었으나 트랜잭션이 적용되지 않음.
@Service
@RequiredArgsConstructor
public class PaymentConfirmPaymentUseCase {
// ... 의존성 주입 ...
public PaymentConfirmResponseDto confirmPayment(Long memberId, PaymentConfirmRequestDto requestDto) {
// ... 검증 로직 ...
// 1. 외부 API 호출 (Non-Transactional)
TossPaymentsConfirmResponseDto tossResponse = tossPaymentsApiClient.confirm(requestDto);
// 2. 내부 DB 반영 메소드 호출 (문제 발생 지점)
// 'this'를 통해 호출하므로 Spring 프록시를 거치지 않음
return processPaymentFinalization(payment, tossResponse);
}
@Transactional // -> 동작하지 않음 (무시됨)
protected PaymentConfirmResponseDto processPaymentFinalization(Payment payment, TossPaymentsConfirmResponseDto tossResponse) {
// 3. 여러 DB 작업 수행 (원자성 보장 안 됨)
payment.complete(tossResponse.paymentKey());
wallet.addBalance(payment.getAmount());
paymentTransactionRepository.save(paymentTransaction);
return PaymentConfirmResponseDto.of(tossResponse, wallet.getBalance());
}
}
"Spring AOP의 프록시(Proxy) 동작 방식에 의한 제한"
@Transactional은 해당 빈(Bean)을 감싸는 프록시 객체를 통해 동작한다. 외부에서 호출할 때는 프록시를 통하므로 트랜잭션 처리가 시작(begin)되고 종료(commit/rollback)된다.this.method())할 때는 프록시를 거치지 않고 실제 인스턴스(Target)의 메소드를 직접 실행한다.@Transactional 어노테이션은 무시되고, 단순한 메소드 호출로 처리되어 트랜잭션 관리가 전혀 이루어지지 않는다."트랜잭션 메소드를 별도의 클래스(Bean)로 분리하여 외부 호출로 변경"
DB 트랜잭션이 필요한 로직을 별도의 컴포넌트(Service)로 추출하고, 이를 주입받아 호출함으로써 프록시를 경유하도록 구조를 개선한다.
3-1. 트랜잭션 전용 클래스 생성 (PaymentConfirmFinalizer)
@Component // 또는 @Service
@RequiredArgsConstructor
public class PaymentConfirmFinalizer {
private final WalletRepository walletRepository;
private final PaymentRepository paymentRepository;
private final PaymentTransactionRepository paymentTransactionRepository;
@Transactional // 트랜잭션 정상 작동
public PaymentConfirmResponseDto finalizePayment(Payment payment, TossPaymentsConfirmResponseDto tossResponse) {
// 기존 processPaymentFinalization 내부 로직 이동
payment.complete(tossResponse.paymentKey());
paymentRepository.save(payment); // payment는 영속성 컨텍스트와 연결이 끊긴 상태라 명시적으로 저장
Wallet wallet = walletRepository.findByMemberId(payment.getMember().getId())
.orElseThrow(() -> new CustomException(ErrorType.WALLET_NOT_FOUND));
wallet.addBalance(payment.getAmount());
PaymentTransaction paymentTransaction = PaymentTransaction.builder().build();
paymentTransactionRepository.save(paymentTransaction);
return PaymentConfirmResponseDto.of(tossResponse, wallet.getBalance());
}
}
3-2. 기존 UseCase 수정
@Service
@RequiredArgsConstructor
public class PaymentConfirmPaymentUseCase {
private final TossPaymentsApiClient tossPaymentsApiClient;
// ... 기존 Repository ...
private final PaymentFinalizer paymentFinalizer; // 분리한 컴포넌트 주입
public PaymentConfirmResponseDto confirmPayment(Long memberId, PaymentConfirmRequestDto requestDto) {
Payment payment = validateRequest(memberId, requestDto);
// 1. 외부 API 호출 (트랜잭션 없음, 유지)
TossPaymentsConfirmResponseDto tossResponse = tossPaymentsApiClient.confirm(requestDto);
// 2. 외부 빈을 통한 호출로 변경 (프록시 경유 -> 트랜잭션 적용됨)
return paymentConfirmFinalizer.finalizePayment(payment, tossResponse);
}
// ... validateRequest 로직 ...
}