문제 상황
Spring Scheduler를 활용하여 10분이 지난 IN_PROGRESS 상태의 결제를 자동으로 취소하는 방식으로 구현하면서 트러블 슈팅이 발생했습니다.
(악의적 사용자가 아닌)일반 사용자가 어떠한 사유로 9분 5x초에 결제를 진행하면 결제 요청과 스케줄러의 실행이 동시에 이루어질 수 있습니다. 이로 인해 실제 결제는 성공했지만 시스템에서는 결제가 취소되는 데이터 정합성 문제가 발생했습니다.
[문제 상황 설명]

- 사용자가 결제 페이지에 진입
- Payment 상태 : IN_PROGRESS
- 사용자는 결제 정보 입력 후, 9분 5x초에 결제 진행
- TossPayments에 결제 요청
- TossPayments에서 /success 엔드포인트로 PaymentKey, orderId, amount(가격) 정보와 함께 POST 요청을 보냄
- DB에 저장된 결제 금액과 TossPayments의 요청 금액을 검증
- 검증을 진행하면서 Payment 상태를 CONFIRMING으로 변경
- 동시에 10분이 지나면서 스케줄러 실행
- 스케줄러가 IN_PROGRESS 상태이고 10분이 지난 Payments들을 조회
- 스케줄러가 조회한 시점은 IN_PROGRESS 이므로 CONFIRMING으로 변경 되어도 스케줄러는 주문 취소를 진행
- TossPayment에 최종 승인 요청
- TossPayments로 최종 승인 요청을 보냄
- 실제 결제는 성공했지만 금액이 차감됨.
→문제 발생!
- 결제는 성공했는데, 시스템에서는 결제가 취소된 것으로 기록
- 결국 사용자는 돈을 냈지만 결제 취소가 되는 치명적인 오류가 발생!
[문제 발생 원인 분석]
- 트랜잭션 타이밍 이슈
- 스케줄러가 실행될 때 Payment 상태가 IN_PROGRESS로 조회됨
- 하지만 거의 동시에 결제 승인 요청이 들어와 Payment 상태가 CONFIRMING으로 변경됨
- 스케줄러는 이미 조회된 Payment 정보를 기반으로 CANCEL 처리를 진행하여 실제 결제가 완료되었음에도 불구하고 시스템에서는 결제가 취소된 상태가 되어버림
해결과정
- 스케줄러가 실행될 때 Payment 데이터를 조회하면서 상태를 IN_PROGRESS로 확인한다.
- 이 상태가 CONFIRMING으로 변경되었더라도 스케줄러가 이미 조회한 데이터 기준으로 취소를 진행할 가능성이 있다.
- 즉 이미 조회된 데이터(IN_PROGRESS)를 기반으로 취소를 실행하면 결국 결제가 완료되었는데도 취소될 수 있다!
⇒ "그럼 스케줄러가 조회하고 다시 한번 최신상태로 조회하는 로직을 짜면 어떨까?" 생각을 했습니다.

@Scheduled(cron = "0 */10 * * * *")
public void paymentTimeoutScheduler() {
List<Payment> payments = paymentRepository.findAllExpired(); // 10분이 지난 IN_PROGRESS 상태들을 불러옴.
for (Payment payment : payments) {
paymentStateService.cancelTimeOutPayments(payment);
}
}
@Transactional
public void cancelTimeOutPayments(Payment payment) {
// 여기서 다시 한번 조회하여 최신 상태 조회
Payment paymentInDB = paymentRepository.findById(payment.getId())
.orElseThrow();
if (paymentInDB.getState() != PaymentState.IN_PROGRESS) {
return;
}
paymentInDB.updatePaymentStatus(PaymentState.CANCEL);
paymentInDB.getOrder().updateState(OrderState.CANCELED);
paymentInDB.getOrder().getTickets().forEach(orderTicket ->
orderTicket.getTicket().updateState(SeatState.IDLE));
paymentRepository.save(paymentInDB);
}
- 스케줄러가 실행되고 1차적으로 결제 진행이 10분 이상된 결제 정보들을 불러오고 그 사이에 결제 상태가 바뀐 것을 해결하기 위해 최종적으로 조회하여 최신 상태를 반영 합니다.
- IN_PROGRESS 상태가 CONFIRMING 또는 COMPLETED로 변경되었다면 취소하지 않고 바로 return하여 스케줄 메서드를 종료합니다.
결론
이번 트러블 슈팅을 통해 스케줄러가 조회한 오래된 데이터(IN_PROGRESS 상태)로 인해 결제가 완료되었음에도 취소되는 문제를 해결할 수 있었습니다. 기존에는 스케줄러가 조회한 데이터를 바로 취소 처리하는 방식이었지만, 최신 상태를 다시 한 번 조회하는 로직을 추가함으로써, CONFIRMING 또는 COMPLETED 상태로 변경된 결제는 취소되지 않도록 방어 로직을 적용하였습니다.
이를 통해 데이터 정합성을 유지하면서도 불필요한 취소를 방지할 수 있었으며, 결제 과정에서 발생하는 타이밍 이슈를 보다 안전하게 처리할 수 있게 되었습니다.
'Project' 카테고리의 다른 글
| 스프링 프로젝트에서 공통 응답 + 페이징 + 전역 예외처리 (0) | 2025.03.30 |
|---|---|
| 결제 시나리오 3. 악의적 선점 - 데이터 정합성 문제(2) (0) | 2025.03.17 |
| 결제 시나리오 3. 악의적 예약 선점 (0) | 2025.03.17 |
| 결제 시나리오 2. 중복 결제 방지 (0) | 2025.03.17 |
| 결제 시나리오 1. 결제 재시도 - Self-Invocation 문제 (0) | 2025.03.17 |