1616import com .samhap .kokomen .payment .service .dto .ConfirmRequest ;
1717import com .samhap .kokomen .payment .service .dto .PaymentResponse ;
1818import java .net .SocketTimeoutException ;
19+ import java .util .UUID ;
1920import lombok .RequiredArgsConstructor ;
2021import lombok .extern .slf4j .Slf4j ;
22+ import org .springframework .retry .support .RetryTemplate ;
2123import org .springframework .stereotype .Service ;
2224import org .springframework .web .client .HttpClientErrorException ;
2325import org .springframework .web .client .HttpServerErrorException ;
@@ -31,6 +33,7 @@ public class PaymentFacadeService {
3133 private final TosspaymentsTransactionService tosspaymentsTransactionService ;
3234 private final TosspaymentsPaymentService tosspaymentsPaymentService ;
3335 private final TosspaymentsClient tosspaymentsClient ;
36+ private final RetryTemplate tosspaymentsConfirmRetryTemplate ;
3437
3538 public PaymentResponse confirmPayment (ConfirmRequest request ) {
3639 TosspaymentsPayment tosspaymentsPayment = tosspaymentsPaymentService .saveTosspaymentsPayment (request );
@@ -40,20 +43,33 @@ public PaymentResponse confirmPayment(ConfirmRequest request) {
4043 } catch (KokomenException | HttpServerErrorException | ResourceAccessException e ) {
4144 // inner에서 상태 처리 완료
4245 throw e ;
43- } catch (Exception e ) {
46+ } catch (Exception e ) {
4447 // 예상치 못한 예외만 NEED_CANCEL 설정
4548 tosspaymentsPaymentService .updateState (tosspaymentsPayment .getId (), PaymentState .NEED_CANCEL );
4649 throw e ;
4750 }
4851 }
4952
50- private TosspaymentsPaymentResponse confirmPayment (ConfirmRequest request , TosspaymentsPayment tosspaymentsPayment ) {
53+ private TosspaymentsPaymentResponse confirmPayment (ConfirmRequest request ,
54+ TosspaymentsPayment tosspaymentsPayment ) {
55+ String idempotencyKey = UUID .randomUUID ().toString ();
5156 try {
52- TosspaymentsPaymentResponse tosspaymentsConfirmResponse = tosspaymentsClient .confirmPayment (request .toTosspaymentsConfirmRequest ());
53- tosspaymentsPayment .validateTosspaymentsResult (tosspaymentsConfirmResponse .paymentKey (), tosspaymentsConfirmResponse .orderId (),
57+ TosspaymentsPaymentResponse tosspaymentsConfirmResponse = tosspaymentsConfirmRetryTemplate .execute (
58+ context -> {
59+ if (context .getRetryCount () > 0 ) {
60+ log .warn ("토스페이먼츠 결제 승인 재시도 {}회차, paymentKey = {}" ,
61+ context .getRetryCount (), request .paymentKey ());
62+ }
63+ return tosspaymentsClient .confirmPayment (request .toTosspaymentsConfirmRequest (),
64+ idempotencyKey );
65+ });
66+ tosspaymentsPayment .validateTosspaymentsResult (tosspaymentsConfirmResponse .paymentKey (),
67+ tosspaymentsConfirmResponse .orderId (),
5468 tosspaymentsConfirmResponse .totalAmount ());
55- TosspaymentsPaymentResult tosspaymentsPaymentResult = tosspaymentsConfirmResponse .toTosspaymentsPaymentResult (tosspaymentsPayment );
56- tosspaymentsTransactionService .applyTosspaymentsPaymentResult (tosspaymentsPaymentResult , PaymentState .COMPLETED );
69+ TosspaymentsPaymentResult tosspaymentsPaymentResult = tosspaymentsConfirmResponse .toTosspaymentsPaymentResult (
70+ tosspaymentsPayment );
71+ tosspaymentsTransactionService .applyTosspaymentsPaymentResult (tosspaymentsPaymentResult ,
72+ PaymentState .COMPLETED );
5773 return tosspaymentsConfirmResponse ;
5874 } catch (HttpClientErrorException e ) {
5975 throw handleConfirmClientError (e , tosspaymentsPayment );
@@ -66,7 +82,8 @@ private TosspaymentsPaymentResponse confirmPayment(ConfirmRequest request, Tossp
6682 }
6783 }
6884
69- private RuntimeException handleConfirmClientError (HttpClientErrorException e , TosspaymentsPayment tosspaymentsPayment ) {
85+ private RuntimeException handleConfirmClientError (HttpClientErrorException e ,
86+ TosspaymentsPayment tosspaymentsPayment ) {
7087 Failure failure = e .getResponseBodyAs (Failure .class );
7188 if (failure == null ) {
7289 log .error ("토스 결제 실패(400) - 응답 파싱 실패" , e );
@@ -75,6 +92,12 @@ private RuntimeException handleConfirmClientError(HttpClientErrorException e, To
7592 }
7693 String code = failure .code ();
7794
95+ if ("IDEMPOTENT_REQUEST_PROCESSING" .equals (code )) {
96+ log .error ("토스 결제 처리 중 상태 지속 (409), paymentKey = {}" , tosspaymentsPayment .getPaymentKey ());
97+ tosspaymentsPaymentService .updateState (tosspaymentsPayment .getId (), PaymentState .NEED_CANCEL );
98+ return new InternalServerErrorException (PaymentServiceErrorMessage .CONFIRM_SERVER_ERROR .getMessage (), e );
99+ }
100+
78101 if (TosspaymentsInternalServerErrorCode .contains (code )) {
79102 log .error ("토스 결제 실패(서버 원인 400), code = {}, message = {}" , code , failure .message ());
80103 tosspaymentsPaymentService .updateState (tosspaymentsPayment .getId (), PaymentState .SERVER_BAD_REQUEST );
@@ -87,11 +110,13 @@ private RuntimeException handleConfirmClientError(HttpClientErrorException e, To
87110 }
88111
89112 private void handleConfirmServerError (HttpServerErrorException e , TosspaymentsPayment tosspaymentsPayment ) {
90- // TODO: retry
91113 try {
92- TosspaymentsPaymentResponse tosspaymentsConfirmResponse = e .getResponseBodyAs (TosspaymentsPaymentResponse .class );
93- TosspaymentsPaymentResult tosspaymentsPaymentResult = tosspaymentsConfirmResponse .toTosspaymentsPaymentResult (tosspaymentsPayment );
94- tosspaymentsTransactionService .applyTosspaymentsPaymentResult (tosspaymentsPaymentResult , PaymentState .NEED_CANCEL );
114+ TosspaymentsPaymentResponse tosspaymentsConfirmResponse = e .getResponseBodyAs (
115+ TosspaymentsPaymentResponse .class );
116+ TosspaymentsPaymentResult tosspaymentsPaymentResult = tosspaymentsConfirmResponse .toTosspaymentsPaymentResult (
117+ tosspaymentsPayment );
118+ tosspaymentsTransactionService .applyTosspaymentsPaymentResult (tosspaymentsPaymentResult ,
119+ PaymentState .NEED_CANCEL );
95120 } catch (Exception parseException ) {
96121 log .warn ("토스 5xx 응답 파싱 실패, 상태만 업데이트합니다. paymentId = {}" , tosspaymentsPayment .getId (), parseException );
97122 tosspaymentsPaymentService .updateState (tosspaymentsPayment .getId (), PaymentState .NEED_CANCEL );
@@ -101,12 +126,10 @@ private void handleConfirmServerError(HttpServerErrorException e, TosspaymentsPa
101126 private void handleConfirmNetworkError (ResourceAccessException e , TosspaymentsPayment tosspaymentsPayment ) {
102127 if (e .getRootCause () instanceof SocketTimeoutException socketTimeoutException ) {
103128 if (socketTimeoutException .getMessage ().contains ("Connect timed out" )) {
104- // TODO: retry
105129 tosspaymentsPaymentService .updateState (tosspaymentsPayment .getId (), PaymentState .CONNECTION_TIMEOUT );
106130 return ;
107131 }
108132 if (socketTimeoutException .getMessage ().contains ("Read timed out" )) {
109- // TODO: retry
110133 tosspaymentsPaymentService .updateState (tosspaymentsPayment .getId (), PaymentState .NEED_CANCEL );
111134 return ;
112135 }
@@ -115,17 +138,20 @@ private void handleConfirmNetworkError(ResourceAccessException e, TosspaymentsPa
115138 }
116139
117140 public void cancelPayment (CancelRequest request ) {
118- TosspaymentsPaymentCancelRequest tosspaymentsPaymentCancelRequest = new TosspaymentsPaymentCancelRequest (request .cancelReason ());
141+ TosspaymentsPaymentCancelRequest tosspaymentsPaymentCancelRequest = new TosspaymentsPaymentCancelRequest (
142+ request .cancelReason ());
119143 try {
120- TosspaymentsPaymentResponse response = tosspaymentsClient .cancelPayment (request .paymentKey (), tosspaymentsPaymentCancelRequest );
144+ TosspaymentsPaymentResponse response = tosspaymentsClient .cancelPayment (request .paymentKey (),
145+ tosspaymentsPaymentCancelRequest );
121146 tosspaymentsTransactionService .applyCancelResult (response );
122147 } catch (HttpClientErrorException e ) {
123148 Failure failure = e .getResponseBodyAs (Failure .class );
124149 if (failure == null ) {
125150 log .error ("결제 취소 실패(400) - 응답 파싱 실패, paymentKey: {}" , request .paymentKey (), e );
126151 throw new InternalServerErrorException (PaymentServiceErrorMessage .CANCEL_SERVER_ERROR .getMessage (), e );
127152 }
128- log .error ("결제 취소 실패(400) - paymentKey: {}, code: {}, message: {}" , request .paymentKey (), failure .code (), failure .message ());
153+ log .error ("결제 취소 실패(400) - paymentKey: {}, code: {}, message: {}" , request .paymentKey (), failure .code (),
154+ failure .message ());
129155 throw new BadRequestException (failure .message (), e );
130156 } catch (HttpServerErrorException e ) {
131157 log .error ("결제 취소 실패(5xx) - paymentKey: {}, status: {}" , request .paymentKey (), e .getStatusCode ());
0 commit comments