Skip to content

Commit ceb53ce

Browse files
committed
refactor: SalesCountEvent 제거 및 캐시 삭제 작업을 위한ProductStockEvent 기반 재고 관리 로직 개선
- SalesCountEvent를 제거하고 관련 로직을 ProductStockEvent로 전면 대체 - MetricsEventConsumer에서 ProductStockEvent를 처리하도록 수정 - ProductMetricsService를 확장하여 재고 기반 지표 관리 및 재고 소진 시 Redis 캐시 삭제 로직 추가 - ProductRepository에 findStockById 메서드, ProductService에 getStock 메서드 추가 - Redis 캐시에서 페이징 응답을 감싸기 위한 PageWrapper 도입 - 패턴 기반 캐시 삭제를 지원하도록 RedisCacheHandler 이동 및 기능 확장
1 parent 432cd74 commit ceb53ce

10 files changed

Lines changed: 105 additions & 34 deletions

File tree

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package com.loopers.application.order.event;
22

33
import com.loopers.domain.event.OutboxService;
4-
import com.loopers.event.SalesCountEvent;
4+
import com.loopers.domain.product.ProductService;
5+
import com.loopers.event.ProductStockEvent;
56
import lombok.RequiredArgsConstructor;
67
import org.springframework.context.ApplicationEventPublisher;
78
import org.springframework.scheduling.annotation.Async;
@@ -13,6 +14,7 @@
1314
@RequiredArgsConstructor
1415
public class OrderSalesAggregateListener {
1516

17+
private final ProductService productService;
1618
private final OutboxService outboxService;
1719
private final ApplicationEventPublisher eventPublisher;
1820

@@ -22,12 +24,14 @@ public void handleOrderCreated(OrderCreatedEvent event) {
2224

2325
event.items().forEach(item -> {
2426

25-
SalesCountEvent kafkaEvent = SalesCountEvent.of(
27+
int currentStock = productService.getStock(item.productId());
28+
ProductStockEvent kafkaEvent = ProductStockEvent.of(
2629
item.productId(),
27-
item.quantity()
30+
item.quantity(),
31+
currentStock
2832
);
2933

30-
outboxService.saveEvent("SALES_METRICS", String.valueOf(item.productId()), kafkaEvent);
34+
outboxService.saveEvent("STOCKS_METRICS", String.valueOf(item.productId()), kafkaEvent);
3135
eventPublisher.publishEvent(kafkaEvent);
3236
});
3337
}

apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ public interface ProductRepository {
1313
Optional<Product> findById(Long id);
1414

1515
Page<Product> findByBrandId(Long brandId, Pageable pageable);
16+
17+
int findStockById(Long id);
1618
}

apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.loopers.domain.product;
22

33
import com.loopers.domain.order.OrderItem;
4-
import com.loopers.support.cache.RedisCacheHandler;
4+
import com.loopers.core.cache.RedisCacheHandler;
55
import com.loopers.support.error.CoreException;
66
import com.loopers.support.error.ErrorType;
77
import java.time.Duration;
@@ -104,4 +104,8 @@ private String makeCacheKey(String prefix, Pageable pageable) {
104104
}
105105
return sb.toString();
106106
}
107+
108+
public int getStock(Long id) {
109+
return productRepository.findStockById(id);
110+
}
107111
}

apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package com.loopers.domain.metrics;
22

3+
import com.loopers.core.cache.RedisCacheHandler;
34
import com.loopers.domain.event.EventHandled;
45
import com.loopers.event.LikeCountEvent;
6+
import com.loopers.event.ProductStockEvent;
57
import com.loopers.event.ProductViewEvent;
6-
import com.loopers.event.SalesCountEvent;
78
import com.loopers.infrastructure.EventHandledRepository;
89
import com.loopers.infrastructure.ProductMetricsRepository;
910
import java.time.LocalDateTime;
@@ -17,6 +18,7 @@ public class ProductMetricsService {
1718

1819
private final ProductMetricsRepository metricsRepository;
1920
private final EventHandledRepository eventHandledRepository;
21+
private final RedisCacheHandler redisCacheHandler;
2022

2123
@Transactional
2224
public void processLikeCountEvent(LikeCountEvent event) {
@@ -41,12 +43,18 @@ public void processProductViewEvent(ProductViewEvent event) {
4143
}
4244

4345
@Transactional
44-
public void processSalesCountEvent(SalesCountEvent event) {
46+
public void processSalesCountEvent(ProductStockEvent event) {
4547
if (isAlreadyHandled(event.eventId())) return;
4648

4749
ProductMetrics metrics = getOrCreateMetrics(event.productId());
4850

49-
metrics.addSalesCount(event.quantity());
51+
metrics.addSalesCount(event.sellQuantity());
52+
53+
if (event.currentStock() <= 0) {
54+
redisCacheHandler.delete("product:detail:" + event.productId());
55+
redisCacheHandler.deleteByPattern("product:list");
56+
57+
}
5058

5159
completeProcess(event.eventId(), metrics);
5260
}

apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/MetricsEventConsumer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import com.loopers.domain.metrics.ProductMetricsService;
44
import com.loopers.event.LikeCountEvent;
5+
import com.loopers.event.ProductStockEvent;
56
import com.loopers.event.ProductViewEvent;
6-
import com.loopers.event.SalesCountEvent;
77
import lombok.RequiredArgsConstructor;
88
import lombok.extern.slf4j.Slf4j;
99
import org.apache.kafka.clients.consumer.ConsumerRecord;
@@ -48,7 +48,7 @@ public void consumeProductView(ProductViewEvent event, Acknowledgment ack) {
4848
topics = "catalog-events",
4949
groupId = "metrics-group"
5050
)
51-
public void consumeSalesCount(SalesCountEvent event, Acknowledgment ack) {
51+
public void consumeSalesCount(ProductStockEvent event, Acknowledgment ack) {
5252
try {
5353
metricsService.processSalesCountEvent(event);
5454
ack.acknowledge();

apps/commerce-streamer/src/test/java/com/loopers/domain/metrics/IdempotencyIntegrationTest.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44

5-
import com.loopers.event.SalesCountEvent;
5+
import com.loopers.config.redis.RedisConfig;
6+
import com.loopers.core.cache.RedisCacheHandler;
7+
import com.loopers.event.ProductStockEvent;
68
import com.loopers.infrastructure.ProductMetricsRepository;
79
import com.loopers.utils.DatabaseCleanUp;
810
import org.junit.jupiter.api.AfterEach;
911
import org.junit.jupiter.api.DisplayName;
1012
import org.junit.jupiter.api.Test;
1113
import org.springframework.beans.factory.annotation.Autowired;
1214
import org.springframework.boot.test.context.SpringBootTest;
15+
import org.springframework.context.annotation.Import;
16+
import org.springframework.test.context.bean.override.mockito.MockitoBean;
1317

1418
@SpringBootTest
19+
@Import(RedisConfig.class)
1520
class IdempotencyIntegrationTest {
1621

1722
@Autowired
@@ -20,6 +25,9 @@ class IdempotencyIntegrationTest {
2025
@Autowired
2126
private ProductMetricsRepository metricsRepository;
2227

28+
@MockitoBean
29+
private RedisCacheHandler redisCacheHandler;
30+
2331
@Autowired
2432
private DatabaseCleanUp databaseCleanUp;
2533

@@ -35,8 +43,9 @@ void shouldHandleDuplicateEventIdempotently() {
3543
// Given: 동일한 ID를 가진 이벤트 준비
3644
Long productId = 99L;
3745
int salesQuantity = 2;
46+
int currentStock = 10;
3847

39-
SalesCountEvent firstEvent = SalesCountEvent.of(productId, salesQuantity);
48+
ProductStockEvent firstEvent = ProductStockEvent.of(productId, salesQuantity, currentStock);
4049

4150
// When: 첫 번째 전송
4251
metricsService.processSalesCountEvent(firstEvent);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.loopers.event;
2+
3+
import java.util.UUID;
4+
5+
public record ProductStockEvent(
6+
String eventId,
7+
Long productId,
8+
int sellQuantity,
9+
int currentStock,
10+
long timestamp
11+
) {
12+
public static ProductStockEvent of(Long productId, int sellQuantity, int currentStock) {
13+
return new ProductStockEvent(
14+
UUID.randomUUID().toString(),
15+
productId,
16+
sellQuantity,
17+
currentStock,
18+
System.currentTimeMillis()
19+
);
20+
}
21+
}

modules/kafka/src/main/java/com/loopers/event/SalesCountEvent.java

Lines changed: 0 additions & 20 deletions
This file was deleted.

apps/commerce-api/src/main/java/com/loopers/support/cache/RedisCacheHandler.java renamed to modules/redis/src/main/java/com/loopers/core/cache/RedisCacheHandler.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
package com.loopers.support.cache;
1+
package com.loopers.core.cache;
22

3-
import com.loopers.support.page.PageWrapper;
3+
import com.loopers.core.cache.page.PageWrapper;
44
import java.time.Duration;
5+
import java.util.Set;
56
import java.util.function.Supplier;
67
import lombok.RequiredArgsConstructor;
78
import org.springframework.data.domain.Page;
@@ -49,4 +50,15 @@ public <T> T getOrLoad(String key, Duration ttl, Class<T> type, Supplier<T> dbFe
4950

5051
return result;
5152
}
53+
54+
public void delete(String key) {
55+
redisTemplate.delete(key);
56+
}
57+
58+
public void deleteByPattern(String pattern) {
59+
Set<String> keys = redisTemplate.keys(pattern + "*");
60+
if (keys != null && !keys.isEmpty()) {
61+
redisTemplate.delete(keys);
62+
}
63+
}
5264
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.loopers.core.cache.page;
2+
3+
import java.util.List;
4+
import org.springframework.data.domain.Page;
5+
import org.springframework.data.domain.PageImpl;
6+
import org.springframework.data.domain.PageRequest;
7+
8+
public class PageWrapper<T> {
9+
private List<T> content;
10+
private long totalElements;
11+
private int pageNumber;
12+
private int pageSize;
13+
14+
public PageWrapper() {}
15+
16+
public PageWrapper(Page<T> page) {
17+
this.content = page.getContent();
18+
this.totalElements = page.getTotalElements();
19+
this.pageNumber = page.getNumber();
20+
this.pageSize = page.getSize();
21+
}
22+
23+
public Page<T> toPage() {
24+
return new PageImpl<>(content, PageRequest.of(pageNumber, pageSize), totalElements);
25+
}
26+
27+
public List<T> getContent() { return content; }
28+
public long getTotalElements() { return totalElements; }
29+
public int getPageNumber() { return pageNumber; }
30+
public int getPageSize() { return pageSize; }
31+
}

0 commit comments

Comments
 (0)