Skip to content

Conversation

@JVHE
Copy link
Collaborator

@JVHE JVHE commented Dec 26, 2025

๐Ÿ“Œ Summary

Redis ZSET ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ๋žญํ‚น ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. Kafka Consumer์—์„œ ์ด๋ฒคํŠธ(์กฐํšŒ/์ข‹์•„์š”/์ฃผ๋ฌธ)๋ฅผ ์†Œ๋น„ํ•˜์—ฌ Redis ZSET์— ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ์ ์ˆ˜๋ฅผ ์ ์žฌํ•˜๊ณ , Ranking API๋ฅผ ํ†ตํ•ด Top-N ๋žญํ‚น๊ณผ ๊ฐœ๋ณ„ ์ƒํ’ˆ ์ˆœ์œ„๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์ฝœ๋“œ ์Šคํƒ€ํŠธ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Score Carry-Over ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ๊ตฌํ˜„ ์‚ฌํ•ญ:

  • Redis ZSET ๊ธฐ๋ฐ˜ ๋žญํ‚น ์„œ๋น„์Šค (RankingService, RankingScoreCalculator)
  • Kafka Consumer์—์„œ ZSET ์ ์ˆ˜ ์ ์žฌ (RankingEventListener)
  • Ranking API ๊ตฌํ˜„ (GET /api/v1/rankings)
  • ์ƒํ’ˆ ์ƒ์„ธ API์— ๋žญํ‚น ์ •๋ณด ์ถ”๊ฐ€
  • ์ฝœ๋“œ ์Šคํƒ€ํŠธ ํ•ด๊ฒฐ (Score Carry-Over Scheduler)
  • ํฌํŠธ ์ถฉ๋Œ ํ•ด๊ฒฐ (commerce-streamer: 8081 โ†’ 8090)

๐Ÿ’ฌ Review Points

(1) ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์ค‘์ ์ ์œผ๋กœ ๋ด์คฌ์œผ๋ฉด ํ•˜๋Š” ๋ถ€๋ถ„

  • ๊ฐ€์ค‘์น˜ ์„ค๊ณ„: ์กฐํšŒ(0.1), ์ข‹์•„์š”(0.2), ์ฃผ๋ฌธ(0.7) ๊ฐ€์ค‘์น˜๊ฐ€ ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๋Š”์ง€ ํ™•์ธ
  • ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ: RankingEventListener์—์„œ eventId:ranking ํ˜•์‹์œผ๋กœ ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง
  • Score Carry-Over: Lua ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•œ ์ „๋‚  ์ ์ˆ˜ 10% ๋ณต์‚ฌ ๋กœ์ง์˜ ์ •ํ™•์„ฑ

(2) ๊ณ ๋ฏผํ–ˆ๋˜ ์„ค๊ณ„ ํฌ์ธํŠธ๋‚˜ ๋กœ์ง

  • Consumer Group ๋ถ„๋ฆฌ: ๊ธฐ์กด Metrics ์ง‘๊ณ„(commerce-metrics-consumer-group)์™€ ๋žญํ‚น ์ง‘๊ณ„(commerce-ranking-consumer-group) ๋ถ„๋ฆฌ
  • ์ฃผ๋ฌธ ์ ์ˆ˜ ๊ณ„์‚ฐ: OrderItem์˜ pricePerItem * quantity๋ฅผ ์‚ฌ์šฉํ•œ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง

(3) ๋ฆฌ๋ทฐ์–ด๊ฐ€ ํ™•์ธํ•ด์คฌ์œผ๋ฉด ํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋‚˜ ์˜ˆ์™ธ ์ƒํ™ฉ

  • ๋žญํ‚น์— ์—†๋Š” ์ƒํ’ˆ: ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ rankingInfo๊ฐ€ null์ธ์ง€ ํ™•์ธ
  • ์ค‘๋ณต ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ: ๋™์ผ eventId๋กœ ์ค‘๋ณต ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ๋˜๋Š”์ง€ ํ™•์ธ

(4) ๊ธฐํƒ€ ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์ฐธ๊ณ ํ•ด์•ผ ํ•  ์‚ฌํ•ญ

  • ์˜์กด์„ฑ: commerce-core์— RankingService, RankingScoreCalculator ๊ตฌํ˜„
  • Redis ์„ค์ •: RedisTemplate<String, String> ์‚ฌ์šฉ (ZSET ์—ฐ์‚ฐ)
  • ์Šค์ผ€์ค„๋Ÿฌ: CommerceStreamerApplication์— @EnableScheduling ์ถ”๊ฐ€๋จ
  • ํฌํŠธ ๋ณ€๊ฒฝ: commerce-streamer ์›น ์„œ๋ฒ„ ํฌํŠธ 8081 โ†’ 8090, Actuator ํฌํŠธ 8084 โ†’ 8091

โœ… Checklist

  • Redis ZSET ๊ธฐ๋ฐ˜ ๋žญํ‚น ์„œ๋น„์Šค ๊ตฌํ˜„ (RankingService, RankingScoreCalculator)
  • Kafka Consumer์—์„œ ZSET ์ ์ˆ˜ ์ ์žฌ (RankingEventListener)
  • Ranking API ๊ตฌํ˜„ (GET /api/v1/rankings)
  • ์ƒํ’ˆ ์ƒ์„ธ API์— ๋žญํ‚น ์ •๋ณด ์ถ”๊ฐ€
  • ์ฝœ๋“œ ์Šคํƒ€ํŠธ ํ•ด๊ฒฐ (Score Carry-Over Scheduler)
  • ํฌํŠธ ์ถฉ๋Œ ํ•ด๊ฒฐ (commerce-streamer ํฌํŠธ ๋ณ€๊ฒฝ)
  • Bean ์ถฉ๋Œ ํ•ด๊ฒฐ (์ค‘๋ณต ํด๋ž˜์Šค ์‚ญ์ œ)
  • ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (RankingService, RankingScoreCalculator)
  • ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (RankingEventListener, RankingV1Controller)
  • E2E ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (์ด๋ฒคํŠธ ๋ฐœํ–‰ โ†’ ๋žญํ‚น ์กฐํšŒ ํ”Œ๋กœ์šฐ)
  • API ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ (Swagger)

๐Ÿ“Ž References

Summary by CodeRabbit

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

  • ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ
    • ์ƒํ’ˆ ์ˆœ์œ„ ์‹œ์Šคํ…œ ์ถ”๊ฐ€ - ์ƒํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€์— ์ˆœ์œ„ ์ •๋ณด ํ‘œ์‹œ
    • ์ˆœ์œ„ ์กฐํšŒ API ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ (ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ง€์›)
    • ์ƒํ’ˆ ์กฐํšŒ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๊ธฐ๋ฐ˜์˜ ์‹ค์‹œ๊ฐ„ ์ˆœ์œ„ ๊ณ„์‚ฐ
    • ์ผ์ผ ์ˆœ์œ„ ์ž๋™ ์ง‘๊ณ„ ๋ฐ ๋ˆ„์  ๊ธฐ๋Šฅ

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 26, 2025

๊ฐœ์š”

์ƒํ’ˆ ์ˆœ์œ„(Ranking) ๊ธฐ๋Šฅ์ด ์ƒˆ๋กœ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ์— ์ˆœ์œ„ ์ •๋ณด๋ฅผ ํ†ตํ•ฉํ•˜๊ณ , Redis ๊ธฐ๋ฐ˜์˜ ์ˆœ์œ„ ๊ด€๋ฆฌ ์„œ๋น„์Šค๋ฅผ ๊ตฌ์ถ•ํ•˜๋ฉฐ, Kafka ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ˆœ์œ„๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ REST API ์—”๋“œํฌ์ธํŠธ์™€ ์ผ์ผ ์ˆœ์œ„ ์ธ์ˆ˜์ธ๊ณ„ ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์›Œํฌ์Šค๋ฃจ

์ƒˆ๋กœ์šด ์ƒํ’ˆ ์ˆœ์œ„ ๊ธฐ๋Šฅ์ด ๋„์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ProductFacade์— ์ˆœ์œ„ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” ๋ฉ”์„œ๋“œ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ๊ณ , Redis ๊ธฐ๋ฐ˜์˜ RankingService๋ฅผ ํ†ตํ•ด ์ผ์ผ ์ˆœ์œ„๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Kafka ๋ฆฌ์Šค๋„ˆ๊ฐ€ ์ƒํ’ˆ ์กฐํšŒ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜์—ฌ ์ˆœ์œ„ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ  ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ๋งค์ผ ๋ฐค ์ „๋‚  ์ˆœ์œ„์˜ 10%๋ฅผ ์˜ค๋Š˜ ์ˆœ์œ„๋กœ ๋„˜๊ธฐ๋Š” ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

๋ณ€๊ฒฝ์‚ฌํ•ญ

์ฝ”ํ˜ธํŠธ / ํŒŒ์ผ ๋ณ€๊ฒฝ ์š”์•ฝ
์ˆœ์œ„ ํ†ตํ•ฉ ๋ฐ API ๊ณ„์ธต
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java
ProductFacade์— ์ˆœ์œ„ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋Š” getProductDetailWithRanking() ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€. RankingFacade ์‹ ๊ทœ ์ƒ์„ฑ์œผ๋กœ Redis ๊ธฐ๋ฐ˜ ์ˆœ์œ„ ์กฐํšŒ(getRankings, getProductRanking) ๊ตฌํ˜„. RankingInfo ์ปจํ…Œ์ด๋„ˆ๋กœ ์ˆœ์œ„ ์‘๋‹ต DTO ์ •์˜(RankingsPageResponse, RankingItem, ProductRankingInfo).
์ƒํ’ˆ API ์‘๋‹ต ํ™•์žฅ
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java
ProductV1Controller์—์„œ getProductDetailWithRanking ํ˜ธ์ถœ๋กœ ๋ณ€๊ฒฝ. ProductV1Dto.ProductResponse์— rankingInfo ํ•„๋“œ ์ถ”๊ฐ€ ๋ฐ ๋งคํ•‘ ๋ฉ”์„œ๋“œ ํ™•์žฅ(from ๋ฉ”์„œ๋“œ ์˜ค๋ฒ„๋กœ๋”ฉ).
์ˆœ์œ„ REST API
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.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
RankingV1ApiSpec ์ธํ„ฐํŽ˜์ด์Šค๋กœ Swagger ๋ช…์„ธ ์ •์˜. RankingV1Controller๋กœ GET /api/v1/rankings ์—”๋“œํฌ์ธํŠธ ๊ตฌํ˜„. RankingV1Dto๋กœ ์ˆœ์œ„ ๋ชฉ๋ก, ๊ฐœ๋ณ„ ํ•ญ๋ชฉ, ์ƒํ’ˆ ์ˆœ์œ„ ์ •๋ณด DTO ์ •์˜.
์ˆœ์œ„ ๋„๋ฉ”์ธ ๋กœ์ง
apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingScoreCalculator.java, apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingService.java
RankingScoreCalculator ์‹ ๊ทœ ์ƒ์„ฑ์œผ๋กœ ์กฐํšŒ/์ข‹์•„์š”/์ฃผ๋ฌธ ์ด๋ฒคํŠธ๋ณ„ ์ ์ˆ˜ ๊ณ„์‚ฐ(calculateViewScore, calculateLikeScore, calculateOrderScore). RankingService๋กœ Redis ZSET ๊ธฐ๋ฐ˜ ์ผ์ผ ์ˆœ์œ„ ๊ด€๋ฆฌ(incrementScore, getTopNWithScores, getRank ๋“ฑ).
์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์ˆœ์œ„ ์—…๋ฐ์ดํŠธ
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/RankingEventListener.java, apps/commerce-streamer/src/main/java/com/loopers/infrastructure/scheduler/RankingCarryOverScheduler.java
RankingEventListener๋กœ Kafka ์ด๋ฒคํŠธ(ProductViewed, ProductLiked, OrderPaid) ์ˆ˜์‹  ๋ฐ ์ฒ˜๋ฆฌ(๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ, DLQ ์žฌ์‹œ๋„). RankingCarryOverScheduler๋กœ ๋งค์ผ 23:50์— ์ „๋‚  ์ˆœ์œ„์˜ 10%๋ฅผ ์˜ค๋Š˜ ์ˆœ์œ„๋กœ ์ด์ „(Lua ์Šคํฌ๋ฆฝํŠธ ํ™œ์šฉ).
์„ค์ • ๋ฐ ์—”ํ‹ฐํ‹ฐ ๋ณ€๊ฒฝ
apps/commerce-api/src/main/resources/application.yml, apps/commerce-streamer/src/main/resources/application.yml, apps/commerce-core/src/main/java/com/loopers/domain/event/outbox/OutboxEvent.java, apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java, supports/monitoring/src/main/resources/monitoring.yml
commerce-api ํฌํŠธ๋ฅผ 8080, actuator ํฌํŠธ๋ฅผ 8081๋กœ ์„ค์ •. commerce-streamer ํฌํŠธ๋ฅผ 8090, actuator ํฌํŠธ๋ฅผ 8091๋กœ ๋ณ€๊ฒฝ. OutboxEvent ํ…Œ์ด๋ธ”๋ช… outbox_events โ†’ tb_outbox_events. CommerceStreamerApplication์— @EnableScheduling ์ถ”๊ฐ€. monitoring.yml์—์„œ ํฌํŠธ ์„ค์ • ์ œ๊ฑฐ ๋ฐ ์ฃผ์„ ์ถ”๊ฐ€.

์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ

sequenceDiagram
    actor User
    participant API as ProductV1Controller
    participant ProdFacade as ProductFacade
    participant RankFacade as RankingFacade
    participant RankSvc as RankingService
    participant Redis
    participant DB as Database

    User->>API: GET /api/v1/products/{id}
    API->>ProdFacade: getProductDetailWithRanking(productId)
    
    rect rgb(220, 240, 255)
        Note over ProdFacade,DB: ์ƒํ’ˆ ์ •๋ณด ์กฐํšŒ
        ProdFacade->>DB: getProductDetail(productId)
        DB-->>ProdFacade: ProductInfo
    end
    
    rect rgb(240, 255, 240)
        Note over RankFacade,Redis: ์ˆœ์œ„ ์ •๋ณด ์กฐํšŒ
        ProdFacade->>RankFacade: getProductRanking(productId, today)
        RankFacade->>RankSvc: getRank(key, productId)
        RankSvc->>Redis: ZREVRANK ranking:all:{date}
        Redis-->>RankSvc: rank (0-based)
        RankSvc->>Redis: ZSCORE ranking:all:{date}
        Redis-->>RankSvc: score
        RankSvc-->>RankFacade: ProductRankingInfo(rank, score)
    end
    
    RankFacade-->>ProdFacade: ProductRankingInfo
    ProdFacade-->>API: ProductInfoWithRanking(info, rankingInfo)
    API->>API: ProductV1Dto.ProductResponse.from(info, rankingInfo)
    API-->>User: ApiResponse{productInfo, rankingInfo}
Loading
sequenceDiagram
    participant Kafka
    participant Listener as RankingEventListener
    participant Calc as RankingScoreCalculator
    participant RankSvc as RankingService
    participant Redis
    participant DLQ as DeadLetterQueue

    Kafka->>Listener: ConsumerRecord{ProductViewed}
    
    rect rgb(255, 245, 240)
        Note over Listener,Redis: ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ (๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ)
        Listener->>Listener: checkIdempotency(eventId)
        alt ์ค‘๋ณต ์ฒ˜๋ฆฌ๋จ
            Listener->>Listener: skip
        else ์‹ ๊ทœ ์ด๋ฒคํŠธ
            Listener->>Calc: calculateScore(eventType, eventData)
            Calc-->>Listener: score(0.1)
            Listener->>RankSvc: incrementScore(key, productId, 0.1)
            RankSvc->>Redis: ZINCRBY ranking:all:{date}
            Redis-->>RankSvc: newScore
        end
    end
    
    Listener->>Listener: markProcessed(eventId)
    Listener-->>Kafka: acknowledge()
    
    rect rgb(255, 230, 230)
        Note over Listener,DLQ: ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
        alt ์žฌ์‹œ๋„ ์ดˆ๊ณผ
            Listener->>DLQ: send(message)
            Listener->>Listener: cleanupRetry(eventId)
        end
    end
Loading

์˜ˆ์ƒ ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋…ธ๋ ฅ

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

๊ด€๋ จ ๊ฐ€๋Šฅ์„ฑ ์žˆ๋Š” PR

  • round8: ์นดํ”„์นด ๋ชจ๋“ˆ ์ ์šฉ, ๋„๋ฉ”์ธ ๊ด€๋ จ ์ฝ”๋“œ core๋กœ ๋ถ„๋ฆฌย #207: OutboxEvent ํ…Œ์ด๋ธ”๋ช… ๋ณ€๊ฒฝ ๋ฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ ์ธํ”„๋ผ๊ฐ€ ์ด PR์˜ RankingEventListener์™€ outbox ํŒจํ„ด์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๊ณ  ์žˆ์–ด ๊ด€๋ จ๋จ.
  • Round3: Product, Brand, Like, Orderย #132: ProductFacade ๋ฐ ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ํ๋ฆ„์— ์ˆœ์œ„ ์ •๋ณด ํ†ตํ•ฉ์ด ์ด PR์—์„œ ์ถ”๊ฐ€๋˜์—ˆ์œผ๋ฏ€๋กœ ๊ด€๋ จ๋จ.
  • Round8ย #203: ProductFacade ์ˆ˜์ • ์‚ฌํ•ญ์ด ๊ฒน์น˜๋ฉฐ, ์ด PR์€ ์ˆœ์œ„ ํ†ตํ•ฉ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ฒ€์ƒ‰ PR์€ ์ด๋ฒคํŠธ ๋ฐœํ–‰์„ ์ถ”๊ฐ€ํ•˜๋ฏ€๋กœ ๊ด€๋ จ๋จ.

์ถ”์ฒœ ๋ ˆ์ด๋ธ”

enhancement

์‹œ

๐Ÿฐ ์ˆœ์œ„์˜ ์ •์›์ด ํ”ผ์–ด๋‚˜๊ณ ,
Redis์˜ ๋ฐ˜์ง์ž„์ด ํ๋ฅด๋Š” ๊ณณ,
๋งค์ผ ๋ฐค ์–ด์ œ์˜ ํ–ฅ๊ธฐ๋ฅผ ์˜ค๋Š˜์— ๋‹ด์•„,
Kafka์˜ ๋…ธ๋ž˜์— ์ ์ˆ˜๊ฐ€ ์˜ค๋ฅด๋„ค!
์ƒํ’ˆ๋“ค์ด ์ถค์„ ์ถ”๋ฉฐ ์ˆœ์œ„๋ฅผ ๋‹คํˆฌ๋Š”,
ํ–‰์šด์˜ ์‹œ์ž‘์ด ์—ฌ๊ธฐ ์žˆ๋„ค์š”! โœจ

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 46.34% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
โœ… Passed checks (2 passed)
Check name Status Explanation
Title check โœ… Passed PR ์ œ๋ชฉ์€ Redis ๊ธฐ๋ฐ˜ ์ƒํ’ˆ ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌํ˜„์ด๋ผ๋Š” ํ•ต์‹ฌ ๋ณ€ํ™”๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์š”์•ฝํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
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.

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: 9

๐Ÿงน Nitpick comments (4)
supports/monitoring/src/main/resources/monitoring.yml (1)

33-36: ํฌํŠธ ๋ฌธ์„œํ™”๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋ช…์‹œ์ ์ธ ํฌํŠธ ์„ค์ •์„ ์ œ๊ฑฐํ•˜๊ณ  ๊ฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๋ณ„ ํฌํŠธ๋ฅผ ์ฃผ์„์œผ๋กœ ์•ˆ๋‚ดํ•˜๋Š” ์ ‘๊ทผ ๋ฐฉ์‹์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ํฌํŠธ ๋ฒˆํ˜ธ๋„ PR ๋ชฉํ‘œ(commerce-streamer 8090, actuator 8091)์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

์„ ํƒ์  ๊ฐœ์„  ์‚ฌํ•ญ: ์šฉ์–ด ๋ช…ํ™•ํ™” ๋ฐ ์ปจํ…์ŠคํŠธ ์ถ”๊ฐ€

๋ช‡ ๊ฐ€์ง€ ์„ ํƒ์  ๊ฐœ์„  ์‚ฌํ•ญ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”:

  1. "๊ธฐ๋ณธ" ์šฉ์–ด๊ฐ€ ํ˜ผ๋ž€์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํฌํŠธ๋“ค์€ ๊ธฐ๋ณธ๊ฐ’์ด๋ผ๊ธฐ๋ณด๋‹ค๋Š” ์„ค์ •๋œ ๊ฐ’์ž…๋‹ˆ๋‹ค.
  2. ํฌํŠธ ์ถฉ๋Œ ํ•ด๊ฒฐ ๋ฐฐ๊ฒฝ์„ ๊ฐ„๋‹จํžˆ ์–ธ๊ธ‰ํ•˜๋ฉด ํ–ฅํ›„ ์œ ์ง€๋ณด์ˆ˜์— ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
-  # server.port๋Š” ๊ฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ application.yml์—์„œ ์„ค์ •
-  # commerce-api: 8080 (๊ธฐ๋ณธ), actuator: 8081
-  # commerce-streamer: 8090 (๊ธฐ๋ณธ), actuator: 8091
-  # pg-simulator: 8082 (๊ธฐ๋ณธ), actuator: 8083
+  # server.port๋Š” ๊ฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ application.yml์—์„œ ์„ค์ •๋จ
+  # commerce-api: 8080 (์• ํ”Œ๋ฆฌ์ผ€์ด์…˜), 8081 (management)
+  # commerce-streamer: 8090 (์• ํ”Œ๋ฆฌ์ผ€์ด์…˜), 8091 (management)
+  # pg-simulator: 8082 (์• ํ”Œ๋ฆฌ์ผ€์ด์…˜), 8083 (management)
+  # ์ฐธ๊ณ : ํฌํŠธ ์ถฉ๋Œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ๊ฐ ์„œ๋น„์Šค๋งˆ๋‹ค ๊ณ ์œ ํ•œ ํฌํŠธ ํ• ๋‹น
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (1)

17-27: @Schema ์–ด๋…ธํ…Œ์ด์…˜์ด ์ค‘๋ณต๋ฉ๋‹ˆ๋‹ค.

๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ @Parameter์™€ @Schema๊ฐ€ ๋™์ผํ•œ description๊ณผ example์„ ๊ฐ–๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. OpenAPI ์ŠคํŽ™์—์„œ query parameter์—๋Š” @Parameter๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž Proposed fix
     ApiResponse<RankingV1Dto.RankingsPageResponse> getRankings(
             @Parameter(description = "๋‚ ์งœ (yyyyMMdd ํ˜•์‹, ๋ฏธ์ง€์ • ์‹œ ์˜ค๋Š˜ ๋‚ ์งœ)", example = "20241219")
-            @Schema(description = "๋‚ ์งœ (yyyyMMdd ํ˜•์‹, ๋ฏธ์ง€์ • ์‹œ ์˜ค๋Š˜ ๋‚ ์งœ)", example = "20241219")
             String date,
             @Parameter(description = "ํŽ˜์ด์ง€ ํฌ๊ธฐ", example = "20")
-            @Schema(description = "ํŽ˜์ด์ง€ ํฌ๊ธฐ", example = "20")
             int size,
             @Parameter(description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘)", example = "0")
-            @Schema(description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘)", example = "0")
             int page
     );
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/RankingEventListener.java (1)

217-221: ์ฃผ๋ฌธ ์ ์ˆ˜ ๊ณ„์‚ฐ ์‹œ ๊ฐ€๊ฒฉ ์ •๊ทœํ™” ๊ณ ๋ ค

ํ˜„์žฌ calculateOrderScore(price, quantity)๋Š” 0.7 * price * quantity๋กœ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๊ณ ๊ฐ€ ์ƒํ’ˆ์ด ๋žญํ‚น์—์„œ ํฌ๊ฒŒ ์œ ๋ฆฌํ•ด์ง€๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

์˜ˆ์‹œ:

  • 100,000์› ์ƒํ’ˆ 1๊ฐœ ์ฃผ๋ฌธ = 70,000์ 
  • 10,000์› ์ƒํ’ˆ 1๊ฐœ ์ฃผ๋ฌธ = 7,000์ 
  • ์กฐํšŒ 1ํšŒ = 0.1์ 

๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ๋‹ค๋ฅด์ง€๋งŒ, ์ฃผ๋ฌธ ๊ฑด์ˆ˜ ๊ธฐ๋ฐ˜ ๋žญํ‚น์„ ์›ํ•œ๋‹ค๋ฉด ๊ฐ€๊ฒฉ ์ •๊ทœํ™”(์˜ˆ: log(price) ๋˜๋Š” ๊ตฌ๊ฐ„๋ณ„ ์ ์ˆ˜)๋ฅผ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

296-300: FQN ์‚ฌ์šฉ ๊ฐœ์„  ๊ถŒ์žฅ

com.loopers.application.ranking.RankingInfo.ProductRankingInfo๋ฅผ ์™„์ „ํ•œ ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ์„ ์œ„ํ•ด import ๋ฌธ ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •

import ๋ฌธ ์ถ”๊ฐ€:

import com.loopers.application.ranking.RankingInfo.ProductRankingInfo;

record ์ˆ˜์ •:

-    public record ProductInfoWithRanking(
-            ProductInfo productInfo,
-            com.loopers.application.ranking.RankingInfo.ProductRankingInfo rankingInfo
-    ) {
-    }
+    public record ProductInfoWithRanking(
+            ProductInfo productInfo,
+            ProductRankingInfo rankingInfo
+    ) {
+    }
๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between f2609ee and bcf0868.

๐Ÿ“’ Files selected for processing (17)
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.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/main/resources/application.yml
  • apps/commerce-core/src/main/java/com/loopers/domain/event/outbox/OutboxEvent.java
  • apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingScoreCalculator.java
  • apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingService.java
  • apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/RankingEventListener.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/scheduler/RankingCarryOverScheduler.java
  • apps/commerce-streamer/src/main/resources/application.yml
  • supports/monitoring/src/main/resources/monitoring.yml
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (2)
๐Ÿ“š Learning: 2025-12-19T20:59:57.713Z
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:57.713Z
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/infrastructure/listener/RankingEventListener.java
  • apps/commerce-core/src/main/java/com/loopers/domain/event/outbox/OutboxEvent.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/product/ProductFacade.java
๐Ÿงฌ Code graph analysis (6)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
  • ProductV1Dto (8-75)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java (1)
  • RankingInfo (12-76)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java (1)
  • RankingInfo (12-76)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingScoreCalculator.java (2)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
  • Slf4j (28-149)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/scheduler/RankingCarryOverScheduler.java (1)
  • Slf4j (21-92)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java (1)
  • RankingInfo (12-76)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingService.java (4)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
  • Slf4j (23-106)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingScoreCalculator.java (1)
  • Slf4j (16-120)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/RankingEventListener.java (1)
  • Slf4j (31-320)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/scheduler/RankingCarryOverScheduler.java (1)
  • Slf4j (21-92)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java (1)
  • RankingInfo (12-76)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
  • RankingV1Dto (11-86)
๐Ÿ”‡ Additional comments (16)
apps/commerce-streamer/src/main/resources/application.yml (1)

38-43: LGTM!

commerce-api์™€์˜ ํฌํŠธ ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ํฌํŠธ ๋ถ„๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์„์œผ๋กœ ์ถฉ๋Œ ๋ฐฉ์ง€ ์˜๋„๊ฐ€ ๋ช…ํ™•ํžˆ ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/resources/application.yml (1)

102-107: LGTM!

commerce-streamer์™€์˜ ํฌํŠธ ๋ถ„๋ฆฌ๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1)

7-20: LGTM!

@EnableScheduling ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. RankingCarryOverScheduler์˜ ์ผ๊ฐ„ ์Šค์ฝ”์–ด ์ด์›” ์Šค์ผ€์ค„๋ง์„ ํ™œ์„ฑํ™”ํ•˜๋Š” ๋ฐ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)

82-86: LGTM!

๋žญํ‚น ์ •๋ณด ํ†ตํ•ฉ์ด ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ProductV1Dto.RankingInfo.from()์˜ null ์ฒ˜๋ฆฌ๋กœ ๋žญํ‚น์— ์—†๋Š” ์ƒํ’ˆ์˜ ๊ฒฝ์šฐ rankingInfo๊ฐ€ null๋กœ ์‘๋‹ต๋ฉ๋‹ˆ๋‹ค.

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

23-55: LGTM!

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

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/RankingEventListener.java (2)

59-63: @Transactional๊ณผ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์กฐํ•ฉ ํ™•์ธ ํ•„์š”

@Transactional์ด Kafka ๋ฆฌ์Šค๋„ˆ์— ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ๋‚ด ํ•˜๋‚˜์˜ ๋ฉ”์‹œ์ง€๋ผ๋„ ์‹คํŒจํ•˜๋ฉด ์ „์ฒด ๋ฐฐ์น˜๊ฐ€ ๋กค๋ฐฑ๋˜๊ณ  acknowledgment.acknowledge()๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์•„ ์ „์ฒด ๋ฐฐ์น˜๊ฐ€ ์žฌ์ „์†ก๋ฉ๋‹ˆ๋‹ค.

์ด๋Š” at-least-once ์‹œ๋ฉ˜ํ‹ฑ์Šค์—์„œ ์ •์ƒ ๋™์ž‘์ด๋‚˜, ๋‹ค์Œ ์‚ฌํ•ญ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”:

  1. ์ธ๋ฉ”๋ชจ๋ฆฌ retryCountMap์€ ์žฌ์ „์†ก ์‹œ ์ดˆ๊ธฐํ™”๋˜์–ด ์žฌ์‹œ๋„ ์นด์šดํŠธ๊ฐ€ ์ •ํ™•ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ
  2. ์„ฑ๊ณตํ•œ ๋ฉ”์‹œ์ง€๋„ ๋‹ค์‹œ ์ฒ˜๋ฆฌ๋˜๋ฏ€๋กœ ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ๊ฐ€ ํ•„์ˆ˜ (ํ˜„์žฌ ๊ตฌํ˜„๋จ โœ“)

82-110: ๋ฉฑ๋“ฑ์„ฑ ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

  • eventId:ranking ํ˜•์‹์œผ๋กœ ๋žญํ‚น ์ฒ˜๋ฆฌ ์ „์šฉ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค๋ฅธ ์ปจ์Šˆ๋จธ์™€์˜ ์ถฉ๋Œ์„ ๋ฐฉ์ง€
  • ์ฒ˜๋ฆฌ ์ „ ์ฒดํฌ โ†’ ์ฒ˜๋ฆฌ โ†’ ์ฒ˜๋ฆฌ ํ›„ ๋งˆํ‚น ์ˆœ์„œ๊ฐ€ ์˜ฌ๋ฐ”๋ฆ„
  • markAsHandled ์‹คํŒจ ์‹œ ์žฌ์ฒ˜๋ฆฌ๋  ์ˆ˜ ์žˆ์œผ๋‚˜, ์ด๋Š” Redis ZINCRBY์˜ ๋ฉฑ๋“ฑ์„ฑ์— ์˜ํ–ฅ ์—†์Œ (์ ์ˆ˜ ๋ˆ„์ ์€ ๋น„๋ฉฑ๋“ฑ)

๋‹จ, ํ˜„์žฌ ์ ์ˆ˜ ์ ์šฉ์€ ๋น„๋ฉฑ๋“ฑ์ ์ž…๋‹ˆ๋‹ค(ZINCRBY). ์žฌ์ฒ˜๋ฆฌ ์‹œ ์ ์ˆ˜๊ฐ€ ์ค‘๋ณต ์ ์šฉ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ๊ฐ€ ๋ฐ˜๋“œ์‹œ ์„ฑ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingScoreCalculator.java (2)

20-24: ๊ฐ€์ค‘์น˜ ์„ค๊ณ„ ํ™•์ธ ์™„๋ฃŒ

๊ฐ€์ค‘์น˜ ํ•ฉ๊ณ„๊ฐ€ 1.0 (0.1 + 0.2 + 0.7)์œผ๋กœ ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. PR ์š”์ฒญ๋œ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ์— ๋Œ€ํ•ด: ์กฐํšŒ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๊ฐ„์˜ ๋น„์œจ์ด ๋ช…ํ™•ํ•˜๊ณ  ์ฃผ๋ฌธ์— ๊ฐ€์žฅ ๋†’์€ ๊ฐ€์ค‘์น˜๋ฅผ ๋ถ€์—ฌํ•˜์—ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๊ฐ€์น˜๋ฅผ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค.


50-58: ์ฃผ๋ฌธ ์ ์ˆ˜์˜ ์Šค์ผ€์ผ ๋ถˆ๊ท ํ˜• ๊ฐ€๋Šฅ์„ฑ ๊ฒ€ํ†  ํ•„์š”

ํ˜„์žฌ 0.7 * price * amount ๊ณ„์‚ฐ์œผ๋กœ ๊ณ ๊ฐ€ ์ƒํ’ˆ(์˜ˆ: 1000๋งŒ์› * 100๊ฐœ = 7์–ต ์ )์ด ์กฐํšŒ/์ข‹์•„์š” ์ ์ˆ˜(0.1~0.2)๋ฅผ ์••๋„์ ์œผ๋กœ ๋Šฅ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์„ ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ ์ •๊ทœํ™” ๋ฐฉ์•ˆ์ด ์ด๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์œผ๋‚˜, ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”:

  1. ํ˜„์žฌ ๋ฐฉ์‹ ์œ ์ง€ (๋งค์ถœ ๊ธฐ๋ฐ˜ ๋žญํ‚น ์˜๋„ ์‹œ)
  2. ๋กœ๊ทธ ์ •๊ทœํ™” ํ™œ์„ฑํ™” (Math.log(1 + price * amount))
  3. ์ ์ˆ˜ ์ƒํ•œ(cap) ์ ์šฉ
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)

276-291: ๊ตฌํ˜„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค

getProductDetail ํ˜ธ์ถœ๋กœ ์ƒํ’ˆ ์กฐํšŒ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋˜๊ณ , ๋žญํ‚น ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜์—ฌ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. PR ์„ค๊ณ„ ๋…ธํŠธ์—์„œ ์–ธ๊ธ‰๋œ ๋Œ€๋กœ ๋žญํ‚น์— ์—†๋Š” ์ƒํ’ˆ์˜ ๊ฒฝ์šฐ rankingInfo๊ฐ€ null๋กœ ๋ฐ˜ํ™˜๋˜๋Š” ๋™์ž‘์ด ์˜๋„๋œ ๊ฒƒ์œผ๋กœ ํ™•์ธ๋ฉ๋‹ˆ๋‹ค.

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

1-76: LGTM!

DTO ๊ตฌ์กฐ๊ฐ€ ๊น”๋”ํ•˜๊ณ  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ํŒจํ„ด์ด ์ ์ ˆํ•˜๊ฒŒ ์‚ฌ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. RankingsPageResponse, RankingItem, ProductRankingInfo ๊ฐ๊ฐ์˜ ์—ญํ• ์ด ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

65-75: ์‚ญ์ œ๋œ ์ƒํ’ˆ ์ฒ˜๋ฆฌ ์‹œ ์ˆœ์œ„ ๋ถˆ์ผ์น˜ ๊ฐ€๋Šฅ์„ฑ

productMap์— ์ƒํ’ˆ์ด ์—†์œผ๋ฉด ํ•ด๋‹น ํ•ญ๋ชฉ์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  rank++๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด:

  1. ๋ฐ˜ํ™˜๋˜๋Š” items ๊ฐœ์ˆ˜๊ฐ€ ์š”์ฒญํ•œ size๋ณด๋‹ค ์ ์„ ์ˆ˜ ์žˆ์Œ
  2. ์ˆœ์œ„๊ฐ€ ์—ฐ์†์ ์œผ๋กœ ํ‘œ์‹œ๋˜์–ด Redis์˜ ์‹ค์ œ ์ˆœ์œ„์™€ ๋ถˆ์ผ์น˜

์˜๋„๋œ ๋™์ž‘์ด๋ผ๋ฉด ๊ดœ์ฐฎ์ง€๋งŒ, ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”:

  • ์‚ญ์ œ๋œ ์ƒํ’ˆ๋„ ์ˆœ์œ„์— ํฌํ•จํ•˜๋˜ "์‚ญ์ œ๋จ" ํ‘œ์‹œ
  • ๋˜๋Š” ํ˜„์žฌ ๋™์ž‘์„ Javadoc์— ๋ช…์‹œ

91-105: ์ ์ ˆํ•œ null ์ฒ˜๋ฆฌ ๋ฐ 1-based ์ˆœ์œ„ ๋ณ€ํ™˜

rank ๋˜๋Š” score๊ฐ€ null์ผ ๋•Œ null์„ ๋ฐ˜ํ™˜ํ•˜๊ณ , 0-based Redis ์ˆœ์œ„๋ฅผ 1-based๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/scheduler/RankingCarryOverScheduler.java (1)

35-51: Lua ์Šคํฌ๋ฆฝํŠธ ๋กœ์ง ์ •ํ™•์„ฑ ํ™•์ธ

PR์—์„œ ์š”์ฒญํ•œ Lua carry-over ๋กœ์ง ๊ฒ€ํ†  ๊ฒฐ๊ณผ:

  1. ZRANGE ... WITHSCORES๋กœ ์ „๋‚  ๋ชจ๋“  ๋ฉค๋ฒ„์™€ ์ ์ˆ˜ ์กฐํšŒ
  2. ์ ์ˆ˜์— ๊ฐ€์ค‘์น˜(10%) ์ ์šฉ ํ›„ ZINCRBY๋กœ ์˜ค๋Š˜ ๋žญํ‚น์— ๋ˆ„์ 
  3. TTL ์„ค์ •์œผ๋กœ 2์ผ ํ›„ ์ž๋™ ๋งŒ๋ฃŒ
  4. ์›์ž์  ์‹คํ–‰์œผ๋กœ ์ผ๊ด€์„ฑ ๋ณด์žฅ

์Šคํฌ๋ฆฝํŠธ ๋กœ์ง์€ ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.

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

1-86: LGTM!

API DTO๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๊ตฌ์กฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค. from() ๋งคํ•‘ ๋ฉ”์„œ๋“œ, @Schema ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•œ OpenAPI ๋ฌธ์„œํ™”, ProductRankingInfo.from()์˜ null ์ฒ˜๋ฆฌ๊ฐ€ ๋ชจ๋‘ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingService.java (1)

140-148: LGTM!

RankingEntry ํด๋ž˜์Šค์™€ ๋‚˜๋จธ์ง€ Redis ์กฐํšŒ ๋ฉ”์„œ๋“œ๋“ค(getRank, getScore, getRankingSize, setTtl)์ด ์ ์ ˆํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Comment on lines +26 to +39
public ApiResponse<RankingV1Dto.RankingsPageResponse> getRankings(
@RequestParam(required = false) String date,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "0") int page
) {
String rankingDate = date != null ? date :
LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));

Pageable pageable = PageRequest.of(page, size);
RankingInfo.RankingsPageResponse response =
rankingFacade.getRankings(rankingDate, pageable);

return ApiResponse.success(RankingV1Dto.RankingsPageResponse.from(response));
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

์ž…๋ ฅ ํŒŒ๋ผ๋ฏธํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ํ•„์š”

size ํŒŒ๋ผ๋ฏธํ„ฐ์— ์ƒํ•œ์ด ์—†์–ด ๋งค์šฐ ํฐ ๊ฐ’(์˜ˆ: 1000000) ์š”์ฒญ ์‹œ Redis์™€ DB์— ๋ถ€ํ•˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ date ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ํ˜•์‹(yyyyMMdd) ๊ฒ€์ฆ์ด ์—†์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Pattern;
+
 @GetMapping
 @Override
 public ApiResponse<RankingV1Dto.RankingsPageResponse> getRankings(
-        @RequestParam(required = false) String date,
-        @RequestParam(defaultValue = "20") int size,
-        @RequestParam(defaultValue = "0") int page
+        @RequestParam(required = false) 
+        @Pattern(regexp = "^\\d{8}$", message = "๋‚ ์งœ ํ˜•์‹์€ yyyyMMdd์ž…๋‹ˆ๋‹ค") String date,
+        @RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
+        @RequestParam(defaultValue = "0") @Min(0) int page
 ) {
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
around lines 26 to 39, add validation for the incoming request parameters:
enforce a sensible bounds check on size (e.g., clamp or reject values below 1
and above a configured MAX_PAGE_SIZE such as 100 or 1000) and for page ensure
non-negative; validate the date parameter by attempting to parse it with
DateTimeFormatter.ofPattern("yyyyMMdd") and if parsing fails return a 400 Bad
Request (or fall back to LocalDate.now() only when date is null), and reject or
sanitize excessively large page requests before calling rankingFacade to prevent
DB/Redis load. Ensure error responses use ApiResponse.error or appropriate HTTP
status and include a clear message about which parameter failed validation.

Comment on lines +13 to 15
@Table(name = "tb_outbox_events", indexes = {
@Index(name = "idx_outbox_status_created", columnList = "status, created_at")
})
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# Find the OutboxEvent.java file
find . -name "OutboxEvent.java" -type f

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 171


๐Ÿ Script executed:

# Search for migration files in common locations
fd -e sql -e xml . -path "*/db/migration*" -o -path "*/flyway*" -o -path "*/liquibase*" 2>/dev/null | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 70


๐Ÿ Script executed:

# Look for any files referencing outbox_events or tb_outbox_events
rg -l "outbox_events|tb_outbox_events" --type sql --type xml --type java 2>/dev/null | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 169


๐Ÿ Script executed:

# Read the OutboxEvent.java file
cat -n apps/commerce-core/src/main/java/com/loopers/domain/event/outbox/OutboxEvent.java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 2486


๐Ÿ Script executed:

# Find migration directory structure
find . -type d -name "*migration*" -o -type d -name "*flyway*" -o -type d -name "*liquibase*" 2>/dev/null | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 70


๐Ÿ Script executed:

# Check if there are any SQL files in the project
fd -e sql -type f 2>/dev/null | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 70


๐Ÿ Script executed:

# Find application properties or yml files
find . -name "application*.properties" -o -name "application*.yml" -o -name "application*.yaml" 2>/dev/null | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 260


๐Ÿ Script executed:

# Check for Spring Boot configuration files
fd "application" . -e properties -e yml -e yaml 2>/dev/null | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 260


๐Ÿ Script executed:

# Look for git history to see if table name changed
git log --oneline -- apps/commerce-core/src/main/java/com/loopers/domain/event/outbox/OutboxEvent.java 2>/dev/null | head -10

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 113


๐Ÿ Script executed:

# Check git diff to see what changed
git diff HEAD~1 -- apps/commerce-core/src/main/java/com/loopers/domain/event/outbox/OutboxEvent.java 2>/dev/null || echo "No previous version or git not available"

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 128


๐Ÿ Script executed:

# Check JPA/Hibernate configuration in application.yml files
cat -n apps/commerce-api/src/main/resources/application.yml | grep -A 10 -B 2 "jpa\|hibernate"

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 365


๐Ÿ Script executed:

# Search entire codebase for "outbox_events" (the old table name mentioned in review)
rg "outbox_events" --type java --type yml --type properties 2>/dev/null || echo "No references to 'outbox_events' found"

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 126


๐Ÿ Script executed:

# Check if OutboxEvent entity was recently created
git log --follow --all -- apps/commerce-core/src/main/java/com/loopers/domain/event/outbox/OutboxEvent.java 2>/dev/null | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 234


๐Ÿ Script executed:

# Check if there are any other outbox-related files
fd "outbox" . -type f 2>/dev/null | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 70


๐Ÿ Script executed:

# Check jpa.yml for Hibernate/JPA configuration
cat -n apps/commerce-api/src/main/resources/jpa.yml

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 164


๐Ÿ Script executed:

# Find jpa.yml file
find . -name "jpa.yml" -type f 2>/dev/null

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 128


๐Ÿ Script executed:

# Check all config files in commerce-api resources
ls -la apps/commerce-api/src/main/resources/ 2>/dev/null

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 263


๐Ÿ Script executed:

# Read jpa.yml configuration
cat -n ./modules/jpa/src/main/resources/jpa.yml

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 2906


์ƒˆ๋กœ์šด ํ…Œ์ด๋ธ” ์ƒ์„ฑ์„ ์œ„ํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

OutboxEvent ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์ƒˆ๋กœ ์ถ”๊ฐ€๋˜์—ˆ์œผ๋ฉฐ, JPA์˜ ddl-auto: none ์„ค์ •์œผ๋กœ ์ธํ•ด ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์ž๋™ ํ…Œ์ด๋ธ” ์ƒ์„ฑ์ด ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ tb_outbox_events ํ…Œ์ด๋ธ”์„ ์ˆ˜๋™์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ ์ค‘ ํ•˜๋‚˜์˜ ๋ฐฉ์‹์œผ๋กœ ์ถ”๊ฐ€ํ•ด ์ฃผ์„ธ์š”:

  • Flyway ๋˜๋Š” Liquibase ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ž‘์„ฑ
  • ๋˜๋Š” ๋ฐฐํฌ ์ „ ์‹คํ–‰ํ•  SQL ์Šคํฌ๋ฆฝํŠธ ๋ฌธ์„œํ™”
๐Ÿค– Prompt for AI Agents
In
apps/commerce-core/src/main/java/com/loopers/domain/event/outbox/OutboxEvent.java
around lines 13 to 15, the new OutboxEvent entity maps to tb_outbox_events but
DDL auto-create is disabled; add a migration to create this table and its index.
Create a Flyway (preferred) or Liquibase migration that defines the
tb_outbox_events table with the entity's columns (primary key, payload/body,
status, created_at, updated_at and any other fields present in the entity) and
add the index idx_outbox_status_created on (status, created_at); if you cannot
add a migration file, document a SQL script to be run before deployment
containing the equivalent CREATE TABLE and CREATE INDEX statements and reference
the migration filename or script location in the deployment notes.

Comment on lines +67 to +81
public double calculateScore(String eventType, Map<String, Object> eventData) {
switch (eventType) {
case "ProductViewed":
return calculateViewScore();
case "ProductLiked":
return calculateLikeScore();
case "OrderPaid":
Long price = extractPrice(eventData);
Integer amount = extractAmount(eventData);
return calculateOrderScore(price, amount);
default:
log.debug("Unknown event type for ranking score: {}", eventType);
return 0.0;
}
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

find . -type f -name "*.java" | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 1805


๐Ÿ Script executed:

# Search for calculateScore method calls
rg "calculateScore" --type java -n -B2 -A2

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 1447


๐Ÿ Script executed:

# Search for RankingEventListener and OrderPaid handling
rg "RankingEventListener|handleOrderEventsForRanking|OrderPaid" --type java -n -B2 -A5

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 41799


๐Ÿ Script executed:

rg "\.calculateScore\(" --type java -B3 -A3

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 1048


OrderPaid ์ผ€์ด์Šค๋Š” ์‹คํ–‰๋˜์ง€ ์•Š๋Š” ์ฃฝ์€ ์ฝ”๋“œ

calculateScore๋Š” RankingEventListener์˜ handleProductEventsForRanking ๋ฉ”์„œ๋“œ์—์„œ๋งŒ ํ˜ธ์ถœ๋˜๋ฉฐ, ์ด๊ณณ์€ ProductViewed, ProductLiked ์ด๋ฒคํŠธ๋งŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. OrderPaid ์ด๋ฒคํŠธ๋Š” ๋™์ผ ํด๋ž˜์Šค์˜ ๋ณ„๋„ ๋ฉ”์„œ๋“œ์ธ handleOrderEventsForRanking์—์„œ ์ง์ ‘ calculateOrderScore๋ฅผ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ, ์ด switch ๋ฌธ์˜ OrderPaid ๋ถ„๊ธฐ๋Š” ์ ˆ๋Œ€ ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ถˆํ•„์š”ํ•œ ๋ถ„๊ธฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋ช…ํ™•ํžˆ ๋ฌธ์„œํ™”ํ•ด ์ฃผ์„ธ์š”.

Comment on lines +63 to +70
public Double incrementScore(String key, Long productId, double score) {
// TTL ์„ค์ • (ํ‚ค๊ฐ€ ์—†์„ ๋•Œ๋งŒ)
if (Boolean.FALSE.equals(redisTemplate.hasKey(key))) {
redisTemplate.expire(key, TTL_SECONDS, TimeUnit.SECONDS);
}

return redisTemplate.opsForZSet().incrementScore(key, productId.toString(), score);
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

TTL ์„ค์ • ์‹œ ๊ฒฝ์Ÿ ์กฐ๊ฑด(Race Condition) ์กด์žฌ

hasKey ํ™•์ธ๊ณผ expire ํ˜ธ์ถœ ์‚ฌ์ด์— ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ ํ‚ค๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ํ‚ค๊ฐ€ ์—†์„ ๋•Œ expire๋ฅผ ํ˜ธ์ถœํ•ด๋„ ํšจ๊ณผ๊ฐ€ ์—†์œผ๋ฉฐ, incrementScore๊ฐ€ ํ‚ค๋ฅผ ์ƒ์„ฑํ•œ ํ›„์—๋Š” TTL์ด ์„ค์ •๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ๋™์ž‘: ์ฒซ ๋ฒˆ์งธ incrementScore ํ˜ธ์ถœ ์‹œ ํ‚ค๊ฐ€ ์ƒ์„ฑ๋˜์ง€๋งŒ TTL์ด ์—†๊ณ , ๋‘ ๋ฒˆ์งธ ํ˜ธ์ถœ๋ถ€ํ„ฐ TTL์ด ์„ค์ •๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
 public Double incrementScore(String key, Long productId, double score) {
-    // TTL ์„ค์ • (ํ‚ค๊ฐ€ ์—†์„ ๋•Œ๋งŒ)
-    if (Boolean.FALSE.equals(redisTemplate.hasKey(key))) {
-        redisTemplate.expire(key, TTL_SECONDS, TimeUnit.SECONDS);
-    }
-
-    return redisTemplate.opsForZSet().incrementScore(key, productId.toString(), score);
+    Double result = redisTemplate.opsForZSet().incrementScore(key, productId.toString(), score);
+    
+    // TTL์ด ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋งŒ ์„ค์ • (-1์€ TTL ์—†์Œ์„ ์˜๋ฏธ)
+    Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
+    if (ttl != null && ttl == -1) {
+        redisTemplate.expire(key, TTL_SECONDS, TimeUnit.SECONDS);
+    }
+    
+    return result;
 }

Comment on lines +93 to +105
public List<RankingEntry> getTopNWithScores(String key, long start, long end) {
Set<ZSetOperations.TypedTuple<String>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
if (tuples == null || tuples.isEmpty()) {
return Collections.emptyList();
}
return tuples.stream()
.map(tuple -> new RankingEntry(
Long.parseLong(tuple.getValue()),
tuple.getScore() != null ? tuple.getScore() : 0.0
))
.collect(Collectors.toList());
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

tuple.getValue() null ์ฒดํฌ ๋ˆ„๋ฝ

TypedTuple.getValue()๊ฐ€ null์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด ๊ฒฝ์šฐ Long.parseLong(null)์ด NPE๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
 return tuples.stream()
+        .filter(tuple -> tuple.getValue() != null)
         .map(tuple -> new RankingEntry(
                 Long.parseLong(tuple.getValue()),
                 tuple.getScore() != null ? tuple.getScore() : 0.0
         ))
         .collect(Collectors.toList());
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public List<RankingEntry> getTopNWithScores(String key, long start, long end) {
Set<ZSetOperations.TypedTuple<String>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
if (tuples == null || tuples.isEmpty()) {
return Collections.emptyList();
}
return tuples.stream()
.map(tuple -> new RankingEntry(
Long.parseLong(tuple.getValue()),
tuple.getScore() != null ? tuple.getScore() : 0.0
))
.collect(Collectors.toList());
}
public List<RankingEntry> getTopNWithScores(String key, long start, long end) {
Set<ZSetOperations.TypedTuple<String>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
if (tuples == null || tuples.isEmpty()) {
return Collections.emptyList();
}
return tuples.stream()
.filter(tuple -> tuple.getValue() != null)
.map(tuple -> new RankingEntry(
Long.parseLong(tuple.getValue()),
tuple.getScore() != null ? tuple.getScore() : 0.0
))
.collect(Collectors.toList());
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingService.java
around lines 93 to 105, the code calls Long.parseLong(tuple.getValue()) without
checking for null (or invalid number) which can throw NPE/NumberFormatException;
update the stream to filter out tuples with null values (and/or non-numeric
values) before parsing, or wrap the parse in a safe mapping that skips invalid
entries (or logs them) so only tuples with a non-null, parseable string are
converted into RankingEntry.

Comment on lines +43 to +46
// ๋ฉ”์‹œ์ง€๋ณ„ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ถ”์  (๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜, ์žฌ์‹œ์ž‘ ์‹œ ์ดˆ๊ธฐํ™”๋จ)
private final Map<String, Integer> retryCountMap = new ConcurrentHashMap<>();

private static final int MAX_RETRY_COUNT = 3;
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

retryCountMap์˜ ๋™์‹œ์„ฑ ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฌธ์ œ

  1. Race condition: getOrDefault โ†’ +1 โ†’ put ํŒจํ„ด์ด ์›์ž์ ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋™์ผ ํ‚ค์— ๋Œ€ํ•œ ๋™์‹œ ์—…๋ฐ์ดํŠธ ์‹œ ์นด์šดํŠธ๊ฐ€ ์†์‹ค๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜: ์žฌ์‹œ๋„ ํ›„ ์„ฑ๊ณตํ•˜๊ฑฐ๋‚˜ DLQ๋กœ ์ „์†ก๋˜์ง€ ์•Š๋Š” ๋ฉ”์‹œ์ง€์˜ ๊ฒฝ์šฐ retryCountMap์—์„œ ์ œ๊ฑฐ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
๐Ÿ”Ž Proposed fix
-    // ๋ฉ”์‹œ์ง€๋ณ„ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ถ”์  (๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜, ์žฌ์‹œ์ž‘ ์‹œ ์ดˆ๊ธฐํ™”๋จ)
-    private final Map<String, Integer> retryCountMap = new ConcurrentHashMap<>();
+    // ๋ฉ”์‹œ์ง€๋ณ„ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ถ”์  (๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜, ์žฌ์‹œ์ž‘ ์‹œ ์ดˆ๊ธฐํ™”๋จ)
+    private final ConcurrentHashMap<String, Integer> retryCountMap = new ConcurrentHashMap<>();

์‚ฌ์šฉ ์‹œ:

-                int retryCount = retryCountMap.getOrDefault(messageKey, 0) + 1;
-                retryCountMap.put(messageKey, retryCount);
+                int retryCount = retryCountMap.compute(messageKey, (k, v) -> (v == null) ? 1 : v + 1);

์„ฑ๊ณต ์‹œ cleanup ์ถ”๊ฐ€:

                 idempotencyService.markAsHandled(
                         rankingEventId,
                         eventType,
                         aggregateId,
                         "RankingEventListener"
                 );
+                // ์„ฑ๊ณต ์‹œ retry count ์ œ๊ฑฐ
+                if (record.key() != null) {
+                    retryCountMap.remove(record.key().toString());
+                }

Committable suggestion skipped: line range outside the PR's diff.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/RankingEventListener.java
around lines 43โ€“46, the retryCountMap uses getOrDefault -> +1 -> put which is
not atomic (race condition) and entries are never removed (memory leak); change
increments to an atomic map operation such as ConcurrentHashMap.compute/merge to
increment counts atomically, and ensure you remove the key from retryCountMap
when processing succeeds or the message is forwarded to DLQ (or when max retry
reached); optionally consider a bounded eviction strategy or scheduled cleanup
for long-lived entries, but minimally replace the get/put pattern with
compute/merge and add removal on success/DLQ paths.

Comment on lines +89 to +102
// ๋žญํ‚น ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ ์ ์žฌ
Long productId = Long.parseLong(aggregateId);
double score = scoreCalculator.calculateScore(eventType, message);

if (score != 0.0) {
// ProductUnliked์˜ ๊ฒฝ์šฐ ์Œ์ˆ˜ ์ ์ˆ˜๋กœ ์ฐจ๊ฐ
if ("ProductUnliked".equals(eventType)) {
score = -scoreCalculator.calculateLikeScore();
}

Double newScore = rankingService.incrementScore(todayKey, productId, score);
log.debug("Updated ranking score: productId={}, score={}, newTotal={}",
productId, score, newScore);
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

ProductUnliked ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋กœ์ง์— ๋ฒ„๊ทธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

RankingScoreCalculator.calculateScore()๋Š” ProductViewed, ProductLiked, OrderPaid๋งŒ ์ฒ˜๋ฆฌํ•˜๊ณ , ์•Œ ์ˆ˜ ์—†๋Š” ์ด๋ฒคํŠธ ํƒ€์ž…์— ๋Œ€ํ•ด 0.0์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ProductUnliked์˜ ๊ฒฝ์šฐ:

  1. Line 91: score = 0.0 (unknown event type)
  2. Line 93: if (score != 0.0) ์กฐ๊ฑด์ด false๊ฐ€ ๋˜์–ด ์ „์ฒด ๋ธ”๋ก์ด ์Šคํ‚ต๋จ
  3. Lines 95-97์˜ ์Œ์ˆ˜ ์ ์ˆ˜ ์ ์šฉ ๋กœ์ง์— ๋„๋‹ฌํ•˜์ง€ ๋ชปํ•จ
๐Ÿ”Ž Proposed fix
                 // ๋žญํ‚น ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ ์ ์žฌ
                 Long productId = Long.parseLong(aggregateId);
-                double score = scoreCalculator.calculateScore(eventType, message);
-
-                if (score != 0.0) {
-                    // ProductUnliked์˜ ๊ฒฝ์šฐ ์Œ์ˆ˜ ์ ์ˆ˜๋กœ ์ฐจ๊ฐ
-                    if ("ProductUnliked".equals(eventType)) {
-                        score = -scoreCalculator.calculateLikeScore();
-                    }
-
+                double score;
+                if ("ProductUnliked".equals(eventType)) {
+                    // ProductUnliked์˜ ๊ฒฝ์šฐ ์Œ์ˆ˜ ์ ์ˆ˜๋กœ ์ฐจ๊ฐ
+                    score = -scoreCalculator.calculateLikeScore();
+                } else {
+                    score = scoreCalculator.calculateScore(eventType, message);
+                }
+
+                if (score != 0.0) {
                     Double newScore = rankingService.incrementScore(todayKey, productId, score);
                     log.debug("Updated ranking score: productId={}, score={}, newTotal={}",
                             productId, score, newScore);
                 }
๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/RankingEventListener.java
around lines 89 to 102, the ProductUnliked case is skipped because
calculateScore(...) returns 0.0 for unknown types; change the logic to detect
ProductUnliked by eventType before the score nonโ€‘zero gate so it always applies
a negative like score: call calculateScore as now, but use a conditional like if
(score != 0.0 || "ProductUnliked".equals(eventType)) then if ProductUnliked set
score = -scoreCalculator.calculateLikeScore() and proceed to incrementScore and
logging; this ensures ProductUnliked updates are applied even though
calculateScore returns 0.0.

Comment on lines +112 to +119
} catch (Exception e) {
String messageKey = record.key() != null ? record.key().toString() :
extractEventId(parseMessage(record.value()));
int retryCount = retryCountMap.getOrDefault(messageKey, 0) + 1;
retryCountMap.put(messageKey, retryCount);

log.error("Failed to process ranking event: topic={}, key={}, offset={}, retryCount={}",
record.topic(), record.key(), record.offset(), retryCount, e);
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

catch ๋ธ”๋ก ๋‚ด parseMessage ์žฌํ˜ธ์ถœ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ ๊ฐ€๋Šฅ

Line 114์—์„œ parseMessage(record.value())๊ฐ€ ์›๋ž˜ ์‹คํŒจ ์›์ธ์ด์—ˆ๋‹ค๋ฉด, catch ๋ธ”๋ก์—์„œ๋„ ๋™์ผํ•˜๊ฒŒ ์‹คํŒจํ•˜์—ฌ ์˜ˆ์™ธ๊ฐ€ ์ „ํŒŒ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž Proposed fix
         for (ConsumerRecord<Object, Object> record : records) {
+            String messageKey = record.key() != null ? record.key().toString() : 
+                    String.format("%s-%d-%d", record.topic(), record.partition(), record.offset());
             try {
                 Map<String, Object> message = parseMessage(record.value());
-                String eventId = extractEventId(message);
+                String eventId = extractEventId(message);
+                if (eventId != null) {
+                    messageKey = eventId;  // ๋” ์˜๋ฏธ ์žˆ๋Š” ํ‚ค๋กœ ์—…๋ฐ์ดํŠธ
+                }
                 // ... rest of try block
             } catch (Exception e) {
-                String messageKey = record.key() != null ? record.key().toString() :
-                        extractEventId(parseMessage(record.value()));
                 int retryCount = retryCountMap.getOrDefault(messageKey, 0) + 1;

Committable suggestion skipped: line range outside the PR's diff.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/RankingEventListener.java
around lines 112 to 119, do not re-call parseMessage(record.value()) directly
inside the catch since that can throw again; instead obtain messageKey safely by
first checking record.key() and if null attempt to parse the message inside its
own try/catch (or use a safe helper that returns Optional) and fall back to a
stable default (e.g., "unknown-"+record.offset() or similar) on parse failure,
then use that safe messageKey when updating retryCountMap and logging so no
secondary exception is thrown from the catch block.

Comment on lines +53 to +68
/**
* ๋งค์ผ 23์‹œ 50๋ถ„์— ์‹คํ–‰ (๋‹ค์Œ ๋‚  ๋žญํ‚น ๋ฏธ๋ฆฌ ์ƒ์„ฑ)
* ์ „๋‚  ์ ์ˆ˜์˜ 10%๋ฅผ ์˜ค๋Š˜ ๋žญํ‚น์— ๋ณต์‚ฌ
*/
@Scheduled(cron = "0 50 23 * * *") // ๋งค์ผ 23:50
public void carryOverRankingScore() {
try {
LocalDate yesterday = LocalDate.now().minusDays(1);
LocalDate today = LocalDate.now();

String yesterdayKey = rankingService.getRankingKey(
yesterday.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
);
String todayKey = rankingService.getRankingKey(
today.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
);
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

์ฃผ์„๊ณผ ์‹ค์ œ ๋™์ž‘ ๋ถˆ์ผ์น˜

์ฃผ์„์—๋Š” "๋‹ค์Œ ๋‚  ๋žญํ‚น ๋ฏธ๋ฆฌ ์ƒ์„ฑ"์ด๋ผ๊ณ  ๋˜์–ด ์žˆ์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” ์–ด์ œ(yesterday) ์ ์ˆ˜๋ฅผ ์˜ค๋Š˜(today) ๋žญํ‚น์— ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค. 23:50์— ์‹คํ–‰๋˜๋ฉด "์˜ค๋Š˜" ๋žญํ‚น์— 10๋ถ„ ๋™์•ˆ๋งŒ ์ ์šฉ๋˜๊ณ , ๋‹ค์Œ ๋‚ ์—๋Š” ์ƒˆ๋กœ์šด "์˜ค๋Š˜"์ด ๋ฉ๋‹ˆ๋‹ค.

cold-start ์™„ํ™” ๋ชฉ์ ์ด๋ผ๋ฉด tomorrow๋กœ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜, ์ฃผ์„์„ ์ˆ˜์ •ํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ • (๋‹ค์Œ ๋‚  ๋žญํ‚น ์ƒ์„ฑ ์˜๋„์ธ ๊ฒฝ์šฐ)
 @Scheduled(cron = "0 50 23 * * *") // ๋งค์ผ 23:50
 public void carryOverRankingScore() {
     try {
-        LocalDate yesterday = LocalDate.now().minusDays(1);
-        LocalDate today = LocalDate.now();
+        LocalDate today = LocalDate.now();
+        LocalDate tomorrow = LocalDate.now().plusDays(1);
 
         String yesterdayKey = rankingService.getRankingKey(
-                yesterday.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
+                today.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
         );
         String todayKey = rankingService.getRankingKey(
-                today.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
+                tomorrow.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
         );
๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/scheduler/RankingCarryOverScheduler.java
around lines 53 to 68, the comment says "pre-generate next day's ranking" but
the code copies yesterday's scores into today's ranking (LocalDate.now()); to
match the intended behavior change LocalDate today = LocalDate.now(); to
LocalDate tomorrow = LocalDate.now().plusDays(1); and use tomorrow when building
the target key (or alternatively update the comment to reflect that it copies
into today's ranking if that was intended). Ensure variable names and key
construction (yesterdayKey and targetKey) are updated consistently.

@looper88888
Copy link
Contributor

๐Ÿ“ˆ Ranking Consumer

  • ๋žญํ‚น ZSET ์˜ TTL, ํ‚ค ์ „๋žต์„ ์ ์ ˆํ•˜๊ฒŒ ๊ตฌ์„ฑํ•˜์˜€๋‹ค
  • ๋‚ ์งœ๋ณ„๋กœ ์ ์žฌํ•  ํ‚ค๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์—ˆ๋‹ค
  • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ํ›„, ZSET ์— ์ ์ˆ˜๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ฐ˜์˜๋œ๋‹ค

โšพ Ranking API

  • ๋žญํ‚น Page ์กฐํšŒ ์‹œ ์ •์ƒ์ ์œผ๋กœ ๋žญํ‚น ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค
  • ๋žญํ‚น Page ์กฐํšŒ ์‹œ ๋‹จ์ˆœํžˆ ์ƒํ’ˆ ID ๊ฐ€ ์•„๋‹Œ ์ƒํ’ˆ์ •๋ณด๊ฐ€ Aggregation ๋˜์–ด ์ œ๊ณต๋œ๋‹ค
  • ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ํ•ด๋‹น ์ƒํ’ˆ์˜ ์ˆœ์œ„๊ฐ€ ํ•จ๊ป˜ ๋ฐ˜ํ™˜๋œ๋‹ค (์ˆœ์œ„์— ์—†๋‹ค๋ฉด null)

@coderabbitai ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ™•์ธํ•ด

@coderabbitai
Copy link

coderabbitai bot commented Dec 27, 2025

Tip

For best results, initiate chat on the files or code changes.

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Verify Ranking Consumer implementation - check RankingService for TTL and key strategy
cat apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingService.java

Length of output: 4203


๐Ÿ Script executed:

#!/bin/bash
# Verify RankingEventListener for event processing
cat apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/RankingEventListener.java

Length of output: 13077


๐Ÿ Script executed:

#!/bin/bash
# Verify RankingScoreCalculator for score calculation
cat apps/commerce-core/src/main/java/com/loopers/domain/ranking/RankingScoreCalculator.java

Length of output: 3487


๐Ÿ Script executed:

#!/bin/bash
# Verify Ranking API - RankingV1Controller
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java

Length of output: 1526


๐Ÿ Script executed:

#!/bin/bash
# Verify RankingFacade for product aggregation
cat apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java

Length of output: 3338


๐Ÿ Script executed:

#!/bin/bash
# Verify ProductFacade for product detail with ranking
cat apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java

Length of output: 12945


@looper88888 ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค! ๋ชจ๋“  ํ•ญ๋ชฉ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. โœ…

๐Ÿ“ˆ Ranking Consumer

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

RankingService.java์—์„œ ํ™•์ธ:

  • ํ‚ค ์ „๋žต: ranking:all:{yyyyMMdd} ํ˜•์‹ ์‚ฌ์šฉ
  • TTL: 2์ผ (172,800์ดˆ) ์„ค์ •
  • incrementScore() ๋ฉ”์„œ๋“œ์—์„œ ํ‚ค๊ฐ€ ์ฒ˜์Œ ์ƒ์„ฑ๋  ๋•Œ ์ž๋™์œผ๋กœ TTL ์ ์šฉ
  • getRankingKey(String date) ๋ฐ getTodayRankingKey() ๋ฉ”์„œ๋“œ๋กœ ์ผ๊ด€๋œ ํ‚ค ์ƒ์„ฑ

โœ… ๋‚ ์งœ๋ณ„๋กœ ์ ์žฌํ•  ํ‚ค๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์—ˆ๋‹ค

RankingService.java์— ๊ตฌํ˜„๋จ:

public String getRankingKey(String date) {
    return RANKING_KEY_PREFIX + date;  // "ranking:all:20231227"
}

public String getTodayRankingKey() {
    return getRankingKey(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
}

โœ… ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ํ›„, ZSET ์— ์ ์ˆ˜๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ฐ˜์˜๋œ๋‹ค

RankingEventListener.java์—์„œ ํ™•์ธ:

  • catalog-events ํ† ํ”ฝ: ProductViewed(0.1), ProductLiked(0.2), ProductUnliked(-0.2) ์ฒ˜๋ฆฌ
  • order-events ํ† ํ”ฝ: OrderPaid(0.7 ร— price ร— quantity) ์ฒ˜๋ฆฌ
  • ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ: eventId:ranking ํ˜•์‹์œผ๋กœ ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€
  • ์žฌ์‹œ๋„ ๋กœ์ง: ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„, ์‹คํŒจ ์‹œ DLQ ์ „์†ก
  • RankingScoreCalculator๋ฅผ ํ†ตํ•œ ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ์ ์ˆ˜ ๊ณ„์‚ฐ

โšพ Ranking API

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

RankingV1Controller.java์— ๊ตฌํ˜„๋จ:

  • ์—”๋“œํฌ์ธํŠธ: GET /api/v1/rankings
  • ํŒŒ๋ผ๋ฏธํ„ฐ:
    • date (์„ ํƒ, ๊ธฐ๋ณธ๊ฐ’: ์˜ค๋Š˜)
    • size (๊ธฐ๋ณธ๊ฐ’: 20)
    • page (๊ธฐ๋ณธ๊ฐ’: 0)
  • ์‘๋‹ต: ApiResponse<RankingsPageResponse> ํ˜•์‹์œผ๋กœ items, page, size, total ํฌํ•จ

โœ… ๋žญํ‚น Page ์กฐํšŒ ์‹œ ๋‹จ์ˆœํžˆ ์ƒํ’ˆ ID ๊ฐ€ ์•„๋‹Œ ์ƒํ’ˆ์ •๋ณด๊ฐ€ Aggregation ๋˜์–ด ์ œ๊ณต๋œ๋‹ค

RankingFacade.java์˜ getRankings() ๋ฉ”์„œ๋“œ์—์„œ:

  1. ZSET์—์„œ Top-N ์ƒํ’ˆ ID์™€ ์ ์ˆ˜ ์กฐํšŒ
  2. productService.getProductMapByIds()๋กœ ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด ์ผ๊ด„ ์กฐํšŒ
  3. RankingInfo.RankingItem์— ๋‹ค์Œ ์ •๋ณด ํฌํ•จ:
    • rank (์ˆœ์œ„)
    • productId
    • score (์ ์ˆ˜)
    • productName (์ƒํ’ˆ๋ช…)
    • brandId (๋ธŒ๋žœ๋“œ ID)
    • price (๊ฐ€๊ฒฉ)

โœ… ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ํ•ด๋‹น ์ƒํ’ˆ์˜ ์ˆœ์œ„๊ฐ€ ํ•จ๊ป˜ ๋ฐ˜ํ™˜๋œ๋‹ค (์ˆœ์œ„์— ์—†๋‹ค๋ฉด null)

ProductFacade.java์˜ getProductDetailWithRanking() ๋ฉ”์„œ๋“œ:

  • ๊ธฐ์กด getProductDetail()๋กœ ์ƒํ’ˆ ์ •๋ณด ์กฐํšŒ
  • rankingFacade.getProductRanking()๋กœ ์˜ค๋Š˜ ๋‚ ์งœ ๊ธฐ์ค€ ๋žญํ‚น ์ •๋ณด ์กฐํšŒ
  • ์ˆœ์œ„๊ฐ€ ์—†์œผ๋ฉด null ๋ฐ˜ํ™˜ (RankingFacade.getProductRanking()์—์„œ ์ฒ˜๋ฆฌ)
  • ProductInfoWithRanking ๋ ˆ์ฝ”๋“œ๋กœ ์‘๋‹ต ๋ฐ˜ํ™˜

ProductV1Controller.java์—์„œ:

  • getProductDetail() ๋ฉ”์„œ๋“œ๊ฐ€ getProductDetailWithRanking() ํ˜ธ์ถœ
  • ์‘๋‹ต DTO์— rankingInfo ํ•„๋“œ ํฌํ•จ (null ํ—ˆ์šฉ)

๐ŸŽฏ ์ถ”๊ฐ€ ํ™•์ธ ์‚ฌํ•ญ

  1. ๊ฐ€์ค‘์น˜ ์„ค๊ณ„: view=0.1, like=0.2, order=0.7 โœ…
  2. ๋ฉฑ๋“ฑ์„ฑ ๋กœ์ง: eventId:ranking ํ‚ค ํ˜•์‹ ์‚ฌ์šฉ โœ…
  3. Score Carry-Over: RankingCarryOverScheduler์—์„œ ๋งค์ผ 23:50์— 10% ์ด์›” โœ…

๋ชจ๋“  ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ์ด ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰

 />/)
(โ€ข_โ€ข)
/ >๐Ÿ†

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.

2 participants