11package com .back .api .payment .payment .service ;
22
3+ import java .util .Optional ;
4+
35import org .springframework .context .ApplicationEventPublisher ;
46import org .springframework .stereotype .Service ;
57import org .springframework .transaction .annotation .Transactional ;
68
79import com .back .api .payment .order .service .OrderService ;
810import com .back .api .payment .payment .client .PaymentClient ;
11+ import com .back .domain .payment .order .entity .V2_Order ;
912import com .back .api .payment .payment .dto .request .PaymentConfirmCommand ;
1013import com .back .api .payment .payment .dto .request .V2_PaymentConfirmRequest ;
1114import com .back .api .payment .payment .dto .response .PaymentConfirmResult ;
1518import com .back .api .queue .service .QueueEntryProcessService ;
1619import com .back .api .ticket .service .TicketService ;
1720import com .back .domain .notification .systemMessage .OrderSuccessMessage ;
18- import com .back .domain .notification .systemMessage .OrderSuccessV2Message ;
1921import com .back .domain .payment .order .entity .Order ;
20- import com .back .domain .payment .order .entity .V2_Order ;
2122import com .back .domain .payment .payment .entity .ApproveStatus ;
22- import com .back .domain .payment .payment .entity .Payment ;
23- import com .back .domain .payment .payment .repository .PaymentRepository ;
2423import com .back .domain .ticket .entity .Ticket ;
2524import com .back .global .error .code .PaymentErrorCode ;
2625import com .back .global .error .exception .ErrorException ;
2928import lombok .RequiredArgsConstructor ;
3029import lombok .extern .slf4j .Slf4j ;
3130
32- /**
33- * Payment 관련 비즈니스 로직 처리
34- * TODO: PG 연동 시 트랜잭션 경계 재설정 필요
35- *
36- * [설계 방향]
37- * 1. PG API 호출: 트랜잭션 밖으로 이동
38- * 2. 핵심 DB 변경만 @Transactional
39- * 3. Queue/Notification: @TransactionalEventListener(AFTER_COMMIT)
40- *
41- * [리팩토링 시점]
42- * - 실제 PG 연동 구현 시 (TossPaymentClient 등)
43- */
4431@ Service
4532@ Slf4j
4633@ RequiredArgsConstructor
@@ -51,8 +38,8 @@ public class PaymentService {
5138 private final TicketService ticketService ;
5239 private final QueueEntryProcessService queueEntryProcessService ;
5340 private final ApplicationEventPublisher eventPublisher ;
54- private final PaymentRepository paymentRepository ;
5541 private final TossPaymentService tossPaymentService ;
42+ private final PaymentTransactionService paymentTransactionService ;
5643 private final BusinessMetrics businessMetrics ;
5744
5845 @ Transactional
@@ -138,82 +125,47 @@ public PaymentReceiptResponse getPaymentReceipt(Long orderId, Long userId) {
138125 return PaymentReceiptResponse .from (order , ticket );
139126 }
140127
141- @ Transactional
128+ /**
129+ * V2 결제 승인 - PG 호출은 트랜잭션 밖에서 처리
130+ *
131+ * 트랜잭션 분리 이유:
132+ * - PG API 호출 중 DB 커넥션 점유 방지
133+ * - 외부 API 타임아웃 시 트랜잭션 롤백 문제 방지
134+ *
135+ * 처리 흐름:
136+ * 0. 멱등성 확인 (이미 결제 완료된 주문이면 기존 결과 반환)
137+ * 1. Order 검증 (읽기 트랜잭션)
138+ * 2. PG API 호출 (트랜잭션 밖)
139+ * 3. 성공/실패 DB 처리 (각각 단일 쓰기 트랜잭션 - PaymentTransactionService)
140+ */
142141 public V2_PaymentConfirmResponse v2_confirmPayment (
143142 String orderId ,
144143 String paymentKey ,
145144 Long clientAmount ,
146145 Long userId
147146 ) {
147+ // 0. 멱등성 확인: 이미 결제 완료된 주문이면 기존 결과 반환
148+ Optional <V2_Order > paidOrder = orderService .v2_findPaidOrder (orderId , userId );
149+ if (paidOrder .isPresent ()) {
150+ log .info ("[Payment] 이미 결제 완료된 주문 - orderId: {}" , orderId );
151+ // 간단한 응답 반환 (전체 데이터 조회 불필요)
152+ return new V2_PaymentConfirmResponse (orderId , true );
153+ }
148154
149- // OrderService가 order의 정합성(주문자/주문상태/amount) 보장
150- V2_Order order = orderService .v2_getOrderForPayment (orderId , userId , clientAmount );
151-
152- // log.info("[v2 결제 디버깅] - 결제 승인 메서드: 결제 서비스 로그");
153- // log.info("[v2 결제 디버깅] - orderId : {}", orderId);
154- // log.info("[v2 결제 디버깅] - paymentKey : {}", paymentKey);
155- // log.info("[v2 결제 디버깅] - userId : {}", userId);
156-
157- V2_PaymentConfirmRequest request = new V2_PaymentConfirmRequest (orderId , paymentKey , order .getAmount ());
155+ // 1. Order 검증 (읽기 트랜잭션)
156+ Long ticketId = orderService .v2_validateAndGetTicketId (orderId , userId , clientAmount );
158157
158+ // 2. PG 호출 (트랜잭션 밖)
159+ V2_PaymentConfirmRequest request = new V2_PaymentConfirmRequest (orderId , paymentKey , clientAmount );
159160 TossPaymentResponse result = tossPaymentService .confirmPayment (request );
160161
161- log .info ("[v2 결제 디버깅] - 결제 승인 결과 {}" , result .status ());
162-
163- if (result .status () != ApproveStatus .DONE ) { // 결제 승인 완료시 토스 API 응답 : Status = "DONE"
164- order .markFailed ();
165- ticketService .failPayment (order .getTicket ().getId ()); // Ticket FAILED + Seat 해제
162+ // 3. 결과에 따라 분기 - PaymentTransactionService에서 단일 트랜잭션으로 처리
163+ if (result .status () != ApproveStatus .DONE ) {
164+ paymentTransactionService .handleFailure (orderId , ticketId );
166165 businessMetrics .paymentConfirmFailure ("TOSS_PAYMENT_NOT_DONE" );
167- //TODO 결제 실패 로직 추가
168166 throw new ErrorException (PaymentErrorCode .PAYMENT_FAILED );
169167 }
170168
171- //결제 엔티티 생성 및 DB 저장 (결제 정보 저장)
172- Payment savedPayment = paymentRepository .save (
173- new Payment (
174- paymentKey ,
175- orderId ,
176- order .getAmount (),
177- result .method (),
178- result .status ()
179- )
180- );
181-
182- order .setPayment (savedPayment );
183-
184- // Order status PENDING -> PAID, paymentKey DB 저장 (주문 상태 업테이트)
185- order .markPaid (result .paymentKey ());
186-
187- // 테스트용 : DataInit으로 만든 테스트 데이터 사용한 결제 테스트에서 사용
188- //order.getTicket().getSeat().markAsReserved();
189-
190- // Ticket 발급
191- Ticket ticket = ticketService .confirmPayment (
192- order .getTicket ().getId (),
193- userId
194- );
195-
196- // 결제 성공 메트릭
197- businessMetrics .paymentConfirmSuccess (ticket .getEvent ().getId ());
198-
199- // Queue 완료 // 테스트 데이터로 진행 시 : 큐 대기열이 없으므로 이부분도 주석처리
200- queueEntryProcessService .completePayment (
201- ticket .getEvent ().getId (),
202- userId
203- );
204-
205- String eventTitle = ticket .getEvent ().getTitle ();
206-
207- // 알림 메시지 발행
208- eventPublisher .publishEvent (
209- new OrderSuccessV2Message (
210- userId ,
211- orderId ,
212- order .getAmount (),
213- eventTitle
214- )
215- );
216-
217- return V2_PaymentConfirmResponse .from (order , true );
169+ return paymentTransactionService .handleSuccess (orderId , result , userId );
218170 }
219171}
0 commit comments