Skip to content

Commit 1cda434

Browse files
authored
Merge pull request #189 from rnqhstmd/round7
[volume-7] Decoupling with Event
2 parents 095f955 + df4f788 commit 1cda434

75 files changed

Lines changed: 3362 additions & 852 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package com.loopers.application.like;
22

33
import com.loopers.application.product.ProductCacheService;
4+
import com.loopers.domain.like.LikeEvent;
45
import com.loopers.domain.like.LikeService;
56
import com.loopers.domain.product.Product;
67
import com.loopers.domain.product.ProductService;
78
import com.loopers.domain.user.User;
89
import com.loopers.domain.user.UserService;
910
import lombok.RequiredArgsConstructor;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.context.ApplicationEventPublisher;
1013
import org.springframework.stereotype.Component;
1114
import org.springframework.transaction.annotation.Transactional;
1215

16+
@Slf4j
1317
@Component
1418
@RequiredArgsConstructor
1519
public class LikeFacade {
@@ -18,32 +22,46 @@ public class LikeFacade {
1822
private final UserService userService;
1923
private final ProductService productService;
2024
private final ProductCacheService productCacheService;
25+
private final ApplicationEventPublisher eventPublisher;
2126

2227
@Transactional
23-
public void addLike(String userId, Long productId) {
24-
User user = userService.getUserByUserId(userId);
28+
public void addLike(String loginId, Long productId) {
29+
User user = userService.getUserByLoginId(loginId);
2530
Product product = productService.getProductWithPessimisticLock(productId);
2631

2732
boolean isNewLike = likeService.addLike(user, product);
2833

2934
if (isNewLike) {
30-
productService.incrementLikeCount(productId);
31-
productCacheService.evictProductDetailCache(productId);
32-
productCacheService.evictProductListCachesByLikesSort();
35+
eventPublisher.publishEvent(LikeEvent.added(user.getId(), productId));
36+
log.info("좋아요 등록 이벤트 발행: userId={}, productId={}", user.getId(), productId);
3337
}
3438
}
3539

3640
@Transactional
37-
public void removeLike(String userId, Long productId) {
38-
User user = userService.getUserByUserId(userId);
41+
public void removeLike(String loginId, Long productId) {
42+
User user = userService.getUserByLoginId(loginId);
3943
Product product = productService.getProductWithPessimisticLock(productId);
4044

4145
boolean wasRemoved = likeService.removeLike(user, product);
4246

4347
if (wasRemoved) {
44-
productService.decrementLikeCount(productId);
45-
productCacheService.evictProductDetailCache(productId);
46-
productCacheService.evictProductListCachesByLikesSort();
48+
eventPublisher.publishEvent(LikeEvent.removed(user.getId(), productId));
49+
log.info("좋아요 취소 이벤트 발행: userId={}, productId={}", user.getId(), productId);
4750
}
4851
}
52+
53+
@Transactional
54+
public void incrementLikeCount(Long productId) {
55+
productService.incrementLikeCount(productId);
56+
}
57+
58+
@Transactional
59+
public void decrementLikeCount(Long productId) {
60+
productService.decrementLikeCount(productId);
61+
}
62+
63+
public void evictLikeRelatedCache(Long productId) {
64+
productCacheService.evictProductDetailCache(productId);
65+
productCacheService.evictProductListCachesByLikesSort();
66+
}
4967
}

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

Lines changed: 85 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import com.loopers.domain.coupon.Coupon;
44
import com.loopers.domain.coupon.CouponService;
55
import com.loopers.domain.order.Order;
6+
import com.loopers.domain.order.OrderCreatedEvent;
67
import com.loopers.domain.order.OrderService;
8+
import com.loopers.domain.order.OrderStatus;
79
import com.loopers.domain.product.Product;
810
import com.loopers.domain.product.ProductService;
911
import com.loopers.domain.user.User;
@@ -12,6 +14,7 @@
1214
import com.loopers.support.error.ErrorType;
1315
import lombok.RequiredArgsConstructor;
1416
import lombok.extern.slf4j.Slf4j;
17+
import org.springframework.context.ApplicationEventPublisher;
1518
import org.springframework.stereotype.Component;
1619
import org.springframework.transaction.annotation.Transactional;
1720

@@ -29,47 +32,83 @@ public class OrderFacade {
2932
private final ProductService productService;
3033
private final CouponService couponService;
3134
private final OrderService orderService;
35+
private final ApplicationEventPublisher eventPublisher;
3236

37+
/**
38+
* 주문 생성
39+
* - 핵심 트랜잭션: 재고 차감, 주문 저장
40+
* - 후속 처리(쿠폰 사용, 데이터 플랫폼 전송): 이벤트로 분리
41+
*/
3342
@Transactional
3443
public OrderInfo createOrder(OrderPlaceCommand command) {
35-
log.info("주문 생성 시작: userBusinessId={}, items={}",
36-
command.userId(), command.items().size());
37-
3844
// 1. 사용자 조회
39-
User user = userService.getUserByUserId(command.userId());
45+
User user = userService.getUserByLoginId(command.loginId());
4046

41-
// 2. 상품 조회 (데드락 방지를 위한 정렬 + 비관적 락)
47+
// 2. 상품 조회 (데드락 방지를 위해 ID 정렬 후 조회)
4248
List<Long> productIds = extractAndSortProductIds(command.items());
4349
List<Product> products = productService.getProductsByIdsWithPessimisticLock(productIds);
4450
Map<Long, Product> productMap = toProductMap(products);
4551

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

49-
// 4. 주문 생성 (도메인 서비스 위임)
55+
// 4. 쿠폰 검증 (사용은 이벤트로 분리)
56+
Long discountAmount = 0L;
57+
if (command.couponId() != null) {
58+
Coupon coupon = couponService.getCouponWithOptimisticLock(command.couponId());
59+
couponService.validateCouponUsable(coupon, user);
60+
discountAmount = coupon.calculateDiscount(calculateTotalAmount(command.items(), productMap));
61+
}
62+
63+
// 5. 주문 생성
5064
List<OrderService.OrderItemRequest> itemRequests = command.items().stream()
5165
.map(item -> OrderService.OrderItemRequest.of(item.productId(), item.quantity()))
5266
.toList();
5367
Order order = orderService.createOrderWithItems(user, itemRequests, productMap);
5468

55-
// 5. 쿠폰 적용 (선택)
56-
Long discountAmount = applyCouponIfExists(command.couponId(), user, order);
69+
// 6. 쿠폰 ID 저장 (실제 사용은 이벤트로)
70+
if (command.couponId() != null) {
71+
order.applyCoupon(command.couponId());
72+
}
5773

58-
// 6. 주문 저장
74+
// 7. 주문 저장
5975
Order savedOrder = orderService.save(order);
6076

61-
log.info("주문 생성 완료: orderId={}, totalAmount={}, discountAmount={}",
62-
savedOrder.getId(), savedOrder.getTotalAmountValue(), discountAmount);
77+
// 8. 이벤트 발행 → 쿠폰 사용, 데이터 플랫폼 전송, 유저 행동 로깅
78+
eventPublisher.publishEvent(OrderCreatedEvent.from(savedOrder, discountAmount));
79+
80+
log.info("주문 생성 완료: orderId={}, userId={}, couponId={}",
81+
savedOrder.getId(), user.getId(), command.couponId());
6382

6483
return OrderInfo.from(savedOrder, discountAmount);
6584
}
6685

86+
/**
87+
* 쿠폰 사용 처리 - OrderEventListener에서 호출
88+
*/
89+
@Transactional
90+
public void useCoupon(Long couponId) {
91+
log.info("쿠폰 사용 처리: couponId={}", couponId);
92+
Coupon coupon = couponService.getCouponWithOptimisticLock(couponId);
93+
coupon.use();
94+
couponService.save(coupon);
95+
}
96+
97+
/**
98+
* 주문 취소 - PaymentEventListener에서 호출 (보상 트랜잭션)
99+
*/
67100
@Transactional
68101
public void cancelOrder(Long orderId, Long couponId) {
69102
log.info("주문 취소 시작: orderId={}", orderId);
70103

71104
Order order = orderService.getOrderById(orderId);
72105

106+
// 이미 취소된 주문은 스킵 (멱등성)
107+
if (order.getStatus() == OrderStatus.CANCELLED) {
108+
log.info("이미 취소된 주문입니다: orderId={}", orderId);
109+
return;
110+
}
111+
73112
// 1. 재고 복구
74113
List<Long> productIds = order.getOrderItems().stream()
75114
.map(item -> item.getProductId())
@@ -93,20 +132,39 @@ public void cancelOrder(Long orderId, Long couponId) {
93132
log.info("주문 취소 완료: orderId={}", orderId);
94133
}
95134

135+
/**
136+
* 주문 완료 처리 - PaymentEventListener에서 호출
137+
*/
96138
@Transactional
97139
public void completeOrder(Long orderId) {
98140
log.info("주문 완료 처리: orderId={}", orderId);
99141

100142
Order order = orderService.getOrderById(orderId);
101-
order.completePayment();
143+
144+
// 이미 완료된 주문은 스킵 (멱등성)
145+
if (order.getStatus() == OrderStatus.COMPLETED) {
146+
log.info("이미 완료된 주문입니다: orderId={}", orderId);
147+
return;
148+
}
149+
150+
order.markAsCompleted();
102151
orderService.save(order);
103152

104153
log.info("주문 완료: orderId={}", orderId);
105154
}
106155

156+
/**
157+
* 주문에서 사용자 ID 조회 (데이터 플랫폼 전송용)
158+
*/
107159
@Transactional(readOnly = true)
108-
public List<OrderInfo> getMyOrders(String userId) {
109-
User user = userService.getUserByUserId(userId);
160+
public Long getUserIdByOrderId(Long orderId) {
161+
Order order = orderService.getOrderById(orderId);
162+
return order.getUser().getId();
163+
}
164+
165+
@Transactional(readOnly = true)
166+
public List<OrderInfo> getMyOrders(String loginId) {
167+
User user = userService.getUserByLoginId(loginId);
110168
List<Order> orders = orderService.getOrdersByUser(user);
111169

112170
return orders.stream()
@@ -115,13 +173,22 @@ public List<OrderInfo> getMyOrders(String userId) {
115173
}
116174

117175
@Transactional(readOnly = true)
118-
public OrderInfo getOrderDetail(Long orderId, String userId) {
119-
User user = userService.getUserByUserId(userId);
176+
public OrderInfo getOrderDetail(Long orderId, String loginId) {
177+
User user = userService.getUserByLoginId(loginId);
120178
Order order = orderService.getOrderByIdAndUser(orderId, user);
121179

122180
return OrderInfo.from(order, 0L);
123181
}
124182

183+
private Long calculateTotalAmount(List<OrderPlaceCommand.OrderItemCommand> items, Map<Long, Product> productMap) {
184+
return items.stream()
185+
.mapToLong(item -> {
186+
Product product = productMap.get(item.productId());
187+
return product.getPrice().getValue() * item.quantity();
188+
})
189+
.sum();
190+
}
191+
125192
private void validateAndDecreaseStock(
126193
List<OrderPlaceCommand.OrderItemCommand> items,
127194
Map<Long, Product> productMap
@@ -143,53 +210,20 @@ private void validateAndDecreaseStock(
143210
}
144211
}
145212

146-
/**
147-
* 쿠폰 적용
148-
*/
149-
private Long applyCouponIfExists(Long couponId, User user, Order order) {
150-
if (couponId == null) {
151-
return 0L;
152-
}
153-
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;
166-
}
167-
168-
/**
169-
* 쿠폰 복구
170-
*/
171213
private void restoreCoupon(Long couponId) {
172214
Coupon coupon = couponService.getCouponWithOptimisticLock(couponId);
173215
coupon.restore();
174216
couponService.save(coupon);
175217
log.debug("쿠폰 복구: couponId={}", couponId);
176218
}
177219

178-
/**
179-
* 상품 ID 추출 및 정렬
180-
*/
181-
private List<Long> extractAndSortProductIds(
182-
List<OrderPlaceCommand.OrderItemCommand> items
183-
) {
220+
private List<Long> extractAndSortProductIds(List<OrderPlaceCommand.OrderItemCommand> items) {
184221
return items.stream()
185222
.map(OrderPlaceCommand.OrderItemCommand::productId)
186223
.sorted()
187224
.toList();
188225
}
189226

190-
/**
191-
* 상품 리스트를 Map으로 변환
192-
*/
193227
private Map<Long, Product> toProductMap(List<Product> products) {
194228
return products.stream()
195229
.collect(Collectors.toMap(Product::getId, Function.identity()));

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public static OrderItemInfo from(OrderItem item) {
3939
public static OrderInfo from(Order order, Long discountAmount) {
4040
return new OrderInfo(
4141
order.getId(),
42-
order.getUser().getUserIdValue(),
42+
order.getUser().getLoginIdValue(),
4343
order.getTotalAmountValue(),
4444
discountAmount != null ? discountAmount : 0L,
4545
order.getStatus(),
@@ -55,7 +55,7 @@ public static OrderInfo from(Order order, Long discountAmount) {
5555
public static OrderInfo from(Order order, Long discountAmount, String paymentMethod, Long paymentId) {
5656
return new OrderInfo(
5757
order.getId(),
58-
order.getUser().getUserIdValue(),
58+
order.getUser().getLoginIdValue(),
5959
order.getTotalAmountValue(),
6060
discountAmount != null ? discountAmount : 0L,
6161
order.getStatus(),

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import java.util.List;
44

55
public record OrderPlaceCommand(
6-
String userId,
6+
String loginId,
77
List<OrderItemCommand> items,
88
Long couponId,
99
PaymentMethod paymentMethod,

apps/commerce-api/src/main/java/com/loopers/application/payment/CardPaymentStrategy.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import com.loopers.domain.order.Order;
44
import com.loopers.domain.order.OrderService;
55
import com.loopers.domain.payment.Payment;
6-
import com.loopers.domain.payment.PaymentFailedEvent;
6+
import com.loopers.domain.payment.event.PaymentFailedEvent;
77
import com.loopers.domain.payment.PaymentService;
88
import com.loopers.domain.payment.PaymentStrategy;
99
import com.loopers.infrastructure.pg.PgService;

0 commit comments

Comments
 (0)