Date: Mon, 8 Dec 2025 00:08:11 +0900
Subject: [PATCH 16/22] =?UTF-8?q?refactor:=20=EC=A3=BC=EB=AC=B8=20?=
=?UTF-8?q?=EA=B2=B0=EC=A0=9C=EC=8B=9C=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?=
=?UTF-8?q?=EB=98=90=EB=8A=94=20=EC=B9=B4=EB=93=9C=EB=A7=8C=EC=9D=84=20?=
=?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20=EA=B2=B0=EC=A0=9C=20?=
=?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98?=
=?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../purchasing/PurchasingFacade.java | 179 ++++++++++++------
.../order/OrderCancellationService.java | 13 +-
.../com/loopers/domain/payment/Payment.java | 40 +++-
.../domain/payment/PaymentService.java | 29 +++
.../purchasing/PurchasingV1Controller.java | 1 +
.../api/purchasing/PurchasingV1Dto.java | 7 +-
6 files changed, 209 insertions(+), 60 deletions(-)
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
index e24f92f05..2d5e89b64 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
@@ -15,6 +15,7 @@
import com.loopers.domain.order.OrderPaymentResultService;
import com.loopers.domain.order.OrderCancellationService;
import com.loopers.domain.payment.PaymentService;
+import com.loopers.domain.payment.Payment;
import com.loopers.domain.payment.PaymentStatus;
import com.loopers.domain.payment.CardType;
import com.loopers.support.error.CoreException;
@@ -24,6 +25,8 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.springframework.transaction.support.TransactionTemplate;
+import org.springframework.transaction.PlatformTransactionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -52,15 +55,26 @@ public class PurchasingFacade {
private final OrderCancellationService orderCancellationService;
private final OrderPaymentResultService orderPaymentResultService;
private final PaymentService paymentService; // Payment 관련: PaymentService만 의존 (DIP 준수)
+ private final PlatformTransactionManager transactionManager;
/**
* 주문을 생성한다.
*
* 1. 사용자 조회 및 존재 여부 검증
* 2. 상품 재고 검증 및 차감
- * 3. 사용자 포인트 검증 및 차감
- * 4. 주문 저장
- * 5. PG 결제 요청 (비동기)
+ * 3. 쿠폰 할인 적용
+ * 4. 사용자 포인트 차감 (지정된 금액만)
+ * 5. 주문 저장
+ * 6. Payment 생성 (포인트+쿠폰 혼합 지원)
+ * 7. PG 결제 금액이 0이면 바로 완료, 아니면 PG 결제 요청 (비동기)
+ *
+ *
+ * 결제 방식:
+ *
+ * - 포인트+쿠폰 전액 결제: paidAmount == 0이면 PG 요청 없이 바로 완료
+ * - 혼합 결제: 포인트 일부 사용 + PG 결제 나머지 금액
+ * - 카드만 결제: 포인트 사용 없이 카드로 전체 금액 결제
+ *
*
*
* 동시성 제어 전략:
@@ -92,12 +106,13 @@ public class PurchasingFacade {
*
* @param userId 사용자 식별자 (로그인 ID)
* @param commands 주문 상품 정보
- * @param cardType 카드 타입 (SAMSUNG, KB, HYUNDAI)
- * @param cardNo 카드 번호 (xxxx-xxxx-xxxx-xxxx 형식)
+ * @param usedPoint 포인트 사용량 (선택, 기본값: 0)
+ * @param cardType 카드 타입 (paidAmount > 0일 때만 필수)
+ * @param cardNo 카드 번호 (paidAmount > 0일 때만 필수)
* @return 생성된 주문 정보
*/
@Transactional
- public OrderInfo createOrder(String userId, List commands, String cardType, String cardNo) {
+ public OrderInfo createOrder(String userId, List commands, Long usedPoint, String cardType, String cardNo) {
if (userId == null || userId.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "사용자 ID는 필수입니다.");
}
@@ -159,41 +174,84 @@ public OrderInfo createOrder(String userId, List commands, Str
discountAmount = couponService.applyCoupon(user.getId(), couponCode, calculateSubtotal(orderItems));
}
+ // 주문 총액 계산 (쿠폰 할인 적용)
+ Integer orderTotalAmount = calculateSubtotal(orderItems) - discountAmount;
+
+ // 포인트 차감 (지정된 금액만)
+ Long usedPointAmount = Objects.requireNonNullElse(usedPoint, 0L);
+
+ // 포인트 잔액 검증: 포인트를 사용하는 경우에만 검증
+ // 재고 차감 전에 검증하여 원자성 보장 (검증 실패 시 아무것도 변경되지 않음)
+ if (usedPointAmount > 0) {
+ Long userPointBalance = user.getPoint().getValue();
+ if (userPointBalance < usedPointAmount) {
+ throw new CoreException(ErrorType.BAD_REQUEST,
+ String.format("포인트가 부족합니다. (현재 잔액: %d, 사용 요청 금액: %d)", userPointBalance, usedPointAmount));
+ }
+ }
+
// OrderService를 사용하여 주문 생성
Order savedOrder = orderService.create(user.getId(), orderItems, couponCode, discountAmount);
// 주문은 PENDING 상태로 생성됨 (Order 생성자에서 기본값으로 설정)
// 결제 성공 후에만 COMPLETED로 변경됨
+ // 재고 차감
decreaseStocksForOrderItems(savedOrder.getItems(), products);
- deductUserPoint(user, savedOrder.getTotalAmount());
- // 주문은 PENDING 상태로 유지 (결제 요청 중 상태)
- // 결제 성공 시 콜백이나 상태 확인 API를 통해 COMPLETED로 변경됨
- productService.saveAll(products);
- userService.save(user);
- // 주문은 PENDING 상태로 저장됨
+ // 포인트 차감 (지정된 금액만)
+ if (usedPointAmount > 0) {
+ deductUserPoint(user, usedPointAmount.intValue());
+ }
- // Payment 생성 (PaymentService 사용)
- paymentService.create(
+ // PG 결제 금액 계산
+ // Order.getTotalAmount()는 이미 쿠폰 할인이 적용된 금액이므로 discountAmount를 다시 빼면 안 됨
+ Long totalAmount = savedOrder.getTotalAmount().longValue();
+ Long paidAmount = totalAmount - usedPointAmount;
+
+ // Payment 생성 (포인트+쿠폰 혼합 지원)
+ CardType cardTypeEnum = (cardType != null && !cardType.isBlank()) ? convertCardType(cardType) : null;
+ Payment payment = paymentService.create(
savedOrder.getId(),
user.getId(),
- convertCardType(cardType),
+ totalAmount,
+ usedPointAmount,
+ cardTypeEnum,
cardNo,
- savedOrder.getTotalAmount().longValue(),
java.time.LocalDateTime.now()
);
+ // 포인트+쿠폰으로 전액 결제 완료된 경우
+ if (paidAmount == 0) {
+ // PG 요청 없이 바로 완료
+ orderService.completeOrder(savedOrder.getId());
+ paymentService.toSuccess(payment.getId(), java.time.LocalDateTime.now());
+ productService.saveAll(products);
+ userService.save(user);
+ log.info("포인트+쿠폰으로 전액 결제 완료. (orderId: {})", savedOrder.getId());
+ return OrderInfo.from(savedOrder);
+ }
+
+ // PG 결제가 필요한 경우
+ if (cardType == null || cardType.isBlank() || cardNo == null || cardNo.isBlank()) {
+ throw new CoreException(ErrorType.BAD_REQUEST,
+ "포인트와 쿠폰만으로 결제할 수 없습니다. 카드 정보를 입력해주세요.");
+ }
+
+ productService.saveAll(products);
+ userService.save(user);
+
// PG 결제 요청을 트랜잭션 커밋 후에 실행하여 DB 커넥션 풀 고갈 방지
// 트랜잭션 내에서 외부 HTTP 호출을 하면 PG 지연/타임아웃 시 DB 커넥션이 오래 유지되어 커넥션 풀 고갈 위험
Long orderId = savedOrder.getId();
- Integer totalAmount = savedOrder.getTotalAmount();
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 트랜잭션 커밋 후 PG 호출 (DB 커넥션 해제 후 실행)
try {
- String transactionKey = requestPaymentToGateway(userId, user.getId(), orderId, cardType, cardNo, totalAmount);
+ String transactionKey = requestPaymentToGateway(
+ userId, user.getId(), orderId, cardType, cardNo, paidAmount.intValue()
+ );
if (transactionKey != null) {
// 결제 성공: 별도 트랜잭션에서 주문 상태를 COMPLETED로 변경
updateOrderStatusToCompleted(orderId, transactionKey);
@@ -437,6 +495,13 @@ private String requestPaymentToGateway(String userId, Long userEntityId, Long or
// PaymentService 내부에서 이미 실패 분류가 완료되었으므로, 여기서는 처리만 수행
// 비즈니스 실패는 PaymentService에서 이미 처리되었으므로, 여기서는 타임아웃/외부 시스템 장애만 처리
+ // Circuit Breaker OPEN은 외부 시스템 장애이므로 주문을 취소하지 않음
+ if ("CIRCUIT_BREAKER_OPEN".equals(failure.errorCode())) {
+ // 외부 시스템 장애: 주문은 PENDING 상태로 유지
+ log.info("Circuit Breaker OPEN 상태. 주문은 PENDING 상태로 유지됩니다. (orderId: {})", orderId);
+ return null;
+ }
+
if (failure.isTimeout()) {
// 타임아웃: 상태 확인 후 복구
log.info("타임아웃 발생. PG 결제 상태 확인 API를 호출하여 실제 결제 상태를 확인합니다. (orderId: {})", orderId);
@@ -673,9 +738,9 @@ public void recoverOrderStatusByPaymentCheck(String userId, Long orderId) {
*
* 트랜잭션 전략:
*
- * - REQUIRES_NEW: 별도 트랜잭션으로 실행하여 외부 시스템 호출과 독립적으로 처리
+ * - TransactionTemplate 사용: afterCommit 콜백에서 호출되므로 명시적으로 새 트랜잭션 생성
* - 결제 실패 처리 중 오류가 발생해도 기존 주문 생성 트랜잭션에 영향을 주지 않음
- * - Self-invocation 문제 해결: 별도 메서드로 분리하여 Spring AOP 프록시가 정상적으로 적용되도록 함
+ * - Self-invocation 문제 해결: TransactionTemplate을 사용하여 명시적으로 트랜잭션 관리
*
*
*
@@ -691,44 +756,52 @@ public void recoverOrderStatusByPaymentCheck(String userId, Long orderId) {
* @param errorCode 오류 코드
* @param errorMessage 오류 메시지
*/
- @Transactional(propagation = Propagation.REQUIRES_NEW)
private void handlePaymentFailure(String userId, Long orderId, String errorCode, String errorMessage) {
- try {
- // 사용자 조회 (Service를 통한 접근)
- User user;
- try {
- user = userService.findByUserId(userId);
- } catch (CoreException e) {
- log.warn("결제 실패 처리 시 사용자를 찾을 수 없습니다. (userId: {}, orderId: {})", userId, orderId);
- return;
- }
-
- // 주문 조회 (Service를 통한 접근)
- Order order;
+ // TransactionTemplate을 사용하여 명시적으로 새 트랜잭션 생성
+ // afterCommit 콜백에서 호출되므로 @Transactional 어노테이션이 작동하지 않음
+ TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
+ transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
+
+ transactionTemplate.executeWithoutResult(status -> {
try {
- order = orderService.getById(orderId);
- } catch (CoreException e) {
- log.warn("결제 실패 처리 시 주문을 찾을 수 없습니다. (orderId: {})", orderId);
- return;
- }
+ // 사용자 조회 (Service를 통한 접근)
+ User user;
+ try {
+ user = userService.findByUserId(userId);
+ } catch (CoreException e) {
+ log.warn("결제 실패 처리 시 사용자를 찾을 수 없습니다. (userId: {}, orderId: {})", userId, orderId);
+ return;
+ }
- // 이미 취소된 주문인 경우 처리하지 않음
- if (order.getStatus() == OrderStatus.CANCELED) {
- log.info("이미 취소된 주문입니다. 결제 실패 처리를 건너뜁니다. (orderId: {})", orderId);
- return;
- }
+ // 주문 조회 (Service를 통한 접근)
+ Order order;
+ try {
+ order = orderService.getById(orderId);
+ } catch (CoreException e) {
+ log.warn("결제 실패 처리 시 주문을 찾을 수 없습니다. (orderId: {})", orderId);
+ return;
+ }
- // 주문 취소 및 리소스 원복
- orderCancellationService.cancel(order, user);
+ // 이미 취소된 주문인 경우 처리하지 않음
+ if (order.getStatus() == OrderStatus.CANCELED) {
+ log.info("이미 취소된 주문입니다. 결제 실패 처리를 건너뜁니다. (orderId: {})", orderId);
+ return;
+ }
- log.info("결제 실패로 인한 주문 취소 완료. (orderId: {}, errorCode: {}, errorMessage: {})",
- orderId, errorCode, errorMessage);
- } catch (Exception e) {
- // 결제 실패 처리 중 오류 발생 시에도 로그만 기록
- // 이미 주문은 생성되어 있으므로, 나중에 수동으로 처리할 수 있도록 로그 기록
- log.error("결제 실패 처리 중 오류 발생. (orderId: {}, errorCode: {})",
- orderId, errorCode, e);
- }
+ // 주문 취소 및 리소스 원복
+ orderCancellationService.cancel(order, user);
+
+ log.info("결제 실패로 인한 주문 취소 완료. (orderId: {}, errorCode: {}, errorMessage: {})",
+ orderId, errorCode, errorMessage);
+ } catch (Exception e) {
+ // 결제 실패 처리 중 오류 발생 시에도 로그만 기록
+ // 이미 주문은 생성되어 있으므로, 나중에 수동으로 처리할 수 있도록 로그 기록
+ log.error("결제 실패 처리 중 오류 발생. (orderId: {}, errorCode: {})",
+ orderId, errorCode, e);
+ // 예외를 다시 던져서 트랜잭션이 롤백되도록 함
+ throw new RuntimeException("결제 실패 처리 중 오류 발생", e);
+ }
+ });
}
}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderCancellationService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderCancellationService.java
index 643644749..aeb549631 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderCancellationService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderCancellationService.java
@@ -1,5 +1,7 @@
package com.loopers.domain.order;
+import com.loopers.domain.payment.Payment;
+import com.loopers.domain.payment.PaymentService;
import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductRepository;
import com.loopers.domain.user.Point;
@@ -33,6 +35,7 @@ public class OrderCancellationService {
private final OrderRepository orderRepository;
private final UserRepository userRepository;
private final ProductRepository productRepository;
+ private final PaymentService paymentService;
/**
* 주문을 취소하고 포인트를 환불하며 재고를 원복합니다.
@@ -82,7 +85,15 @@ public void cancel(Order order, User user) {
order.cancel();
increaseStocksForOrderItems(order.getItems(), products);
- lockedUser.receivePoint(Point.of((long) order.getTotalAmount()));
+
+ // 실제로 사용된 포인트만 환불 (Payment에서 확인)
+ Long refundPointAmount = paymentService.findByOrderId(order.getId())
+ .map(Payment::getUsedPoint)
+ .orElse(0L);
+
+ if (refundPointAmount > 0) {
+ lockedUser.receivePoint(Point.of(refundPointAmount));
+ }
products.forEach(productRepository::save);
userRepository.save(lockedUser);
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java
index 45e7c7ac6..2a9178162 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java
@@ -125,6 +125,34 @@ public static Payment of(
Long totalAmount,
Long usedPoint,
LocalDateTime requestedAt
+ ) {
+ return of(orderId, userId, totalAmount, usedPoint, null, null, requestedAt);
+ }
+
+ /**
+ * 포인트와 카드 혼합 결제용 Payment를 생성합니다.
+ *
+ * 포인트와 쿠폰 할인을 적용한 후 남은 금액을 카드로 결제하는 경우 사용합니다.
+ *
+ *
+ * @param orderId 주문 ID
+ * @param userId 사용자 ID
+ * @param totalAmount 총 결제 금액
+ * @param usedPoint 사용 포인트
+ * @param cardType 카드 타입 (paidAmount > 0일 때만 필수)
+ * @param cardNo 카드 번호 (paidAmount > 0일 때만 필수)
+ * @param requestedAt PG 요청 시각
+ * @return 생성된 Payment 인스턴스
+ * @throws CoreException 유효성 검증 실패 시
+ */
+ public static Payment of(
+ Long orderId,
+ Long userId,
+ Long totalAmount,
+ Long usedPoint,
+ CardType cardType,
+ String cardNo,
+ LocalDateTime requestedAt
) {
validateOrderId(orderId);
validateUserId(userId);
@@ -135,6 +163,14 @@ public static Payment of(
Long paidAmount = totalAmount - usedPoint;
validatePaidAmount(paidAmount);
+ // paidAmount > 0이면 카드 정보 필수
+ if (paidAmount > 0) {
+ if (cardType == null || cardNo == null || cardNo.isBlank()) {
+ throw new CoreException(ErrorType.BAD_REQUEST,
+ "포인트와 쿠폰만으로 결제할 수 없습니다. 카드 정보를 입력해주세요.");
+ }
+ }
+
Payment payment = new Payment();
payment.orderId = orderId;
payment.userId = userId;
@@ -142,8 +178,8 @@ public static Payment of(
payment.usedPoint = usedPoint;
payment.paidAmount = paidAmount;
payment.status = (paidAmount == 0L) ? PaymentStatus.SUCCESS : PaymentStatus.PENDING;
- payment.cardType = null;
- payment.cardNo = null;
+ payment.cardType = cardType; // paidAmount > 0일 때만 설정
+ payment.cardNo = cardNo; // paidAmount > 0일 때만 설정
payment.pgRequestedAt = requestedAt;
return payment;
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java
index 3cb2a562c..8414fed9a 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java
@@ -81,6 +81,35 @@ public Payment create(
return paymentRepository.save(payment);
}
+ /**
+ * 포인트와 카드 혼합 결제를 생성합니다.
+ *
+ * 포인트와 쿠폰 할인을 적용한 후 남은 금액을 카드로 결제하는 경우 사용합니다.
+ *
+ *
+ * @param orderId 주문 ID
+ * @param userId 사용자 ID
+ * @param totalAmount 총 결제 금액
+ * @param usedPoint 사용 포인트
+ * @param cardType 카드 타입 (paidAmount > 0일 때만 필수)
+ * @param cardNo 카드 번호 (paidAmount > 0일 때만 필수)
+ * @param requestedAt PG 요청 시각
+ * @return 생성된 Payment
+ */
+ @Transactional
+ public Payment create(
+ Long orderId,
+ Long userId,
+ Long totalAmount,
+ Long usedPoint,
+ CardType cardType,
+ String cardNo,
+ LocalDateTime requestedAt
+ ) {
+ Payment payment = Payment.of(orderId, userId, totalAmount, usedPoint, cardType, cardNo, requestedAt);
+ return paymentRepository.save(payment);
+ }
+
/**
* 결제를 SUCCESS 상태로 전이합니다.
*
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Controller.java
index fcea74ac2..e6fd5ee27 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Controller.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Controller.java
@@ -41,6 +41,7 @@ public ApiResponse createOrder(
OrderInfo orderInfo = purchasingFacade.createOrder(
userId,
request.toCommands(),
+ request.payment().usedPoint(),
request.payment().cardType(),
request.payment().cardNo()
);
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Dto.java
index e1307ca42..f2e552d09 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Dto.java
@@ -35,10 +35,9 @@ public List toCommands() {
* 결제 정보 요청 DTO.
*/
public record PaymentRequest(
- @NotNull(message = "카드 타입은 필수입니다.")
- String cardType,
- @NotNull(message = "카드 번호는 필수입니다.")
- String cardNo
+ Long usedPoint, // 포인트 사용량 (선택, 기본값: 0)
+ String cardType, // 카드 타입 (paidAmount > 0일 때만 필수)
+ String cardNo // 카드 번호 (paidAmount > 0일 때만 필수)
) {
}
From 92fd4830dfb72e6cac887467ecaa6255df1881e8 Mon Sep 17 00:00:00 2001
From: minor7295
Date: Mon, 8 Dec 2025 00:08:49 +0900
Subject: [PATCH 17/22] =?UTF-8?q?refactor:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8?=
=?UTF-8?q?=20=EB=98=90=EB=8A=94=20=EC=B9=B4=EB=93=9C=EB=A5=BC=20=EC=82=AC?=
=?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EA=B2=B0=EC=A0=9C=ED=95=A0=20?=
=?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=ED=85=8C=EC=8A=A4?=
=?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=AC=EA=B5=AC=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../PurchasingFacadeCircuitBreakerTest.java | 23 ++++++--
.../PurchasingFacadeConcurrencyTest.java | 14 +++--
.../PurchasingFacadePaymentCallbackTest.java | 3 +
.../PurchasingFacadePaymentGatewayTest.java | 9 ++-
.../purchasing/PurchasingFacadeTest.java | 45 +++++++--------
.../order/OrderCancellationServiceTest.java | 6 ++
.../domain/payment/PaymentServiceTest.java | 32 +++++++----
.../loopers/domain/payment/PaymentTest.java | 55 +++++++++++++------
.../api/PurchasingV1ApiE2ETest.java | 8 +--
9 files changed, 131 insertions(+), 64 deletions(-)
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeCircuitBreakerTest.java b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeCircuitBreakerTest.java
index 72b6052a8..f13e550e0 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeCircuitBreakerTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeCircuitBreakerTest.java
@@ -142,6 +142,7 @@ void createOrder_consecutiveFailures_circuitBreakerOpens() {
purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111"
);
@@ -189,6 +190,7 @@ void createOrder_circuitBreakerOpen_fallbackExecuted() {
purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111"
);
@@ -244,6 +246,7 @@ void createOrder_circuitBreakerHalfOpen_success_transitionsToClosed() {
purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111"
);
@@ -299,6 +302,7 @@ void createOrder_circuitBreakerHalfOpen_failure_transitionsToOpen() {
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111"
);
@@ -337,9 +341,11 @@ void createOrder_circuitBreakerOpen_internalSystemRespondsNormally() {
}
// act
+ // 포인트를 사용하지 않고 카드로만 결제 (Circuit Breaker OPEN 상태에서도 주문은 PENDING 상태로 유지)
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111"
);
@@ -350,12 +356,13 @@ void createOrder_circuitBreakerOpen_internalSystemRespondsNormally() {
assertThat(orderInfo.orderId()).isNotNull();
assertThat(orderInfo.status()).isEqualTo(OrderStatus.PENDING);
- // 재고와 포인트는 정상적으로 차감되어야 함
+ // 재고는 정상적으로 차감되어야 함
Product savedProduct = productRepository.findById(product.getId()).orElseThrow();
assertThat(savedProduct.getStock()).isEqualTo(9);
+ // 포인트는 사용하지 않았으므로 차감되지 않음
User savedUser = userRepository.findByUserId(user.getUserId());
- assertThat(savedUser.getPoint().getValue()).isEqualTo(40_000L);
+ assertThat(savedUser.getPoint().getValue()).isEqualTo(50_000L);
}
@Test
@@ -396,6 +403,7 @@ void createOrder_fallbackResponseWithCircuitBreakerOpen_orderRemainsPending() {
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111"
);
@@ -452,6 +460,7 @@ void createOrder_retryFailure_circuitBreakerOpens_fallbackExecuted() {
purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111"
);
@@ -519,9 +528,11 @@ void createOrder_retryFailure_fallbackCalled_circuitBreakerOpenHandled() {
.thenReturn(fallbackResponse);
// act
+ // 포인트를 사용하지 않고 카드로만 결제 (Circuit Breaker OPEN 상태에서도 주문은 PENDING 상태로 유지)
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111"
);
@@ -537,12 +548,13 @@ void createOrder_retryFailure_fallbackCalled_circuitBreakerOpenHandled() {
// 3. CIRCUIT_BREAKER_OPEN은 외부 시스템 장애로 간주되므로 주문 취소가 발생하지 않아야 함
assertThat(savedOrder.getStatus()).isNotEqualTo(OrderStatus.CANCELED);
- // 4. 재고와 포인트는 정상적으로 차감되어야 함 (주문은 생성되었지만 결제는 PENDING)
+ // 4. 재고는 정상적으로 차감되어야 함 (주문은 생성되었지만 결제는 PENDING)
Product savedProduct = productRepository.findById(product.getId()).orElseThrow();
assertThat(savedProduct.getStock()).isEqualTo(9);
+ // 포인트는 사용하지 않았으므로 차감되지 않음
User savedUser = userRepository.findByUserId(user.getUserId());
- assertThat(savedUser.getPoint().getValue()).isEqualTo(40_000L);
+ assertThat(savedUser.getPoint().getValue()).isEqualTo(50_000L);
}
@Test
@@ -582,6 +594,7 @@ void createOrder_fallbackResponse_circuitBreakerOpenErrorCode_orderRemainsPendin
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111"
);
@@ -645,6 +658,7 @@ void createOrder_retryExhausted_circuitBreakerOpens_fallbackCalled_orderPending(
purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111"
);
@@ -692,6 +706,7 @@ void createOrder_retryExhausted_circuitBreakerOpens_fallbackCalled_orderPending(
OrderInfo fallbackOrderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111"
);
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeConcurrencyTest.java b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeConcurrencyTest.java
index bdfda839d..f4fc9e213 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeConcurrencyTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeConcurrencyTest.java
@@ -128,7 +128,8 @@ void concurrencyTest_pointShouldProperlyDecreaseWhenOrderCreated() throws Interr
List commands = List.of(
OrderItemCommand.of(products.get(index).getId(), 1)
);
- purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111");
+ // 포인트를 사용하여 주문 (각 주문마다 10,000 포인트 사용)
+ purchasingFacade.createOrder(userId, commands, 10_000L, "SAMSUNG", "4111-1111-1111-1111");
successCount.incrementAndGet();
} catch (Exception e) {
synchronized (exceptions) {
@@ -176,11 +177,13 @@ void concurrencyTest_stockShouldBeProperlyDecreasedWhenOrdersCreated() throws In
List commands = List.of(
OrderItemCommand.of(productId, quantityPerOrder)
);
- purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111");
+ purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111");
successCount.incrementAndGet();
} catch (Exception e) {
synchronized (exceptions) {
exceptions.add(e);
+ System.out.println("Exception in stock test: " + e.getClass().getSimpleName() + " - " + e.getMessage());
+ e.printStackTrace();
}
} finally {
latch.countDown();
@@ -194,6 +197,7 @@ void concurrencyTest_stockShouldBeProperlyDecreasedWhenOrdersCreated() throws In
Product savedProduct = productRepository.findById(productId).orElseThrow();
int expectedStock = 100 - (successCount.get() * quantityPerOrder);
+ System.out.println("Success count: " + successCount.get() + ", Exceptions: " + exceptions.size());
assertThat(savedProduct.getStock()).isEqualTo(expectedStock);
assertThat(successCount.get() + exceptions.size()).isEqualTo(orderCount);
}
@@ -228,7 +232,7 @@ void concurrencyTest_couponShouldBeUsedOnlyOnceWhenOrdersCreated() throws Interr
List commands = List.of(
new OrderItemCommand(product.getId(), 1, couponCode)
);
- purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111");
+ purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111");
successCount.incrementAndGet();
} catch (Exception e) {
synchronized (exceptions) {
@@ -277,7 +281,7 @@ void concurrencyTest_cancelOrderShouldRestoreStockAccuratelyDuringConcurrentStoc
List commands = List.of(
OrderItemCommand.of(productId, orderQuantity)
);
- OrderInfo orderInfo = purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111");
+ OrderInfo orderInfo = purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111");
Long orderId = orderInfo.orderId();
// 주문 취소 전 재고 확인 (100 - 5 = 95)
@@ -317,7 +321,7 @@ void concurrencyTest_cancelOrderShouldRestoreStockAccuratelyDuringConcurrentStoc
List otherCommands = List.of(
OrderItemCommand.of(productId, 3)
);
- purchasingFacade.createOrder(userId, otherCommands, "SAMSUNG", "4111-1111-1111-1111");
+ purchasingFacade.createOrder(userId, otherCommands, null, "SAMSUNG", "4111-1111-1111-1111");
orderSuccess.incrementAndGet();
} catch (Exception e) {
synchronized (exceptions) {
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentCallbackTest.java b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentCallbackTest.java
index 24195fab4..331a41ed2 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentCallbackTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentCallbackTest.java
@@ -121,6 +121,7 @@ void handlePaymentCallback_successCallback_orderStatusUpdatedToCompleted() {
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111" // 유효한 Luhn 알고리즘 통과 카드 번호
);
@@ -197,6 +198,7 @@ void handlePaymentCallback_failureCallback_orderStatusUpdatedToCanceled() {
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111" // 유효한 Luhn 알고리즘 통과 카드 번호
);
@@ -268,6 +270,7 @@ void recoverOrderStatus_afterTimeout_statusRecoveredByStatusCheck() {
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111" // 유효한 Luhn 알고리즘 통과 카드 번호
);
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentGatewayTest.java b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentGatewayTest.java
index 6a3f9f837..f66ef1e2c 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentGatewayTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentGatewayTest.java
@@ -111,6 +111,7 @@ void createOrder_paymentGatewayTimeout_orderCreatedWithPendingStatus() {
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111" // 유효한 Luhn 알고리즘 통과 카드 번호
);
@@ -122,9 +123,9 @@ void createOrder_paymentGatewayTimeout_orderCreatedWithPendingStatus() {
Product savedProduct = productRepository.findById(product.getId()).orElseThrow();
assertThat(savedProduct.getStock()).isEqualTo(9);
- // 포인트는 차감되었는지 확인
+ // 포인트 차감 확인 (usedPoint가 null이므로 포인트 차감 없음)
User savedUser = userRepository.findByUserId(user.getUserId());
- assertThat(savedUser.getPoint().getValue()).isEqualTo(40_000L);
+ assertThat(savedUser.getPoint().getValue()).isEqualTo(50_000L); // 포인트 차감 없음
// 주문이 저장되었는지 확인
Order savedOrder = orderRepository.findById(orderInfo.orderId()).orElseThrow();
@@ -160,6 +161,7 @@ void createOrder_paymentGatewayFailure_orderCreatedWithPendingStatus() {
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111" // 유효한 Luhn 알고리즘 통과 카드 번호
);
@@ -197,6 +199,7 @@ void createOrder_paymentGatewayServerError_orderCreatedWithPendingStatus() {
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111" // 유효한 Luhn 알고리즘 통과 카드 번호
);
@@ -234,6 +237,7 @@ void createOrder_paymentGatewayConnectionFailure_orderCreatedWithPendingStatus()
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111" // 유효한 Luhn 알고리즘 통과 카드 번호
);
@@ -266,6 +270,7 @@ void createOrder_paymentGatewayTimeout_internalSystemRespondsNormally() {
OrderInfo orderInfo = purchasingFacade.createOrder(
user.getUserId(),
commands,
+ null,
"SAMSUNG",
"4111-1111-1111-1111" // 유효한 Luhn 알고리즘 통과 카드 번호
);
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeTest.java
index e9b01e55e..cb66c913c 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeTest.java
@@ -153,7 +153,7 @@ void createOrder_successFlow() {
);
// act
- OrderInfo orderInfo = purchasingFacade.createOrder(user.getUserId(), commands, "SAMSUNG", "4111-1111-1111-1111");
+ OrderInfo orderInfo = purchasingFacade.createOrder(user.getUserId(), commands, null, "SAMSUNG", "4111-1111-1111-1111");
// assert
// createOrder는 주문을 PENDING 상태로 생성하고, PG 결제 요청은 afterCommit 콜백에서 비동기로 실행됨
@@ -165,9 +165,9 @@ void createOrder_successFlow() {
assertThat(savedProduct1.getStock()).isEqualTo(8); // 10 - 2
assertThat(savedProduct2.getStock()).isEqualTo(4); // 5 - 1
- // 포인트 차감 확인
+ // 포인트 차감 확인 (usedPoint가 null이므로 포인트 차감 없음)
User savedUser = userRepository.findByUserId(user.getUserId());
- assertThat(savedUser.getPoint().getValue()).isEqualTo(25_000L); // 50_000 - (10_000 * 2 + 5_000 * 1)
+ assertThat(savedUser.getPoint().getValue()).isEqualTo(50_000L); // 포인트 차감 없음
}
@Test
@@ -178,7 +178,7 @@ void createOrder_emptyItems_throwsException() {
List emptyCommands = List.of();
// act & assert
- assertThatThrownBy(() -> purchasingFacade.createOrder(userId, emptyCommands, "SAMSUNG", "4111-1111-1111-1111"))
+ assertThatThrownBy(() -> purchasingFacade.createOrder(userId, emptyCommands, null, "SAMSUNG", "4111-1111-1111-1111"))
.isInstanceOf(CoreException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.BAD_REQUEST);
}
@@ -193,7 +193,7 @@ void createOrder_userNotFound() {
);
// act & assert
- assertThatThrownBy(() -> purchasingFacade.createOrder(unknownUserId, commands, "SAMSUNG", "4111-1111-1111-1111"))
+ assertThatThrownBy(() -> purchasingFacade.createOrder(unknownUserId, commands, null, "SAMSUNG", "4111-1111-1111-1111"))
.isInstanceOf(CoreException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND);
}
@@ -215,7 +215,7 @@ void createOrder_stockNotEnough() {
);
// act & assert
- assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111"))
+ assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111"))
.isInstanceOf(CoreException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.BAD_REQUEST);
@@ -245,7 +245,7 @@ void createOrder_stockZero() {
);
// act & assert
- assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111"))
+ assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111"))
.isInstanceOf(CoreException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.BAD_REQUEST);
@@ -275,7 +275,8 @@ void createOrder_pointNotEnough() {
);
// act & assert
- assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111"))
+ // 포인트를 사용하려고 하지만 잔액이 부족한 경우
+ assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, 10_000L, "SAMSUNG", "4111-1111-1111-1111"))
.isInstanceOf(CoreException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.BAD_REQUEST);
@@ -305,7 +306,7 @@ void createOrder_duplicateProducts_throwsException() {
);
// act & assert
- assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111"))
+ assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111"))
.isInstanceOf(CoreException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.BAD_REQUEST);
@@ -325,7 +326,7 @@ void getOrders_returnsUserOrders() {
List commands = List.of(
OrderItemCommand.of(product.getId(), 1)
);
- purchasingFacade.createOrder(user.getUserId(), commands, "SAMSUNG", "4111-1111-1111-1111");
+ purchasingFacade.createOrder(user.getUserId(), commands, null, "SAMSUNG", "4111-1111-1111-1111");
// act
List orders = purchasingFacade.getOrders(user.getUserId());
@@ -347,7 +348,7 @@ void getOrder_returnsSingleOrder() {
List commands = List.of(
OrderItemCommand.of(product.getId(), 1)
);
- OrderInfo createdOrder = purchasingFacade.createOrder(user.getUserId(), commands, "SAMSUNG", "4111-1111-1111-1111");
+ OrderInfo createdOrder = purchasingFacade.createOrder(user.getUserId(), commands, null, "SAMSUNG", "4111-1111-1111-1111");
// act
OrderInfo found = purchasingFacade.getOrder(user.getUserId(), createdOrder.orderId());
@@ -373,7 +374,7 @@ void getOrder_withDifferentUser_throwsException() {
List commands = List.of(
OrderItemCommand.of(product.getId(), 1)
);
- OrderInfo user1Order = purchasingFacade.createOrder(user1Id, commands, "SAMSUNG", "4111-1111-1111-1111");
+ OrderInfo user1Order = purchasingFacade.createOrder(user1Id, commands, null, "SAMSUNG", "4111-1111-1111-1111");
final Long orderId = user1Order.orderId();
// act & assert
@@ -405,7 +406,7 @@ void createOrder_atomicityGuaranteed() {
);
// act & assert
- assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111"))
+ assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111"))
.isInstanceOf(CoreException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.BAD_REQUEST);
@@ -447,7 +448,7 @@ void createOrder_success_allOperationsReflected() {
final int totalAmount = (10_000 * 3) + (15_000 * 2);
// act
- OrderInfo orderInfo = purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111");
+ OrderInfo orderInfo = purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111");
// assert
// 주문이 정상적으로 생성되었는지 확인
@@ -461,9 +462,9 @@ void createOrder_success_allOperationsReflected() {
assertThat(savedProduct1.getStock()).isEqualTo(initialStock1 - 3);
assertThat(savedProduct2.getStock()).isEqualTo(initialStock2 - 2);
- // 포인트가 정상적으로 차감되었는지 확인
+ // 포인트 차감 확인 (usedPoint가 null이므로 포인트 차감 없음)
User savedUser = userRepository.findByUserId(userId);
- assertThat(savedUser.getPoint().getValue()).isEqualTo(initialPoint - totalAmount);
+ assertThat(savedUser.getPoint().getValue()).isEqualTo(initialPoint); // 포인트 차감 없음
// 주문이 저장되었는지 확인
List orders = purchasingFacade.getOrders(userId);
@@ -488,10 +489,10 @@ void createOrder_withFixedAmountCoupon_success() {
);
// act
- OrderInfo orderInfo = purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111");
+ OrderInfo orderInfo = purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111");
// assert
- // createOrder는 주문을 PENDING 상태로 생성하고, PG 결제 요청은 afterCommit 콜백에서 비동기로 실행됨
+ // 쿠폰 할인 후 남은 금액(5,000원)을 카드로 결제해야 하므로 주문은 PENDING 상태로 유지됨
assertThat(orderInfo.status()).isEqualTo(OrderStatus.PENDING);
assertThat(orderInfo.totalAmount()).isEqualTo(5_000); // 10,000 - 5,000 = 5,000
@@ -518,7 +519,7 @@ void createOrder_withPercentageCoupon_success() {
);
// act
- OrderInfo orderInfo = purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111");
+ OrderInfo orderInfo = purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111");
// assert
// createOrder는 주문을 PENDING 상태로 생성하고, PG 결제 요청은 afterCommit 콜백에서 비동기로 실행됨
@@ -545,7 +546,7 @@ void createOrder_withNonExistentCoupon_shouldFail() {
);
// act & assert
- assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111"))
+ assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111"))
.isInstanceOf(CoreException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND);
}
@@ -568,7 +569,7 @@ void createOrder_withCouponNotOwnedByUser_shouldFail() {
);
// act & assert
- assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111"))
+ assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111"))
.isInstanceOf(CoreException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND);
}
@@ -592,7 +593,7 @@ void createOrder_withUsedCoupon_shouldFail() {
);
// act & assert
- assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, "SAMSUNG", "4111-1111-1111-1111"))
+ assertThatThrownBy(() -> purchasingFacade.createOrder(userId, commands, null, "SAMSUNG", "4111-1111-1111-1111"))
.isInstanceOf(CoreException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.BAD_REQUEST);
}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderCancellationServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderCancellationServiceTest.java
index 3f2deed68..5c39429b5 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderCancellationServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderCancellationServiceTest.java
@@ -1,5 +1,6 @@
package com.loopers.domain.order;
+import com.loopers.domain.payment.PaymentService;
import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductRepository;
import com.loopers.domain.user.Gender;
@@ -40,6 +41,9 @@ public class OrderCancellationServiceTest {
@Mock
private ProductRepository productRepository;
+ @Mock
+ private PaymentService paymentService;
+
@InjectMocks
private OrderCancellationService orderCancellationService;
@@ -66,6 +70,8 @@ void cancelsOrderAndRecoversResources() {
when(orderRepository.save(any(Order.class))).thenReturn(order);
when(userRepository.save(any(User.class))).thenReturn(user);
when(productRepository.save(any(Product.class))).thenReturn(product1, product2);
+ // Payment가 없는 경우 (포인트를 사용하지 않은 주문)
+ when(paymentService.findByOrderId(any(Long.class))).thenReturn(Optional.empty());
// act
orderCancellationService.cancel(order, user);
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java
index b05e6a0a8..0c501395c 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java
@@ -77,7 +77,7 @@ void createsPointPayment() {
Long orderId = PaymentTestFixture.ValidPayment.ORDER_ID;
Long userId = PaymentTestFixture.ValidPayment.USER_ID;
Long totalAmount = PaymentTestFixture.ValidPayment.AMOUNT;
- Long usedPoint = PaymentTestFixture.ValidPayment.ZERO_POINT;
+ Long usedPoint = PaymentTestFixture.ValidPayment.FULL_POINT; // 포인트로 전액 결제
LocalDateTime requestedAt = PaymentTestFixture.ValidPayment.REQUESTED_AT;
Payment expectedPayment = Payment.of(orderId, userId, totalAmount, usedPoint, requestedAt);
@@ -103,8 +103,9 @@ void transitionsToSuccess() {
Payment payment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
LocalDateTime completedAt = LocalDateTime.of(2025, 12, 1, 10, 5, 0);
@@ -129,8 +130,9 @@ void transitionsToFailed() {
Payment payment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
LocalDateTime completedAt = LocalDateTime.of(2025, 12, 1, 10, 5, 0);
@@ -180,8 +182,9 @@ void findsById() {
Payment expectedPayment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
@@ -203,8 +206,9 @@ void findsByOrderId() {
Payment expectedPayment = Payment.of(
orderId,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
@@ -438,8 +442,9 @@ void handlesSuccessCallback() {
Payment payment = Payment.of(
orderId,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
LocalDateTime.now()
);
@@ -468,8 +473,9 @@ void handlesFailedCallback() {
Payment payment = Payment.of(
orderId,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
LocalDateTime.now()
);
@@ -498,8 +504,9 @@ void maintainsStatus_whenPendingCallback() {
Payment payment = Payment.of(
orderId,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
LocalDateTime.now()
);
@@ -549,8 +556,9 @@ void recoversToSuccess() {
Payment payment = Payment.of(
orderId,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
LocalDateTime.now()
);
@@ -580,8 +588,9 @@ void recoversToFailed() {
Payment payment = Payment.of(
orderId,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
LocalDateTime.now()
);
@@ -611,8 +620,9 @@ void maintainsPendingStatus() {
Payment payment = Payment.of(
orderId,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
LocalDateTime.now()
);
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
index c4c8787b7..cc49a6467 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
@@ -77,16 +77,22 @@ void hasPendingStatus_whenPointDoesNotCoverTotalAmount() {
// arrange
Long orderId = PaymentTestFixture.ValidPayment.ORDER_ID;
Long userId = PaymentTestFixture.ValidPayment.USER_ID;
- Long totalAmount = PaymentTestFixture.ValidPayment.AMOUNT;
- Long usedPoint = 0L; // 포인트 미사용
+ Long amount = PaymentTestFixture.ValidPayment.AMOUNT;
// act
- Payment payment = Payment.of(orderId, userId, totalAmount, usedPoint, PaymentTestFixture.ValidPayment.REQUESTED_AT);
+ Payment payment = Payment.of(
+ orderId,
+ userId,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
+ amount,
+ PaymentTestFixture.ValidPayment.REQUESTED_AT
+ );
// assert
assertThat(payment.getStatus()).isEqualTo(PaymentStatus.PENDING);
- assertThat(payment.getUsedPoint()).isEqualTo(usedPoint);
- assertThat(payment.getPaidAmount()).isEqualTo(totalAmount);
+ assertThat(payment.getUsedPoint()).isEqualTo(0L);
+ assertThat(payment.getPaidAmount()).isEqualTo(amount);
}
@DisplayName("포인트로 부분 결제하면 PENDING 상태로 생성된다.")
@@ -96,10 +102,18 @@ void hasPendingStatus_whenPointPartiallyCoversTotalAmount() {
Long orderId = PaymentTestFixture.ValidPayment.ORDER_ID;
Long userId = PaymentTestFixture.ValidPayment.USER_ID;
Long totalAmount = PaymentTestFixture.ValidPayment.AMOUNT;
- Long usedPoint = totalAmount / 2; // 포인트로 절반 결제
+ Long usedPoint = PaymentTestFixture.ValidPayment.PARTIAL_POINT; // 포인트로 절반 결제
// act
- Payment payment = Payment.of(orderId, userId, totalAmount, usedPoint, PaymentTestFixture.ValidPayment.REQUESTED_AT);
+ Payment payment = Payment.of(
+ orderId,
+ userId,
+ totalAmount,
+ usedPoint,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
+ PaymentTestFixture.ValidPayment.REQUESTED_AT
+ );
// assert
assertThat(payment.getStatus()).isEqualTo(PaymentStatus.PENDING);
@@ -114,8 +128,9 @@ void canTransitionToSuccess_whenPending() {
Payment payment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
LocalDateTime completedAt = LocalDateTime.of(2025, 12, 1, 10, 5, 0);
@@ -135,8 +150,9 @@ void canTransitionToFailed_whenPending() {
Payment payment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
LocalDateTime completedAt = LocalDateTime.of(2025, 12, 1, 10, 5, 0);
@@ -158,8 +174,9 @@ void throwsException_whenTransitioningToSuccessFromFailed() {
Payment payment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
payment.toFailed("실패 사유", LocalDateTime.now());
@@ -180,8 +197,9 @@ void throwsException_whenTransitioningToFailedFromSuccess() {
Payment payment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
payment.toSuccess(LocalDateTime.now());
@@ -202,8 +220,9 @@ void returnsTrue_whenPaymentIsCompleted() {
Payment successPayment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
successPayment.toSuccess(LocalDateTime.now());
@@ -211,8 +230,9 @@ void returnsTrue_whenPaymentIsCompleted() {
Payment failedPayment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
failedPayment.toFailed("ERROR", LocalDateTime.now());
@@ -220,8 +240,9 @@ void returnsTrue_whenPaymentIsCompleted() {
Payment pendingPayment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
@@ -238,8 +259,9 @@ void doesNotThrowException_whenTransitioningToSuccessFromSuccess() {
Payment payment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
LocalDateTime firstCompletedAt = LocalDateTime.of(2025, 12, 1, 10, 5, 0);
@@ -261,8 +283,9 @@ void doesNotThrowException_whenTransitioningToFailedFromFailed() {
Payment payment = Payment.of(
PaymentTestFixture.ValidPayment.ORDER_ID,
PaymentTestFixture.ValidPayment.USER_ID,
+ PaymentTestFixture.ValidPayment.CARD_TYPE,
+ PaymentTestFixture.ValidPayment.CARD_NO,
PaymentTestFixture.ValidPayment.AMOUNT,
- PaymentTestFixture.ValidPayment.ZERO_POINT,
PaymentTestFixture.ValidPayment.REQUESTED_AT
);
LocalDateTime firstCompletedAt = LocalDateTime.of(2025, 12, 1, 10, 5, 0);
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java
index 46e6f964a..1b025fccc 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java
@@ -119,7 +119,7 @@ private HttpEntity createOrderRequest(Long produc
List.of(
new PurchasingV1Dto.ItemRequest(savedProduct.getId(), 1)
),
- new PurchasingV1Dto.PaymentRequest("SAMSUNG", "4111-1111-1111-1111")
+ new PurchasingV1Dto.PaymentRequest(null, "SAMSUNG", "4111-1111-1111-1111")
);
HttpHeaders headers = new HttpHeaders();
@@ -252,7 +252,7 @@ void returns200_whenPaymentCallbackSuccess() {
List.of(
new PurchasingV1Dto.ItemRequest(savedProduct.getId(), 1)
),
- new PurchasingV1Dto.PaymentRequest("SAMSUNG", "4111-1111-1111-1111")
+ new PurchasingV1Dto.PaymentRequest(null, "SAMSUNG", "4111-1111-1111-1111")
);
HttpHeaders createHeaders = new HttpHeaders();
@@ -357,7 +357,7 @@ void returns200_whenPaymentCallbackFailure() {
List.of(
new PurchasingV1Dto.ItemRequest(savedProduct.getId(), 1)
),
- new PurchasingV1Dto.PaymentRequest("SAMSUNG", "4111-1111-1111-1111")
+ new PurchasingV1Dto.PaymentRequest(null, "SAMSUNG", "4111-1111-1111-1111")
);
HttpHeaders createHeaders = new HttpHeaders();
@@ -468,7 +468,7 @@ void returns200_whenOrderStatusRecovered() {
List.of(
new PurchasingV1Dto.ItemRequest(savedProduct.getId(), 1)
),
- new PurchasingV1Dto.PaymentRequest("SAMSUNG", "4111-1111-1111-1111")
+ new PurchasingV1Dto.PaymentRequest(null, "SAMSUNG", "4111-1111-1111-1111")
);
HttpHeaders createHeaders = new HttpHeaders();
From 6665994f8f546b6c06e4d6961867f8cc4eea781b Mon Sep 17 00:00:00 2001
From: minor7295
Date: Mon, 8 Dec 2025 23:25:26 +0900
Subject: [PATCH 18/22] =?UTF-8?q?refactor:=20=EB=8B=A4=EB=A5=B8=20applicat?=
=?UTF-8?q?ion=EB=A0=88=EC=9D=B4=EC=96=B4=EC=99=80=20=EB=8F=99=EC=9D=BC?=
=?UTF-8?q?=ED=95=98=EA=B2=8C=20command=EC=9D=98=20=EC=9C=84=EC=B9=98?=
=?UTF-8?q?=EB=A5=BC=20domain=EC=97=90=EC=84=9C=20application=EC=9C=BC?=
=?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../purchasing}/PaymentRequestCommand.java | 5 ++---
.../main/java/com/loopers/domain/payment/PaymentGateway.java | 2 ++
.../main/java/com/loopers/domain/payment/PaymentService.java | 1 +
.../infrastructure/paymentgateway/PaymentGatewayImpl.java | 2 +-
.../java/com/loopers/domain/payment/PaymentServiceTest.java | 1 +
5 files changed, 7 insertions(+), 4 deletions(-)
rename apps/commerce-api/src/main/java/com/loopers/{domain/payment => application/purchasing}/PaymentRequestCommand.java (91%)
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentRequestCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PaymentRequestCommand.java
similarity index 91%
rename from apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentRequestCommand.java
rename to apps/commerce-api/src/main/java/com/loopers/application/purchasing/PaymentRequestCommand.java
index 9931d48c9..3834136a6 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentRequestCommand.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PaymentRequestCommand.java
@@ -1,4 +1,4 @@
-package com.loopers.domain.payment;
+package com.loopers.application.purchasing;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
@@ -6,7 +6,7 @@
/**
* 결제 요청 명령.
*
- * PG 결제 요청에 필요한 정보를 담는 도메인 모델입니다.
+ * PG 결제 요청에 필요한 정보를 담는 명령 모델입니다.
*
*
* @author Loopers
@@ -41,4 +41,3 @@ public record PaymentRequestCommand(
}
}
}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentGateway.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentGateway.java
index cc51f65ae..a8f2864d8 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentGateway.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentGateway.java
@@ -1,5 +1,7 @@
package com.loopers.domain.payment;
+import com.loopers.application.purchasing.PaymentRequestCommand;
+
/**
* 결제 게이트웨이 인터페이스.
*
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java
index 8414fed9a..9a7ac16c6 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java
@@ -1,5 +1,6 @@
package com.loopers.domain.payment;
+import com.loopers.application.purchasing.PaymentRequestCommand;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayImpl.java
index 62530d59d..ac5e0bdbc 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayImpl.java
@@ -1,7 +1,7 @@
package com.loopers.infrastructure.paymentgateway;
import com.loopers.domain.payment.PaymentGateway;
-import com.loopers.domain.payment.PaymentRequestCommand;
+import com.loopers.application.purchasing.PaymentRequestCommand;
import com.loopers.domain.payment.PaymentRequestResult;
import com.loopers.domain.payment.PaymentStatus;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java
index 0c501395c..963eab173 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java
@@ -1,5 +1,6 @@
package com.loopers.domain.payment;
+import com.loopers.application.purchasing.PaymentRequestCommand;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import org.junit.jupiter.api.BeforeEach;
From fd1155df4c26cd7b051792424992aba093e76a31 Mon Sep 17 00:00:00 2001
From: minor7295
Date: Tue, 9 Dec 2025 00:47:22 +0900
Subject: [PATCH 19/22] =?UTF-8?q?refactor:=20Order=EB=8F=84=EB=A9=94?=
=?UTF-8?q?=EC=9D=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90=20=EB=8C=80?=
=?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?=
=?UTF-8?q?=EC=A4=91=20application=20=EB=A0=88=EC=9D=B4=EC=96=B4=EC=97=90?=
=?UTF-8?q?=20=EB=8D=94=20=EC=A0=81=ED=95=A9=ED=95=9C=20=EB=B6=80=EB=B6=84?=
=?UTF-8?q?=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../order/OrderCancellationServiceTest.java | 189 --------------
.../order/OrderPaymentResultServiceTest.java | 242 ------------------
.../domain/order/OrderServiceTest.java | 220 ++++++++++++++++
3 files changed, 220 insertions(+), 431 deletions(-)
delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderCancellationServiceTest.java
delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderPaymentResultServiceTest.java
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderCancellationServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderCancellationServiceTest.java
deleted file mode 100644
index 5c39429b5..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderCancellationServiceTest.java
+++ /dev/null
@@ -1,189 +0,0 @@
-package com.loopers.domain.order;
-
-import com.loopers.domain.payment.PaymentService;
-import com.loopers.domain.product.Product;
-import com.loopers.domain.product.ProductRepository;
-import com.loopers.domain.user.Gender;
-import com.loopers.domain.user.Point;
-import com.loopers.domain.user.User;
-import com.loopers.domain.user.UserRepository;
-import com.loopers.support.error.CoreException;
-import com.loopers.support.error.ErrorType;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.util.List;
-import java.util.Optional;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
-
-/**
- * OrderCancellationService 테스트.
- */
-@ExtendWith(MockitoExtension.class)
-@DisplayName("OrderCancellationService")
-public class OrderCancellationServiceTest {
-
- @Mock
- private OrderRepository orderRepository;
-
- @Mock
- private UserRepository userRepository;
-
- @Mock
- private ProductRepository productRepository;
-
- @Mock
- private PaymentService paymentService;
-
- @InjectMocks
- private OrderCancellationService orderCancellationService;
-
- @DisplayName("주문 취소")
- @Nested
- class CancelOrder {
- @DisplayName("주문을 취소하고 리소스를 원복할 수 있다.")
- @Test
- void cancelsOrderAndRecoversResources() {
- // arrange
- Long userId = OrderTestFixture.ValidOrder.USER_ID;
- User user = createUser(userId);
- Order order = Order.of(userId, OrderTestFixture.ValidOrderItem.createMultipleItems());
-
- List items = order.getItems();
- Product product1 = createProduct(items.get(0).getProductId());
- Product product2 = createProduct(items.get(1).getProductId());
-
- when(userRepository.findByUserIdForUpdate(user.getUserId())).thenReturn(user);
- when(productRepository.findByIdForUpdate(items.get(0).getProductId()))
- .thenReturn(Optional.of(product1));
- when(productRepository.findByIdForUpdate(items.get(1).getProductId()))
- .thenReturn(Optional.of(product2));
- when(orderRepository.save(any(Order.class))).thenReturn(order);
- when(userRepository.save(any(User.class))).thenReturn(user);
- when(productRepository.save(any(Product.class))).thenReturn(product1, product2);
- // Payment가 없는 경우 (포인트를 사용하지 않은 주문)
- when(paymentService.findByOrderId(any(Long.class))).thenReturn(Optional.empty());
-
- // act
- orderCancellationService.cancel(order, user);
-
- // assert
- verify(userRepository, times(1)).findByUserIdForUpdate(user.getUserId());
- verify(productRepository, times(1)).findByIdForUpdate(items.get(0).getProductId());
- verify(productRepository, times(1)).findByIdForUpdate(items.get(1).getProductId());
- verify(orderRepository, times(1)).save(order);
- verify(userRepository, times(1)).save(user);
- verify(productRepository, times(2)).save(any(Product.class));
- // 상태 변경 검증은 OrderTest에서 이미 검증했으므로 제거
- }
-
- @DisplayName("주문이 null이면 예외가 발생한다.")
- @Test
- void throwsException_whenOrderIsNull() {
- // arrange
- User user = createUser(OrderTestFixture.ValidOrder.USER_ID);
-
- // act
- CoreException result = assertThrows(CoreException.class, () -> {
- orderCancellationService.cancel(null, user);
- });
-
- // assert
- assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
- verify(orderRepository, never()).save(any(Order.class));
- }
-
- @DisplayName("사용자가 null이면 예외가 발생한다.")
- @Test
- void throwsException_whenUserIsNull() {
- // arrange
- Order order = Order.of(
- OrderTestFixture.ValidOrder.USER_ID,
- OrderTestFixture.ValidOrderItem.createMultipleItems()
- );
-
- // act
- CoreException result = assertThrows(CoreException.class, () -> {
- orderCancellationService.cancel(order, null);
- });
-
- // assert
- assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
- verify(orderRepository, never()).save(any(Order.class));
- }
-
- @DisplayName("사용자를 찾을 수 없으면 예외가 발생한다.")
- @Test
- void throwsException_whenUserNotFound() {
- // arrange
- Long userId = OrderTestFixture.ValidOrder.USER_ID;
- User user = createUser(userId);
- Order order = Order.of(userId, OrderTestFixture.ValidOrderItem.createMultipleItems());
-
- when(userRepository.findByUserIdForUpdate(user.getUserId())).thenReturn(null);
-
- // act
- CoreException result = assertThrows(CoreException.class, () -> {
- orderCancellationService.cancel(order, user);
- });
-
- // assert
- assertThat(result.getErrorType()).isEqualTo(ErrorType.NOT_FOUND);
- verify(userRepository, times(1)).findByUserIdForUpdate(user.getUserId());
- verify(orderRepository, never()).save(any(Order.class));
- }
-
- @DisplayName("상품을 찾을 수 없으면 예외가 발생한다.")
- @Test
- void throwsException_whenProductNotFound() {
- // arrange
- Long userId = OrderTestFixture.ValidOrder.USER_ID;
- User user = createUser(userId);
- Order order = Order.of(userId, OrderTestFixture.ValidOrderItem.createMultipleItems());
- List items = order.getItems();
-
- when(userRepository.findByUserIdForUpdate(user.getUserId())).thenReturn(user);
- when(productRepository.findByIdForUpdate(items.get(0).getProductId()))
- .thenReturn(Optional.empty());
-
- // act
- CoreException result = assertThrows(CoreException.class, () -> {
- orderCancellationService.cancel(order, user);
- });
-
- // assert
- assertThat(result.getErrorType()).isEqualTo(ErrorType.NOT_FOUND);
- verify(userRepository, times(1)).findByUserIdForUpdate(user.getUserId());
- verify(productRepository, times(1)).findByIdForUpdate(items.get(0).getProductId());
- verify(orderRepository, never()).save(any(Order.class));
- }
- }
-
- private User createUser(Long userId) {
- return User.of(
- String.valueOf(userId),
- "test@example.com",
- "1990-01-01",
- Gender.MALE,
- Point.of(0L)
- );
- }
-
- private Product createProduct(Long productId) {
- // Mock을 사용하여 ID 설정
- Product mockedProduct = mock(Product.class);
- when(mockedProduct.getId()).thenReturn(productId);
- doNothing().when(mockedProduct).increaseStock(any(Integer.class));
- return mockedProduct;
- }
-}
-
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderPaymentResultServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderPaymentResultServiceTest.java
deleted file mode 100644
index 0d70ee580..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderPaymentResultServiceTest.java
+++ /dev/null
@@ -1,242 +0,0 @@
-package com.loopers.domain.order;
-
-import com.loopers.domain.payment.PaymentStatus;
-import com.loopers.domain.user.Gender;
-import com.loopers.domain.user.Point;
-import com.loopers.domain.user.User;
-import com.loopers.domain.user.UserRepository;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.util.Optional;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
-
-/**
- * OrderPaymentResultService 테스트.
- */
-@ExtendWith(MockitoExtension.class)
-@DisplayName("OrderPaymentResultService")
-public class OrderPaymentResultServiceTest {
-
- @Mock
- private OrderRepository orderRepository;
-
- @Mock
- private UserRepository userRepository;
-
- @Mock
- private OrderCancellationService orderCancellationService;
-
- @InjectMocks
- private OrderPaymentResultService orderPaymentResultService;
-
- @DisplayName("결제 결과에 따른 주문 처리")
- @Nested
- class ProcessByPaymentResult {
- @DisplayName("결제 성공 시 주문을 완료 상태로 변경할 수 있다.")
- @Test
- void completesOrder_whenPaymentSuccess() {
- // arrange
- Long orderId = 1L;
- Order order = Order.of(
- OrderTestFixture.ValidOrder.USER_ID,
- OrderTestFixture.ValidOrderItem.createMultipleItems()
- );
- String transactionKey = "TXN123456";
-
- when(orderRepository.findById(orderId)).thenReturn(Optional.of(order));
- when(orderRepository.save(any(Order.class))).thenReturn(order);
-
- // act
- boolean result = orderPaymentResultService.updateByPaymentStatus(
- orderId,
- PaymentStatus.SUCCESS,
- transactionKey,
- null
- );
-
- // assert
- assertThat(result).isTrue();
- verify(orderRepository, times(1)).findById(orderId);
- verify(orderRepository, times(1)).save(order);
- verify(orderCancellationService, never()).cancel(any(), any());
- // 상태 변경 검증은 OrderTest에서 이미 검증했으므로 제거
- }
-
- @DisplayName("결제 실패 시 주문을 취소 상태로 변경할 수 있다.")
- @Test
- void cancelsOrder_whenPaymentFailed() {
- // arrange
- Long orderId = 1L;
- Long userId = OrderTestFixture.ValidOrder.USER_ID;
- Order order = Order.of(userId, OrderTestFixture.ValidOrderItem.createMultipleItems());
- User user = createUser(userId);
- String transactionKey = "TXN123456";
- String reason = "카드 한도 초과";
-
- when(orderRepository.findById(orderId)).thenReturn(Optional.of(order));
- when(userRepository.findById(userId)).thenReturn(user);
- doNothing().when(orderCancellationService).cancel(any(Order.class), any(User.class));
-
- // act
- boolean result = orderPaymentResultService.updateByPaymentStatus(
- orderId,
- PaymentStatus.FAILED,
- transactionKey,
- reason
- );
-
- // assert
- assertThat(result).isTrue();
- verify(orderRepository, times(1)).findById(orderId);
- verify(userRepository, times(1)).findById(userId);
- verify(orderCancellationService, times(1)).cancel(order, user);
- }
-
- @DisplayName("결제 대기 상태면 주문 상태를 유지한다.")
- @Test
- void maintainsOrderStatus_whenPaymentPending() {
- // arrange
- Long orderId = 1L;
- Order order = Order.of(
- OrderTestFixture.ValidOrder.USER_ID,
- OrderTestFixture.ValidOrderItem.createMultipleItems()
- );
- String transactionKey = "TXN123456";
-
- when(orderRepository.findById(orderId)).thenReturn(Optional.of(order));
-
- // act
- boolean result = orderPaymentResultService.updateByPaymentStatus(
- orderId,
- PaymentStatus.PENDING,
- transactionKey,
- null
- );
-
- // assert
- assertThat(result).isTrue();
- verify(orderRepository, times(1)).findById(orderId);
- verify(orderRepository, never()).save(any(Order.class));
- verify(orderCancellationService, never()).cancel(any(), any());
- }
-
- @DisplayName("이미 완료된 주문은 처리하지 않는다.")
- @Test
- void skipsProcessing_whenOrderAlreadyCompleted() {
- // arrange
- Long orderId = 1L;
- Order order = Order.of(
- OrderTestFixture.ValidOrder.USER_ID,
- OrderTestFixture.ValidOrderItem.createMultipleItems()
- );
- order.complete(); // 이미 완료 상태
-
- when(orderRepository.findById(orderId)).thenReturn(Optional.of(order));
-
- // act
- boolean result = orderPaymentResultService.updateByPaymentStatus(
- orderId,
- PaymentStatus.SUCCESS,
- "TXN123456",
- null
- );
-
- // assert
- assertThat(result).isTrue();
- verify(orderRepository, times(1)).findById(orderId);
- verify(orderRepository, never()).save(any(Order.class));
- }
-
- @DisplayName("이미 취소된 주문은 처리하지 않는다.")
- @Test
- void skipsProcessing_whenOrderAlreadyCanceled() {
- // arrange
- Long orderId = 1L;
- Long userId = OrderTestFixture.ValidOrder.USER_ID;
- Order order = Order.of(userId, OrderTestFixture.ValidOrderItem.createMultipleItems());
- order.cancel(); // 이미 취소 상태
-
- when(orderRepository.findById(orderId)).thenReturn(Optional.of(order));
-
- // act
- boolean result = orderPaymentResultService.updateByPaymentStatus(
- orderId,
- PaymentStatus.FAILED,
- "TXN123456",
- "실패 사유"
- );
-
- // assert
- assertThat(result).isTrue();
- verify(orderRepository, times(1)).findById(orderId);
- verify(orderCancellationService, never()).cancel(any(), any());
- }
-
- @DisplayName("주문을 찾을 수 없으면 false를 반환한다.")
- @Test
- void returnsFalse_whenOrderNotFound() {
- // arrange
- Long orderId = 999L;
- when(orderRepository.findById(orderId)).thenReturn(Optional.empty());
-
- // act
- boolean result = orderPaymentResultService.updateByPaymentStatus(
- orderId,
- PaymentStatus.SUCCESS,
- "TXN123456",
- null
- );
-
- // assert
- assertThat(result).isFalse();
- verify(orderRepository, times(1)).findById(orderId);
- verify(orderRepository, never()).save(any(Order.class));
- }
-
- @DisplayName("결제 실패 시 사용자를 찾을 수 없으면 false를 반환한다.")
- @Test
- void returnsFalse_whenUserNotFound() {
- // arrange
- Long orderId = 1L;
- Long userId = OrderTestFixture.ValidOrder.USER_ID;
- Order order = Order.of(userId, OrderTestFixture.ValidOrderItem.createMultipleItems());
-
- when(orderRepository.findById(orderId)).thenReturn(Optional.of(order));
- when(userRepository.findById(userId)).thenReturn(null);
-
- // act
- boolean result = orderPaymentResultService.updateByPaymentStatus(
- orderId,
- PaymentStatus.FAILED,
- "TXN123456",
- "실패 사유"
- );
-
- // assert
- assertThat(result).isFalse();
- verify(orderRepository, times(1)).findById(orderId);
- verify(userRepository, times(1)).findById(userId);
- verify(orderCancellationService, never()).cancel(any(Order.class), any(User.class));
- }
- }
-
- private User createUser(Long userId) {
- return User.of(
- String.valueOf(userId),
- "test@example.com",
- "1990-01-01",
- Gender.MALE,
- Point.of(0L)
- );
- }
-}
-
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java
index 11964d67d..1dab0a951 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java
@@ -1,5 +1,10 @@
package com.loopers.domain.order;
+import com.loopers.domain.payment.PaymentStatus;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.Point;
+import com.loopers.domain.user.User;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import org.junit.jupiter.api.DisplayName;
@@ -234,5 +239,220 @@ void throwsException_whenOrderNotFound() {
verify(orderRepository, never()).save(any(Order.class));
}
}
+
+ @DisplayName("주문 취소")
+ @Nested
+ class CancelOrder {
+ @DisplayName("주문을 취소하고 재고를 원복하며 포인트를 환불할 수 있다.")
+ @Test
+ void cancelsOrderAndRecoversResources() {
+ // arrange
+ Long userId = OrderTestFixture.ValidOrder.USER_ID;
+ User user = createUser(userId);
+ Order order = Order.of(userId, OrderTestFixture.ValidOrderItem.createMultipleItems());
+
+ List items = order.getItems();
+ Product product1 = createProduct(items.get(0).getProductId());
+ Product product2 = createProduct(items.get(1).getProductId());
+ List products = List.of(product1, product2);
+ Long refundPointAmount = 5000L;
+
+ when(orderRepository.save(any(Order.class))).thenReturn(order);
+
+ // act
+ orderService.cancelOrder(order, products, user, refundPointAmount);
+
+ // assert
+ verify(orderRepository, times(1)).save(order);
+ verify(product1, times(1)).increaseStock(items.get(0).getQuantity());
+ verify(product2, times(1)).increaseStock(items.get(1).getQuantity());
+ // 상태 변경 검증은 OrderTest에서 이미 검증했으므로 제거
+ }
+
+ @DisplayName("주문이 null이면 예외가 발생한다.")
+ @Test
+ void throwsException_whenOrderIsNull() {
+ // arrange
+ User user = createUser(OrderTestFixture.ValidOrder.USER_ID);
+ List products = List.of();
+ Long refundPointAmount = 0L;
+
+ // act
+ CoreException result = assertThrows(CoreException.class, () -> {
+ orderService.cancelOrder(null, products, user, refundPointAmount);
+ });
+
+ // assert
+ assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
+ verify(orderRepository, never()).save(any(Order.class));
+ }
+
+ @DisplayName("사용자가 null이면 예외가 발생한다.")
+ @Test
+ void throwsException_whenUserIsNull() {
+ // arrange
+ Order order = Order.of(
+ OrderTestFixture.ValidOrder.USER_ID,
+ OrderTestFixture.ValidOrderItem.createMultipleItems()
+ );
+ List products = List.of();
+ Long refundPointAmount = 0L;
+
+ // act
+ CoreException result = assertThrows(CoreException.class, () -> {
+ orderService.cancelOrder(order, products, null, refundPointAmount);
+ });
+
+ // assert
+ assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
+ verify(orderRepository, never()).save(any(Order.class));
+ }
+
+ @DisplayName("포인트를 사용하지 않은 주문은 포인트 환불 없이 취소할 수 있다.")
+ @Test
+ void cancelsOrderWithoutPointRefund() {
+ // arrange
+ Long userId = OrderTestFixture.ValidOrder.USER_ID;
+ User user = createUser(userId);
+ Order order = Order.of(userId, OrderTestFixture.ValidOrderItem.createMultipleItems());
+
+ List items = order.getItems();
+ Product product1 = createProduct(items.get(0).getProductId());
+ Product product2 = createProduct(items.get(1).getProductId());
+ List products = List.of(product1, product2);
+ Long refundPointAmount = 0L;
+
+ when(orderRepository.save(any(Order.class))).thenReturn(order);
+
+ // act
+ orderService.cancelOrder(order, products, user, refundPointAmount);
+
+ // assert
+ verify(orderRepository, times(1)).save(order);
+ verify(product1, times(1)).increaseStock(items.get(0).getQuantity());
+ verify(product2, times(1)).increaseStock(items.get(1).getQuantity());
+ }
+ }
+
+ @DisplayName("결제 결과에 따른 주문 상태 업데이트")
+ @Nested
+ class UpdateStatusByPaymentResult {
+ @DisplayName("결제 성공 시 주문을 완료 상태로 변경할 수 있다.")
+ @Test
+ void completesOrder_whenPaymentSuccess() {
+ // arrange
+ Order order = Order.of(
+ OrderTestFixture.ValidOrder.USER_ID,
+ OrderTestFixture.ValidOrderItem.createMultipleItems()
+ );
+ when(orderRepository.save(any(Order.class))).thenReturn(order);
+
+ // act
+ orderService.updateStatusByPaymentResult(order, PaymentStatus.SUCCESS);
+
+ // assert
+ verify(orderRepository, times(1)).save(order);
+ // 상태 변경 검증은 OrderTest에서 이미 검증했으므로 제거
+ }
+
+ @DisplayName("결제 실패 시 주문을 취소 상태로 변경할 수 있다.")
+ @Test
+ void cancelsOrder_whenPaymentFailed() {
+ // arrange
+ Order order = Order.of(
+ OrderTestFixture.ValidOrder.USER_ID,
+ OrderTestFixture.ValidOrderItem.createMultipleItems()
+ );
+ when(orderRepository.save(any(Order.class))).thenReturn(order);
+
+ // act
+ orderService.updateStatusByPaymentResult(order, PaymentStatus.FAILED);
+
+ // assert
+ verify(orderRepository, times(1)).save(order);
+ // 상태 변경 검증은 OrderTest에서 이미 검증했으므로 제거
+ }
+
+ @DisplayName("결제 대기 상태면 주문 상태를 유지한다.")
+ @Test
+ void maintainsOrderStatus_whenPaymentPending() {
+ // arrange
+ Order order = Order.of(
+ OrderTestFixture.ValidOrder.USER_ID,
+ OrderTestFixture.ValidOrderItem.createMultipleItems()
+ );
+
+ // act
+ orderService.updateStatusByPaymentResult(order, PaymentStatus.PENDING);
+
+ // assert
+ verify(orderRepository, never()).save(any(Order.class));
+ }
+
+ @DisplayName("이미 완료된 주문은 처리하지 않는다.")
+ @Test
+ void skipsProcessing_whenOrderAlreadyCompleted() {
+ // arrange
+ Order order = Order.of(
+ OrderTestFixture.ValidOrder.USER_ID,
+ OrderTestFixture.ValidOrderItem.createMultipleItems()
+ );
+ order.complete(); // 이미 완료 상태
+
+ // act
+ orderService.updateStatusByPaymentResult(order, PaymentStatus.SUCCESS);
+
+ // assert
+ verify(orderRepository, never()).save(any(Order.class));
+ }
+
+ @DisplayName("이미 취소된 주문은 처리하지 않는다.")
+ @Test
+ void skipsProcessing_whenOrderAlreadyCanceled() {
+ // arrange
+ Order order = Order.of(
+ OrderTestFixture.ValidOrder.USER_ID,
+ OrderTestFixture.ValidOrderItem.createMultipleItems()
+ );
+ order.cancel(); // 이미 취소 상태
+
+ // act
+ orderService.updateStatusByPaymentResult(order, PaymentStatus.FAILED);
+
+ // assert
+ verify(orderRepository, never()).save(any(Order.class));
+ }
+
+ @DisplayName("주문이 null이면 예외가 발생한다.")
+ @Test
+ void throwsException_whenOrderIsNull() {
+ // act
+ CoreException result = assertThrows(CoreException.class, () -> {
+ orderService.updateStatusByPaymentResult(null, PaymentStatus.SUCCESS);
+ });
+
+ // assert
+ assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
+ verify(orderRepository, never()).save(any(Order.class));
+ }
+ }
+
+ private User createUser(Long userId) {
+ return User.of(
+ String.valueOf(userId),
+ "test@example.com",
+ "1990-01-01",
+ Gender.MALE,
+ Point.of(0L)
+ );
+ }
+
+ private Product createProduct(Long productId) {
+ // Mock을 사용하여 ID 설정
+ Product mockedProduct = mock(Product.class);
+ when(mockedProduct.getId()).thenReturn(productId);
+ doNothing().when(mockedProduct).increaseStock(any(Integer.class));
+ return mockedProduct;
+ }
}
From c725d49a8e91c7529d55c23bccb5df1b0a567eb0 Mon Sep 17 00:00:00 2001
From: minor7295
Date: Tue, 9 Dec 2025 00:48:53 +0900
Subject: [PATCH 20/22] =?UTF-8?q?refactor:=20Order=20=EB=8F=84=EB=A9=94?=
=?UTF-8?q?=EC=9D=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90=20=EC=9E=88?=
=?UTF-8?q?=EB=8D=98=20=EB=82=B4=EC=9A=A9=20=EC=A4=91=20=EC=96=B4=ED=94=8C?=
=?UTF-8?q?=EB=A6=AC=EC=BC=80=EC=9D=B4=EC=85=98=20=EC=84=9C=EB=B9=84?=
=?UTF-8?q?=EC=8A=A4=EC=97=90=20=ED=95=B4=EB=8B=B9=ED=95=98=EB=8A=94=20?=
=?UTF-8?q?=EB=B6=80=EB=B6=84=EB=93=A4=EC=9D=80=20application=20=EB=A0=88?=
=?UTF-8?q?=EC=9D=B4=EC=96=B4=EB=A1=9C=20=EC=9D=B4=EB=8F=99=EC=8B=9C?=
=?UTF-8?q?=ED=82=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../purchasing/PurchasingFacade.java | 135 +++++++++++++++---
.../order/OrderCancellationService.java | 117 ---------------
.../order/OrderPaymentResultService.java | 100 -------------
.../loopers/domain/order/OrderService.java | 90 ++++++++++++
4 files changed, 203 insertions(+), 239 deletions(-)
delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderCancellationService.java
delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPaymentResultService.java
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
index 2d5e89b64..19020de50 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
@@ -12,8 +12,6 @@
import com.loopers.domain.coupon.CouponService;
import com.loopers.infrastructure.paymentgateway.PaymentGatewayDto;
import com.loopers.domain.payment.PaymentRequestResult;
-import com.loopers.domain.order.OrderPaymentResultService;
-import com.loopers.domain.order.OrderCancellationService;
import com.loopers.domain.payment.PaymentService;
import com.loopers.domain.payment.Payment;
import com.loopers.domain.payment.PaymentStatus;
@@ -52,8 +50,6 @@ public class PurchasingFacade {
private final ProductService productService;
private final CouponService couponService;
private final OrderService orderService;
- private final OrderCancellationService orderCancellationService;
- private final OrderPaymentResultService orderPaymentResultService;
private final PaymentService paymentService; // Payment 관련: PaymentService만 의존 (DIP 준수)
private final PlatformTransactionManager transactionManager;
@@ -174,9 +170,6 @@ public OrderInfo createOrder(String userId, List commands, Lon
discountAmount = couponService.applyCoupon(user.getId(), couponCode, calculateSubtotal(orderItems));
}
- // 주문 총액 계산 (쿠폰 할인 적용)
- Integer orderTotalAmount = calculateSubtotal(orderItems) - discountAmount;
-
// 포인트 차감 (지정된 금액만)
Long usedPointAmount = Objects.requireNonNullElse(usedPoint, 0L);
@@ -287,18 +280,48 @@ public void afterCommit() {
* @param order 주문 엔티티
* @param user 사용자 엔티티
*/
- /**
- * 주문을 취소하고 포인트를 환불하며 재고를 원복한다.
- *
- * OrderCancellationService를 사용하여 처리합니다.
- *
- *
- * @param order 주문 엔티티
- * @param user 사용자 엔티티
- */
@Transactional
public void cancelOrder(Order order, User user) {
- orderCancellationService.cancel(order, user);
+ if (order == null || user == null) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "취소할 주문과 사용자 정보는 필수입니다.");
+ }
+
+ // ✅ Deadlock 방지: User 락을 먼저 획득하여 createOrder와 동일한 락 획득 순서 보장
+ User lockedUser = userService.findByUserIdForUpdate(user.getUserId());
+ if (lockedUser == null) {
+ throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다.");
+ }
+
+ // ✅ Deadlock 방지: 상품 ID를 정렬하여 일관된 락 획득 순서 보장
+ List sortedProductIds = order.getItems().stream()
+ .map(OrderItem::getProductId)
+ .distinct()
+ .sorted()
+ .toList();
+
+ // 정렬된 순서대로 상품 락 획득 (Deadlock 방지)
+ Map productMap = new java.util.HashMap<>();
+ for (Long productId : sortedProductIds) {
+ Product product = productService.findByIdForUpdate(productId);
+ productMap.put(productId, product);
+ }
+
+ // OrderItem 순서대로 Product 리스트 생성
+ List products = order.getItems().stream()
+ .map(item -> productMap.get(item.getProductId()))
+ .toList();
+
+ // 실제로 사용된 포인트만 환불 (Payment에서 확인)
+ Long refundPointAmount = paymentService.findByOrderId(order.getId())
+ .map(Payment::getUsedPoint)
+ .orElse(0L);
+
+ // 도메인 서비스를 통한 주문 취소 처리
+ orderService.cancelOrder(order, products, lockedUser, refundPointAmount);
+
+ // 저장
+ productService.saveAll(products);
+ userService.save(lockedUser);
}
/**
@@ -431,6 +454,74 @@ private PaymentGatewayDto.TransactionStatus convertToInfraStatus(PaymentStatus p
};
}
+ /**
+ * 결제 상태에 따라 주문 상태를 업데이트합니다.
+ *
+ * 별도 트랜잭션으로 실행하여 외부 시스템 호출과 독립적으로 처리합니다.
+ *
+ *
+ * @param orderId 주문 ID
+ * @param paymentStatus 결제 상태 (도메인 모델)
+ * @param transactionKey 트랜잭션 키
+ * @param reason 실패 사유 (실패 시)
+ * @return 업데이트 성공 여부 (true: 성공, false: 실패)
+ */
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
+ public boolean updateOrderStatusByPaymentResult(
+ Long orderId,
+ PaymentStatus paymentStatus,
+ String transactionKey,
+ String reason
+ ) {
+ try {
+ Order order = orderService.findById(orderId).orElse(null);
+
+ if (order == null) {
+ log.warn("주문 상태 업데이트 시 주문을 찾을 수 없습니다. (orderId: {})", orderId);
+ return false;
+ }
+
+ // 이미 완료되거나 취소된 주문인 경우 처리하지 않음 (정상적인 경우이므로 true 반환)
+ if (order.getStatus() == OrderStatus.COMPLETED) {
+ log.info("이미 완료된 주문입니다. 상태 업데이트를 건너뜁니다. (orderId: {})", orderId);
+ return true;
+ }
+
+ if (order.getStatus() == OrderStatus.CANCELED) {
+ log.info("이미 취소된 주문입니다. 상태 업데이트를 건너뜁니다. (orderId: {})", orderId);
+ return true;
+ }
+
+ if (paymentStatus == PaymentStatus.SUCCESS) {
+ // 결제 성공: 주문 완료
+ orderService.updateStatusByPaymentResult(order, paymentStatus);
+ log.info("결제 상태 확인 결과, 주문 상태를 COMPLETED로 업데이트했습니다. (orderId: {}, transactionKey: {})",
+ orderId, transactionKey);
+ return true;
+ } else if (paymentStatus == PaymentStatus.FAILED) {
+ // 결제 실패: 주문 취소 및 리소스 원복
+ User user = userService.findById(order.getUserId());
+ if (user == null) {
+ log.warn("주문 상태 업데이트 시 사용자를 찾을 수 없습니다. (orderId: {}, userId: {})",
+ orderId, order.getUserId());
+ return false;
+ }
+ cancelOrder(order, user);
+ log.info("결제 상태 확인 결과, 주문 상태를 CANCELED로 업데이트했습니다. (orderId: {}, transactionKey: {}, reason: {})",
+ orderId, transactionKey, reason);
+ return true;
+ } else {
+ // PENDING 상태: 아직 처리 중 (정상적인 경우이므로 true 반환)
+ log.info("결제 상태 확인 결과, 아직 처리 중입니다. 주문은 PENDING 상태로 유지됩니다. (orderId: {}, transactionKey: {})",
+ orderId, transactionKey);
+ return true;
+ }
+ } catch (Exception e) {
+ log.error("주문 상태 업데이트 중 오류 발생. (orderId: {})", orderId, e);
+ return false;
+ }
+ }
+
/**
* 주문 상태를 COMPLETED로 업데이트합니다.
*
@@ -594,8 +685,8 @@ public void handlePaymentCallback(Long orderId, PaymentGatewayDto.CallbackReques
callbackRequest.reason()
);
- // OrderPaymentResultService를 사용하여 주문 상태 업데이트
- boolean updated = orderPaymentResultService.updateByPaymentStatus(
+ // 주문 상태 업데이트 처리
+ boolean updated = updateOrderStatusByPaymentResult(
orderId,
paymentStatus,
callbackRequest.transactionKey(),
@@ -708,8 +799,8 @@ public void recoverOrderStatusByPaymentCheck(String userId, Long orderId) {
// 결제 상태 조회
PaymentStatus paymentStatus = paymentService.getPaymentStatus(userId, orderId);
- // OrderPaymentResultService를 사용하여 주문 상태 업데이트
- boolean updated = orderPaymentResultService.updateByPaymentStatus(orderId, paymentStatus, null, null);
+ // 주문 상태 업데이트 처리
+ boolean updated = updateOrderStatusByPaymentResult(orderId, paymentStatus, null, null);
if (!updated) {
log.warn("상태 복구 실패. 주문 상태 업데이트에 실패했습니다. (orderId: {})", orderId);
@@ -789,7 +880,7 @@ private void handlePaymentFailure(String userId, Long orderId, String errorCode,
}
// 주문 취소 및 리소스 원복
- orderCancellationService.cancel(order, user);
+ cancelOrder(order, user);
log.info("결제 실패로 인한 주문 취소 완료. (orderId: {}, errorCode: {}, errorMessage: {})",
orderId, errorCode, errorMessage);
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderCancellationService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderCancellationService.java
deleted file mode 100644
index aeb549631..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderCancellationService.java
+++ /dev/null
@@ -1,117 +0,0 @@
-package com.loopers.domain.order;
-
-import com.loopers.domain.payment.Payment;
-import com.loopers.domain.payment.PaymentService;
-import com.loopers.domain.product.Product;
-import com.loopers.domain.product.ProductRepository;
-import com.loopers.domain.user.Point;
-import com.loopers.domain.user.User;
-import com.loopers.domain.user.UserRepository;
-import com.loopers.support.error.CoreException;
-import com.loopers.support.error.ErrorType;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 주문 취소 도메인 서비스.
- *
- * 주문 취소 및 리소스 원복을 처리합니다.
- *
- *
- * @author Loopers
- * @version 1.0
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class OrderCancellationService {
-
- private final OrderRepository orderRepository;
- private final UserRepository userRepository;
- private final ProductRepository productRepository;
- private final PaymentService paymentService;
-
- /**
- * 주문을 취소하고 포인트를 환불하며 재고를 원복합니다.
- *
- * 동시성 제어:
- *
- * - 비관적 락 사용: 재고 원복 시 동시성 제어를 위해 findByIdForUpdate 사용
- * - Deadlock 방지: 상품 ID를 정렬하여 일관된 락 획득 순서 보장
- *
- *
- *
- * @param order 주문 엔티티
- * @param user 사용자 엔티티
- */
- @Transactional
- public void cancel(Order order, User user) {
- if (order == null || user == null) {
- throw new CoreException(ErrorType.BAD_REQUEST, "취소할 주문과 사용자 정보는 필수입니다.");
- }
-
- // ✅ Deadlock 방지: User 락을 먼저 획득하여 createOrder와 동일한 락 획득 순서 보장
- User lockedUser = userRepository.findByUserIdForUpdate(user.getUserId());
- if (lockedUser == null) {
- throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다.");
- }
-
- // ✅ Deadlock 방지: 상품 ID를 정렬하여 일관된 락 획득 순서 보장
- List sortedProductIds = order.getItems().stream()
- .map(OrderItem::getProductId)
- .distinct()
- .sorted()
- .toList();
-
- // 정렬된 순서대로 상품 락 획득 (Deadlock 방지)
- Map productMap = new HashMap<>();
- for (Long productId : sortedProductIds) {
- Product product = productRepository.findByIdForUpdate(productId)
- .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
- String.format("상품을 찾을 수 없습니다. (상품 ID: %d)", productId)));
- productMap.put(productId, product);
- }
-
- // OrderItem 순서대로 Product 리스트 생성
- List products = order.getItems().stream()
- .map(item -> productMap.get(item.getProductId()))
- .toList();
-
- order.cancel();
- increaseStocksForOrderItems(order.getItems(), products);
-
- // 실제로 사용된 포인트만 환불 (Payment에서 확인)
- Long refundPointAmount = paymentService.findByOrderId(order.getId())
- .map(Payment::getUsedPoint)
- .orElse(0L);
-
- if (refundPointAmount > 0) {
- lockedUser.receivePoint(Point.of(refundPointAmount));
- }
-
- products.forEach(productRepository::save);
- userRepository.save(lockedUser);
- orderRepository.save(order);
- }
-
- private void increaseStocksForOrderItems(List items, List products) {
- Map productMap = products.stream()
- .collect(java.util.stream.Collectors.toMap(Product::getId, product -> product));
-
- for (OrderItem item : items) {
- Product product = productMap.get(item.getProductId());
- if (product == null) {
- throw new CoreException(ErrorType.NOT_FOUND,
- String.format("상품을 찾을 수 없습니다. (상품 ID: %d)", item.getProductId()));
- }
- product.increaseStock(item.getQuantity());
- }
- }
-}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPaymentResultService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPaymentResultService.java
deleted file mode 100644
index 001336252..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPaymentResultService.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package com.loopers.domain.order;
-
-import com.loopers.domain.payment.PaymentStatus;
-import com.loopers.domain.user.User;
-import com.loopers.domain.user.UserRepository;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Propagation;
-import org.springframework.transaction.annotation.Transactional;
-
-/**
- * 주문 결제 결과 처리 도메인 서비스.
- *
- * 결제 결과에 따라 주문 상태를 처리합니다.
- *
- *
- * @author Loopers
- * @version 1.0
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class OrderPaymentResultService {
-
- private final OrderRepository orderRepository;
- private final UserRepository userRepository;
- private final OrderCancellationService orderCancellationService;
-
- /**
- * 결제 상태에 따라 주문 상태를 업데이트합니다.
- *
- * 별도 트랜잭션으로 실행하여 외부 시스템 호출과 독립적으로 처리합니다.
- *
- *
- * @param orderId 주문 ID
- * @param paymentStatus 결제 상태 (도메인 모델)
- * @param transactionKey 트랜잭션 키
- * @param reason 실패 사유 (실패 시)
- * @return 업데이트 성공 여부 (true: 성공, false: 실패)
- */
- @Transactional(propagation = Propagation.REQUIRES_NEW)
- public boolean updateByPaymentStatus(
- Long orderId,
- PaymentStatus paymentStatus,
- String transactionKey,
- String reason
- ) {
- try {
- Order order = orderRepository.findById(orderId)
- .orElse(null);
-
- if (order == null) {
- log.warn("주문 상태 업데이트 시 주문을 찾을 수 없습니다. (orderId: {})", orderId);
- return false;
- }
-
- // 이미 완료되거나 취소된 주문인 경우 처리하지 않음 (정상적인 경우이므로 true 반환)
- if (order.getStatus() == OrderStatus.COMPLETED) {
- log.info("이미 완료된 주문입니다. 상태 업데이트를 건너뜁니다. (orderId: {})", orderId);
- return true;
- }
-
- if (order.getStatus() == OrderStatus.CANCELED) {
- log.info("이미 취소된 주문입니다. 상태 업데이트를 건너뜁니다. (orderId: {})", orderId);
- return true;
- }
-
- if (paymentStatus == PaymentStatus.SUCCESS) {
- // 결제 성공: 주문 완료
- order.complete();
- orderRepository.save(order);
- log.info("결제 상태 확인 결과, 주문 상태를 COMPLETED로 업데이트했습니다. (orderId: {}, transactionKey: {})",
- orderId, transactionKey);
- return true;
- } else if (paymentStatus == PaymentStatus.FAILED) {
- // 결제 실패: 주문 취소 및 리소스 원복
- User user = userRepository.findById(order.getUserId());
- if (user == null) {
- log.warn("주문 상태 업데이트 시 사용자를 찾을 수 없습니다. (orderId: {}, userId: {})",
- orderId, order.getUserId());
- return false;
- }
- orderCancellationService.cancel(order, user);
- log.info("결제 상태 확인 결과, 주문 상태를 CANCELED로 업데이트했습니다. (orderId: {}, transactionKey: {}, reason: {})",
- orderId, transactionKey, reason);
- return true;
- } else {
- // PENDING 상태: 아직 처리 중 (정상적인 경우이므로 true 반환)
- log.info("결제 상태 확인 결과, 아직 처리 중입니다. 주문은 PENDING 상태로 유지됩니다. (orderId: {}, transactionKey: {})",
- orderId, transactionKey);
- return true;
- }
- } catch (Exception e) {
- log.error("주문 상태 업데이트 중 오류 발생. (orderId: {})", orderId, e);
- return false;
- }
- }
-}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java
index 88207b645..e74c024d7 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java
@@ -1,5 +1,9 @@
package com.loopers.domain.order;
+import com.loopers.domain.payment.PaymentStatus;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.user.Point;
+import com.loopers.domain.user.User;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
@@ -7,6 +11,7 @@
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
/**
@@ -122,5 +127,90 @@ public Order completeOrder(Long orderId) {
order.complete();
return orderRepository.save(order);
}
+
+ /**
+ * 주문을 취소 상태로 변경하고 재고를 원복하며 포인트를 환불합니다.
+ *
+ * 도메인 로직만 처리합니다. 사용자 조회, 상품 조회, Payment 조회는 애플리케이션 레이어에서 처리합니다.
+ *
+ *
+ * @param order 주문 엔티티
+ * @param products 주문 아이템에 해당하는 상품 목록 (락이 이미 획득된 상태)
+ * @param user 사용자 엔티티 (락이 이미 획득된 상태)
+ * @param refundPointAmount 환불할 포인트 금액
+ * @throws CoreException 주문 또는 사용자 정보가 null인 경우
+ */
+ @Transactional
+ public void cancelOrder(Order order, List products, User user, Long refundPointAmount) {
+ if (order == null || user == null) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "취소할 주문과 사용자 정보는 필수입니다.");
+ }
+
+ // 주문 취소
+ order.cancel();
+
+ // 재고 원복
+ increaseStocksForOrderItems(order.getItems(), products);
+
+ // 포인트 환불
+ if (refundPointAmount > 0) {
+ user.receivePoint(Point.of(refundPointAmount));
+ }
+
+ orderRepository.save(order);
+ }
+
+ /**
+ * 결제 상태에 따라 주문 상태를 업데이트합니다.
+ *
+ * 도메인 로직만 처리합니다. 사용자 조회, 트랜잭션 관리, 로깅은 애플리케이션 레이어에서 처리합니다.
+ *
+ *
+ * @param order 주문 엔티티
+ * @param paymentStatus 결제 상태
+ * @throws CoreException 주문이 null이거나 이미 완료/취소된 경우
+ */
+ @Transactional
+ public void updateStatusByPaymentResult(Order order, PaymentStatus paymentStatus) {
+ if (order == null) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "주문 정보는 필수입니다.");
+ }
+
+ // 이미 완료되거나 취소된 주문인 경우 처리하지 않음
+ if (order.getStatus() == OrderStatus.COMPLETED || order.getStatus() == OrderStatus.CANCELED) {
+ return;
+ }
+
+ if (paymentStatus == PaymentStatus.SUCCESS) {
+ // 결제 성공: 주문 완료
+ order.complete();
+ orderRepository.save(order);
+ } else if (paymentStatus == PaymentStatus.FAILED) {
+ // 결제 실패: 주문 취소 (재고 원복 및 포인트 환불은 애플리케이션 레이어에서 처리)
+ order.cancel();
+ orderRepository.save(order);
+ }
+ // PENDING 상태: 상태 유지 (아무 작업도 하지 않음)
+ }
+
+ /**
+ * 주문 아이템에 대해 재고를 증가시킵니다.
+ *
+ * @param items 주문 아이템 목록
+ * @param products 상품 목록
+ */
+ private void increaseStocksForOrderItems(List items, List products) {
+ Map productMap = products.stream()
+ .collect(java.util.stream.Collectors.toMap(Product::getId, product -> product));
+
+ for (OrderItem item : items) {
+ Product product = productMap.get(item.getProductId());
+ if (product == null) {
+ throw new CoreException(ErrorType.NOT_FOUND,
+ String.format("상품을 찾을 수 없습니다. (상품 ID: %d)", item.getProductId()));
+ }
+ product.increaseStock(item.getQuantity());
+ }
+ }
}
From a527a9140d27cdbe2bc7cb1c36820280ab09d379 Mon Sep 17 00:00:00 2001
From: minor7295
Date: Tue, 9 Dec 2025 01:00:05 +0900
Subject: [PATCH 21/22] =?UTF-8?q?refactor:=20infrastructure=20=EB=A0=88?=
=?UTF-8?q?=EC=9D=B4=EC=96=B4=EC=97=90=20domain=EB=A0=88=EC=9D=B4=EC=96=B4?=
=?UTF-8?q?=EC=99=80=20=EB=8B=AC=EB=A6=AC=20payment=EC=99=80=20paymentgate?=
=?UTF-8?q?way=EA=B0=80=20=EA=B5=AC=EB=B6=84=EB=90=98=EC=96=B4=EC=9E=88?=
=?UTF-8?q?=EC=96=B4=20=ED=86=B5=EC=9D=BC=ED=95=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../loopers/application/purchasing/PurchasingFacade.java | 2 +-
.../{paymentgateway => payment}/DelayProvider.java | 3 +--
.../{paymentgateway => payment}/PaymentGatewayClient.java | 3 +--
.../{paymentgateway => payment}/PaymentGatewayDto.java | 3 +--
.../{paymentgateway => payment}/PaymentGatewayImpl.java | 3 +--
.../{paymentgateway => payment}/PaymentGatewayMetrics.java | 3 +--
.../PaymentGatewaySchedulerClient.java | 3 +--
.../{paymentgateway => payment}/ThreadDelayProvider.java | 3 +--
.../interfaces/api/purchasing/PurchasingV1Controller.java | 2 +-
.../purchasing/PurchasingFacadeCircuitBreakerTest.java | 4 ++--
.../purchasing/PurchasingFacadePaymentCallbackTest.java | 6 +++---
.../purchasing/PurchasingFacadePaymentGatewayTest.java | 4 ++--
.../application/purchasing/PurchasingFacadeTest.java | 4 ++--
.../PaymentGatewayClientTest.java | 3 +--
.../com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java | 6 +++---
15 files changed, 22 insertions(+), 30 deletions(-)
rename apps/commerce-api/src/main/java/com/loopers/infrastructure/{paymentgateway => payment}/DelayProvider.java (89%)
rename apps/commerce-api/src/main/java/com/loopers/infrastructure/{paymentgateway => payment}/PaymentGatewayClient.java (98%)
rename apps/commerce-api/src/main/java/com/loopers/infrastructure/{paymentgateway => payment}/PaymentGatewayDto.java (98%)
rename apps/commerce-api/src/main/java/com/loopers/infrastructure/{paymentgateway => payment}/PaymentGatewayImpl.java (99%)
rename apps/commerce-api/src/main/java/com/loopers/infrastructure/{paymentgateway => payment}/PaymentGatewayMetrics.java (97%)
rename apps/commerce-api/src/main/java/com/loopers/infrastructure/{paymentgateway => payment}/PaymentGatewaySchedulerClient.java (97%)
rename apps/commerce-api/src/main/java/com/loopers/infrastructure/{paymentgateway => payment}/ThreadDelayProvider.java (88%)
rename apps/commerce-api/src/test/java/com/loopers/infrastructure/{paymentgateway => payment}/PaymentGatewayClientTest.java (99%)
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
index 19020de50..eb7ceb035 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
@@ -10,7 +10,7 @@
import com.loopers.domain.user.User;
import com.loopers.domain.user.UserService;
import com.loopers.domain.coupon.CouponService;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayDto;
+import com.loopers.infrastructure.payment.PaymentGatewayDto;
import com.loopers.domain.payment.PaymentRequestResult;
import com.loopers.domain.payment.PaymentService;
import com.loopers.domain.payment.Payment;
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/DelayProvider.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/DelayProvider.java
similarity index 89%
rename from apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/DelayProvider.java
rename to apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/DelayProvider.java
index 11cc69f71..22fabd259 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/DelayProvider.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/DelayProvider.java
@@ -1,4 +1,4 @@
-package com.loopers.infrastructure.paymentgateway;
+package com.loopers.infrastructure.payment;
import java.time.Duration;
@@ -21,4 +21,3 @@ public interface DelayProvider {
*/
void delay(Duration duration) throws InterruptedException;
}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayClient.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayClient.java
similarity index 98%
rename from apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayClient.java
rename to apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayClient.java
index 9357ec6d3..cf6e1b2d6 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayClient.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayClient.java
@@ -1,4 +1,4 @@
-package com.loopers.infrastructure.paymentgateway;
+package com.loopers.infrastructure.payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@@ -82,4 +82,3 @@ PaymentGatewayDto.ApiResponse getTransactionsBy
@RequestParam("orderId") String orderId
);
}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayDto.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayDto.java
similarity index 98%
rename from apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayDto.java
rename to apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayDto.java
index 812fbed96..4ca22424f 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayDto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayDto.java
@@ -1,4 +1,4 @@
-package com.loopers.infrastructure.paymentgateway;
+package com.loopers.infrastructure.payment;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -103,4 +103,3 @@ public enum Result {
}
}
}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayImpl.java
similarity index 99%
rename from apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayImpl.java
rename to apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayImpl.java
index ac5e0bdbc..6ef5d43fb 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayImpl.java
@@ -1,4 +1,4 @@
-package com.loopers.infrastructure.paymentgateway;
+package com.loopers.infrastructure.payment;
import com.loopers.domain.payment.PaymentGateway;
import com.loopers.application.purchasing.PaymentRequestCommand;
@@ -147,4 +147,3 @@ private PaymentStatus convertToPaymentStatus(PaymentGatewayDto.TransactionStatus
}
}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayMetrics.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayMetrics.java
similarity index 97%
rename from apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayMetrics.java
rename to apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayMetrics.java
index 57c97df10..72bc0b96b 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayMetrics.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayMetrics.java
@@ -1,4 +1,4 @@
-package com.loopers.infrastructure.paymentgateway;
+package com.loopers.infrastructure.payment;
import io.micrometer.core.instrument.MeterRegistry;
import lombok.RequiredArgsConstructor;
@@ -83,4 +83,3 @@ public void recordSuccess(String clientName) {
).increment();
}
}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewaySchedulerClient.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewaySchedulerClient.java
similarity index 97%
rename from apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewaySchedulerClient.java
rename to apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewaySchedulerClient.java
index 44d693912..01451ab62 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/PaymentGatewaySchedulerClient.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewaySchedulerClient.java
@@ -1,4 +1,4 @@
-package com.loopers.infrastructure.paymentgateway;
+package com.loopers.infrastructure.payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@@ -67,4 +67,3 @@ PaymentGatewayDto.ApiResponse getTransactionsBy
@RequestParam("orderId") String orderId
);
}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/ThreadDelayProvider.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/ThreadDelayProvider.java
similarity index 88%
rename from apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/ThreadDelayProvider.java
rename to apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/ThreadDelayProvider.java
index 803a6f304..d31ef49d9 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/paymentgateway/ThreadDelayProvider.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/ThreadDelayProvider.java
@@ -1,4 +1,4 @@
-package com.loopers.infrastructure.paymentgateway;
+package com.loopers.infrastructure.payment;
import org.springframework.stereotype.Component;
@@ -18,4 +18,3 @@ public void delay(Duration duration) throws InterruptedException {
Thread.sleep(duration.toMillis());
}
}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Controller.java
index e6fd5ee27..937a9cf20 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Controller.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/purchasing/PurchasingV1Controller.java
@@ -2,7 +2,7 @@
import com.loopers.application.purchasing.OrderInfo;
import com.loopers.application.purchasing.PurchasingFacade;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayDto;
+import com.loopers.infrastructure.payment.PaymentGatewayDto;
import com.loopers.interfaces.api.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeCircuitBreakerTest.java b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeCircuitBreakerTest.java
index f13e550e0..c8de02c57 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeCircuitBreakerTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeCircuitBreakerTest.java
@@ -12,8 +12,8 @@
import com.loopers.domain.user.Point;
import com.loopers.domain.user.User;
import com.loopers.domain.user.UserRepository;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayClient;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayDto;
+import com.loopers.infrastructure.payment.PaymentGatewayClient;
+import com.loopers.infrastructure.payment.PaymentGatewayDto;
import com.loopers.utils.DatabaseCleanUp;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentCallbackTest.java b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentCallbackTest.java
index 331a41ed2..b2c9cc516 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentCallbackTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentCallbackTest.java
@@ -11,9 +11,9 @@
import com.loopers.domain.user.Point;
import com.loopers.domain.user.User;
import com.loopers.domain.user.UserRepository;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayClient;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayDto;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewaySchedulerClient;
+import com.loopers.infrastructure.payment.PaymentGatewayClient;
+import com.loopers.infrastructure.payment.PaymentGatewayDto;
+import com.loopers.infrastructure.payment.PaymentGatewaySchedulerClient;
import com.loopers.utils.DatabaseCleanUp;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentGatewayTest.java b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentGatewayTest.java
index f66ef1e2c..366a0b91a 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentGatewayTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadePaymentGatewayTest.java
@@ -11,8 +11,8 @@
import com.loopers.domain.user.Point;
import com.loopers.domain.user.User;
import com.loopers.domain.user.UserRepository;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayClient;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayDto;
+import com.loopers.infrastructure.payment.PaymentGatewayClient;
+import com.loopers.infrastructure.payment.PaymentGatewayDto;
import com.loopers.utils.DatabaseCleanUp;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeTest.java
index cb66c913c..4fa80edfa 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/purchasing/PurchasingFacadeTest.java
@@ -15,8 +15,8 @@
import com.loopers.domain.user.Point;
import com.loopers.domain.user.User;
import com.loopers.domain.user.UserRepository;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayClient;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayDto;
+import com.loopers.infrastructure.payment.PaymentGatewayClient;
+import com.loopers.infrastructure.payment.PaymentGatewayDto;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import com.loopers.utils.DatabaseCleanUp;
diff --git a/apps/commerce-api/src/test/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayClientTest.java b/apps/commerce-api/src/test/java/com/loopers/infrastructure/payment/PaymentGatewayClientTest.java
similarity index 99%
rename from apps/commerce-api/src/test/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayClientTest.java
rename to apps/commerce-api/src/test/java/com/loopers/infrastructure/payment/PaymentGatewayClientTest.java
index 89b3eef61..dbfac8411 100644
--- a/apps/commerce-api/src/test/java/com/loopers/infrastructure/paymentgateway/PaymentGatewayClientTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/infrastructure/payment/PaymentGatewayClientTest.java
@@ -1,4 +1,4 @@
-package com.loopers.infrastructure.paymentgateway;
+package com.loopers.infrastructure.payment;
import com.loopers.utils.DatabaseCleanUp;
import org.junit.jupiter.api.AfterEach;
@@ -274,4 +274,3 @@ void getTransaction_success_returnsResponse() {
assertThat(response.data().transactionKey()).isEqualTo(transactionKey);
}
}
-
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java
index 1b025fccc..a09516c93 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java
@@ -9,9 +9,9 @@
import com.loopers.domain.product.ProductRepository;
import com.loopers.domain.user.Gender;
import com.loopers.domain.user.UserTestFixture;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayClient;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewayDto;
-import com.loopers.infrastructure.paymentgateway.PaymentGatewaySchedulerClient;
+import com.loopers.infrastructure.payment.PaymentGatewayClient;
+import com.loopers.infrastructure.payment.PaymentGatewayDto;
+import com.loopers.infrastructure.payment.PaymentGatewaySchedulerClient;
import com.loopers.interfaces.api.ApiResponse;
import com.loopers.interfaces.api.purchasing.PurchasingV1Dto;
import com.loopers.utils.DatabaseCleanUp;
From 73678c867acc06b8190724e70746c12a65abca6d Mon Sep 17 00:00:00 2001
From: minor7295
Date: Tue, 9 Dec 2025 01:05:52 +0900
Subject: [PATCH 22/22] =?UTF-8?q?chore:=20=EC=A4=91=EB=B3=B5=EB=90=98?=
=?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EA=B7=B8=20=EC=A0=95=EB=A6=AC,=20?=
=?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=98=EA=B2=8C=20=EB=86=92?=
=?UTF-8?q?=EC=9D=80=20level=EC=9D=B8=20=EB=A1=9C=EA=B7=B8=EB=8A=94=20debu?=
=?UTF-8?q?g=20=EB=A1=9C=EA=B7=B8=EB=A1=9C=20=EC=A0=84=ED=99=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../purchasing/PurchasingFacade.java | 26 ++++++++-----------
.../config/Resilience4jRetryConfig.java | 6 ++---
.../domain/payment/PaymentService.java | 4 +--
.../payment/PaymentGatewayImpl.java | 1 -
4 files changed, 16 insertions(+), 21 deletions(-)
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
index eb7ceb035..650870153 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
@@ -220,7 +220,7 @@ public OrderInfo createOrder(String userId, List commands, Lon
paymentService.toSuccess(payment.getId(), java.time.LocalDateTime.now());
productService.saveAll(products);
userService.save(user);
- log.info("포인트+쿠폰으로 전액 결제 완료. (orderId: {})", savedOrder.getId());
+ log.debug("포인트+쿠폰으로 전액 결제 완료. (orderId: {})", savedOrder.getId());
return OrderInfo.from(savedOrder);
}
@@ -248,11 +248,10 @@ public void afterCommit() {
if (transactionKey != null) {
// 결제 성공: 별도 트랜잭션에서 주문 상태를 COMPLETED로 변경
updateOrderStatusToCompleted(orderId, transactionKey);
- log.info("PG 결제 요청 완료. (orderId: {}, transactionKey: {})", orderId, transactionKey);
} else {
// PG 요청 실패: 외부 시스템 장애로 간주
// 주문은 PENDING 상태로 유지되어 나중에 상태 확인 API나 콜백으로 복구 가능
- log.info("PG 결제 요청 실패. 주문은 PENDING 상태로 유지됩니다. (orderId: {})", orderId);
+ log.debug("PG 결제 요청 실패. 주문은 PENDING 상태로 유지됩니다. (orderId: {})", orderId);
}
} catch (Exception e) {
// PG 요청 중 예외 발생 시에도 주문은 이미 저장되어 있으므로 유지
@@ -483,12 +482,12 @@ public boolean updateOrderStatusByPaymentResult(
// 이미 완료되거나 취소된 주문인 경우 처리하지 않음 (정상적인 경우이므로 true 반환)
if (order.getStatus() == OrderStatus.COMPLETED) {
- log.info("이미 완료된 주문입니다. 상태 업데이트를 건너뜁니다. (orderId: {})", orderId);
+ log.debug("이미 완료된 주문입니다. 상태 업데이트를 건너뜁니다. (orderId: {})", orderId);
return true;
}
if (order.getStatus() == OrderStatus.CANCELED) {
- log.info("이미 취소된 주문입니다. 상태 업데이트를 건너뜁니다. (orderId: {})", orderId);
+ log.debug("이미 취소된 주문입니다. 상태 업데이트를 건너뜁니다. (orderId: {})", orderId);
return true;
}
@@ -512,7 +511,7 @@ public boolean updateOrderStatusByPaymentResult(
return true;
} else {
// PENDING 상태: 아직 처리 중 (정상적인 경우이므로 true 반환)
- log.info("결제 상태 확인 결과, 아직 처리 중입니다. 주문은 PENDING 상태로 유지됩니다. (orderId: {}, transactionKey: {})",
+ log.debug("결제 상태 확인 결과, 아직 처리 중입니다. 주문은 PENDING 상태로 유지됩니다. (orderId: {}, transactionKey: {})",
orderId, transactionKey);
return true;
}
@@ -537,7 +536,7 @@ public void updateOrderStatusToCompleted(Long orderId, String transactionKey) {
Order order = orderService.getById(orderId);
if (order.getStatus() == OrderStatus.COMPLETED) {
- log.info("이미 완료된 주문입니다. 상태 업데이트를 건너뜁니다. (orderId: {})", orderId);
+ log.debug("이미 완료된 주문입니다. 상태 업데이트를 건너뜁니다. (orderId: {})", orderId);
return;
}
@@ -579,8 +578,6 @@ private String requestPaymentToGateway(String userId, Long userEntityId, Long or
// 결과 처리
if (result instanceof PaymentRequestResult.Success success) {
- log.info("PG 결제 요청 성공. (orderId: {}, transactionKey: {})",
- orderId, success.transactionKey());
return success.transactionKey();
} else if (result instanceof PaymentRequestResult.Failure failure) {
// PaymentService 내부에서 이미 실패 분류가 완료되었으므로, 여기서는 처리만 수행
@@ -589,13 +586,13 @@ private String requestPaymentToGateway(String userId, Long userEntityId, Long or
// Circuit Breaker OPEN은 외부 시스템 장애이므로 주문을 취소하지 않음
if ("CIRCUIT_BREAKER_OPEN".equals(failure.errorCode())) {
// 외부 시스템 장애: 주문은 PENDING 상태로 유지
- log.info("Circuit Breaker OPEN 상태. 주문은 PENDING 상태로 유지됩니다. (orderId: {})", orderId);
+ log.warn("Circuit Breaker OPEN 상태. 주문은 PENDING 상태로 유지됩니다. (orderId: {})", orderId);
return null;
}
if (failure.isTimeout()) {
// 타임아웃: 상태 확인 후 복구
- log.info("타임아웃 발생. PG 결제 상태 확인 API를 호출하여 실제 결제 상태를 확인합니다. (orderId: {})", orderId);
+ log.debug("타임아웃 발생. PG 결제 상태 확인 API를 호출하여 실제 결제 상태를 확인합니다. (orderId: {})", orderId);
paymentService.recoverAfterTimeout(userId, orderId);
} else if (!failure.isRetryable()) {
// 비즈니스 실패: 주문 취소 (별도 트랜잭션으로 처리)
@@ -613,7 +610,6 @@ private String requestPaymentToGateway(String userId, Long userEntityId, Long or
} catch (Exception e) {
// 기타 예외 처리
log.error("PG 결제 요청 중 예상치 못한 오류 발생. (orderId: {})", orderId, e);
- log.info("예상치 못한 오류 발생. 주문은 PENDING 상태로 유지됩니다. (orderId: {})", orderId);
return null;
}
}
@@ -660,13 +656,13 @@ public void handlePaymentCallback(Long orderId, PaymentGatewayDto.CallbackReques
// 이미 완료되거나 취소된 주문인 경우 처리하지 않음
if (order.getStatus() == OrderStatus.COMPLETED) {
- log.info("이미 완료된 주문입니다. 콜백 처리를 건너뜁니다. (orderId: {}, transactionKey: {})",
+ log.debug("이미 완료된 주문입니다. 콜백 처리를 건너뜁니다. (orderId: {}, transactionKey: {})",
orderId, callbackRequest.transactionKey());
return;
}
if (order.getStatus() == OrderStatus.CANCELED) {
- log.info("이미 취소된 주문입니다. 콜백 처리를 건너뜁니다. (orderId: {}, transactionKey: {})",
+ log.debug("이미 취소된 주문입니다. 콜백 처리를 건너뜁니다. (orderId: {}, transactionKey: {})",
orderId, callbackRequest.transactionKey());
return;
}
@@ -875,7 +871,7 @@ private void handlePaymentFailure(String userId, Long orderId, String errorCode,
// 이미 취소된 주문인 경우 처리하지 않음
if (order.getStatus() == OrderStatus.CANCELED) {
- log.info("이미 취소된 주문입니다. 결제 실패 처리를 건너뜁니다. (orderId: {})", orderId);
+ log.debug("이미 취소된 주문입니다. 결제 실패 처리를 건너뜁니다. (orderId: {})", orderId);
return;
}
diff --git a/apps/commerce-api/src/main/java/com/loopers/config/Resilience4jRetryConfig.java b/apps/commerce-api/src/main/java/com/loopers/config/Resilience4jRetryConfig.java
index 90923e2f3..ee554579f 100644
--- a/apps/commerce-api/src/main/java/com/loopers/config/Resilience4jRetryConfig.java
+++ b/apps/commerce-api/src/main/java/com/loopers/config/Resilience4jRetryConfig.java
@@ -119,9 +119,9 @@ public RetryRegistry retryRegistry() {
// Exponential Backoff 적용하여 일시적 오류 자동 복구
retryRegistry.addConfiguration("paymentGatewaySchedulerClient", retryConfig);
- log.info("Resilience4j Retry 설정 완료:");
- log.info(" - 결제 요청 API (paymentGatewayClient): Retry 없음 (유저 요청 경로 - 빠른 실패)");
- log.info(" - 조회 API (paymentGatewaySchedulerClient): Exponential Backoff 적용 (스케줄러 - 비동기/배치 기반 Retry)");
+ log.debug("Resilience4j Retry 설정 완료:");
+ log.debug(" - 결제 요청 API (paymentGatewayClient): Retry 없음 (유저 요청 경로 - 빠른 실패)");
+ log.debug(" - 조회 API (paymentGatewaySchedulerClient): Exponential Backoff 적용 (스케줄러 - 비동기/배치 기반 Retry)");
return retryRegistry;
}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java
index 9a7ac16c6..b7308675c 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java
@@ -303,7 +303,7 @@ public void handleCallback(Long orderId, String transactionKey, PaymentStatus st
orderId, transactionKey, reason);
} else {
// PENDING 상태: 상태 유지
- log.info("결제 콜백 처리: PENDING 상태 유지. (orderId: {}, transactionKey: {})", orderId, transactionKey);
+ log.debug("결제 콜백 처리: PENDING 상태 유지. (orderId: {}, transactionKey: {})", orderId, transactionKey);
}
}
@@ -343,7 +343,7 @@ public void recoverAfterTimeout(String userId, Long orderId, Duration delayDurat
log.warn("타임아웃 후 상태 확인 완료: FAILED. (orderId: {})", orderId);
} else {
// PENDING 상태: 상태 유지
- log.info("타임아웃 후 상태 확인: PENDING 상태 유지. (orderId: {})", orderId);
+ log.debug("타임아웃 후 상태 확인: PENDING 상태 유지. (orderId: {})", orderId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayImpl.java
index 6ef5d43fb..5d4b994fa 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayImpl.java
@@ -124,7 +124,6 @@ private PaymentRequestResult toDomainResult(
&& response.meta().result() == PaymentGatewayDto.ApiResponse.Metadata.Result.SUCCESS
&& response.data() != null) {
String transactionKey = response.data().transactionKey();
- log.info("PG 결제 요청 성공. (orderId: {}, transactionKey: {})", orderId, transactionKey);
metrics.recordSuccess("paymentGatewayClient");
return new PaymentRequestResult.Success(transactionKey);
} else {