Skip to content

Conversation

@HongChangMo
Copy link
Collaborator

@HongChangMo HongChangMo commented Dec 26, 2025

๐Ÿ“Œ Summary

  • Redis ZSET ๊ธฐ๋ฐ˜ ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌํ˜„
  • Kafka Batch ๋ฆฌ์Šค๋„ˆ ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ณ€๊ฒฝ
  • ์ƒํ’ˆ ์ด๋ฒคํŠธ ๋ˆ„๊ณ„ -> DB ์ €์žฅ -> ์Šค์ผ€์ค„๋Ÿฌ ๊ธฐ๋ฐ˜ ๋žญํ‚น ์ ์žฌ ํŒŒ์ดํ”„๋ผ์ธ ์ ์šฉ

๐Ÿ’ฌ Review Points

1. ์ผ์ž๋ณ„ ์ „์ฒด ์บ์‹œ ์ดˆ๊ธฐํ™” ๊ธฐ๋Šฅ

๋ฉ˜ํ† ๋ง๋•Œ ๋ฉ˜ํ† ๋‹˜๊ป˜์„œ ์กฐ์–ธํ•ด์ฃผ์‹ ๋Œ€๋กœ Scheduler๋ฅผ ํ†ตํ•ด Score๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.
๊ถ๊ธˆํ•œ ์ ์€, ์ „์ฒด score์— ๋Œ€ํ•œ sync๋ฅผ ๋งž์ถ”๊ธฐ ์œ„ํ•œ "์ผ์ž๋ณ„ ์ „์ฒด ์บ์‹œ ์ดˆ๊ธฐํ™”" ๊ธฐ๋Šฅ ๊ฐ™์€๊ฒŒ ์žˆ์œผ๋ฉด ์ข‹์€์ง€ ๊ถ๊ธˆํ•˜์—ฌ ์งˆ๋ฌธ๋“œ๋ฆฝ๋‹ˆ๋‹ค.
ํ˜„์žฌ๋Š” ProductMetricsDaily(์ผ์ž๋ณ„ ์ƒํ’ˆ ์ง‘๊ณ„ ์ •๋ณด) ํ…Œ์ด๋ธ”์—์„œ ์ผ์ž๋ณ„ ์ฆ๋ถ„ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๋žญํ‚น ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋„๋ก ํ•˜์˜€๋Š”๋ฐ,
๋งŒ์•ฝ์— "์ผ์ž๋ณ„ ์ „์ฒด ์บ์‹œ ์ดˆ๊ธฐํ™”" ๊ฐ™์€ ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค๋ฉด, ProductMetrics(์ƒํ’ˆ ์ง‘๊ณ„ ์ •๋ณด) ํ…Œ์ด๋ธ”์—์„œ ํ•ด๋‹น ์ƒํ’ˆ์— ๋Œ€ํ•œ ์ง‘๊ณ„(์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ๋„๋Š” ๋‚ ์งœ ์ง‘๊ณ„)๋ฅผ ๊ฐ€์ง€๊ณ  ์™€์„œ
์บ์‹œ๋ฅผ ์ „์ฒด ์ดˆ๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด ์žˆ์œผ๋ฉด ์–ด๋–จ๊นŒ ๊ถ๊ธˆํ•˜์—ฌ ์งˆ๋ฌธ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

2. ๋žญํ‚น ์‹œ์Šคํ…œ ํ๋ฆ„

๋ฉ˜ํ† ๋ง๋•Œ ๋ง์”€ ์ฃผ์‹  ๋‚ด์šฉ์„ ์ฐธ๊ณ ํ•ด์„œ ๋žญํ‚น ์‹œ์Šคํ…œ์˜ ํ๋ฆ„์„ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

[commerce-api]
    โ†“ Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ (์ข‹์•„์š”, ์กฐํšŒ, ์ฃผ๋ฌธ ๋“ฑ)
[Kafka Commit Log]
    โ†“
[commerce-collector]
    โ†“ Kafka ์ด๋ฒคํŠธ Consume
    โ†“ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ฐ ๋ˆ„๊ณ„
    โ†“ DB ์ €์žฅ (ProductMetricsDaily)
[Ranking Scheduler]
    โ†“ ProductMetricsDaily ๋ˆ„๊ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ
    โ†“ Score ๊ณ„์‚ฐ
[Redis ZSET ์ ์žฌ]

๊ถ๊ธˆํ•œ์ ์€ ๋ฉ˜ํ† ๋ง ๋•Œ ๋ง์”€ํ•ด์ฃผ์‹  ๋‚ด์šฉ์ฒ˜๋Ÿผ ๋ˆ„๊ณ„์™€ ๋žญํ‚น ๊ฐฑ์‹ ์„ Scheduler๋ฅผ ํ†ตํ•ด ๋ถ„๋ฆฌํ–ˆ๋Š”๋ฐ, ์ด ๊ตฌ์กฐ์ฒ˜๋Ÿผ ๋ถ„๋ฆฌํ•˜๋Š”๊ฒŒ ๋งž๋Š”์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค.

โœ… Checklist

๐Ÿ“ˆ Ranking Consumer

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

โšพ Ranking API

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

๐Ÿ“Ž References

Summary by CodeRabbit

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

  • ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ

    • ์ƒํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€์— ์ข‹์•„์š”, ์กฐํšŒ, ์ฃผ๋ฌธ ์ˆœ์œ„ ์ •๋ณด ์ถ”๊ฐ€
    • ์ƒํ’ˆ ์ˆœ์œ„ ์กฐํšŒ API ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ (/api/v1/rankings)
    • ์ƒํ’ˆ ์กฐํšŒ ์ด๋ฒคํŠธ ์ถ”์  ๊ธฐ๋Šฅ ๊ฐ•ํ™”
  • ๊ฐœ์„  ์‚ฌํ•ญ

    • ์ฃผ๋ฌธ ์ƒ์„ฑ ์ด๋ฒคํŠธ์— ์ฃผ๋ฌธ ํ•ญ๋ชฉ ์ƒ์„ธ ์ •๋ณด ํฌํ•จ
    • ์ˆœ์œ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋ฐ ์ฒ˜๋ฆฌ ํšจ์œจ ๊ฐœ์„ 

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

- ProductMetricsService๋ฅผ ProductMetricsFacade๋กœ ๋ณ€๊ฒฝ
- EventHandledService๋ฅผ EventHandledFacade๋กœ ๋ณ€๊ฒฝ
- ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ: Kafka ๋ฉ”์‹œ์ง€ ๋‹จ๊ฑด ์ฒ˜๋ฆฌ -> ๋ฐฐ์น˜(Batch) ์ฒ˜๋ฆฌ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ฒ˜๋ฆฌ๋Ÿ‰ ๊ฐœ์„ 
- ์ผ ๋‹จ์œ„ ๋ˆ„๊ณ„ ์ €์žฅ์„ ์œ„ํ•œ ProductMetricsDaily ์—”ํ‹ฐํ‹ฐ ์ ์šฉ
- ๊ธฐ์กด ๋‹จ๊ฑด์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋˜ ์ฃผ๋ฌธ๋ณ„ ์ƒํ’ˆ ๋ชฉ๋ก, List ๋ณ€๊ฒฝ
- ์ƒํ’ˆ ๋‹จ๊ฑด ์ง‘๊ณ„ ์ฒ˜๋ฆฌ์—์„œ ์ƒํ’ˆ๋ณ„ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์ˆ˜์ •
- ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ: Kafka ๋ฉ”์‹œ์ง€ ๋‹จ๊ฑด ์ฒ˜๋ฆฌ -> ๋ฐฐ์น˜(Batch) ์ฒ˜๋ฆฌ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ฒ˜๋ฆฌ๋Ÿ‰ ๊ฐœ์„ 
- ์ผ์ผ ์ƒํ’ˆ ์ฃผ๋ฌธ ๋ˆ„๊ณ„ ๋ฐ์ดํ„ฐ ์ ์žฌ
- ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ: Kafka ๋ฉ”์‹œ์ง€ ๋‹จ๊ฑด ์ฒ˜๋ฆฌ -> ๋ฐฐ์น˜(Batch) ์ฒ˜๋ฆฌ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ฒ˜๋ฆฌ๋Ÿ‰ ๊ฐœ์„ 
- ์ผ์ผ ์ƒํ’ˆ ์กฐํšŒ ๋ˆ„๊ณ„ ๋ฐ์ดํ„ฐ ์ ์žฌ
- ์ƒํ’ˆ ๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ๋กœ ์ ์žฌ๋œ(ProductMetricsDaily) ๊ธฐ๋ฐ˜ ๋žญํ‚น ๊ฐฑ์‹  ์Šค์ผ€์ค„๋Ÿฌ ๊ตฌํ˜„
- ๋žญํ‚น ์กฐํšŒ API(TOP-N, PAGE, ์ „์ฒด) ๊ตฌํ˜„
- Redis ZSET ๊ธฐ๋ฐ˜ ์กฐํšŒ
- ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ๋žญํ‚น ์ •๋ณด ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
@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

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

Changes

Cohort / File(s) ๋ณ€๊ฒฝ ์š”์•ฝ
์ฃผ๋ฌธ ์ด๋ฒคํŠธ ํŽ˜์ด๋กœ๋“œ ํ™•์žฅ
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java, apps/commerce-api/src/main/java/com/loopers/domain/order/event/OrderCreatedEvent.java
OrderCreatedEvent์— ์ƒˆ๋กœ์šด OrderItem ์ค‘์ฒฉ ๋ ˆ์ฝ”๋“œ์™€ items ํ•„๋“œ ์ถ”๊ฐ€. OrderFacade์—์„œ ์ €์žฅ๋œ ์ฃผ๋ฌธ ํ•ญ๋ชฉ์œผ๋กœ๋ถ€ํ„ฐ OrderItem ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ตฌ์„ฑํ•˜์—ฌ ์ด๋ฒคํŠธ ํŽ˜์ด๋กœ๋“œ์— ์ „๋‹ฌ. KafkaTopics ์ฐธ์กฐ ์ •์ •.
์ˆœ์œ„ ์‹œ์Šคํ…œ - ๋„๋ฉ”์ธ ๊ณ„์ธต
apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java, apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingType.java, apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RedisRankingRepository.java
Ranking ๋„๋ฉ”์ธ ๋ชจ๋ธ(rank, productId, score) ๋ฐ RankingType ์—ด๊ฑฐํ˜•(LIKE, VIEW, ORDER, ALL) ๋„์ž…. RankingService๊ฐ€ RedisRankingRepository์— ์œ„์ž„ํ•˜๊ณ  ์ž…๋ ฅ ๊ฒ€์ฆ ์ˆ˜ํ–‰. Redis ZSET ๊ธฐ๋ฐ˜ ์ €์žฅ์†Œ๋กœ ์ƒ์œ„ ์ˆœ์œ„, ํŽ˜์ด์ง€๋ณ„ ์ˆœ์œ„, ๊ฐœ๋ณ„ ์ƒํ’ˆ ์ˆœ์œ„, ์ˆœ์œ„ ํฌ๊ธฐ ์กฐํšŒ ์ง€์›.
์ˆœ์œ„ ์‹œ์Šคํ…œ - ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณ„์ธต
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/SimpleRankingInfo.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
RankingInfo ๋ฐ nested ProductRankings ๋ ˆ์ฝ”๋“œ๋กœ ์ˆœ์œ„ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๋ง. SimpleRankingInfo๋Š” nullable rank/score ์Œ ํ‘œํ˜„. RankingFacade๋Š” ์ƒ์œ„ ์ˆœ์œ„, ํŽ˜์ด์ง€๋ณ„ ์ˆœ์œ„, ๊ฐœ๋ณ„ ์ƒํ’ˆ ์ˆœ์œ„, ๋ชจ๋“  ์ˆœ์œ„ ์œ ํ˜• ์กฐํšŒ ๋ฐ ์ƒํ’ˆ ์ •๋ณด๋กœ ๋ฆฌ์นญ ์ œ๊ณต.
์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด ๋ฐ ์ˆœ์œ„ ํ†ตํ•ฉ
apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java, apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java, apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
ProductDetailInfo์— rankings ํ•„๋“œ ์ถ”๊ฐ€ ๋ฐ of(Product, ProductRankings) ํŒฉํ† ๋ฆฌ ๋ฉ”์†Œ๋“œ ์‹ ์„ค. ProductInfo์— from(Product) ์ •์  ์ƒ์„ฑ์ž ์ถ”๊ฐ€. ProductFacade์—์„œ RankingFacade ์˜์กด์„ฑ ์ถ”๊ฐ€ํ•˜๊ณ  ์บ์‹œ ๋ˆ„๋ฝ ์‹œ ์ˆœ์œ„ ๋ฐ์ดํ„ฐ๋กœ ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด ๊ตฌ์„ฑ.
์ƒํ’ˆ ๋ทฐ ์ด๋ฒคํŠธ ๋ฐœํ–‰
apps/commerce-api/src/main/java/com/loopers/application/product/ProductEventPublisher.java, apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java
ProductEventPublisher ์‹ ์„ค(ProductViewedEvent ๋ฐœํ–‰), OutboxEventPublisher์—์„œ PRODUCT_VIEW ์ง‘๊ณ„ ํƒ€์ž… ์ฒ˜๋ฆฌ ์ถ”๊ฐ€. ์ด์ „ ์ธ๋ผ์ธ ๋ฐœํ–‰ ๋กœ์ง ์ œ๊ฑฐ.
์ƒํ’ˆ ์ €์žฅ์†Œ ํ™•์žฅ
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java, apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
์ผ๊ด„ ์กฐํšŒ๋ฅผ ์œ„ํ•œ findAllByIdIn(List) ๋ฉ”์†Œ๋“œ๋ฅผ ์ €์žฅ์†Œ ๊ณ„์ธต ์ „๋ฐ˜์— ์ถ”๊ฐ€.
์ˆœ์œ„ API ์—”๋“œํฌ์ธํŠธ
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java
/api/v1/rankings ๊ฒฝ๋กœ์— ํŽ˜์ด์ง€๋ณ„, ์ƒ์œ„, ๊ฐœ๋ณ„ ์ƒํ’ˆ ์ˆœ์œ„ ์กฐํšŒ ์—”๋“œํฌ์ธํŠธ ์‹ ์„ค. ์š”์ฒญ/์‘๋‹ต DTO ์ •์˜ ๋ฐ Date ํŒŒ์‹ฑ ๋กœ์ง ํ†ตํ•ฉ.
์ƒํ’ˆ DTO ์ˆœ์œ„ ํ•„๋“œ ์ถ”๊ฐ€
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1DTO.java
ProductDetailResponse์— RankingResponse ํ•„๋“œ ์ถ”๊ฐ€ ๋ฐ nested SimpleRankingResponse ๋ ˆ์ฝ”๋“œ ์ •์˜. RankingInfo.ProductRankings์—์„œ DTO๋กœ ๋ณ€ํ™˜ ๋กœ์ง ๊ตฌํ˜„.
์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํŒŒ์‚ฌ๋“œํ™” - Collector
apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledFacade.java, apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledInfo.java, apps/commerce-collector/src/main/java/com/loopers/domain/eventhandled/EventHandledService.java (์‚ญ์ œ), apps/commerce-collector/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledRepositoryImpl.java
EventHandledService ์ œ๊ฑฐ, EventHandledFacade ์‹ ์„ค(isAlreadyHandled, markAsHandled, markAsHandledBatch). ๋ฐฐ์น˜ ์ €์žฅ ์ง€์› ์ถ”๊ฐ€.
๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ - ์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ ๋ฐ ์ €์žฅ์†Œ
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDaily.java, apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDailyRepository.java, apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyJpaRepository.java, apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java
ProductMetricsDaily JPA ์—”ํ‹ฐํ‹ฐ(productId, metricDate, ๋ธํƒ€, ์ฒ˜๋ฆฌ ์ƒํƒœ) ๋ฐ ์ €์žฅ์†Œ ์‹ ์„ค. ์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ, ์ €์žฅ, upsert, ์ •๋ฆฌ ๊ธฐ๋Šฅ ์ œ๊ณต.
๋ฉ”ํŠธ๋ฆญ ํŒŒ์‚ฌ๋“œ ๋ฆฌํŒฉํ† ๋ง
apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java, apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsDailyFacade.java, apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java
ProductMetricsService๋ฅผ ProductMetricsFacade๋กœ ์ด๋™ ๋ฐ ๋ฐฐ์น˜ upsert ๋ฉ”์†Œ๋“œ(updateLikeCountBatch, updateViewCountBatch, updateOrderCountBatch) ์ถ”๊ฐ€. ProductMetricsDailyFacade ์‹ ์„ค๋กœ ์ผ์ผ ๋ธํƒ€ ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ ์œ„์ž„. JDBC ๋ฐฐ์น˜ ์ž‘์—… ์ง€์›.
๋ฐฐ์น˜ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ - ์ƒํ’ˆ ๋ทฐ
apps/commerce-collector/src/main/java/com/loopers/application/product/ProductBatchEventHandler.java, apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductBatchEventConsumer.java, apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductEventConsumer.java, apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/dto/ProductEvent.java
ProductBatchEventHandler ์‹ ์„ค๋กœ ๋ฐฐ์น˜ ProductEvent ์ฒ˜๋ฆฌ(ํ•„ํ„ฐ๋ง, ์ค‘๋ณต ์ œ๊ฑฐ, ๋ธํƒ€ ์ง‘๊ณ„, ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ, ์ด๋ฒคํŠธ ํ‘œ์‹œ). ProductBatchEventConsumer๋Š” Kafka ๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ. ProductEventConsumer๋ฅผ consumer/product ํŒจํ‚ค์ง€๋กœ ์ด๋™ ๋ฐ ๋น„ํ™œ์„ฑํ™”. ProductEvent DTO ์ถ”๊ฐ€.
๋ฐฐ์น˜ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ - ์ƒํ’ˆ ์ข‹์•„์š”
apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeBatchEventHandler.java, apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeBatchEventConsumer.java, apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeEventConsumer.java, apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/dto/ProductLikeEvent.java
ProductLikeBatchEventHandler ์‹ ์„ค๋กœ ๋ฐฐ์น˜ ProductLikeEvent ์ฒ˜๋ฆฌ(์œ ํ˜•๋ณ„ ๋ธํƒ€, ์ด๋ฒคํŠธ ํ‘œ์‹œ). ProductLikeBatchEventConsumer๋Š” Kafka ๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ. ProductLikeEventConsumer๋ฅผ consumer/like ํŒจํ‚ค์ง€๋กœ ์ด๋™ ๋ฐ ๋น„ํ™œ์„ฑํ™”. ProductLikeEvent DTO ์ถ”๊ฐ€.
๋ฐฐ์น˜ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ - ์ฃผ๋ฌธ
apps/commerce-collector/src/main/java/com/loopers/application/order/OrderBatchEventHandler.java, apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderBatchEventConsumer.java, apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderEventConsumer.java, apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/dto/OrderEvent.java
OrderBatchEventHandler ์‹ ์„ค๋กœ ๋ฐฐ์น˜ OrderEvent ์ฒ˜๋ฆฌ(ORDER_CREATED ํ•„ํ„ฐ๋ง, ์ˆ˜๋Ÿ‰ ์ง‘๊ณ„, ์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ). OrderBatchEventConsumer๋Š” Kafka ๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ. OrderEventConsumer ๋ฆฌํŒฉํ† ๋ง(DTO ๊ธฐ๋ฐ˜ ํŒŒ์‹ฑ, handleOrderCreatedEvent ๋‚ด๋ถ€ ํ•ธ๋“ค๋Ÿฌ). OrderEvent/OrderCreatedPayload/OrderItem DTO ๊ตฌ์กฐ ์ถ”๊ฐ€.
์ˆœ์œ„ ์—…๋ฐ์ดํŠธ ์Šค์ผ€์ค„๋ง
apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingFacade.java, apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingScheduler.java, apps/commerce-collector/src/main/java/com/loopers/CommerceCollectorApplication.java
RankingFacade(Redis ZSET ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ, TTL/๊ฐ€์ค‘์น˜ ์„ค์ •) ์‹ ์„ค. RankingScheduler ์‹ ์„ค๋กœ 5๋ถ„๋งˆ๋‹ค ๋ฏธ์ฒ˜๋ฆฌ ์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ์ฒ˜๋ฆฌ, ๋ณตํ•ฉ ์ˆœ์œ„ ๊ณ„์‚ฐ, 10์ผ ์ด์ƒ ๋œ ๊ธฐ๋ก ์ •๋ฆฌ ์Šค์ผ€์ค„. @EnableScheduling ์ถ”๊ฐ€.
์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ - ์„œ๋น„์Šค์—์„œ ํŒŒ์‚ฌ๋“œ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeEventHandler.java, apps/commerce-collector/src/main/java/com/loopers/application/order/OrderEventHandler.java, apps/commerce-collector/src/main/java/com/loopers/application/product/ProductEventHandler.java
EventHandledService โ†’ EventHandledFacade, ProductMetricsService โ†’ ProductMetricsFacade ์˜์กด์„ฑ ๋ณ€๊ฒฝ. ๋ชจ๋“  ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ์„ ํŒŒ์‚ฌ๋“œ ๊ธฐ๋ฐ˜์œผ๋กœ ์—…๋ฐ์ดํŠธ. ์ œ์–ด ํ๋ฆ„ ์œ ์ง€.
๋ฉ”ํŠธ๋ฆญ ์ €์žฅ์†Œ ํ™•์žฅ
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
upsertLikeDeltas, upsertViewDeltas, upsertOrderDeltas ๋ฐฐ์น˜ ๋ฉ”์†Œ๋“œ ์ถ”๊ฐ€.
์„ค์ • ์—…๋ฐ์ดํŠธ
apps/commerce-collector/src/main/resources/application.yml
Kafka max.poll.records 10โ†’100, fetch-min-size/fetch-max-wait ์ถ”๊ฐ€. ์ˆœ์œ„ ์„ค์ • ๋ธ”๋ก ์‹ ์„ค(ttl-days: 2, ๊ฐ€์ค‘์น˜: like 0.2, view 0.1, order 0.6).

Estimated code review effort

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

Possibly related PRs

Suggested labels

enhancement, feature, backend

Poem

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

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 24.86% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check โ“ Inconclusive PR ์„ค๋ช…์ด ๊ธฐ๋ณธ ์š”๊ตฌ ์‚ฌํ•ญ ์„น์…˜์„ ๋Œ€๋ถ€๋ถ„ ํฌํ•จํ•˜์ง€๋งŒ, ์ผ๋ถ€ ์„น์…˜์ด ๋ถˆ์™„์ „ํ•ฉ๋‹ˆ๋‹ค. Summary์™€ Review Points๋Š” ์ž‘์„ฑ๋˜์—ˆ์œผ๋‚˜, Checklist ํ•ญ๋ชฉ๋“ค์ด ๋Œ€๋ถ€๋ถ„ ๋ฏธ์™„๋ฃŒ([ ] ์ฒดํฌ ์•ˆ๋จ) ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ์™„๋ฃŒ๋œ ํ•ญ๋ชฉ๊ณผ ๋ฏธ์™„๋ฃŒ ํ•ญ๋ชฉ์„ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•˜๊ณ , ํŠนํžˆ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ํฌํ•จ ์—ฌ๋ถ€์™€ ๊ฐ™์€ ์ค‘์š” ํ•ญ๋ชฉ์— ๋Œ€ํ•œ ๊ตฌ์ฒด์ ์ธ ์„ค๋ช…์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.
โœ… Passed checks (1 passed)
Check name Status Explanation
Title check โœ… Passed PR ์ œ๋ชฉ์€ '[volume-9] Product Ranking with Redis'๋กœ Redis๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌํ˜„์ด๋ผ๋Š” ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์š”์•ฝํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (1)
apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeEventConsumer.java (1)

17-89: ๋น„ํ™œ์„ฑํ™”๋œ ์ปจ์Šˆ๋จธ ์ฝ”๋“œ ์ •๋ฆฌ ํ•„์š”

@Component ์–ด๋…ธํ…Œ์ด์…˜์ด ์ฃผ์„ ์ฒ˜๋ฆฌ๋˜์–ด ์ด ์ปจ์Šˆ๋จธ๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์ง€๋งŒ, ์ „์ฒด ๊ตฌํ˜„ ์ฝ”๋“œ(89์ค„)๊ฐ€ ๊ทธ๋Œ€๋กœ ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ์ปจ์Šˆ๋จธ(ProductLikeBatchEventConsumer)๋กœ ๋Œ€์ฒด๋œ ๊ฒƒ์œผ๋กœ ๋ณด์ด๋ฏ€๋กœ, ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”:

  1. ๊ถŒ์žฅ: ์ด ํŒŒ์ผ์„ ์™„์ „ํžˆ ์‚ญ์ œ
  2. ์ฐธ๊ณ ์šฉ์œผ๋กœ ๋ณด์กดํ•˜๋ ค๋Š” ๊ฒฝ์šฐ, ํŒŒ์ผ ์ƒ๋‹จ์— ๋ช…ํ™•ํ•œ ์ฃผ์„ ์ถ”๊ฐ€ (์˜ˆ: "์ด ํด๋ž˜์Šค๋Š” ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋กœ ๋Œ€์ฒด๋˜์—ˆ์œผ๋ฉฐ ์ฐธ๊ณ ์šฉ์œผ๋กœ๋งŒ ๋ณด์กด๋จ")

ํ˜„์žฌ ์ƒํƒœ๋Š” ์ฝ”๋“œ๋ฒ ์ด์Šค์— ํ˜ผ๋ž€์„ ์ฃผ๊ณ  ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ์ €ํ•ดํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ฝ”๋“œ ์ •๋ฆฌ๋ฅผ ์œ„ํ•œ ์ œ์•ˆ

Option 1: ํŒŒ์ผ ์‚ญ์ œ (๊ถŒ์žฅ)

# ์ด ํŒŒ์ผ์„ git์—์„œ ์ œ๊ฑฐ
git rm apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeEventConsumer.java

Option 2: ๋ช…ํ™•ํ•œ ๋ฌธ์„œํ™”
ํŒŒ์ผ์„ ์œ ์ง€ํ•˜๋ ค๋ฉด ์ƒ๋‹จ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฃผ์„์„ ์ถ”๊ฐ€:

 package com.loopers.interfaces.consumer.like;

+/**
+ * @deprecated ์ด ํด๋ž˜์Šค๋Š” ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋ฐฉ์‹(ProductLikeBatchEventConsumer)์œผ๋กœ ๋Œ€์ฒด๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
+ * ์ฐธ๊ณ  ๋ชฉ์ ์œผ๋กœ๋งŒ ๋ณด์กด๋˜๋ฉฐ, ํ”„๋กœ๋•์…˜์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
+ * TODO: ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๊ฐ€ ์•ˆ์ •ํ™”๋˜๋ฉด ์ด ํŒŒ์ผ์„ ์‚ญ์ œํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.
+ */
 // ๋ฉ”์‹œ์ง€ ๋‹จ๊ฑด ์ฒ˜๋ฆฌ ๋น„ํ™œ์„ฑํ™”
 //@Component
๐Ÿงน Nitpick comments (23)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java (1)

8-10: rank ํ•„๋“œ์˜ ํƒ€์ž… ์ผ๊ด€์„ฑ ๊ฒ€ํ†  ํ•„์š”

rank๊ฐ€ primitive int๋กœ ์„ ์–ธ๋˜์–ด ์žˆ์ง€๋งŒ, SimpleRankingInfo์—์„œ๋Š” Integer(nullable)๋กœ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ˆœ์œ„๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ๋ฅผ ํ‘œํ˜„ํ•ด์•ผ ํ•œ๋‹ค๋ฉด Integer๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด ์ผ๊ด€์„ฑ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
 @Getter
 public class Ranking {
-    private final int rank;
+    private final Integer rank;
     private final Long productId;
     private final Double score;

     @Builder
-    public Ranking(int rank, Long productId, Double score) {
+    public Ranking(Integer rank, Long productId, Double score) {
         this.rank = rank;
         this.productId = productId;
         this.score = score;
     }
 }
apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/dto/ProductEvent.java (1)

3-8: ์ด๋ฒคํŠธ DTO ์„ค๊ณ„ ์Šน์ธ

๊น”๋”ํ•œ record ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ DTO์ž…๋‹ˆ๋‹ค.

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

apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledInfo.java (1)

9-12: ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ๋‹จ์ˆœํ™” ๊ณ ๋ ค

of() ๋ฉ”์„œ๋“œ๊ฐ€ ๋‹จ์ˆœํžˆ ์ƒ์„ฑ์ž๋ฅผ ํ˜ธ์ถœ๋งŒ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Record๋Š” ์ด๋ฏธ public ์ •๊ทœ ์ƒ์„ฑ์ž๋ฅผ ์ œ๊ณตํ•˜๋ฏ€๋กœ, ์ด ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋Š” ์ถ”๊ฐ€์ ์ธ ๊ฐ€์น˜๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„๋„ ๋™์ž‘ํ•˜์ง€๋งŒ, ํ˜ธ์ถœ ์ธก์—์„œ new EventHandledInfo(...)๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ์‹ค์ œ๋กœ ๊ฒ€์ฆ์ด๋‚˜ ๋ณ€ํ™˜ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

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

29-37: ์—”๋“œํฌ์ธํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ ๋ฐฉ์‹์˜ ๋ถˆ์ผ์น˜ ๋ฐ date ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ

getTopRanking๊ณผ getRankingWithPaging ์—”๋“œํฌ์ธํŠธ๋Š” ์š”์ฒญ DTO๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ˜๋ฉด, getProductRanking์€ ๊ฐœ๋ณ„ @PathVariable๊ณผ @RequestParam์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด ์ผ๊ด€์„ฑ์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ์˜ date ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ช…์‹œ์ ์ธ @Pattern ๊ฒ€์ฆ ์—†์ด ๋ฌธ์ž์—ด๋กœ ์„ ์–ธ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ๋Š” ์ปจํŠธ๋กค๋Ÿฌ์˜ parseDate() ๋ฉ”์„œ๋“œ์—์„œ yyyyMMdd ํ˜•์‹์œผ๋กœ ๋Ÿฐํƒ€์ž„์— ํŒŒ์‹ฑ๋˜๊ณ  ์žˆ์œผ๋ฉฐ, ํ˜•์‹์ด @Schema ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ ์ž˜๋ชป๋œ ํ˜•์‹์˜ date๊ฐ€ ์ „๋‹ฌ๋˜๋ฉด DateTimeParseException์ด ๋ฐœ์ƒํ•˜๋ฏ€๋กœ, ๋‹ค์Œ ๊ฐœ์„ ์„ ๊ณ ๋ คํ•˜์„ธ์š”:

  1. ์„ค๊ณ„ ์ผ๊ด€์„ฑ: getProductRanking ์—”๋“œํฌ์ธํŠธ๋ฅผ GetProductRankingRequest DTO๋กœ ํ†ต์ผํ•˜๊ฑฐ๋‚˜, ํ˜„์žฌ ์„ค๊ณ„๊ฐ€ ์˜๋„์ ์ธ ๊ฒฝ์šฐ ๊ทธ ์ด์œ ๋ฅผ ๋ฌธ์„œํ™”ํ•ด์ฃผ์„ธ์š”.
  2. date ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ: ๋ชจ๋“  date ํŒŒ๋ผ๋ฏธํ„ฐ์— @Pattern(regexp = "\\d{8}") ๊ฐ™์€ ๋ช…์‹œ์  ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ์„ ์„ ์–ธ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeBatchEventHandler.java (2)

59-63: ๋ฐฐ์น˜ ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ ์ตœ์ ํ™” ๊ณ ๋ ค

ํ˜„์žฌ ๊ฐ ์ด๋ฒคํŠธ๋งˆ๋‹ค ๊ฐœ๋ณ„์ ์œผ๋กœ isAlreadyHandled๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋Œ€๋Ÿ‰์˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์‹œ N๋ฒˆ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. EventHandledFacade์— ๋ฐฐ์น˜ ์ฒดํฌ ๋ฉ”์„œ๋“œ(์˜ˆ: areAlreadyHandled(List<String> eventIds))๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


85-92: ์ด๋ฒคํŠธ ํƒ€์ž… ๋น„๊ต์— ์ƒ์ˆ˜ ์‚ฌ์šฉ ๊ถŒ์žฅ

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

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ๊ฐœ์„ ์•ˆ
 private int calculateDelta(String eventType) {
     if (KafkaTopics.ProductLike.LIKE_ADDED.equals(eventType)) {
         return 1;
     } else if (KafkaTopics.ProductLike.LIKE_REMOVED.equals(eventType)) {
         return -1;
     }
+    log.warn("์•Œ ์ˆ˜ ์—†๋Š” ์ด๋ฒคํŠธ ํƒ€์ž…: {}", eventType);
     return 0;
 }
apps/commerce-api/src/main/java/com/loopers/application/product/ProductEventPublisher.java (1)

32-55: ์ด๋ฒคํŠธ ๋ฐœํ–‰ ํŒจํ„ด์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

REQUIRES_NEW ์ „ํŒŒ ์ „๋žต์œผ๋กœ ๋ถ€๋ชจ ํŠธ๋žœ์žญ์…˜๊ณผ ๋…๋ฆฝ์ ์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰์„ ๋ณด์žฅํ•˜๋ฉฐ, ์‹คํŒจ ์‹œ ์กฐํšŒ ์ž‘์—…์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š” fire-and-forget ํŒจํ„ด์ด ์ ์ ˆํžˆ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์„ ํƒ์  ๊ฐœ์„  ์‚ฌํ•ญ: ๋กœ๊ทธ ๋ ˆ๋ฒจ ์กฐ์ •

Line 45์˜ ์„ฑ๊ณต ๋กœ๊ทธ๊ฐ€ debug ๋ ˆ๋ฒจ๋กœ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ๋ฐœํ–‰์€ ์ค‘์š”ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋™์ž‘์ด๋ฏ€๋กœ ๊ฐ์‚ฌ ์ถ”์ (audit trail)์„ ์œ„ํ•ด info ๋ ˆ๋ฒจ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

-            log.debug("ProductViewedEvent ๋ฐœํ–‰ ์„ฑ๊ณต - productId: {}", productId);
+            log.info("ProductViewedEvent ๋ฐœํ–‰ ์„ฑ๊ณต - productId: {}", productId);
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDaily.java (1)

71-74: ํƒ€์ž„์กด ๋ช…์‹œ ๊ถŒ์žฅ

Line 73์˜ ZonedDateTime.now()๋Š” ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ํƒ€์ž„์กด์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ช…์‹œ์ ์œผ๋กœ ZonedDateTime.now(ZoneId.of("Asia/Seoul"))์„ ์‚ฌ์šฉํ•˜์—ฌ ํƒ€์ž„์กด ๋ถˆ์ผ์น˜๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
 public void markAsProcessed() {
     this.isProcessed = true;
-    this.processedAt = ZonedDateTime.now();
+    this.processedAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
 }

ํ•„์š” ์‹œ ์ƒ์ˆ˜๋กœ ์ถ”์ถœ:

private static final ZoneId KST = ZoneId.of("Asia/Seoul");
apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java (1)

20-45: from(Product)๊ณผ of(Product, RankingInfo.ProductRankings) ๊ฐ„ ์ค‘๋ณต ์ฝ”๋“œ ์ œ๊ฑฐ ๊ถŒ์žฅ

๋‘ ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ๋™์ผํ•œ ํ•„๋“œ ๋งคํ•‘ ๋กœ์ง์„ ๋ฐ˜๋ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. from(Product)์ด of(Product, null)์— ์œ„์ž„ํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ค‘๋ณต ์ œ๊ฑฐ ์ œ์•ˆ
     public static ProductDetailInfo from(Product product) {
-        return new ProductDetailInfo(
-                product.getId(),
-                product.getProductCode(),
-                product.getProductName(),
-                product.getPrice().getAmount(),
-                product.getStockQuantity(),
-                product.getLikeCount(),
-                product.getBrand() != null ? BrandInfo.from(product.getBrand()) : null,
-                null
-        );
+        return of(product, null);
     }
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)

1-14: ๋„๋ฉ”์ธ ๊ณ„์ธต์—์„œ ์ธํ”„๋ผ์ŠคํŠธ๋Ÿญ์ฒ˜ ๊ณ„์ธต ์ง์ ‘ ์˜์กด ๋ฌธ์ œ

RankingService๊ฐ€ ๋„๋ฉ”์ธ ํŒจํ‚ค์ง€(com.loopers.domain.ranking)์— ์œ„์น˜ํ•˜์ง€๋งŒ, ์ธํ”„๋ผ์ŠคํŠธ๋Ÿญ์ฒ˜ ๊ณ„์ธต์˜ RedisRankingRepository๋ฅผ ์ง์ ‘ ์˜์กดํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์˜์กด์„ฑ ์—ญ์ „ ์›์น™(DIP)์„ ์œ„๋ฐ˜ํ•ฉ๋‹ˆ๋‹ค.

๋„๋ฉ”์ธ ๊ณ„์ธต์— RankingRepository ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ•˜๊ณ , RedisRankingRepository๊ฐ€ ์ด๋ฅผ ๊ตฌํ˜„ํ•˜๋„๋ก ํ•˜๊ฑฐ๋‚˜, ์ด ์„œ๋น„์Šค๋ฅผ application ๊ณ„์ธต์œผ๋กœ ์ด๋™ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/dto/OrderEvent.java (1)

31-35: OrderItem ๋ ˆ์ฝ”๋“œ์—๋„ @JsonIgnoreProperties ์ถ”๊ฐ€ ๊ถŒ์žฅ

OrderEvent์™€ OrderCreatedPayload์—๋Š” @JsonIgnoreProperties(ignoreUnknown = true)๊ฐ€ ์ ์šฉ๋˜์–ด ์žˆ์œผ๋‚˜, OrderItem์—๋Š” ๋ˆ„๋ฝ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์Šคํ‚ค๋งˆ ์ง„ํ™” ์‹œ ์ผ๊ด€๋œ ์—ญํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด ์ถ”๊ฐ€๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
+        @JsonIgnoreProperties(ignoreUnknown = true)
         public record OrderItem(
                 Long productId,
                 Integer quantity,
                 BigDecimal price
         ) {}
apps/commerce-collector/src/main/java/com/loopers/application/product/ProductBatchEventHandler.java (1)

57-61: filterUnprocessedEvents์—์„œ N+1 ์ฟผ๋ฆฌ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ

๊ฐ ์ด๋ฒคํŠธ๋งˆ๋‹ค eventHandledFacade.isAlreadyHandled(event.eventId())๋ฅผ ๊ฐœ๋ณ„ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ์–ด, ์ด๋ฒคํŠธ ์ˆ˜๊ฐ€ ๋งŽ์„ ๊ฒฝ์šฐ N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

EventHandledFacade์— isAlreadyHandledBatch(List<String> eventIds) ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ตœ์ ํ™” ์ œ์•ˆ
     private List<ProductEvent> filterUnprocessedEvents(List<ProductEvent> events) {
+        List<String> eventIds = events.stream()
+                .map(ProductEvent::eventId)
+                .collect(Collectors.toList());
+        Set<String> handledEventIds = eventHandledFacade.findHandledEventIds(eventIds);
+        
         return events.stream()
-                .filter(event -> !eventHandledFacade.isAlreadyHandled(event.eventId()))
+                .filter(event -> !handledEventIds.contains(event.eventId()))
                 .collect(Collectors.toList());
     }
apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductBatchEventConsumer.java (1)

37-41: ๋ฏธ์‚ฌ์šฉ ํŒŒ๋ผ๋ฏธํ„ฐ keys ํ™•์ธ

@Header(KafkaHeaders.RECEIVED_KEY) List<String> keys ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์„ ์–ธ๋˜์–ด ์žˆ์ง€๋งŒ ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์˜๋„์ ์ธ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ๋ฉด ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜, ๋กœ๊น…/์ถ”์  ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderBatchEventConsumer.java (1)

66-80: ArrayList import ์‚ฌ์šฉ ๊ถŒ์žฅ

java.util.ArrayList๋ฅผ ์ง์ ‘ ์ฐธ์กฐํ•˜๋Š” ๋Œ€์‹  import ๋ฌธ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ผ๊ด€์„ฑ ์žˆ๋Š” ์ฝ”๋“œ ์Šคํƒ€์ผ์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
 import java.util.List;
+import java.util.ArrayList;

...

 private List<OrderEvent> parseMessages(List<String> messages, List<String> keys) {
-    List<OrderEvent> events = new java.util.ArrayList<>();
+    List<OrderEvent> events = new ArrayList<>();
apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingScheduler.java (1)

39-39: ํƒ€์ž„์กด ๊ณ ๋ ค ํ•„์š”

LocalDate.now()๋Š” ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ํƒ€์ž„์กด์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„ ๋ฐฐํฌ ํ™˜๊ฒฝ์ด ๋‹ค๋ฅธ ํƒ€์ž„์กด์ผ ๊ฒฝ์šฐ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋™์ž‘์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •
-LocalDate today = LocalDate.now();
+LocalDate today = LocalDate.now(ZoneId.of("Asia/Seoul"));

๋˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์—ญ์—์„œ ์‚ฌ์šฉํ•  Clock ๋นˆ์„ ์ฃผ์ž…๋ฐ›์•„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋„ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ๋†’์ด๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

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

88-88: ํƒ€์ž„์กด ์ผ๊ด€์„ฑ ํ™•์ธ ํ•„์š”

LocalDate.now()๊ฐ€ RankingScheduler์™€ ์ด ํด๋ž˜์Šค์—์„œ ๋ชจ๋‘ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋‘ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‹ค๋ฅธ ์‹œ์ ์— ์‹คํ–‰๋  ๋•Œ ํƒ€์ž„์กด ๊ฒฝ๊ณ„์—์„œ ๋‚ ์งœ ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ช…์‹œ์  ํƒ€์ž„์กด ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •
+private static final ZoneId ZONE_ID = ZoneId.of("Asia/Seoul");
+
 private void incrementRanking(String prefix, Map<Long, Integer> deltas, double weight) {
     // ...
-    String todayKey = prefix + ":" + LocalDate.now().format(DATE_FORMATTER);
+    String todayKey = prefix + ":" + LocalDate.now(ZONE_ID).format(DATE_FORMATTER);

Also applies to: 129-129


137-138: ๋ถ€๋™์†Œ์ˆ˜์  ๋น„๊ต ์‹œ ์ฃผ์˜

entry.getValue() == 0.0 ๋น„๊ต๋Š” ๋ถ€๋™์†Œ์ˆ˜์  ์ •๋ฐ€๋„ ๋ฌธ์ œ๋กœ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๊ฒฝ์šฐ ์ ์ˆ˜๊ฐ€ ์ •์ˆ˜ ์—ฐ์‚ฐ์˜ ๊ฒฐ๊ณผ์ด๋ฏ€๋กœ ์‹ค์ œ๋กœ๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋” ์•ˆ์ „ํ•œ ๋น„๊ต ๋ฐฉ์‹
-if (entry.getValue() == 0.0) continue;
+if (Math.abs(entry.getValue()) < 1e-9) continue;
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RedisRankingRepository.java (1)

129-142: NumberFormatException ์ฒ˜๋ฆฌ ํ•„์š”

Long.parseLong(tuple.getValue().toString())์€ Redis์— ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋œ ๊ฒฝ์šฐ NumberFormatException์„ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฉ์–ด์  ์ฒ˜๋ฆฌ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •
 for (ZSetOperations.TypedTuple<Object> tuple : result) {
     if (tuple.getValue() == null) {
         continue;
     }

+    Long productId;
+    try {
+        productId = Long.parseLong(tuple.getValue().toString());
+    } catch (NumberFormatException e) {
+        log.warn("์ž˜๋ชป๋œ productId ํ˜•์‹ - value: {}", tuple.getValue());
+        continue;
+    }
-    Long productId = Long.parseLong(tuple.getValue().toString());
     Double score = tuple.getScore();

     entries.add(Ranking.builder()
             .rank(rank++)
             .productId(productId)
             .score(score)
             .build());
 }
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (3)

91-96: ์ž˜๋ชป๋œ ๋‚ ์งœ ํ˜•์‹ ์ฒ˜๋ฆฌ ํ•„์š”

LocalDate.parse()๋Š” ์ž˜๋ชป๋œ ํ˜•์‹์˜ ๋‚ ์งœ ๋ฌธ์ž์—ด์— ๋Œ€ํ•ด DateTimeParseException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ํ˜„์žฌ ์ „์—ญ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์žˆ๋‹ค๋ฉด ๋ฌธ์ œ์—†์ง€๋งŒ, ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์œ„ํ•ด ๋ช…์‹œ์  ์ฒ˜๋ฆฌ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •
 private LocalDate parseDate(String dateString) {
     if (dateString == null || dateString.isBlank()) {
         return LocalDate.now();
     }
+    try {
         return LocalDate.parse(dateString, DATE_FORMATTER);
+    } catch (DateTimeParseException e) {
+        throw new IllegalArgumentException("์ž˜๋ชป๋œ ๋‚ ์งœ ํ˜•์‹์ž…๋‹ˆ๋‹ค. yyyyMMdd ํ˜•์‹์„ ์‚ฌ์šฉํ•ด์ฃผ์„ธ์š”: " + dateString);
+    }
 }

๊ด€๋ จ ์ฝ”๋“œ ์Šค๋‹ˆํŽซ ApiControllerAdvice.kt์—์„œ ์ผ๋ฐ˜ Throwable์„ INTERNAL_ERROR๋กœ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ, IllegalArgumentException์ด๋‚˜ ์ปค์Šคํ…€ ์˜ˆ์™ธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ ์ ˆํ•œ 400 ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.


82-84: null ์‘๋‹ต ๋Œ€์‹  404 ๋ฐ˜ํ™˜ ๊ณ ๋ ค

ApiResponse.success(null) ๋Œ€์‹  ๋ช…ํ™•ํ•œ 404 ์‘๋‹ต์ด๋‚˜ ๋นˆ ์‘๋‹ต ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์ด API ์‚ฌ์šฉ์ž์—๊ฒŒ ๋” ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์„์—์„œ๋„ "์ ์ ˆํ•œ ์—๋Ÿฌ ์‘๋‹ต"์„ ์–ธ๊ธ‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ • ์˜ต์…˜

์˜ต์…˜ 1: 404 ์‘๋‹ต ๋ฐ˜ํ™˜

 if (ranking == null) {
-    return ApiResponse.success(null);  // ๋˜๋Š” ์ ์ ˆํ•œ ์—๋Ÿฌ ์‘๋‹ต
+    throw new CoreException(ErrorType.NOT_FOUND, "ํ•ด๋‹น ์ƒํ’ˆ์˜ ๋žญํ‚น ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.");
 }

์˜ต์…˜ 2: Optional ํŒจํ„ด ์‚ฌ์šฉ

-if (ranking == null) {
-    return ApiResponse.success(null);  // ๋˜๋Š” ์ ์ ˆํ•œ ์—๋Ÿฌ ์‘๋‹ต
-}
-
-return ApiResponse.success(
-        RankingV1Dto.ProductRankingResponse.from(ranking)
-);
+return ApiResponse.success(
+        ranking != null ? RankingV1Dto.ProductRankingResponse.from(ranking) : null
+);

22-46: ๋žญํ‚น ์กฐํšŒ์™€ ์นด์šดํŠธ ์กฐํšŒ ๋ถ„๋ฆฌ

getRankingWithPaging๊ณผ getTopRanking ๋ชจ๋‘ ๋žญํ‚น ๋ฐ์ดํ„ฐ์™€ ์ด ๊ฐœ์ˆ˜๋ฅผ ๋ณ„๋„๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•˜๋‹ค๋ฉด RankingFacade์—์„œ ํ•œ ๋ฒˆ์— ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ์‹์„ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Also applies to: 48-69

apps/commerce-collector/src/main/java/com/loopers/application/order/OrderBatchEventHandler.java (1)

125-138: NPE ๋ฐฉ์ง€ ๋ฐ ์Šคํƒ€์ผ ๊ฐœ์„ 

  1. Line 126: new java.util.ArrayList<>() ๋Œ€์‹  import ์‚ฌ์šฉ ๊ถŒ์žฅ
  2. Line 133: event.payload().orderId()๊ฐ€ null์ธ ๊ฒฝ์šฐ NPE ๋ฐœ์ƒ ๊ฐ€๋Šฅ
๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •
+import java.util.ArrayList;

 private List<EventHandledInfo> createEventHandledInfos(Iterable<OrderEvent> events) {
-    List<EventHandledInfo> infos = new java.util.ArrayList<>();
+    List<EventHandledInfo> infos = new ArrayList<>();
     for (OrderEvent event : events) {
         if (KafkaTopics.Order.ORDER_CREATED.equals(event.eventType())) {
+            OrderEvent.OrderCreatedPayload payload = event.payload();
+            if (payload == null || payload.orderId() == null) {
+                log.warn("์œ ํšจํ•˜์ง€ ์•Š์€ payload - eventId: {}", event.eventId());
+                continue;
+            }
             infos.add(EventHandledInfo.of(
                     event.eventId(),
                     event.eventType(),
                     AggregateTypes.ORDER,
-                    event.payload().orderId().toString()
+                    payload.orderId().toString()
             ));
         }
     }
     return infos;
 }
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java (1)

48-151: ๋ฐฐ์น˜ upsert ๊ตฌํ˜„์ด ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

BatchPreparedStatementSetter๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์™€ ON DUPLICATE KEY UPDATE ํŒจํ„ด์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๋นˆ ๋งต์— ๋Œ€ํ•œ early return๋„ ์ข‹์€ ๋ฐฉ์–ด์  ์ฝ”๋”ฉ์ž…๋‹ˆ๋‹ค.

์„ ํƒ์ ์œผ๋กœ, ์„ธ upsert ๋ฉ”์„œ๋“œ์˜ ๊ตฌ์กฐ๊ฐ€ ๊ฑฐ์˜ ๋™์ผํ•˜๋ฏ€๋กœ ๊ณตํ†ต private ํ—ฌํผ ๋ฉ”์„œ๋“œ๋กœ ์ถ”์ถœํ•˜๋ฉด ์ค‘๋ณต์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

private void upsertDeltas(Map<Long, Integer> deltas, LocalDate metricDate, 
                          String sql, String metricName) {
    if (deltas.isEmpty()) return;
    // ... ๊ณตํ†ต ๋กœ์ง
}
๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 128f47d and 5b67c36.

๐Ÿ“’ Files selected for processing (54)
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductEventPublisher.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/SimpleRankingInfo.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/event/OrderCreatedEvent.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingType.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RedisRankingRepository.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-collector/src/main/java/com/loopers/CommerceCollectorApplication.java
  • apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledFacade.java
  • apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledInfo.java
  • apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeBatchEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsDailyFacade.java
  • apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java
  • apps/commerce-collector/src/main/java/com/loopers/application/order/OrderBatchEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/application/order/OrderEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/application/product/ProductBatchEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/application/product/ProductEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingFacade.java
  • apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingScheduler.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/eventhandled/EventHandledRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/eventhandled/EventHandledService.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDaily.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDailyRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledRepositoryImpl.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyJpaRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/dto/ProductLikeEvent.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/dto/OrderEvent.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/dto/ProductEvent.java
  • apps/commerce-collector/src/main/resources/application.yml
๐Ÿ’ค Files with no reviewable changes (1)
  • apps/commerce-collector/src/main/java/com/loopers/domain/eventhandled/EventHandledService.java
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (9)
๐Ÿ“š 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/application/outbox/OutboxEventPublisher.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductEventPublisher.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/application/order/OrderBatchEventHandler.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/event/OrderCreatedEvent.java
  • apps/commerce-collector/src/main/java/com/loopers/application/order/OrderEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeEventHandler.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
  • apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledFacade.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/dto/ProductLikeEvent.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/dto/OrderEvent.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/application/product/ProductBatchEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeBatchEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/application/product/ProductEventHandler.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/application/outbox/OutboxEventPublisher.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductEventPublisher.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/dto/ProductEvent.java
  • apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledInfo.java
  • apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/dto/ProductLikeEvent.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeBatchEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/application/product/ProductEventHandler.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-collector/src/main/java/com/loopers/domain/eventhandled/EventHandledRepository.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductEventPublisher.java
  • apps/commerce-collector/src/main/java/com/loopers/application/order/OrderBatchEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/application/order/OrderEventHandler.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledRepositoryImpl.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderEventConsumer.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/infrastructure/product/ProductJpaRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDaily.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeEventHandler.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/dto/ProductLikeEvent.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
  • apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsDailyFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java
  • apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java
๐Ÿ“š Learning: 2025-11-21T03:38:07.494Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 99
File: apps/commerce-api/src/main/resources/application.yml:26-30
Timestamp: 2025-11-21T03:38:07.494Z
Learning: The batch job implementation for likeCount synchronization in apps/commerce-api is temporary and intended for development environment only. It will be replaced with Event-Driven Architecture (EDA) before production deployment, so production-level configuration concerns (like profile-based initialize-schema settings) are not required.

Applied to files:

  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeBatchEventHandler.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-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductBatchEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeEventConsumer.java
  • apps/commerce-collector/src/main/java/com/loopers/application/product/ProductEventHandler.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-12-07T15:47:18.277Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 52
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/payment/PgFeignConfig.kt:0-0
Timestamp: 2025-12-07T15:47:18.277Z
Learning: In Spring Cloud OpenFeign, configuration classes specified in FeignClient(configuration = ...) do NOT need the Configuration annotation. Spring Cloud OpenFeign creates a separate ApplicationContext for each named client and directly instantiates Bean methods from the configuration class. Adding Configuration would cause the class to be registered globally via ComponentScan, making the configuration apply to ALL Feign clients instead of just the specific client, which is usually undesirable.

Applied to files:

  • apps/commerce-collector/src/main/java/com/loopers/CommerceCollectorApplication.java
๐Ÿ“š Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.

Applied to files:

  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyJpaRepository.java
๐Ÿงฌ Code graph analysis (14)
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java (1)
modules/kafka/src/main/java/com/loopers/kafka/KafkaTopics.java (1)
  • KafkaTopics (3-58)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductEventPublisher.java (3)
modules/kafka/src/main/java/com/loopers/kafka/AggregateTypes.java (1)
  • AggregateTypes (3-14)
modules/kafka/src/main/java/com/loopers/kafka/KafkaTopics.java (2)
  • KafkaTopics (3-58)
  • ProductDetail (20-26)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)
  • Slf4j (17-132)
apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeBatchEventConsumer.java (4)
modules/kafka/src/main/java/com/loopers/kafka/KafkaTopics.java (1)
  • KafkaTopics (3-58)
apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsDailyFacade.java (1)
  • Component (12-42)
apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledFacade.java (1)
  • Component (12-58)
apps/commerce-collector/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledRepositoryImpl.java (1)
  • Component (10-34)
apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingFacade.java (2)
apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingScheduler.java (1)
  • Slf4j (19-139)
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt (1)
  • log (22-119)
apps/commerce-collector/src/main/java/com/loopers/application/order/OrderBatchEventHandler.java (4)
modules/kafka/src/main/java/com/loopers/kafka/AggregateTypes.java (1)
  • AggregateTypes (3-14)
modules/kafka/src/main/java/com/loopers/kafka/KafkaTopics.java (2)
  • KafkaTopics (3-58)
  • Order (28-36)
apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java (1)
  • Slf4j (13-70)
apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsDailyFacade.java (1)
  • Component (12-42)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
modules/kafka/src/main/java/com/loopers/kafka/KafkaTopics.java (2)
  • KafkaTopics (3-58)
  • Coupon (38-45)
apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledFacade.java (1)
apps/commerce-collector/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledRepositoryImpl.java (1)
  • Component (10-34)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1DTO.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Dto.java (1)
  • BrandV1Dto (5-19)
apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductBatchEventConsumer.java (1)
modules/kafka/src/main/java/com/loopers/kafka/KafkaTopics.java (1)
  • KafkaTopics (3-58)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
  • RankingV1Dto (13-151)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
  • Component (18-127)
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java (2)
apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsDailyFacade.java (1)
  • Component (12-42)
apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledFacade.java (1)
  • Component (12-58)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (1)
  • RequiredArgsConstructor (15-80)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
  • Component (10-56)
apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java (3)
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
  • Slf4j (18-144)
apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsDailyFacade.java (1)
  • Component (12-42)
apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledFacade.java (1)
  • Component (12-58)
๐Ÿ”‡ Additional comments (50)
apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductEventConsumer.java (2)

1-1: ํŒจํ‚ค์ง€ ์žฌ๊ตฌ์„ฑ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์ œํ’ˆ(product) ๋„๋ฉ”์ธ๋ณ„ ์ปจ์Šˆ๋จธ ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋กœ์˜ ์žฌ๊ตฌ์„ฑ์ด PR์˜ ์ „๋ฐ˜์ ์ธ ๊ตฌ์กฐ ๊ฐœ์„ ๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.


20-79: ๋ฐฐ์น˜ ์ปจ์Šˆ๋จธ๊ฐ€ ์กด์žฌํ•˜๊ณ  PRODUCT_VIEWED ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€๋งŒ, ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์ „๋žต์ด ๋‹จ๊ฑด ์ปจ์Šˆ๋จธ์™€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

ProductBatchEventConsumer๋Š” ์กด์žฌํ•˜๋ฉฐ PRODUCT_VIEWED ์ด๋ฒคํŠธ๋ฅผ ์ •์ƒ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์—๋Ÿฌ ํ•ธ๋“ค๋ง ํŒจํ„ด์˜ ๋ถˆ์ผ์น˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ProductEventConsumer: JSON ํŒŒ์‹ฑ ์—๋Ÿฌ ์‹œ ๋ช…์‹œ์ ์œผ๋กœ acknowledgment.acknowledge()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์žฌ์‹œ๋„๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์—๋Ÿฌ๋Š” ์˜ˆ์™ธ๋ฅผ ๋˜์ ธ ์žฌ์‹œ๋„๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ProductBatchEventConsumer: JSON ํŒŒ์‹ฑ ์—๋Ÿฌ๋ฅผ parseEvent() ๋ฉ”์„œ๋“œ์—์„œ catchํ•˜์—ฌ null์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  Objects::nonNull๋กœ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์—๋Ÿฌ๋ฅผ ์•”๋ฌต์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ๋ฐฐ์น˜ ๋ ˆ๋ฒจ ์˜ˆ์™ธ๋งŒ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

๋˜ํ•œ ์ถ”๊ฐ€ ๊ฒ€์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

  1. ์ด ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ๋Š” EventInboxAspect ํŒจํ„ด์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ์ˆ˜๋™ try-catch๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  2. JSON ํŒŒ์‹ฑ ์‹คํŒจ์— ๋Œ€ํ•œ ์žฌ์‹œ๋„ ์ •์ฑ…์ด ๋‘ ์ปจ์Šˆ๋จธ ๊ฐ„์— ๋ถˆ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ๋ฒ„์ „์—์„œ ํŒŒ์‹ฑ ์‹คํŒจ ๋ฉ”์‹œ์ง€๊ฐ€ ์ž๋™์œผ๋กœ ๋ฒ„๋ ค์ง€๋Š”๋ฐ, ์ด๊ฒƒ์ด ์˜๋„๋œ ๋™์ž‘์ธ์ง€ ํ™•์ธํ•˜์„ธ์š”.
apps/commerce-collector/src/main/resources/application.yml (1)

31-33: application.yml์˜ Kafka ์„ค์ •์ด ํšจ๊ณผ ์—†์Œ

์„ ํƒ๋œ ์„ค์ •๊ฐ’๋“ค์ด modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java์˜ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ƒ์ˆ˜๋กœ ์ธํ•ด ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ์ ์šฉ๋˜๋Š” ์„ค์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • max.poll.records: 100 โ†’ 3000 (ํ•˜๋“œ์ฝ”๋”ฉ๋จ)
  • fetch-min-size: 1024 โ†’ 1MB (ํ•˜๋“œ์ฝ”๋”ฉ๋จ)
  • fetch-max-wait: 500ms โ†’ 5000ms (ํ•˜๋“œ์ฝ”๋”ฉ๋จ)

๋”ฐ๋ผ์„œ max.poll.interval.ms(๊ธฐ๋ณธ 5๋ถ„) ๋Œ€์‹  MAX_POLL_INTERVAL_MS = 2๋ถ„(120์ดˆ)์ด ์‹ค์ œ ์ œํ•œ๊ฐ’์ž…๋‹ˆ๋‹ค. 3000๊ฐœ ๋ฉ”์‹œ์ง€๋ฅผ 3๊ฐœ ๋™์‹œ์„ฑ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ๋•Œ 2๋ถ„ ๋‚ด์— ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋˜์ง€ ์•Š์œผ๋ฉด ์ปจ์Šˆ๋จธ ๋ฆฌ๋ฐธ๋Ÿฐ์‹ฑ ์œ„ํ—˜์ด ๋†’์Šต๋‹ˆ๋‹ค. KafkaConfig์˜ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๊ฐ’๋“ค์ด ์„ค๊ณ„ ์˜๋„๋ฅผ ๋ฐ˜์˜ํ•˜๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์ด ์‹ค์ œ๋กœ 2๋ถ„ ์ด๋‚ด๋กœ ์™„๋ฃŒ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•˜์‹ญ์‹œ์˜ค.

Likely an incorrect or invalid review comment.

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

19-20: ๋„๋ฉ”์ธ ์ธํ„ฐํŽ˜์ด์Šค ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜๊ฐ€ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ณ„์ธต ์ „์ฒด์™€ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

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

58-61: ๊ตฌํ˜„์ด ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋กœ์˜ ๋‹จ์ˆœ ์œ„์ž„ ํŒจํ„ด์ด ์ •ํ™•ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์œผ๋ฉฐ, @Override ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ธํ„ฐํŽ˜์ด์Šค ๊ณ„์•ฝ์„ ๋ช…์‹œ์ ์œผ๋กœ ์ค€์ˆ˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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

6-16: LGTM!

Redis ํ‚ค ์ ‘๋‘์‚ฌ์™€ ์„ค๋ช…์„ ์บก์Аํ™”ํ•œ ๊น”๋”ํ•œ enum ์„ค๊ณ„์ž…๋‹ˆ๋‹ค. Lombok ์–ด๋…ธํ…Œ์ด์…˜ ์‚ฌ์šฉ์ด ์ ์ ˆํ•˜๋ฉฐ, ์ผ๊ด€๋œ ํ‚ค ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜(ranking:{type})์ด Redis ZSET ๊ธฐ๋ฐ˜ ๋žญํ‚น ์‹œ์Šคํ…œ์— ์ž˜ ๋ถ€ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java (1)

11-14: LGTM!

๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. Kafka ๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ์—์„œ ์ง‘๊ณ„๋œ ๋ธํƒ€๊ฐ’์„ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ์„ค๊ณ„๊ฐ€ ์ž˜ ๋ฐ˜์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ์ด ๋ช…ํ™•ํ•˜๊ณ  ์ผ๊ด€์„ฑ ์žˆ์Šต๋‹ˆ๋‹ค.

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

14-21: items ๋ฆฌ์ŠคํŠธ์˜ null ์•ˆ์ „์„ฑ ํ™•์ธ ํ•„์š”

items ํ•„๋“œ๊ฐ€ null๋กœ ์ „๋‹ฌ๋  ๊ฒฝ์šฐ downstream ์ปจ์Šˆ๋จธ์—์„œ NPE๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์—์„œ null ์ฒดํฌ ๋˜๋Š” ๋นˆ ๋ฆฌ์ŠคํŠธ ๊ธฐ๋ณธ๊ฐ’ ์ฒ˜๋ฆฌ๋ฅผ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
     public static OrderCreatedEvent of(
             Long orderId,
             Long userId,
             BigDecimal totalPrice,
             PaymentType paymentType,
             List<OrderItem> items
     ) {
         return new OrderCreatedEvent(
                 orderId,
                 userId,
                 totalPrice,
                 paymentType,
-                items,
+                items != null ? items : List.of(),
                 LocalDateTime.now()
         );
     }
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)

161-168: LGTM - ์ฃผ๋ฌธ ์•„์ดํ…œ ๋งคํ•‘ ๋กœ์ง

savedOrder.getOrderItems()์—์„œ OrderCreatedEvent.OrderItem์œผ๋กœ์˜ ๋ณ€ํ™˜์ด ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ, item.getPrice()๊ฐ€ null์ผ ๊ฒฝ์šฐ NPE ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์œผ๋‹ˆ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.

apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java (1)

94-94: LGTM!

PRODUCT_VIEW aggregate type์— ๋Œ€ํ•œ ํ† ํ”ฝ ๋งคํ•‘์ด ์ ์ ˆํ•˜๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ProductEventPublisher์˜ outbox ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ๋ฐœํ–‰๊ณผ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์—ฐ๋™๋ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/CommerceCollectorApplication.java (1)

6-9: LGTM!

@EnableScheduling ์ถ”๊ฐ€๋กœ RankingScheduler ๋“ฑ ์Šค์ผ€์ค„๋ง ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ ์ ˆํ•œ ๋ณ€๊ฒฝ์ž…๋‹ˆ๋‹ค.

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

9-17: LGTM - ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ์„ค๊ณ„ ํ™•์ธ

from() ๋ฉ”์„œ๋“œ์—์„œ null ์ž…๋ ฅ ์‹œ null์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ์„ค๊ณ„๊ฐ€ JSON ์ง๋ ฌํ™” ์‹œ ํ•„๋“œ ์ƒ๋žต ๋ชฉ์ ์ด๋ผ๋ฉด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ, ํ˜ธ์ถœ๋ถ€์—์„œ null ์ฒดํฌ๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ์‚ฌ์šฉ ํŒจํ„ด์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.

apps/commerce-collector/src/main/java/com/loopers/domain/eventhandled/EventHandledRepository.java (1)

8-8: ๋ฐฐ์น˜ ์ €์žฅ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ ์Šน์ธ

๋ฐฐ์น˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ saveAll ๋ฉ”์„œ๋“œ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” Spring Data Repository ํŒจํ„ด๊ณผ ์ผ๊ด€์„ฑ์ด ์žˆ์œผ๋ฉฐ, PR์—์„œ ๋„์ž…๋œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์ž˜ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/dto/ProductLikeEvent.java (1)

3-8: ๊ฐ„๊ฒฐํ•œ ์ด๋ฒคํŠธ DTO ์„ค๊ณ„ ์Šน์ธ

๋ถˆ๋ณ€์„ฑ์„ ์ œ๊ณตํ•˜๋Š” record๋ฅผ ์‚ฌ์šฉํ•œ ์ด๋ฒคํŠธ DTO ์„ค๊ณ„๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํŒจํ„ด์— ์ž˜ ๋ถ€ํ•ฉํ•˜๋ฉฐ, ํ•„๋“œ ๊ตฌ์„ฑ๋„ ํ•ฉ๋ฆฌ์ ์ž…๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledRepositoryImpl.java (1)

25-28: ๋ฐฐ์น˜ ์ €์žฅ ๊ตฌํ˜„ ์Šน์ธ

JPA Repository์— ์œ„์ž„ํ•˜๋Š” ๊ฐ„๊ฒฐํ•œ ๊ตฌํ˜„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. void ๋ฐ˜ํ™˜ ํƒ€์ž…์€ ํ˜ธ์ถœ์ž๊ฐ€ ์ €์žฅ๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ํ•„์š”๋กœ ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.

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

16-25: ๊ฐ€๊ฒฉ ํ•„๋“œ๋Š” ์—”ํ‹ฐํ‹ฐ ๋ ˆ๋ฒจ์—์„œ ํ•„์ˆ˜ ๊ฒ€์ฆ์ด ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค

Product ์—”ํ‹ฐํ‹ฐ์˜ ์ƒ์„ฑ์ž์—์„œ validationPrice() ๋ฉ”์„œ๋“œ๋กœ price๊ฐ€ null์ด๋ฉด CoreException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค(lines 99-102). ๋”ฐ๋ผ์„œ product.getPrice().getAmount()๋Š” NPE ์œ„ํ—˜์ด ์—†์œผ๋ฉฐ, ํ˜„์žฌ ์ฝ”๋“œ๋Š” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

apps/commerce-collector/src/main/java/com/loopers/application/product/ProductEventHandler.java (1)

1-40: ํŒŒ์‚ฌ๋“œ ํŒจํ„ด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋„๋ฉ”์ธ ์„œ๋น„์Šค์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒŒ์‚ฌ๋“œ๋กœ์˜ ์ „ํ™˜์ด ์ผ๊ด€๋˜๊ฒŒ ์ ์šฉ๋˜์—ˆ์œผ๋ฉฐ, ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ โ†’ ๋ฉ”ํŠธ๋ฆญ ์ฆ๊ฐ€ โ†’ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ๊ธฐ๋ก์˜ ํ๋ฆ„์ด ์œ ์ง€๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/application/order/OrderEventHandler.java (1)

1-65: ํŒŒ์‚ฌ๋“œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐ ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰ ์ฒ˜๋ฆฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฐ˜์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋„๋ฉ”์ธ ์„œ๋น„์Šค์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒŒ์‚ฌ๋“œ๋กœ์˜ ์ „ํ™˜์ด ์ผ๊ด€๋˜๊ฒŒ ์ ์šฉ๋˜์—ˆ์œผ๋ฉฐ, incrementOrderCount์— ์ˆ˜๋Ÿ‰ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์ถ”๊ฐ€๋˜์–ด ์ฃผ๋ฌธ ์ง‘๊ณ„๊ฐ€ ๋” ์ •ํ™•ํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/like/ProductLikeBatchEventConsumer.java (1)

44-48: ์ž˜๋ชป๋œ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ „๋žต ํ™•์ธ ํ•„์š”

ํŒŒ์‹ฑ์— ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋Š” null๋กœ ๋ณ€ํ™˜๋˜์–ด ํ•„ํ„ฐ๋ง๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ํ•ด๋‹น ๋ฉ”์‹œ์ง€๊ฐ€ ์˜๊ตฌ์ ์œผ๋กœ ์†์‹ค๋จ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์ผ์‹œ์ ์ธ ํŒŒ์‹ฑ ์˜ค๋ฅ˜(์˜ˆ: ObjectMapper ์„ค์ • ๋ถˆ์ผ์น˜)๋ผ๋ฉด DLQ(Dead Letter Queue)๋กœ ๋ผ์šฐํŒ…ํ•˜๊ฑฐ๋‚˜ ๋ณ„๋„๋กœ ๊ธฐ๋กํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ๋ฐฐ์น˜ ๋‚ด ์ผ๋ถ€ ๋ฉ”์‹œ์ง€๋งŒ ์„ฑ๊ณตํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ์Šคํ‚ต๋˜๋Š” ๋ถ€๋ถ„ ์„ฑ๊ณต ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์˜๋„๋œ ๋™์ž‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.

apps/commerce-collector/src/main/java/com/loopers/application/eventhandled/EventHandledFacade.java (1)

1-58: ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ƒํƒœ ๊ด€๋ฆฌ ํŒŒ์‚ฌ๋“œ๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

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

apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsDailyFacade.java (1)

1-42: ์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ ํŒŒ์‚ฌ๋“œ๊ฐ€ ์ ์ ˆํžˆ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Propagation.REQUIRED๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ์œ„ ํŠธ๋žœ์žญ์…˜์— ์ฐธ์—ฌํ•จ์œผ๋กœ์จ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ์™€ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ ๊ฐ„์˜ ์›์ž์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ์„ธ ๊ฐ€์ง€ ๋ฉ”ํŠธ๋ฆญ ํƒ€์ž…(์ข‹์•„์š”, ์กฐํšŒ, ์ฃผ๋ฌธ)์— ๋Œ€ํ•œ ์ผ๊ด€๋œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDaily.java (2)

14-19: ์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ์ค‘๋ณต ๋ฐฉ์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

(product_id, metric_date)์— ๋Œ€ํ•œ ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด์ด ์ผ๋ณ„ ๋ฉ”ํŠธ๋ฆญ ๋ ˆ์ฝ”๋“œ์˜ ์ค‘๋ณต์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. Upsert ์—ฐ์‚ฐ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


59-69: ์ด ๋ฉ”์„œ๋“œ๋“ค์€ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋™์‹œ์„ฑ ์ œ์–ด ๊ฒ€ํ† ๊ฐ€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

addLikeDelta, addViewDelta, addOrderDelta ๋ฉ”์„œ๋“œ๋Š” ProductMetricsDaily ์—”ํ‹ฐํ‹ฐ์— ์ •์˜๋˜์–ด ์žˆ์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” ์–ด๋””์„œ๋„ ํ˜ธ์ถœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ๋Š” ProductMetricsDailyFacade๋ฅผ ํ†ตํ•ด ProductMetricsDailyRepository์˜ upsert ๋ฉ”์„œ๋“œ๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉฐ, ์ด ๋ฉ”์„œ๋“œ๋“ค์€ MySQL์˜ ON DUPLICATE KEY UPDATE ์›์ž์  ์—ฐ์‚ฐ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. (product_id, metric_date) ๊ณ ์œ  ์ œ์•ฝ์œผ๋กœ ์ธํ•ด ๋™์‹œ ์‹คํ–‰ ์‹œ์—๋„ ์›์ž์„ฑ์ด ๋ณด์žฅ๋˜๋ฏ€๋กœ lost update ๋ฌธ์ œ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeBatchEventHandler.java (1)

98-99: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹œ์ž‘ ์‹œ TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"))๋กœ ๊ธฐ๋ณธ ํƒ€์ž„์กด์„ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •ํ•˜๋ฏ€๋กœ, ๋ช…์‹œ์ ์ธ ZoneId ์ง€์ •์€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ฝ”๋“œ๋Š” RankingScheduler, RankingFacade, OrderBatchEventHandler ๋“ฑ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์—์„œ ์ผ๊ด€๋˜๊ฒŒ ์‚ฌ์šฉ๋˜๋Š” ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

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

19-55: ์ž…๋ ฅ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ฐ ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋กœ์ง ํ™•์ธ

limit, page, size ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ๊ณผ offset ๊ณ„์‚ฐ ๋กœ์ง์ด ์ ์ ˆํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

6-47: ๋ ˆ์ฝ”๋“œ ๊ตฌ์กฐ ๋ฐ ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ ์ ์ ˆ

RankingInfo์™€ ์ค‘์ฒฉ ๋ ˆ์ฝ”๋“œ ProductRankings์˜ ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ , null ์ฒ˜๋ฆฌ ๋กœ์ง(๋ชจ๋“  ๋žญํ‚น์ด null์ด๋ฉด ์ „์ฒด๋ฅผ null๋กœ ๋ฐ˜ํ™˜)์ด API ์‘๋‹ต์˜ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ์ ์ ˆํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/application/product/ProductBatchEventHandler.java (1)

31-55: ์ „์ฒด์ ์ธ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋กœ์ง ์ ์ ˆ

ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๋‚ด์—์„œ ํ•„ํ„ฐ๋ง, ์ค‘๋ณต ์ œ๊ฑฐ, ๋ธํƒ€ ๊ณ„์‚ฐ, ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ, ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ๋งˆํ‚น๊นŒ์ง€์˜ ํ๋ฆ„์ด ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ๋กค๋ฐฑ์„ ์œ„ํ•œ RuntimeException ๋ž˜ํ•‘๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (2)

47-54: like_count ์Œ์ˆ˜ ๋ฐฉ์ง€ ๋กœ์ง ํ™•์ธ

INSERT ์‹œ GREATEST(?, 0)๋กœ ์Œ์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ์žˆ์œผ๋‚˜, UPDATE ์‹œ์—๋Š” like_count = like_count + VALUES(like_count)๋กœ ๋ธํƒ€๋ฅผ ์ง์ ‘ ๋”ํ•ฉ๋‹ˆ๋‹ค. ์ทจ์†Œ ์ด๋ฒคํŠธ๋กœ ์ธํ•ด ์Œ์ˆ˜ ๋ธํƒ€๊ฐ€ ์ „๋‹ฌ๋˜๊ณ  like_count๊ฐ€ ํ•ด๋‹น ๊ฐ’๋ณด๋‹ค ์ž‘์„ ๊ฒฝ์šฐ, ๊ฒฐ๊ณผ๊ฐ€ ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์Œ์ˆ˜ ๋ฐฉ์ง€ ์ˆ˜์ • ์ œ์•ˆ
             ON DUPLICATE KEY UPDATE
-                like_count = like_count + VALUES(like_count),
+                like_count = GREATEST(like_count + VALUES(like_count), 0),
                 updated_at = NOW()

41-73: ๋ฐฐ์น˜ ์—…์„œํŠธ ํŒจํ„ด ๊ตฌํ˜„ ์ ์ ˆ

๋นˆ ๋งต ์ฒดํฌ, BatchPreparedStatementSetter ํ™œ์šฉ, ๋กœ๊น… ๋“ฑ ๋ฐฐ์น˜ ์—…์„œํŠธ ํŒจํ„ด์ด ์ผ๊ด€๋˜๊ฒŒ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ, ON DUPLICATE KEY UPDATE ๊ตฌ๋ฌธ์€ MySQL ์ „์šฉ์ด๋ฏ€๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ด์‹์„ฑ์ด ํ•„์š”ํ•  ๊ฒฝ์šฐ ๊ณ ๋ ค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/product/ProductBatchEventConsumer.java (2)

45-53: ์ด๋ฒคํŠธ ํƒ€์ž… ํ•„ํ„ฐ๋ง ์œ„์น˜ ๊ฒ€ํ† 

๊ธฐ์กด ProductEventConsumer๋Š” PRODUCT_VIEWED ์ด๋ฒคํŠธ๋งŒ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•„ํ„ฐ๋งํ•˜์ง€๋งŒ, ์ด ๋ฐฐ์น˜ ์ปจ์Šˆ๋จธ๋Š” ๋ชจ๋“  ์ด๋ฒคํŠธ๋ฅผ ํ•ธ๋“ค๋Ÿฌ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ํ•ธ๋“ค๋Ÿฌ์˜ calculateDelta์—์„œ PRODUCT_VIEWED๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ 0์„ ๋ฐ˜ํ™˜ํ•˜์ง€๋งŒ, ํ•ด๋‹น ์ด๋ฒคํŠธ๋„ "์ฒ˜๋ฆฌ ์™„๋ฃŒ"๋กœ ๋งˆํ‚น๋ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์ด ์˜๋„๋œ ๋™์ž‘์ด๋ผ๋ฉด ๋ฌธ์ œ ์—†์œผ๋‚˜, PRODUCT_VIEWED ์™ธ์˜ ์ด๋ฒคํŠธ๊ฐ€ product ํ† ํ”ฝ์— ๋ฐœํ–‰๋  ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•˜์—ฌ ์ปจ์Šˆ๋จธ ๋ ˆ๋ฒจ์—์„œ ํ•„ํ„ฐ๋ง์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ๋„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


65-96: ๋ฉ”์‹œ์ง€ ํŒŒ์‹ฑ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋กœ์ง ์ ์ ˆ

ํ•„์ˆ˜ ํ•„๋“œ ๊ฒ€์ฆ, ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ null ๋ฐ˜ํ™˜ ๋ฐ ๊ฒฝ๊ณ  ๋กœ๊น… ๋“ฑ ๋ฐฉ์–ด์  ํ”„๋กœ๊ทธ๋ž˜๋ฐ์ด ์ž˜ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ProductLikeBatchEventConsumer์™€ ๋™์ผํ•œ ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์–ด ์ผ๊ด€์„ฑ์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderBatchEventConsumer.java (2)

86-96: JSON ํŒŒ์‹ฑ ์‹คํŒจ ์ฒ˜๋ฆฌ - LGTM

ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ null์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๋Š” ๋ฐฉ์‹์€ poison pill ๋ฉ”์‹œ์ง€๋ฅผ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์žฌ์‹œ๋„๊ฐ€ ๋ถˆํ•„์š”ํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์Šคํ‚ตํ•˜์—ฌ ๋ฐฐ์น˜ ์ „์ฒด๊ฐ€ ์‹คํŒจํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.


28-32: ๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

KafkaConfig.BATCH_LISTENER๋Š” MAX_POLL_RECORDS_CONFIG๊ฐ€ 3000์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ๊ณ , ๋™์‹œ์„ฑ์ด 3์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์œผ๋ฉฐ, ์ˆ˜๋™ ์Šน์ธ(MANUAL) ๋ชจ๋“œ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. BatchMessagingMessageConverter๋„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์–ด ๋ฐฐ์น˜ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ์— ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

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

127-138: ์ผ์ž๋ณ„ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ๋กœ์ง - LGTM

10์ผ ์ด์ „ ๋ฐ์ดํ„ฐ ์‚ญ์ œ ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ์—๋Ÿฌ ํ•ธ๋“ค๋ง๊ณผ ๋กœ๊น…๋„ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

64-102: ๋žญํ‚น DTO ์ถ”๊ฐ€ - LGTM

RankingResponse์™€ SimpleRankingResponse ๋ ˆ์ฝ”๋“œ๊ฐ€ null ์•ˆ์ „ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์œผ๋ฉฐ, ๊ธฐ์กด DTO ํŒจํ„ด(BrandV1Dto.BrandResponse)๊ณผ ์ผ๊ด€์„ฑ ์žˆ๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.


36-51: ProductDetailResponse ๋žญํ‚น ํ•„๋“œ ์ถ”๊ฐ€ - LGTM

rankings ํ•„๋“œ ์ถ”๊ฐ€์™€ ๋งคํ•‘ ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. null ์ฒดํฌ๋ฅผ ํ†ตํ•ด ๋žญํ‚น ์ •๋ณด๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ๋„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

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

91-107: Redis ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„ - LGTM

SessionCallback์„ ์‚ฌ์šฉํ•œ ํŒŒ์ดํ”„๋ผ์ธ ์ฒ˜๋ฆฌ๋กœ Redis ํ˜ธ์ถœ ํšŸ์ˆ˜๋ฅผ ์ตœ์†Œํ™”ํ•œ ์ข‹์€ ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค. TTL ์„ค์ •๋„ ํŒŒ์ดํ”„๋ผ์ธ ๋‚ด์—์„œ ํ•จ๊ป˜ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/application/like/ProductLikeEventHandler.java (2)

3-16: Facade ํŒจํ„ด์œผ๋กœ์˜ ๋ฆฌํŒฉํ† ๋ง - LGTM

EventHandledService โ†’ EventHandledFacade, ProductMetricsService โ†’ ProductMetricsFacade๋กœ์˜ ๋ณ€๊ฒฝ์ด ์ผ๊ด€๋˜๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ์™€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ๋ฆ„์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.


21-36: ์ข‹์•„์š” ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋กœ์ง - LGTM

handleLikeAdded์™€ handleLikeRemoved ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ, ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ, ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ๊ธฐ๋ก์˜ ์ˆœ์„œ๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ•™์Šต๋œ ๋‚ด์šฉ์— ๋”ฐ๋ฅด๋ฉด, Like ์ด๋ฒคํŠธ๋Š” productId๋ฅผ aggregateId๋กœ ์‚ฌ์šฉํ•˜์—ฌ ํŒŒํ‹ฐ์…˜ ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 41-56

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

32-45: TOP N ๋žญํ‚น ์กฐํšŒ - LGTM

ZREVRANGE๋ฅผ ์‚ฌ์šฉํ•œ ์ ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ ์กฐํšŒ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋นˆ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.


81-103: ๋‹จ์ผ ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ - LGTM

์ ์ˆ˜์™€ ์ˆœ์œ„๋ฅผ ๊ฐ๊ฐ ์กฐํšŒํ•˜์—ฌ Ranking ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. null ์ฒดํฌ๋„ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/application/order/OrderBatchEventHandler.java (2)

78-92: ORDER_CREATED ์™ธ ์ด๋ฒคํŠธ ํƒ€์ž… ์ฒ˜๋ฆฌ

aggregateOrderQuantities์—์„œ ORDER_CREATED๊ฐ€ ์•„๋‹Œ ์ด๋ฒคํŠธ๋Š” ๊ฒฝ๊ณ  ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์Šคํ‚ตํ•ฉ๋‹ˆ๋‹ค. ์ด ๋™์ž‘์ด ์˜๋„๋œ ๊ฒƒ์ธ์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋‹ค๋ฅธ ์ฃผ๋ฌธ ์ด๋ฒคํŠธ(ORDER_COMPLETED, ORDER_CANCELLED ๋“ฑ)๋„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค๋ฉด ๋ณ„๋„ ๋กœ์ง์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.


30-61: ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํ๋ฆ„ - LGTM

์ „์ฒด์ ์ธ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํ๋ฆ„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค:

  1. ๋ฏธ์ฒ˜๋ฆฌ ์ด๋ฒคํŠธ ํ•„ํ„ฐ๋ง
  2. ์ค‘๋ณต ์ œ๊ฑฐ
  3. ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰ ์ง‘๊ณ„
  4. ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ
  5. ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ๊ธฐ๋ก

ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ๋ชจ๋“  ์ž‘์—…์ด ์›์ž์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋ฉฐ, ์‹คํŒจ ์‹œ ์ „์ฒด ๋กค๋ฐฑ๋ฉ๋‹ˆ๋‹ค. ํ•™์Šต๋œ ๋‚ด์šฉ์— ๋”ฐ๋ฅด๋ฉด JPA dirty checking์„ ํ™œ์šฉํ•œ ํŒจํ„ด๊ณผ ์ผ๊ด€๋ฉ๋‹ˆ๋‹ค.

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

40-84: LGTM! Cache-Aside ํŒจํ„ด๊ณผ ๋žญํ‚น ํ†ตํ•ฉ์ด ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์บ์‹œ ํžˆํŠธ/๋ฏธ์Šค ์–‘์ชฝ ๊ฒฝ๋กœ์—์„œ ์ด๋ฒคํŠธ ๋ฐœํ–‰์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ๋˜๊ณ  ์žˆ์œผ๋ฉฐ, ๋žญํ‚น ์ •๋ณด ์กฐํšŒ ๋ฐ ProductDetailInfo ๊ตฌ์„ฑ ๋กœ์ง์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

ํ•œ ๊ฐ€์ง€ ์ฐธ๊ณ  ์‚ฌํ•ญ: ์บ์‹œ๋œ ProductDetailInfo์— ๋žญํ‚น ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์–ด TTL(5๋ถ„) ๋™์•ˆ ๋žญํ‚น์ด ๋ณ€๊ฒฝ๋˜์–ด๋„ ๋ฐ˜์˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ๋žญํ‚น์ด ์ค‘์š”ํ•˜๋‹ค๋ฉด ๋žญํ‚น ์ •๋ณด๋ฅผ ๋ณ„๋„๋กœ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/interfaces/consumer/order/OrderEventConsumer.java (1)

17-18: ๋‹จ๊ฑด ์ฒ˜๋ฆฌ ์ปจ์Šˆ๋จธ ๋น„ํ™œ์„ฑํ™” ํ™•์ธ.

๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ๋กœ ์ „ํ™˜ํ•˜๋ฉด์„œ @Component๋ฅผ ์ฃผ์„ ์ฒ˜๋ฆฌํ•œ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ํ–ฅํ›„ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์œ„ํ•ด ์ด ํด๋ž˜์Šค๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜, ํ•„์š” ์‹œ ์žฌํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ช…ํ™•ํ•œ ์ฃผ์„์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

apps/commerce-collector/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java (1)

43-56: ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ API ์ถ”๊ฐ€๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

Propagation.REQUIRED๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ์œ„ ํŠธ๋žœ์žญ์…˜์— ์ฐธ์—ฌํ•˜๋Š” ์„ค๊ณ„๊ฐ€ ProductMetricsDailyFacade์™€ ์ผ๊ด€๋ฉ๋‹ˆ๋‹ค. Repository์˜ upsert ๋ฉ”์„œ๋“œ๋กœ ์œ„์ž„ํ•˜๋Š” ๋ฐฉ์‹๋„ ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค.

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

28-52: ๋žญํ‚น ์กฐํšŒ ๋ฉ”์„œ๋“œ๋“ค์˜ ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

getTopRanking๊ณผ getRankingWithPaging ๋ฉ”์„œ๋“œ๊ฐ€ ๋นˆ ๊ฒฐ๊ณผ์— ๋Œ€ํ•ด early returnํ•˜๊ณ , enrichWithProductInfo๋กœ ์ƒํ’ˆ ์ •๋ณด๋ฅผ ๋ณด๊ฐ•ํ•˜๋Š” ํŒจํ„ด์ด ์ผ๊ด€๋ฉ๋‹ˆ๋‹ค.

๋‹จ, ์œ„์—์„œ ์ง€์ ํ•œ enrichWithProductInfo์˜ NPE ๋ฒ„๊ทธ ์ˆ˜์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

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

15-58: ์š”์ฒญ DTO์˜ ๊ธฐ๋ณธ๊ฐ’ ์ฒ˜๋ฆฌ๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

Record์˜ compact constructor๋ฅผ ํ™œ์šฉํ•˜์—ฌ limit, page, size์— ๊ธฐ๋ณธ๊ฐ’์„ ์„ค์ •ํ•˜๋Š” ํŒจํ„ด์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. @Min/@Max ๊ฒ€์ฆ ์–ด๋…ธํ…Œ์ด์…˜๋„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


83-127: ์‘๋‹ต DTO ๊ตฌ์กฐ๊ฐ€ ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค.

TopRankingResponse์™€ PagingRankingResponse์˜ ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ๋„๋ฉ”์ธ ๊ฐ์ฒด์—์„œ DTO๋กœ์˜ ๋ณ€ํ™˜์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. totalPages ๊ณ„์‚ฐ ๋กœ์ง๋„ @Min(1) ๊ฒ€์ฆ์œผ๋กœ ์ธํ•ด 0 ๋‚˜๋ˆ—์…ˆ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDailyRepository.java (1)

8-21: ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค ์„ค๊ณ„๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

๋„๋ฉ”์ธ ๊ณ„์ธต์—์„œ ์ธํ”„๋ผ ๊ตฌํ˜„์„ ๋ถ„๋ฆฌํ•˜๋Š” ์ข‹์€ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค. CRUD, ๋ฐฐ์น˜ upsert, ์ •๋ฆฌ(cleanup) ๋ฉ”์„œ๋“œ๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉฐ, ๋ฐ˜ํ™˜ ํƒ€์ž…๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค (Optional for single find, int for delete count).

- ProductMetrics order_count์™€ total_order_quantity ๊ตฌ๋ถ„
- ์Šค์ผ€์ค„๋Ÿฌ ์ž์ • ๊ฒฝ๊ณ„ ๋ฌธ์ œ ์ˆ˜์ •
@HongChangMo
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 TTL and key strategy in RankingFacade (commerce-collector)
echo "=== RankingFacade TTL and Key Strategy ==="
cat apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingFacade.java | head -80

echo ""
echo "=== Application YAML ranking configuration ==="
cat apps/commerce-collector/src/main/resources/application.yml | grep -A 10 "ranking:"

echo ""
echo "=== RedisRankingRepository buildRankingKey method ==="
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RedisRankingRepository.java | grep -A 15 "buildRankingKey"

echo ""
echo "=== RankingScheduler processing and update logic ==="
cat apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingScheduler.java | grep -A 30 "processUnprocessedDeltas"

echo ""
echo "=== RankingV1Controller getRankingWithPaging endpoint ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java | grep -A 20 "getRankingWithPaging"

echo ""
echo "=== RankingFacade (commerce-api) enrichWithProductInfo ==="
cat apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java | grep -A 30 "enrichWithProductInfo"

echo ""
echo "=== ProductFacade product detail with ranking ==="
cat apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java | grep -A 40 "getProductDetail"

Length of output: 12352


๐Ÿ Script executed:

#!/bin/bash
# Check if TTL is actually applied in incrementRanking method
echo "=== RankingFacade incrementRanking method (TTL application) ==="
cat apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingFacade.java | grep -A 60 "private void incrementRanking"

echo ""
echo "=== Check incrementRankingWithCompositeScores for TTL ==="
cat apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingFacade.java | grep -A 60 "incrementRankingWithCompositeScores"

Length of output: 6341


@HongChangMo ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฒ€์ฆํ•œ ๊ฒฐ๊ณผ๋ฅผ ๊ณต์œ ๋“œ๋ฆฝ๋‹ˆ๋‹ค:

๐Ÿ“ˆ Ranking Consumer

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

    • โœ… TTL: application.yml์—์„œ ranking.ttl-days: 2๋กœ ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฉฐ, RankingFacade์˜ incrementRanking ๋ฉ”์„œ๋“œ์—์„œ operations.expire(todayKey, rankingTtlDays, TimeUnit.DAYS)๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค
    • โœ… ํ‚ค ์ „๋žต: ranking:{type}:yyyyMMdd ํ˜•์‹ ์‚ฌ์šฉ (์˜ˆ: ranking:like:20251226)
    • โœ… 4๊ฐ€์ง€ ํƒ€์ž…๋ณ„ prefix ์ •์˜: ranking:like, ranking:view, ranking:order, ranking:all
  • ๋‚ ์งœ๋ณ„๋กœ ์ ์žฌํ•  ํ‚ค๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์—ˆ๋‹ค

    • โœ… RedisRankingRepository.buildRankingKey(RankingType, LocalDate) ๋ฉ”์„œ๋“œ ๊ตฌํ˜„
    • โœ… RankingFacade์—์„œ prefix + ":" + LocalDate.now().format(DATE_FORMATTER) ํ˜•์‹์œผ๋กœ ์˜ค๋Š˜ ๋‚ ์งœ ํ‚ค ์ƒ์„ฑ
    • โœ… DATE_FORMATTER: yyyyMMdd ํŒจํ„ด ์‚ฌ์šฉ
  • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ํ›„, ZSET ์— ์ ์ˆ˜๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ฐ˜์˜๋œ๋‹ค

    • โœ… ์ด๋ฒคํŠธ ํ๋ฆ„: Kafka ๋ฐฐ์น˜ ์ปจ์Šˆ๋จธ โ†’ ProductMetricsDailyFacade (์ผ๋ณ„ ์ฆ๊ฐ ์ €์žฅ) โ†’ RankingScheduler (5๋ถ„๋งˆ๋‹ค ์‹คํ–‰) โ†’ RankingFacade.incrementXXXRanking โ†’ Redis ZSET ์—…๋ฐ์ดํŠธ
    • โœ… ๊ฐ€์ค‘์น˜ ์ ์šฉ: like 0.2, view 0.1, order 0.6
    • โœ… zSetOps.incrementScore() ์‚ฌ์šฉํ•˜์—ฌ ์ ์ˆ˜ ์ฆ๋ถ„ ์—…๋ฐ์ดํŠธ

โšพ Ranking API

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

    • โœ… RankingV1Controller.getRankingWithPaging ์—”๋“œํฌ์ธํŠธ ๊ตฌํ˜„
    • โœ… PagingRankingResponse ๋ฐ˜ํ™˜: rankings, currentPage, pageSize, totalCount, totalPages ํฌํ•จ
    • โœ… ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ฒ€์ฆ: page >= 0, size: 1-100
  • ๋žญํ‚น Page ์กฐํšŒ ์‹œ ๋‹จ์ˆœํžˆ ์ƒํ’ˆ ID ๊ฐ€ ์•„๋‹Œ ์ƒํ’ˆ์ •๋ณด๊ฐ€ Aggregation ๋˜์–ด ์ œ๊ณต๋œ๋‹ค

    • โœ… RankingFacade.enrichWithProductInfo ๋ฉ”์„œ๋“œ์—์„œ productService.getAllByIdIn(productIds)๋กœ ๋ฐฐ์น˜ ์กฐํšŒ
    • โœ… RankingInfo์— ProductInfo ํฌํ•จ
    • โœ… ์‘๋‹ต์˜ RankingItem์— rank, productId, product(์ƒํ’ˆ ์ƒ์„ธ์ •๋ณด), score ๋ชจ๋‘ ํฌํ•จ
  • ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ํ•ด๋‹น ์ƒํ’ˆ์˜ ์ˆœ์œ„๊ฐ€ ํ•จ๊ป˜ ๋ฐ˜ํ™˜๋œ๋‹ค (์ˆœ์œ„์— ์—†๋‹ค๋ฉด null)

    • โœ… ProductFacade.getProductDetail์—์„œ rankingFacade.getAllRankingsForProduct(productId, null) ํ˜ธ์ถœ
    • โœ… ProductDetailInfo.of(product, rankings) ๋กœ ๋žญํ‚น ์ •๋ณด ํฌํ•จํ•˜์—ฌ ๋ฐ˜ํ™˜
    • โœ… getAllRankingsForProduct๋Š” 4๊ฐ€์ง€ ํƒ€์ž…(like, view, order, all)์˜ ๋žญํ‚น์„ ์กฐํšŒํ•˜๋ฉฐ, ๋ชจ๋“  ๋žญํ‚น์ด ์—†์œผ๋ฉด null ๋ฐ˜ํ™˜

๋ชจ๋“  ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ์ด ์ถฉ์กฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰

@HongChangMo HongChangMo self-assigned this Dec 27, 2025
- Kafka DLQ ์ ์šฉ
- OrderMetrics DTO ๋ถ„๋ฆฌ
@HongChangMo HongChangMo merged commit 0129d10 into Loopers-dev-lab:HongChangMo Jan 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant