PR 링크
https://github.com/standard-Chan/aws-shop/pull/7
결제를 진행하다보면, 멱등성을 보장해야하는 상황이 있다.
결제가 2번 발생할 수 있다.
물론 Toss Payment API 자체에서 중복 결제를 막아주긴한다. 하지만 문제는 다음과 같다.
아래는 현재의 결제 코드이다.
public TossPaymentConfirmResponse confirmPayment(ConfirmPaymentRequest confirmRequest) {
log.info("[Payment] 결제 승인 요청, 결제 정보 : 결제 id={}, 주문 id={}, 결제 금액={}",
confirmRequest.paymentKey(),
confirmRequest.orderId(), confirmRequest.amount());
Payment payment = paymentRepository.findById(confirmRequest.paymentId())
.orElseThrow(() -> new PaymentNotFoundException(confirmRequest.paymentId()));
OrderSummary order = getConfirmableOrder(payment.getOrderId());
// payment 만료 여부 검증
if (payment.isExpired(LocalDateTime.now())) {
payment.expire();
paymentRepository.save(payment);
orderClient.updatePendingOrder(payment.getOrderId());
throw new PaymentExpiredException(payment.getOrderId(), payment.getId());
}
// 종료 상태 결제는 승인 시도 실패로 닫지 않고, 승인 흐름 진입 전에 거부한다.
payment.validateConfirmableStatus(); // 결제 가능한지 상태 검증
List<OrderLineSummary> reservedLines = List.of();
try {
// 결제 로직 검증
payment.validateOrderId(confirmRequest.orderId()); // ★ 위치 try문 밖으로 변경. 주문 검증
payment.validateConfirmAmount(confirmRequest.amount()); // ★ 위치 try 문 밖으로 변경. 수량 검증
// 결제 승인 처리 시작 및 상태 등록
payment.start(confirmRequest.paymentKey()); // ★ 핵심 코드 라인 ★
// 주문 상품 전체 재고 예약 처리
reservedLines = reserveOrderStocks(order);
paymentRepository.save(payment);
TossPaymentConfirmResponse response = tossPaymentClient.confirm(
new TossPaymentConfirmRequest(confirmRequest.paymentId(),
confirmRequest.paymentKey(), confirmRequest.amount()));
// 결제 승인 완료
payment.complete();
log.info("[Payment] 결제 승인 완료. paymentKey={}, paymentId={}, amount={}",
response.paymentKey(), response.orderId(), response.totalAmount());
// Order 완료 처리
orderClient.updateCompleteOrder(payment.getOrderId());
paymentRepository.save(payment);
return response;
} catch (PaymentException | StockException exception) {
// 해당 결제 실패 처리
payment.fail();
paymentRepository.save(payment);
// Order 상태 pending 변경
orderClient.updatePendingOrder(payment.getOrderId());
restoreReservedStocks(reservedLines);
log.warn("[Payment] 결제 실패. {} \n paymentKey={}, orderId={}, amount={}", exception,
confirmRequest.paymentKey(), confirmRequest.orderId(), confirmRequest.amount());
throw new PaymentConfirmExternalException(confirmRequest.paymentId(),
confirmRequest.paymentKey(), exception);
} catch (Exception e) {
log.error(e.getMessage());
throw new PaymentException("[Payment] 알 수 없는 에러가 발생하였습니다.", e);
}
}
위 핵심 코드라인인, payment.start() 에서 상태 변경이 발생한다.
public void start(String paymentKey) {
if (this.status != PaymentStatus.NOT_STARTED) {
throw new PaymentInvalidStatusException(PaymentStatus.NOT_STARTED, this.status);
}
registerPaymentKey(paymentKey);
this.status = PaymentStatus.EXECUTING;
}
해당 결제가 NOT_STARTED 상태라면, 정상 결제처리가 진행되지만, 해당 상태가 아닌 경우, 에러가 발생하는 코드이다.