Skip to content

Conversation

@minor7295
Copy link
Collaborator

@minor7295 minor7295 commented Dec 23, 2025

๐Ÿ“Œ Summary

Kafka Consumer๋ฅผ ํ†ตํ•ด ์ˆ˜์‹ ํ•œ ์ด๋ฒคํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Redis ZSET์„ ์ด์šฉํ•œ ์‹ค์‹œ๊ฐ„ ๋žญํ‚น ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ๊ตฌํ˜„ ๋‚ด์šฉ:

  • ๋žญํ‚น ์ง‘๊ณ„ (commerce-streamer): Kafka ์ด๋ฒคํŠธ ์ˆ˜์‹  โ†’ Spring ApplicationEvent ๋ฐœํ–‰ โ†’ ๋žญํ‚น ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ Redis ZSET ์ ์žฌ
  • ๋žญํ‚น ์กฐํšŒ (commerce-api): Redis ZSET์—์„œ ๋žญํ‚น ์กฐํšŒ ๋ฐ ์ƒํ’ˆ ์ •๋ณด Aggregation
  • ์ด๋ฒคํŠธ๋ณ„ ๊ฐ€์ค‘์น˜ ์ ์šฉ: ์กฐํšŒ(0.1), ์ข‹์•„์š”(0.2), ์ฃผ๋ฌธ(0.6) ๊ฐ€์ค‘์น˜๋กœ ์ ์ˆ˜ ๊ณ„์‚ฐ
  • Graceful Degradation: Redis ์žฅ์•  ์‹œ ์Šค๋ƒ…์ƒท โ†’ ๊ธฐ๋ณธ ๋žญํ‚น(์ข‹์•„์š”์ˆœ) Fallback
  • ZSET ๋ชจ๋“ˆ ๋ถ„๋ฆฌ: Redis ZSET ์กฐ์ž‘ ๋กœ์ง์„ ๋ณ„๋„ ๋ชจ๋“ˆ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์žฌ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ

๊ตฌํ˜„๋œ ๊ธฐ๋Šฅ:

  • GET /api/v1/rankings?date=yyyyMMdd&size=20&page=1: ๋žญํ‚น ํŽ˜์ด์ง€ ์กฐํšŒ
  • ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ํ•ด๋‹น ์ƒํ’ˆ์˜ ๋žญํ‚น ์ •๋ณด ํฌํ•จ

๐Ÿ’ฌ Review Points

1. ๋žญํ‚น์€ ๋„๋ฉ”์ธ์ด ์•„๋‹Œ ์œ ์Šค์ผ€์ด์Šค๋กœ ํŒ๋‹จ

๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ œ ์ƒํ™ฉ:
๋žญํ‚น ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ•  ๋•Œ, ๋žญํ‚น์ด ๋„๋ฉ”์ธ์ธ์ง€ ์œ ์Šค์ผ€์ด์Šค์ธ์ง€ ํŒ๋‹จํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋„๋ฉ”์ธ์œผ๋กœ ์ทจ๊ธ‰ํ•˜๋ฉด ๋ณ„๋„์˜ ๋„๋ฉ”์ธ ๋ ˆ์ด์–ด๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•˜์ง€๋งŒ, ๋žญํ‚น์€ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ๊ฐ–๋Š” ๋…๋ฆฝ์ ์ธ ๋„๋ฉ”์ธ์ด ์•„๋‹ˆ๋ผ ์กฐํšŒ์šฉ ํŒŒ์ƒ ๋ฐ์ดํ„ฐ(Read Model)์ž…๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ:
๋žญํ‚น์„ ๋„๋ฉ”์ธ์ด ์•„๋‹Œ Application ๋ ˆ์ด์–ด์˜ ์œ ์Šค์ผ€์ด์Šค๋กœ ํŒ๋‹จํ•˜์—ฌ, ๋ณ„๋„์˜ ๋„๋ฉ”์ธ ๋ ˆ์ด์–ด๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๊ณ  Application ๋ ˆ์ด์–ด์—๋งŒ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋žญํ‚น์€ CQRS ํŒจํ„ด์˜ Read Side๋กœ ์ทจ๊ธ‰ํ•˜์—ฌ, Write Side(๋„๋ฉ”์ธ ์ด๋ฒคํŠธ) โ†’ Kafka โ†’ Read Side(๋žญํ‚น ์ง‘๊ณ„) โ†’ Redis ZSET ๊ตฌ์กฐ๋กœ ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ด€๋ จ ์ฝ”๋“œ:

// apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingService.java
/**
 * ๋žญํ‚น ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ ZSET ์ ์žฌ ์„œ๋น„์Šค.
 * <p>
 * Application ์œ ์ฆˆ์ผ€์ด์Šค: Ranking์€ ๋„๋ฉ”์ธ์ด ์•„๋‹Œ ํŒŒ์ƒ View๋กœ ์ทจ๊ธ‰
 * CQRS Read Model: Write Side(๋„๋ฉ”์ธ) โ†’ Kafka โ†’ Read Side(Application) โ†’ Redis ZSET
 */
@Service
public class RankingService {
    // ๋žญํ‚น ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ ZSET ์ ์žฌ ๋กœ์ง
}

๊ณ ๋ฏผํ•œ ์ :

  • ๋žญํ‚น์„ ๋„๋ฉ”์ธ์œผ๋กœ ์ทจ๊ธ‰ํ•˜๋ฉด ๋ณ„๋„์˜ ๋„๋ฉ”์ธ ๋ ˆ์ด์–ด๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•˜์ง€๋งŒ, ๋žญํ‚น์€ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ๊ฐ–๋Š” ๋…๋ฆฝ์ ์ธ ๋„๋ฉ”์ธ์ด ์•„๋‹ˆ๋ผ ์กฐํšŒ์šฉ ํŒŒ์ƒ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ Application ๋ ˆ์ด์–ด์—๋งŒ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ๋žญํ‚น์€ CQRS ํŒจํ„ด์˜ Read Side๋กœ ์ทจ๊ธ‰ํ•˜์—ฌ, Write Side(๋„๋ฉ”์ธ ์ด๋ฒคํŠธ)์™€ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋„๋ฉ”์ธ ๋กœ์ง๊ณผ ๋žญํ‚น ์ง‘๊ณ„ ๋กœ์ง์ด ๋…๋ฆฝ์ ์œผ๋กœ ์ง„ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2. ์™ธ๋ถ€ ์ด๋ฒคํŠธ์™€ ๋‚ด๋ถ€ ์ด๋ฒคํŠธ ๊ตฌ๋ถ„: Spring ApplicationEvent ์‚ฌ์šฉ

๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ œ ์ƒํ™ฉ:
Kafka Consumer์—์„œ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•œ ํ›„, ๋žญํ‚น ์ง‘๊ณ„์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” ๊ฒƒ๊ณผ ๋žญํ‚น ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์€ ์„œ๋กœ ๋‹ค๋ฅธ ์ฑ…์ž„์ž…๋‹ˆ๋‹ค. Kafka๋กœ consumeํ•˜๋Š” ๊ฒƒ์€ ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ์˜ ํ†ต์‹ (์ธํ„ฐํŽ˜์ด์Šค ๊ณ„์ธต)์ด๊ณ , ZSET์˜ ์ ์ˆ˜ ๊ณ„์‚ฐ์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€ ๋กœ์ง(์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณ„์ธต)์ž…๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ:
๋„๋ฉ”์ธ์˜ ์ฑ…์ž„์„ ๋ช…ํ™•ํžˆ ํ•˜๊ธฐ ์œ„ํ•ด, Kafka Consumer๋Š” ์™ธ๋ถ€ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ  Spring ApplicationEvent๋กœ ๋ฐœํ–‰ํ•˜๋Š” ์—ญํ• ๋งŒ ๋‹ด๋‹นํ•˜๊ณ , ๋žญํ‚น ๊ณ„์‚ฐ ๋กœ์ง์€ ApplicationEvent๋ฅผ ๊ตฌ๋…ํ•˜๋Š” ๋ณ„๋„์˜ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Kafka Consumer๋Š” ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ /ํŒŒ์‹ฑ๋งŒ ๋‹ด๋‹นํ•˜๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณ„์ธต์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ตฌ์กฐ:

Kafka (์™ธ๋ถ€ ์‹œ์Šคํ…œ)
    โ†“
RankingConsumer (์ธํ„ฐํŽ˜์ด์Šค ๋ ˆ์ด์–ด)
    โ”œโ”€ Kafka ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ /ํŒŒ์‹ฑ
    โ”œโ”€ ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ
    โ””โ”€ Spring ApplicationEvent ๋ฐœํ–‰
        โ†“
RankingEventListener (์ธํ„ฐํŽ˜์ด์Šค ๋ ˆ์ด์–ด)
    โ””โ”€ @EventListener ๊ตฌ๋… (@Async๋กœ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ)
        โ†“
RankingEventHandler (์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ ˆ์ด์–ด)
    โ””โ”€ RankingService ํ˜ธ์ถœ
        โ†“
RankingService (์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ ˆ์ด์–ด)
    โ””โ”€ Redis ZSET ์ ์žฌ

๊ด€๋ จ ์ฝ”๋“œ:

// apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/RankingConsumer.java
@KafkaListener(topics = "like-events", containerFactory = KafkaConfig.BATCH_LISTENER)
public void consumeLikeEvents(List<ConsumerRecord<String, Object>> records, Acknowledgment acknowledgment) {
    // Kafka ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ /ํŒŒ์‹ฑ ํ›„ Spring ApplicationEvent ๋ฐœํ–‰
    LikeEvent.LikeAdded event = parseLikeEvent(record.value());
    applicationEventPublisher.publishEvent(event);
    eventHandledService.markAsHandled(eventId, "LikeAdded", "like-events");
}

// apps/commerce-streamer/src/main/java/com/loopers/interfaces/event/ranking/RankingEventListener.java
@Async
@EventListener
public void handleLikeAdded(LikeEvent.LikeAdded event) {
    rankingEventHandler.handleLikeAdded(event);
}

// apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingEventHandler.java
public void handleLikeAdded(LikeEvent.LikeAdded event) {
    rankingService.addLikeScore(event.productId(), LocalDate.now(), true);
}

๊ณ ๋ฏผํ•œ ์ :

  • Kafka Consumer์—์„œ ์ง์ ‘ RankingService๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹๋„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Kafka Consumer๊ฐ€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํฌํ•จํ•˜๊ฒŒ ๋˜์–ด ์ฑ…์ž„์ด ์„ž์ž…๋‹ˆ๋‹ค. Spring ApplicationEvent๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ด€์‹ฌ์‚ฌ๊ฐ€ ๋ช…ํ™•ํžˆ ๋ถ„๋ฆฌ๋ฉ๋‹ˆ๋‹ค.
  • @Async๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋žญํ‚น ์ง‘๊ณ„ ์ฒ˜๋ฆฌ๋ฅผ ๋น„๋™๊ธฐ๋กœ ์‹คํ–‰ํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Kafka Consumer์˜ ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๊ณ  ๋žญํ‚น ์ง‘๊ณ„๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3. ์ฝœ๋“œ ์Šคํƒ€ํŠธ ๋ฌธ์ œ ํ•ด๊ฒฐ: Score Carry-Over ๋ฐฉ์‹

๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ œ ์ƒํ™ฉ:
์ผ๋ณ„ ๋žญํ‚น์„ ๋…๋ฆฝ์ ์œผ๋กœ ๊ณ„์‚ฐํ•˜๋ฉด, ๋งค์ผ ์ž์ •์— ๋žญํ‚น์ด 0์ ์—์„œ ์‹œ์ž‘ํ•˜๋Š” ์ฝœ๋“œ ์Šคํƒ€ํŠธ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์–ด์ œ ์ธ๊ธฐ ์žˆ๋˜ ์ƒํ’ˆ์ด ์˜ค๋Š˜ ์ž์ •์— ๊ฐ‘์ž๊ธฐ ๋žญํ‚น์—์„œ ์‚ฌ๋ผ์ง€๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ์ข‹์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ ๋ฐ ๋ฐฉ์‹ ์„ ํƒ:

์ฝœ๋“œ ์Šคํƒ€ํŠธ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์„ ๊ณ ๋ คํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ๋Š” ZUNIONSTORE ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ๊ฐ„๋ณ„ ๋žญํ‚น์„ ๋ณ„๋„๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์ด๋ฅผ ์ผ๊ฐ„ ๋žญํ‚น์œผ๋กœ ์ง‘๊ณ„ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ ์‹œ๊ฐ„๋ณ„ ๋žญํ‚น ํ‚ค(ranking:hourly:yyyyMMddHH)์™€ ์ผ๊ฐ„ ๋žญํ‚น ํ‚ค(ranking:all:yyyyMMdd)์— ์ด์ค‘์œผ๋กœ ์ ์ˆ˜๋ฅผ ์ ์žฌํ•˜๊ณ , ZUNIONSTORE๋กœ ์‹œ๊ฐ„๋ณ„ ๋žญํ‚น์„ ์ผ๊ฐ„ ๋žญํ‚น์œผ๋กœ ์ง‘๊ณ„ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์˜ ์žฅ์ ์€ ์‹œ๊ฐ„ ๋‹จ์œ„ ๋žญํ‚น ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ณ , ์‹œ๊ฐ„๋ณ„ ๊ฐ€์ค‘์น˜๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋ฐฐ์น˜ ์ง‘๊ณ„๊ฐ€ ์ตœ์ ํ™”๋œ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‹จ์ ์œผ๋กœ๋Š” Redis ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ์•ฝ 2.4๋ฐฐ ์ฆ๊ฐ€ํ•˜๊ณ , ์‹œ๊ฐ„๋ณ„ ๋žญํ‚น ์ ์žฌ ๋กœ์ง, ์ง‘๊ณ„ ์Šค์ผ€์ค„๋Ÿฌ, ์‹œ๊ฐ„ ๋‹จ์œ„ Carry-Over ์Šค์ผ€์ค„๋Ÿฌ ๋“ฑ์œผ๋กœ ์ธํ•ด ๊ตฌํ˜„ ๋ณต์žก๋„๊ฐ€ ํฌ๊ฒŒ ์ฆ๊ฐ€ํ•˜๋ฉฐ, ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ 3๊ฐœ๋‚˜ ๊ด€๋ฆฌํ•ด์•ผ ํ•˜๊ณ , ๋‘ ๊ฐœ์˜ ZSET์— ์ ์žฌํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์‹ค์‹œ๊ฐ„์„ฑ๋„ ์•ฝ๊ฐ„ ์ €ํ•˜๋ฉ๋‹ˆ๋‹ค.

๋‘ ๋ฒˆ์งธ ๋ฐฉ์‹์€ ์ผ๊ฐ„ ๋žญํ‚น ํ‚ค์— ์ง์ ‘ ์ ์ˆ˜๋ฅผ ์ ์žฌํ•˜๊ณ , ์ผ๊ฐ„ ๋žญํ‚น Carry-Over๋งŒ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ ๊ตฌํ˜„์ด ๋‹จ์ˆœํ•˜๊ณ , Redis ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ตœ์†Œํ™”ํ•˜๋ฉฐ, ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ 1๊ฐœ๋งŒ ๊ด€๋ฆฌํ•˜๋ฉด ๋˜๊ณ , ๋‹จ์ผ ZSET์—๋งŒ ์ ์žฌํ•˜๋ฏ€๋กœ ์‹ค์‹œ๊ฐ„์„ฑ๋„ ์šฐ์ˆ˜ํ•˜๋ฉฐ, ์ฝ”๋“œ ๋ณต์žก๋„๊ฐ€ ๋‚ฎ์•„ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ ์‹œ๊ฐ„๋ณ„ ๊ฐ€์ค‘์น˜ ์ ์šฉ์€ ๋ถˆ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ํ˜„์žฌ ์š”๊ตฌ์‚ฌํ•ญ์—๋Š” ์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์‹ค๋ฌด ๊ด€์ ์—์„œ ZUNIONSTORE๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋‘ ๋ฒˆ์งธ ๋ฐฉ์‹์„ ์ตœ์ข…์ ์œผ๋กœ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์‹œ๊ฐ„๋ณ„ ๊ฐ€์ค‘์น˜ ์ ์šฉ์ด ํ•„์š”ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ZUNIONSTORE๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ๋น„์šฉ(Redis ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์•ฝ 2.4๋ฐฐ ์ฆ๊ฐ€, ์Šค์ผ€์ค„๋Ÿฌ ๊ด€๋ฆฌ ๋ณต์žก๋„ ์ฆ๊ฐ€) ๋Œ€๋น„ ์–ป๋Š” ์ด์ ์ด ํ˜„์žฌ ์ƒํ™ฉ์—์„œ๋Š” ์ ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ์ผ๊ฐ„ ๋žญํ‚น์— ์ง์ ‘ ์ ์žฌํ•˜๋Š” ๋ฐฉ์‹์ด ๋” ๋‹จ์ˆœํ•˜๊ณ  ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šฐ๋ฉฐ, ์šด์˜ํ•˜๊ธฐ๋„ ์‰ฌ์›Œ ์‹ค๋ฌด์—์„œ ๋” ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ํ–ฅํ›„ ์‹œ๊ฐ„๋ณ„ ๋žญํ‚น ๊ธฐ๋Šฅ์ด ์‹ค์ œ๋กœ ํ•„์š”ํ•˜๋‹ค๋ฉด, ๊ทธ๋•Œ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ๋” ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.

๊ตฌํ˜„ ์ฝ”๋“œ:

// apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingService.java
/**
 * ์ ์ˆ˜๋Š” ์ผ๊ฐ„ ๋žญํ‚น ํ‚ค์— ์ง์ ‘ ์ ์žฌ๋ฉ๋‹ˆ๋‹ค.
 */
public void addViewScore(Long productId, LocalDate date) {
    String key = keyGenerator.generateDailyKey(date); // ranking:all:yyyyMMdd
    incrementScore(key, productId, VIEW_WEIGHT);
}

/**
 * Score Carry-Over: ์˜ค๋Š˜์˜ ๋žญํ‚น์„ ๊ฐ€์ค‘์น˜๋ฅผ ์ ์šฉํ•˜์—ฌ ๋‚ด์ผ ๋žญํ‚น์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค.
 */
public Long carryOverScore(LocalDate today, LocalDate tomorrow, double carryOverWeight) {
    String todayKey = keyGenerator.generateDailyKey(today);
    String tomorrowKey = keyGenerator.generateDailyKey(tomorrow);
    return zSetTemplate.unionStoreWithWeight(tomorrowKey, todayKey, carryOverWeight);
}

์Šค์ผ€์ค„๋Ÿฌ ๊ตฌํ˜„:

// apps/commerce-streamer/src/main/java/com/loopers/infrastructure/scheduler/RankingCarryOverScheduler.java
/**
 * ๋žญํ‚น Score Carry-Over ์Šค์ผ€์ค„๋Ÿฌ.
 * ๋งค์ผ ์ž์ •์— ์ „๋‚  ๋žญํ‚น์„ ์˜ค๋Š˜ ๋žญํ‚น์— ์ผ๋ถ€ ๋ฐ˜์˜ํ•˜์—ฌ ์ฝœ๋“œ ์Šคํƒ€ํŠธ ๋ฌธ์ œ๋ฅผ ์™„ํ™”ํ•ฉ๋‹ˆ๋‹ค.
 */
@Scheduled(cron = "0 0 0 * * ?") // ๋งค์ผ ์ž์ • (00:00:00)
public void carryOverScore() {
    LocalDate today = LocalDate.now();
    LocalDate yesterday = today.minusDays(1);
    rankingService.carryOverScore(yesterday, today, DEFAULT_CARRY_OVER_WEIGHT);
}

๊ณ ๋ฏผํ•œ ์ :

  • ์ผ๋ณ„ ๋žญํ‚น์„ ๋…๋ฆฝ์ ์œผ๋กœ ๊ณ„์‚ฐํ•˜๋ฉด ๋งค์ผ ์ž์ •์— ๋žญํ‚น์ด 0์ ์—์„œ ์‹œ์ž‘ํ•˜๋Š” ์ฝœ๋“œ ์Šคํƒ€ํŠธ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Score Carry-Over ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.
  • Score Carry-Over ๊ฐ€์ค‘์น˜(์˜ˆ: 0.1 = 10%)๋Š” ์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ ์กฐ์ •์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋„ˆ๋ฌด ๋†’์œผ๋ฉด ์˜ค๋ž˜๋œ ๋žญํ‚น์ด ๊ณ„์† ๋ฐ˜์˜๋˜์–ด ์‹ ์„ ๋„๊ฐ€ ๋–จ์–ด์ง€๊ณ , ๋„ˆ๋ฌด ๋‚ฎ์œผ๋ฉด ์ฝœ๋“œ ์Šคํƒ€ํŠธ ๋ฌธ์ œ๊ฐ€ ์™„์ „ํžˆ ํ•ด๊ฒฐ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋งค์ผ ์ž์ •์— ์ž๋™์œผ๋กœ ์ „๋‚  ๋žญํ‚น์„ ์˜ค๋Š˜ ๋žญํ‚น์— ๋ฐ˜์˜ํ•˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ, ์ฝœ๋“œ ์Šคํƒ€ํŠธ ๋ฌธ์ œ๋ฅผ ์™„ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

4. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ: Graceful Degradation ์ „๋žต

๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ œ ์ƒํ™ฉ:
Redis ์žฅ์• ๋‚˜ ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋กœ ์ธํ•ด ๋žญํ‚น ์กฐํšŒ๊ฐ€ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž์—๊ฒŒ ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค, ๋Œ€์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ด ๋” ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ:
๋ฉ˜ํ† ๋ง ์„ธ์…˜์—์„œ "DB ์‹ค์‹œ๊ฐ„ ์žฌ๊ณ„์‚ฐ์€ ์œ„ํ—˜ํ•˜๋ฏ€๋กœ ์Šค๋ƒ…์ƒท ์„œ๋น™์ด ํ˜„์‹ค์ "์ด๋ผ๋Š” ์กฐ์–ธ์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ Redis ์žฅ์•  ์‹œ ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ์— ์ €์žฅ๋œ ๋žญํ‚น ์Šค๋ƒ…์ƒท์„ ์„œ๋น™ํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ์Šค๋ƒ…์ƒท๋„ ์—†์„ ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด ๊ธฐ๋ณธ ๋žญํ‚น(์ข‹์•„์š”์ˆœ)์„ ์ตœ์ข… Fallback์œผ๋กœ ์ œ๊ณตํ•˜์ง€๋งŒ, ์ด๋Š” ๋žญํ‚น์„ ์ƒˆ๋กœ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ด๋ฏธ ์ง‘๊ณ„๋œ ์ข‹์•„์š” ์ˆ˜๋ฅผ ๋‹จ์ˆœ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ DB ๋ถ€ํ•˜๊ฐ€ ํฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๊ตฌํ˜„ ์„ธ๋ถ€์‚ฌํ•ญ:

  1. Redis ์กฐํšŒ ์‹œ๋„: ๋จผ์ € ์š”์ฒญํ•œ ๋‚ ์งœ์˜ ๋žญํ‚น์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  2. ์Šค๋ƒ…์ƒท Fallback: Redis ์žฅ์•  ์‹œ ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ์— ์ €์žฅ๋œ ๋žญํ‚น ์Šค๋ƒ…์ƒท์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  3. ์ „๋‚  ์Šค๋ƒ…์ƒท Fallback: ๋‹น์ผ ์Šค๋ƒ…์ƒท์ด ์—†์œผ๋ฉด ์ „๋‚  ์Šค๋ƒ…์ƒท์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  4. ๊ธฐ๋ณธ ๋žญํ‚น Fallback: ์Šค๋ƒ…์ƒท๋„ ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋žญํ‚น(์ข‹์•„์š”์ˆœ)์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋žญํ‚น์„ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ด๋ฏธ ์ง‘๊ณ„๋œ ์ข‹์•„์š” ์ˆ˜๋ฅผ ์กฐํšŒํ•˜๋Š” ๋‹จ์ˆœ ์ฟผ๋ฆฌ์ด๋ฏ€๋กœ DB ๋ถ€ํ•˜๊ฐ€ ํฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๊ด€๋ จ ์ฝ”๋“œ:

// apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java
@Transactional(readOnly = true)
public RankingsResponse getRankings(LocalDate date, int page, int size) {
    try {
        return getRankingsFromRedis(date, page, size);
    } catch (DataAccessException e) {
        // Redis ์žฅ์•  ์‹œ ์Šค๋ƒ…์ƒท์œผ๋กœ Fallback
        Optional<RankingsResponse> snapshot = rankingSnapshotService.getSnapshot(date);
        if (snapshot.isPresent()) {
            return snapshot.get();
        }
        
        // ์ „๋‚  ์Šค๋ƒ…์ƒท ์‹œ๋„
        Optional<RankingsResponse> yesterdaySnapshot = rankingSnapshotService.getSnapshot(date.minusDays(1));
        if (yesterdaySnapshot.isPresent()) {
            return yesterdaySnapshot.get();
        }
        
        // ์ตœ์ข… Fallback: ๊ธฐ๋ณธ ๋žญํ‚น(์ข‹์•„์š”์ˆœ) - ๋‹จ์ˆœ ์กฐํšŒ, ๊ณ„์‚ฐ ์•„๋‹˜
        return getDefaultRankings(page, size);
    }
}

private RankingsResponse getDefaultRankings(int page, int size) {
    // ์ข‹์•„์š”์ˆœ์œผ๋กœ ์ƒํ’ˆ ์กฐํšŒ (์ด๋ฏธ ์ง‘๊ณ„๋œ ์ข‹์•„์š” ์ˆ˜ ๋‹จ์ˆœ ์กฐํšŒ)
    List<Product> products = productService.findAll(null, "likes_desc", page, size);
    // ... ์ƒํ’ˆ ์ •๋ณด Aggregation ๋ฐ ๋žญํ‚น ํ•ญ๋ชฉ ์ƒ์„ฑ
}

์Šค๋ƒ…์ƒท ์ €์žฅ ๋ฐ ์กฐํšŒ ๊ตฌํ˜„:

// apps/commerce-api/src/main/java/com/loopers/infrastructure/scheduler/RankingSnapshotScheduler.java
@Scheduled(fixedRate = 3600000) // 1์‹œ๊ฐ„๋งˆ๋‹ค
public void saveRankingSnapshot() {
    LocalDate today = LocalDate.now();
    try {
        // ์ƒ์œ„ 100๊ฐœ ๋žญํ‚น์„ ์Šค๋ƒ…์ƒท์œผ๋กœ ์ €์žฅ
        RankingService.RankingsResponse rankings = rankingService.getRankingsFromRedis(today, 0, 100);
        rankingSnapshotService.saveSnapshot(today, rankings);
    } catch (DataAccessException e) {
        // Redis ์žฅ์•  ์‹œ ์Šค๋ƒ…์ƒท ์ €์žฅ ์Šคํ‚ต (๋‹ค์Œ ์Šค์ผ€์ค„์—์„œ ์žฌ์‹œ๋„)
    }
}

// apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingSnapshotService.java
@Service
public class RankingSnapshotService {
    private final Map<String, RankingService.RankingsResponse> snapshotCache = new ConcurrentHashMap<>();
    private static final int MAX_SNAPSHOTS = 7; // ์ตœ๊ทผ 7์ผ์น˜๋งŒ ๋ณด๊ด€

    public void saveSnapshot(LocalDate date, RankingService.RankingsResponse rankings) {
        snapshotCache.put(date.format(DATE_FORMATTER), rankings);
        cleanupOldSnapshots(); // ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ
    }

    public Optional<RankingService.RankingsResponse> getSnapshot(LocalDate date) {
        return Optional.ofNullable(snapshotCache.get(date.format(DATE_FORMATTER)));
    }
}

๊ณ ๋ฏผํ•œ ์ :

  • ์Šค๋ƒ…์ƒท ์ €์žฅ ๋ฐฉ์‹ ์„ ํƒ: ๋ฉ˜ํ† ๋ง ์„ธ์…˜์—์„œ "DB ์‹ค์‹œ๊ฐ„ ์žฌ๊ณ„์‚ฐ์€ ์œ„ํ—˜ํ•˜๋ฏ€๋กœ ์Šค๋ƒ…์ƒท ์„œ๋น™์ด ํ˜„์‹ค์ "์ด๋ผ๋Š” ์กฐ์–ธ์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. ์Šค๋ƒ…์ƒท ์ €์žฅ ๋ฐฉ์‹์œผ๋กœ๋Š” ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ์™€ ํŒŒ์ผ ์‹œ์Šคํ…œ ๋‘ ๊ฐ€์ง€๋ฅผ ๊ณ ๋ คํ–ˆ๋Š”๋ฐ, ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋žญํ‚น์€ ๋น„์ฆˆ๋‹ˆ์Šค ๊ฒฐ์ •์ด ์•„๋‹Œ ์กฐํšŒ์šฉ ํŒŒ์ƒ ๋ฐ์ดํ„ฐ์ด๋ฏ€๋กœ, ์˜์†์„ฑ์ด ํ•„์ˆ˜๋Š” ์•„๋‹ˆ๋ฉฐ ์Šค๋ƒ…์ƒท์ด ์—†์–ด๋„ ๊ธฐ๋ณธ ๋žญํ‚น(์ข‹์•„์š”์ˆœ)์œผ๋กœ ๋Œ€์ฒด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ๋Š” ๊ตฌํ˜„์ด ๊ฐ„๋‹จํ•˜๊ณ  ์„ฑ๋Šฅ์ด ์šฐ์ˆ˜ํ•˜๋ฉฐ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์žฌ์‹œ์ž‘ ์‹œ ์Šค๋ƒ…์ƒท์ด ์‚ฌ๋ผ์ง€๋”๋ผ๋„ ๊ธฐ๋ณธ ๋žญํ‚น์œผ๋กœ Fallbackํ•  ์ˆ˜ ์žˆ์–ด ์ถฉ๋ถ„ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค. ํ–ฅํ›„ ์˜์†์„ฑ์ด ํ•„์š”ํ•ด์ง€๋ฉด ํŒŒ์ผ ์‹œ์Šคํ…œ์ด๋‚˜ Redis์— ๋ณ„๋„ ํ‚ค๋กœ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์Šค๋ƒ…์ƒท ์ €์žฅ ์ฃผ๊ธฐ ์„ ํƒ: ์Šค๋ƒ…์ƒท ์ €์žฅ ์ฃผ๊ธฐ๋ฅผ 1์‹œ๊ฐ„์œผ๋กœ ์ง€์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋žญํ‚น์€ ์ƒ์œ„ 10์œ„ ๋‚ด์—์„œ๋Š” ์ƒ๋Œ€์ ์œผ๋กœ ์•ˆ์ •์ ์ด๋ฏ€๋กœ, 1์‹œ๊ฐ„์ •๋„๋กœ ์—…๋ฐ์ดํŠธํ•ด๋„ ์‚ฌ์šฉ์ž๊ฐ€ ์ฒด๊ฐํ•˜๊ธฐ ์–ด๋ ต๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ Redis ์žฅ์•  ์‹œ 1์‹œ๊ฐ„ ์ „ ์Šค๋ƒ…์ƒท๋„ ์–ด์ œ ๋žญํ‚น๋ณด๋‹ค ํ›จ์”ฌ ์‹ ์„ ํ•˜๋ฉฐ, ๊ธฐ๋ณธ ๋žญํ‚น์œผ๋กœ ์ตœ์ข… Fallbackํ•  ์ˆ˜ ์žˆ์–ด ์ถฉ๋ถ„ํ•ด ๋ณด์ž…๋‹ˆ๋‹ค.
  • ๊ธฐ๋ณธ ๋žญํ‚น Fallback์˜ ์ •๋‹น์„ฑ: ์Šค๋ƒ…์ƒท๋„ ์—†์„ ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด ๊ธฐ๋ณธ ๋žญํ‚น(์ข‹์•„์š”์ˆœ)์„ ์ตœ์ข… Fallback์œผ๋กœ ์ œ๊ณตํ•˜์ง€๋งŒ, ์ด๋Š” ๋žญํ‚น์„ ์ƒˆ๋กœ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ด๋ฏธ ์ง‘๊ณ„๋œ ์ข‹์•„์š” ์ˆ˜๋ฅผ ๋‹จ์ˆœ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. productService.findAll(null, "likes_desc", page, size)๋Š” ์ธ๋ฑ์Šค๊ฐ€ ์žˆ๋Š” ์ปฌ๋Ÿผ์„ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ๋œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋‹จ์ˆœ ์ฟผ๋ฆฌ์ด๋ฏ€๋กœ, ๋žญํ‚น์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ DB ๋ถ€ํ•˜๊ฐ€ ํฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ ๋ฉ˜ํ† ๋ง ์กฐ์–ธ์— ๋”ฐ๋ผ ์Šค๋ƒ…์ƒท์„ ์šฐ์„ ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ณ , ๊ธฐ๋ณธ ๋žญํ‚น์€ ์ตœํ›„์˜ ์ˆ˜๋‹จ์œผ๋กœ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

5. ZSET์„ ๋ณ„๋„ ๋ชจ๋“ˆ๋กœ ๋ถ„๋ฆฌ

๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ œ ์ƒํ™ฉ:
Redis ZSET ์กฐ์ž‘ ๋กœ์ง์ด ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด ๋ณ„๋„ ๋ชจ๋“ˆ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ:
Redis ZSET ์กฐ์ž‘ ๋กœ์ง์„ modules/redis ๋ชจ๋“ˆ์˜ RedisZSetTemplate ํด๋ž˜์Šค๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ, ๋‹ค๋ฅธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋„ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ตฌํ˜„ ์„ธ๋ถ€์‚ฌํ•ญ:

  • RedisZSetTemplate: ZSET ์กฐ์ž‘ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ํ…œํ”Œ๋ฆฟ ํด๋ž˜์Šค
    • incrementScore: ์ ์ˆ˜ ์ฆ๊ฐ€
    • getTopRankings: ์ƒ์œ„ N๊ฐœ ์กฐํšŒ
    • getRank: ํŠน์ • ๋ฉค๋ฒ„์˜ ์ˆœ์œ„ ์กฐํšŒ
    • getSize: ZSET ํฌ๊ธฐ ์กฐํšŒ
    • unionStore: ์—ฌ๋Ÿฌ ZSET ํ•ฉ์น˜๊ธฐ
    • unionStoreWithWeight: ๊ฐ€์ค‘์น˜๋ฅผ ์ ์šฉํ•˜์—ฌ ZSET ํ•ฉ์น˜๊ธฐ
    • setTtlIfNotExists: TTL ์„ค์ •

๊ด€๋ จ ์ฝ”๋“œ:

// modules/redis/src/main/java/com/loopers/zset/RedisZSetTemplate.java
/**
 * Redis ZSET ํ…œํ”Œ๋ฆฟ.
 * <p>
 * Redis Sorted Set (ZSET) ์กฐ์ž‘ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
 * ZSET์€ Redis ์ „์šฉ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ด๋ฏ€๋กœ ์ธํ„ฐํŽ˜์ด์Šค ๋ถ„๋ฆฌ ์—†์ด ํด๋ž˜์Šค๋กœ ์ง์ ‘ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
 * </p>
 */
@Component
@RequiredArgsConstructor
public class RedisZSetTemplate {
    private final RedisTemplate<String, String> redisTemplate;
    
    public void incrementScore(String key, String member, double score) {
        redisTemplate.opsForZSet().incrementScore(key, member, score);
    }
    
    public List<ZSetEntry> getTopRankings(String key, long start, long end) {
        Set<ZSetOperations.TypedTuple<String>> tuples = redisTemplate.opsForZSet()
            .reverseRangeWithScores(key, start, end);
        // ... ๋ณ€ํ™˜ ๋กœ์ง
    }
    
    // ... ๊ธฐํƒ€ ๋ฉ”์„œ๋“œ
}

๊ณ ๋ฏผํ•œ ์ :

  • ZSET์€ Redis ์ „์šฉ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ด๋ฏ€๋กœ ์ธํ„ฐํŽ˜์ด์Šค ๋ถ„๋ฆฌ ์—†์ด ํด๋ž˜์Šค๋กœ ์ง์ ‘ ์ œ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ถˆํ•„์š”ํ•œ ์ถ”์ƒํ™”๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๋‹จ์ˆœ์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • RedisZSetTemplate์„ @Component๋กœ ๋“ฑ๋กํ•˜์—ฌ ๋‹ค๋ฅธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

โœ… Checklist

Ranking Consumer

  • ๋žญํ‚น ZSET์˜ TTL, ํ‚ค ์ „๋žต์„ ์ ์ ˆํ•˜๊ฒŒ ๊ตฌ์„ฑํ•˜์˜€๋‹ค

    • ํ‚ค ํ˜•์‹: ranking:all:yyyyMMdd (์ผ๊ฐ„ ๋žญํ‚น)
    • TTL: 2์ผ (Duration.ofDays(2))
    • apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingKeyGenerator.java
  • ๋‚ ์งœ๋ณ„๋กœ ์ ์žฌํ•  ํ‚ค๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์—ˆ๋‹ค

    • RankingKeyGenerator.generateDailyKey(): ์ผ๊ฐ„ ๋žญํ‚น ํ‚ค ์ƒ์„ฑ (ranking:all:yyyyMMdd)
    • apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingKeyGenerator.java
  • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ํ›„, ZSET์— ์ ์ˆ˜๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ฐ˜์˜๋œ๋‹ค

    • ์กฐํšŒ: Weight = 0.1
    • ์ข‹์•„์š”: Weight = 0.2
    • ์ฃผ๋ฌธ: Weight = 0.6, Score = log(1 + orderAmount) * ORDER_WEIGHT
    • apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingService.java

Ranking API

  • ๋žญํ‚น Page ์กฐํšŒ ์‹œ ์ •์ƒ์ ์œผ๋กœ ๋žญํ‚น ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค

    • GET /api/v1/rankings?date=yyyyMMdd&size=20&page=1
    • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
  • ๋žญํ‚น Page ์กฐํšŒ ์‹œ ๋‹จ์ˆœํžˆ ์ƒํ’ˆ ID๊ฐ€ ์•„๋‹Œ ์ƒํ’ˆ์ •๋ณด๊ฐ€ Aggregation ๋˜์–ด ์ œ๊ณต๋œ๋‹ค

    • ์ƒํ’ˆ ์ •๋ณด ๋ฐฐ์น˜ ์กฐํšŒ (N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ ๋ฐฉ์ง€)
    • ๋ธŒ๋žœ๋“œ ์ •๋ณด ๋ฐฐ์น˜ ์กฐํšŒ
    • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (98-170์ค„)
  • ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ํ•ด๋‹น ์ƒํ’ˆ์˜ ์ˆœ์œ„๊ฐ€ ํ•จ๊ป˜ ๋ฐ˜ํ™˜๋œ๋‹ค (์ˆœ์œ„์— ์—†๋‹ค๋ฉด null)

    • RankingService.getProductRank(): ํŠน์ • ์ƒํ’ˆ์˜ ์ˆœ์œ„ ์กฐํšŒ
    • apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java (๋žญํ‚น ์ •๋ณด ํฌํ•จ)

-->

๐Ÿ“Ž References

Summary by CodeRabbit

๋ฆด๋ฆฌ์Šค ๋…ธํŠธ

  • ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ
    • ์ƒํ’ˆ์— ์ˆœ์œ„ ์ •๋ณด ์ถ”๊ฐ€: ์กฐํšŒ์ˆ˜, ์ข‹์•„์š”, ์ฃผ๋ฌธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ณ„์‚ฐ๋œ ์ƒํ’ˆ ์ˆœ์œ„๊ฐ€ ์ƒํ’ˆ ์‘๋‹ต์— ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.
    • ๋žญํ‚น API ์ถ”๊ฐ€: /api/v1/rankings ์—”๋“œํฌ์ธํŠธ๋ฅผ ํ†ตํ•ด ๋‚ ์งœ๋ณ„ ์ƒํ’ˆ ์ˆœ์œ„๋ฅผ ํŽ˜์ด์ง€๋„ค์ด์…˜๊ณผ ํ•จ๊ป˜ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โœ๏ธ Tip: You can customize this high-level summary in your review settings.

* feat: zset ๋ชจ๋“ˆ ์ถ”๊ฐ€

zset

* test: ๋žญํ‚น ๊ณ„์‚ฐ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€

* feat: ๋žญํ‚น ๊ณ„์‚ฐ ์„œ๋น„์Šค ๊ตฌํ˜„

* test: ๋žญํ‚น ์ด๋ฒคํŠธ ์ปจ์Šˆ๋จธ ํ…Œ์ŠคํŠธ ๋กœ์ง ์ถ”๊ฐ€

* feat: ๋žญํ‚น ์ปจ์Šˆ๋จธ ๊ตฌํ˜„

* test: ๋žญํ‚น ์กฐํšŒ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€

* feat: ๋žญํ‚น ์กฐํšŒ ์„œ๋น„์Šค ๋กœ์ง ๊ตฌํ˜„

* feat: ๋žญํ‚น ์กฐํšŒ ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€

* test: ๋žญํ‚น ์ •๋ณด ํฌํ•จํ•˜์—ฌ ์ƒํ’ˆ ์กฐํšŒํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ

* feat: ๋žญํ‚น ํฌํ•จํ•˜์—ฌ ์ƒํ’ˆ ์ •๋ณด ์กฐํšŒํ•˜๋„๋ก api ์ˆ˜์ •

---------

Co-authored-by: แ„‹แ…ตแ„€แ…ฅแ†ซแ„‹แ…งแ†ผ <>
@coderabbitai
Copy link

coderabbitai bot commented Dec 23, 2025

Walkthrough

์ƒํ’ˆ์— ๋žญํ‚น ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ๋กœ, ์บ์‹œ์—์„œ ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•œ ํ›„ ๋Ÿฐํƒ€์ž„์— Redis ZSET์—์„œ ๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ„๋„๋กœ ์กฐํšŒํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ตฌ์กฐ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์นดํ”„์นด ์ด๋ฒคํŠธ(์ข‹์•„์š”, ์ฃผ๋ฌธ, ์กฐํšŒ)๋ฅผ ์†Œ๋น„ํ•˜์—ฌ Redis์˜ ์ •๋ ฌ๋œ ์ง‘ํ•ฉ์— ๋žญํ‚น ์ ์ˆ˜๋ฅผ ๋ˆ„์ ํ•ฉ๋‹ˆ๋‹ค.

Changes

Cohort / File(s) ๋ณ€๊ฒฝ ์š”์•ฝ
ProductInfo ๋ฐ DTO ํ™•์žฅ
apps/commerce-api/src/main/java/com/loopers/application/catalog/ProductInfo.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/ProductV1Dto.java
ProductInfo ๋ ˆ์ฝ”๋“œ์— rank ํ•„๋“œ ์ถ”๊ฐ€; withoutRank(), withRank() ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ๋„์ž…. API ์‘๋‹ต DTO์ธ ProductResponse์—๋„ rank ํ•„๋“œ ์ถ”๊ฐ€
์บ์‹œ ๋ฐ ์กฐํšŒ ๋กœ์ง ๊ฐœ์„ 
apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java, apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java
CatalogFacade์— RankingService ์ฃผ์ž…; ์บ์‹œ๋Š” ๋žญํ‚น ์—†์ด ์ €์žฅํ•˜๋˜ ๋ฐ˜ํ™˜ ์‹œ์ ์— ๋Ÿฐํƒ€์ž„ ๋žญํ‚น ์กฐํšŒ. ProductCacheService.applyLikeCountDelta()์—์„œ withoutRank() ์‚ฌ์šฉ
Redis ๋žญํ‚น ํ•ต์‹ฌ ๋กœ์ง
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingKeyGenerator.java
API ๊ณ„์ธต์—์„œ RankingService๋กœ Redis ZSET์—์„œ ์ƒ์œ„ ๋žญํ‚น ์กฐํšŒ ๋ฐ ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด ํ†ตํ•ฉ, RankingKeyGenerator๋กœ ์ผ์ผ/์‹œ๊ฐ„๋ณ„ Redis ํ‚ค ์ƒ์„ฑ
Redis ๋žญํ‚น API ์—”๋“œํฌ์ธํŠธ
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java
์ƒˆ๋กœ์šด /api/v1/rankings ์—”๋“œํฌ์ธํŠธ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜๊ณผ ๋‚ ์งœ ํ•„ํ„ฐ๋ง ์ง€์›; ์‘๋‹ต DTO ๊ตฌ์กฐ๋Š” RankingItemResponse, RankingsResponse
์ŠคํŠธ๋ฆฌ๋จธ ๋žญํ‚น ์„œ๋น„์Šค
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingService.java, apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingKeyGenerator.java
์ŠคํŠธ๋ฆฌ๋จธ ๊ณ„์ธต์˜ RankingService๋Š” ์กฐํšŒ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ Redis ZSET์— ๋ˆ„์ ; ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋ฐ TTL ๊ด€๋ฆฌ
Kafka ์ด๋ฒคํŠธ ์†Œ๋น„ ๋ฐ ๋žญํ‚น ์—…๋ฐ์ดํŠธ
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/RankingConsumer.java
์ข‹์•„์š”, ์ฃผ๋ฌธ, ์ƒํ’ˆ ์กฐํšŒ ์นดํ”„์นด ํ† ํ”ฝ ์†Œ๋น„; ์ด๋ฒคํŠธ ID ๊ธฐ๋ฐ˜ ๋ฉฑ๋“ฑ์„ฑ ๊ฒ€์ฆ; ๊ฐ ์ด๋ฒคํŠธ ํƒ€์ž…๋ณ„ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ RankingService ํ˜ธ์ถœ
Redis ZSET ์ถ”์ƒํ™” ๊ณ„์ธต
modules/redis/src/main/java/com/loopers/zset/RedisZSetTemplate.java, modules/redis/src/main/java/com/loopers/zset/ZSetEntry.java
RedisZSetTemplate์œผ๋กœ ZSET ์ ์ˆ˜ ์ฆ๊ฐ€, TTL ์„ค์ •, ๋žญํฌ ์กฐํšŒ, ์ƒ์œ„ ๋žญํ‚น ์กฐํšŒ, ํฌ๊ธฐ ์กฐํšŒ ๊ธฐ๋Šฅ ์ œ๊ณต; ZSetEntry ๋ ˆ์ฝ”๋“œ๋กœ ๋ฉค๋ฒ„์™€ ์ ์ˆ˜ ํ‘œํ˜„
API ๊ณ„์ธต ํ…Œ์ŠคํŠธ
apps/commerce-api/src/test/java/com/loopers/application/catalog/CatalogFacadeTest.java, apps/commerce-api/src/test/java/com/loopers/application/ranking/RankingServiceTest.java
CatalogFacade ์บ์‹œ ํžˆํŠธ/๋ฏธ์Šค ์‹œ ๋žญํ‚น ํ†ตํ•ฉ ๊ฒ€์ฆ; RankingService ์ƒ์œ„ ๋žญํ‚น ์กฐํšŒ, ํŽ˜์ด์ง•, ๋นˆ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ๋“ฑ ๊ฒ€์ฆ
์ŠคํŠธ๋ฆฌ๋จธ ๊ณ„์ธต ํ…Œ์ŠคํŠธ
apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingServiceTest.java, apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/RankingConsumerTest.java
์ŠคํŠธ๋ฆฌ๋จธ RankingService์˜ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ, TTL ๊ฒ€์ฆ; RankingConsumer์˜ ์ด๋ฒคํŠธ ์†Œ๋น„, ๋ฉฑ๋“ฑ์„ฑ, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ฒ€์ฆ

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as RankingV1Controller
    participant CatalogFacade
    participant Cache as ProductCacheService
    participant RankingAPI as RankingService<br/>(API)
    participant Redis as RedisZSetTemplate
    participant ProductService
    
    Client->>API: GET /api/v1/products/{id}
    API->>CatalogFacade: getProduct(productId)
    
    alt Cache Hit
        CatalogFacade->>Cache: getCachedProductDetail(productId)
        Cache-->>CatalogFacade: ProductDetail (without rank)
        CatalogFacade->>RankingAPI: getProductRank(productId, today)
        RankingAPI->>Redis: getRank(daily_key, productId)
        Redis-->>RankingAPI: Long rank
        RankingAPI-->>CatalogFacade: Long rank
        CatalogFacade-->>API: ProductInfo(detail, rank)
    else Cache Miss
        CatalogFacade->>ProductService: getProduct(productId)
        ProductService-->>CatalogFacade: Product
        CatalogFacade->>Cache: cacheProductDetail(ProductDetail without rank)
        CatalogFacade->>RankingAPI: getProductRank(productId, today)
        RankingAPI->>Redis: getRank(daily_key, productId)
        Redis-->>RankingAPI: Long rank
        RankingAPI-->>CatalogFacade: Long rank
        CatalogFacade-->>API: ProductInfo(detail, rank)
    end
    
    API-->>Client: ApiResponse<ProductResponse>
Loading
sequenceDiagram
    participant Kafka as Kafka Topics
    participant Consumer as RankingConsumer
    participant EventService as EventHandledService
    participant RankingService
    participant Redis as RedisZSetTemplate
    
    Kafka->>Consumer: ConsumerRecord (Like/Order/View Event)
    Consumer->>EventService: isAlreadyHandled(eventId)
    
    alt Not Yet Handled
        alt Like Event
            Consumer->>RankingService: addLikeScore(productId, date, isAdded)
        else Order Event
            Consumer->>RankingService: addOrderScore(productId, date, amount)
        else Product Event
            Consumer->>RankingService: addViewScore(productId, date)
        end
        
        RankingService->>Redis: incrementScore(key, productId, score)
        Redis-->>RankingService: โœ“
        RankingService->>Redis: setTtlIfNotExists(key, 2days)
        Redis-->>RankingService: โœ“
        
        Consumer->>EventService: markAsHandled(eventId)
        EventService-->>Consumer: โœ“
    else Already Handled
        Note over Consumer: Skip processing
    end
    
    Consumer-->>Kafka: acknowledge batch
Loading

Estimated code review effort

๐ŸŽฏ 4 (Complex) | โฑ๏ธ ~45 minutes

Possibly related PRs

  • [volume-5] ์ธ๋ฑ์Šค ๋ฐ Redis ์บ์‹œ ์ ์šฉย #140: ์ƒํ’ˆ์˜ ์ข‹์•„์š” ๊ฐœ์ˆ˜์™€ ์บ์‹ฑ/Redis ๋™์ž‘์„ ์ˆ˜์ •ํ–ˆ์œผ๋ฉฐ, ๋ณธ PR์ด ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜์—ฌ ProductInfo, ์บ์‹œ ์ฒ˜๋ฆฌ, ๋žญํ‚น ๊ด€๋ จ Redis ZSET ์‚ฌ์šฉ๋ฒ•์„ ํ™•์žฅํ•ฉ๋‹ˆ๋‹ค.

Suggested labels

enhancement

Poem

๐Ÿฐ ํ† ๋ผ๊ฐ€ ๋…ธ๋ž˜ํ•˜๋Š” ๋žญํ‚น ์ถ•๊ฐ€

Redis ์ ์ˆ˜ ์Œ“์ด๊ณ , ๋žญํ‚น ์ˆœ์œ„ ์ •๋ ฌ๋˜๋‹ˆ,
์ƒํ’ˆ๋“ค ๊ฒฝ์Ÿํ•˜๋ฉฐ ์ตœ๊ณ  ์ž๋ฆฌ ์ฐจ์ง€ํ•˜๋„ค.
์ข‹์•„์š”, ์ฃผ๋ฌธ, ์กฐํšŒ๊ฐ€ ์นดํ”„์นด๋ฅผ ํƒ€๊ณ ,
๋น ๋ฅด๊ฒŒ ZSET์— ๋ชจ์—ฌ ๋“ฑ์ˆ˜ ๋งค๊ธฐ๊ณ  ๋‚˜ํƒ€๋‚˜,
๋งจ ์œ„๋ถ€ํ„ฐ ํŽผ์ณ์ง„ ์˜๊ด‘์˜ ๋ชฉ๋ก์„! ๐Ÿ†โœจ

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 54.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check โ“ Inconclusive PR ์ œ๋ชฉ '[volume - 9] What is Popularity?'์€ ๋ณ€๊ฒฝ์‚ฌํ•ญ์˜ ํ•ต์‹ฌ์ธ ๋žญํ‚น ๊ธฐ๋Šฅ ์ถ”๊ฐ€๋ฅผ ๊ฐ„์ ‘์ ์œผ๋กœ๋งŒ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ๊ตฌ์ฒด์ ์ด๊ณ  ๋ช…ํ™•ํ•œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ์ œ๋ชฉ์„ 'Add product ranking feature with Redis-backed ZSET scoring' ๊ฐ™์€ ๊ตฌ์ฒด์ ์ธ ํ‘œํ˜„์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ณ€๊ฒฝ์‚ฌํ•ญ์˜ ์ฃผ์š” ๋‚ด์šฉ์„ ๋ช…ํ™•ํžˆ ๋“œ๋Ÿฌ๋‚ด๋„๋ก ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.
โœ… Passed checks (1 passed)
Check name Status Explanation
Description check โœ… Passed PR ์„ค๋ช…์ด ํ…œํ”Œ๋ฆฟ์˜ ๋ชจ๋“  ํ•„์ˆ˜ ์„น์…˜(๐Ÿ“Œ Summary, ๐Ÿ’ฌ Review Points, โœ… Checklist)์„ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๋‚ด์šฉ์ด ๊ตฌ์ฒด์ ์ด๊ณ  ์ƒ์„ธํ•ฉ๋‹ˆ๋‹ค.
โœจ Finishing touches
  • ๐Ÿ“ Generate docstrings
๐Ÿงช Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

โค๏ธ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@minor7295
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 23, 2025

โœ… Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

โ™ป๏ธ Duplicate comments (1)
apps/commerce-api/src/test/java/com/loopers/application/ranking/RankingServiceTest.java (1)

48-69: ๋ฆฌํ”Œ๋ ‰์…˜ ํ—ฌํผ ๋ฉ”์„œ๋“œ๊ฐ€ CatalogFacadeTest์™€ ์ค‘๋ณต๋ฉ๋‹ˆ๋‹ค.

์•ž์„œ ์–ธ๊ธ‰ํ•œ ๋Œ€๋กœ, ๊ณตํ†ต ํ…Œ์ŠคํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค๋กœ ์ถ”์ถœ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

๐Ÿงน Nitpick comments (15)
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingKeyGenerator.java (1)

34-37: null ์ž…๋ ฅ์— ๋Œ€ํ•œ ๋ฐฉ์–ด ์ฝ”๋“œ ๊ณ ๋ ค

date ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ null์ธ ๊ฒฝ์šฐ NullPointerException์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ํ˜ธ์ถœ์ž๊ฐ€ ํ•ญ์ƒ non-null์„ ๋ณด์žฅํ•œ๋‹ค๋ฉด ๊ดœ์ฐฎ์ง€๋งŒ, ๋ฐฉ์–ด์  ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ด€์ ์—์„œ null ์ฒดํฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ @NonNull ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”.

apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java (2)

128-132: ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ์œ„ํ•œ ์‹œ๊ฐ„ ์ถ”์ƒํ™” ๊ณ ๋ ค

LocalDate.now()๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ๋‚ ์งœ๋ฅผ ์ œ์–ดํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. Clock์„ ์ฃผ์ž…๋ฐ›๊ฑฐ๋‚˜, ๋ณ„๋„์˜ ๋‚ ์งœ ์ œ๊ณต์ž๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ…Œ์ŠคํŠธ ์‹œ ์‹œ๊ฐ„์„ ๊ณ ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž Clock ์ฃผ์ž… ์˜ˆ์‹œ
// ํ•„๋“œ ์ถ”๊ฐ€
private final Clock clock;

// ์ƒ์„ฑ์ž์—์„œ ์ฃผ์ž… (๋˜๋Š” @Value๋กœ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •)

// ์‚ฌ์šฉ ์‹œ
LocalDate today = LocalDate.now(clock);

147-159: ์บ์‹œ ๋ฏธ์Šค ์‹œ ํ๋ฆ„ ๊ฐœ์„  ๊ฐ€๋Šฅ

ํ˜„์žฌ ํ๋ฆ„์—์„œ ProductInfo.withoutRank(productDetail)์„ ๋‘ ๋ฒˆ ์ƒ์„ฑํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค (Line 152, 158). ํ•œ ๋ฒˆ๋งŒ ์ƒ์„ฑํ•˜์—ฌ ์žฌ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ๋” ๋ช…ํ™•ํ•ด์ง‘๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฐœ์„  ์ œ์•ˆ
         // ๋žญํ‚น ์ •๋ณด ์กฐํšŒ
         LocalDate today = LocalDate.now();
         Long rank = rankingService.getProductRank(productId, today);
         
-        // ์บ์‹œ์— ์ €์žฅ (๋žญํ‚น ์ •๋ณด๋Š” ์ œ์™ธํ•˜๊ณ  ์ €์žฅ - ๋žญํ‚น์€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์กฐํšŒ)
-        productCacheService.cacheProduct(productId, ProductInfo.withoutRank(productDetail));
+        // ์บ์‹œ์— ์ €์žฅ (๋žญํ‚น ์ •๋ณด๋Š” ์ œ์™ธํ•˜๊ณ  ์ €์žฅ - ๋žญํ‚น์€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์กฐํšŒ)
+        ProductInfo productInfoWithoutRank = ProductInfo.withoutRank(productDetail);
+        productCacheService.cacheProduct(productId, productInfoWithoutRank);
         
         // โœ… ์ƒํ’ˆ ์กฐํšŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (๋ฉ”ํŠธ๋ฆญ ์ง‘๊ณ„์šฉ)
         productEventPublisher.publish(ProductEvent.ProductViewed.from(productId));
         
         // ๋กœ์ปฌ ์บ์‹œ์˜ ์ข‹์•„์š” ์ˆ˜ ๋ธํƒ€ ์ ์šฉ (DB ์กฐํšŒ ๊ฒฐ๊ณผ์—๋„ ๋ธํƒ€ ๋ฐ˜์˜)
-        ProductInfo deltaApplied = productCacheService.applyLikeCountDelta(ProductInfo.withoutRank(productDetail));
+        ProductInfo deltaApplied = productCacheService.applyLikeCountDelta(productInfoWithoutRank);
         return ProductInfo.withRank(deltaApplied.productDetail(), rank);
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (3)

6-6: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” import ์ œ๊ฑฐ

@DateTimeFormat ์–ด๋…ธํ…Œ์ด์…˜์ด import๋˜์–ด ์žˆ์ง€๋งŒ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ
-import org.springframework.format.annotation.DateTimeFormat;

82-87: ์ž˜๋ชป๋œ ๋‚ ์งœ ํ˜•์‹์— ๋Œ€ํ•œ ํ”ผ๋“œ๋ฐฑ ๋ถ€์žฌ

๋‚ ์งœ ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ๋ฅผ ๋ฌด์‹œํ•˜๊ณ  ์˜ค๋Š˜ ๋‚ ์งœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ž˜๋ชป๋œ ํ˜•์‹์„ ์ „๋‹ฌํ–ˆ์„ ๋•Œ ์ด๋ฅผ ์ธ์ง€ํ•˜์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊น…์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ๋ช…์‹œ์ ์œผ๋กœ 400 Bad Request๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

๐Ÿ”Ž ๋กœ๊น… ์ถ”๊ฐ€ ์˜ˆ์‹œ
         try {
             return LocalDate.parse(dateStr, DATE_FORMATTER);
         } catch (DateTimeParseException e) {
-            // ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์˜ค๋Š˜ ๋‚ ์งœ ๋ฐ˜ํ™˜
+            log.warn("๋‚ ์งœ ํŒŒ์‹ฑ ์‹คํŒจ, ์˜ค๋Š˜ ๋‚ ์งœ ์‚ฌ์šฉ: input={}", dateStr);
             return LocalDate.now();
         }

54-62: ๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ ์žฌํ• ๋‹น ์ง€์–‘

page์™€ size ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ ์žฌํ• ๋‹นํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์„ ์ €ํ•˜์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณ„๋„์˜ ์ง€์—ญ ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฐœ์„  ์ œ์•ˆ
-        // ํŽ˜์ด์ง• ๊ฒ€์ฆ
-        if (page < 0) {
-            page = 0;
-        }
-        if (size < 1) {
-            size = 20;
-        }
-        if (size > 100) {
-            size = 100; // ์ตœ๋Œ€ 100๊ฐœ๋กœ ์ œํ•œ
-        }
+        // ํŽ˜์ด์ง• ๊ฒ€์ฆ
+        int validPage = Math.max(page, 0);
+        int validSize = Math.max(1, Math.min(size, 100));

-        RankingService.RankingsResponse result = rankingService.getRankings(targetDate, page, size);
+        RankingService.RankingsResponse result = rankingService.getRankings(targetDate, validPage, validSize);
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingService.java (1)

133-137: ๊ฐœ๋ณ„ ํ˜ธ์ถœ๋งˆ๋‹ค TTL ์„ค์ • ํ˜ธ์ถœ ์ตœ์ ํ™” ๊ณ ๋ ค

incrementScore๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ๋งˆ๋‹ค setTtlIfNotExists๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. setTtlIfNotExists๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ TTL์ด ์—†์„ ๋•Œ๋งŒ ์„ค์ •ํ•˜์ง€๋งŒ, ๋งค๋ฒˆ Redis ํ˜ธ์ถœ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณ ๋นˆ๋„ ํ˜ธ์ถœ ์‹œ ๋ถˆํ•„์š”ํ•œ ๋„คํŠธ์›Œํฌ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

addScoresBatch์ฒ˜๋Ÿผ ๋ฐฐ์น˜ ๋‹จ์œ„๋กœ TTL์„ ์„ค์ •ํ•˜๊ฑฐ๋‚˜, ํ‚ค๋ณ„๋กœ ๋กœ์ปฌ์—์„œ TTL ์„ค์ • ์—ฌ๋ถ€๋ฅผ ์ถ”์ ํ•˜๋Š” ๋ฐฉ์‹์„ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”.

apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingKeyGenerator.java (1)

18-51: ์ฝ”๋“œ ์ค‘๋ณต: commerce-streamer ๋ชจ๋“ˆ๊ณผ ๋™์ผํ•œ RankingKeyGenerator ํด๋ž˜์Šค๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingKeyGenerator.java์— ๋™์ผํ•œ ๋กœ์ง์˜ ํด๋ž˜์Šค๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‘ ๋ชจ๋“ˆ ๋ชจ๋‘์—์„œ ๋™์ผํ•œ ํ‚ค ์ƒ์„ฑ ๋กœ์ง์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ๊ณต์œ  ๋ชจ๋“ˆ(์˜ˆ: modules/redis ๋˜๋Š” ๋ณ„๋„์˜ modules/ranking-common)๋กœ ์ถ”์ถœํ•˜์—ฌ ์ค‘๋ณต์„ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„ ์ž์ฒด๋Š” ์˜ฌ๋ฐ”๋ฅด๋ฉฐ, DateTimeFormatter๋ฅผ static final๋กœ ์žฌ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์ข‹์€ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (1)

82-93: Collectors.toMap์—์„œ ์ค‘๋ณต ํ‚ค ๋ฐœ์ƒ ์‹œ ์˜ˆ์™ธ ๊ฐ€๋Šฅ์„ฑ

Collectors.toMap์€ ์ค‘๋ณต ํ‚ค๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ IllegalStateException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. Redis ZSET์—์„œ ๋ฐ˜ํ™˜๋œ productIds์— ์ค‘๋ณต์ด ์—†๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์ง€๋งŒ, productService.getProducts()๊ฐ€ ์ค‘๋ณต๋œ ID๋ฅผ ๊ฐ€์ง„ Product๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ค‘๋ณต ํ‚ค ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ˆ˜์ • ์ œ์•ˆ
 Map<Long, Product> productMap = products.stream()
-    .collect(Collectors.toMap(Product::getId, product -> product));
+    .collect(Collectors.toMap(Product::getId, product -> product, (existing, replacement) -> existing));

 Map<Long, Brand> brandMap = brandService.getBrands(brandIds).stream()
-    .collect(Collectors.toMap(Brand::getId, brand -> brand));
+    .collect(Collectors.toMap(Brand::getId, brand -> brand, (existing, replacement) -> existing));
apps/commerce-api/src/test/java/com/loopers/application/catalog/CatalogFacadeTest.java (1)

67-88: ๋ฆฌํ”Œ๋ ‰์…˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฉ”์„œ๋“œ ์ค‘๋ณต

setId(Product, Long)์™€ setId(Brand, Long) ๋ฉ”์„œ๋“œ๊ฐ€ RankingServiceTest์—๋„ ๋™์ผํ•˜๊ฒŒ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค๋กœ ์ถ”์ถœํ•˜๋ฉด ์ฝ”๋“œ ์ค‘๋ณต์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ lines 157-163๊ณผ 199-206์—์„œ likeCount ์„ค์ •์„ ์œ„ํ•œ ๋ฆฌํ”Œ๋ ‰์…˜ ์ฝ”๋“œ๋„ ์ค‘๋ณต๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

modules/redis/src/main/java/com/loopers/zset/RedisZSetTemplate.java (2)

59-69: TTL ์กฐ๊ฑด ๊ฒ€์‚ฌ์—์„œ ํ‚ค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ๋ˆ„๋ฝ

getExpire()๋Š” ํ‚ค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์„ ๋•Œ -2๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์กฐ๊ฑด currentTtl == null || currentTtl == -1์€ ์ด ๊ฒฝ์šฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‹จ, ZSET์— incrementScore๊ฐ€ ๋จผ์ € ํ˜ธ์ถœ๋œ ํ›„ setTtlIfNotExists๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ์ˆœ์„œ๋ผ๋ฉด ํ‚ค๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•˜๋ฏ€๋กœ ๋ฌธ์ œ์—†์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋” ๋ช…ํ™•ํ•œ ์กฐ๊ฑด ์ฒ˜๋ฆฌ ์ œ์•ˆ
 public void setTtlIfNotExists(String key, Duration ttl) {
     try {
         Long currentTtl = redisTemplate.getExpire(key);
-        if (currentTtl == null || currentTtl == -1) {
+        if (currentTtl == null || currentTtl < 0) {
             // TTL์ด ์—†๊ฑฐ๋‚˜ -1(๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์—†์Œ)์ธ ๊ฒฝ์šฐ์—๋งŒ ์„ค์ •
+            // -2๋Š” ํ‚ค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ
             redisTemplate.expire(key, ttl);
         }
     } catch (Exception e) {
         log.warn("ZSET TTL ์„ค์ • ์‹คํŒจ: key={}", key, e);
     }
 }

41-48: Silent failure ๋™์ž‘์— ๋Œ€ํ•œ ๋ฌธ์„œํ™” ๊ณ ๋ ค

incrementScore์—์„œ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ๊ณ„์† ์ง„ํ–‰ํ•˜๋Š” ๊ฒƒ์€ ์˜๋„๋œ ์„ค๊ณ„์ด์ง€๋งŒ, ์ด๋กœ ์ธํ•ด ๋ฐ์ดํ„ฐ ์œ ์‹ค์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ์—์„œ ์ด ๋™์ž‘์„ ์ธ์ง€ํ•  ์ˆ˜ ์žˆ๋„๋ก Javadoc์— ์ด ๋™์ž‘์„ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/RankingConsumerTest.java (1)

185-222: ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹ค์ œ ๋ฐฐ์น˜ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์™„์ „ํžˆ ๋ฐ˜์˜ํ•˜์ง€ ์•Š์Œ

canConsumeMultipleEvents ํ…Œ์ŠคํŠธ๋Š” consumeLikeEvents์™€ consumeProductEvents๋ฅผ ๋ณ„๋„๋กœ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ๋‚˜๋ฆฌ์˜ค(๋‹จ์ผ consumer ํ˜ธ์ถœ์—์„œ ์—ฌ๋Ÿฌ ๋ ˆ์ฝ”๋“œ ์ฒ˜๋ฆฌ)๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด ๋™์ผํ•œ ์ด๋ฒคํŠธ ํƒ€์ž…์˜ ์—ฌ๋Ÿฌ ๋ ˆ์ฝ”๋“œ๋ฅผ ํ•˜๋‚˜์˜ ๋ฆฌ์ŠคํŠธ๋กœ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ํ…Œ์ŠคํŠธ๋Š” ์—ฌ์ „ํžˆ ์œ ํšจํ•˜์ง€๋งŒ, ํ…Œ์ŠคํŠธ ์ด๋ฆ„๊ณผ ์˜๋„๊ฐ€ ์ผ์น˜ํ•˜๋„๋ก ์กฐ์ •ํ•˜๊ฑฐ๋‚˜ ์‹ค์ œ ๋ฐฐ์น˜ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/RankingConsumer.java (2)

320-344: Like ์ด๋ฒคํŠธ ํŒŒ์‹ฑ ๋กœ์ง์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Lines 323, 339์˜ ํŒŒ์‹ฑ ๋กœ์ง์ด ๋น„ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค:

  • ์ด๋ฏธ ํƒ€์ž…์ด ์ง€์ •๋œ ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ์—๋„ JSON์œผ๋กœ ์ง๋ ฌํ™” ํ›„ ๋‹ค์‹œ ์—ญ์ง๋ ฌํ™”ํ•˜๋Š” ๋ถˆํ•„์š”ํ•œ ๊ณผ์ •์„ ๊ฑฐ์นฉ๋‹ˆ๋‹ค.
  • parseOrderCreatedEvent์™€ parseProductViewedEvent๋Š” instanceof ์ฒดํฌ๋ฅผ ๋จผ์ € ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐ˜๋ฉด, ์ด ๋‘ ๋ฉ”์„œ๋“œ๋Š” ๊ทธ๋ ‡์ง€ ์•Š์•„ ์ผ๊ด€์„ฑ์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.
๐Ÿ”Ž ๊ฐœ์„ ๋œ ํŒŒ์‹ฑ ๋กœ์ง ์ œ์•ˆ
 private LikeEvent.LikeAdded parseLikeEvent(Object value) {
     try {
+        if (value instanceof LikeEvent.LikeAdded) {
+            return (LikeEvent.LikeAdded) value;
+        }
+        
         // JSON ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ ํŒŒ์‹ฑ
         String json = value instanceof String ? (String) value : objectMapper.writeValueAsString(value);
         return objectMapper.readValue(json, LikeEvent.LikeAdded.class);
     } catch (Exception e) {
         throw new RuntimeException("LikeAdded ์ด๋ฒคํŠธ ํŒŒ์‹ฑ ์‹คํŒจ", e);
     }
 }

 private LikeEvent.LikeRemoved parseLikeRemovedEvent(Object value) {
     try {
+        if (value instanceof LikeEvent.LikeRemoved) {
+            return (LikeEvent.LikeRemoved) value;
+        }
+        
         // JSON ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ ํŒŒ์‹ฑ
         String json = value instanceof String ? (String) value : objectMapper.writeValueAsString(value);
         return objectMapper.readValue(json, LikeEvent.LikeRemoved.class);
     } catch (Exception e) {
         throw new RuntimeException("LikeRemoved ์ด๋ฒคํŠธ ํŒŒ์‹ฑ ์‹คํŒจ", e);
     }
 }

420-433: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.

extractVersion ๋ฉ”์„œ๋“œ๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์ง€๋งŒ ์ฝ”๋“œ ์–ด๋””์—์„œ๋„ ํ˜ธ์ถœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ–ฅํ›„ ์‚ฌ์šฉ ๊ณ„ํš์ด ์žˆ๋‹ค๋ฉด ์œ ์ง€ํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด ์ œ๊ฑฐ๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”.

#!/bin/bash
# extractVersion ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ์ฒ˜ ํ™•์ธ
rg -n "extractVersion" --type=java
๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 3af88f9 and 22bb0a9.

๐Ÿ“’ Files selected for processing (17)
  • apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/catalog/ProductInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingKeyGenerator.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/ProductV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java
  • apps/commerce-api/src/test/java/com/loopers/application/catalog/CatalogFacadeTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/ranking/RankingServiceTest.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingKeyGenerator.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingService.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/RankingConsumer.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingServiceTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/RankingConsumerTest.java
  • modules/redis/src/main/java/com/loopers/zset/RedisZSetTemplate.java
  • modules/redis/src/main/java/com/loopers/zset/ZSetEntry.java
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (7)
๐Ÿ““ Common learnings
Learnt from: junoade
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 0
File: :0-0
Timestamp: 2025-12-02T08:12:06.383Z
Learning: ProductQueryService์—์„œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ์‹œ Redis ์บ์‹œ๋ฅผ ์ ์šฉํ–ˆ์œผ๋ฉฐ, ์บ์‹œ ํ‚ค๋Š” brandId, sortType, pageNumber, pageSize์˜ ์กฐํ•ฉ์œผ๋กœ ๊ตฌ์„ฑ๋˜๊ณ  TTL์€ 5๋ถ„์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ๋‹ค.
๐Ÿ“š Learning: 2025-12-19T21:30:08.018Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/outbox/OutboxEventListener.kt:0-0
Timestamp: 2025-12-19T21:30:08.018Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template Kafka event pipeline, Like events (LikeCreatedEventV1, LikeCanceledEventV1) intentionally use aggregateType="Like" with aggregateId=productId. The aggregateId serves as a partitioning/grouping key (not a unique Like entity identifier), ensuring all like events for the same product go to the same partition for ordering guarantees and aligning with ProductStatisticService's product-based aggregation logic. Using individual like_id would scatter events across partitions and break the statistics aggregation pattern.

Applied to files:

  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/RankingConsumerTest.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/RankingConsumer.java
๐Ÿ“š Learning: 2025-12-19T20:59:50.009Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: docs/week8/round8-detailed-design.md:151-178
Timestamp: 2025-12-19T20:59:50.009Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository's Kafka event pipeline, only 5 domain events are intentionally published to Kafka via CloudEventEnvelopeFactory: OrderPaidEventV1, LikeCreatedEventV1, LikeCanceledEventV1, ProductViewedEventV1, and StockDepletedEventV1. Other domain events (OrderCreatedEventV1, OrderCanceledEventV1, PaymentCreatedEventV1, PaymentPaidEventV1, PaymentFailedEventV1) are internal-only and intentionally not mapped in resolveMetadata(), which correctly returns null for them to exclude them from Outbox publication.

Applied to files:

  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/RankingConsumer.java
๐Ÿ“š Learning: 2025-12-18T13:24:51.650Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:51.650Z
Learning: Adopt centralized error handling for Kafka consumers by using the EventInboxAspect to intercept methods annotated with InboxEvent. Ensure that service methods annotated with InboxEvent save failed EventInbox entries and log errors, avoiding duplicating error handling logic in individual consumers. Apply this pattern broadly to similar consumer/service layers within the commerce-streamer module.

Applied to files:

  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/RankingConsumer.java
๐Ÿ“š Learning: 2025-12-19T09:30:12.459Z
Learnt from: HongChangMo
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 198
File: apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventListener.java:0-0
Timestamp: 2025-12-19T09:30:12.459Z
Learning: In the loopers-spring-java-template repository's commerce-api module, when entities are managed within a transactional context (e.g., Transactional methods), prefer relying on JPA dirty checking for automatic persistence rather than explicit save() calls. Both Payment and Order entities in PaymentEventListener use this pattern, with state changes automatically flushed on transaction commit.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java
๐Ÿ“š Learning: 2025-12-02T08:12:06.383Z
Learnt from: junoade
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 0
File: :0-0
Timestamp: 2025-12-02T08:12:06.383Z
Learning: ProductQueryService์—์„œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ์‹œ Redis ์บ์‹œ๋ฅผ ์ ์šฉํ–ˆ์œผ๋ฉฐ, ์บ์‹œ ํ‚ค๋Š” brandId, sortType, pageNumber, pageSize์˜ ์กฐํ•ฉ์œผ๋กœ ๊ตฌ์„ฑ๋˜๊ณ  TTL์€ 5๋ถ„์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ๋‹ค.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java
๐Ÿ“š Learning: 2025-11-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product ์—”ํ‹ฐํ‹ฐ (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)๋Š” ์œ ์ฆˆ์ผ€์ด์Šค๋ณ„๋กœ ์˜๋„์ ์œผ๋กœ ๋‹ค๋ฅธ ๋ฝ ์ „๋žต์„ ์‚ฌ์šฉํ•œ๋‹ค: ์ข‹์•„์š” ๊ธฐ๋Šฅ์—๋Š” ๋น„๊ด€์  ๋ฝ(findByIdForUpdate)์„, ์žฌ๊ณ  ์ฐจ๊ฐ์—๋Š” ๋‚™๊ด€์  ๋ฝ(Version + ์žฌ์‹œ๋„)์„ ์‚ฌ์šฉํ•œ๋‹ค.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java
๐Ÿงฌ Code graph analysis (3)
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/RankingConsumerTest.java (2)
apps/commerce-api/src/test/java/com/loopers/application/ranking/RankingServiceTest.java (1)
  • ExtendWith (27-400)
apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingServiceTest.java (1)
  • ExtendWith (25-247)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/RankingConsumer.java (3)
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingService.java (1)
  • Slf4j (38-138)
modules/redis/src/main/java/com/loopers/zset/RedisZSetTemplate.java (1)
  • Slf4j (24-140)
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingKeyGenerator.java (1)
  • Component (18-52)
modules/redis/src/main/java/com/loopers/zset/RedisZSetTemplate.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingKeyGenerator.java (1)
  • Component (18-52)
๐Ÿ”‡ Additional comments (20)
modules/redis/src/main/java/com/loopers/zset/ZSetEntry.java (1)

1-12: LGTM!

ZSET ์—”ํŠธ๋ฆฌ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ๊ฐ„๊ฒฐํ•œ record์ž…๋‹ˆ๋‹ค. Javadoc๋„ ์ ์ ˆํžˆ ์ž‘์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java (1)

272-272: LGTM!

withoutRank ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋žญํ‚น ์ •๋ณด ์—†์ด ProductInfo๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๋žญํ‚น ์ •๋ณด๋Š” ์‹ค์‹œ๊ฐ„์œผ๋กœ ์กฐํšŒ๋˜๋ฏ€๋กœ ์บ์‹œ๋œ ์ข‹์•„์š” ๋ธํƒ€ ์ ์šฉ ์‹œ ๋žญํ‚น์„ ํฌํ•จํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์˜ฌ๋ฐ”๋ฅธ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java (1)

96-96: ์ƒํ’ˆ ๋ชฉ๋ก์— ๋žญํ‚น ์ •๋ณด ๋ฏธํฌํ•จ ํ™•์ธ

์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ์‹œ withoutRank๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋žญํ‚น ์ •๋ณด๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์˜๋„๋œ ๋™์ž‘์ธ์ง€ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. ๋ชฉ๋ก์—์„œ๋„ ๋žญํ‚น์„ ๋ณด์—ฌ์ฃผ์–ด์•ผ ํ•œ๋‹ค๋ฉด ์ˆ˜์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/catalog/ProductInfo.java (1)

11-31: LGTM!

withoutRank์™€ withRank ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋žญํ‚น ์ •๋ณด ์œ ๋ฌด์— ๋”ฐ๋ฅธ ProductInfo ์ƒ์„ฑ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•œ ์ข‹์€ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค. record์˜ ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ํ™•์žฅ์„ฑ ์žˆ๋Š” ๊ตฌ์กฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/ProductV1Dto.java (1)

26-51: LGTM!

rank ํ•„๋“œ๊ฐ€ ProductResponse์— ์ ์ ˆํžˆ ์ถ”๊ฐ€๋˜์—ˆ์œผ๋ฉฐ, ProductInfo๋กœ๋ถ€ํ„ฐ์˜ ๋งคํ•‘๋„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Javadoc๋„ ๋ช…ํ™•ํ•˜๊ฒŒ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingService.java (2)

70-76: ์ข‹์•„์š” ์ทจ์†Œ ์‹œ ์Œ์ˆ˜ ์ ์ˆ˜ ๊ฐ€๋Šฅ์„ฑ

์ข‹์•„์š” ์ทจ์†Œ ์‹œ -LIKE_WEIGHT๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์ข‹์•„์š”๊ฐ€ ์ถ”๊ฐ€๋˜๊ธฐ ์ „์— ์ทจ์†Œ ์ด๋ฒคํŠธ๊ฐ€ ๋จผ์ € ์ฒ˜๋ฆฌ๋˜๋ฉด (์ด๋ฒคํŠธ ์ˆœ์„œ ์—ญ์ „), ํ•ด๋‹น ์ƒํ’ˆ์˜ ์ ์ˆ˜๊ฐ€ ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Eventually Consistent ์„ค๊ณ„ ์›์น™์— ๋”ฐ๋ผ ํ—ˆ์šฉ ๊ฐ€๋Šฅํ•œ ์ƒํ™ฉ์ธ์ง€ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.


38-48: LGTM!

๋žญํ‚น ์ ์ˆ˜ ๊ฐ€์ค‘์น˜์™€ TTL ์„ค์ •์ด ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. CQRS Read Model ์„ค๊ณ„ ์›์น™์— ๋”ฐ๋ฅธ ๊ตฌํ˜„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (2)

60-61: @Transactional(readOnly = true) ์–ด๋…ธํ…Œ์ด์…˜ ์žฌ๊ณ  ํ•„์š”

์ด ๋ฉ”์„œ๋“œ๋Š” ์ฃผ๋กœ Redis์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๊ณ , ProductService์™€ BrandService๋ฅผ ํ†ตํ•ด DB ์กฐํšŒ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ProductService.getProducts()์™€ BrandService.getBrands()๊ฐ€ ์ด๋ฏธ ์ž์ฒด์ ์œผ๋กœ ํŠธ๋žœ์žญ์…˜์„ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ๋‹ค๋ฉด, ์ด ๋ ˆ๋ฒจ์—์„œ ๋ณ„๋„์˜ ํŠธ๋žœ์žญ์…˜์€ ๋ถˆํ•„์š”ํ•œ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์œ„ ์„œ๋น„์Šค์˜ ํŠธ๋žœ์žญ์…˜ ์„ค์ •์„ ํ™•์ธํ•˜๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์œ ์ง€ํ•˜์„ธ์š”.


96-132: LGTM! ๋ฐฐ์น˜ ์กฐํšŒ ํŒจํ„ด์ด ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ƒํ’ˆ๊ณผ ๋ธŒ๋žœ๋“œ๋ฅผ ๋ฐฐ์น˜๋กœ ์กฐํšŒํ•˜๊ณ , ๋ˆ„๋ฝ๋œ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๋ฉฐ ์Šคํ‚ตํ•˜๋Š” ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. hasNext ๊ณ„์‚ฐ ๋กœ์ง๋„ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/application/catalog/CatalogFacadeTest.java (1)

90-229: LGTM! ์บ์‹œ ํžˆํŠธ/๋ฏธ์Šค ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋Œ€ํ•œ ๋žญํ‚น ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๊ฐ€ ํฌ๊ด„์ ์ž…๋‹ˆ๋‹ค.

4๊ฐ€์ง€ ์ฃผ์š” ์‹œ๋‚˜๋ฆฌ์˜ค(์บ์‹œ ํžˆํŠธ + ๋žญํ‚น ์žˆ์Œ/์—†์Œ, ์บ์‹œ ๋ฏธ์Šค + ๋žญํ‚น ์žˆ์Œ/์—†์Œ)๋ฅผ ๋ชจ๋‘ ์ปค๋ฒ„ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, mock ์„ค์ •๊ณผ ๊ฒ€์ฆ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)

14-94: LGTM! DTO ์„ค๊ณ„๊ฐ€ ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค.

from() ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•œ ๋„๋ฉ”์ธ-API ๋งคํ•‘์ด ๋ช…ํ™•ํ•˜๊ณ , record๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ถˆ๋ณ€ DTO๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฒƒ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/application/ranking/RankingServiceTest.java (1)

71-399: LGTM! RankingService์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ์‹œ๋‚˜๋ฆฌ์˜ค๋“ค์ด ๋ชจ๋‘ ํ…Œ์ŠคํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

  • ๊ธฐ๋ณธ ๋žญํ‚น ์กฐํšŒ ๋ฐ ํŽ˜์ด์ง•
  • ๋นˆ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ
  • ๋ˆ„๋ฝ๋œ ์ƒํ’ˆ/๋ธŒ๋žœ๋“œ ์Šคํ‚ต
  • hasNext ํ”Œ๋ž˜๊ทธ ๊ณ„์‚ฐ
  • ๋‹จ์ผ ์ƒํ’ˆ ์ˆœ์œ„ ์กฐํšŒ
  • ๋™์ผ ๋ธŒ๋žœ๋“œ ์ค‘๋ณต ์ œ๊ฑฐ

๊ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ๋ช…ํ™•ํ•œ arrange/act/assert ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingServiceTest.java (1)

24-247: LGTM! commerce-streamer ๋ชจ๋“ˆ์˜ RankingService ์ ์ˆ˜ ์—…๋ฐ์ดํŠธ ๋กœ์ง์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์กฐํšŒ/์ข‹์•„์š”/์ฃผ๋ฌธ ์ ์ˆ˜ ์ถ”๊ฐ€, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ, ๋นˆ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ, ๋‹ค์ค‘ ๋‚ ์งœ ๋…๋ฆฝ ์ฒ˜๋ฆฌ, ์ ์ˆ˜ ๋ˆ„์  ๋“ฑ ํ•ต์‹ฌ ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ๋ชจ๋‘ ์ปค๋ฒ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ArgumentCaptor๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋™์  ์ ์ˆ˜ ๊ฐ’์„ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

modules/redis/src/main/java/com/loopers/zset/RedisZSetTemplate.java (1)

102-120: LGTM! getTopRankings ๊ตฌํ˜„์ด ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

reverseRangeWithScores๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ ์ˆ˜๊ฐ€ ๋†’์€ ์ˆœ์„œ๋กœ ์ •๋ ฌ๋œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , null ์ฒดํฌ ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/RankingConsumerTest.java (2)

415-455: LGTM! ๋ฉฑ๋“ฑ์„ฑ ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋™์ผํ•œ eventId๋ฅผ ๊ฐ€์ง„ ์ค‘๋ณต ๋ฉ”์‹œ์ง€ 3๊ฐœ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ ์ฒซ ๋ฒˆ์งธ๋งŒ ์‹ค์ œ ์ฒ˜๋ฆฌ๋˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ์Šคํ‚ต๋˜๋Š” ๊ฒƒ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. isAlreadyHandled์˜ ์ˆœ์ฐจ์  ๋ฐ˜ํ™˜๊ฐ’ ์„ค์ •๊ณผ ํ˜ธ์ถœ ํšŸ์ˆ˜ ๊ฒ€์ฆ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.


1-414: LGTM! RankingConsumer์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€

๋‹ค์Œ ์‹œ๋‚˜๋ฆฌ์˜ค๋“ค์ด ์ž˜ ํ…Œ์ŠคํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

  • ๊ฐ ์ด๋ฒคํŠธ ํƒ€์ž…(LikeAdded, LikeRemoved, OrderCreated, ProductViewed) ์ฒ˜๋ฆฌ
  • ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ์ด๋ฒคํŠธ ์Šคํ‚ต
  • eventId ์—†๋Š” ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
  • ๊ฐœ๋ณ„ ์ด๋ฒคํŠธ ์‹คํŒจ ์‹œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๊ณ„์†
  • DataIntegrityViolationException ์ฒ˜๋ฆฌ (๋™์‹œ์„ฑ ์ƒํ™ฉ)
  • ์ฃผ๋ฌธ ์ด๋ฒคํŠธ์˜ ์—ฃ์ง€ ์ผ€์ด์Šค (totalQuantity=0, subtotal=null)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/RankingConsumer.java (4)

22-66: ํด๋ž˜์Šค ๊ตฌ์กฐ ๋ฐ ๋ฌธ์„œํ™”๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

JavaDoc์ด ์ƒ์„ธํ•˜๊ณ  ์˜์กด์„ฑ ์ฃผ์ž…์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ํƒ€์ž…, Manual Ack ์ „๋žต, CQRS ์„ค๊ณ„ ์›์น™์ด ๋ช…ํ™•ํ•˜๊ฒŒ ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์–ด ์ฝ”๋“œ ์ดํ•ด์— ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.


139-143: ๊ฐœ๋ณ„ ๋ ˆ์ฝ”๋“œ ์ฒ˜๋ฆฌ ์‹คํŒจ ์‹œ ๋ฐ์ดํ„ฐ ์†์‹ค ๊ฐ€๋Šฅ์„ฑ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

๊ฐœ๋ณ„ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๊ฐ€ ์‹คํŒจํ•ด๋„ ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ๊ณ„์† ์ง„ํ–‰ํ•˜๋ฉฐ, ๋ฐฐ์น˜ ์ „์ฒด๋Š” ์ปค๋ฐ‹๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” "At Most Once" ์‹œ๋งจํ‹ฑ์œผ๋กœ ์‹คํŒจํ•œ ์ด๋ฒคํŠธ๋Š” ์˜๊ตฌ์ ์œผ๋กœ ์†์‹ค๋ฉ๋‹ˆ๋‹ค. ๋žญํ‚น ๋ฐ์ดํ„ฐ์˜ ํŠน์„ฑ์ƒ ์ผ๋ถ€ ์†์‹ค์ด ํ—ˆ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด ํ˜„์žฌ ๊ตฌ์กฐ๊ฐ€ ์ ์ ˆํ•˜์ง€๋งŒ, ๋‹ค์Œ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”:

  • Dead Letter Queue(DLQ)๋กœ ์‹คํŒจํ•œ ์ด๋ฒคํŠธ๋ฅผ ๋ณ„๋„ ํ† ํ”ฝ์— ๋ฐœํ–‰ํ•˜์—ฌ ์ถ”ํ›„ ์žฌ์ฒ˜๋ฆฌ
  • ๋ฉ”ํŠธ๋ฆญ/์•Œ๋žŒ์„ ํ†ตํ•ด ์‹คํŒจ์œจ ๋ชจ๋‹ˆํ„ฐ๋ง

ํ˜„์žฌ ์„ค๊ณ„๊ฐ€ ์˜๋„์ ์ด๋ผ๋ฉด JavaDoc์— ๋ฐ์ดํ„ฐ ์†์‹ค ๊ฐ€๋Šฅ์„ฑ์„ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.


352-384: Order ๋ฐ Product ์ด๋ฒคํŠธ ํŒŒ์‹ฑ ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

ํƒ€์ž… ์ฒดํฌ๋ฅผ ๋จผ์ € ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํŒจํ„ด์„ Like ์ด๋ฒคํŠธ ํŒŒ์‹ฑ์—๋„ ์ ์šฉํ•˜๋ฉด ์ผ๊ด€์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.


392-412: ํ—ค๋” ์ถ”์ถœ ๋กœ์ง์ด ์•ˆ์ „ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

lastHeader()๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  null ์ฒดํฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ฐฉ์–ด์ ์œผ๋กœ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

* feat: zset ๋ชจ๋“ˆ์— zunionstore ์—ฐ์‚ฐ ์ฒ˜๋ฆฌ ๋ฉ”์†Œ๋“œ ์ถ”๊ฐ€

* test: ๋žญํ‚น ์ง‘๊ณ„์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘๊ณผ ๋žญํ‚น ๊ณ„์‚ฐ ๋กœ์ง์„ application event ๊ธฐ์ค€์œผ๋กœ ๋ถ„๋ฆฌํ•˜๋„๋ก ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ •

* feat: ๋žญํ‚น ์ง‘๊ณ„์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘๊ณผ ๋žญํ‚น ๊ณ„์‚ฐ ๋กœ์ง์„ application event ๊ธฐ์ค€์œผ๋กœ ๋ถ„๋ฆฌํ•˜๋„๋ก ํ•จ
* test: ๋žญํ‚น ์กฐํšŒ ์‹คํŒจํ•  ๋•Œ์˜ ํ…Œ์ŠคํŠธ์ฝ”๋“œ ์ถ”๊ฐ€

* feat: ๋žญํ‚น ์กฐํšŒ ์‹คํŒจ์‹œ ์ „๋‚  ํ˜น์€ ์ข‹์•„์š” ์ˆœ ๋ฐ์ดํ„ฐ๋กœ ์‘๋‹ตํ•˜๋„๋ก ๋ณด์™„

* feat: ๋žญํ‚น fallback ์ „๋žต ๊ตฌํ˜„

* test: ๋žญํ‚น fallback ์ „๋žต์— ๋งž์ถฐ ํ…Œ์ŠคํŠธ์ฝ”๋“œ ์ˆ˜์ •

* refactor: ์ผ์ž ๋‹จ์œ„ carry over ๋„์ž…์— ๋”ฐ๋ผ unionstore ์ œ๊ฑฐ

* chore: ํด๋ž˜์Šค๋ช…๊ณผ ๋™์ผํ•˜๊ฒŒ ํŒŒ์ผ ์ด๋ฆ„ ๋ณ€๊ฒฝ

* refactor: ๋žญํ‚น ์ด๋ฒคํŠธ ์ปจ์Šˆ๋จธ์—์„œ ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ ๋กœ์ง, ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง, ๋ฐฐ์น˜ ์ปค๋ฐ‹ ๋กœ์ง ๋ฐ˜๋ณต ์ œ๊ฑฐ

* refactor: ๋ถˆํ•„์š”ํ•œ stubbing ์ œ๊ฑฐ

* chore: ์‹œ๊ฐ„๋Œ€ ์„ค์ • ์ถ”๊ฐ€
@minor7295 minor7295 self-assigned this Dec 26, 2025
@minor7295 minor7295 marked this pull request as ready for review December 26, 2025 04:57
@minor7295 minor7295 changed the title Feature/ranking (#37) [volume - 9] What is Popularity? Dec 26, 2025
@minor7295 minor7295 merged commit f8db897 into Loopers-dev-lab:minor7295 Dec 28, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant