Skip to content

Commit cd878d5

Browse files
committed
feature: 지표 메트릭 개선 및 RankingService 추가
- 이벤트 타임스탬프 필드를 `LocalDateTime`으로 변경하고 이벤트 생성 로직 업데이트 - `RankingService` 추가로 상품 점수 관리 로직 구현 (조회수, 좋아요, 판매량 가중치 적용) - MetricsEventConsumer 확장: 랭킹 점수 계산 및 Redis 업데이트 지원 - Kafka 리스너 및 배치 처리 로직 개선, 이벤트별 점수 업데이트 처리 추가 - OutboxHandler 수정: 이벤트 타입 변경 및 카탈로그 이벤트 발행 정리
1 parent 6de080b commit cd878d5

7 files changed

Lines changed: 87 additions & 17 deletions

File tree

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

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

33
import com.loopers.domain.event.OutboxService;
4+
import com.loopers.event.ProductStockEvent;
45
import lombok.RequiredArgsConstructor;
56
import org.springframework.kafka.core.KafkaTemplate;
67
import org.springframework.stereotype.Component;
@@ -14,8 +15,8 @@ public class OrderEventOutboxHandler {
1415
private final OutboxService outboxService;
1516

1617
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
17-
public void handle(OrderCreatedEvent event) {
18-
kafkaTemplate.send("order-events", String.valueOf(event.orderId()), event)
18+
public void handle(ProductStockEvent event) {
19+
kafkaTemplate.send("catalog-events", String.valueOf(event.productId()), event)
1920
.whenComplete((result, ex) -> {
2021
if (ex == null) {
2122
outboxService.markPublished(event.eventId());

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

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

33
import com.loopers.application.event.FailedEventStore;
44
import com.loopers.application.like.event.LikeCreatedEvent;
5-
import com.loopers.event.LikeCountEvent;
65
import com.loopers.domain.product.ProductService;
6+
import com.loopers.event.LikeCountEvent;
77
import lombok.RequiredArgsConstructor;
88
import org.springframework.context.ApplicationEventPublisher;
99
import org.springframework.orm.ObjectOptimisticLockingFailureException;
@@ -30,7 +30,7 @@ public void handleLikeCreatedEvent(LikeCreatedEvent event) {
3030
try {
3131
int updatedLikeCount = performAggregation(event);
3232

33-
eventPublisher.publishEvent(new LikeCountEvent(
33+
eventPublisher.publishEvent(LikeCountEvent.of(
3434
event.eventId(),
3535
event.productId(),
3636
updatedLikeCount
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.loopers.domain.rank;
2+
3+
import java.time.LocalDateTime;
4+
import java.time.format.DateTimeFormatter;
5+
import java.util.Map;
6+
import java.util.concurrent.TimeUnit;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.data.redis.core.RedisTemplate;
9+
import org.springframework.stereotype.Component;
10+
11+
@Component
12+
@RequiredArgsConstructor
13+
public class RankingService {
14+
15+
private final RedisTemplate<String, String> redisTemplate;
16+
17+
private static final String KEY_PREFIX = "ranking:all:";
18+
private static final double VIEW_WEIGHT = 0.1;
19+
private static final double LIKE_WEIGHT = 0.2;
20+
private static final double ORDER_WEIGHT = 0.6;
21+
22+
public void addScore(Long productId, double baseScore, double weight, LocalDateTime dateTime) {
23+
String dateKey = KEY_PREFIX + dateTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
24+
double finalScore = baseScore * weight;
25+
26+
redisTemplate.opsForZSet().incrementScore(dateKey, productId.toString(), finalScore);
27+
redisTemplate.expire(dateKey, 2, TimeUnit.DAYS);
28+
}
29+
30+
public void addOrderScoresBatch(Map<String, Map<Long, Double>> updates) {
31+
updates.forEach((dateKey, productScores) -> {
32+
productScores.forEach((productId, score) -> {
33+
redisTemplate.opsForZSet().incrementScore(dateKey, productId.toString(), score);
34+
});
35+
redisTemplate.expire(dateKey, 2, TimeUnit.DAYS);
36+
});
37+
}
38+
}

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

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
import com.loopers.event.LikeCountEvent;
66
import com.loopers.event.ProductStockEvent;
77
import com.loopers.event.ProductViewEvent;
8+
import java.time.format.DateTimeFormatter;
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
812
import lombok.RequiredArgsConstructor;
913
import lombok.extern.slf4j.Slf4j;
10-
import org.apache.kafka.clients.consumer.ConsumerRecord;
1114
import org.springframework.kafka.annotation.KafkaListener;
1215
import org.springframework.kafka.support.Acknowledgment;
1316
import org.springframework.stereotype.Component;
@@ -24,9 +27,10 @@ public class MetricsEventConsumer {
2427
topics = "catalog-events",
2528
groupId = "metrics-group"
2629
)
27-
public void consumeLikeCount(ConsumerRecord<String, LikeCountEvent> record, Acknowledgment ack) {
30+
public void consumeLikeCount(LikeCountEvent event, Acknowledgment ack) {
2831
try {
29-
metricsService.processLikeCountEvent(record.value());
32+
metricsService.processLikeCountEvent(event);
33+
rankingService.addScore(event.productId(), 1.0, 0.2, event.createdAt());
3034
ack.acknowledge();
3135
} catch (Exception e) {
3236
log.error("좋아요 메트릭 처리 실패: {}", e.getMessage());
@@ -40,6 +44,7 @@ public void consumeLikeCount(ConsumerRecord<String, LikeCountEvent> record, Ackn
4044
public void consumeProductView(ProductViewEvent event, Acknowledgment ack) {
4145
try {
4246
metricsService.processProductViewEvent(event);
47+
rankingService.addScore(event.productId(), 1.0, 0.1, event.createdAt());
4348
ack.acknowledge();
4449
} catch (Exception e) {
4550
log.error("조회수 메트릭 처리 실패: {}", event.eventId(), e);
@@ -48,14 +53,27 @@ public void consumeProductView(ProductViewEvent event, Acknowledgment ack) {
4853

4954
@KafkaListener(
5055
topics = "catalog-events",
51-
groupId = "metrics-group"
56+
groupId = "metrics-group",
57+
containerFactory = "batchFactory"
5258
)
53-
public void consumeSalesCount(ProductStockEvent event, Acknowledgment ack) {
59+
public void consumeSalesCount(List<ProductStockEvent> events, Acknowledgment ack) {
5460
try {
55-
metricsService.processSalesCountEvent(event);
61+
Map<String, Map<Long, Double>> rankingUpdates = new HashMap<>();
62+
63+
for (ProductStockEvent event : events) {
64+
metricsService.processSalesCountEvent(event);
65+
66+
String dateKey = "ranking:all:" + event.createdAt().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
67+
double orderScore = (event.price() * event.sellQuantity()) * 0.6;
68+
69+
rankingUpdates.computeIfAbsent(dateKey, k -> new HashMap<>())
70+
.merge(event.productId(), orderScore, Double::sum);
71+
}
72+
73+
rankingService.addOrderScoresBatch(rankingUpdates);
5674
ack.acknowledge();
5775
} catch (Exception e) {
58-
log.error("판매량 메트릭 처리 실패: {}", event.eventId(), e);
76+
log.error("판매량 메트릭 처리 실패", e);
5977
}
6078
}
6179
}
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
package com.loopers.event;
22

3+
import java.time.LocalDateTime;
4+
35
public record LikeCountEvent(
4-
String eventId,
6+
String eventId,
57
Long productId,
6-
int currentLikeCount
8+
int currentLikeCount,
9+
LocalDateTime createdAt
710
) {
811

12+
public static LikeCountEvent of(String eventId, Long productId, int currentLikeCount) {
13+
return new LikeCountEvent(
14+
eventId,
15+
productId,
16+
currentLikeCount,
17+
LocalDateTime.now()
18+
);
19+
}
920
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.loopers.event;
22

3+
import java.time.LocalDateTime;
34
import java.util.UUID;
45

56
public record ProductStockEvent(
@@ -8,7 +9,7 @@ public record ProductStockEvent(
89
int sellQuantity,
910
int currentStock,
1011
long price,
11-
long timestamp
12+
LocalDateTime createdAt
1213
) {
1314
public static ProductStockEvent of(Long productId, int sellQuantity, int currentStock, long price) {
1415
return new ProductStockEvent(
@@ -17,7 +18,7 @@ public static ProductStockEvent of(Long productId, int sellQuantity, int current
1718
sellQuantity,
1819
currentStock,
1920
price,
20-
System.currentTimeMillis()
21+
LocalDateTime.now()
2122
);
2223
}
2324
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package com.loopers.event;
22

3+
import java.time.LocalDateTime;
34
import java.util.UUID;
45

56
public record ProductViewEvent(
67
String eventId,
78
Long productId,
8-
long timestamp
9+
LocalDateTime createdAt
910
) {
1011

1112
public static ProductViewEvent from(Long productId) {
1213
return new ProductViewEvent(
1314
UUID.randomUUID().toString(),
1415
productId,
15-
System.currentTimeMillis()
16+
LocalDateTime.now()
1617
);
1718
}
1819
}

0 commit comments

Comments
 (0)