Skip to content

Commit 5e43c2a

Browse files
authored
Merge pull request #209 from Kimjipang/main
[volume - 8] Decoupling with Kafka
2 parents 2ad99ed + 73c0244 commit 5e43c2a

35 files changed

Lines changed: 561 additions & 60 deletions

apps/commerce-api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ dependencies {
22
// add-ons
33
implementation(project(":modules:jpa"))
44
implementation(project(":modules:redis"))
5+
implementation(project(":modules:kafka"))
56
implementation(project(":supports:jackson"))
67
implementation(project(":supports:logging"))
78
implementation(project(":supports:monitoring"))

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
77
import org.springframework.cache.annotation.EnableCaching;
88
import org.springframework.cloud.openfeign.EnableFeignClients;
9+
import org.springframework.scheduling.annotation.EnableScheduling;
910

1011
import java.util.TimeZone;
1112

1213
@ConfigurationPropertiesScan
1314
@SpringBootApplication
1415
@EnableCaching
1516
@EnableFeignClients
17+
@EnableScheduling
1618
public class CommerceApiApplication {
1719

1820
@PostConstruct

apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java

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

3-
import com.loopers.application.product.UserActionEvent;
4-
import com.loopers.domain.actionlog.ActionType;
53
import com.loopers.domain.like.Like;
64
import com.loopers.domain.like.LikeRepository;
5+
import com.loopers.domain.outbox.AggregateType;
6+
import com.loopers.domain.outbox.OutboxEvent;
7+
import com.loopers.domain.outbox.OutboxRepository;
8+
import com.loopers.domain.outbox.OutboxType;
79
import com.loopers.domain.product.ProductRepository;
810
import com.loopers.domain.user.UserRepository;
911
import com.loopers.interfaces.api.like.LikeV1Dto;
1012
import com.loopers.support.error.CoreException;
1113
import com.loopers.support.error.ErrorType;
1214
import lombok.RequiredArgsConstructor;
13-
import org.springframework.context.ApplicationEventPublisher;
1415
import org.springframework.stereotype.Component;
1516
import org.springframework.transaction.annotation.Transactional;
1617

@@ -20,7 +21,7 @@ public class LikeFacade {
2021
private final LikeRepository likeRepository;
2122
private final UserRepository userRepository;
2223
private final ProductRepository productRepository;
23-
private final ApplicationEventPublisher publisher;
24+
private final OutboxRepository outBoxRepository;
2425

2526
@Transactional
2627
public LikeInfo doLike(LikeV1Dto.LikeRequest request) {
@@ -46,8 +47,13 @@ public LikeInfo doLike(LikeV1Dto.LikeRequest request) {
4647
Like newLike = request.toEntity();
4748
likeRepository.save(newLike);
4849

49-
publisher.publishEvent(new LikeCreateEvent(userId, productId));
50-
publisher.publishEvent(new UserActionEvent(userId, productId, ActionType.DO_LIKE));
50+
OutboxEvent outBoxEvent = OutboxEvent.of(
51+
AggregateType.PRODUCT,
52+
productId,
53+
OutboxType.PRODUCT_LIKED
54+
);
55+
56+
outBoxRepository.save(outBoxEvent);
5157

5258
return LikeInfo.from(newLike);
5359
});
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
package com.loopers.application.order;
22

3-
public record OrderCreatedEvent(Long couponId) {
3+
import com.loopers.interfaces.api.payment.PaymentV1Dto;
4+
5+
public record OrderCreatedEvent(
6+
Long couponId,
7+
String orderId,
8+
PaymentV1Dto.CardTypeDto cardType,
9+
String cardNo,
10+
Long amount,
11+
String callbackUrl
12+
) {
413
}

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

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
package com.loopers.application.order;
22

33
import com.loopers.application.orderitem.OrderItemInfo;
4-
import com.loopers.application.product.UserActionEvent;
54
import com.loopers.domain.coupon.Coupon;
65
import com.loopers.domain.coupon.CouponRepository;
76
import com.loopers.domain.order.Order;
87
import com.loopers.domain.order.OrderRepository;
98
import com.loopers.domain.orderitem.OrderItem;
109
import com.loopers.domain.orderitem.OrderItemRepository;
10+
import com.loopers.domain.outbox.AggregateType;
11+
import com.loopers.domain.outbox.OutboxEvent;
12+
import com.loopers.domain.outbox.OutboxRepository;
13+
import com.loopers.domain.outbox.OutboxType;
1114
import com.loopers.domain.product.Product;
1215
import com.loopers.domain.product.ProductRepository;
1316
import com.loopers.domain.user.UserRepository;
1417
import com.loopers.interfaces.api.order.OrderV1Dto;
1518
import com.loopers.support.error.CoreException;
1619
import com.loopers.support.error.ErrorType;
1720
import lombok.RequiredArgsConstructor;
18-
import org.springframework.context.ApplicationEventPublisher;
1921
import org.springframework.stereotype.Component;
2022
import org.springframework.transaction.annotation.Transactional;
2123

@@ -30,7 +32,7 @@ public class OrderFacade {
3032
private final UserRepository userRepository;
3133
private final ProductRepository productRepository;
3234
private final CouponRepository couponRepository;
33-
private final ApplicationEventPublisher publisher;
35+
private final OutboxRepository outBoxRepository;
3436

3537
@Transactional
3638
public OrderResultInfo createOrder(OrderV1Dto.OrderRequest request) {
@@ -49,31 +51,31 @@ public OrderResultInfo createOrder(OrderV1Dto.OrderRequest request) {
4951
List<OrderV1Dto.OrderItemRequest> orderItemRequests = request.orderItems();
5052

5153
List<OrderItem> orderItems = orderItemRequests.stream()
52-
.map(item -> {
53-
// 상품 검증
54-
Long productId = item.productId();
55-
Product product = productRepository.findById(productId).orElseThrow(
56-
() -> new CoreException(ErrorType.NOT_FOUND, "존재하는 상품이 아닙니다.")
57-
);
58-
59-
if (product.getStock() < item.quantity()) {
60-
throw new CoreException(ErrorType.BAD_REQUEST, product.getName() + " 상품의 재고가 부족합니다.");
61-
}
62-
63-
product.decreaseStock(item.quantity());
64-
65-
OrderItem orderItem = item.toEntity(
66-
null,
67-
product.getPrice().multiply(BigDecimal.valueOf(item.quantity()))
68-
);
69-
return orderItem;
70-
71-
})
72-
.toList();
54+
.map(item -> {
55+
// 상품 검증
56+
Long productId = item.productId();
57+
Product product = productRepository.findById(productId).orElseThrow(
58+
() -> new CoreException(ErrorType.NOT_FOUND, "존재하는 상품이 아닙니다.")
59+
);
60+
61+
if (product.getStock() < item.quantity()) {
62+
throw new CoreException(ErrorType.BAD_REQUEST, product.getName() + " 상품의 재고가 부족합니다.");
63+
}
64+
65+
product.decreaseStock(item.quantity());
66+
67+
OrderItem orderItem = item.toEntity(
68+
null,
69+
product.getPrice().multiply(BigDecimal.valueOf(item.quantity()))
70+
);
71+
return orderItem;
72+
73+
})
74+
.toList();
7375

7476
BigDecimal totalPrice = orderItems.stream()
75-
.map(OrderItem::getOrderPrice)
76-
.reduce(BigDecimal.ZERO, BigDecimal::add);
77+
.map(OrderItem::getOrderPrice)
78+
.reduce(BigDecimal.ZERO, BigDecimal::add);
7779

7880
Long couponId = request.couponId();
7981

@@ -91,15 +93,25 @@ public OrderResultInfo createOrder(OrderV1Dto.OrderRequest request) {
9193
.multiply(BigDecimal.valueOf(100 - rate))
9294
.divide(BigDecimal.valueOf(100));
9395

94-
publisher.publishEvent(new OrderCreatedEvent(couponId));
96+
coupon.useCoupon();
9597

9698
Order order = request.toEntity(totalPrice);
9799
Order saved = orderRepository.save(order);
98100

99101
orderItems.forEach(item -> item.assignOrderId(saved.getId()));
100-
orderItemRepository.saveAll(orderItems);
102+
List<OrderItem> savedOrderItems = orderItemRepository.saveAll(orderItems);
103+
104+
savedOrderItems.forEach(orderItem -> {
105+
OutboxEvent outboxEvent = OutboxEvent.of(
106+
AggregateType.PRODUCT,
107+
orderItem.getProductId(),
108+
OutboxType.PRODUCT_SALES
109+
);
110+
111+
outBoxRepository.save(outboxEvent);
112+
});
101113

102-
List<OrderItemInfo> orderItemInfos = orderItems.stream()
114+
List<OrderItemInfo> orderItemInfos = savedOrderItems.stream()
103115
.map(orderItem -> OrderItemInfo.from(orderItem, orderItem.getOrderPrice()))
104116
.toList();
105117

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

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

3+
import com.loopers.domain.payment.Payment;
4+
import com.loopers.domain.payment.PaymentStatus;
35
import com.loopers.infrastructure.payment.client.PgClient;
46
import com.loopers.interfaces.api.ApiResponse;
57
import com.loopers.interfaces.api.payment.PaymentV1Dto;
@@ -11,17 +13,32 @@
1113
@RequiredArgsConstructor
1214
public class PaymentFacade {
1315
private final PgClient pgClient;
16+
private final PaymentRepository paymentRepository;
1417

1518
@CircuitBreaker(name = "pgCircuit", fallbackMethod = "fallback")
16-
public PaymentV1Dto.TransactionResponse requestPayment(String userId, PaymentV1Dto.PaymentRequest request) {
17-
ApiResponse<PaymentV1Dto.TransactionResponse> result = pgClient.requestPayment(userId, request);
19+
public PaymentV1Dto.TransactionResponse requestPayment(String userId, PaymentV1Dto.PgPaymentRequest pgPaymentRequest) {
20+
ApiResponse<PaymentV1Dto.TransactionResponse> result = pgClient.requestPayment(userId, pgPaymentRequest);
1821

1922
PaymentV1Dto.TransactionResponse response = result.data();
2023

24+
PaymentV1Dto.PaymentRequest paymentRequest = new PaymentV1Dto.PaymentRequest(
25+
userId,
26+
pgPaymentRequest.orderId(),
27+
response.transactionKey()
28+
);
29+
30+
Payment payment = Payment.create(
31+
paymentRequest.userId(),
32+
paymentRequest.orderNo(),
33+
paymentRequest.transactionKey(),
34+
PaymentStatus.PENDING
35+
);
36+
paymentRepository.save(payment);
37+
2138
return response;
2239
}
2340

24-
public PaymentV1Dto.TransactionResponse fallback(String userId, PaymentV1Dto.PaymentRequest request, Throwable throwable) {
41+
public PaymentV1Dto.TransactionResponse fallback(String userId, PaymentV1Dto.PgPaymentRequest request, Throwable throwable) {
2542
return new PaymentV1Dto.TransactionResponse(
2643
null,
2744
PaymentV1Dto.TransactionStatusResponse.FAILED,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.loopers.application.payment;
2+
3+
import com.loopers.domain.payment.Payment;
4+
5+
public interface PaymentRepository {
6+
Payment save(Payment payment);
7+
}

apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.loopers.application.product;
22

3-
import com.loopers.domain.actionlog.ActionType;
43
import com.loopers.domain.brand.BrandRepository;
4+
import com.loopers.domain.outbox.AggregateType;
5+
import com.loopers.domain.outbox.OutboxEvent;
6+
import com.loopers.domain.outbox.OutboxType;
7+
import com.loopers.domain.outbox.OutboxRepository;
58
import com.loopers.domain.product.Product;
69
import com.loopers.domain.product.ProductRepository;
710
import com.loopers.interfaces.api.product.ProductV1Dto;
@@ -10,7 +13,6 @@
1013
import lombok.RequiredArgsConstructor;
1114
import org.springframework.cache.annotation.CacheEvict;
1215
import org.springframework.cache.annotation.Cacheable;
13-
import org.springframework.context.ApplicationEventPublisher;
1416
import org.springframework.stereotype.Component;
1517
import org.springframework.transaction.annotation.Transactional;
1618

@@ -22,8 +24,7 @@
2224
public class ProductFacade {
2325
private final ProductRepository productRepository;
2426
private final BrandRepository brandRepository;
25-
private final ApplicationEventPublisher publisher;
26-
27+
private final OutboxRepository outBoxRepository;
2728

2829
@Transactional
2930
public ProductInfo registerProduct(ProductV1Dto.ProductRequest request) {
@@ -49,15 +50,20 @@ public List<ProductInfo> findAllProducts() {
4950
.toList();
5051
}
5152

52-
@Transactional(readOnly = true)
53+
@Transactional
5354
@Cacheable(value = "product", key = "#id")
5455
public ProductInfo findProductById(Long id) {
5556
Product product = productRepository.findById(id).orElseThrow(
5657
() -> new CoreException(ErrorType.NOT_FOUND, "찾고자 하는 상품이 존재하지 않습니다.")
5758
);
5859

59-
// 유저 ID는 임시로 하드 코딩했습니다. 추후 인증/인가 기능이 추가되면 수정할 예정입니다.
60-
publisher.publishEvent(new UserActionEvent(1L, product.getId(), ActionType.PRODUCT_LOOKED_UP));
60+
OutboxEvent outBoxEvent = OutboxEvent.of(
61+
AggregateType.PRODUCT,
62+
product.getId(),
63+
OutboxType.PRODUCT_VIEWED
64+
);
65+
66+
outBoxRepository.save(outBoxEvent);
6167

6268
return ProductInfo.from(product);
6369
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.loopers.domain.outbox;
2+
3+
public enum AggregateType {
4+
PRODUCT,
5+
ORDER,
6+
USER
7+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.loopers.domain.outbox;
2+
3+
import com.loopers.domain.BaseEntity;
4+
import jakarta.persistence.Column;
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.EnumType;
7+
import jakarta.persistence.Enumerated;
8+
import jakarta.persistence.Table;
9+
import lombok.AccessLevel;
10+
import lombok.Getter;
11+
import lombok.NoArgsConstructor;
12+
13+
@Entity
14+
@Table(name = "outbox")
15+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
16+
@Getter
17+
public class OutboxEvent extends BaseEntity {
18+
@Enumerated(EnumType.STRING)
19+
@Column(name = "aggregate_type", nullable = false)
20+
private AggregateType aggregateType;
21+
22+
@Column(name = "aggregate_id", nullable = false)
23+
private Long aggregateId;
24+
25+
@Enumerated(EnumType.STRING)
26+
@Column(name = "event_type", nullable = false)
27+
private OutboxType eventType;
28+
29+
@Enumerated(EnumType.STRING)
30+
@Column(name = "status", nullable = false)
31+
private OutboxStatus status;
32+
33+
public OutboxEvent(AggregateType aggregateType, Long aggregateId, OutboxType eventType, OutboxStatus status) {
34+
this.aggregateType = aggregateType;
35+
this.aggregateId = aggregateId;
36+
this.eventType = eventType;
37+
this.status = status;
38+
}
39+
40+
public static OutboxEvent of(AggregateType aggregateType, Long aggregateId, OutboxType eventType) {
41+
return new OutboxEvent(aggregateType, aggregateId, eventType, OutboxStatus.PENDING);
42+
}
43+
44+
public void markAsProcessed() {
45+
this.status = OutboxStatus.PROCESSED;
46+
}
47+
48+
}

0 commit comments

Comments
 (0)