Skip to content

Conversation

@junoade
Copy link
Collaborator

@junoade junoade commented Dec 26, 2025

๐Ÿ“Œ Summary

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

image
  1. ๋žญํ‚น ZSET ๋ฐ ํ‚ค ์ „๋žต
  • ProductLike ์ด๋ฒคํŠธ์— ๋Œ€ํ•ด์„œ weight=0.2, score = weight * 1 ๋กœ ๊ณ„์‚ฐํ•˜์—ฌ zset์— ์ง‘๊ณ„
  • TTL : 2Day / Key: ranking:all:{yyyyMMdd}
  1. ์ปจ์Šˆ๋จธ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ๋ฐ ์ง‘๊ณ„
  • (์ดˆ๊ธฐ) streamer ๋ชจ๋“ˆ์— ๋žญํ‚น ์ง‘๊ณ„ ์„œ๋น„์Šค ๊ตฌํ˜„ ๋ฐ ์ปจ์Šˆ๋จธ ๋ฆฌ์Šค๋„ˆ์— ๋‹จ์ผ ๋ฉ”์‹œ์ง€ ๋‹จ๊ฑด ์ฒ˜๋ฆฌ
  1. Carry over ๊ตฌํ˜„ ๋“ฑ

๐Ÿ’ฌ Review Points

  1. ๋ฉ€ํ‹ฐ๋ชจ๋“ˆ ๊ตฌ์กฐ์—์„œ ๋‹ค์–‘ํ•œ ์ปจ์Šˆ๋จธ ์„œ๋น„์Šค, ๋ ˆ๋””์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ˜ธ์ถœ ์ฃผ์ฒด๊ฐ€ ๋Š˜์–ด๋‚จ์— ๋”ฐ๋ผ
  • ํ•œ DTO๋ฅผ ์—ฌ๋Ÿฌ ๊ตฐ๋ฐ์„œ ์ •์˜ํ•˜๊ณ  ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋Š” ์ผ€์ด์Šค๊ฐ€ ์ƒ๊ธธ ๊ฒƒ์œผ๋กœ ๋ณด์•˜๋Š”๋ฐ ํ˜น์‹œ ๊ด€๋ จ๋˜์–ด ์‹ค๋ฌด์—์„œ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•˜์‹œ๋Š” ๊ฑธ ์„ ํ˜ธํ•˜์‹œ๋Š”์ง€์™€ ๊ด€๋ จ๋œ ํ‚ค์›Œ๋“œ ๋“ค์„ ์•Œ ์ˆ˜ ์žˆ์„๊นŒ์š”?

์ €ํฌ ํšŒ์‚ฌ์—์„œ๋Š” ํ˜ธ์ถœ ์ฃผ์ฒด์˜ ํ”„๋กœ์ ํŠธ ํ™˜๊ฒฝ์— ๋™์ผํ•œ DTO๋ฅผ ์ •์˜ํ•˜๊ณ  ์†Œ์Šค DTO๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์—…๋ฌด์—ฐ๋ฝ ๋“ฑ์„ ํ†ตํ•ด ๋‹ค๊ฐ™์ด ๋ณ€๊ฒฝ์ผ์ •์„ ๋งž์ถ”๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.. (e.g ๊ณ„์ •๊ณ„ DTO <-> ์ธํ„ฐ๋„ท ์•ฑ ์„œ๋น„์Šค ๋‚ด DTO , ๋ชจ๋ฐ”์ผ ์•ฑ ์„œ๋น„์Šค๋‚ด DTO ๋“ฑ n๊ฐœ ์กด์žฌ)

โœ… Checklist

๐Ÿ“ˆ Ranking Consumer

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

โšพ Ranking API

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

๐Ÿ“Ž References

Summary by CodeRabbit

Release Notes

  • New Features

    • ์ผ์ผ ์ธ๊ธฐ ์ƒํ’ˆ ์กฐํšŒ API ์ถ”๊ฐ€
    • ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด์— ์ˆœ์œ„ ์ ์ˆ˜ ํฌํ•จ
    • ์ข‹์•„์š” ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์ˆœ์œ„ ์ž๋™ ์ง‘๊ณ„ ๋ฐ ์ผ์ผ ๋ˆ„์ 
  • Tests

    • Kafka ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ฐœ์„ 
    • ์ˆœ์œ„ ์ง‘๊ณ„ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€

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

junoade and others added 8 commits December 22, 2025 02:56
1. kafka.yml ์— ๊ธฐ์žฌ๋œ
testImplementation(testFixtures(project(":modules:kafka")))
์— ๋Œ€ํ•œ ์„ค์ • ์ฝ”๋“œ๋“ค์„ ์ถ”๊ฐ€.

2. ๊ทธ ํ›„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜ํ–‰์‹œ,
- ์Šคํ”„๋ง์ปจํ…์ŠคํŠธ, ์นดํ”„์นด ํ† ํ”ฝ, ์ปจ์Šˆ๋จธ๊ทธ๋ฃน, ์ปจ์Šˆ๋จธ ๋“ฑ ์ƒ์„ฑ -> ํ…Œ์ŠคํŠธ ๋  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •
- @beforeeach ์‹œ ์•„๋ž˜์™€ ๊ฐ™์ด ๋–ณ๊ณ , ์ด๋Š” ์ปจ์Šˆ๋จธ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ๋จผ์ € ๋– ์„œ ํ† ํ”ฝ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š”๋ฐ, ๊ทธ ์‹œ์ ์— ํ† ํ”ฝ์ด ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
```
org.apache.kafka.clients.NetworkClient   : [Consumer clientId=commerce-streamer-0, groupId=loopers-default-consumer] Error while fetching metadata with correlation id 2 : {order-events=UNKNOWN_TOPIC_OR_PARTITION}
```
> ์ผ๋‹จ ๋Œ์•„๊ฐ€๊ฒŒ ๊ตฌํ˜„
1. ์ดˆ๊ธฐ ๋žญํ‚น ZSET ๋ฐ ํ‚ค ์ „๋žต
- ProductLike ์ด๋ฒคํŠธ์— ๋Œ€ํ•ด์„œ weight=0.2, score = weight * 1 ๋กœ ๊ณ„์‚ฐํ•˜์—ฌ zset์— ์ง‘๊ณ„
- TTL : 2Day / Key: ranking:all:{yyyyMMdd}

2. ์ปจ์Šˆ๋จธ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ๋ฐ ์ง‘๊ณ„
- (์ดˆ๊ธฐ) streamer ๋ชจ๋“ˆ์— ๋žญํ‚น ์ง‘๊ณ„ ์„œ๋น„์Šค ๊ตฌํ˜„ ๋ฐ ์ปจ์Šˆ๋จธ ๋ฆฌ์Šค๋„ˆ์— ๋‹จ์ผ ๋ฉ”์‹œ์ง€ ๋‹จ๊ฑด ์ฒ˜๋ฆฌ
Week9 feature redis ์ดˆ๊ธฐ๊ตฌํ˜„
> ์ผ๋‹จ ๋Œ์•„๊ฐ€๊ฒŒ ๊ตฌํ˜„
์˜ค๋Š˜ ZSET ์ ์ˆ˜๋ฅผ 0.1 ๊ฐ€์ค‘์น˜๋กœ ๋‚ด์ผ ํ‚ค๋กœ ๋ณต์‚ฌํ•˜๊ณ  TTL์„ ์„ค์ •
- Redis์—์„œ EXPIRE๋Š” ์“ฐ๊ธฐ ์—ฐ์‚ฐ์ด๋‹ค.
- ๋„คํŠธ์›Œํฌ RTT
  - ๋งค๋ฒˆ ZINCRBY + EXPIRE = ์™•๋ณต 2๋ฒˆ
  - Kafka ์ด๋ฒคํŠธ๋‹น 1~2 RTT โ†’ TPS ๋†’์•„์งˆ์ˆ˜๋ก ์ฒด๊ฐ
@coderabbitai
Copy link

coderabbitai bot commented Dec 26, 2025

Walkthrough

์ผ์ผ ์ƒํ’ˆ ์ˆœ์œ„ ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. Redis ZSET ๊ธฐ๋ฐ˜์œผ๋กœ ์ข‹์•„์š” ์ด๋ฒคํŠธ๋ฅผ ์ถ”์ ํ•˜๊ณ , ๋งค์ผ ์ •ํ•ด์ง„ ์‹œ๊ฐ„์— ์ ์ˆ˜๋ฅผ ์ด์›”ํ•ฉ๋‹ˆ๋‹ค. REST API๋ฅผ ํ†ตํ•ด ์ˆœ์œ„ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ์— ์ˆœ์œ„ ์ ์ˆ˜๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. Kafka ํ…Œ์ŠคํŠธ ์ธํ”„๋ผ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.

Changes

์ˆœ์œ„ ์ง€์ • ๋งค์žฅ / ํŒŒ์ผ ๋ณ€๊ฒฝ ์š”์•ฝ
Redis ์ˆœ์œ„ ๋ชจ๋“ˆ
modules/redis/src/main/java/com/loopers/ranking/RankingEntry.java, DailyRankingResponse.java, RankingKey.java, RankingPolicy.java, RankingZSetRepository.java
์ˆœ์œ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์‹ ๊ทœ ๋ ˆ์ฝ”๋“œ ํƒ€์ž…(RankingEntry, DailyRankingResponse)๊ณผ Redis ZSET ๋ฆฌํฌ์ง€ํ† ๋ฆฌ(RankingZSetRepository) ์ถ”๊ฐ€. RankingKey๋ฅผ ํ†ตํ•œ ์ผ์ผ ์ˆœ์œ„ ํ‚ค ์ƒ์„ฑ.
Commerce API ์ˆœ์œ„ ์„œ๋น„์Šค ๊ณ„์ธต
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryResponse.java, RankingQueryService.java
RankingQueryResponse ๋ ˆ์ฝ”๋“œ์™€ RankingQueryService ์ถ”๊ฐ€. ์ผ์ผ ์ธ๊ธฐ ์ƒํ’ˆ ์กฐํšŒ ๋ฐ ์ƒํ’ˆ ์ˆœ์œ„ ์ ์ˆ˜ ์กฐํšŒ ๊ธฐ๋Šฅ ์ œ๊ณต.
Commerce API ์ˆœ์œ„ REST ์—”๋“œํฌ์ธํŠธ
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
/api/v1/ranking GET ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€. ์„ ํƒ์  ๋‚ ์งœ ๋ฐ ํฌ๊ธฐ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ผ์ผ ์ˆœ์œ„ ์กฐํšŒ.
Commerce API ์ƒํ’ˆ ํ†ตํ•ฉ
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java, ProductV1Dto.java
ProductDetailResponse์— rankingScore ํ•„๋“œ ์ถ”๊ฐ€. ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ์ˆœ์œ„ ์ ์ˆ˜ ํฌํ•จ.
Commerce Streamer ์ˆœ์œ„ ์ง‘๊ณ„
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregationService.java
์ผ์ผ ์ˆœ์œ„ ์ ์ˆ˜ ์ฆ๊ฐ€(applyLike) ๋ฐ ์ •์ผ ์ˆœ์œ„ ์ด์›”(carryOverDailyRanking) ๊ธฐ๋Šฅ. 23:30์— ์ž๋™ ์˜ˆ์•ฝ๋˜์–ด ์ด์ „ ๋‚ ์งœ ์ ์ˆ˜๋ฅผ ๊ฐ€์ค‘์น˜ 0.1๋กœ ์ด์›”.
Commerce Streamer ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductLikeEventConsumer.java
RankingAggregationService ์˜์กด์„ฑ ์ถ”๊ฐ€. ์ƒํ’ˆ ์ข‹์•„์š” ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์‹œ ์ˆœ์œ„ ์ง‘๊ณ„ ํ˜ธ์ถœ.
Kafka ํ…Œ์ŠคํŠธ ์ธํ”„๋ผ
modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java, com/loopers/utils/KafkaCleanUp.java, modules/kafka/src/main/resources/kafka.yml
Testcontainers ๊ธฐ๋ฐ˜ Kafka ํ…Œ์ŠคํŠธ ์„ค์ • ํด๋ž˜์Šค ๋ฐ ํ† ํ”ฝ/์ปจ์Šˆ๋จธ ๊ทธ๋ฃน ์ •๋ฆฌ ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ถ”๊ฐ€. Kafka ๋ถ€ํŠธ์ŠคํŠธ๋žฉ ์„œ๋ฒ„ ํ™˜๊ฒฝ๋ณ€์ˆ˜ํ™”.
ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ
apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingAggregationServiceTest.java, apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
์ˆœ์œ„ ์ด์›” ๋กœ์ง ํ…Œ์ŠคํŠธ ๋ฐ Kafka ํ…Œ์ŠคํŠธ ์ธํ”„๋ผ ํ†ตํ•ฉ. Testcontainers ๊ธฐ๋ฐ˜ ์„ค์ •์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜.

Sequence Diagram(s)

sequenceDiagram
    participant PLE as ProductLikeEvent
    participant Consumer as ProductLikeEventConsumer
    participant RAS as RankingAggregationService
    participant Redis as Redis ZSET
    participant Scheduler as Scheduler (23:30)

    PLE->>Consumer: ์ข‹์•„์š” ์ด๋ฒคํŠธ ์ˆ˜์‹ 
    Consumer->>RAS: applyLike(productId, occurredAt)
    RAS->>Redis: ZSET ์ ์ˆ˜ ์ฆ๊ฐ€ (0.2 LIKE_WEIGHT)
    Redis-->>RAS: ์™„๋ฃŒ
    RAS->>Redis: ์ผ์ผ ํ‚ค TTL ์„ค์ • (2์ผ)

    rect rgb(200, 220, 250)
    Note over Scheduler,Redis: ๋งค์ผ 23:30 ์‹คํ–‰
    Scheduler->>RAS: carryOverDailyRanking()
    RAS->>Redis: ZUNIONSTORE (๊ฐ€์ค‘์น˜: 0.1)
    RAS->>Redis: ๋‚ด์ผ ํ‚ค TTL ์„ค์ •
    Redis-->>RAS: ์ด์›” ์™„๋ฃŒ
    end
Loading
sequenceDiagram
    participant Client as ํด๋ผ์ด์–ธํŠธ
    participant Controller as ProductV1Controller
    participant RankingService as RankingQueryService
    participant ProductService as ProductQueryService
    participant Redis as Redis

    Client->>Controller: ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์š”์ฒญ
    Controller->>RankingService: getDailyRankingScore(productId)
    RankingService->>Redis: ์ผ์ผ ์ˆœ์œ„ ์ ์ˆ˜ ์กฐํšŒ
    Redis-->>RankingService: ์ ์ˆ˜ ๋ฐ˜ํ™˜
    
    Controller->>ProductService: ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ
    ProductService-->>Controller: ์ƒํ’ˆ ์ •๋ณด ๋ฐ˜ํ™˜
    
    Controller-->>Client: ProductDetailResponse (content + rankingScore)
Loading

Estimated code review effort

๐ŸŽฏ 3 (Moderate) | โฑ๏ธ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

๐Ÿฐ Redis์˜ ์ •๋ ฌ๋œ ์ง‘ํ•ฉ ์œ„๋กœ,
ํ•˜๋ฃจํ•˜๋ฃจ ์ ์ˆ˜ ์Œ“์—ฌ๋งŒ ๊ฐ€๊ณ ,
๋ฐค ์—ด์‹œ๋ฐ˜์—” ์‚ด๋ฉฐ์‹œ ๊ฐ€์ค‘์น˜๋ฅผ ์ž…ํ˜€,
๋‚ด์ผ์˜ ์ˆœ์œ„๊ฐ€ ์ ์ ์ด ์ƒ๊ฒจ๋‚˜๋„ค!
์ข‹์•„์š”๋ผ๋Š” ์ถ•๋ณต์„ ์ˆ˜์ง‘ํ•˜๋Š” ์ƒํ’ˆ๋“ค์˜ ํ–ฅ์—ฐ~ ๐ŸŒŸ

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 29.03% 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 ์ œ๋ชฉ์€ '[volume-9] Product Ranking with Redis'๋กœ, ์‹ค์ œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ธ 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: 5

๐Ÿงน Nitpick comments (13)
modules/redis/src/main/java/com/loopers/ranking/RankingPolicy.java (1)

1-7: ๋นˆ ํ”Œ๋ ˆ์ด์Šคํ™€๋” ํด๋ž˜์Šค - ๊ตฌํ˜„ ํ•„์š”

ํ˜„์žฌ RankingPolicy ํด๋ž˜์Šค๋Š” ์ฃผ์„๋งŒ ์žˆ๊ณ  ๊ตฌํ˜„์ด ์—†๋Š” ํ”Œ๋ ˆ์ด์Šคํ™€๋”์ž…๋‹ˆ๋‹ค. PR ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ฅด๋ฉด ์ด๋ฒคํŠธ ํƒ€์ž…๋ณ„๋กœ ๊ฐ€์ค‘์น˜๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๋กœ์ง์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค (์˜ˆ: ProductLike โ†’ 0.2, ์ผ์ผ ์ด์›” โ†’ 0.1).

๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ตฌํ˜„ ๋ฐฉ์‹์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”:

  • Enum ๊ธฐ๋ฐ˜: ๊ฐ ์ด๋ฒคํŠธ ํƒ€์ž…์— ๊ฐ€์ค‘์น˜๋ฅผ ๋งคํ•‘
  • Strategy ํŒจํ„ด: ์ธํ„ฐํŽ˜์ด์Šค์™€ ๊ตฌํ˜„์ฒด๋กœ ๋ถ„๋ฆฌ
  • ์ •์  ์ƒ์ˆ˜/๋ฉ”์„œ๋“œ: ๊ฐ„๋‹จํ•œ ๋งคํ•‘ ๋กœ์ง

๊ตฌํ˜„ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜, ์ด ์ž‘์—…์„ ์ถ”์ ํ•˜๊ธฐ ์œ„ํ•œ ์ด์Šˆ๋ฅผ ์ƒ์„ฑํ•ด๋“œ๋ฆด๊นŒ์š”?

modules/kafka/src/main/resources/kafka.yml (1)

18-18: TODO ์ฃผ์„์„ ํ•ด๊ฒฐํ•˜๊ฑฐ๋‚˜ ์ด์Šˆ๋กœ ์ถ”์ ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

ํ”„๋กœ๋“€์„œ์˜ acks, enable.idempotence, max.in.flight.requests.per.connection ์„ค์ •์€ ๋ฉ”์‹œ์ง€ ์ „์†ก ์‹ ๋ขฐ์„ฑ๊ณผ ์ˆœ์„œ ๋ณด์žฅ์— ์ค‘์š”ํ•œ ์˜ํ–ฅ์„ ๋ฏธ์นฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์„ค์ •์ด ํ”„๋กœ์ ํŠธ ์š”๊ตฌ์‚ฌํ•ญ์— ์ ํ•ฉํ•œ์ง€ ๊ฒ€ํ† ํ•˜๊ณ , TODO๋ฅผ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ๋ณ„๋„ ์ด์Šˆ๋กœ ์ถ”์ ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

์ด ์„ค์ •๋“ค์— ๋Œ€ํ•œ ๊ถŒ์žฅ ๊ตฌ์„ฑ์„ ์ œ์•ˆํ•˜๊ฑฐ๋‚˜ ๊ด€๋ จ ๋ฌธ์„œ๋ฅผ ์ฐพ์•„๋“œ๋ฆด๊นŒ์š”?

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

7-7: ๋ฏธ์‚ฌ์šฉ import๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.

RankingEntry import๊ฐ€ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Line 49์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํƒ€์ž…์€ OptionalDouble์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
-import com.loopers.ranking.RankingEntry;
apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java (1)

44-44: ์ฃผ์„ ์ฒ˜๋ฆฌ๋œ ์ฝ”๋“œ์˜ ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜์„ธ์š”.

resetAllTestTopics() ํ˜ธ์ถœ์ด ์ฃผ์„ ์ฒ˜๋ฆฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์˜๋„์ ์ธ ๋ณ€๊ฒฝ์ธ์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ํ† ํ”ฝ ๋ฆฌ์…‹์ด ๋ถˆํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ฃผ์„์ด ์•„๋‹Œ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

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

3-3: ๋ฏธ์‚ฌ์šฉ import๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.

RankingEntry import๊ฐ€ ์ด ํŒŒ์ผ์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
-import com.loopers.ranking.RankingEntry;

36-38: null ๋Œ€์‹  OptionalDouble.empty()๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

rankingScore๊ฐ€ ์—†์„ ๋•Œ null์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋Œ€์‹  OptionalDouble.empty()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์˜๋ฏธ๊ฐ€ ๋” ๋ช…ํ™•ํ•ด์ง‘๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
         static <T> ProductDetailResponse<T> of(T content) {
-            return new ProductDetailResponse<>(content, null);
+            return new ProductDetailResponse<>(content, OptionalDouble.empty());
         }
modules/redis/src/main/java/com/loopers/ranking/RankingKey.java (1)

11-16: Javadoc์„ ์™„์„ฑํ•˜์„ธ์š”.

๋ฉ”์†Œ๋“œ์˜ @param๊ณผ @return ํƒœ๊ทธ์— ์„ค๋ช…์ด ๋ˆ„๋ฝ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ๋ฐ˜ํ™˜๊ฐ’์˜ ์˜๋ฏธ๋ฅผ ๋ช…ํ™•ํžˆ ๋ฌธ์„œํ™”ํ•˜์„ธ์š”.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
     /**
      * ์ผ๋ณ„ ๋žญํ‚น ๋ ˆ๋””์Šคํ‚ค๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
      * ranking:all:{yyyyMMdd}
-     * @param date
-     * @return
+     * @param date ๋žญํ‚น์„ ์กฐํšŒํ•  ๋‚ ์งœ
+     * @return Redis์— ์ €์žฅํ•  ์ผ๋ณ„ ๋žญํ‚น ํ‚ค (ํ˜•์‹: ranking:all:yyyyMMdd)
      */
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)

18-24: ์‘๋‹ต ๋ž˜ํผ ์ผ๊ด€์„ฑ ๊ฒ€ํ†  ํ•„์š”

ProductV1Controller๋Š” ApiResponse<>๋กœ ์‘๋‹ต์„ ๊ฐ์‹ธ๊ณ  ์žˆ์ง€๋งŒ, ์ด ์ปจํŠธ๋กค๋Ÿฌ๋Š” RankingQueryResponse๋ฅผ ์ง์ ‘ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. API ์‘๋‹ต ํ˜•์‹์˜ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ๋™์ผํ•œ ํŒจํ„ด์„ ์ ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
-    public RankingQueryResponse getDailyRanking(
+    public ApiResponse<RankingQueryResponse> getDailyRanking(
             @RequestParam(required = false, name = "date") String date,
             @RequestParam(defaultValue = "20", name = "size") int size
     ) {
-        return rankingQueryService.getDailyPopularProducts(date, size);
+        return ApiResponse.success(rankingQueryService.getDailyPopularProducts(date, size));
     }
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryResponse.java (1)

9-16: ํ•„๋“œ ๋ช…๋ช… ์ผ๊ด€์„ฑ: ๋ณต์ˆ˜ํ˜• ์‚ฌ์šฉ ๊ถŒ์žฅ

productLikeSummary ํ•„๋“œ๊ฐ€ List<ProductLikeSummary>๋ฅผ ๋‹ด๊ณ  ์žˆ์œผ๋ฏ€๋กœ, ๋ณต์ˆ˜ํ˜• productLikeSummaries๋กœ ๋ช…๋ช…ํ•˜๋Š” ๊ฒƒ์ด ๋” ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
 public record RankingQueryResponse(
         LocalDate date,
         List<RankingEntry> rankingEntries,
-        List<ProductLikeSummary> productLikeSummary
+        List<ProductLikeSummary> productLikeSummaries
 ) {
-    public static RankingQueryResponse of(LocalDate date, List<RankingEntry> rankingEntries, List<ProductLikeSummary> productLikeSummary) {
-        return new RankingQueryResponse(date, rankingEntries, productLikeSummary);
+    public static RankingQueryResponse of(LocalDate date, List<RankingEntry> rankingEntries, List<ProductLikeSummary> productLikeSummaries) {
+        return new RankingQueryResponse(date, rankingEntries, productLikeSummaries);
     }
 }
modules/redis/src/main/java/com/loopers/ranking/RankingZSetRepository.java (1)

30-33: TODO ์ฝ”๋ฉ˜ํŠธ ํ•ด๊ฒฐ ํ•„์š”

"๋žญํ‚น์ •๋ณด๊ฐ€ ์—†๋‹ค๋ฉด?" TODO๊ฐ€ ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค. ๋นˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ˜„์žฌ ๋™์ž‘์ด ์˜๋„๋œ ๊ฒƒ์ธ์ง€, ์•„๋‹ˆ๋ฉด ์ถ”๊ฐ€ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ์ง€ ๋ช…ํ™•ํžˆ ํ•ด์ฃผ์„ธ์š”.

์ด TODO๋ฅผ ํ•ด๊ฒฐํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ์šด ์ด์Šˆ๋กœ ๋“ฑ๋กํ•ด ๋“œ๋ฆด๊นŒ์š”?

apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregationService.java (1)

23-32: ํƒ€์ž„์กด ๋ช…์‹œ์  ์ง€์ • ๊ถŒ์žฅ

ZoneId.systemDefault()๋Š” ์„œ๋ฒ„ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์–ด ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์—์„œ ์ผ๊ด€์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋น„์Šค ์ง€์—ญ์— ๋งž๋Š” ๋ช…์‹œ์  ํƒ€์ž„์กด(์˜ˆ: ZoneId.of("Asia/Seoul")) ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
+    private static final ZoneId SERVICE_ZONE = ZoneId.of("Asia/Seoul");
+
     public void applyLike(long productId, Instant occurredAt) {
-        LocalDate day = occurredAt.atZone(ZoneId.systemDefault()).toLocalDate();
+        LocalDate day = occurredAt.atZone(SERVICE_ZONE).toLocalDate();
         String key = RankingKey.dailyAll(day);
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.java (1)

54-56: ๋ฉ”์„œ๋“œ ๋ช…๋ช…์ด ํ˜ผ๋ž€์Šค๋Ÿฝ์Šต๋‹ˆ๋‹ค

hasValidDate๊ฐ€ ๋‚ ์งœ๊ฐ€ null์ด๊ฑฐ๋‚˜ ๋น„์–ด์žˆ์„ ๋•Œ true๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฆ„์ด ๋กœ์ง๊ณผ ๋ฐ˜๋Œ€์ž…๋‹ˆ๋‹ค. isNullOrBlank ๋˜๋Š” shouldUseDefaultDate์™€ ๊ฐ™์ด ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๋Š” ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜์„ธ์š”.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
-    private boolean hasValidDate(String date) {
-        return date == null || date.isBlank();
+    private boolean shouldUseDefaultDate(String date) {
+        return date == null || date.isBlank();
     }
modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.java (1)

76-81: Thread.sleep ๋Œ€์‹  ํด๋ง ๋ฐฉ์‹ ๊ณ ๋ ค

Thread.sleep(1000)์€ ํ† ํ”ฝ ์‚ญ์ œ ์™„๋ฃŒ๋ฅผ ๋ณด์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ† ํ”ฝ ๋ชฉ๋ก์—์„œ ์‚ญ์ œ๋˜์—ˆ๋Š”์ง€ ํด๋งํ•˜๋Š” ๋ฐฉ์‹์ด ๋” ์•ˆ์ •์ ์ž…๋‹ˆ๋‹ค. ๋‹ค๋งŒ, ํ…Œ์ŠคํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ์—์„œ๋Š” ํ˜„์žฌ ์ ‘๊ทผ ๋ฐฉ์‹๋„ ์‹ค์šฉ์ ์ž…๋‹ˆ๋‹ค.

๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between ecc5479 and 86dc621.

๐Ÿ“’ Files selected for processing (17)
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryResponse.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.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/RankingV1Controller.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregationService.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductLikeEventConsumer.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingAggregationServiceTest.java
  • modules/kafka/src/main/resources/kafka.yml
  • modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java
  • modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.java
  • modules/redis/src/main/java/com/loopers/ranking/DailyRankingResponse.java
  • modules/redis/src/main/java/com/loopers/ranking/RankingEntry.java
  • modules/redis/src/main/java/com/loopers/ranking/RankingKey.java
  • modules/redis/src/main/java/com/loopers/ranking/RankingPolicy.java
  • modules/redis/src/main/java/com/loopers/ranking/RankingZSetRepository.java
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (7)
๐Ÿ“š Learning: 2025-12-19T21:30:16.024Z
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:16.024Z
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/main/java/com/loopers/interfaces/consumer/ProductLikeEventConsumer.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
๐Ÿ“š 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/interfaces/consumer/ProductLikeEventConsumer.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.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/ProductLikeEventConsumer.java
๐Ÿ“š Learning: 2025-12-18T13:24:54.339Z
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:54.339Z
Learning: In this codebase, Kafka consumers delegate error handling and event tracking to the service layer via EventInboxAspect. Service methods annotated with InboxEvent are intercepted by the aspect, which handles failures by saving failed EventInbox entries and logging errors. This centralized approach avoids duplicating error handling logic across multiple consumers.

Applied to files:

  • apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
๐Ÿ“š Learning: 2025-12-19T23:39:20.851Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-streamer/src/main/kotlin/com/loopers/interfaces/consumer/product/ProductEventConsumer.kt:0-0
Timestamp: 2025-12-19T23:39:20.851Z
Learning: In Loopers-dev-lab/loopers-spring-kotlin-template, toongri prefers pragmatic idempotency handling in Kafka consumers: when idempotency key persistence fails after successful business logic execution, log a warning and continue (accepting low risk of duplicates on retry) rather than rolling back business data. This keeps business logic decoupled from idempotency store (which might be Redis/external system, not RDB), avoids over-engineering for rare edge cases, and prioritizes core business value over auxiliary metadata. Transaction-based atomicity between business data and idempotency keys is considered excessive when duplicate event probability is very low.

Applied to files:

  • apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
  • modules/kafka/src/main/resources/kafka.yml
๐Ÿ“š Learning: 2025-12-19T03:23:54.756Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 191
File: modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java:24-34
Timestamp: 2025-12-19T03:23:54.756Z
Learning: In the loopers-spring-java-template project, test container configurations (MySQL, Redis, Kafka) use static initialization blocks for consistency across the template. This pattern should be maintained for uniformity, even if alternative lifecycle management approaches exist.

Applied to files:

  • apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
๐Ÿ“š Learning: 2025-12-19T03:23:51.674Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 191
File: modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java:24-34
Timestamp: 2025-12-19T03:23:51.674Z
Learning: Maintain the use of static initialization blocks in test fixtures for testcontainers configurations (e.g., MySQL, Redis, Kafka) to ensure consistency and predictable initialization across the loopers-spring-java-template project. Apply this pattern to all test-fixture Java files under src/testFixtures/java to preserve uniform lifecycle behavior, unless there's a compelling, well-justified reason to diverge.

Applied to files:

  • modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.java
  • modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java
๐Ÿงฌ Code graph analysis (6)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
  • RequiredArgsConstructor (19-52)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
  • RequiredArgsConstructor (11-26)
apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingAggregationServiceTest.java (1)
modules/redis/src/main/java/com/loopers/ranking/RankingKey.java (1)
  • RankingKey (9-21)
modules/redis/src/main/java/com/loopers/ranking/RankingZSetRepository.java (1)
modules/redis/src/main/java/com/loopers/ranking/RankingKey.java (1)
  • RankingKey (9-21)
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregationService.java (1)
modules/redis/src/main/java/com/loopers/ranking/RankingKey.java (1)
  • RankingKey (9-21)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
  • RequiredArgsConstructor (19-52)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
  • RequiredArgsConstructor (11-26)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
  • ProductV1Dto (9-44)
๐Ÿ”‡ Additional comments (9)
modules/redis/src/main/java/com/loopers/ranking/RankingEntry.java (1)

1-4: LGTM!

๊ฐ„๊ฒฐํ•˜๊ณ  ๋ช…ํ™•ํ•œ record ์ •์˜์ž…๋‹ˆ๋‹ค. ๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ์œ„ํ•œ double ํƒ€์ž… ์‚ฌ์šฉ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

47-51: LGTM!

๋žญํ‚น ์ ์ˆ˜๋ฅผ ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด์— ํ†ตํ•ฉํ•˜๋Š” ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. OptionalDouble์„ ์‚ฌ์šฉํ•œ nullable ์ฒ˜๋ฆฌ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

modules/redis/src/main/java/com/loopers/ranking/DailyRankingResponse.java (1)

1-7: LGTM!

์ผ๋ณ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด๋Š” ๊ฐ„๊ฒฐํ•œ record ์ •์˜์ž…๋‹ˆ๋‹ค. LocalDate์™€ List ์กฐํ•ฉ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

modules/redis/src/main/java/com/loopers/ranking/RankingKey.java (1)

17-19: LGTM!

Redis ํ‚ค ์ƒ์„ฑ ๋กœ์ง์ด ๋ช…ํ™•ํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. BASIC_ISO_DATE ํฌ๋งท ์‚ฌ์šฉ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductLikeEventConsumer.java (1)

26-26: LGTM!

๋žญํ‚น ์ง‘๊ณ„ ์„œ๋น„์Šค๊ฐ€ ๊น”๋”ํ•˜๊ฒŒ ํ†ตํ•ฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฉ”ํŠธ๋ฆญ ์ง‘๊ณ„ ํ›„ ๋žญํ‚น ์ง‘๊ณ„๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ์ˆœ์„œ๊ฐ€ ์ ์ ˆํ•˜๋ฉฐ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” ๊ธฐ์กด ํŒจํ„ด๋Œ€๋กœ aspect์— ์œ„์ž„๋˜์–ด ์ผ๊ด€์„ฑ์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

Also applies to: 47-47

modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java (1)

28-46: LGTM!

์ •์  ์ดˆ๊ธฐํ™” ๋ธ”๋ก ํŒจํ„ด์ด ํ”„๋กœ์ ํŠธ์˜ testcontainers ์„ค์ • ์ปจ๋ฒค์…˜๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. ์ปจํ…Œ์ด๋„ˆ ์‹œ์ž‘, ๋™์  ํฌํŠธ ์ฃผ์ž…, ํ† ํ”ฝ ๋นˆ ์ •์˜๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Based on learnings, ์ด ํŒจํ„ด์€ ํ”„๋กœ์ ํŠธ ์ „๋ฐ˜์—์„œ ์ผ๊ด€๋˜๊ฒŒ ์œ ์ง€๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

modules/redis/src/main/java/com/loopers/ranking/RankingZSetRepository.java (1)

35-40: NumberFormatException ์ฒ˜๋ฆฌ ํ•„์š”

ZSET ๋ฉค๋ฒ„๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฐ’์„ ํฌํ•จํ•  ๊ฒฝ์šฐ Long.parseLong()์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋ฐฉ์–ด์  ์ฝ”๋”ฉ์„ ์œ„ํ•ด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์ด ๋ณด์žฅ๋˜๋Š”์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
         for(ZSetOperations.TypedTuple<String> tuple : tuples) {
             if(tuple.getValue() == null) continue;
-            long productId = Long.parseLong(tuple.getValue());
-            double score = tuple.getScore() == null ? 0d : tuple.getScore();
-            result.add(new RankingEntry(productId, score));
+            try {
+                long productId = Long.parseLong(tuple.getValue());
+                double score = tuple.getScore() == null ? 0d : tuple.getScore();
+                result.add(new RankingEntry(productId, score));
+            } catch (NumberFormatException e) {
+                // ์ž˜๋ชป๋œ ํ˜•์‹์˜ ๋ฉค๋ฒ„๋Š” ๊ฑด๋„ˆ๋œ€
+                continue;
+            }
         }
apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingAggregationServiceTest.java (1)

38-69: ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ์–‘ํ˜ธ

Carry-over ๋กœ์ง์˜ ํ•ต์‹ฌ ์‹œ๋‚˜๋ฆฌ์˜ค(๊ฐ€์ค‘์น˜ ์ ์šฉ ๋ฐ TTL ์„ค์ •)๋ฅผ ์ ์ ˆํžˆ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ถ”๊ฐ€๋กœ ์—ฃ์ง€ ์ผ€์ด์Šค(๋นˆ ์†Œ์Šค, ๊ธฐ์กด ํƒ€๊ฒŸ ์กด์žฌ ์‹œ)์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋„ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”.

modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.java (1)

31-46: ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ ์œ ํ‹ธ๋ฆฌํ‹ฐ ์„ค๊ณ„ ์–‘ํ˜ธ

KafkaAdmin์„ ์ฃผ์ž…๋ฐ›์•„ ๋™์ ์œผ๋กœ AdminClient๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ์‹๊ณผ, ํ† ํ”ฝ/์ปจ์Šˆ๋จธ ๊ทธ๋ฃน ๊ด€๋ฆฌ ๋ฉ”์„œ๋“œ๋“ค์ด ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ฒฉ๋ฆฌ์— ํšจ๊ณผ์ ์ธ ์œ ํ‹ธ๋ฆฌํ‹ฐ์ž…๋‹ˆ๋‹ค.

Comment on lines +65 to +81
private List<ProductLikeSummary> findProductSummaryFrom(List<RankingEntry> rankingEntries) {
List<ProductLikeSummary> result = new ArrayList<>();

for(RankingEntry rankingEntry : rankingEntries) {
ProductLikeSummary summary;
try {
summary = productQueryService.getProductLikeSummary(rankingEntry.productId());
} catch (CoreException e) {
if(e.getErrorType() == ErrorType.NOT_FOUND) {
log.error("Could not find product like summary for {}", rankingEntry.productId());
}
summary = null;
}
result.add(summary);
}
return result;
}
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

N+1 ์ฟผ๋ฆฌ ํŒจํ„ด ๋ฐ null ์ฒ˜๋ฆฌ ๊ฐœ์„  ํ•„์š”

  1. N+1 ์ฟผ๋ฆฌ: ๊ฐ ๋žญํ‚น ํ•ญ๋ชฉ๋งˆ๋‹ค ๊ฐœ๋ณ„ DB ์กฐํšŒ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. productId ๋ชฉ๋ก์œผ๋กœ ์ผ๊ด„ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์ด ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.

  2. Null ์ฒ˜๋ฆฌ: ์ฐพ์ง€ ๋ชปํ•œ ์ƒํ’ˆ์— ๋Œ€ํ•ด null์„ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ์—์„œ NPE๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•„ํ„ฐ๋งํ•˜๊ฑฐ๋‚˜ ๋ช…์‹œ์ ์ธ "์‚ญ์ œ๋จ" ํ‘œ์‹œ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

  3. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ: NOT_FOUND๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ์˜ˆ์™ธ๋„ ๋ฌด์‹œ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
     private List<ProductLikeSummary> findProductSummaryFrom(List<RankingEntry> rankingEntries) {
-        List<ProductLikeSummary> result = new ArrayList<>();
-
-        for(RankingEntry rankingEntry : rankingEntries) {
-            ProductLikeSummary summary;
-            try {
-                summary = productQueryService.getProductLikeSummary(rankingEntry.productId());
-            } catch (CoreException e) {
-                if(e.getErrorType() == ErrorType.NOT_FOUND) {
-                    log.error("Could not find product like summary for {}", rankingEntry.productId());
-                }
-                summary =  null;
-            }
-            result.add(summary);
+        List<Long> productIds = rankingEntries.stream()
+                .map(RankingEntry::productId)
+                .toList();
+        
+        // TODO: ProductQueryService์— ์ผ๊ด„ ์กฐํšŒ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ ๊ณ ๋ ค
+        List<ProductLikeSummary> result = new ArrayList<>();
+        for (RankingEntry rankingEntry : rankingEntries) {
+            try {
+                ProductLikeSummary summary = productQueryService.getProductLikeSummary(rankingEntry.productId());
+                result.add(summary);
+            } catch (CoreException e) {
+                if (e.getErrorType() == ErrorType.NOT_FOUND) {
+                    log.warn("Product not found for ranking entry: {}", rankingEntry.productId());
+                    // null ๋Œ€์‹  ๊ฑด๋„ˆ๋›ฐ๊ธฐ
+                } else {
+                    throw e; // NOT_FOUND๊ฐ€ ์•„๋‹Œ ์˜ˆ์™ธ๋Š” ์ „ํŒŒ
+                }
+            }
         }
         return result;
     }
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.java
around lines 65-81, the current loop causes N+1 queries, inserts nulls for
missing products and swallows non-NOT_FOUND exceptions; change to collect all
productIds from rankingEntries and call productQueryService to fetch summaries
in bulk, build a Map<productId,ProductLikeSummary>, iterate rankingEntries to
map to summaries while skipping or marking missing products (do not add nulls),
and for exceptions other than NOT_FOUND either log with details and rethrow or
propagate so failures aren't silently ignored; preserve original ranking order
in the final list.

Comment on lines +71 to +76
private void setTTLOnlyOnce(String key) {
Boolean exists = redisTemplate.hasKey(key);
if (Boolean.FALSE.equals(exists)) {
redisTemplate.expire(key, TTL);
}
}
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

TTL์ด ์„ค์ •๋˜์ง€ ์•Š๋Š” ์‹ฌ๊ฐํ•œ ๋ฒ„๊ทธ

setTTLOnlyOnce๊ฐ€ incrementScore ์ดํ›„์— ํ˜ธ์ถœ๋˜๋ฏ€๋กœ, ์ด ์‹œ์ ์—์„œ ํ‚ค๋Š” ํ•ญ์ƒ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ hasKey๊ฐ€ ํ•ญ์ƒ true๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ TTL์ด ์ ˆ๋Œ€ ์„ค์ •๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
-    private void setTTLOnlyOnce(String key) {
-        Boolean exists = redisTemplate.hasKey(key);
-        if (Boolean.FALSE.equals(exists)) {
-            redisTemplate.expire(key, TTL);
-        }
-    }
+    /**
+     * TTL์ด ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋งŒ TTL์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
+     */
+    private void setTTLOnlyOnce(String key) {
+        Long currentTtl = redisTemplate.getExpire(key);
+        // TTL์ด -1์ด๋ฉด ๋งŒ๋ฃŒ ์‹œ๊ฐ„์ด ์„ค์ •๋˜์ง€ ์•Š์€ ์ƒํƒœ
+        if (currentTtl != null && currentTtl == -1) {
+            redisTemplate.expire(key, TTL);
+        }
+    }
๐Ÿ“ 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
private void setTTLOnlyOnce(String key) {
Boolean exists = redisTemplate.hasKey(key);
if (Boolean.FALSE.equals(exists)) {
redisTemplate.expire(key, TTL);
}
}
/**
* TTL์ด ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋งŒ TTL์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
*/
private void setTTLOnlyOnce(String key) {
Long currentTtl = redisTemplate.getExpire(key);
// TTL์ด -1์ด๋ฉด ๋งŒ๋ฃŒ ์‹œ๊ฐ„์ด ์„ค์ •๋˜์ง€ ์•Š์€ ์ƒํƒœ
if (currentTtl != null && currentTtl == -1) {
redisTemplate.expire(key, TTL);
}
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregationService.java
around lines 71-76, setTTLOnlyOnce currently checks hasKey after incrementScore,
so the key always exists and TTL is never set; change the logic to set TTL when
the key is newly created โ€” e.g., after performing redis increment, check the
returned incremented value and call redisTemplate.expire(key, TTL) only if the
increment result equals 1 (meaning the key was just created), or alternatively
call a set-if-absent before increment and set TTL when that returns true; update
setTTLOnlyOnce or incrementScore accordingly so TTL is applied exactly once when
the key is created.

Comment on lines +69 to +71
Map<String, Object> consumerProps = kafkaProperties.buildConsumerProperties();
consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "${spring.kafka.consumer.group-id}");

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

consumer properties ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Line 70์—์„œ "${spring.kafka.consumer.group-id}" ๋ฌธ์ž์—ด ๋ฆฌํ„ฐ๋Ÿด์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ์ด๋Š” Spring ํ”Œ๋ ˆ์ด์Šคํ™€๋” ๋ฌธ๋ฒ•์œผ๋กœ ๋Ÿฐํƒ€์ž„์— ํ•ด์„๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ์ด๋ ‡๊ฒŒ ์ƒ์„ฑํ•œ consumerProps ๋งต์ด ์‹ค์ œ๋กœ ์‚ฌ์šฉ๋˜์ง€ ์•Š๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์‹ค์ œ group-id ๊ฐ’์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•˜์„ธ์š”:

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
-        Map<String, Object> consumerProps = kafkaProperties.buildConsumerProperties();
-        consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "${spring.kafka.consumer.group-id}");
+        // ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉํ•  ์‹ค์ œ consumer group id
+        String actualGroupId = kafkaProperties.getConsumer().getGroupId();

๋งŒ์•ฝ ์ด ์ฝ”๋“œ๊ฐ€ ์‹ค์ œ๋กœ ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค๋ฉด ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

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

๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
around lines 69 to 71, the test sets
consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG,
"${spring.kafka.consumer.group-id}") which incorrectly uses a Spring placeholder
string (and the consumerProps map is never used); either remove the unused
consumerProps block entirely, or replace the placeholder with the real group-id
value retrieved from the KafkaProperties object (e.g.,
kafkaProperties.getConsumer().getGroupId()) or from the Environment, and ensure
the populated consumerProps map is actually passed into the consumer setup so
the test uses the correct group-id at runtime.

Comment on lines +65 to +68
// TTL ํ™•์ธ (expire๋ฅผ ๋จผ์ € ๊ฑธ๊ณ  unionstore๋ฅผ ํ•˜๋ฏ€๋กœ TTL์ด ์กด์žฌํ•ด์•ผ ํ•จ)
Long ttl = redisTemplate.getExpire(targetKey); // seconds
assertThat(ttl).isNotNull();
assertThat(ttl).isGreaterThan(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

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

์ฃผ์„์— "expire๋ฅผ ๋จผ์ € ๊ฑธ๊ณ  unionstore๋ฅผ ํ•˜๋ฏ€๋กœ"๋ผ๊ณ  ๋˜์–ด ์žˆ์ง€๋งŒ, RankingAggregationService.carryOver()๋Š” ZUNIONSTORE ์‹คํ–‰ ํ›„ EXPIRE๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์„์„ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜ ์ œ๊ฑฐํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
-        // TTL ํ™•์ธ (expire๋ฅผ ๋จผ์ € ๊ฑธ๊ณ  unionstore๋ฅผ ํ•˜๋ฏ€๋กœ TTL์ด ์กด์žฌํ•ด์•ผ ํ•จ)
+        // TTL ํ™•์ธ (ZUNIONSTORE ํ›„ EXPIRE๋ฅผ ์„ค์ •ํ•˜๋ฏ€๋กœ TTL์ด ์กด์žฌํ•ด์•ผ ํ•จ)
         Long ttl = redisTemplate.getExpire(targetKey); // seconds
๐Ÿ“ 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
// TTL ํ™•์ธ (expire๋ฅผ ๋จผ์ € ๊ฑธ๊ณ  unionstore๋ฅผ ํ•˜๋ฏ€๋กœ TTL์ด ์กด์žฌํ•ด์•ผ ํ•จ)
Long ttl = redisTemplate.getExpire(targetKey); // seconds
assertThat(ttl).isNotNull();
assertThat(ttl).isGreaterThan(0);
// TTL ํ™•์ธ (ZUNIONSTORE ํ›„ EXPIRE๋ฅผ ์„ค์ •ํ•˜๋ฏ€๋กœ TTL์ด ์กด์žฌํ•ด์•ผ ํ•จ)
Long ttl = redisTemplate.getExpire(targetKey); // seconds
assertThat(ttl).isNotNull();
assertThat(ttl).isGreaterThan(0);
๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingAggregationServiceTest.java
around lines 65 to 68, the inline comment wrongly states that EXPIRE is set
before unionstore whereas RankingAggregationService.carryOver() actually calls
ZUNIONSTORE first and then sets EXPIRE; update or remove the comment to reflect
the real order (e.g., "EXPIRE is set after ZUNIONSTORE in carryOver() so TTL
must exist") and keep the existing TTL assertions unchanged.

Comment on lines +104 to +113
// ํ† ํ”ฝ ์ƒ์„ฑ
NewTopic newTopic = TopicBuilder.name(topicName)
.partitions(3)
.replicas(1)
.config("min.insync.replicas", "1")
.build();

adminClient.createTopics(Collections.singletonList(newTopic))
.all()
.get(5, TimeUnit.SECONDS);
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

ํŒŒํ‹ฐ์…˜ ์ˆ˜ ๋ถˆ์ผ์น˜

KafkaTestContainersConfig์—์„œ product-like-events ํ† ํ”ฝ์„ 1๊ฐœ ํŒŒํ‹ฐ์…˜์œผ๋กœ ์ƒ์„ฑํ•˜์ง€๋งŒ, ์ด ์œ ํ‹ธ๋ฆฌํ‹ฐ๋Š” ์žฌ์ƒ์„ฑ ์‹œ 3๊ฐœ ํŒŒํ‹ฐ์…˜์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ๋™์ž‘์— ์ผ๊ด€์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

ํ† ํ”ฝ๋ณ„ ์„ค์ •์„ ์ •์˜ํ•˜๊ฑฐ๋‚˜, KafkaTestContainersConfig์™€ ๋™์ผํ•œ ํŒŒํ‹ฐ์…˜ ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”:

                 NewTopic newTopic = TopicBuilder.name(topicName)
-                        .partitions(3)
+                        .partitions(1)
                         .replicas(1)
                         .config("min.insync.replicas", "1")
                         .build();
๐Ÿค– Prompt for AI Agents
In modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.java
around lines 104 to 113, the topic is being recreated with 3 partitions which
conflicts with KafkaTestContainersConfig that creates the product-like-events
topic with 1 partition; change the NewTopic creation to use the same partition
count as the test container config (e.g., set .partitions(1) for
product-like-events) or implement per-topic partition configuration (lookup the
expected partition count for each topic and use that value when building
NewTopic) so topic recreation matches the test environment.

@junoade junoade merged commit 0f9f582 into Loopers-dev-lab:junoade Dec 31, 2025
1 check 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