From d7c735daff3b724b8848f1cdbcbe4baeb41bce3b Mon Sep 17 00:00:00 2001
From: minor7295
+ * ZUNIONSTORE 명령어를 사용하여 여러 소스 ZSET의 점수를 합산합니다.
+ * 같은 멤버가 여러 ZSET에 있으면 점수가 합산됩니다.
+ *
+ * 사용 사례:
+ *
+ *
+ *
+ * 소스 ZSET의 점수에 가중치를 곱한 후 목적지 ZSET에 합산합니다. + * 목적지 ZSET이 이미 존재하면 기존 점수에 합산됩니다. + *
+ *+ * 사용 사례: + *
- * Kafka에서 이벤트를 수취하여 Redis ZSET에 랭킹 점수를 적재합니다. + * Kafka에서 이벤트를 수취하여 Spring ApplicationEvent로 발행합니다. * 조회, 좋아요, 주문 이벤트를 기반으로 실시간 랭킹을 구축합니다. *
*@@ -43,20 +42,22 @@ *
* 설계 원칙: *
+ * 좋아요 추가/취소, 주문 생성, 상품 조회 이벤트를 받아 랭킹 점수를 집계하는 애플리케이션 로직을 처리합니다. + *
+ *+ * DDD/EDA 관점: + *
+ * 주문 금액 계산: + *
+ * 하루의 모든 시간 단위 랭킹을 ZUNIONSTORE로 합쳐서 일간 랭킹을 생성합니다. + *
+ * + * @param date 날짜 + * @return 집계된 멤버 수 + */ + public Long aggregateHourlyToDaily(LocalDate date) { + String dailyKey = keyGenerator.generateDailyKey(date); + List+ * 콜드 스타트 문제를 완화하기 위해 오늘의 랭킹을 가중치를 적용하여 내일 랭킹에 반영합니다. + * 예: 오늘 랭킹의 10%를 내일 랭킹에 반영 + *
+ * + * @param today 오늘 날짜 + * @param tomorrow 내일 날짜 + * @param carryOverWeight Carry-Over 가중치 (예: 0.1 = 10%) + * @return 반영된 멤버 수 + */ + public Long carryOverScore(LocalDate today, LocalDate tomorrow, double carryOverWeight) { + String todayKey = keyGenerator.generateDailyKey(today); + String tomorrowKey = keyGenerator.generateDailyKey(tomorrow); + + // 오늘 랭킹을 가중치를 적용하여 내일 랭킹에 합산 + Long result = zSetTemplate.unionStoreWithWeight(tomorrowKey, todayKey, carryOverWeight); + + // TTL 설정 + zSetTemplate.setTtlIfNotExists(tomorrowKey, TTL); + + log.info("Score Carry-Over 완료: today={}, tomorrow={}, weight={}, memberCount={}", + today, tomorrow, carryOverWeight, result); + return result; + } + /** * ZSET에 점수를 증가시킵니다. *diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/event/ranking/RankingEventListener.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/event/ranking/RankingEventListener.java new file mode 100644 index 000000000..b72cc4a48 --- /dev/null +++ b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/event/ranking/RankingEventListener.java @@ -0,0 +1,121 @@ +package com.loopers.interfaces.event.ranking; + +import com.loopers.application.ranking.RankingEventHandler; +import com.loopers.domain.event.LikeEvent; +import com.loopers.domain.event.OrderEvent; +import com.loopers.domain.event.ProductEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 랭킹 이벤트 리스너. + *
+ * 좋아요 추가/취소, 주문 생성, 상품 조회 이벤트를 받아서 랭킹 점수를 집계하는 인터페이스 레이어의 어댑터입니다. + *
+ *+ * 레이어 역할: + *
+ * EDA 원칙: + *
+ * 비동기로 실행되어 랭킹 점수를 집계합니다. + *
+ * + * @param event 좋아요 추가 이벤트 + */ + @Async + @EventListener + public void handleLikeAdded(LikeEvent.LikeAdded event) { + try { + rankingEventHandler.handleLikeAdded(event); + } catch (Exception e) { + log.error("좋아요 추가 이벤트 처리 중 오류 발생: productId={}, userId={}", + event.productId(), event.userId(), e); + // 이벤트 처리 실패는 다른 리스너에 영향을 주지 않도록 예외를 삼킴 + } + } + + /** + * 좋아요 취소 이벤트를 처리합니다. + *+ * 비동기로 실행되어 랭킹 점수를 차감합니다. + *
+ * + * @param event 좋아요 취소 이벤트 + */ + @Async + @EventListener + public void handleLikeRemoved(LikeEvent.LikeRemoved event) { + try { + rankingEventHandler.handleLikeRemoved(event); + } catch (Exception e) { + log.error("좋아요 취소 이벤트 처리 중 오류 발생: productId={}, userId={}", + event.productId(), event.userId(), e); + // 이벤트 처리 실패는 다른 리스너에 영향을 주지 않도록 예외를 삼킴 + } + } + + /** + * 주문 생성 이벤트를 처리합니다. + *+ * 비동기로 실행되어 랭킹 점수를 집계합니다. + *
+ * + * @param event 주문 생성 이벤트 + */ + @Async + @EventListener + public void handleOrderCreated(OrderEvent.OrderCreated event) { + try { + rankingEventHandler.handleOrderCreated(event); + } catch (Exception e) { + log.error("주문 생성 이벤트 처리 중 오류 발생: orderId={}", event.orderId(), e); + // 이벤트 처리 실패는 다른 리스너에 영향을 주지 않도록 예외를 삼킴 + } + } + + /** + * 상품 조회 이벤트를 처리합니다. + *+ * 비동기로 실행되어 랭킹 점수를 집계합니다. + *
+ * + * @param event 상품 조회 이벤트 + */ + @Async + @EventListener + public void handleProductViewed(ProductEvent.ProductViewed event) { + try { + rankingEventHandler.handleProductViewed(event); + } catch (Exception e) { + log.error("상품 조회 이벤트 처리 중 오류 발생: productId={}", event.productId(), e); + // 이벤트 처리 실패는 다른 리스너에 영향을 주지 않도록 예외를 삼킴 + } + } +} +