Skip to content

Commit 095f955

Browse files
authored
Merge pull request #153 from rnqhstmd/round6
[volume-6] 외부 시스템 장애 및 지연 대응
2 parents bf1b6ca + ec20f80 commit 095f955

File tree

97 files changed

+4624
-132
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+4624
-132
lines changed

apps/commerce-api/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,11 @@ dependencies {
2121
testImplementation(testFixtures(project(":modules:redis")))
2222

2323
testImplementation("net.datafaker:datafaker:2.0.2")
24+
25+
// Resilience4j
26+
implementation("io.github.resilience4j:resilience4j-spring-boot3")
27+
implementation("org.springframework.boot:spring-boot-starter-aop")
28+
29+
// OpenFeign
30+
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
2431
}

apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@
44
import org.springframework.boot.SpringApplication;
55
import org.springframework.boot.autoconfigure.SpringBootApplication;
66
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
7+
import org.springframework.cloud.openfeign.EnableFeignClients;
8+
import org.springframework.scheduling.annotation.EnableAsync;
9+
import org.springframework.scheduling.annotation.EnableScheduling;
10+
711
import java.util.TimeZone;
812

13+
@EnableFeignClients(basePackages = "com.loopers.infrastructure.pg")
14+
@EnableScheduling
15+
@EnableAsync
916
@ConfigurationPropertiesScan
1017
@SpringBootApplication
1118
public class CommerceApiApplication {

apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java

Lines changed: 130 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
import com.loopers.domain.coupon.CouponService;
55
import com.loopers.domain.order.Order;
66
import com.loopers.domain.order.OrderService;
7-
import com.loopers.domain.point.PointService;
87
import com.loopers.domain.product.Product;
98
import com.loopers.domain.product.ProductService;
109
import com.loopers.domain.user.User;
1110
import com.loopers.domain.user.UserService;
1211
import com.loopers.support.error.CoreException;
1312
import com.loopers.support.error.ErrorType;
1413
import lombok.RequiredArgsConstructor;
15-
import org.springframework.orm.ObjectOptimisticLockingFailureException;
14+
import lombok.extern.slf4j.Slf4j;
1615
import org.springframework.stereotype.Component;
1716
import org.springframework.transaction.annotation.Transactional;
1817

@@ -21,66 +20,118 @@
2120
import java.util.function.Function;
2221
import java.util.stream.Collectors;
2322

23+
@Slf4j
2424
@Component
2525
@RequiredArgsConstructor
26-
@Transactional(readOnly = true)
2726
public class OrderFacade {
2827

29-
private final OrderService orderService;
3028
private final UserService userService;
3129
private final ProductService productService;
32-
private final PointService pointService;
3330
private final CouponService couponService;
31+
private final OrderService orderService;
3432

3533
@Transactional
36-
public OrderInfo placeOrder(OrderPlaceCommand command) {
37-
User user = userService.getUserByUserId(command.userId());
34+
public OrderInfo createOrder(OrderPlaceCommand command) {
35+
log.info("주문 생성 시작: userBusinessId={}, items={}",
36+
command.userId(), command.items().size());
3837

39-
List<Long> productIds = command.items().stream()
40-
.map(OrderPlaceCommand.OrderItemCommand::productId)
41-
.sorted()
42-
.toList();
38+
// 1. 사용자 조회
39+
User user = userService.getUserByUserId(command.userId());
4340

41+
// 2. 상품 조회 (데드락 방지를 위한 정렬 + 비관적 락)
42+
List<Long> productIds = extractAndSortProductIds(command.items());
4443
List<Product> products = productService.getProductsByIdsWithPessimisticLock(productIds);
45-
Map<Long, Product> productMap = products.stream()
46-
.collect(Collectors.toMap(Product::getId, Function.identity()));
44+
Map<Long, Product> productMap = toProductMap(products);
4745

46+
// 3. 재고 검증 및 차감 (도메인 로직은 Product가 처리)
4847
validateAndDecreaseStock(command.items(), productMap);
4948

50-
Order order = Order.create(user);
49+
// 4. 주문 생성 (도메인 서비스 위임)
50+
List<OrderService.OrderItemRequest> itemRequests = command.items().stream()
51+
.map(item -> OrderService.OrderItemRequest.of(item.productId(), item.quantity()))
52+
.toList();
53+
Order order = orderService.createOrderWithItems(user, itemRequests, productMap);
5154

52-
for (OrderPlaceCommand.OrderItemCommand item : command.items()) {
53-
Product product = productMap.get(item.productId());
54-
order.addOrderItem(product, item.quantity());
55-
}
55+
// 5. 쿠폰 적용 (선택)
56+
Long discountAmount = applyCouponIfExists(command.couponId(), user, order);
57+
58+
// 6. 주문 저장
59+
Order savedOrder = orderService.save(order);
60+
61+
log.info("주문 생성 완료: orderId={}, totalAmount={}, discountAmount={}",
62+
savedOrder.getId(), savedOrder.getTotalAmountValue(), discountAmount);
5663

57-
Long totalAmount = order.getTotalAmountValue();
58-
Long discountAmount = 0L;
59-
Long couponId = command.couponId();
64+
return OrderInfo.from(savedOrder, discountAmount);
65+
}
6066

61-
if (couponId != null) {
62-
Coupon coupon = couponService.getCouponWithOptimisticLock(couponId);
63-
couponService.validateCouponUsable(coupon, user);
67+
@Transactional
68+
public void cancelOrder(Long orderId, Long couponId) {
69+
log.info("주문 취소 시작: orderId={}", orderId);
6470

65-
discountAmount = coupon.calculateDiscount(totalAmount);
66-
coupon.use();
67-
couponService.save(coupon);
71+
Order order = orderService.getOrderById(orderId);
72+
73+
// 1. 재고 복구
74+
List<Long> productIds = order.getOrderItems().stream()
75+
.map(item -> item.getProductId())
76+
.sorted()
77+
.toList();
78+
List<Product> products = productService.getProductsByIdsWithPessimisticLock(productIds);
79+
Map<Long, Product> productMap = toProductMap(products);
80+
81+
orderService.restoreStock(order, productMap);
82+
83+
// 2. 쿠폰 복구
84+
Long couponIdToRestore = couponId != null ? couponId : order.getCouponId();
85+
if (couponIdToRestore != null) {
86+
restoreCoupon(couponIdToRestore);
6887
}
69-
long finalAmount = totalAmount - discountAmount;
70-
pointService.usePointWithLock(user.getUserIdValue(), finalAmount);
7188

89+
// 3. 주문 취소 상태 변경
90+
order.markAsCancelled();
91+
orderService.save(order);
92+
93+
log.info("주문 취소 완료: orderId={}", orderId);
94+
}
95+
96+
@Transactional
97+
public void completeOrder(Long orderId) {
98+
log.info("주문 완료 처리: orderId={}", orderId);
99+
100+
Order order = orderService.getOrderById(orderId);
72101
order.completePayment();
73-
Order savedOrder = orderService.save(order);
102+
orderService.save(order);
103+
104+
log.info("주문 완료: orderId={}", orderId);
105+
}
106+
107+
@Transactional(readOnly = true)
108+
public List<OrderInfo> getMyOrders(String userId) {
109+
User user = userService.getUserByUserId(userId);
110+
List<Order> orders = orderService.getOrdersByUser(user);
111+
112+
return orders.stream()
113+
.map(order -> OrderInfo.from(order, 0L))
114+
.toList();
115+
}
116+
117+
@Transactional(readOnly = true)
118+
public OrderInfo getOrderDetail(Long orderId, String userId) {
119+
User user = userService.getUserByUserId(userId);
120+
Order order = orderService.getOrderByIdAndUser(orderId, user);
74121

75-
return OrderInfo.from(savedOrder);
122+
return OrderInfo.from(order, 0L);
76123
}
77124

78-
private void validateAndDecreaseStock(List<OrderPlaceCommand.OrderItemCommand> items, Map<Long, Product> productMap) {
125+
private void validateAndDecreaseStock(
126+
List<OrderPlaceCommand.OrderItemCommand> items,
127+
Map<Long, Product> productMap
128+
) {
79129
for (OrderPlaceCommand.OrderItemCommand item : items) {
80130
Product product = productMap.get(item.productId());
81131

82132
if (product == null) {
83-
throw new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.");
133+
throw new CoreException(ErrorType.NOT_FOUND,
134+
"상품을 찾을 수 없습니다: " + item.productId());
84135
}
85136

86137
if (!product.isStockAvailable(item.quantity())) {
@@ -92,19 +143,55 @@ private void validateAndDecreaseStock(List<OrderPlaceCommand.OrderItemCommand> i
92143
}
93144
}
94145

95-
public List<OrderInfo> getMyOrders(String userId) {
96-
User user = userService.getUserByUserId(userId);
97-
List<Order> orders = orderService.getOrdersByUser(user);
146+
/**
147+
* 쿠폰 적용
148+
*/
149+
private Long applyCouponIfExists(Long couponId, User user, Order order) {
150+
if (couponId == null) {
151+
return 0L;
152+
}
98153

99-
return orders.stream()
100-
.map(OrderInfo::from)
101-
.toList();
154+
Coupon coupon = couponService.getCouponWithOptimisticLock(couponId);
155+
couponService.validateCouponUsable(coupon, user);
156+
157+
Long discountAmount = coupon.calculateDiscount(order.getTotalAmountValue());
158+
coupon.use();
159+
couponService.save(coupon);
160+
161+
order.applyCoupon(couponId);
162+
163+
log.info("쿠폰 적용 완료: couponId={}, discountAmount={}", couponId, discountAmount);
164+
165+
return discountAmount;
102166
}
103167

104-
public OrderInfo getOrderDetail(Long orderId, String userId) {
105-
User user = userService.getUserByUserId(userId);
106-
Order order = orderService.getOrderByIdAndUser(orderId, user);
168+
/**
169+
* 쿠폰 복구
170+
*/
171+
private void restoreCoupon(Long couponId) {
172+
Coupon coupon = couponService.getCouponWithOptimisticLock(couponId);
173+
coupon.restore();
174+
couponService.save(coupon);
175+
log.debug("쿠폰 복구: couponId={}", couponId);
176+
}
107177

108-
return OrderInfo.from(order);
178+
/**
179+
* 상품 ID 추출 및 정렬
180+
*/
181+
private List<Long> extractAndSortProductIds(
182+
List<OrderPlaceCommand.OrderItemCommand> items
183+
) {
184+
return items.stream()
185+
.map(OrderPlaceCommand.OrderItemCommand::productId)
186+
.sorted()
187+
.toList();
188+
}
189+
190+
/**
191+
* 상품 리스트를 Map으로 변환
192+
*/
193+
private Map<Long, Product> toProductMap(List<Product> products) {
194+
return products.stream()
195+
.collect(Collectors.toMap(Product::getId, Function.identity()));
109196
}
110197
}

apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ public record OrderInfo(
1111
Long orderId,
1212
String userId,
1313
Long totalAmount,
14+
Long discountAmount,
1415
OrderStatus status,
1516
ZonedDateTime paidAt,
16-
List<OrderItemInfo> items
17+
List<OrderItemInfo> items,
18+
String paymentMethod,
19+
Long paymentId
1720
) {
1821
public record OrderItemInfo(
1922
Long productId,
@@ -33,16 +36,39 @@ public static OrderItemInfo from(OrderItem item) {
3336
}
3437
}
3538

36-
public static OrderInfo from(Order order) {
39+
public static OrderInfo from(Order order, Long discountAmount) {
3740
return new OrderInfo(
3841
order.getId(),
3942
order.getUser().getUserIdValue(),
4043
order.getTotalAmountValue(),
44+
discountAmount != null ? discountAmount : 0L,
4145
order.getStatus(),
4246
order.getPaidAt(),
4347
order.getOrderItems().stream()
4448
.map(OrderItemInfo::from)
45-
.toList()
49+
.toList(),
50+
null,
51+
null
4652
);
4753
}
54+
55+
public static OrderInfo from(Order order, Long discountAmount, String paymentMethod, Long paymentId) {
56+
return new OrderInfo(
57+
order.getId(),
58+
order.getUser().getUserIdValue(),
59+
order.getTotalAmountValue(),
60+
discountAmount != null ? discountAmount : 0L,
61+
order.getStatus(),
62+
order.getPaidAt(),
63+
order.getOrderItems().stream()
64+
.map(OrderItemInfo::from)
65+
.toList(),
66+
paymentMethod,
67+
paymentId
68+
);
69+
}
70+
71+
public Long getFinalAmount() {
72+
return totalAmount - discountAmount;
73+
}
4874
}

apps/commerce-api/src/main/java/com/loopers/application/order/OrderPlaceCommand.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,33 @@
55
public record OrderPlaceCommand(
66
String userId,
77
List<OrderItemCommand> items,
8-
Long couponId
8+
Long couponId,
9+
PaymentMethod paymentMethod,
10+
CardInfo cardInfo
911
) {
1012
public record OrderItemCommand(
1113
Long productId,
1214
Integer quantity
13-
) {}
15+
) {
16+
}
17+
18+
public record CardInfo(
19+
String cardType,
20+
String cardNo
21+
) {
22+
}
23+
24+
public enum PaymentMethod {
25+
POINT,
26+
PG_CARD
27+
}
28+
29+
public List<Long> getSortedProductIds() {
30+
return items.stream()
31+
.map(OrderItemCommand::productId)
32+
.sorted()
33+
.distinct()
34+
.toList();
35+
}
1436
}
37+

0 commit comments

Comments
 (0)