Skip to content

Conversation

@rnqhstmd
Copy link
Collaborator

@rnqhstmd rnqhstmd commented Dec 26, 2025

๐Ÿ“Œ Summary

  • Redis ZSET์„ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌํ˜„

๐Ÿ’ฌ Review Points

  • ๊ฐ€์ค‘์น˜ ์‹ค์‹œ๊ฐ„ ๋ณ€๊ฒฝ์„ ์œ„ํ•ด ๊ฐ€์ค‘์น˜ ์ €์žฅ์†Œ๋ฅผ yml์ด ์•„๋‹Œ Redis ์ ์žฌ๋ฅผ ์„ ํƒํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.
  • ๊ฐ€์ค‘์น˜ ๊ด€๋ฆฌ API๋ฅผ commerce-streamer ๋ชจ๋“ˆ์— ๋‘์—ˆ๋Š”๋ฐ, ์ ์ ˆํ•œ์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค.
  • ์ฝœ๋“œ ์Šคํƒ€ํŠธ ํ•ด๊ฒฐ์„ ์œ„ํ•ด ๋งค์ผ 23:50์— ์ „๋‚  ์ ์ˆ˜์˜ 10%๋ฅผ ๋‹ค์Œ๋‚  ํ‚ค์— ๋ณต์‚ฌํ•˜๋Š” ์Šค์ผ€์ฅด๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋งŒ์•ฝ ์ „๋‚  ๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๋ณ„๋„ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋Š” ๋กœ์ง์€ ์—†๋Š”๋ฐ ํ•„์š”ํ• ๊นŒ์š”..?

โœ… Checklist

๐Ÿ“ˆ Ranking Consumer

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

โšพ Ranking API

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

Summary by CodeRabbit

Release Notes

  • New Features
    • ์ƒํ’ˆ ์ˆœ์œ„ ์‹œ์Šคํ…œ ์ถ”๊ฐ€ - ์‚ฌ์šฉ์ž ์กฐํšŒ, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋งค์ผ ์ž๋™ ๊ณ„์‚ฐ
    • ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด์— ํ˜„์žฌ ์ˆœ์œ„ ํ‘œ์‹œ
    • ์ˆœ์œ„ ์กฐํšŒ API ์ถ”๊ฐ€ - ๋‚ ์งœ๋ณ„ ์ „์ฒด ์ˆœ์œ„ ๋ชฉ๋ก ๋ฐ ์ƒ์œ„ ์ƒํ’ˆ ์กฐํšŒ ์ง€์›
    • ๊ด€๋ฆฌ์ž์šฉ ์ˆœ์œ„ ๊ฐ€์ค‘์น˜ ์„ค์ • API - ๊ฐ ์ด๋ฒคํŠธ์˜ ๊ฐ€์ค‘์น˜ ์กฐ์ • ๋ฐ ์ดˆ๊ธฐํ™” ๊ฐ€๋Šฅ

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

@rnqhstmd rnqhstmd self-assigned this Dec 26, 2025
@rnqhstmd rnqhstmd added the enhancement New feature or request label Dec 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 26, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

์ƒํ’ˆ ์ˆœ์œ„ ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. commerce-api์—์„œ๋Š” ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ Redis์—์„œ ๋‹น์ผ ์ˆœ์œ„๋ฅผ ์กฐํšŒํ•˜์—ฌ ์‘๋‹ต์— ํฌํ•จํ•˜๊ณ , commerce-streamer์—์„œ๋Š” ์กฐํšŒ/์ข‹์•„์š”/์ฃผ๋ฌธ ์ด๋ฒคํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ •๋ ฌ ์ง‘ํ•ฉ์— ์ ์ˆ˜๋ฅผ ๋ˆ„์ ํ•ฉ๋‹ˆ๋‹ค. ๋งค์ผ ๋ฐค 11:50์— ์ ์ˆ˜๋ฅผ ๋‹ค์Œ ๋‚ ๋กœ ์ด์›”ํ•˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ํ†ตํ•ด ์ฝœ๋“œ ์Šคํƒ€ํŠธ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

Changes

Cohort / File(s) ๋ณ€๊ฒฝ ์‚ฌํ•ญ
commerce-api - ์ƒํ’ˆ ์ˆœ์œ„ ๋„๋ฉ”์ธ ๋ชจ๋ธ
com/loopers/domain/ranking/RankingEntry.java, com/loopers/domain/ranking/RankingInfo.java, com/loopers/domain/ranking/RankingKey.java, com/loopers/domain/ranking/RankingService.java
์ˆœ์œ„ ์—”ํŠธ๋ฆฌ ๋ ˆ์ฝ”๋“œ, ์ˆœ์œ„ ์ •๋ณด ๋ ˆ์ฝ”๋“œ, Redis ํ‚ค ์œ ํ‹ธ๋ฆฌํ‹ฐ, Redis ์ •๋ ฌ ์ง‘ํ•ฉ ๊ธฐ๋ฐ˜ ์ˆœ์œ„ ์กฐํšŒ ์„œ๋น„์Šค ์ถ”๊ฐ€
commerce-api - ์ƒํ’ˆ ์ˆœ์œ„ ์‘์šฉ ๊ณ„์ธต
com/loopers/application/ranking/RankingCommand.java, com/loopers/application/ranking/RankingPageInfo.java, com/loopers/application/ranking/RankingFacade.java
ํŽ˜์ด์ง• ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ, ์ˆœ์œ„ ํŽ˜์ด์ง€ ์‘๋‹ต, ์ƒํ’ˆ ์กฐํšŒ ๋ฐ ์ˆœ์œ„ ์ •๋ณด ๋งคํ•‘ ๋กœ์ง ์ถ”๊ฐ€
commerce-api - ์ƒํ’ˆ ํ†ตํ•ฉ
com/loopers/application/product/ProductDetailInfo.java, com/loopers/application/product/ProductFacade.java, com/loopers/interfaces/api/product/ProductV1ApiSpec.java, com/loopers/interfaces/api/product/ProductV1Controller.java, com/loopers/interfaces/api/product/ProductV1Dto.java
์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด์— ์ˆœ์œ„ ํ•„๋“œ ์ถ”๊ฐ€, ProductFacade์—์„œ ์ˆœ์œ„ ์กฐํšŒ ํ†ตํ•ฉ, ์ƒํ’ˆ API ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„
commerce-api - ์ˆœ์œ„ API
com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java, com/loopers/interfaces/api/ranking/RankingV1Controller.java, com/loopers/interfaces/api/ranking/RankingV1Dto.java
์ˆœ์œ„ ํŽ˜์ด์ง€ ๋ฐ ์ƒ์œ„ N๊ฐœ ์ˆœ์œ„ ์กฐํšŒ ์—”๋“œํฌ์ธํŠธ, ์š”์ฒญ/์‘๋‹ต DTO ์ถ”๊ฐ€
commerce-api - ์„ค์ •
com/loopers/support/config/ClockConfig.java
Clock ๋นˆ ๊ตฌ์„ฑ ์ถ”๊ฐ€
commerce-api - ํ…Œ์ŠคํŠธ
domain/ranking/RankingServiceTest.java, interfaces/api/ranking/RankingV1ApiE2ETest.java
Redis ์ •๋ ฌ ์ง‘ํ•ฉ ์ƒํ˜ธ์ž‘์šฉ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ, ์ˆœ์œ„ API ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
commerce-streamer - ์ˆœ์œ„ ๋„๋ฉ”์ธ
domain/ranking/RankingKey.java, domain/ranking/RankingService.java, domain/ranking/RankingWeight.java
Redis ํ‚ค ์ƒ์„ฑ ๋ฐ ํŒŒ์‹ฑ ์œ ํ‹ธ๋ฆฌํ‹ฐ, ์ ์ˆ˜ ์ฆ๊ฐ ์„œ๋น„์Šค, ๊ฐ€์ค‘์น˜ ๊ด€๋ฆฌ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€
commerce-streamer - ์ˆœ์œ„ ์‘์šฉ ๊ณ„์ธต
application/ranking/RankingFacade.java, application/ranking/RankingScheduler.java, application/metrics/ProductMetricsFacade.java
์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ฐ ์ ์ˆ˜ ์—…๋ฐ์ดํŠธ ํŒŒ์‚ฌ๋“œ, ๋งค์ผ ๋ฐค 11:50 ์Šค์ผ€์ค„ ์ด์›”, ๋ฉ”ํŠธ๋ฆญ ์ฒ˜๋ฆฌ ์‹œ ์ˆœ์œ„ ์—…๋ฐ์ดํŠธ ํ†ตํ•ฉ
commerce-streamer - ์ˆœ์œ„ ์„ค์ • API
interfaces/api/ranking/RankingConfigV1ApiSpec.java, interfaces/api/ranking/RankingConfigV1Controller.java, interfaces/api/ranking/RankingConfigV1Dto.java
์ˆœ์œ„ ๊ฐ€์ค‘์น˜ ์กฐํšŒ/์ˆ˜์ •/๋ฆฌ์…‹ ๊ด€๋ฆฌ์ž ์—”๋“œํฌ์ธํŠธ ๊ตฌํ˜„
commerce-streamer - ์„ค์ •
src/main/resources/application.yml, CommerceStreamerApplication.java
์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๋ช… ๋ณ€๊ฒฝ ๋ฐ ์ด์›” ๊ฐ€์ค‘์น˜ ์„ค์ •, ์Šค์ผ€์ค„๋ง ํ™œ์„ฑํ™”
commerce-streamer - ํ…Œ์ŠคํŠธ
domain/ranking/RankingIntegrationTest.java, domain/ranking/RankingSchedulerTest.java, domain/ranking/RankingServiceTest.java, domain/ranking/RankingWeightTest.java, interfaces/ProductMetricsConsumerTest.java
์ ์ˆ˜ ๋ˆ„์ , TTL ๋™์ž‘, ์ด์›” ๋กœ์ง, ๊ฐ€์ค‘์น˜ ๋™์  ๊ด€๋ฆฌ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๋ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Client
    participant API as ProductV1Controller
    participant PF as ProductFacade
    participant PRep as ProductRepository
    participant RF as RankingFacade
    participant RS as RankingService
    participant Redis
    
    Client->>API: GET /api/v1/products/{id}
    activate API
    API->>PF: getProductDetail(productId)
    activate PF
    
    PF->>PRep: findById(productId)
    activate PRep
    PRep-->>PF: Product
    deactivate PRep
    
    PF->>PF: ์ƒํ’ˆ ์ •๋ณด โ†’ ProductDetailInfo
    
    PF->>RF: getProductRankToday(productId)
    activate RF
    RF->>RS: getRank(productId, today)
    activate RS
    RS->>Redis: ZREVRANK key(today), productId
    Redis-->>RS: rank (1-based)
    RS-->>RF: rank
    deactivate RS
    RF-->>PF: rank
    deactivate RF
    
    PF->>PF: withRank(rank) ์ ์šฉ
    PF-->>API: ProductDetailInfo (rank ํฌํ•จ)
    deactivate PF
    
    API->>API: ProductV1Dto.ProductDetailResponse๋กœ ๋ณ€ํ™˜
    API-->>Client: ApiResponse (ProductDetailResponse)
    deactivate API
Loading
sequenceDiagram
    autonumber
    participant Kafka as Kafka Topics
    participant Consumer as ProductMetricsConsumer
    participant PMF as ProductMetricsFacade
    participant RF as RankingFacade
    participant RS as RankingService
    participant Redis
    
    Kafka->>Consumer: product-view-metrics
    activate Consumer
    Consumer->>PMF: processViewMetrics(command)
    activate PMF
    
    PMF->>PMF: ์กฐํšŒ ๋ฉ”ํŠธ๋ฆญ ์ฒ˜๋ฆฌ
    PMF->>RF: processViewEvent(productId)
    activate RF
    RF->>RS: incrementViewScore(productId, today)
    activate RS
    RS->>Redis: ZINCRBY key(today), viewScore, productId
    RS->>Redis: EXPIRE key(today), 48h
    Redis-->>RS: ์„ฑ๊ณต
    RS-->>RF: ์™„๋ฃŒ
    deactivate RS
    RF-->>PMF: ์™„๋ฃŒ
    deactivate RF
    
    PMF-->>Consumer: ์™„๋ฃŒ
    deactivate PMF
    Consumer->>Kafka: ack
    deactivate Consumer
    
    Note over Redis: ์ ์ˆ˜ ๋ˆ„์  (view, like, order ๋ชจ๋‘ ๋™์ผํ•œ ํ๋ฆ„)
Loading

Estimated code review effort

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

Possibly related PRs

  • PR #196: commerce-streamer์˜ ProductMetricsFacade ๋ฐ Kafka ๋ฉ”ํŠธ๋ฆญ ํŒŒ์ดํ”„๋ผ์ธ์„ ๋„์ž…ํ–ˆ์œผ๋ฉฐ, ์ด๋ฒˆ PR์—์„œ ์ด ํด๋ž˜์Šค๋“ค๊ณผ ์ˆœ์œ„ ์‹œ์Šคํ…œ์„ ํ†ตํ•ฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ ์ˆ˜์ค€์—์„œ ์ง์ ‘ ๊ด€๋ จ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • PR #132: ProductFacade, ProductDetailInfo, ProductV1Controller/ProductV1Dto ๋“ฑ ์ƒํ’ˆ ๊ด€๋ จ ํด๋ž˜์Šค๋“ค์„ ์ฒ˜์Œ ๋„์ž…ํ–ˆ์œผ๋ฉฐ, ์ด๋ฒˆ PR์—์„œ ์ด๋“ค ํด๋ž˜์Šค์— ์ˆœ์œ„ ํ•„๋“œ ๋ฐ ์กฐํšŒ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ด€๋ จ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • PR #147: ProductFacade๋ฅผ ์ˆ˜์ •ํ•˜์—ฌ ์ƒํ’ˆ ๋ชฉ๋ก ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์œผ๋ฉฐ, ์ด๋ฒˆ PR์—์„œ ๋™์ผํ•œ ProductFacade ํด๋ž˜์Šค์— ์ˆœ์œ„ ํ†ตํ•ฉ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ ์ˆ˜์ค€์—์„œ ๊ด€๋ จ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

Poem

๐Ÿฐ ์ˆœ์œ„์˜ ๋งˆ๋ฒ•์„ ๋‹ด์•„ โœจ
Redis์— ์ ์ˆ˜ ์Œ“๊ณ  ์Œ“๊ณ 
๋ฐค ์—ดํ•œ ์‹œ ๋ฐ˜์—๋Š”
๋‚ด์ผ์„ ์œ„ํ•ด ๋‚˜๋ˆ”์„ ๋‚˜๋ˆ„๊ณ 
์ƒํ’ˆ๋„ ์šฐ๋ฆฌ๋„ ์ˆœ์œ„๋ฅผ ๋งž์ดํ•ด
์ˆซ์ž๋กœ ์„ธ์–ด์ง„ ์ธ๊ธฐ์™€ ์‚ฌ๋ž‘! ๐Ÿ’š

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 21.62% 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 ์ œ๋ชฉ์ด ๋ณ€๊ฒฝ ์‚ฌํ•ญ์˜ ์ฃผ์š” ๊ธฐ๋Šฅ(Redis๋ฅผ ํ™œ์šฉํ•œ ์ƒํ’ˆ ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌํ˜„)์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์„ค๋ช…ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๊ฐ„๊ฒฐํ•˜๊ณ  ๊ตฌ์ฒด์ ์ž…๋‹ˆ๋‹ค.
Description check โœ… Passed PR ์„ค๋ช…์€ ํ…œํ”Œ๋ฆฟ์˜ ๊ธฐ๋ณธ ์„น์…˜(Summary, Review Points, Checklist)์„ ๋ชจ๋‘ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌํ˜„ ๋‚ด์šฉ๊ณผ ์„ค๊ณ„ ๊ณ ๋ฏผ์ด ์ถฉ๋ถ„ํžˆ ์„ค๋ช…๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

๐Ÿงน Nitpick comments (22)
apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingKey.java (1)

21-32: ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ์œ„ํ•ด Clock ์‚ฌ์šฉ ๊ถŒ์žฅ

today()์™€ yesterday() ๋ฉ”์„œ๋“œ๊ฐ€ LocalDate.now()๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜์—ฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ClockConfig์—์„œ ์ œ๊ณตํ•˜๋Š” Clock ๋นˆ์„ ํ™œ์šฉํ•˜๊ฑฐ๋‚˜, LocalDate๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ฆฌํŒฉํ† ๋งํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ๋ฆฌํŒฉํ† ๋ง

์˜ต์…˜ 1: Clock์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๊ธฐ

-    public static String today() {
-        return daily(LocalDate.now());
+    public static String today(Clock clock) {
+        return daily(LocalDate.now(clock));
     }
     
-    public static String yesterday() {
-        return daily(LocalDate.now().minusDays(1));
+    public static String yesterday(Clock clock) {
+        return daily(LocalDate.now(clock).minusDays(1));
     }

์˜ต์…˜ 2: ์ด ๋ฉ”์„œ๋“œ๋“ค์„ ์ œ๊ฑฐํ•˜๊ณ  ํ˜ธ์ถœํ•˜๋Š” ์ชฝ์—์„œ daily(LocalDate.now(clock))๋ฅผ ์ง์ ‘ ํ˜ธ์ถœ

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

16-27: ๋ฌต์‹œ์  ๊ฒ€์ฆ ๋ฐฉ์‹ ์žฌ๊ฒ€ํ†  ๊ถŒ์žฅ

์œ ํšจํ•˜์ง€ ์•Š์€ ์ž…๋ ฅ๊ฐ’์„ ์ž๋™์œผ๋กœ ๋ณด์ •ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ํ˜ธ์ถœ์ž์˜ ๋ฒ„๊ทธ๋ฅผ ์ˆจ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์‚ฌํ•ญ์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”:

  1. ์ž˜๋ชป๋œ ์ž…๋ ฅ์— ๋Œ€ํ•ด ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๊ฑฐ๋‚˜
  2. ์ตœ์†Œํ•œ ๊ฒฝ๊ณ  ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๊ฑฐ๋‚˜
  3. ํ˜„์žฌ ๋ฐฉ์‹์„ ์œ ์ง€ํ•˜๋˜ ๋ฌธ์„œํ™”

๋˜ํ•œ LocalDate.now()๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜์—ฌ ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. Clock์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฐœ์„  ์ œ์•ˆ

์˜ต์…˜ 1: ์˜ˆ์™ธ ๋˜์ง€๊ธฐ

     public RankingCommand {
-        if (page < 0) {
-            page = 0;
+        if (page < 0) {
+            throw new IllegalArgumentException("Page must be non-negative: " + page);
         }
-        if (size <= 0 || size > MAX_PAGE_SIZE) {
-            size = DEFAULT_PAGE_SIZE;
+        if (size <= 0 || size > MAX_PAGE_SIZE) {
+            throw new IllegalArgumentException("Size must be between 1 and " + MAX_PAGE_SIZE);
         }
         if (date == null) {
-            date = LocalDate.now();
+            throw new IllegalArgumentException("Date must not be null");
         }
     }

์˜ต์…˜ 2: Clock ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€

public RankingCommand {
    if (date == null) {
        throw new IllegalArgumentException("Use today() factory method instead of passing null date");
    }
    // ... validation
}

public static RankingCommand today(int page, int size, Clock clock) {
    return new RankingCommand(LocalDate.now(clock), page, size);
}

38-47: ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๋กœ๊น… ๊ณ ๋ ค

parseDate ๋ฉ”์„œ๋“œ๊ฐ€ ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ํ˜„์žฌ ๋‚ ์งœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์˜ค๋ฅ˜๋ฅผ ์ˆจ๊น๋‹ˆ๋‹ค. ๋””๋ฒ„๊น…์„ ์œ„ํ•ด ํŒŒ์‹ฑ ์‹คํŒจ๋ฅผ ๋กœ๊น…ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”.

๐Ÿ”Ž ๋กœ๊น… ์ถ”๊ฐ€ ์ œ์•ˆ
+    private static final Logger log = LoggerFactory.getLogger(RankingCommand.class);
+
     private static LocalDate parseDate(String dateString) {
         if (dateString == null || dateString.isBlank()) {
             return LocalDate.now();
         }
         try {
             return LocalDate.parse(dateString, DATE_FORMATTER);
         } catch (DateTimeParseException e) {
+            log.warn("Failed to parse date string: {}, using current date", dateString, e);
             return LocalDate.now();
         }
     }
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingKey.java (1)

21-22: ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ์œ„ํ•ด Clock ์‚ฌ์šฉ ๊ถŒ์žฅ

today() ๋ฉ”์„œ๋“œ๊ฐ€ LocalDate.now()๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜์—ฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ClockConfig์—์„œ ์ œ๊ณตํ•˜๋Š” Clock ๋นˆ์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๊ฑฐ๋‚˜, ํ˜ธ์ถœํ•˜๋Š” ์ชฝ์—์„œ daily(LocalDate.now(clock))๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

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

21-22: carryOverWeight ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

carryOverWeight์— ๋Œ€ํ•œ ๋ฒ”์œ„ ๊ฒ€์ฆ์ด ์—†์Šต๋‹ˆ๋‹ค. ์Œ์ˆ˜๋‚˜ 1.0์„ ์ดˆ๊ณผํ•˜๋Š” ๊ฐ’์ด ์„ค์ •๋˜๋ฉด ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋™์ž‘์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. @Value ์ฃผ์ž… ํ›„ @PostConstruct์—์„œ ๋ฒ”์œ„๋ฅผ ๊ฒ€์ฆํ•˜๊ฑฐ๋‚˜, @Validated์™€ @DecimalMin/@DecimalMax ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •์•ˆ
+import jakarta.annotation.PostConstruct;
+
 @Slf4j
 @Component
 @RequiredArgsConstructor
 public class RankingScheduler {
 
     private final RankingService rankingService;
     private final Clock clock;
 
     @Value("${ranking.carry-over.weight:0.1}")
     private double carryOverWeight;
+
+    @PostConstruct
+    public void validateConfiguration() {
+        if (carryOverWeight < 0.0 || carryOverWeight > 1.0) {
+            throw new IllegalArgumentException(
+                "ranking.carry-over.weight must be between 0.0 and 1.0, but was: " + carryOverWeight
+            );
+        }
+    }
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (2)

25-34: ํŒŒ๋ผ๋ฏธํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์–ด๋…ธํ…Œ์ด์…˜์„ ๊ณ ๋ คํ•˜์„ธ์š”.

page, size ํŒŒ๋ผ๋ฏธํ„ฐ์— @Min(0) ๋“ฑ์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์ž˜๋ชป๋œ ์š”์ฒญ์„ ์กฐ๊ธฐ์— ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Controller ๋˜๋Š” Service ๊ณ„์ธต์—์„œ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, API ์ŠคํŽ™ ๋ ˆ๋ฒจ์—์„œ ๋ช…์‹œํ•˜๋ฉด ๋ฌธ์„œํ™” ์ธก๋ฉด์—์„œ๋„ ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.


45-51: ํŒŒ๋ผ๋ฏธํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์–ด๋…ธํ…Œ์ด์…˜์„ ๊ณ ๋ คํ•˜์„ธ์š”.

n ํŒŒ๋ผ๋ฏธํ„ฐ์— @Min(1) ๋“ฑ์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์Œ์ˆ˜๋‚˜ 0 ๊ฐ’์„ ์กฐ๊ธฐ์— ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

5-9: ๊ฐ€์ค‘์น˜ ํŒŒ๋ผ๋ฏธํ„ฐ์— ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

viewWeight, likeWeight, orderWeight์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์ด ์—†์Šต๋‹ˆ๋‹ค. ์Œ์ˆ˜ ๊ฐ’์ด๋‚˜ ๋น„์ •์ƒ์ ์œผ๋กœ ํฐ ๊ฐ’์ด ์ž…๋ ฅ๋˜๋ฉด ๋žญํ‚น ๊ณ„์‚ฐ์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. @Min, @Max, @Positive ๋“ฑ์˜ Bean Validation ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •์•ˆ
+import jakarta.validation.constraints.PositiveOrZero;
+
 public class RankingConfigV1Dto {
 
     public record WeightConfigRequest(
+            @PositiveOrZero
             double viewWeight,
+            @PositiveOrZero
             double likeWeight,
+            @PositiveOrZero
             double orderWeight
     ) {}
apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingV1ApiE2ETest.java (1)

51-70: ํ…Œ์ŠคํŠธ ์‹œ๊ฐ„ ์˜์กด์„ฑ ๊ณ ๋ ค

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

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

35-38: ์ž…๋ ฅ ๊ฐ’ ๊ฒ€์ฆ ์ถ”๊ฐ€ ๊ณ ๋ ค

WeightConfigRequest์— @Valid ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค. ๊ฐ€์ค‘์น˜ ๊ฐ’์˜ ๋ฒ”์œ„ ๊ฒ€์ฆ(์˜ˆ: ์Œ์ˆ˜ ๋ถˆ๊ฐ€, ํ•ฉ๊ณ„๊ฐ€ 1.0 ๋“ฑ)์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
     @PutMapping("/weights")
     RankingConfigV1Dto.WeightConfigResponse updateWeights(
-            @RequestBody RankingConfigV1Dto.WeightConfigRequest request
+            @Valid @RequestBody RankingConfigV1Dto.WeightConfigRequest request
     );
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)

114-125: NumberFormatException ๊ฐ€๋Šฅ์„ฑ

Long.parseLong(tuple.getValue())์—์„œ Redis์— ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋œ ๊ฒฝ์šฐ NumberFormatException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์ƒ์œ„ ๋ฉ”์„œ๋“œ์˜ catch ๋ธ”๋ก์—์„œ ์ฒ˜๋ฆฌ๋˜์ง€๋งŒ, ์ผ๋ถ€ ์œ ํšจํ•œ ์—”ํŠธ๋ฆฌ๊ฐ€ ์†์‹ค๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
     private List<RankingEntry> convertToRankingEntries(Set<ZSetOperations.TypedTuple<String>> tuples) {
         List<RankingEntry> entries = new ArrayList<>();
         for (ZSetOperations.TypedTuple<String> tuple : tuples) {
             if (tuple.getValue() != null && tuple.getScore() != null) {
-                entries.add(new RankingEntry(
-                        Long.parseLong(tuple.getValue()),
-                        tuple.getScore()
-                ));
+                try {
+                    entries.add(new RankingEntry(
+                            Long.parseLong(tuple.getValue()),
+                            tuple.getScore()
+                    ));
+                } catch (NumberFormatException e) {
+                    log.warn("Invalid productId in ranking: {}", tuple.getValue());
+                }
             }
         }
         return entries;
     }
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (2)

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

productMap.get(entry.productId())๊ฐ€ null์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ํ•ด๋‹น ํ•ญ๋ชฉ์„ ๊ฑด๋„ˆ๋›ฐ์ง€๋งŒ, startRank + i๋Š” ์—ฌ์ „ํžˆ ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ๋ฐ˜ํ™˜๋˜๋Š” ์ˆœ์œ„์— ๋นˆ ๊ณต๊ฐ„์ด ์ƒ๊ฒจ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ˜ผ๋ž€์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ: 1์œ„ ์ƒํ’ˆ์ด ์‚ญ์ œ๋œ ๊ฒฝ์šฐ, ๊ฒฐ๊ณผ๊ฐ€ 2์œ„๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
         List<RankingInfo> rankings = new ArrayList<>();
-        long startRank = (long) command.page() * command.size() + 1;
+        long currentRank = (long) command.page() * command.size() + 1;
 
         for (int i = 0; i < entries.size(); i++) {
             RankingEntry entry = entries.get(i);
             Product product = productMap.get(entry.productId());
 
             if (product != null) {
                 rankings.add(RankingInfo.of(
                         product.getId(),
                         product.getName(),
                         product.getPriceValue(),
                         product.getBrand().getName(),
-                        startRank + i,
+                        currentRank++,
                         entry.score()
                 ));
+            } else {
+                log.warn("๋žญํ‚น์— ํฌํ•จ๋œ ์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ: productId={}", entry.productId());
             }
         }

101-116: getRankingPage์™€ ๋™์ผํ•œ null ์ƒํ’ˆ ์ฒ˜๋ฆฌ ๋ฌธ์ œ ๋ฐ ์ฝ”๋“œ ์ค‘๋ณต

getTopN์—์„œ๋„ ๋™์ผํ•œ ์ˆœ์œ„ ๊ณ„์‚ฐ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‘ ๋ฉ”์„œ๋“œ์˜ ๋งคํ•‘ ๋กœ์ง์„ ๋ณ„๋„ ํ—ฌํผ ๋ฉ”์„œ๋“œ๋กœ ์ถ”์ถœํ•˜๋ฉด ์ค‘๋ณต์„ ์ค„์ด๊ณ  ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingWeightTest.java (1)

163-188: ๊ฐœ๋ณ„ ๊ฐ€์ค‘์น˜ ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ ํ…Œ์ŠคํŠธ ๋ˆ„๋ฝ

updateViewWeight, updateLikeWeight, updateOrderWeight ๊ฐœ๋ณ„ ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. updateAllWeights๋งŒ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์žˆ์–ด ๊ฐœ๋ณ„ ๋ฉ”์„œ๋“œ์˜ ๋™์ž‘์„ ๊ฒ€์ฆํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ ์ œ์•ˆ
@Test
@DisplayName("๊ฐœ๋ณ„ ๊ฐ€์ค‘์น˜๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค")
void shouldUpdateIndividualWeight() {
    // when
    rankingWeight.updateViewWeight(0.15);

    // then
    verify(hashOperations).put(WEIGHT_KEY, "view", "0.15");
}
apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingWeight.java (2)

37-39: * 1.0 ์—ฐ์‚ฐ์ด ๋ถˆํ•„์š”ํ•จ

getViewWeight() * 1.0์€ getViewWeight()์™€ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๊ณ„์‚ฐ ๋ฉ”์„œ๋“œ์™€์˜ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ์˜๋„์ ์ธ ๊ฒƒ์ผ ์ˆ˜ ์žˆ์œผ๋‚˜, ๋ถˆํ•„์š”ํ•œ ์—ฐ์‚ฐ์ž…๋‹ˆ๋‹ค.


65-75: ๊ฐ€์ค‘์น˜ ๊ฐ’ ๊ฒ€์ฆ ๋ถ€์žฌ

๊ฐ€์ค‘์น˜ ์—…๋ฐ์ดํŠธ ์‹œ ์Œ์ˆ˜๋‚˜ ๋น„์ •์ƒ์ ์œผ๋กœ ํฐ ๊ฐ’์— ๋Œ€ํ•œ ๊ฒ€์ฆ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ๊ฐ€์ค‘์น˜๊ฐ€ ์„ค์ •๋˜๋ฉด ๋žญํ‚น ๊ณ„์‚ฐ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€ ์ œ์•ˆ
     public void updateViewWeight(double weight) {
+        validateWeight(weight);
         updateWeight("view", weight);
     }
 
+    private void validateWeight(double weight) {
+        if (weight < 0 || weight > 1.0) {
+            throw new IllegalArgumentException("๊ฐ€์ค‘์น˜๋Š” 0๊ณผ 1 ์‚ฌ์ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค: " + weight);
+        }
+    }
apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingService.java (2)

110-123: ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์—์„œ ์„ฑ๋Šฅ ์ €ํ•˜ ๊ฐ€๋Šฅ์„ฑ

rangeWithScores(fromKey, 0, -1)๋กœ ๋ชจ๋“  ๋ฉค๋ฒ„๋ฅผ ์กฐํšŒํ•˜๊ณ , ๊ฐœ๋ณ„ add() ํ˜ธ์ถœ๋กœ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์ƒํ’ˆ ์ˆ˜๊ฐ€ ๋งŽ์•„์ง€๋ฉด ๋„คํŠธ์›Œํฌ ๋ผ์šด๋“œํŠธ๋ฆฝ์ด ์ฆ๊ฐ€ํ•˜์—ฌ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ํŒŒ์ดํ”„๋ผ์ด๋‹ ์‚ฌ์šฉ ์ œ์•ˆ
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    for (ZSetOperations.TypedTuple<String> tuple : tuples) {
        if (tuple.getValue() != null && tuple.getScore() != null) {
            double newScore = tuple.getScore() * weight;
            connection.zSetCommands().zAdd(
                toKey.getBytes(), 
                newScore, 
                tuple.getValue().getBytes()
            );
        }
    }
    return null;
});

16-21: ์„ ํƒ์  ๋ฆฌ๋ทฐ - ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ ์žฌ๊ฒ€ํ†  ํ•„์š”

์ด ํ”„๋กœ์ ํŠธ๋Š” ๋‘ ๊ฐœ์˜ ๋ณ„๋„ RankingService๋ฅผ ๊ฐ€์ง„ ๋ฉ€ํ‹ฐ ๋ชจ๋“ˆ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค:

  • commerce-api: getTopNWithScores, getRankingPage, getRank, getScore ๋“ฑ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ํฌํ•จ
  • commerce-streamer: incrementViewScore, carryOverScores ๋“ฑ ์“ฐ๊ธฐ ์ž‘์—…๋งŒ ํฌํ•จ

์ œ๊ณต๋œ ์ฝ”๋“œ(commerce-streamer)์— ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ์—†๋Š” ๊ฒƒ์€ ์„ค๊ณ„์— ๋”ฐ๋ฅธ ๊ฒƒ์ž…๋‹ˆ๋‹ค. commerce-api์˜ RankingFacade๋Š” commerce-api์˜ RankingService๋ฅผ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ, ๋ฉ”์„œ๋“œ ๋ˆ„๋ฝ์ด ์•„๋‹ˆ๋ผ ๋ชจ๋“ˆ ๊ฐ„ ์—ญํ•  ๋ถ„๋‹ด์ž…๋‹ˆ๋‹ค. ๋‹ค๋งŒ ์ด๋Ÿฌํ•œ ๋ถ„๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ์˜๋„๋œ ์„ค๊ณ„์ธ์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingIntegrationTest.java (4)

56-62: ๋А์Šจํ•œ ๋‹จ์–ธ๋ฌธ(assertion)์œผ๋กœ ์ธํ•ด ๊ณ„์‚ฐ ๋ฒ„๊ทธ๋ฅผ ๋†“์น  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ isGreaterThanOrEqualTo(0.4) ๋‹จ์–ธ์€ ์‹ค์ œ ์˜ˆ์ƒ๊ฐ’์ธ ์ •ํ™•ํžˆ 0.4๋ฅผ ๊ฒ€์ฆํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฐ€์ค‘์น˜๊ฐ€ ์ •ํ•ด์ ธ ์žˆ๋‹ค๋ฉด(view=0.1, like=0.2), ๋ถ€๋™์†Œ์ˆ˜์  ๋น„๊ต๋ฅผ ์œ„ํ•ด isCloseTo๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
-            assertThat(score).isGreaterThanOrEqualTo(0.4);
+            assertThat(score).isCloseTo(0.4, org.assertj.core.api.Assertions.within(0.01));

98-102: TTL ์œ ํšจ ๋ฒ”์œ„ ๊ฒ€์ฆ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ TTL์ด 0๋ณด๋‹ค ํฌ๋‹ค๋Š” ๊ฒƒ๋งŒ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ์„ค์ •๋œ TTL ๊ฐ’(์˜ˆ: 2์ผ)์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉ๋˜์—ˆ๋Š”์ง€ ๋ฒ”์œ„ ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์„ค์ • ์˜ค๋ฅ˜๋ฅผ ์กฐ๊ธฐ์— ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ • ์˜ˆ์‹œ
             Long ttl = redisTemplate.getExpire(key);
             assertThat(ttl).isNotNull();
-            assertThat(ttl).isGreaterThan(0);
+            // ์˜ˆ์ƒ TTL์ด 2์ผ(172800์ดˆ)์ธ ๊ฒฝ์šฐ
+            assertThat(ttl).isGreaterThan(0);
+            assertThat(ttl).isLessThanOrEqualTo(172800L); // ์ตœ๋Œ€ 2์ผ

174-177: ๋ถ€๋™์†Œ์ˆ˜์  ๋น„๊ต์— isCloseTo ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

isEqualTo๋ฅผ ์‚ฌ์šฉํ•œ double ๋น„๊ต๋Š” ๋ถ€๋™์†Œ์ˆ˜์  ์ •๋ฐ€๋„ ๋ฌธ์ œ๋กœ ์ธํ•ด ๋ถˆ์•ˆ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ(line 131)์™€ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด isCloseTo๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
-            assertThat(rankingWeight.getViewWeight()).isEqualTo(newViewWeight);
-            assertThat(rankingWeight.getLikeWeight()).isEqualTo(newLikeWeight);
-            assertThat(rankingWeight.getOrderWeight()).isEqualTo(newOrderWeight);
+            assertThat(rankingWeight.getViewWeight()).isCloseTo(newViewWeight, within(0.001));
+            assertThat(rankingWeight.getLikeWeight()).isCloseTo(newLikeWeight, within(0.001));
+            assertThat(rankingWeight.getOrderWeight()).isCloseTo(newOrderWeight, within(0.001));

within์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด static import๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

import static org.assertj.core.api.Assertions.within;

189-193: ๊ธฐ๋ณธ๊ฐ’ ์ƒ์ˆ˜ํ™”๋ฅผ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

ํ…Œ์ŠคํŠธ์— ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๊ธฐ๋ณธ๊ฐ’(0.1, 0.2, 0.7)์ด RankingWeight ๊ตฌํ˜„์ฒด์˜ ์‹ค์ œ ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋™๊ธฐํ™”๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋ฏ€๋กœ, ์ƒ์ˆ˜๋ฅผ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜ RankingWeight์—์„œ ๊ธฐ๋ณธ๊ฐ’์„ ์ œ๊ณตํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์œ ์ง€๋ณด์ˆ˜์— ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between e7bf613 and fd11c00.

๐Ÿ“’ Files selected for processing (34)
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingCommand.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingPageInfo.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingEntry.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingInfo.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingKey.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.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/java/com/loopers/support/config/ClockConfig.java
  • apps/commerce-api/src/test/java/com/loopers/domain/ranking/RankingServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingV1ApiE2ETest.java
  • apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingFacade.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingScheduler.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingKey.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingService.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingWeight.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1ApiSpec.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1Controller.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1Dto.java
  • apps/commerce-streamer/src/main/resources/application.yml
  • apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingIntegrationTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingSchedulerTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingServiceTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingWeightTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/ProductMetricsConsumerTest.java
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (7)
๐Ÿ““ Common learnings
Learnt from: junoade
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 0
File: :0-0
Timestamp: 2025-12-02T08:12:06.383Z
Learning: ProductQueryService์—์„œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ์‹œ Redis ์บ์‹œ๋ฅผ ์ ์šฉํ–ˆ์œผ๋ฉฐ, ์บ์‹œ ํ‚ค๋Š” brandId, sortType, pageNumber, pageSize์˜ ์กฐํ•ฉ์œผ๋กœ ๊ตฌ์„ฑ๋˜๊ณ  TTL์€ 5๋ถ„์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ๋‹ค.
๐Ÿ“š Learning: 2025-12-22T16:33:50.678Z
Learnt from: jikimee64
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 71
File: apps/commerce-streamer/src/main/kotlin/com/loopers/application/ranking/RankingFacade.kt:167-173
Timestamp: 2025-12-22T16:33:50.678Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository, using system default timezone (ZonedDateTime.now() without explicit ZoneId) in date key calculations is an intentional design choice. The deployment ensures all instances share the same timezone configuration.

Applied to files:

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

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.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/test/java/com/loopers/interfaces/ProductMetricsConsumerTest.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/interfaces/ProductMetricsConsumerTest.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/interfaces/ProductMetricsConsumerTest.java
๐Ÿงฌ Code graph analysis (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
  • ProductV1Dto (8-75)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1Controller.java (1)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1Dto.java (1)
  • RankingConfigV1Dto (3-20)
๐Ÿ”‡ Additional comments (35)
apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1)

7-7: @EnableScheduling ํ™œ์„ฑํ™”๋Š” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹คโ€”carryOverScores๊ฐ€ ๋ฉฑ๋“ฑ์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

RankingScheduler์˜ carryOverScores ๋ฉ”์„œ๋“œ๋Š” ์ด๋ฏธ ์ค€๋น„๋œ ๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜๋ฉด ์กฐ๊ธฐ์— ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค(line 104-108). ๋‹ค์ค‘ ์ธ์Šคํ„ด์Šค ํ™˜๊ฒฝ์—์„œ ์ฒซ ๋ฒˆ์งธ ์ธ์Šคํ„ด์Šค๊ฐ€ ์‹คํ–‰๋˜์–ด ๋ฐ์ดํ„ฐ๋ฅผ ์ค€๋น„ํ•˜๋ฉด, ์ดํ›„ ์ธ์Šคํ„ด์Šค๋“ค์€ ๋ฐ์ดํ„ฐ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ณ  ์ž‘์—…์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ค‘๋ณต ์‹คํ–‰์— ๋Œ€ํ•œ ์šฐ๋ ค๋Š” ์ œ๊ฑฐ๋˜๋ฉฐ, ๋ถ„์‚ฐ ๋ฝ์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

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

17-17: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ด๋ฆ„ ์ˆ˜์ • ํ™•์ธ

๋ชจ๋“ˆ ์ด๋ฆ„๊ณผ ์ผ์น˜ํ•˜๋„๋ก commerce-streamer๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java (3)

14-16: rank ํ•„๋“œ ์ถ”๊ฐ€ ํ™•์ธ

์ˆœ์œ„ ์ •๋ณด๋ฅผ ๋‹ด๋Š” rank ํ•„๋“œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. nullable Long ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜์—ฌ ์ˆœ์œ„๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ null๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


17-41: ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ ํ™•์ธ

๊ธฐ์กด of ๋ฉ”์„œ๋“œ๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ rank๋ฅผ null๋กœ ์„ค์ •ํ•˜๊ณ , ์ƒˆ๋กœ์šด ์˜ค๋ฒ„๋กœ๋“œ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํ•˜์œ„ ํ˜ธํ™˜์„ฑ๊ณผ ํ™•์žฅ์„ฑ์„ ๋ชจ๋‘ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.


43-54: withRank ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ ํ™•์ธ

๋ถˆ๋ณ€ ๋ ˆ์ฝ”๋“œ ํŒจํ„ด์„ ๋”ฐ๋ผ ์ƒˆ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๋Š” withRank ๋ฉ”์„œ๋“œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingEntry.java (1)

1-7: LGTM!

Redis ZSET ์—”ํŠธ๋ฆฌ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ๊ฐ„๋‹จํ•˜๊ณ  ๋ช…ํ™•ํ•œ ๋„๋ฉ”์ธ ๋ ˆ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/support/config/ClockConfig.java (1)

1-15: Clock ๋นˆ ์„ค์ • ํ™•์ธ

Clock.systemDefaultZone()์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ํƒ€์ž„์กด์œผ๋กœ Clock ๋นˆ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํ•™์Šต๋œ ๋‚ด์šฉ์— ๋”ฐ๋ฅด๋ฉด ๋ชจ๋“  ์ธ์Šคํ„ด์Šค๊ฐ€ ๋™์ผํ•œ ํƒ€์ž„์กด ์„ค์ •์„ ๊ณต์œ ํ•˜๋„๋ก ๋ฐฐํฌ ํ™˜๊ฒฝ์ด ๊ตฌ์„ฑ๋˜์–ด ์žˆ์–ด ์ด ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ์ด ๋นˆ์„ ํ™œ์šฉํ•˜์—ฌ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Based on learnings, the deployment ensures all instances share the same timezone configuration.

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

1-21: ์ˆœ์œ„ ์ •๋ณด ๋ ˆ์ฝ”๋“œ ๊ตฌํ˜„ ํ™•์ธ

์ƒํ’ˆ ์ˆœ์œ„ ์ •๋ณด๋ฅผ ๋‹ด๋Š” RankingInfo ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. of ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋Š” ๋‹จ์ˆœํžˆ ์ƒ์„ฑ์ž๋ฅผ ํ˜ธ์ถœํ•˜์ง€๋งŒ, API ์ผ๊ด€์„ฑ์„ ์œ„ํ•œ ํŒจํ„ด์œผ๋กœ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

1-75: LGTM!

DTO ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ , ProductDetailResponse์˜ rank ํ•„๋“œ๊ฐ€ nullable Long ํƒ€์ž…์œผ๋กœ ์„ ์–ธ๋˜์–ด ์ˆœ์œ„๊ฐ€ ์—†์„ ๋•Œ null์„ ๋ฐ˜ํ™˜ํ•˜๋Š” PR ๋ชฉํ‘œ์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. ๋งคํ•‘ ๋กœ์ง๋„ ๊ฐ„๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingSchedulerTest.java (1)

1-71: LGTM!

์Šค์ผ€์ค„๋Ÿฌ ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ณ ์ •๋œ ์‹œ๊ณ„๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‚ ์งœ ๊ณ„์‚ฐ์„ ๊ฒ€์ฆํ•˜๊ณ , ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ์—๋„ ์ •์ƒ ์ข…๋ฃŒ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

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

198-222: LGTM!

๋žญํ‚น ์—…๋ฐ์ดํŠธ ํ…Œ์ŠคํŠธ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. processViewMetrics ํ˜ธ์ถœ๊ณผ productId ๊ฒ€์ฆ์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.


224-243: LGTM!

์ž˜๋ชป๋œ JSON ๋ฉ”์‹œ์ง€๋ฅผ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋‹ค์Œ ๋ฉ”์‹œ์ง€๋ฅผ ์ •์ƒ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง์„ ์ž˜ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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

28-42: LGTM!

์Šค์ผ€์ค„๋Ÿฌ ๋กœ์ง์ด ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ช…์‹œ์ ์œผ๋กœ today/tomorrow๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ , ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. Clock ์ฃผ์ž…์„ ํ†ตํ•œ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ๋„ ์šฐ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค.

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

1-203: LGTM!

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

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

16-18: LGTM!

์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ๊ฐ„๋‹จํ•˜๊ณ  ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

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

20-35: LGTM!

getProducts ๋ฉ”์„œ๋“œ ๊ตฌํ˜„์ด ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค. ProductGetListCommand๋ฅผ ํ†ตํ•œ ์ปค๋งจ๋“œ ํŒจํ„ด ์‚ฌ์šฉ๊ณผ DTO ๋ณ€ํ™˜์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.


37-48: LGTM!

userId null/blank ์ฒดํฌ ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ/๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ๊ฐ€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingV1ApiE2ETest.java (1)

186-210: LGTM!

Top-N ๋žญํ‚น ์กฐํšŒ ํ…Œ์ŠคํŠธ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

25-30: LGTM!

RankingCommand.of()๋ฅผ ํ†ตํ•œ ์ปค๋งจ๋“œ ์ƒ์„ฑ์ด ๊ฐ„๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ๋‚ ์งœ/ํŽ˜์ด์ง€ ํŒŒ์‹ฑ ๋กœ์ง์ด ์ปค๋งจ๋“œ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌ๋˜๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

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

25-41: LGTM!

Top-N ์กฐํšŒ ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ๋นˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” graceful degradation ํŒจํ„ด์ด ์ž˜ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/domain/ranking/RankingServiceTest.java (2)

22-37: LGTM!

ํ…Œ์ŠคํŠธ ์…‹์—…์ด ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค. RedisTemplate๊ณผ ZSetOperations ๋ชจํ‚น์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


222-233: LGTM!

TypedTuple ํ—ฌํผ ๋ฉ”์„œ๋“œ๊ฐ€ ํ…Œ์ŠคํŠธ ๊ฐ€๋…์„ฑ์„ ๋†’์—ฌ์ค๋‹ˆ๋‹ค.

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

27-29: LGTM!

empty() ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ๋นˆ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ์ ์ ˆํ•œ ๊ธฐ๋ณธ๊ฐ’์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

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

8-22: LGTM!

์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„์ด ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค. RankingWeight ๋„๋ฉ”์ธ ์„œ๋น„์Šค์— ์œ„์ž„ํ•˜๋Š” ํŒจํ„ด์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

121-133: LGTM!

Clock ์ฃผ์ž…์„ ํ†ตํ•ด ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ํ™•๋ณดํ•œ ์ข‹์€ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

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

10-71: LGTM!

DTO ๊ตฌ์กฐ๊ฐ€ ๊น”๋”ํ•˜๊ณ , DateTimeFormatter๋ฅผ static final๋กœ ์„ ์–ธํ•˜์—ฌ ์Šค๋ ˆ๋“œ ์•ˆ์ „์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค. Factory ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•œ ๋งคํ•‘ ํŒจํ„ด๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingWeightTest.java (1)

1-35: LGTM!

ํ…Œ์ŠคํŠธ ๊ตฌ์กฐ๊ฐ€ @Nested ํด๋ž˜์Šค๋กœ ์ž˜ ์ •๋ฆฌ๋˜์–ด ์žˆ๊ณ , Redis ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ํด๋ฐฑ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ ์ ˆํžˆ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java (2)

65-68: LGTM!

์žฌ๊ณ  ๊ฐ์†Œ(DECREASED) ์‹œ์—๋งŒ ์ฃผ๋ฌธ ์ด๋ฒคํŠธ๋ฅผ ๋žญํ‚น์— ๋ฐ˜์˜ํ•˜๋Š” ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.


46-49: ๋žญํ‚น ์—…๋ฐ์ดํŠธ๋Š” ์ด๋ฏธ ํŠธ๋žœ์žญ์…˜ ์˜ํ–ฅ์„ ๊ณ ๋ คํ•œ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค

RankingService.incrementScore() ๋ฉ”์„œ๋“œ์—์„œ ๋ชจ๋“  ์˜ˆ์™ธ๊ฐ€ ์บ์น˜๋˜์–ด ๋กœ๊ทธ๋งŒ ๊ธฐ๋กํ•˜๊ณ  ์žฌ๋ฐœ์ƒ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋žญํ‚น ์—…๋ฐ์ดํŠธ ์‹คํŒจ ์‹œ ๋ฉ”ํŠธ๋ฆญ ์ €์žฅ ํŠธ๋žœ์žญ์…˜์ด ๋กค๋ฐฑ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ถ”๊ฐ€ ์ˆ˜์ •์ด ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

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

15-22: LGTM!

Clock ์ฃผ์ž…์œผ๋กœ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ํ™•๋ณดํ–ˆ๊ณ , ๋‹จ์ผ ์ฑ…์ž„ ์›์น™์„ ์ž˜ ๋”ฐ๋ฅด๋Š” ๊น”๋”ํ•œ Facade ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.


27-49: RankingService์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ™•์ธ๋จ

๊ฒ€์ฆ ๊ฒฐ๊ณผ, RankingService๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ ๋ชจ๋“  ์ ์ˆ˜ ์—…๋ฐ์ดํŠธ ์ž‘์—…(incrementScore ๋ฉ”์„œ๋“œ, 62-77์ค„)์— ๋Œ€ํ•ด ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋Š” ๋กœ๊น…๋˜๊ณ  ์žฌ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ProductMetricsFacade์˜ ํŠธ๋žœ์žญ์…˜์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ RankingFacade์—์„œ ์ถ”๊ฐ€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋Š” ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

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

15-62: LGTM!

OpenAPI ๋ช…์„ธ๊ฐ€ ์ž˜ ์ •์˜๋˜์–ด ์žˆ๊ณ , ํ•œ๊ธ€ ์„ค๋ช…์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋žญํ‚น ์ •๋ณด๊ฐ€ ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ์— ํฌํ•จ๋œ๋‹ค๋Š” ์ ์ด ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

80-93: LGTM!

getRankingSize ๋ฉ”์„œ๋“œ์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•˜๋ฉฐ, null ์ฒดํฌ์™€ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜์ด ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingIntegrationTest.java (2)

13-39: LGTM!

ํ…Œ์ŠคํŠธ ์„ค์ •์ด ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. @BeforeEach๋กœ ๊ฐ ํ…Œ์ŠคํŠธ ์ „ Redis๋ฅผ ์ •๋ฆฌํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ๋ฅผ ๋ณด์žฅํ•˜๊ณ , ๊ณ ์ •๋œ testDate๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์žฌํ˜„ ๊ฐ€๋Šฅํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.


105-157: LGTM!

Carry-over ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ line 131์—์„œ ๋ถ€๋™์†Œ์ˆ˜์  ๋น„๊ต์— isCloseTo๋ฅผ ์‚ฌ์šฉํ•œ ์ ๊ณผ, ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฎ์–ด์“ฐ์ง€ ์•Š๋Š” ๋™์ž‘์„ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์ข‹์Šต๋‹ˆ๋‹ค.

@rnqhstmd rnqhstmd merged commit 0c91616 into Loopers-dev-lab:rnqhstmd Dec 29, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant