|
| 1 | +package com.loopers.application.purchasing; |
| 2 | + |
| 3 | +import com.loopers.domain.order.Order; |
| 4 | +import com.loopers.domain.order.OrderRepository; |
| 5 | +import com.loopers.domain.order.OrderStatus; |
| 6 | +import com.loopers.infrastructure.paymentgateway.PaymentGatewayClient; |
| 7 | +import com.loopers.infrastructure.paymentgateway.PaymentGatewayDto; |
| 8 | +import com.loopers.infrastructure.user.UserJpaRepository; |
| 9 | +import lombok.RequiredArgsConstructor; |
| 10 | +import lombok.extern.slf4j.Slf4j; |
| 11 | +import org.springframework.scheduling.annotation.Scheduled; |
| 12 | +import org.springframework.stereotype.Component; |
| 13 | +import org.springframework.transaction.annotation.Transactional; |
| 14 | + |
| 15 | +import java.util.List; |
| 16 | + |
| 17 | +/** |
| 18 | + * 결제 상태 복구 스케줄러. |
| 19 | + * <p> |
| 20 | + * 콜백이 오지 않은 PENDING 상태의 주문들을 주기적으로 조회하여 |
| 21 | + * PG 시스템의 결제 상태 확인 API를 통해 상태를 복구합니다. |
| 22 | + * </p> |
| 23 | + * <p> |
| 24 | + * <b>동작 원리:</b> |
| 25 | + * <ol> |
| 26 | + * <li>주기적으로 실행 (기본: 1분마다)</li> |
| 27 | + * <li>PENDING 상태인 주문들을 조회</li> |
| 28 | + * <li>각 주문에 대해 PG 결제 상태 확인 API 호출</li> |
| 29 | + * <li>결제 상태에 따라 주문 상태 업데이트</li> |
| 30 | + * </ol> |
| 31 | + * </p> |
| 32 | + * <p> |
| 33 | + * <b>설계 근거:</b> |
| 34 | + * <ul> |
| 35 | + * <li><b>주기적 복구:</b> 콜백이 오지 않아도 자동으로 상태 복구</li> |
| 36 | + * <li><b>Eventually Consistent:</b> 약간의 지연 허용 가능</li> |
| 37 | + * <li><b>안전한 처리:</b> 각 주문별로 독립적으로 처리하여 실패 시에도 다른 주문에 영향 없음</li> |
| 38 | + * <li><b>성능 고려:</b> 배치로 처리하여 PG 시스템 부하 최소화</li> |
| 39 | + * </ul> |
| 40 | + * </p> |
| 41 | + * |
| 42 | + * @author Loopers |
| 43 | + * @version 1.0 |
| 44 | + */ |
| 45 | +@Slf4j |
| 46 | +@RequiredArgsConstructor |
| 47 | +@Component |
| 48 | +public class PaymentRecoveryScheduler { |
| 49 | + |
| 50 | + private final OrderRepository orderRepository; |
| 51 | + private final UserJpaRepository userJpaRepository; |
| 52 | + private final PurchasingFacade purchasingFacade; |
| 53 | + |
| 54 | + /** |
| 55 | + * PENDING 상태인 주문들의 결제 상태를 복구합니다. |
| 56 | + * <p> |
| 57 | + * 1분마다 실행되어 PENDING 상태인 주문들을 조회하고, |
| 58 | + * 각 주문에 대해 PG 결제 상태 확인 API를 호출하여 상태를 복구합니다. |
| 59 | + * </p> |
| 60 | + * <p> |
| 61 | + * <b>처리 전략:</b> |
| 62 | + * <ul> |
| 63 | + * <li><b>배치 처리:</b> 한 번에 여러 주문 처리</li> |
| 64 | + * <li><b>독립적 처리:</b> 각 주문별로 독립적으로 처리하여 실패 시에도 다른 주문에 영향 없음</li> |
| 65 | + * <li><b>안전한 예외 처리:</b> 개별 주문 처리 실패 시에도 계속 진행</li> |
| 66 | + * </ul> |
| 67 | + * </p> |
| 68 | + */ |
| 69 | + @Scheduled(fixedDelay = 60000) // 1분마다 실행 |
| 70 | + public void recoverPendingOrders() { |
| 71 | + try { |
| 72 | + log.debug("결제 상태 복구 스케줄러 시작"); |
| 73 | + |
| 74 | + // PENDING 상태인 주문들 조회 |
| 75 | + List<Order> pendingOrders = orderRepository.findAllByStatus(OrderStatus.PENDING); |
| 76 | + |
| 77 | + if (pendingOrders.isEmpty()) { |
| 78 | + log.debug("복구할 PENDING 상태 주문이 없습니다."); |
| 79 | + return; |
| 80 | + } |
| 81 | + |
| 82 | + log.info("PENDING 상태 주문 {}건에 대한 결제 상태 복구 시작", pendingOrders.size()); |
| 83 | + |
| 84 | + int successCount = 0; |
| 85 | + int failureCount = 0; |
| 86 | + |
| 87 | + // 각 주문에 대해 결제 상태 확인 및 복구 |
| 88 | + for (Order order : pendingOrders) { |
| 89 | + try { |
| 90 | + // Order의 userId는 User의 id (Long)이므로 User를 조회하여 userId (String)를 가져옴 |
| 91 | + var userOptional = userJpaRepository.findById(order.getUserId()); |
| 92 | + if (userOptional.isEmpty()) { |
| 93 | + log.warn("주문의 사용자를 찾을 수 없습니다. 복구를 건너뜁니다. (orderId: {}, userId: {})", |
| 94 | + order.getId(), order.getUserId()); |
| 95 | + failureCount++; |
| 96 | + continue; |
| 97 | + } |
| 98 | + |
| 99 | + String userId = userOptional.get().getUserId(); |
| 100 | + |
| 101 | + // 결제 상태 확인 및 복구 |
| 102 | + purchasingFacade.recoverOrderStatusByPaymentCheck(userId, order.getId()); |
| 103 | + successCount++; |
| 104 | + } catch (Exception e) { |
| 105 | + // 개별 주문 처리 실패 시에도 계속 진행 |
| 106 | + log.error("주문 상태 복구 중 오류 발생. (orderId: {})", order.getId(), e); |
| 107 | + failureCount++; |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + log.info("결제 상태 복구 완료. 성공: {}건, 실패: {}건", successCount, failureCount); |
| 112 | + |
| 113 | + } catch (Exception e) { |
| 114 | + log.error("결제 상태 복구 스케줄러 실행 중 오류 발생", e); |
| 115 | + } |
| 116 | + } |
| 117 | +} |
| 118 | + |
0 commit comments