Skip to content

Conversation

@jeonga1022
Copy link
Collaborator

@jeonga1022 jeonga1022 commented Dec 26, 2025

๐Ÿ“Œ Summary

  • Redis Sorted Set ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌํ˜„
  • Write/Read ๊ฒฝ๋กœ ๋ถ„๋ฆฌ: ์ ์žฌ๋Š” Kafka ๋น„๋™๊ธฐ, ์กฐํšŒ๋Š” Redis ๋™๊ธฐ
  • Outbox ํŒจํ„ด์œผ๋กœ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์‹ ๋ขฐ์„ฑ ๋ณด์žฅ
  • Consumer ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ (EventHandled ํ…Œ์ด๋ธ”)
  • ์ƒํ’ˆ ์ƒ์„ธ API์— rank ํ•„๋“œ ์ถ”๊ฐ€

๐Ÿ’ฌ Review Points

1. Redis Hot Key ๋ถ€ํ•˜ ๋ถ„์‚ฐ ์‹œ์ 

ํ˜„์žฌ ๊ตฌ์กฐ๋Š” ์ผ๋ณ„ ๋žญํ‚น ํ‚ค(ranking:all:yyyyMMdd) ํ•˜๋‚˜์— ๋ชจ๋“  ์“ฐ๊ธฐ ํŠธ๋ž˜ํ”ฝ์ด ์ง‘์ค‘๋ฉ๋‹ˆ๋‹ค. ์„œ๋น„์Šค๊ฐ€ ํ™•์žฅ๋  ๊ฒฝ์šฐ ํŠน์ • Redis ๋…ธ๋“œ์— ๋ถ€ํ•˜๊ฐ€ ์ ๋ฆฌ๋Š” Hot Key ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

ํ‚ค๋ฅผ ๋ถ„๋ฆฌํ•˜๊ฑฐ๋‚˜ ์ƒค๋”ฉํ•˜๋Š” ๋ฐฉ๋ฒ•, Consumer์—์„œ Write-back ๋ฐฉ์‹์œผ๋กœ ์“ฐ๊ธฐ๋ฅผ ์ง€์—ฐ์‹œํ‚ค๋Š” ๋ฐฉ๋ฒ• ๋“ฑ์ด ์žˆ๋‹ค๊ณ  ๋“ค์—ˆ๋Š”๋ฐ, ์‹ค๋ฌด์—์„œ๋Š” ๋ณดํ†ต ์–ด๋А ์ •๋„ ํŠธ๋ž˜ํ”ฝ ๊ทœ๋ชจ์—์„œ ์ด๋Ÿฐ ๊ตฌ์กฐ์  ๊ฐœ์„ ์„ ๊ณ ๋ คํ•˜๋‚˜์š”?

2. Lua ์Šคํฌ๋ฆฝํŠธ ๊ตฌ์กฐ

ZINCRBY์™€ EXPIRE๋ฅผ ์›์ž์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด Lua ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

private static final String ZINCRBY_WITH_EXPIRE_SCRIPT = """
    redis.call('ZINCRBY', KEYS[1], ARGV[1], ARGV[2])
    redis.call('EXPIRE', KEYS[1], ARGV[3])
    return 1
    """;

์›์ž์„ฑ ๋ณด์žฅ์„ ์œ„ํ•ด ์ด ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ๋Š”๋ฐ, ์ด ์ ‘๊ทผ์ด ์ ์ ˆํ• ๊นŒ์š”? ๋” ๊ณ ๋ คํ•  ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ์•Œ๋ ค์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

3. ํ”„๋กœ์ ํŠธ ๋ฌธ์„œํ™” ๊ด€๋ฆฌ ๋ฐฉ์‹

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

์‹ค๋ฌด์—์„œ ๋ฌธ์„œ๋ฅผ ๊ด€๋ฆฌํ•˜์‹ค ๋•Œ ํ•ญ์ƒ ์ตœ์‹  ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋Š” 'ํ˜„ํ–‰ํ™”' ๋ฅผ ์šฐ์„ ํ•˜์‹œ๋Š”์ง€, ์•„๋‹ˆ๋ฉด ์˜์‚ฌ๊ฒฐ์ •์˜ ํ๋ฆ„์ด ๋ณด์ด๋Š” '๋ณ€๊ฒฝ ์ด๋ ฅ ๊ด€๋ฆฌ' ๋ฅผ ๋” ์„ ํ˜ธํ•˜์‹œ๋Š”์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค.


โœ… Checklist

๐Ÿ“ˆ Ranking Consumer

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

โšพ Ranking API

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

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

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

์‹ค์‹œ๊ฐ„ ์ œํ’ˆ ์ˆœ์œ„ ์‹œ์Šคํ…œ์„ Redis ZSET ๊ธฐ๋ฐ˜์œผ๋กœ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค. ์ƒํ’ˆ ์กฐํšŒ, ์ƒํ’ˆ ์ข‹์•„์š”, ์ฃผ๋ฌธ ์™„๋ฃŒ ์‹œ ์ˆœ์œ„ ์ ์ˆ˜๋ฅผ ์ฆ๊ฐ€์‹œํ‚ค๊ณ , Kafka ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋กœ ๊ตฌํ˜„ํ•˜๋ฉฐ, ์ˆœ์œ„ API์™€ ์ œํ’ˆ ์ƒ์„ธ ์‘๋‹ต์— ์ˆœ์œ„ ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

Changes

์ฝ”ํ˜ธํŠธ / ํŒŒ์ผ(๋“ค) ๋ณ€๊ฒฝ ์‚ฌํ•ญ
์ˆœ์œ„ ๋„๋ฉ”์ธ ๋ฐ ์ธํ”„๋ผ
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRedisService.java, RankingEntry.java
Redis ZSET์„ ์‚ฌ์šฉํ•œ ์ผ์ผ ์ˆœ์œ„ ๊ด€๋ฆฌ ์„œ๋น„์Šค ์ถ”๊ฐ€; ์กฐํšŒ, ์ข‹์•„์š”, ์ฃผ๋ฌธ์— ๋”ฐ๋ฅธ ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ์ ์ˆ˜ ์ฆ๊ฐ€ (0.1, 0.2, 0.6ร—์ˆ˜๋Ÿ‰); TTL 2์ผ ์„ค์ •
์ˆœ์œ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณ„์ธต
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
์ˆœ์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๋ฐ ์ œํ’ˆ ์ •๋ณด์™€ ๊ฒฐํ•ฉํ•˜๋Š” ํŒŒ์‚ฌ๋“œ; ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ง€์›; ๋‚ ์งœ ๊ธฐ๋ณธ๊ฐ’์„ ์˜ค๋Š˜๋กœ ์„ค์ •
์ƒํ’ˆ ๋ทฐ ๋กœ๊น…
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewLog.java, ProductViewLogRepository.java, ProductViewLogRepositoryImpl.java, ProductViewLogJpaRepository.java
JPA ์—”ํ‹ฐํ‹ฐ ๋ฐ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋กœ ์ƒํ’ˆ ์กฐํšŒ ๋กœ๊ทธ ์ €์žฅ; ์ž๋™ ์ƒ์„ฑ๋œ ํƒ€์ž„์Šคํƒฌํ”„ ํฌํ•จ
์ด๋ฒคํŠธ ๋„๋ฉ”์ธ
apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductViewedEvent.java, StockDepletedEvent.java, ProductLikedEvent.java, OrderCompletedEvent.java
์ƒˆ๋กœ์šด ProductViewedEvent ์ถ”๊ฐ€; ๊ธฐ์กด ์ด๋ฒคํŠธ๋“ค์„ ๋ถˆ๋ณ€โ†’๊ฐ€๋ณ€์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ํ”„๋ ˆ์ž„์›Œํฌ ์ง๋ ฌํ™” ์ง€์›; OrderCompletedEvent์— ์•„์ดํ…œ ๋ชฉ๋ก ์ถ”๊ฐ€
์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฐ ๋ผ์šฐํŒ…
apps/commerce-api/src/main/java/com/loopers/infrastructure/event/ViewEventPublisher.java, infrastructure/outbox/OutboxEventHandler.java
ProductViewedEvent ๋ฐœํ–‰ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€; ProductLikedEvent์™€ StockDepletedEvent์˜ Kafka ํ† ํ”ฝ์„ "catalog-events"์—์„œ ์ „์šฉ ํ† ํ”ฝ์œผ๋กœ ๋ณ€๊ฒฝ
Kafka ์ปจ์Šˆ๋จธ - ๋ทฐ ๋กœ๊น…
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ViewLogConsumer.java
ProductViewedEvent๋ฅผ ๋ฐฐ์น˜๋กœ ์†Œ๋น„ํ•˜์—ฌ MySQL์— ์ €์žฅํ•˜๊ณ  Redis ์ˆœ์œ„ ์ ์ˆ˜ ์ผ๊ด„ ์—…๋ฐ์ดํŠธ
Kafka ์ปจ์Šˆ๋จธ - ์ข‹์•„์š” ๋ฐ ์ˆœ์œ„
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ProductLikedConsumer.java
ProductLikedEvent ์†Œ๋น„; ์ข‹์•„์š” ์‹œ ์ˆœ์œ„ ์ ์ˆ˜ ์ฆ๊ฐ€; ProductMetrics ์—…๋ฐ์ดํŠธ
Kafka ์ปจ์Šˆ๋จธ - ์ฃผ๋ฌธ ๋ฐ ์ˆœ์œ„
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/OrderCompletedConsumer.java
OrderCompletedEvent ์†Œ๋น„; ๊ฐ ์ฃผ๋ฌธ ์•„์ดํ…œ๋งˆ๋‹ค ์ˆ˜๋Ÿ‰ ๊ธฐ๋ฐ˜ ์ˆœ์œ„ ์ ์ˆ˜ ์ฆ๊ฐ€
Kafka ์ปจ์Šˆ๋จธ - ์žฌ๊ณ  ๋ถ€์กฑ
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/StockDepletedConsumer.java
StockDepletedEvent ์†Œ๋น„; ์ œํ’ˆ ์บ์‹œ ๋ฌดํšจํ™”
์‚ญ์ œ๋œ ์ปจ์Šˆ๋จธ
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/CatalogEventConsumer.java (์ œ๊ฑฐ)
๋‹จ์ผ ์นดํƒˆ๋กœ๊ทธ ์ด๋ฒคํŠธ ์ปจ์Šˆ๋จธ ์ œ๊ฑฐ; ๊ธฐ๋Šฅ์„ ProductLikedConsumer ๋ฐ StockDepletedConsumer๋กœ ๋ถ„๋ฆฌ
์ƒํ’ˆ ์กฐํšŒ ๊ฒฝ๋กœ ์—…๋ฐ์ดํŠธ
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java, interfaces/api/product/ProductDto.java
getProduct ํ˜ธ์ถœ ์‹œ ๋ทฐ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฐ Redis์—์„œ ์ˆœ์œ„ ์กฐํšŒ; ProductDetailResponse์— rank ํ•„๋“œ ์ถ”๊ฐ€; ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์—…๋ฐ์ดํŠธ
์ˆœ์œ„ API ์—”๋“œํฌ์ธํŠธ
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingController.java, RankingDto.java
GET /api/v1/rankings ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€; ๋‚ ์งœ ๋ฐ ํŽ˜์ด์ง€๋„ค์ด์…˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์ง€์›
ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
ProductFacadeRankTest.java, RankingFacadeTest.java, RankingRedisServiceTest.java, ProductLikedConsumerTest.java, OrderCompletedConsumerTest.java, StockDepletedConsumerTest.java, RankingApiE2ETest.java
์ˆœ์œ„ ์กฐํšŒ, Redis ์—ฐ์‚ฐ, ์ปจ์Šˆ๋จธ ๋™์ž‘, E2E ์ˆœ์œ„ API ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
ํ…Œ์ŠคํŠธ ์‚ญ์ œ
CatalogEventConsumerTest.java (์ œ๊ฑฐ)
๋‹จ์ผ ์นดํƒˆ๋กœ๊ทธ ์ด๋ฒคํŠธ ์ปจ์Šˆ๋จธ ํ…Œ์ŠคํŠธ ์ œ๊ฑฐ
ํ…Œ์ŠคํŠธ ์—…๋ฐ์ดํŠธ
OutboxEventHandlerTest.java
ProductLikedEvent ํ† ํ”ฝ ๊ธฐ๋Œ€๊ฐ’์„ "product-liked"๋กœ ์—…๋ฐ์ดํŠธ
์ธํ”„๋ผ ์„ค์ •
docker-compose.yml
Kafka ํฌํŠธ ๋ฐ LISTENERS ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์—…๋ฐ์ดํŠธ
๋ฌธ์„œ
docs/week9/01-requirements.md, 02-sequence-diagrams.md
์ˆœ์œ„ ์‹œ์Šคํ…œ ์š”๊ตฌ์‚ฌํ•ญ ๋ฐ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ ๋ฌธ์„œ ์ถ”๊ฐ€

Sequence Diagrams

sequenceDiagram
    actor Client
    participant ProductAPI
    participant ProductFacade
    participant ProductCache
    participant RankingRedisService
    participant ViewEventPublisher
    participant Kafka
    participant ViewLogConsumer

    Client->>ProductAPI: GET /api/v1/products/{id}
    ProductAPI->>ProductFacade: getProduct(id)
    
    alt Cache Hit
        ProductFacade->>ProductCache: get(id)
        ProductCache-->>ProductFacade: ProductDetailCache
    else Cache Miss
        ProductFacade->>ProductFacade: fetch from DB
    end
    
    rect rgb(200, 220, 255)
    Note over ProductFacade,RankingRedisService: ์ˆœ์œ„ ์กฐํšŒ
    ProductFacade->>RankingRedisService: getRankingPosition(today, id)
    RankingRedisService-->>ProductFacade: rank (1-based or null)
    end
    
    rect rgb(200, 220, 255)
    Note over ProductFacade,ViewEventPublisher: ์กฐํšŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰
    ProductFacade->>ViewEventPublisher: publish(id)
    ViewEventPublisher->>Kafka: ProductViewedEvent
    end
    
    ProductFacade->>ProductFacade: build response with rank
    ProductAPI-->>Client: 200 OK {ProductDetailResponse + rank}
    
    rect rgb(240, 200, 200)
    Note over Kafka,ViewLogConsumer: ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
    Kafka->>ViewLogConsumer: consume ProductViewedEvent batch
    ViewLogConsumer->>ViewLogConsumer: aggregate view counts
    ViewLogConsumer->>RankingRedisService: incrementScoreForView(today, id, count)
    ViewLogConsumer->>Kafka: acknowledge
    end
Loading
sequenceDiagram
    actor Client
    participant LikeAPI
    participant ProductLikedConsumer
    participant ProductMetricsRepository
    participant RankingRedisService
    participant Kafka

    rect rgb(200, 220, 255)
    Note over Client,Kafka: ์ข‹์•„์š” ์ด๋ฒคํŠธ ํ๋ฆ„
    Client->>LikeAPI: POST /likes
    LikeAPI->>Kafka: publish ProductLikedEvent
    LikeAPI-->>Client: 200 OK
    end
    
    rect rgb(240, 200, 200)
    Note over Kafka,RankingRedisService: ์ปจ์Šˆ๋จธ ์ฒ˜๋ฆฌ
    Kafka->>ProductLikedConsumer: consume ProductLikedEvent
    
    alt ๋ฏธ์ฒ˜๋ฆฌ ์ด๋ฒคํŠธ
        ProductLikedConsumer->>ProductMetricsRepository: update/create metrics
        alt liked=true์ธ ๊ฒฝ์šฐ
            ProductLikedConsumer->>RankingRedisService: incrementScoreForLike(today, productId)
        else liked=false์ธ ๊ฒฝ์šฐ
            Note over RankingRedisService: ์ˆœ์œ„ ์ ์ˆ˜ ๋ฏธ๋ณ€๊ฒฝ
        end
        ProductLikedConsumer->>Kafka: save event as handled
    else ์ค‘๋ณต ์ด๋ฒคํŠธ
        Note over ProductLikedConsumer: idempotence check๋กœ ์Šคํ‚ต
    end
    
    ProductLikedConsumer->>Kafka: acknowledge
    end
Loading
sequenceDiagram
    actor Client
    participant RankingAPI
    participant RankingFacade
    participant RankingRedisService
    participant ProductRepository

    Client->>RankingAPI: GET /api/v1/rankings?date=20251201&page=0&size=10
    RankingAPI->>RankingFacade: getRankings(date, page, size)
    
    rect rgb(200, 220, 255)
    Note over RankingFacade,RankingRedisService: Redis์—์„œ ์ƒ์œ„ ์ˆœ์œ„ ์กฐํšŒ
    RankingFacade->>RankingRedisService: getTopProducts(parsedDate, offset, limit)
    RankingRedisService-->>RankingFacade: List<RankingEntry> (productId, score)
    RankingFacade->>RankingRedisService: getTotalCount(parsedDate)
    RankingRedisService-->>RankingFacade: totalCount
    end
    
    rect rgb(220, 240, 200)
    Note over RankingFacade,ProductRepository: ์ œํ’ˆ ์ •๋ณด ๊ฒฐํ•ฉ
    loop ๊ฐ RankingEntry๋งˆ๋‹ค
        RankingFacade->>ProductRepository: findById(productId)
        ProductRepository-->>RankingFacade: Product (id, name, price)
    end
    end
    
    RankingFacade->>RankingFacade: map to RankingResponse (rank, productId, name, price, score)
    RankingFacade-->>RankingAPI: RankingListResponse
    RankingAPI-->>Client: 200 OK {RankingListResponse}
Loading
sequenceDiagram
    actor Client
    participant OrderAPI
    participant OrderCompletedConsumer
    participant RankingRedisService
    participant Kafka

    rect rgb(200, 220, 255)
    Note over Client,Kafka: ์ฃผ๋ฌธ ์™„๋ฃŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰
    Client->>OrderAPI: confirm order
    OrderAPI->>Kafka: publish OrderCompletedEvent {items=[{productId, quantity}, ...]}
    OrderAPI-->>Client: 200 OK
    end
    
    rect rgb(240, 200, 200)
    Note over Kafka,RankingRedisService: ์ปจ์Šˆ๋จธ ์ฒ˜๋ฆฌ (์ˆ˜๋Ÿ‰ ๊ธฐ๋ฐ˜ ์ ์ˆ˜)
    Kafka->>OrderCompletedConsumer: consume OrderCompletedEvent
    
    alt ๋ฏธ์ฒ˜๋ฆฌ ์ด๋ฒคํŠธ
        loop ๊ฐ OrderItem๋งˆ๋‹ค
            OrderCompletedConsumer->>RankingRedisService: incrementScoreForOrder(date, productId, quantity)
            Note over RankingRedisService: score += quantity ร— 0.6
        end
        OrderCompletedConsumer->>Kafka: save event as handled
    else ์ค‘๋ณต ์ด๋ฒคํŠธ
        Note over OrderCompletedConsumer: idempotence check๋กœ ์Šคํ‚ต
    end
    
    OrderCompletedConsumer->>Kafka: acknowledge
    end
Loading

Estimated code review effort

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

Possibly related PRs

  • [volume-5] ์ธ๋ฑ์Šค ๋ฐ Redis ์บ์‹œ ์ ์šฉย #145: ProductFacade ์ˆ˜์ • ํฌํ•จ; ์ˆœ์œ„/์บ์‹œ/์ด๋ฒคํŠธ ์˜์กด์„ฑ ๋ฐ ๋™์ž‘ ์ถ”๊ฐ€๋กœ ์ง์ ‘ ์—ฐ๊ด€
  • [volume-5] ์ธ๋ฑ์Šค ๋ฐ Redis ์บ์‹œ ์ ์šฉย #143: ProductFacade ๋ฐ ProductDto.ProductDetailResponse.from ๋ฉ”์„œ๋“œ ์ˆ˜์ •; ์บ์‹œ ์ง€์› ํ›„ ์ด PR์—์„œ ๋ทฐ ์ด๋ฒคํŠธ ๋ฐ RankingRedisService ํ†ตํ•ฉ์œผ๋กœ ๊ฐ™์€ ํด๋ž˜์Šค ์—ฐ๊ด€
  • [volume - 8] Decoupling with Kafka ย #205: Kafka ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ๋ฐœํ–‰/์ปจ์Šˆ๋จธ, outbox/idempotent ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ, ์ œํ’ˆ ๋ทฐ/์ข‹์•„์š”/์ฃผ๋ฌธ ์ˆœ์œ„ ํ†ตํ•ฉ์ด ์ด์ „ ๋””์ปคํ”Œ๋ง ์ž‘์—…๊ณผ ์ง์ ‘ ํ™•์žฅ ๊ด€๊ณ„

Suggested labels

enhancement, kafka, feature, ranking

Poem

๐Ÿฐ ์ˆœ์œ„์˜ ์ •์›์„ ๊ฐ€๊พธ๋ฉฐ,
Redis ์†์— ์ ์ˆ˜๋“ค์ด ์ถค์„ ์ถ”๊ณ ,
์ด๋ฒคํŠธ์˜ ํ๋ฆ„ ๋”ฐ๋ผ ์ˆœ์œ„๊ฐ€ ํŽผ์ณ์ง€๋„ค.
๋งค์ผ ์ƒˆ๋กœ์šด ์ˆœ์œ„ ๋Œ€ํšŒ,
์ œํ’ˆ๋“ค์˜ ์ธ๊ธฐ ๊ฒฝ์Ÿ, ํ˜ธ์˜ค์˜ ํ–ฅ์—ฐ! ๐Ÿ†

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 1.16% 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 ์„ค๋ช…์ด ์š”๊ตฌ ํ…œํ”Œ๋ฆฟ์˜ ๋ชจ๋“  ์ฃผ์š” ์„น์…˜์„ ์ถฉ๋ถ„ํžˆ ํฌํ•จํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์š”์•ฝ, ๋ฆฌ๋ทฐ ํฌ์ธํŠธ, ์ฒดํฌ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋ชจ๋‘ ์ž‘์„ฑ๋˜์—ˆ์œผ๋ฉฐ, ๋ณ€๊ฒฝ ์‚ฌํ•ญ์˜ ๋ชฉ์ ์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

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

๐Ÿงน Nitpick comments (13)
docker-compose.yml (1)

56-61: Kafka ๋‚ด๋ถ€ ํฌํŠธ ๋ณ€๊ฒฝ ๊ฒ€์ฆ ํ•„์š”

ํฌํŠธ ๋งคํ•‘์ด "19092:9092"์—์„œ "19092:19092"๋กœ ๋ณ€๊ฒฝ๋˜์–ด ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ํฌํŠธ๋„ 19092๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. KAFKA_LISTENERS ์„ค์ •์ด ์ด์— ๋งž๊ฒŒ ์ถ”๊ฐ€๋˜์–ด ๊ตฌ์„ฑ ์ž์ฒด๋Š” ์ •์ƒ ๋™์ž‘ํ•˜์ง€๋งŒ, Kafka์˜ ํ‘œ์ค€ ๋‚ด๋ถ€ ํฌํŠธ๋Š” 9092์ž…๋‹ˆ๋‹ค.

๊ธฐ์กด์— ๋‚ด๋ถ€ ํฌํŠธ 9092๋ฅผ ๊ธฐ๋Œ€ํ•˜๋Š” ์„ค์ •์ด๋‚˜ ์„œ๋น„์Šค๊ฐ€ ์—†๋Š”์ง€ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.

๐Ÿ’ก ํ‘œ์ค€ Kafka ํฌํŠธ ๊ทœ์น™์„ ๋”ฐ๋ฅด๋Š” ๋Œ€์•ˆ ๊ตฌ์„ฑ

๋ณด๋‹ค ์ผ๋ฐ˜์ ์ธ Kafka ์„ค์ • ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

     ports:
-      - "19092:19092"
+      - "19092:9092"
     environment:
       KAFKA_BROKER_ID: 1
       KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
-      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:19092
+      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
       KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:19092

์ด ๋ฐฉ์‹์€ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์—์„œ๋Š” ํ‘œ์ค€ ํฌํŠธ 9092๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ํ˜ธ์ŠคํŠธ์—์„œ๋งŒ 19092๋กœ ์ ‘๊ทผํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ตฌ์„ฑ๋„ ๋™์ž‘ํ•˜์ง€๋งŒ, 19092๋ฅผ ๋‚ด๋ถ€ ํฌํŠธ๋กœ ์‚ฌ์šฉํ•  ํŠน๋ณ„ํ•œ ์ด์œ ๊ฐ€ ์—†๋‹ค๋ฉด ํ‘œ์ค€ ๊ทœ์น™์„ ๋”ฐ๋ฅด๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/OrderCompletedConsumer.java (1)

47-55: null ์•ˆ์ „์„ฑ ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

Line 48์—์„œ event.getItems()๊ฐ€ null์ธ ๊ฒฝ์šฐ NullPointerException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฉ์–ด์  ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
 private void processOrderCompletedEvent(OrderCompletedEvent event) {
+    if (event.getItems() == null || event.getItems().isEmpty()) {
+        log.warn("์ฃผ๋ฌธ ํ•ญ๋ชฉ์ด ๋น„์–ด์žˆ์Œ: orderId={}", event.getOrderId());
+        return;
+    }
     for (OrderCompletedEvent.OrderItemInfo item : event.getItems()) {
         rankingRedisService.incrementScoreForOrder(
                 event.getOccurredAt().toLocalDate(),
                 item.getProductId(),
                 item.getQuantity()
         );
     }
 }
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingDto.java (1)

7-13: LGTM!

ํŽ˜์ด์ง€๋„ค์ด์…˜ ์‘๋‹ต์„ ์œ„ํ•œ ๊น”๋”ํ•œ record ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ ํŽธ์˜๋ฅผ ์œ„ํ•ด totalPages ํ•„๋“œ ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค (optional).

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

18-26: LGTM!

Facade ํŒจํ„ด์„ ์‚ฌ์šฉํ•œ ๊น”๋”ํ•œ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค.

size์— ๋Œ€ํ•œ ์ƒํ•œ ์ œํ•œ(์˜ˆ: max 100)์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์š”์ฒญ์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž size ์ƒํ•œ ์ œํ•œ ์˜ˆ์‹œ
     @GetMapping
     public ApiResponse<RankingDto.RankingListResponse> getRankings(
             @RequestParam(required = false) String date,
             @RequestParam(defaultValue = "0") int page,
-            @RequestParam(defaultValue = "20") int size
+            @RequestParam(defaultValue = "20") @Max(100) int size
     ) {
apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/OrderCompletedConsumerTest.java (1)

47-81: LGTM!

์ •์ƒ ์ฒ˜๋ฆฌ์™€ ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ์ž˜ ์ปค๋ฒ„ํ•˜๋Š” ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค. ์ฃผ๋ฌธ ํ•ญ๋ชฉ๋ณ„ ๋žญํ‚น ์ ์ˆ˜ ์ฆ๊ฐ€์™€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๊ธฐ๋ก ์ €์žฅ์„ ์ ์ ˆํžˆ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ack๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๊ณ  ์žฌ์‹œ๋„๊ฐ€ ๊ฐ€๋Šฅํ•œ์ง€ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ๋„ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

47-53: ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•ด @BeforeEach์—์„œ๋„ ์ •๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

ํ˜„์žฌ @AfterEach์—์„œ๋งŒ ์ •๋ฆฌํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ์ด์ „ ํ…Œ์ŠคํŠธ ์‹คํŒจ๋กœ ์ธํ•ด ์ •๋ฆฌ๊ฐ€ ์•ˆ ๋œ ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ ์˜ค์—ผ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ ํŒŒ์ผ๋“ค(ProductFacadeRankTest, RankingFacadeTest)์ฒ˜๋Ÿผ @BeforeEach์—์„œ๋„ Redis ํ‚ค๋ฅผ ์‚ญ์ œํ•˜๋Š” ๊ฒƒ์ด ๋” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •์‚ฌํ•ญ
+    @BeforeEach
+    void setUp() {
+        String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+        redisTemplate.delete("ranking:all:" + today);
+    }
+
     @AfterEach
     void tearDown() {
         databaseCleanUp.truncateAllTables();
apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/StockDepletedConsumerTest.java (1)

53-64: consumeTest2์—์„œ invalidateProductListCaches ํ˜ธ์ถœ ์—ฌ๋ถ€ ๊ฒ€์ฆ ๋ˆ„๋ฝ

์ค‘๋ณต ์ด๋ฒคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ deleteProductDetail์€ never() ๊ฒ€์ฆ์ด ์žˆ์ง€๋งŒ, invalidateProductListCaches๋„ ํ˜ธ์ถœ๋˜์ง€ ์•Š์•„์•ผ ํ•˜๋ฏ€๋กœ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •์‚ฌํ•ญ
         verify(productCacheService, never()).deleteProductDetail(any());
+        verify(productCacheService, never()).invalidateProductListCaches();
         verify(eventHandledRepository, never()).save(any());
         verify(acknowledgment).acknowledge();
docs/week9/02-sequence-diagrams.md (1)

123-127: ๋‹ค์ด์–ด๊ทธ๋žจ๊ณผ ๊ตฌํ˜„ ๋ถˆ์ผ์น˜

๋‹ค์ด์–ด๊ทธ๋žจ์—์„œ๋Š” "์ตœ์ดˆ ์ ์žฌ ์‹œ"์—๋งŒ EXPIRE๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์œผ๋กœ ํ‘œ์‹œ๋˜์–ด ์žˆ์ง€๋งŒ, RankingRedisService์˜ Lua ์Šคํฌ๋ฆฝํŠธ๋Š” ๋งค ZINCRBY ํ˜ธ์ถœ ์‹œ ํ•ญ์ƒ EXPIRE๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

PR ์„ค๋ช…์—์„œ ์–ธ๊ธ‰๋œ ์งˆ๋ฌธ(#2)๊ณผ ๊ด€๋ จํ•˜์—ฌ, ํ˜„์žฌ ๊ตฌํ˜„(๋งค๋ฒˆ EXPIRE ํ˜ธ์ถœ)์ด ๋” ์•ˆ์ „ํ•˜๊ณ  ๋‹จ์ˆœํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์ด์–ด๊ทธ๋žจ์„ ์‹ค์ œ ๊ตฌํ˜„์— ๋งž๊ฒŒ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜, "opt" ๋Œ€์‹  ํ•ญ์ƒ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์œผ๋กœ ํ‘œ์‹œํ•˜๋Š” ๊ฒƒ์ด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ViewLogConsumer.java (1)

45-57: ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ์ ๊ณผ ์ฒ˜๋ฆฌ ์‹œ์ ์˜ ๋‚ ์งœ ๋ถˆ์ผ์น˜ ๊ฐ€๋Šฅ์„ฑ

LocalDate.now()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋žญํ‚น ๋‚ ์งœ๋ฅผ ๊ฒฐ์ •ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ž์ • ์ „ํ›„์— ๋ฐœํ–‰๋œ ์ด๋ฒคํŠธ๊ฐ€ ๋‹ค์Œ ๋‚  ์ฒ˜๋ฆฌ๋  ๊ฒฝ์šฐ, ์ด๋ฒคํŠธ ๋ฐœ์ƒ์ผ๊ณผ ๋žญํ‚น ์ง‘๊ณ„์ผ์ด ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ProductViewedEvent์— occurredAt ํ•„๋“œ๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ, ์ด๋ฒคํŠธ์˜ ๋ฐœ์ƒ ๋‚ ์งœ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋žญํ‚น์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ด ๋” ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ ์ˆ˜์ •
-            LocalDate today = LocalDate.now();
-            Map<Long, Long> viewCountByProduct = events.stream()
-                    .collect(Collectors.groupingBy(
-                            ProductViewedEvent::getProductId,
-                            Collectors.counting()
-                    ));
-
-            for (Map.Entry<Long, Long> entry : viewCountByProduct.entrySet()) {
-                Long productId = entry.getKey();
-                int count = entry.getValue().intValue();
-                rankingRedisService.incrementScoreForView(today, productId, count);
-            }
+            // ์ด๋ฒคํŠธ ๋ฐœ์ƒ์ผ ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ•‘
+            Map<LocalDate, Map<Long, Long>> viewCountByDateAndProduct = events.stream()
+                    .collect(Collectors.groupingBy(
+                            e -> e.getOccurredAt().toLocalDate(),
+                            Collectors.groupingBy(
+                                    ProductViewedEvent::getProductId,
+                                    Collectors.counting()
+                            )
+                    ));
+
+            for (Map.Entry<LocalDate, Map<Long, Long>> dateEntry : viewCountByDateAndProduct.entrySet()) {
+                LocalDate date = dateEntry.getKey();
+                for (Map.Entry<Long, Long> productEntry : dateEntry.getValue().entrySet()) {
+                    rankingRedisService.incrementScoreForView(date, productEntry.getKey(), productEntry.getValue().intValue());
+                }
+            }
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)

46-59: ์‚ญ์ œ๋œ ์ƒํ’ˆ์œผ๋กœ ์ธํ•œ ์ˆœ์œ„ ๋ˆ„๋ฝ ๊ฐ€๋Šฅ์„ฑ

productMap.get(entry.productId())๊ฐ€ null์ธ ๊ฒฝ์šฐ(์‚ญ์ œ๋œ ์ƒํ’ˆ) ํ•ด๋‹น ํ•ญ๋ชฉ์„ ๊ฑด๋„ˆ๋›ฐ์ง€๋งŒ, rank++๋Š” ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์ˆœ์œ„๊ฐ€ 1, 2, 4์ฒ˜๋Ÿผ ์ค‘๊ฐ„์— ๋น ์งˆ ์ˆ˜ ์žˆ๊ณ , totalCount์™€ ์‹ค์ œ ์‘๋‹ต ๊ฐœ์ˆ˜๊ฐ€ ๋ถˆ์ผ์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜๋„๋œ ๋™์ž‘์ด๋ผ๋ฉด ๊ดœ์ฐฎ์ง€๋งŒ, ์ผ๊ด€๋œ ์ˆœ์œ„๋ฅผ ์›ํ•œ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ ์ˆ˜์ • - ์‚ญ์ œ๋œ ์ƒํ’ˆ๋„ ์ˆœ์œ„์— ๋ฐ˜์˜
         for (RankingEntry entry : entries) {
             Product product = productMap.get(entry.productId());
             if (product != null) {
                 rankings.add(new RankingDto.RankingResponse(
-                        rank++,
+                        rank,
                         product.getId(),
                         product.getName(),
                         product.getPrice(),
                         entry.score()
                 ));
             }
+            rank++; // ์‚ญ์ œ๋œ ์ƒํ’ˆ๋„ ์ˆœ์œ„ ์†Œ๋น„
         }
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRedisService.java (2)

43-47: Redis ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์‹œ ๋ฐฉ์–ด์  ์ฒ˜๋ฆฌ ๊ณ ๋ ค

Long.parseLong(tuple.getValue())์—์„œ Redis์— ์ž˜๋ชป๋œ ํ˜•์‹์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ NumberFormatException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์‹œ์Šคํ…œ์—์„œ ์ง์ ‘ ๊ด€๋ฆฌํ•˜๋Š” ํ‚ค์ด๋ฏ€๋กœ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ์€ ๋‚ฎ์ง€๋งŒ, ์šด์˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด ๋ฐฉ์–ด์  ์ฒ˜๋ฆฌ๋ฅผ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ ์ˆ˜์ •
         for (ZSetOperations.TypedTuple<String> tuple : tuples) {
-            Long productId = Long.parseLong(tuple.getValue());
-            Double score = tuple.getScore();
-            entries.add(new RankingEntry(productId, score));
+            try {
+                Long productId = Long.parseLong(tuple.getValue());
+                Double score = tuple.getScore();
+                entries.add(new RankingEntry(productId, score));
+            } catch (NumberFormatException e) {
+                log.warn("์ž˜๋ชป๋œ productId ํ˜•์‹: {}", tuple.getValue());
+            }
         }

19-21: Hot Key ๊ด€๋ จ ์•„ํ‚คํ…์ฒ˜ ์กฐ์–ธ

PR ์งˆ๋ฌธ(#1)์— ๋Œ€ํ•œ ๋‹ต๋ณ€: ํ˜„์žฌ ๋‹จ์ผ ์ผ๋ณ„ ํ‚ค(ranking:all:{yyyyMMdd}) ๊ตฌ์กฐ๋Š” ์ค‘์†Œ ๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ์—์„œ๋Š” ๋ฌธ์ œ์—†์Šต๋‹ˆ๋‹ค.

ํ™•์žฅ ์‹œ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ๋Š” ์ „๋žต:

  1. ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ƒค๋”ฉ: ranking:category:{categoryId}:{date} - ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋žญํ‚น์ด ํ•„์š”ํ•  ๋•Œ
  2. ์‹œ๊ฐ„๋Œ€ ๋ฒ„ํ‚ท: ranking:all:{date}:{hourBucket} + ์ฃผ๊ธฐ์  ๋ณ‘ํ•ฉ - ์“ฐ๊ธฐ ๋ถ„์‚ฐ
  3. ๋กœ์ปฌ ์–ด๊ทธ๋ฆฌ๊ฒŒ์ด์…˜: Consumer์—์„œ ์ผ์ • ๊ธฐ๊ฐ„ ๋ฉ”๋ชจ๋ฆฌ์— ์ง‘๊ณ„ ํ›„ ๋ฐฐ์น˜๋กœ Redis ์—…๋ฐ์ดํŠธ

Redis Cluster ์‚ฌ์šฉ ์‹œ์—๋„ ๋‹จ์ผ ํ‚ค๋Š” ๋‹จ์ผ ๋…ธ๋“œ์—๋งŒ ํ• ๋‹น๋˜๋ฏ€๋กœ, ํŠธ๋ž˜ํ”ฝ์ด ๊ธ‰์ฆํ•˜๋ฉด ์ƒค๋”ฉ ๊ฒ€ํ† ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/infrastructure/ranking/RankingRedisServiceTest.java (1)

34-52: ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ์šฐ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค

RankingRedisService์˜ ์ฃผ์š” ๊ธฐ๋Šฅ๋“ค์ด ์ž˜ ํ…Œ์ŠคํŠธ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ถ€๋™์†Œ์ˆ˜์  ์˜ค์ฐจ ํ—ˆ์šฉ(within())๊ณผ TTL ํ…Œ์ŠคํŠธ์˜ ์‹œ๊ฐ„ ์˜ค์ฐจ ํ—ˆ์šฉ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์ถ”๊ฐ€๋กœ getTopProducts์˜ ํŽ˜์ด์ง€๋„ค์ด์…˜(offset > 0) ์ผ€์ด์Šค์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋„ ์žˆ์œผ๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ํŽ˜์ด์ง€๋„ค์ด์…˜ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ ์ œ์•ˆ
@Test
@DisplayName("offset์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ์กฐํšŒํ•œ๋‹ค")
void getTopProducts_withOffset() {
    // arrange
    redisTemplate.opsForZSet().add(KEY, "1", 5.0);
    redisTemplate.opsForZSet().add(KEY, "2", 4.0);
    redisTemplate.opsForZSet().add(KEY, "3", 3.0);
    redisTemplate.opsForZSet().add(KEY, "4", 2.0);

    // act - 2๋ฒˆ์งธ ํŽ˜์ด์ง€ (offset=2, limit=2)
    List<RankingEntry> result = rankingRedisService.getTopProducts(TODAY, 2, 2);

    // assert
    assertThat(result).hasSize(2);
    assertThat(result.get(0).productId()).isEqualTo(3L); // 3์œ„
    assertThat(result.get(1).productId()).isEqualTo(4L); // 4์œ„
}
๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 1160a4e and c5a89f6.

๐Ÿ“’ Files selected for processing (34)
  • 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/domain/like/event/ProductLikedEvent.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/event/OrderCompletedEvent.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewLog.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewLogRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductViewedEvent.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/event/StockDepletedEvent.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/CatalogEventConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/OrderCompletedConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ProductLikedConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/StockDepletedConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ViewLogConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/event/ViewEventPublisher.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewLogJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewLogRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingEntry.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRedisService.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingController.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingDto.java
  • apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeRankTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/ranking/RankingFacadeTest.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/CatalogEventConsumerTest.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/OrderCompletedConsumerTest.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/ProductLikedConsumerTest.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/StockDepletedConsumerTest.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventHandlerTest.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/ranking/RankingRedisServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java
  • docker-compose.yml
  • docs/week9/01-requirements.md
  • docs/week9/02-sequence-diagrams.md
๐Ÿ’ค Files with no reviewable changes (2)
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/CatalogEventConsumerTest.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/CatalogEventConsumer.java
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (9)
๐Ÿ““ 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-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-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/event/ViewEventPublisher.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductViewedEvent.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/event/OrderCompletedEvent.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/StockDepletedConsumerTest.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/StockDepletedConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/OrderCompletedConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ViewLogConsumer.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventHandlerTest.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ProductLikedConsumer.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/ProductLikedConsumerTest.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/OrderCompletedConsumerTest.java
๐Ÿ“š 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-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/event/ViewEventPublisher.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventHandlerTest.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ProductLikedConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/event/ProductLikedEvent.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/ProductLikedConsumerTest.java
๐Ÿ“š Learning: 2025-12-19T09:30:12.459Z
Learnt from: HongChangMo
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 198
File: apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventListener.java:0-0
Timestamp: 2025-12-19T09:30:12.459Z
Learning: In the loopers-spring-java-template repository's commerce-api module, when entities are managed within a transactional context (e.g., Transactional methods), prefer relying on JPA dirty checking for automatic persistence rather than explicit save() calls. Both Payment and Order entities in PaymentEventListener use this pattern, with state changes automatically flushed on transaction commit.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/event/OrderCompletedEvent.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewLogRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/OrderCompletedConsumer.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventHandlerTest.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewLogRepository.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/OrderCompletedConsumerTest.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-api/src/test/java/com/loopers/infrastructure/consumer/StockDepletedConsumerTest.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/StockDepletedConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/OrderCompletedConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ViewLogConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ProductLikedConsumer.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-api/src/test/java/com/loopers/infrastructure/consumer/StockDepletedConsumerTest.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/StockDepletedConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/OrderCompletedConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ViewLogConsumer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ProductLikedConsumer.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
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.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/domain/product/ProductViewLog.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/event/ProductLikedEvent.java
๐Ÿ“š Learning: 2025-11-12T13:04:50.782Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 72
File: core/domain/src/main/java/com/loopers/core/domain/productlike/vo/ProductLikeId.java:5-7
Timestamp: 2025-11-12T13:04:50.782Z
Learning: In the com.loopers codebase, domain entity ID value objects (e.g., ProductLikeId, OrderItemId, ProductId, PaymentId, OrderId, BrandId) are system-generated identifiers and do not require pattern validation (regex, length checks). They are implemented as simple records with a String value and an empty() factory method returning null for unsaved entities. This differs from UserIdentifier, which is a user-supplied login ID that requires format validation. Domain IDs should not be flagged for missing validation logic in the create() method.
<!-- [add_learning]
UserIdentifier์™€ ๊ฐ™์€ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ID์™€ ProductLikeId, OrderItemId ๋“ฑ์˜ ๋„๋ฉ”์ธ ID๋Š” ๊ฒ€์ฆ ํŒจํ„ด์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. UserIdentifier๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•˜๋Š” ๋กœ๊ทธ์ธ ID๋กœ์„œ ์ •๊ทœ์‹, ๊ธธ์ด ๋“ฑ์˜ ํ˜•์‹ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜์ง€๋งŒ, ๋„๋ฉ”์ธ ID๋Š” ์‹œ์Šคํ…œ์—์„œ ์ƒ์„ฑํ•˜๋Š” ์‹๋ณ„์ž(UUID, DB ์ƒ์„ฑ ID)์ด๋ฏ€๋กœ ํŒจํ„ด ๊ฒ€์ฆ์ด ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋„๋ฉ”์ธ ID VO๋Š” ๋‹จ์ˆœํ•œ record์™€ empty() ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewLog.java
๐Ÿงฌ Code graph analysis (10)
apps/commerce-api/src/main/java/com/loopers/infrastructure/event/ViewEventPublisher.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductViewedEvent.java (1)
  • ProductViewedEvent (5-26)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)
  • Component (24-111)
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/StockDepletedConsumer.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/product/event/StockDepletedEvent.java (1)
  • StockDepletedEvent (5-29)
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/OrderCompletedConsumer.java (1)
  • Slf4j (16-56)
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java (1)
  • Component (13-63)
apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeRankTest.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.java (1)
  • ProductDto (11-99)
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ViewLogConsumer.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductViewedEvent.java (1)
  • ProductViewedEvent (5-26)
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewLogRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-20)
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt (1)
  • log (22-119)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java (1)
apps/commerce-api/src/test/java/com/loopers/infrastructure/ranking/RankingRedisServiceTest.java (1)
  • SpringBootTest (17-165)
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ProductLikedConsumer.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/like/event/ProductLikedEvent.java (1)
  • ProductLikedEvent (5-45)
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java (1)
  • Component (13-63)
apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/ProductLikedConsumerTest.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/like/event/ProductLikedEvent.java (1)
  • ProductLikedEvent (5-45)
apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventHandlerTest.java (1)
  • ExtendWith (19-54)
apps/commerce-api/src/test/java/com/loopers/application/ranking/RankingFacadeTest.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingDto.java (1)
  • RankingDto (5-23)
apps/commerce-api/src/test/java/com/loopers/infrastructure/ranking/RankingRedisServiceTest.java (1)
  • SpringBootTest (17-165)
apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/OrderCompletedConsumerTest.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/order/event/OrderCompletedEvent.java (1)
  • OrderCompletedEvent (8-85)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingDto.java (1)
  • RankingDto (5-23)
๐Ÿ”‡ Additional comments (30)
docs/week9/01-requirements.md (1)

1-170: ์š”๊ตฌ์‚ฌํ•ญ ๋ฌธ์„œ๊ฐ€ ์ž˜ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Redis ZSET ๊ธฐ๋ฐ˜ ๋žญํ‚น ์‹œ์Šคํ…œ์˜ ์š”๊ตฌ์‚ฌํ•ญ์ด ๋ช…ํ™•ํ•˜๊ฒŒ ์ •๋ฆฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • API ์—”๋“œํฌ์ธํŠธ ๋ฐ ์ œ์•ฝ์‚ฌํ•ญ ๋ช…์‹œ
  • ์ด๋ฒคํŠธ๋ณ„ ๊ฐ€์ค‘์น˜ ๊ณ„์‚ฐ์‹ ๋ฌธ์„œํ™”
  • TTL ๋ฐ ํ‚ค ์ „๋žต ์„ค๋ช…
  • 8์ฃผ์ฐจ ํ”ผ๋“œ๋ฐฑ ๋ฐ˜์˜ ๋‚ด์—ญ ํฌํ•จ

PR ์งˆ๋ฌธ์—์„œ ์–ธ๊ธ‰ํ•˜์‹  hot key ๋ฆฌ์Šคํฌ๋„ ๋ฌธ์„œ์— ์•”์‹œ๋˜์–ด ์žˆ์œผ๋ฉฐ (์ผ๋ณ„ ๋‹จ์ผ ํ‚ค ์ „๋žต), ํ–ฅํ›„ ํŠธ๋ž˜ํ”ฝ ์ฆ๊ฐ€ ์‹œ ์ƒค๋”ฉ/์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋ถ„๋ฆฌ ์ „๋žต์„ ๊ณ ๋ คํ•˜๋Š” ๊ฒƒ์ด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/product/event/StockDepletedEvent.java (1)

7-11: ์—ญ์ง๋ ฌํ™” ์ง€์›์„ ์œ„ํ•œ ์ ์ ˆํ•œ ๋ณ€๊ฒฝ์ž…๋‹ˆ๋‹ค.

ํ•„๋“œ๋ฅผ mutable๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  protected ๊ธฐ๋ณธ ์ƒ์„ฑ์ž๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ Kafka ์ปจ์Šˆ๋จธ์—์„œ์˜ ์—ญ์ง๋ ฌํ™”๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” OrderCompletedEvent์™€ ์ผ๊ด€๋œ ํŒจํ„ด์ด๋ฉฐ, protected ์ ‘๊ทผ ์ œ์–ด์ž๋กœ ์™ธ๋ถ€ ์ธ์Šคํ„ด์Šคํ™”๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/OrderCompletedConsumer.java (2)

24-45: ์ปจ์Šˆ๋จธ ํŒจํ„ด์ด ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ์™€ ์—๋Ÿฌ ํ•ธ๋“ค๋ง์ด StockDepletedConsumer์™€ ์ผ๊ด€๋œ ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • EventHandledRepository๋ฅผ ํ†ตํ•œ ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€
  • ์„ฑ๊ณต ์‹œ acknowledgment, ์‹คํŒจ ์‹œ ์žฌ์ฒ˜๋ฆฌ ๋Œ€๊ธฐ
  • @Transactional์„ ํ†ตํ•œ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ์„ค์ •

์ฐธ๊ณ : Redis๋Š” ํŠธ๋žœ์žญ์…˜ ์™ธ๋ถ€ ์ž์›์ด๋ฏ€๋กœ, Redis ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต ํ›„ DB ์ปค๋ฐ‹ ์‹คํŒจ ์‹œ ์žฌ์ฒ˜๋ฆฌ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ํ•™์Šต๋œ ํŒ€์˜ ์‹ค์šฉ์  ๋ฉฑ๋“ฑ์„ฑ ์ „๋žต(๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์šฐ์„ , ๋‚ฎ์€ ์ค‘๋ณต ํ™•๋ฅ  ํ—ˆ์šฉ)๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.


47-55: ๋‚ ์งœ ๋ณ€ํ™˜ ์‹œ ํƒ€์ž„์กด ๋ช…์‹œ๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”.

Line 50์—์„œ occurredAt.toLocalDate()๋Š” ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ํƒ€์ž„์กด์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„ ํƒ€์ž„์กด ์„ค์ •์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋‚ ์งœ๋กœ ๋ณ€ํ™˜๋  ์ˆ˜ ์žˆ์–ด, ๋ฉ€ํ‹ฐ ๋ฆฌ์ „ ๋ฐฐํฌ ์‹œ ์ผ๊ด€์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ช…์‹œ์  ํƒ€์ž„์กด ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค:

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
+import java.time.ZoneId;
+
 private void processOrderCompletedEvent(OrderCompletedEvent event) {
+    ZoneId koreaZone = ZoneId.of("Asia/Seoul");
     for (OrderCompletedEvent.OrderItemInfo item : event.getItems()) {
         rankingRedisService.incrementScoreForOrder(
-                event.getOccurredAt().toLocalDate(),
+                event.getOccurredAt().atZone(koreaZone).toLocalDate(),
                 item.getProductId(),
                 item.getQuantity()
         );
     }
 }

๋˜๋Š” UTC ๊ธฐ์ค€ ์‚ฌ์šฉ:

         rankingRedisService.incrementScoreForOrder(
-                event.getOccurredAt().toLocalDate(),
+                event.getOccurredAt().atZone(ZoneId.of("UTC")).toLocalDate(),
                 item.getProductId(),
                 item.getQuantity()
         );
apps/commerce-api/src/main/java/com/loopers/domain/order/event/OrderCompletedEvent.java (3)

10-19: ์ด๋ฒคํŠธ ๊ตฌ์กฐ ๋ณ€๊ฒฝ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์ฃผ๋ฌธ ํ•ญ๋ชฉ ์ •๋ณด(items)๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํ•ญ๋ชฉ๋ณ„ ๋žญํ‚น ์—…๋ฐ์ดํŠธ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ํ•„๋“œ๋ฅผ mutable๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  protected ๊ธฐ๋ณธ ์ƒ์„ฑ์ž๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒƒ์€ Kafka ์—ญ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ ํ‘œ์ค€ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.


27-29: ๋ถˆ๋ณ€ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜์— ๋Œ€ํ•œ ์ฐธ๊ณ ์‚ฌํ•ญ

Line 29์˜ .toList()๋Š” ๋ถˆ๋ณ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ด๋ฒคํŠธ์˜ ๋ถˆ๋ณ€์„ฑ์„ ๋ณด์žฅํ•˜๋ฏ€๋กœ ์ ์ ˆํ•˜์ง€๋งŒ, getItems()๋ฅผ ํ†ตํ•ด ๋ฐ˜ํ™˜๋˜๋Š” ๋ฆฌ์ŠคํŠธ๋ฅผ ์ˆ˜์ •ํ•˜๋ ค๋Š” ์‹œ๋„๋Š” UnsupportedOperationException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.


65-84: OrderItemInfo ๊ตฌ์กฐ๊ฐ€ ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค.

ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์—ญ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ ๊ธฐ๋ณธ ์ƒ์„ฑ์ž์™€ public ์ƒ์„ฑ์ž๋ฅผ ๋ชจ๋‘ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. productId์™€ quantity๋งŒ ์ „๋‹ฌํ•˜์—ฌ ์ด๋ฒคํŠธ ํŽ˜์ด๋กœ๋“œ๋ฅผ ์ตœ์†Œํ™”ํ•œ ๊ฒƒ๋„ ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventHandlerTest.java (1)

51-51: ํ…Œ์ŠคํŠธ๊ฐ€ ํ”„๋กœ๋•์…˜ ๋ผ์šฐํŒ… ๋ณ€๊ฒฝ๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

ProductLikedEvent์˜ ํ† ํ”ฝ์ด "product-liked"๋กœ ๋ณ€๊ฒฝ๋œ ๊ฒƒ์„ ๋ฐ˜์˜ํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๊ธฐ๋Œ€๊ฐ’์„ ์ •ํ™•ํžˆ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java (2)

44-54: StockDepletedEvent ๋ผ์šฐํŒ…๋„ ์ผ๊ด€๋˜๊ฒŒ ๋ถ„๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

"stock-depleted" ์ „์šฉ ํ† ํ”ฝ์œผ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์žฌ๊ณ  ์†Œ์ง„ ์ด๋ฒคํŠธ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


20-30: ์ด๋ฒคํŠธ ๋ผ์šฐํŒ…์ด ๊ฐœ์„ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

ProductLikedEvent๋ฅผ ์ „์šฉ "product-liked" ํ† ํ”ฝ์œผ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์ปจ์Šˆ๋จธ ๊ฒฉ๋ฆฌ์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ด๋ฒคํŠธ ํƒ€์ž…๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ์ฒ˜๋ฆฌ ์ „๋žต์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ProductLikedConsumer์™€ StockDepletedConsumer๊ฐ€ ๊ฐ๊ฐ ์˜ฌ๋ฐ”๋ฅธ ํ† ํ”ฝ์„ ๋ฆฌ์Šค๋‹ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewLogRepository.java (1)

1-8: ๊ฐ„๊ฒฐํ•œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค.

๋ฐฐ์น˜ ์ €์žฅ์„ ์œ„ํ•œ saveAll ๋ฉ”์„œ๋“œ๋งŒ ์ œ๊ณตํ•˜๋Š” ๋ช…ํ™•ํ•œ ๊ณ„์•ฝ์ž…๋‹ˆ๋‹ค. ViewLogConsumer์—์„œ ์กฐํšŒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐฐ์น˜๋กœ ์˜์†ํ™”ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋ฉฐ, ํ‘œ์ค€ JPA ํŒจํ„ด์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.

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

1-7: ์ ์ ˆํ•œ ๋ฐ์ดํ„ฐ ์ „์†ก ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.

Redis ZSET์˜ ๋žญํ‚น ์ •๋ณด๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•œ ๋ถˆ๋ณ€ ๋ ˆ์ฝ”๋“œ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. Double ํƒ€์ž…์€ Redis ZSET ์Šค์ฝ”์–ด์™€ ์ผ์น˜ํ•˜๋ฉฐ, ๋ ˆ์ฝ”๋“œ ํŒจํ„ด์œผ๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/like/event/ProductLikedEvent.java (1)

7-13: LGTM!

Kafka/Jackson ์—ญ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ ํ‘œ์ค€ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. final ์ œ๊ฑฐ์™€ protected ๊ธฐ๋ณธ ์ƒ์„ฑ์ž ์ถ”๊ฐ€๋Š” ๋‹ค๋ฅธ ์ด๋ฒคํŠธ ํด๋ž˜์Šค(StockDepletedEvent, OrderCompletedEvent)์™€ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewLogJpaRepository.java (1)

1-7: LGTM!

ProductViewLog ์—”ํ‹ฐํ‹ฐ๋ฅผ ์œ„ํ•œ ํ‘œ์ค€ Spring Data JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/StockDepletedConsumer.java (1)

24-51: LGTM!

OrderCompletedConsumer์™€ ์ผ๊ด€๋œ ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. EventHandledRepository๋ฅผ ํ†ตํ•œ ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€์™€ ์ˆ˜๋™ acknowledge๋ฅผ ํ†ตํ•œ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewLog.java (1)

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

  • JPA ์—”ํ‹ฐํ‹ฐ ๊ตฌ์กฐ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • (product_id, created_at) ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋Š” ์ƒํ’ˆ๋ณ„ ์‹œ๊ฐ„ ๋ฒ”์œ„ ์กฐํšŒ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
  • @PrePersist๋ฅผ ํ†ตํ•œ createdAt ์ž๋™ ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.
  • Factory ๋ฉ”์„œ๋“œ ํŒจํ„ด์ด ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ๊ธฐ์กด ํŒจํ„ด๊ณผ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeRankTest.java (1)

66-103: LGTM! ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์„ธ ๊ฐ€์ง€ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ ์ ˆํžˆ ์ปค๋ฒ„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ๋žญํ‚น 2์œ„ ์ƒํ’ˆ ์กฐํšŒ ์‹œ rank=2 ๋ฐ˜ํ™˜
  2. ๋žญํ‚น 1์œ„ ์ƒํ’ˆ ์กฐํšŒ ์‹œ rank=1 ๋ฐ˜ํ™˜
  3. ๋žญํ‚น์— ์—†๋Š” ์ƒํ’ˆ ์กฐํšŒ ์‹œ rank=null ๋ฐ˜ํ™˜

ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ(Redis ์ •๋ฆฌ + DB ์ดˆ๊ธฐํ™”)๋„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

55-95: LGTM! E2E ํ…Œ์ŠคํŠธ ๊ตฌ์กฐ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Controller ๊ตฌํ˜„ ๋Œ€๊ธฐ ์ค‘์ด๋ผ @Disabled ์ฒ˜๋ฆฌ๋œ ๊ฒƒ์€ ์ ์ ˆํ•˜๋ฉฐ, ํ…Œ์ŠคํŠธ ๋กœ์ง ์ž์ฒด๋Š” Redis ZSET ๋ฐ์ดํ„ฐ ์„ค์ •๋ถ€ํ„ฐ API ํ˜ธ์ถœ, ์‘๋‹ต ๊ฒ€์ฆ๊นŒ์ง€ ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

68-130: LGTM! ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ํฌ๊ด„์ ์œผ๋กœ ์ž‘์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋„ค ๊ฐ€์ง€ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ž˜ ์ปค๋ฒ„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ์ ์ˆ˜ ์ˆœ ์ •๋ ฌ ๋ฐ ์ƒํ’ˆ ์ •๋ณด ๋ฐ˜ํ™˜
  2. ์ „์ฒด ๊ฐœ์ˆ˜(totalElements) ๋ฐ˜ํ™˜
  3. ๋นˆ ๋žญํ‚น ์ฒ˜๋ฆฌ
  4. ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋™์ž‘

ํŠนํžˆ getRankingsTest4์—์„œ 2ํŽ˜์ด์ง€ ์กฐํšŒ ์‹œ 3์œ„ ์ƒํ’ˆ๋งŒ ๋ฐ˜ํ™˜๋˜๊ณ  rank=3์œผ๋กœ ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/ProductLikedConsumerTest.java (1)

47-91: LGTM! Consumer ํ…Œ์ŠคํŠธ๊ฐ€ ํ•ต์‹ฌ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ž˜ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์„ธ ๊ฐ€์ง€ ์ผ€์ด์Šค๋ฅผ ๋ช…ํ™•ํžˆ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค:

  1. ์ข‹์•„์š” ์ด๋ฒคํŠธ: ๋ฉฑ๋“ฑ์„ฑ ์ €์žฅ + ๋ฉ”ํŠธ๋ฆญ์Šค ์—…๋ฐ์ดํŠธ + ๋žญํ‚น ์ ์ˆ˜ ์ฆ๊ฐ€ + ack
  2. ์ข‹์•„์š” ์ทจ์†Œ ์ด๋ฒคํŠธ: ๋ฉ”ํŠธ๋ฆญ์Šค๋งŒ ์—…๋ฐ์ดํŠธ, ๋žญํ‚น ์ ์ˆ˜๋Š” ๋ณ€๊ฒฝ ์•ˆ ํ•จ + ack
  3. ์ค‘๋ณต ์ด๋ฒคํŠธ: ์ฒ˜๋ฆฌ ์Šคํ‚ต, ack๋งŒ ์ˆ˜ํ–‰

ํŠนํžˆ unlike ์‹œ ๋žญํ‚น ์ ์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š” ๋™์ž‘์„ verify(rankingRedisService, never())๋กœ ๋ช…ํ™•ํžˆ ๊ฒ€์ฆํ•œ ์ ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewLogRepositoryImpl.java (1)

10-19: LGTM! ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„์ด ๊ฐ„๊ฒฐํ•˜๊ณ  ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

JPA ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋กœ์˜ ๋‹จ์ˆœ ์œ„์ž„ ํŒจํ„ด์ด ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ๊ธฐ์กด ๊ตฌ์กฐ์™€ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ProductLikedConsumer.java (2)

27-48: ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๋‚ด Redis ์—ฐ์‚ฐ์— ๋Œ€ํ•œ ์ฃผ์˜

@Transactional ๋‚ด์—์„œ Redis ์—ฐ์‚ฐ(rankingRedisService.incrementScoreForLike)์ด ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค. DB ์ปค๋ฐ‹ ์‹คํŒจ ์‹œ Redis ์—…๋ฐ์ดํŠธ๋Š” ๋กค๋ฐฑ๋˜์ง€ ์•Š์•„ ์ผ์‹œ์  ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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


50-61: LGTM!

์ข‹์•„์š” ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋กœ์ง์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. liked=true์ผ ๋•Œ๋งŒ ๋žญํ‚น ์ ์ˆ˜๋ฅผ ์ฆ๊ฐ€์‹œํ‚ค๊ณ , ์ข‹์•„์š” ์ทจ์†Œ ์‹œ์—๋Š” ZSET์„ ์—…๋ฐ์ดํŠธํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ์˜ ์„ค๊ณ„์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

docs/week9/02-sequence-diagrams.md (1)

1-107: ๋ฌธ์„œํ™” ํ’ˆ์งˆ์ด ์šฐ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค

์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ๋“ค์ด ์‹œ์Šคํ…œ์˜ ์ด๋ฒคํŠธ ํ๋ฆ„์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์„ค๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€์ค‘์น˜ ๊ฐ’(0.1, 0.2, 0.6)์ด RankingRedisService์˜ ์ƒ์ˆ˜์™€ ์ผ์น˜ํ•˜๋ฉฐ, ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ์ด ์ž˜ ํ‘œํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ViewLogConsumer.java (1)

32-65: ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ ์—†์ด ๋ฐฐ์น˜ ์†Œ๋น„ - ์˜๋„๋œ ์„ค๊ณ„์ธ์ง€ ํ™•์ธ

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

๋‹ค๋งŒ ์žฌ์ฒ˜๋ฆฌ ์‹œ MySQL์— ์ค‘๋ณต ๋กœ๊ทธ๊ฐ€ ์ €์žฅ๋˜๊ณ  Redis ์ ์ˆ˜๊ฐ€ ์ค‘๋ณต ์ฆ๊ฐ€ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์ด ๋™์ž‘์ด ํ—ˆ์šฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

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

48-86: LGTM!

ProductDetailResponse์— rank ํ•„๋“œ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. nullable Long ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜์—ฌ ๋žญํ‚น์— ์—†๋Š” ์ƒํ’ˆ์„ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๊ณ , ์บ์‹œ ๊ธฐ๋ฐ˜ ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์—์„œ isLiked๋ฅผ null๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ๋„ ์ธ์ฆ ์ปจํ…์ŠคํŠธ ๋ถ€์žฌ ์ƒํ™ฉ์— ๋งž๋Š” ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

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

27-44: LGTM!

ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋กœ์ง๊ณผ ๋ฐฐ์น˜ ์ƒํ’ˆ ์กฐํšŒ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋นˆ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ early return๊ณผ N+1 ๋ฌธ์ œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ findAllByIdIn ์‚ฌ์šฉ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRedisService.java (2)

23-27: Lua ์Šคํฌ๋ฆฝํŠธ ์ ‘๊ทผ ๋ฐฉ์‹์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค

ZINCRBY์™€ EXPIRE๋ฅผ ์›์ž์ ์œผ๋กœ ์‹คํ–‰ํ•˜๋Š” Lua ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

PR ์งˆ๋ฌธ(#2)์— ๋Œ€ํ•œ ๋‹ต๋ณ€: ๋งค๋ฒˆ EXPIRE๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ํ˜„์žฌ ๋ฐฉ์‹์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ์กฐ๊ฑด๋ถ€ EXPIRE(EXPIREAT + PTTL ์ฒดํฌ ๋“ฑ)๋Š” ๋ณต์žก์„ฑ๋งŒ ์ฆ๊ฐ€์‹œํ‚ค๊ณ , ๋งค๋ฒˆ ํ˜ธ์ถœํ•ด๋„ ์„ฑ๋Šฅ ์˜ํ–ฅ์ด ๋ฏธ๋ฏธํ•ฉ๋‹ˆ๋‹ค. TTL์ด ๊ฐฑ์‹ ๋˜์–ด ํ™œ์„ฑ ํ‚ค๊ฐ€ ๋” ์˜ค๋ž˜ ์œ ์ง€๋˜๋Š” ์žฅ์ ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.


52-84: LGTM!

์ ์ˆ˜ ์ฆ๊ฐ€ ๋ฉ”์„œ๋“œ๋“ค๊ณผ ์กฐํšŒ ๋ฉ”์„œ๋“œ๋“ค์ด ์ผ๊ด€๋˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. getTotalCount์—์„œ null ์ฒ˜๋ฆฌ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/infrastructure/ranking/RankingRedisServiceTest.java (1)

26-32: LGTM!

ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. @BeforeEach์—์„œ ํ‚ค๋ฅผ ์‚ญ์ œํ•˜์—ฌ ๊ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

Comment on lines +85 to +89
// 2. ์กฐํšŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (์บ์‹œ ํžˆํŠธ ์—ฌ๋ถ€์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ)
viewEventPublisher.publish(productId);

// 3. ์ˆœ์œ„ ์กฐํšŒ
Long rank = rankingRedisService.getRankingPosition(LocalDate.now(), productId);
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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# Find RankingRedisService implementation
find . -name "*RankingRedisService*" -type f

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

Length of output: 279


๐Ÿ Script executed:

# Search for getRankingPosition method and surrounding code
rg -n -B 5 -A 15 "getRankingPosition" --type java | head -100

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

Length of output: 10577


๐Ÿ Script executed:

# Look at the ProductFacade.java file around line 85-89
fd "ProductFacade.java" --type f | xargs wc -l

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

Length of output: 174


๐Ÿ Script executed:

# Check ProductFacade structure around line 89 for any surrounding error handling
cat -n apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java | sed -n '75,110p'

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

Length of output: 1600


๐Ÿ Script executed:

# Check if there's any exception handling or try-catch at the method level
cat -n apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java | sed -n '80,111p'

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

Length of output: 1426


๐Ÿ Script executed:

# Search for any error handling patterns in ProductFacade class
rg -n "try|catch|throws|Exception" apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java

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

Length of output: 70


Redis ์žฅ์•  ์‹œ ์—๋Ÿฌ ํ•ธ๋“ค๋ง์ด ์—†์Šต๋‹ˆ๋‹ค.

getRankingPosition ํ˜ธ์ถœ์ด try-catch๋กœ ๊ฐ์‹ธ์ ธ ์žˆ์ง€ ์•Š์œผ๋ฉฐ, Redis ์—ฐ๊ฒฐ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ๊ฐ€ ๊ทธ๋Œ€๋กœ ์ „ํŒŒ๋˜์–ด ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ API ์ „์ฒด๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. rank ๊ฐ’์ด null์ผ ์ˆ˜๋Š” ์žˆ์ง€๋งŒ(์ƒํ’ˆ์ด ์ˆœ์œ„์— ์—†๋Š” ๊ฒฝ์šฐ), Redis ์˜ˆ์™ธ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜์˜ ๋ฐฉ์‹์œผ๋กœ graceful degradation์„ ์ ์šฉํ•ด ์ฃผ์„ธ์š”:

  • getRankingPosition ํ˜ธ์ถœ์„ try-catch๋กœ ๊ฐ์‹ธ๊ณ  ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ null ๋ฐ˜ํ™˜
  • RankingRedisService์—์„œ Redis ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ null ๋ฐ˜ํ™˜
  • Redis ์žฅ์•  ์‹œ์—๋„ ์ƒํ’ˆ ์ •๋ณด๋Š” ์ˆœ์œ„ ์—†์ด ๋ฐ˜ํ™˜๋˜๋„๋ก ๊ตฌํ˜„
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
around lines 85-89, the call to rankingRedisService.getRankingPosition is not
protected against Redis failures which can bubble up and break the whole product
detail API; wrap the getRankingPosition call in a try-catch (or handle inside
RankingRedisService) so that on any Redis/connection exception you log a warning
and set rank to null (graceful degradation) and continue returning the product
without rank instead of letting the exception propagate.

Comment on lines +64 to +69
private LocalDate parseDate(String dateStr) {
if (dateStr == null || dateStr.isBlank()) {
return LocalDate.now();
}
return LocalDate.parse(dateStr, DATE_FORMATTER);
}
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

์ž˜๋ชป๋œ ๋‚ ์งœ ํ˜•์‹์— ๋Œ€ํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ•„์š”

LocalDate.parse()๊ฐ€ ์ž˜๋ชป๋œ ํ˜•์‹์˜ dateStr์— ๋Œ€ํ•ด DateTimeParseException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” ์ด ์˜ˆ์™ธ๊ฐ€ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์•„ 500 ์—๋Ÿฌ๋กœ ์ „ํŒŒ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ ์ˆ˜์ •
 private LocalDate parseDate(String dateStr) {
     if (dateStr == null || dateStr.isBlank()) {
         return LocalDate.now();
     }
-    return LocalDate.parse(dateStr, DATE_FORMATTER);
+    try {
+        return LocalDate.parse(dateStr, DATE_FORMATTER);
+    } catch (DateTimeParseException e) {
+        throw new IllegalArgumentException("์ž˜๋ชป๋œ ๋‚ ์งœ ํ˜•์‹์ž…๋‹ˆ๋‹ค. yyyyMMdd ํ˜•์‹์„ ์‚ฌ์šฉํ•ด์ฃผ์„ธ์š”: " + dateStr, e);
+    }
 }
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
around lines 64 to 69, LocalDate.parse(dateStr, DATE_FORMATTER) can throw
DateTimeParseException for invalid formats; wrap the parse call in a try/catch
that catches DateTimeParseException, logs the invalid input with context, and
returns a safe default (e.g., LocalDate.now()) or rethrows a controlled
exception (e.g., IllegalArgumentException or a custom BadRequest) depending on
API semantics; ensure you do not let the raw DateTimeParseException propagate
and cause a 500.

Comment on lines 5 to 17
public class ProductViewedEvent {

private final Long productId;
private final LocalDateTime occurredAt;

private ProductViewedEvent(Long productId) {
this.productId = productId;
this.occurredAt = LocalDateTime.now();
}

public static ProductViewedEvent of(Long productId) {
return new ProductViewedEvent(productId);
}
Copy link

@coderabbitai coderabbitai bot Dec 26, 2025

Choose a reason for hiding this comment

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

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

Kafka ์—ญ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ ๊ธฐ๋ณธ ์ƒ์„ฑ์ž ๋ˆ„๋ฝ

์ด ํด๋ž˜์Šค๋Š” Kafka๋ฅผ ํ†ตํ•ด ๋ฐœํ–‰๋˜๊ณ  ViewLogConsumer์—์„œ ์†Œ๋น„๋ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์ด๋ฒคํŠธ ํด๋ž˜์Šค๋“ค(ProductLikedEvent, StockDepletedEvent, OrderCompletedEvent)๊ณผ ๋‹ฌ๋ฆฌ final ํ•„๋“œ์™€ ๊ธฐ๋ณธ ์ƒ์„ฑ์ž ๋ถ€์žฌ๋กœ ์ธํ•ด Jackson ์—ญ์ง๋ ฌํ™”๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์—ญ์ง๋ ฌํ™” ํ˜ธํ™˜์„ฑ ์ˆ˜์ •
 public class ProductViewedEvent {
 
-    private final Long productId;
-    private final LocalDateTime occurredAt;
+    private Long productId;
+    private LocalDateTime occurredAt;
+
+    protected ProductViewedEvent() {
+    }
 
     private ProductViewedEvent(Long productId) {
         this.productId = productId;
         this.occurredAt = LocalDateTime.now();
     }
๐Ÿ“ 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 class ProductViewedEvent {
private final Long productId;
private final LocalDateTime occurredAt;
private ProductViewedEvent(Long productId) {
this.productId = productId;
this.occurredAt = LocalDateTime.now();
}
public static ProductViewedEvent of(Long productId) {
return new ProductViewedEvent(productId);
}
public class ProductViewedEvent {
private Long productId;
private LocalDateTime occurredAt;
protected ProductViewedEvent() {
}
private ProductViewedEvent(Long productId) {
this.productId = productId;
this.occurredAt = LocalDateTime.now();
}
public static ProductViewedEvent of(Long productId) {
return new ProductViewedEvent(productId);
}
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductViewedEvent.java
around lines 5 to 17, Jackson cannot deserialize this event for Kafka because
the class uses final fields and lacks a no-arg constructor; add a protected
no-argument constructor and make the fields non-final (or provide appropriate
Jackson annotations) so the deserializer can instantiate and populate the
object. Specifically, remove the final modifier from productId and occurredAt
and add a protected ProductViewedEvent() { } that leaves fields uninitialized
(or sets defaults), keeping the static factory and existing getters intact.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

์ˆ˜์ •ํ–ˆ๋Š”๋ฐ.. ๋‹ค๋ฅธ Critical ๋ฌธ์ œ ์žˆ์„๊นŒ?

Copy link

Choose a reason for hiding this comment

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Check for other event classes that might have serialization issues
echo "=== Checking all event classes for no-arg constructors ==="
fd -e java -p "Event\.java$" --exec sh -c '
  file="$1"
  if grep -q "class.*Event" "$file"; then
    echo "File: $file"
    # Check if it has a no-arg constructor
    if grep -q "protected.*Event\s*(\s*)" "$file" || grep -q "public.*Event\s*(\s*)" "$file"; then
      echo "  โœ“ Has no-arg constructor"
    else
      echo "  โœ— Missing no-arg constructor"
    fi
    # Check for final fields
    if grep -q "private final" "$file"; then
      echo "  โš  Has final fields:"
      grep "private final" "$file" | head -3
    fi
    echo ""
  fi
' _ {}

echo -e "\n=== Checking Lua script for ZINCRBY_WITH_EXPIRE ==="
rg -A 20 "ZINCRBY_WITH_EXPIRE" --type java

echo -e "\n=== Checking consumer acknowledgment patterns ==="
rg -B 2 -A 5 "ack\.acknowledge\(\)" --type java -g "*Consumer.java"

Length of output: 13193


๐Ÿ Script executed:

#!/bin/bash
# Check which events are mapped in OutboxEventHandler
echo "=== Checking OutboxEventHandler event mappings ==="
rg -B 3 -A 10 "resolveMetadata|getTopicName" --type java -g "*OutboxEventHandler*"

echo -e "\n=== Checking for Payment/Coupon event consumers ==="
rg "PaymentFailedEvent|CouponUsedEvent|PaymentCompletedEvent" --type java -g "*Consumer.java"

echo -e "\n=== Checking which events are in Outbox publishing flow ==="
ast-grep --pattern 'if ($EVENT instanceof $TYPE) {
  $$$
}'

Length of output: 5191


๐Ÿ Script executed:

#!/bin/bash
# Get the full OutboxEventHandler implementation
echo "=== Full OutboxEventHandler implementation ==="
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java

echo -e "\n=== Check if Payment/Coupon events are domain events ==="
fd -e java -p "Event\.java$" --exec basename {} | sort | uniq

Length of output: 2486


@jeonga1022 ๋„ค, ์ „์ฒด ์ฝ”๋“œ๋ฅผ ๊ฒ€ํ† ํ•œ ๊ฒฐ๊ณผ ๋‹ค๋ฅธ Critical ๋ฌธ์ œ๋Š” ์—†์Šต๋‹ˆ๋‹ค! โœ…

๊ฒ€ํ†  ๊ฒฐ๊ณผ

โœ… ํ•ด๊ฒฐ๋œ ๋ฌธ์ œ

  • ProductViewedEvent: ๊ธฐ๋ณธ ์ƒ์„ฑ์ž ์ถ”๊ฐ€๋กœ Kafka ์—ญ์ง๋ ฌํ™” ๋ฌธ์ œ ํ•ด๊ฒฐ

โœ… ์ •์ƒ ๋™์ž‘ ํ™•์ธ๋œ ๋ถ€๋ถ„

  1. Kafka ์ด๋ฒคํŠธ ์ง๋ ฌํ™”

    • ProductLikedEvent โœ“ (product-liked ํ† ํ”ฝ)
    • OrderCompletedEvent โœ“ (order-events ํ† ํ”ฝ)
    • StockDepletedEvent โœ“ (stock-depleted ํ† ํ”ฝ)
    • ๋ชจ๋‘ protected ๊ธฐ๋ณธ ์ƒ์„ฑ์ž์™€ non-final ํ•„๋“œ ๋ณด์œ 
  2. Consumer ์—๋Ÿฌ ํ•ธ๋“ค๋ง

    • ๋ชจ๋“  Consumer๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋จ
    • ์„ฑ๊ณต ์‹œ: ack.acknowledge() ํ˜ธ์ถœ
    • ์‹คํŒจ ์‹œ: ack ์•ˆ ํ•˜๊ณ  ๋กœ๊ทธ๋งŒ ๋‚จ๊ฒจ ์žฌ์ฒ˜๋ฆฌ ์œ ๋„
  3. Lua ์Šคํฌ๋ฆฝํŠธ

    • ZINCRBY + EXPIRE๊ฐ€ ์›์ž์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด ์•ˆ์ „ํ•จ
    • EXPIRE๋ฅผ ๋งค๋ฒˆ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์€ PR์—์„œ ์–ธ๊ธ‰ํ•œ ์„ค๊ณ„ ์„ ํƒ ์‚ฌํ•ญ์ด์ง€ ๋ฒ„๊ทธ ์•„๋‹˜
  4. ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ

    • EventHandledRepository๋กœ ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€ ์ •์ƒ ์ž‘๋™

โ„น๏ธ ์ฐธ๊ณ  ์‚ฌํ•ญ

PaymentFailedEvent, CouponUsedEvent, PaymentCompletedEvent๊ฐ€ ๊ธฐ๋ณธ ์ƒ์„ฑ์ž ์—†์ด final ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์ด๋“ค์€ OutboxEventHandler์— ๋“ฑ๋ก๋˜์ง€ ์•Š์•„ Kafka๋กœ ๋ฐœํ–‰๋˜์ง€ ์•Š๋Š” ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ์ด๋ฏ€๋กœ ์ˆ˜์ • ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์ „์ฒด์ ์œผ๋กœ ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰


๐Ÿง  Learnings used
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.

Comment on lines +18 to +26
public void publish(Long productId) {
try {
ProductViewedEvent event = ProductViewedEvent.of(productId);
kafkaTemplate.send(TOPIC, String.valueOf(productId), event);
log.debug("์กฐํšŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰: productId={}", productId);
} catch (Exception e) {
log.warn("์กฐํšŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์‹คํŒจ: productId={}, error={}", productId, e.getMessage());
}
}
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

ProductViewedEvent ์—ญ์ง๋ ฌํ™” ํ˜ธํ™˜์„ฑ ํ™•์ธ ํ•„์š”

ProductViewedEvent๋Š” final ํ•„๋“œ์™€ ๊ธฐ๋ณธ ์ƒ์„ฑ์ž๊ฐ€ ์—†์–ด Kafka consumer์—์„œ ์—ญ์ง๋ ฌํ™” ์‹œ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์ด๋ฒคํŠธ ํด๋ž˜์Šค(ProductLikedEvent, StockDepletedEvent)๋Š” ์ด๋ฏธ mutable ํ•„๋“œ์™€ protected ๊ธฐ๋ณธ ์ƒ์„ฑ์ž๋ฅผ ๊ฐ–๋„๋ก ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ProductViewedEvent.java ์ˆ˜์ • ์ œ์•ˆ
 public class ProductViewedEvent {
 
-    private final Long productId;
-    private final LocalDateTime occurredAt;
+    private Long productId;
+    private LocalDateTime occurredAt;
+
+    protected ProductViewedEvent() {
+    }
 
     private ProductViewedEvent(Long productId) {

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

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/event/ViewEventPublisher.java
around lines 18 to 26, the ProductViewedEvent being published likely fails
consumer-side deserialization because its fields are final and it lacks a no-arg
constructor; update ProductViewedEvent to match the other events by removing
final from fields (make them mutable), add a protected no-argument constructor,
and provide public getters (and setters if used by your deserializer) while
keeping the static factory method (of) for creation so existing publishers stay
unchanged.

@jeonga1022
Copy link
Collaborator Author

@coderabbitai

๐Ÿ“ˆ Ranking Consumer

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

โšพ Ranking API

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

ํ•ด๋‹น๋ถ€๋ถ„ ๋‹ค ๊ตฌํ˜„๋˜์—ˆ๋Š”์ง€๋„ ํ™•์ธ ๋ถ€ํƒํ• ๊ฒŒ์š”!

@coderabbitai
Copy link

coderabbitai bot commented Dec 26, 2025

Tip

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Check RankingRedisService for TTL, key strategy, and date-based key calculation
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRedisService.java

Length of output: 3558


๐Ÿ Script executed:

#!/bin/bash
# Check consumers for score updates after events
echo "=== ViewLogConsumer ==="
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ViewLogConsumer.java
echo -e "\n=== ProductLikedConsumer ==="
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/ProductLikedConsumer.java
echo -e "\n=== OrderCompletedConsumer ==="
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/OrderCompletedConsumer.java

Length of output: 7097


๐Ÿ Script executed:

#!/bin/bash
# Check RankingFacade and RankingController for API implementation
echo "=== RankingFacade ==="
cat apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
echo -e "\n=== RankingController ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingController.java
echo -e "\n=== RankingDto ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingDto.java

Length of output: 3983


๐Ÿ Script executed:

#!/bin/bash
# Check ProductFacade for rank field in product detail response
cat apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java | grep -A 30 "public ProductDto.ProductDetailResponse getProduct"

Length of output: 1106


๐Ÿ Script executed:

#!/bin/bash
# Check ProductDto for rank field in ProductDetailResponse
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.java | grep -A 20 "record ProductDetailResponse"

Length of output: 763


@jeonga1022 ๋„ค, ์ฒดํฌ๋ฆฌ์ŠคํŠธ์˜ ๋ชจ๋“  ํ•ญ๋ชฉ์ด ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ํ™•์ธ ๊ฒฐ๊ณผ๋ฅผ ๊ณต์œ ๋“œ๋ฆฝ๋‹ˆ๋‹ค:

๐Ÿ“ˆ Ranking Consumer

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

RankingRedisService์—์„œ ํ™•์ธ:

  • ํ‚ค ์ „๋žต: ranking:all:yyyyMMdd ํ˜•์‹ (์˜ˆ: ranking:all:20231215)
  • TTL: 172800์ดˆ (2์ผ)
  • ๊ตฌํ˜„ ๋ฐฉ์‹: Lua ์Šคํฌ๋ฆฝํŠธ๋กœ ZINCRBY์™€ EXPIRE๋ฅผ ์›์ž์ ์œผ๋กœ ์‹คํ–‰
private static final String KEY_PREFIX = "ranking:all:";
private static final long TTL_SECONDS = 172800; // 2์ผ
private static final String ZINCRBY_WITH_EXPIRE_SCRIPT = """
    redis.call('ZINCRBY', KEYS[1], ARGV[1], ARGV[2])
    redis.call('EXPIRE', KEYS[1], ARGV[3])
    return 1
    """;

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

generateKey(LocalDate date) ๋ฉ”์„œ๋“œ๋กœ ๊ตฌํ˜„:

private String generateKey(LocalDate date) {
    return KEY_PREFIX + date.format(DATE_FORMATTER);
}

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

์„ธ ๊ฐ€์ง€ ์ด๋ฒคํŠธ Consumer๊ฐ€ ๊ฐ๊ฐ ์˜ฌ๋ฐ”๋ฅธ ์ ์ˆ˜๋กœ ๋žญํ‚น์„ ์—…๋ฐ์ดํŠธ:

  • ViewLogConsumer: ์กฐํšŒ ์ด๋ฒคํŠธ โ†’ +0.1์ /ํšŒ
  • ProductLikedConsumer: ์ข‹์•„์š” ์ด๋ฒคํŠธ โ†’ +0.2์  (liked=true์ผ ๋•Œ๋งŒ)
  • OrderCompletedConsumer: ์ฃผ๋ฌธ ์™„๋ฃŒ ์ด๋ฒคํŠธ โ†’ +0.6์  ร— ์ˆ˜๋Ÿ‰

โšพ Ranking API

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

RankingController์˜ /api/v1/rankings ์—”๋“œํฌ์ธํŠธ ๊ตฌํ˜„ ํ™•์ธ:

  • date, page, size ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ง€์›
  • RankingFacade.getRankings() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ ์กฐํšŒ

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

RankingFacade์—์„œ ์ƒํ’ˆ ์ •๋ณด ์กฐ์ธ ํ™•์ธ:

// Redis์—์„œ (productId, score) ์กฐํšŒ
List<RankingEntry> entries = rankingRedisService.getTopProducts(date, offset, size);

// ProductRepository๋กœ ์ƒํ’ˆ ์ •๋ณด ์กฐํšŒ
Map<Long, Product> productMap = productRepository.findAllByIdIn(productIds)...

// RankingResponse์— ํ’๋ถ€ํ•œ ์ •๋ณด ํฌํ•จ
new RankingDto.RankingResponse(
    rank,           // ์ˆœ์œ„
    product.getId(),
    product.getName(),
    product.getPrice(),
    entry.score()   // ๋žญํ‚น ์ ์ˆ˜
)

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

ProductFacade.getProduct()์—์„œ rank ํ•„๋“œ ์ถ”๊ฐ€ ํ™•์ธ:

// ์ˆœ์œ„ ์กฐํšŒ
Long rank = rankingRedisService.getRankingPosition(LocalDate.now(), productId);

// ProductDetailResponse์— rank ํ•„๋“œ ํฌํ•จ (์ˆœ์œ„ ์—†์œผ๋ฉด null)
ProductDto.ProductDetailResponse.from(productId, cache, rank);

getRankingPosition() ๋ฉ”์„œ๋“œ๋Š” ์ˆœ์œ„๊ฐ€ ์—†์„ ๋•Œ null์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


๋ชจ๋“  ์š”๊ตฌ์‚ฌํ•ญ์ด ์™„๋ฒฝํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰

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