Skip to content

Commit 2a38caf

Browse files
committed
feature: Outbox 기반 주문 및 판매 이벤트 처리, Kafka 연동 구현
- `OrderEventOutboxHandler` 추가로 주문 생성 이벤트 처리 및 Kafka 발행 로직 구현. - `OrderSalesAggregateListener`를 통해 판매 집계 Kafka 이벤트 발행 및 비동기 처리 지원. - `OrderCreatedEvent` 확장: 고유 `eventId` 추가 및 주문 품목 전달 정보 구조 변경. - 신규 `SalesCountEvent` 추가로 Kafka 기반 판매 메트릭 관리 가능. - `OrderFacade`, `PointPaymentEventListener`, `PgPaymentEventListener` 등 관련 클래스 수정 및 패키지 정리.
1 parent 7af3ae8 commit 2a38caf

7 files changed

Lines changed: 133 additions & 7 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.loopers.application.order;
22

3+
import com.loopers.application.order.event.OrderCreatedEvent;
34
import com.loopers.domain.coupon.CouponService;
45
import com.loopers.domain.order.Order;
56
import com.loopers.domain.order.OrderCommand.Item;
@@ -62,9 +63,10 @@ public OrderInfo placeOrder(PlaceOrder command) {
6263

6364
productService.deductStock(products, orderItems);
6465

65-
OrderCreatedEvent orderEvent = new OrderCreatedEvent(
66+
OrderCreatedEvent orderEvent = OrderCreatedEvent.of(
6667
order.getId(),
6768
user,
69+
orderItems,
6870
finalPaymentAmount,
6971
command.paymentType(),
7072
command.cardType(),
Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,52 @@
11
package com.loopers.application.order.event;
22

3+
import com.loopers.domain.order.OrderItem;
34
import com.loopers.domain.payment.PaymentType;
45
import com.loopers.domain.user.User;
6+
import java.util.List;
7+
import java.util.UUID;
58

69
public record OrderCreatedEvent(
10+
String eventId,
711
Long orderId,
8-
User user,
12+
Long userId,
13+
List<OrderItemInfo> items,
914
long finalAmount,
1015
PaymentType paymentType,
1116
String cardType,
1217
String cardNo,
1318
Long couponId
14-
) {}
19+
) {
20+
21+
public record OrderItemInfo(Long productId, int quantity) {
22+
23+
}
24+
25+
public static OrderCreatedEvent of(
26+
Long orderId,
27+
User user,
28+
List<OrderItem> orderItems,
29+
long finalAmount,
30+
PaymentType paymentType,
31+
String cardType,
32+
String cardNo,
33+
Long couponId
34+
) {
35+
36+
List<OrderItemInfo> itemInfos = orderItems.stream()
37+
.map(item -> new OrderItemInfo(item.getProductId(), item.getQuantity()))
38+
.toList();
39+
40+
return new OrderCreatedEvent(
41+
UUID.randomUUID().toString(),
42+
orderId,
43+
user.getId(),
44+
itemInfos,
45+
finalAmount,
46+
paymentType,
47+
cardType,
48+
cardNo,
49+
couponId
50+
);
51+
}
52+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.loopers.application.order.event;
2+
3+
import com.loopers.domain.event.OutboxService;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.kafka.core.KafkaTemplate;
6+
import org.springframework.stereotype.Component;
7+
import org.springframework.transaction.event.TransactionPhase;
8+
import org.springframework.transaction.event.TransactionalEventListener;
9+
10+
@Component
11+
@RequiredArgsConstructor
12+
public class OrderEventOutboxHandler {
13+
private final KafkaTemplate<Object, Object> kafkaTemplate;
14+
private final OutboxService outboxService;
15+
16+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
17+
public void handle(OrderCreatedEvent event) {
18+
kafkaTemplate.send("order-events", String.valueOf(event.orderId()), event)
19+
.whenComplete((result, ex) -> {
20+
if (ex == null) {
21+
outboxService.markPublished(event.eventId());
22+
}
23+
});
24+
}
25+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.loopers.application.order.event;
2+
3+
import com.loopers.domain.event.OutboxService;
4+
import com.loopers.event.SalesCountEvent;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.context.ApplicationEventPublisher;
7+
import org.springframework.scheduling.annotation.Async;
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.transaction.event.TransactionPhase;
10+
import org.springframework.transaction.event.TransactionalEventListener;
11+
12+
@Component
13+
@RequiredArgsConstructor
14+
public class OrderSalesAggregateListener {
15+
16+
private final OutboxService outboxService;
17+
private final ApplicationEventPublisher eventPublisher;
18+
19+
@Async
20+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
21+
public void handleOrderCreated(OrderCreatedEvent event) {
22+
23+
event.items().forEach(item -> {
24+
25+
SalesCountEvent kafkaEvent = SalesCountEvent.of(
26+
item.productId(),
27+
item.quantity()
28+
);
29+
30+
outboxService.saveEvent("SALES_METRICS", String.valueOf(item.productId()), kafkaEvent);
31+
eventPublisher.publishEvent(kafkaEvent);
32+
});
33+
}
34+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.loopers.application.payment;
22

3-
import com.loopers.application.order.OrderCreatedEvent;
3+
import com.loopers.application.order.event.OrderCreatedEvent;
44
import com.loopers.application.payment.PaymentEvent.PaymentRequestFailedEvent;
55
import com.loopers.application.payment.PaymentEvent.PaymentRequestedEvent;
66
import com.loopers.domain.payment.Payment;
@@ -36,7 +36,7 @@ public void handleOrderCreatedEvent(OrderCreatedEvent event) {
3636
return;
3737
}
3838

39-
User user = userService.findById(event.user().getId())
39+
User user = userService.findById(event.userId())
4040
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "유저 정보를 찾을 수 없습니다."));
4141

4242
PaymentProcessor processor = paymentProcessors.stream()

apps/commerce-api/src/main/java/com/loopers/application/point/PointPaymentEventListener.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.loopers.application.point;
22

3-
import com.loopers.application.order.OrderCreatedEvent;
3+
import com.loopers.application.order.event.OrderCreatedEvent;
44
import com.loopers.application.payment.PaymentEvent.PaymentCompletedEvent;
55
import com.loopers.application.payment.PaymentEvent.PaymentRequestFailedEvent;
66
import com.loopers.domain.order.OrderService;
77
import com.loopers.domain.order.OrderStatus;
88
import com.loopers.domain.payment.Payment;
99
import com.loopers.domain.payment.PaymentProcessor;
1010
import com.loopers.domain.payment.PaymentType;
11+
import com.loopers.domain.user.User;
12+
import com.loopers.domain.user.UserService;
1113
import com.loopers.support.error.CoreException;
1214
import com.loopers.support.error.ErrorType;
1315
import java.util.List;
@@ -23,8 +25,10 @@
2325
@Component
2426
@RequiredArgsConstructor
2527
public class PointPaymentEventListener {
28+
2629
private final List<PaymentProcessor> paymentProcessors;
2730
private final OrderService orderService;
31+
private final UserService userService;
2832
private final ApplicationEventPublisher eventPublisher;
2933

3034
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@@ -40,10 +44,13 @@ public void handleOrderCreatedEvent(OrderCreatedEvent event) {
4044
.findFirst()
4145
.orElseThrow(() -> new CoreException(ErrorType.BAD_REQUEST, "포인트 결제 프로세서를 찾을 수 없습니다."));
4246

47+
User user = userService.findById(event.userId())
48+
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "유저를 찾을 수 없습니다."));
49+
4350
try {
4451
Payment payment = processor.process(
4552
event.orderId(),
46-
event.user(),
53+
user,
4754
event.finalAmount(),
4855
Map.of()
4956
);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.loopers.event;
2+
3+
import java.util.UUID;
4+
5+
public record SalesCountEvent(
6+
String eventId,
7+
Long productId,
8+
int quantity,
9+
long timestamp
10+
) {
11+
12+
public static SalesCountEvent of(Long productId, int quantity) {
13+
return new SalesCountEvent(
14+
UUID.randomUUID().toString(),
15+
productId,
16+
quantity,
17+
System.currentTimeMillis()
18+
);
19+
}
20+
}

0 commit comments

Comments
 (0)