Skip to content

Conversation

@yeonjiyeon
Copy link
Collaborator

@yeonjiyeon yeonjiyeon commented Jan 2, 2026

๐Ÿ“Œ Summary

Spring Batch๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ฃผ๊ฐ„/์›”๊ฐ„ ์ƒํ’ˆ ๋žญํ‚น ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„ ์ •๋ณด(ProductMetrics)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋žญํ‚น ์ ์ˆ˜๋ฅผ ์‚ฐ์ถœํ•˜๊ณ , Materialized View(MV) ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ ๋ฐ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

  • Spring Batch Job ๊ตฌํ˜„: Chunk-Oriented ๋ฐฉ์‹์„ ํ†ตํ•œ ๋Œ€๋Ÿ‰ ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ ๋กœ์ง ๊ตฌ์ถ•
  • Materialized View ์ ์šฉ: ์กฐํšŒ ์ „์šฉ ํ…Œ์ด๋ธ”(mv_product_rank_weekly/monthly) ์„ค๊ณ„ ๋ฐ ๋ฐ์ดํ„ฐ ์ ์žฌ ํ”„๋กœ์„ธ์Šค ๊ตฌํ˜„
  • Ranking API ํ™•์žฅ: ์ผ๊ฐ„(Redis), ์ฃผ๊ฐ„/์›”๊ฐ„(DB MV) ๋ฐ์ดํ„ฐ๋ฅผ ํ†ตํ•ฉ ์ œ๊ณตํ•˜๋„๋ก RankingFacade ๊ฐœ์„ 
  • Event Enrichment: ๋ชจ๋“ˆ ๊ฐ„ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ”๊ธฐ ์œ„ํ•ด ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์˜ ์ƒํ’ˆ ์ •๋ณด ์Šค๋ƒ…์ƒท ์ „ํŒŒ ๋กœ์ง ์ ์šฉ

๐Ÿ’ฌ Review Points

(1) ๋„๋ฉ”์ธ ๊ฒฉ๋ฆฌ๋ฅผ ๊ณ ๋ คํ•œ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์„ค๊ณ„

commerce-batch ๋ชจ๋“ˆ์ด commerce-api์—๋งŒ ์กด์žฌํ•˜๋Š” Product ๋„๋ฉ”์ธ(DB, Service)์„ ์ง์ ‘ ์˜์กดํ•˜์ง€ ์•Š๋„๋ก ์„ค๊ณ„ํ•˜๋Š” ๋ฐ ๋งŽ์€ ๊ณ ๋ฏผ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ์ฃผ๋ฌธ ๋ฐœ์ƒ ์‹œ ๋ฐœํ–‰๋˜๋Š” ProductStockEvent์— ์ƒํ’ˆ๋ช…๊ณผ ๊ฐ€๊ฒฉ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋Š” Event Enrichment ํŒจํ„ด์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

[๊ด€๋ จ ์งˆ๋ฌธ]

ํ•ด๋‹น ์„ค๊ณ„ ๊ณผ์ •์—์„œ Event Enrichment๋ฅผ ํ†ตํ•ด ๋ฐฐ์น˜์˜ ๋…๋ฆฝ์„ฑ์„ ํ™•๋ณดํ–ˆ์œผ๋‚˜, ์ƒํ’ˆ๋ช…์ด๋‚˜ ๊ฐ€๊ฒฉ์ด ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ณ€๊ฒฝ๋˜๋Š” ๋„๋ฉ”์ธ์ผ ๊ฒฝ์šฐ ์ด๋ฒคํŠธ ๋ฉ”์‹œ์ง€์˜ ํฌ๊ธฐ๊ฐ€ ์ปค์ง€๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ ๊ธฐ๊ฐ„์ด ๊ธธ์–ด์งˆ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์€๋ฐ ์‹ค๋ฌด์—์„œ๋Š” ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ์ด๋ฅผ ํ•ด๊ฒฐ์„ ํ•˜๋Š”์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค.

โš ๏ธ ๋ฆฌ๋ทฐ ๋ฒ”์œ„ ์•ˆ๋‚ด
๋ฆฌ๋ฒ ์ด์Šค ๊ณผ์ •์—์„œ ์ด์ „ ์ฃผ์ฐจ๋“ค์˜ ์ปค๋ฐ‹๋“ค์ด ํฌํ•จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ๊ณผ์ œ(์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น)์˜ ์‹ค์งˆ์ ์ธ ๋ฆฌ๋ทฐ๋Š” ์•„๋ž˜ ์ปค๋ฐ‹๋ถ€ํ„ฐ ํ™•์ธํ•ด ์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์‹œ์ž‘ ์ปค๋ฐ‹: ee16413 ([refactor: ๋™์  ๋žญํ‚น ํ‚ค ์ƒ์„ฑ์„ ์œ„ํ•œ RankingKeyGenerator ๋ถ„๋ฆฌ...])

์ฃผ์š” ๊ตฌํ˜„ ๋ฒ”์œ„: ์ดํ›„ ์ง„ํ–‰๋œ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋ฐฐ์น˜ Job ๊ตฌ์„ฑ, MV ์„ค๊ณ„, API ํ™•์žฅ ๊ด€๋ จ ์ปค๋ฐ‹๋“ค์ž…๋‹ˆ๋‹ค.

โœ… Checklist

๐Ÿงฑ Spring Batch

  • Spring Batch Job ์„ ์ž‘์„ฑํ•˜๊ณ , ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.
  • Chunk Oriented Processing (Reader/Processor/Writer or Tasklet) ๊ธฐ๋ฐ˜์˜ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค.
  • ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  Materialized View ์˜ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์žฌํ–ˆ๋‹ค.

๐Ÿงฉ Ranking API

  • API ๊ฐ€ ์ผ๊ฐ„, ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น์„ ์ œ๊ณตํ•˜๋ฉฐ ์กฐํšŒํ•ด์•ผ ํ•˜๋Š” ํ˜•ํƒœ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋žญํ‚น์„ ์ œ๊ณตํ•œ๋‹ค.

Summary by CodeRabbit

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

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

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

    • ์ƒํ’ˆ ์ •๋ณด ํ‘œ์‹œ: ์žฌ๊ณ  ์ƒํƒœ๋ฅผ ํ’ˆ์ ˆ/ํŒ๋งค์ค‘์œผ๋กœ ๋” ๋ช…ํ™•ํžˆ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
    • API ์‘๋‹ต ํ™•์žฅ: ์ƒํ’ˆ ์ด๋ฆ„ ์ •๋ณด๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๋ฌธ์„œ

    • ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์—…๋ฐ์ดํŠธ

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

JVHE and others added 30 commits October 28, 2025 17:01
ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ )
์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค.
ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.
ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.
๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.
ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.

ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
`X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค.
์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค.
์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
- PaymentRepository ๋ฐ ๊ด€๋ จ ๋ฉ”์„œ๋“œ์—์„œ LocalDateTime ๋Œ€์‹  ZonedDateTime ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •
- ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ์˜ ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ๋ฐฉ์‹์„ Propagation.REQUIRES_NEW๋กœ ๊ฐœ์„ 
- ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ์†Œ์Šค ์„ค์ • ํŒŒ์ผ(jpa.yml, redis.yml) ์‚ญ์ œ
- KafkaConfig ํŒจํ‚ค์ง€๋ช… ์˜คํƒ€ ์ˆ˜์ • (confg โ†’ config)
- build.gradle.kts์— kafka ๋ชจ๋“ˆ ์˜์กด์„ฑ ์ถ”๊ฐ€ ๋ฐ application.yml ์„ค์ • ๋ฐ˜์˜
- `OutboxEvent` ์—”ํ‹ฐํ‹ฐ ๋ฐ JPA ๋งคํ•‘ ์ถ”๊ฐ€๋กœ Outbox ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ๊ด€๋ฆฌ ๊ตฌํ˜„
- `OutboxService`๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ ์ €์žฅ ๋ฐ ์ง๋ ฌํ™” ๋กœ์ง ์ฒ˜๋ฆฌ
- `OutboxRepository`๋กœ ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ์กฐํšŒ ๋ฐ ๊ด€๋ฆฌ ๊ฐ€๋Šฅ
- `OutboxEvent` ๊ตฌ์กฐ ํ™•์žฅ: ๊ณ ์œ  `eventId` ํ•„๋“œ ์ถ”๊ฐ€ ๋ฐ ์ €์žฅ ๋กœ์ง ๊ฐœ์„ .
- `LikeCreatedEvent`์˜ ์ƒ์„ฑ ๋ฉ”์„œ๋“œ(`of`) ๋„์ž…์œผ๋กœ eventId ์ž๋™ ์ƒ์„ฑ ๋ฐ ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€.
- Kafka ์—ฐ๋™์„ ์œ„ํ•œ `LikeEventOutboxHandler` ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€.
- `OutboxService`์— ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์™„๋ฃŒ ์ƒํƒœ ๊ด€๋ฆฌ ๋กœ์ง(`markPublished`) ์ถ”๊ฐ€.
- `LikeFacade` ๋ฐ `LikeCountAggregateListener`์—์„œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋กœ์ง ํ™•์žฅ ๋ฐ ๊ด€๋ จ ํด๋ž˜์Šค ํŒจํ‚ค์ง€ ๊ตฌ์กฐ ์ •๋ฆฌ.
- `LikeEventConsumer` ์ถ”๊ฐ€๋กœ Kafka ์ด๋ฒคํŠธ ์ˆ˜์‹  ๋ฐ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ตฌํ˜„.
- `ProductMetricsService`๋ฅผ ํ†ตํ•ด ์ข‹์•„์š” ์ˆ˜ ์—…๋ฐ์ดํŠธ ์ฒ˜๋ฆฌ.
- `ProductMetrics` ๋ฐ `EventHandled` ์—”ํ‹ฐํ‹ฐ ์ถ”๊ฐ€๋กœ ์ด๋ฒคํŠธ ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€ ๋ฐ ๋ฉ”ํŠธ๋ฆญ ์ €์žฅ.
- ๊ด€๋ จ JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค (`ProductMetricsRepository`, `EventHandledRepository`) ์ถ”๊ฐ€.
- ์ด์— ๋”ฐ๋ฅธ ์ปจ์Šˆ๋จธ ๋ฐ ํผ๋ธ”๋ฆฌ์…” ๊ตฌํ˜„์ฒด ์ˆ˜์ •
- OrderCreatedEvent, LikeEventOutboxHandler, LikeCountAggregateListener ํŒจํ‚ค์ง€ ๊ตฌ์กฐ ์ •๋ฆฌ
- ์ƒํ’ˆ ์กฐํšŒ ์•ก์…˜์„ ๊ธฐ๋กํ•˜๊ธฐ ์œ„ํ•œ ProductViewEvent ๋„์ž…
- ProductViewEvent๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  Kafka์™€ ์—ฐ๋™ํ•˜๋Š” ProductEventOutboxHandler ๊ตฌํ˜„
- ์ƒํ’ˆ ์ •๋ณด ์กฐํšŒ ์‹œ ProductFacade์—์„œ ProductViewEvent ๋ฐœํ–‰ํ•˜๋„๋ก ์ˆ˜์ •
- ์ด๋ฒคํŠธ ์ €์žฅ, ์ค‘๋ณต ์ œ๊ฑฐ, Kafka ๋ฐœํ–‰ ๋กœ์ง ํฌํ•จ
- `OrderEventOutboxHandler` ์ถ”๊ฐ€๋กœ ์ฃผ๋ฌธ ์ƒ์„ฑ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ฐ Kafka ๋ฐœํ–‰ ๋กœ์ง ๊ตฌํ˜„.
- `OrderSalesAggregateListener`๋ฅผ ํ†ตํ•ด ํŒ๋งค ์ง‘๊ณ„ Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฐ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์ง€์›.
- `OrderCreatedEvent` ํ™•์žฅ: ๊ณ ์œ  `eventId` ์ถ”๊ฐ€ ๋ฐ ์ฃผ๋ฌธ ํ’ˆ๋ชฉ ์ „๋‹ฌ ์ •๋ณด ๊ตฌ์กฐ ๋ณ€๊ฒฝ.
- ์‹ ๊ทœ `SalesCountEvent` ์ถ”๊ฐ€๋กœ Kafka ๊ธฐ๋ฐ˜ ํŒ๋งค ๋ฉ”ํŠธ๋ฆญ ๊ด€๋ฆฌ ๊ฐ€๋Šฅ.
- `OrderFacade`, `PointPaymentEventListener`, `PgPaymentEventListener` ๋“ฑ ๊ด€๋ จ ํด๋ž˜์Šค ์ˆ˜์ • ๋ฐ ํŒจํ‚ค์ง€ ์ •๋ฆฌ.
- `LikeEventConsumer`๋ฅผ `MetricsEventConsumer`๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์ƒํ’ˆ ์ง€ํ‘œ(์ข‹์•„์š”, ์กฐํšŒ์ˆ˜, ํŒ๋งค๋Ÿ‰) ์ฒ˜๋ฆฌ ์ง€์›.
- `ProductMetricsService` ํ™•์žฅ: ์กฐํšŒ์ˆ˜(`ProductViewEvent`), ํŒ๋งค๋Ÿ‰(`SalesCountEvent`) ๋ฉ”ํŠธ๋ฆญ ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€.
- `ProductMetrics` ์—”ํ‹ฐํ‹ฐ์— ์กฐํšŒ์ˆ˜์™€ ํŒ๋งค๋Ÿ‰ ์†์„ฑ ๋ฐ ๊ด€๋ จ ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€.
- `OutboxEvent`, `ProductMetrics`, `EventHandled` ์—”ํ‹ฐํ‹ฐ์— @table ์• ๋„ˆํ…Œ์ด์…˜ ์ถ”๊ฐ€.
- `IdempotencyIntegrationTest` ๊ตฌํ˜„์œผ๋กœ ์ค‘๋ณต ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๊ฒ€์ฆ.
โ€ฆ ๋กœ์ง ๊ฐœ์„ 

- SalesCountEvent๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๊ด€๋ จ ๋กœ์ง์„ ProductStockEvent๋กœ ์ „๋ฉด ๋Œ€์ฒด
- MetricsEventConsumer์—์„œ ProductStockEvent๋ฅผ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์ˆ˜์ •
- ProductMetricsService๋ฅผ ํ™•์žฅํ•˜์—ฌ ์žฌ๊ณ  ๊ธฐ๋ฐ˜ ์ง€ํ‘œ ๊ด€๋ฆฌ ๋ฐ ์žฌ๊ณ  ์†Œ์ง„ ์‹œ Redis ์บ์‹œ ์‚ญ์ œ ๋กœ์ง ์ถ”๊ฐ€
- ProductRepository์— findStockById ๋ฉ”์„œ๋“œ, ProductService์— getStock ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
- Redis ์บ์‹œ์—์„œ ํŽ˜์ด์ง• ์‘๋‹ต์„ ๊ฐ์‹ธ๊ธฐ ์œ„ํ•œ PageWrapper ๋„์ž…
- ํŒจํ„ด ๊ธฐ๋ฐ˜ ์บ์‹œ ์‚ญ์ œ๋ฅผ ์ง€์›ํ•˜๋„๋ก RedisCacheHandler ์ด๋™ ๋ฐ ๊ธฐ๋Šฅ ํ™•์žฅ
- `OutboxEvent`์— `OutboxStatus` ๋ฐ `retryCount` ํ•„๋“œ ์ถ”๊ฐ€๋กœ ์ƒํƒœ ๊ด€๋ฆฌ ์ง€์›.
- ์‹คํŒจ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ `markFailed` ๋ฉ”์„œ๋“œ ๊ตฌํ˜„.
- `OutboxRelay` ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€๋กœ ์‹คํŒจ ์ด๋ฒคํŠธ ์žฌ๋ฐœํ–‰ ์Šค์ผ€์ค„๋ง ๋ฐ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ตฌํ˜„.
- ๊ด€๋ จ ์„œ๋น„์Šค ๋ฐ ํ•ธ๋“ค๋Ÿฌ์— ์‹คํŒจ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง ํ†ตํ•ฉ (`markFailed` ํ˜ธ์ถœ ์ถ”๊ฐ€).
- Kafka ์„ค์ •์— enable.idempotence=true ๋ฐ acks=all ํ”„๋กœ๋“€์„œ ์˜ต์…˜ ์ถ”๊ฐ€
- OutboxService์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” import ์ œ๊ฑฐ
- ๋ฏธ์‚ฌ์šฉ ํด๋ž˜์Šค(DemoKafkaConsumer) ์‚ญ์ œ๋กœ ์ฝ”๋“œ ์ •๋ฆฌ.
- ๊ด€๋ จ Kafka ๋ฆฌ์Šค๋„ˆ ๋ฐ ์˜์กด์„ฑ ์ œ๊ฑฐ.
- ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ์˜ ์™ธ๋ถ€ ์„œ๋น„์Šค ์˜์กด์„ฑ ์ œ๊ฑฐ (Decoupling)
- ์ด๋ฒคํŠธ ๊ฐ์ฒด๊ฐ€ ๋ฐœ์ƒ ์‹œ์ ์˜ ์ƒํƒœ(์žฌ๊ณ )๋ฅผ ์˜จ์ „ํžˆ ํ‘œํ˜„ํ•˜๋„๋ก ์ˆ˜์ •
- ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์‹œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ ๊ฐ€๋Šฅ์„ฑ ๋ฐฉ์ง€
- ProductStockEvent์— ๊ฐ€๊ฒฉ(price) ํ•„๋“œ ์ถ”๊ฐ€๋กœ ์ด๋ฒคํŠธ ์ •๋ณด ํ™•์žฅ
- OrderCreatedEvent์—์„œ ์ƒํ’ˆ๊ณผ ๊ฐ€๊ฒฉ ๋ฐ์ดํ„ฐ๋ฅผ ํ•จ๊ป˜ ๋งคํ•‘ํ•˜๋„๋ก ์ˆ˜์ •
- OrderSalesAggregateListener์—์„œ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์‹œ ๊ฐ€๊ฒฉ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•˜๋„๋ก ๊ฐœ์„ 
- ์ด๋ฒคํŠธ ํƒ€์ž„์Šคํƒฌํ”„ ํ•„๋“œ๋ฅผ `LocalDateTime`์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์ด๋ฒคํŠธ ์ƒ์„ฑ ๋กœ์ง ์—…๋ฐ์ดํŠธ
- `RankingService` ์ถ”๊ฐ€๋กœ ์ƒํ’ˆ ์ ์ˆ˜ ๊ด€๋ฆฌ ๋กœ์ง ๊ตฌํ˜„ (์กฐํšŒ์ˆ˜, ์ข‹์•„์š”, ํŒ๋งค๋Ÿ‰ ๊ฐ€์ค‘์น˜ ์ ์šฉ)
- MetricsEventConsumer ํ™•์žฅ: ๋žญํ‚น ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ Redis ์—…๋ฐ์ดํŠธ ์ง€์›
- Kafka ๋ฆฌ์Šค๋„ˆ ๋ฐ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„ , ์ด๋ฒคํŠธ๋ณ„ ์ ์ˆ˜ ์—…๋ฐ์ดํŠธ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€
- OutboxHandler ์ˆ˜์ •: ์ด๋ฒคํŠธ ํƒ€์ž… ๋ณ€๊ฒฝ ๋ฐ ์นดํƒˆ๋กœ๊ทธ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์ •๋ฆฌ
- `RankingFacade` ๋ฐ `RankingService` ์ถ”๊ฐ€๋กœ ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ ๋กœ์ง ๊ตฌํ˜„
- Redis ๊ธฐ๋ฐ˜ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋ฐ ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด ๋งคํ•‘ ์ง€์›
- `RankingV1Controller`์™€ `RankingV1ApiSpec` ์ถ”๊ฐ€๋กœ REST API ์ œ๊ณต
- `RankingV1Dto`์™€ `RankingInfo` ๊ตฌํ˜„์œผ๋กœ ์‘๋‹ต ๋ฐ์ดํ„ฐ ๊ตฌ์กฐํ™”
- `RankingService` ๋ฉ”์„œ๋“œ `getProductRank` ๊ตฌํ˜„์œผ๋กœ Redis ๊ธฐ๋ฐ˜ ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ ์ง€์›
- ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ํ˜„์žฌ ๋žญํ‚น ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋„๋ก `ProductInfo` ๋ฐ `ProductFacade` ์ˆ˜์ •
- ๊ธฐ์กด `RankingService` ํŒจํ‚ค์ง€ ์œ„์น˜๋ฅผ `infrastructure`๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๊ด€๋ฆฌ ๊ฐœ์„ 
- `carryOverRanking` ๋ฉ”์„œ๋“œ๋ฅผ `RankingService`์— ์ถ”๊ฐ€ํ•˜์—ฌ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ด์›” ์ง€์›
- ์ด์›” ์ž‘์—… ์ž๋™ํ™”๋ฅผ ์œ„ํ•œ `RankingScheduler` ๊ตฌํ˜„ ๋ฐ Cron ์Šค์ผ€์ค„ ์„ค์ •
- Redis ์—ฐ์‚ฐ์„ ํ™œ์šฉํ•œ ์ด์›” ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋ฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ • ๋กœ์ง ์ถ”๊ฐ€
โ€ฆ์Šค์ผ€์ค„๋Ÿฌ ์‚ฌ์šฉ์„ ์œ„ํ•œ RankingService ๊ฐœ์„ 

- ๋™์ ์ด๊ณ  ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋žญํ‚น ํ‚ค ์ƒ์„ฑ์„ ์œ„ํ•ด RankingKeyGenerator ์ธํ„ฐํŽ˜์ด์Šค์™€ RedisRankingKeyGenerator ๊ตฌํ˜„์ฒด ๋„์ž…
- ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ์„ ์œ„ํ•ด RankingService์—์„œ RankingKeyGenerator๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ฆฌํŒฉํ† ๋ง
- commerce-api์— ์žˆ๋˜ ์ค‘๋ณต carryOverRanking ๋กœ์ง ์ œ๊ฑฐ ํ›„ commerce-streamer ๋ชจ๋“ˆ๋กœ ํ†ตํ•ฉ
- ๋ฆฌํŒฉํ† ๋ง๋œ RankingService๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก RankingScheduler ์ˆ˜์ •
- `ProductStockEvent`์™€ `OrderCreatedEvent`์˜ ์ƒํ’ˆ๋ช…(`productName`) ํ•„๋“œ ์ถ”๊ฐ€๋กœ ์ด๋ฒคํŠธ ์ •๋ณด๋ฅผ ํ™•์žฅ
- ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ ๋งคํ•‘์„ ์œ„ํ•œ `RankingInfo` ์ƒ์„ฑ ๋ฉ”์„œ๋“œ ํ™•์žฅ (์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ๋žญํ‚น ์ง€์›)
- `productName`, `price`, `isSoldOut` ํ•„๋“œ ์ถ”๊ฐ€๋กœ ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ํ™•์žฅ
- `updateProductSnapshot` ๋ฉ”์„œ๋“œ ๊ตฌํ˜„์œผ๋กœ ์ƒํ’ˆ ์ƒํƒœ ๋™๊ธฐํ™” ์ง€์›
- ํŒจํ‚ค์ง€ ๊ตฌ์กฐ ๊ฐœ์„ : `ProductMetrics`๋ฅผ `metrics`์—์„œ ์ตœ์ƒ์œ„ `domain`์œผ๋กœ ์ด๋™
- `ProductMetricsService`์—์„œ ์ƒํ’ˆ ์ •๋ณด ์—…๋ฐ์ดํŠธ ๋กœ์ง ํ†ตํ•ฉ ๋ฐ Redis ์บ์‹œ ์ฒ˜๋ฆฌ ๊ฐ•ํ™”
- RankingService ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ RankingServiceImpl ์ถ”๊ฐ€
- ์ฃผ๊ฐ„ยท์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ๋ฅผ ์œ„ํ•œ getWeeklyRankings, getMonthlyRankings ๋ฉ”์„œ๋“œ ๊ตฌํ˜„
- ์ž…๋ ฅ๊ฐ’์— ๋”ฐ๋ผ ๋žญํ‚น ํƒ€์ž…์„ ๋™์ ์œผ๋กœ ๋ถ„๊ธฐํ•˜๋„๋ก RankingFacade ๊ฐœ์„ 
- DAILY, WEEKLY, MONTHLY ๋žญํ‚น ํƒ€์ž…์„ ์ง€์›ํ•˜๋„๋ก RankingV1ApiSpec ๋ฐ RankingV1Controller ํ™•์žฅ
- ์ฃผ๊ฐ„ ๋žญํ‚น ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ(WeeklyRankingMV, WeeklyRankingWork) ์ถ”๊ฐ€
- WeeklyRankingJobConfig ๋ฐ RankingChunkConfig๋กœ ๋ฐฐ์น˜ ์žก ๊ตฌ์„ฑ
- WeeklyRankingProcessor ๋ฐ Tasklet์„ ํ†ตํ•ด ์ ์ˆ˜ ๊ณ„์‚ฐ, ๋ฐ์ดํ„ฐ ์ค€๋น„ ๋ฐ ์Šค์™‘ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
- ์Šค์ผ€์ค„๋ง์„ ํ†ตํ•ด ๋ฐฐ์น˜ ์žก ์ž๋™ ์‹คํ–‰ (๋งค์ฃผ ์›”์š”์ผ 2์‹œ ์„ค์ •)
- MonthlyRankingJob ์Šค์ผˆ๋ ˆํ†ค ์ถ”๊ฐ€, ํ–ฅํ›„ ํ™•์žฅ ๊ณ ๋ ค
- ์›”๊ฐ„ ๋žญํ‚น ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ(MonthlyRankingMV) ๋ฐ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ถ”๊ฐ€
- MonthlyRankingJobConfig์™€ Tasklet์œผ๋กœ ๋ฐฐ์น˜ ์žก ๊ตฌ์„ฑ
- ์›”๊ฐ„ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ค€๋น„๋ฅผ ์œ„ํ•œ Processing ๋กœ์ง ๊ตฌํ˜„
- Redis๋ฅผ ํ™œ์šฉํ•œ ์Šค๋ƒ…์ƒท ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋ฐ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์Šค์™‘ ๋กœ์ง ์ถ”๊ฐ€
@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

Walkthrough

GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์ˆœ์œ„ ์‹œ์Šคํ…œ์„ ์žฌ๊ตฌ์กฐํ™”ํ•˜๋ฉฐ ์ƒˆ๋กœ์šด commerce-batch ๋ชจ๋“ˆ์„ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค. ์ฃผ๊ฐ„/์›”๊ฐ„ ์ˆœ์œ„ ๋ฐฐ์น˜ ์ž‘์—…, ์ฃผ๊ธฐ์  ์ˆœ์œ„ ์Šค์ผ€์ค„๋Ÿฌ, ๊ด€๋ จ JPA ์—”ํ‹ฐํ‹ฐ ๋ฐ ์ €์žฅ์†Œ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ OrderCreatedEvent์™€ ProductStockEvent์— productName ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  API ์‘๋‹ต์—์„œ stock์„ isSoldOut ๋ถˆ๋ฆฐ ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

Changes

์ฝ”ํ˜ธํŠธ / ํŒŒ์ผ ๋ณ€๊ฒฝ ์š”์•ฝ
GitHub Actions ๋ฐ ๋นŒ๋“œ
.github/workflows/main.yml, settings.gradle.kts
PR Agent ์›Œํฌํ”Œ๋กœ์šฐ ์ถ”๊ฐ€ ๋ฐ commerce-batch ๋ชจ๋“ˆ์„ ๋ฉ€ํ‹ฐ๋ชจ๋“ˆ ํ”„๋กœ์ ํŠธ์— ํฌํ•จ
์˜์กด์„ฑ ์—…๋ฐ์ดํŠธ
apps/commerce-api/build.gradle.kts, apps/pg-simulator/src/main/resources/application.yml, apps/commerce-api/src/main/resources/application.yml
resilience4j, Spring Cloud OpenFeign ์˜์กด์„ฑ ์ถ”๊ฐ€; Feign ํด๋ผ์ด์–ธํŠธ ํƒ€์ž„์•„์›ƒ ์„ค์ • ์ถ”๊ฐ€; ์ผ๋ถ€ ์žฌ์‹œ๋„ ์„ค์ • ์ œ๊ฑฐ
์ˆœ์œ„ API ๋ฐ ์„œ๋น„์Šค ์ธํ„ฐํŽ˜์ด์Šค ์žฌ๊ตฌ์กฐํ™”
apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/RankingService.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/RankingServiceImpl.java, apps/commerce-api/src/main/java/com/loopers/application/rank/RankingFacade.java, apps/commerce-api/src/main/java/com/loopers/application/rank/RankingInfo.java
RankingService๋ฅผ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ „ํ™˜; ์ฃผ๊ฐ„/์›”๊ฐ„ ์ˆœ์œ„ ์กฐํšŒ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€; RankingInfo์˜ stock์„ isSoldOut ๋ถˆ๋ฆฐ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝ; ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
์ˆœ์œ„ API ์—”๋“œํฌ์ธํŠธ
apps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1ApiSpec.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1Controller.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1Dto.java
getRankings ๋ฉ”์„œ๋“œ์— type ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€; RankingResponse์˜ stock ํ•„๋“œ๋ฅผ isSoldOut์œผ๋กœ ๋ณ€๊ฒฝ
์ด๋ฒคํŠธ ์—…๋ฐ์ดํŠธ
apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderCreatedEvent.java, apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderSalesAggregateListener.java, modules/kafka/src/main/java/com/loopers/event/ProductStockEvent.java
OrderCreatedEvent.OrderItemInfo์— productName ํ•„๋“œ ์ถ”๊ฐ€; ProductStockEvent์— productName ํ•„๋“œ ์ถ”๊ฐ€
๋ฐฐ์น˜ ๋ชจ๋“ˆ ์„ค์ • ๋ฐ ์ง„์ž…์ 
apps/commerce-batch/build.gradle.kts, apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java, apps/commerce-batch/src/main/resources/application.yml
์ƒˆ๋กœ์šด commerce-batch ๋ชจ๋“ˆ ์ƒ์„ฑ; ๋ฐฐ์น˜ ํ”„๋ ˆ์ž„์›Œํฌ ์˜์กด์„ฑ ์ถ”๊ฐ€; ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ • ์ถ”๊ฐ€
๋ฐฐ์น˜ ์ž‘์—… ์„ค์ • - ์ฃผ๊ฐ„ ์ˆœ์œ„
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java, apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/weekly/WeeklyRankingProcessor.java, apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/RankingChunkConfig.java
์ฃผ๊ฐ„ ์ˆœ์œ„ ๋ฐฐ์น˜ ์ž‘์—… ๊ตฌ์„ฑ; ๊ฐ€์ค‘์น˜ ์ ์ˆ˜ ๊ณ„์‚ฐ ํ”„๋กœ์„ธ์„œ; ์ฒญํฌ ๊ธฐ๋ฐ˜ ์ฝ๊ธฐ/์“ฐ๊ธฐ ์„ค์ •
๋ฐฐ์น˜ ์ž‘์—… ์„ค์ • - ์›”๊ฐ„ ์ˆœ์œ„
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java, apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/monthly/MonthlyRankingTableSwapTasklet.java
์›”๊ฐ„ ์ˆœ์œ„ ๋ฐฐ์น˜ ์ž‘์—… ๊ตฌ์„ฑ; ์›”๊ฐ„ ์ˆœ์œ„ ๊ณ„์‚ฐ ๋ฐ ํ…Œ์ด๋ธ” ์Šค์™‘ ๋กœ์ง
๋ฐฐ์น˜ ์ž‘์—… ์„ค์ • - ๋ฐ๋ชจ
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java, apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java
๋ฐ๋ชจ ๋ฐฐ์น˜ ์ž‘์—… ๊ตฌ์„ฑ; ์กฐ๊ฑด๋ถ€ ํ™œ์„ฑํ™” ์„ค์ •
๋ฐฐ์น˜ ํƒ€์Šคํฌ๋ › ๋ฐ ๋ฆฌ์Šค๋„ˆ
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingPrepareTasklet.java, apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingTableSwapTasklet.java, apps/commerce-batch/src/main/java/com/loopers/batch/listener/...
์ˆœ์œ„ ์ค€๋น„/ํ…Œ์ด๋ธ” ์Šค์™‘ ํƒœ์Šคํด๋ ›; ์ž‘์—…/๋‹จ๊ณ„/์ฒญํฌ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/scheduler/RankingScheduler.java
์ฃผ๊ฐ„/์›”๊ฐ„ ์ˆœ์œ„ ๋ฐฐ์น˜ ์ž‘์—… ์Šค์ผ€์ค„๋ง (์›”์š”์ผ 02:00, ์›” 1์ผ 03:00)
๋ฐฐ์น˜ ํ…Œ์ŠคํŠธ
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java, apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java
์ปจํ…์ŠคํŠธ ๋กœ๋“œ ํ…Œ์ŠคํŠธ ๋ฐ ๋ฐ๋ชจ ๋ฐฐ์น˜ ์ž‘์—… E2E ํ…Œ์ŠคํŠธ
์ŠคํŠธ๋ฆฌ๋จธ ์ˆœ์œ„ ํ†ตํ•ฉ
apps/commerce-streamer/src/main/java/com/loopers/application/rank/RankingScheduler.java, apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingService.java, apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingKeyGenerator.java, apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RedisRankingKeyGenerator.java
์ˆœ์œ„ ์บ๋ฆฌ์˜ค๋ฒ„ ์Šค์ผ€์ค„๋Ÿฌ ์ถ”๊ฐ€; ์ˆœ์œ„ ํ‚ค ์ƒ์„ฑ ์ธํ„ฐํŽ˜์ด์Šคํ™”; ์ผ์ผ ์ˆœ์œ„ ํ•ฉ์‚ฐ ๋กœ์ง ์ถ”๊ฐ€
์ œํ’ˆ ๋ฉ”ํŠธ๋ฆญ ๋ฐ ์ˆœ์œ„ ์—”ํ‹ฐํ‹ฐ
modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.java, modules/jpa/src/main/java/com/loopers/domain/rank/weekly/..., modules/jpa/src/main/java/com/loopers/domain/rank/monthly/...
ProductMetrics ์—”ํ‹ฐํ‹ฐ ์ถ”๊ฐ€; WeeklyRankingMV, WeeklyRankingWork, MonthlyRankingMV ์—”ํ‹ฐํ‹ฐ ์ถ”๊ฐ€; ๊ด€๋ จ ์ €์žฅ์†Œ ์ถ”๊ฐ€
๊ธฐํƒ€
README.md, apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java, apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.java
์ˆœ์œ„ ๊ด€๋ จ ๋ณ€๊ฒฝ์‚ฌํ•ญ์— ๋”ฐ๋ฅธ ๋งˆ์ด๋„ˆ ์—…๋ฐ์ดํŠธ

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as RankingScheduler
    participant JobLauncher as JobLauncher
    participant PrepareStep as Prepare Step<br/>(RankingPrepareTasklet)
    participant CalcStep as Calculation Step<br/>(Chunk Processing)
    participant SwapStep as Table Swap Step<br/>(RankingTableSwapTasklet)
    participant Repo as Repository<br/>(WeeklyRankingWork/MV)
    participant Redis as Redis
    participant DB as Database<br/>(ProductMetrics)

    rect rgba(100, 150, 200, 0.2)
    Note over Scheduler,DB: Weekly Ranking Job Flow
    Scheduler->>JobLauncher: runWeeklyRankingJob<br/>(with requestDate param)
    JobLauncher->>PrepareStep: Execute Prepare Step
    PrepareStep->>Repo: deleteAllInBatch()<br/>(WeeklyRankingWork)
    Repo-->>PrepareStep: Cleared
    
    PrepareStep->>CalcStep: Step Completed
    CalcStep->>DB: Read ProductMetrics<br/>(past 7 days)
    DB-->>CalcStep: Metrics List
    CalcStep->>CalcStep: Process: Calculate<br/>weighted score
    CalcStep->>Repo: Write WeeklyRankingWork<br/>(chunks of 100)
    Repo-->>CalcStep: Saved
    
    CalcStep->>SwapStep: Step Completed
    SwapStep->>Repo: findAll()<br/>(WeeklyRankingWork)
    Repo-->>SwapStep: Work Records
    SwapStep->>Repo: saveAll()<br/>(WeeklyRankingMV)
    Repo-->>SwapStep: MV Created
    SwapStep-->>JobLauncher: Job Complete
    end

    rect rgba(150, 100, 200, 0.2)
    Note over Scheduler,DB: Daily Ranking Carry-Over
    Scheduler->>RankingScheduler: scheduleRankingCarryOver<br/>(23:50 daily)
    RankingScheduler->>Redis: ZUNIONSTORE<br/>(today โ†’ tomorrow<br/>with weight 0.1)
    Redis-->>RankingScheduler: Merged Ranking
    RankingScheduler->>Redis: EXPIRE<br/>(tomorrow key)
    Redis-->>RankingScheduler: TTL Set
    end
Loading

Estimated code review effort

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

Possibly related PRs

  • [volume-9] Product Ranking with Redisย #221: ๋™์ผํ•œ ์ œํ’ˆ ์ˆœ์œ„ ๊ธฐ๋Šฅ ์„ธํŠธ(์ˆœ์œ„ ๋„๋ฉ”์ธ ์„œ๋น„์Šค, ์Šค์ผ€์ค„๋Ÿฌ, RankingService/RankingKeyGenerator, JPA/Redis MV ํƒ€์ž…)๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ์ฝ”๋“œ ๋ ˆ๋ฒจ์—์„œ ์ง์ ‘ ๊ด€๋ จ๋จ.
  • [volume-6] ์™ธ๋ถ€ ์‹œ์Šคํ…œ ์žฅ์•  ๋ฐ ์ง€์—ฐ ๋Œ€์‘ ย #154: OrderCreatedEvent, ProductStockEvent ๋“ฑ ๋™์ผํ•œ ์ด๋ฒคํŠธ ํŒŒ์ดํ”„๋ผ์ธ ๋ฐ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ์œ ํ˜• ๋ณ€๊ฒฝ์œผ๋กœ ๋ฉ”์‹œ์ง•/์•„์›ƒ๋ฐ•์Šค ์ฝ”๋“œ ๊ฒฝ๋กœ์—์„œ ๊ด€๋ จ๋จ.
  • [volume-9] Product Ranking with Redis ย #220: Redis ZSET ํ‚ค ์ƒ์„ฑ, RankingFacade/์„œ๋น„์Šค, DTO ๋ฐ API ์—”๋“œํฌ์ธํŠธ, commerce-streamer ์ˆœ์œ„ ํ†ตํ•ฉ ๋“ฑ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ์ˆ˜์ •ํ•˜์—ฌ ์ง์ ‘ ๊ด€๋ จ๋จ.

Suggested labels

enhancement

Poem

๐Ÿฐ ์ˆœ์œ„์˜ ๋ฌผ๊ฒฐ์ด ํ๋ฅด๊ณ  ํ๋ฅด๋‹ˆ,
๋ฐฐ์น˜๋กœ ์ฃผ๊ฐ„ยท์›”๊ฐ„ ๊ณ„์‚ฐํ•˜์—ฌ,
๋งค ์‹œ๊ฐ„ ์บ๋ฆฌ์˜ค๋ฒ„ ๊ฒน๊ฒน์ด ์Œ“์•„,
์ŠคํŠธ๋ฆผ์— ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ํ˜๋Ÿฌ๋‚ด๊ณ ,
์•„, ํ† ๋ผ ๊ฐ™์€ ๋น ๋ฅธ ์ˆœ์œ„ ์—…๋ฐ์ดํŠธ! ๐Ÿ’จ

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 2.86% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check โ“ Inconclusive ์ œ๋ชฉ '[volume-10] Collect, Stack, Zip'์€ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌ์ถ•์ด๋ผ๋Š” ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๊ตฌ์ฒด์ ์œผ๋กœ ์„ค๋ช…ํ•˜์ง€ ๋ชปํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจํ˜ธํ•œ ํ‘œํ˜„์œผ๋กœ ๋™๋ฃŒ๊ฐ€ ๋ณ€๊ฒฝ์‚ฌํ•ญ์˜ ํ•ต์‹ฌ์„ ์‰ฝ๊ฒŒ ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ์ œ๋ชฉ์„ 'Add weekly/monthly ranking batch jobs with materialized views' ๊ฐ™์€ ๊ตฌ์ฒด์ ์ธ ํ‘œํ˜„์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ „๋‹ฌํ•˜์„ธ์š”.
โœ… Passed checks (1 passed)
Check name Status Explanation
Description check โœ… Passed PR ์„ค๋ช…์€ Summary, Review Points, Checklist ์„น์…˜์„ ํฌํ•จํ•˜์—ฌ ํ•„์ˆ˜ ํ…œํ”Œ๋ฆฟ ๊ตฌ์กฐ๋ฅผ ๋Œ€๋ถ€๋ถ„ ์ถฉ์กฑํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ƒ์„ธํ•œ ๊ตฌํ˜„ ๋‚ด์šฉ๊ณผ ์„ค๊ณ„ ๊ณ ๋ฏผ์„ ์ž˜ ์„ค๋ช…ํ–ˆ์Šต๋‹ˆ๋‹ค.
โœจ Finishing touches
  • ๐Ÿ“ Generate docstrings

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

๐Ÿงน Nitpick comments (20)
apps/commerce-api/build.gradle.kts (1)

54-54: ์ƒˆ ์˜์กด์„ฑ์€ ์œ ํšจํ•˜์ง€๋งŒ ๋ฐฐ์น˜๋ฅผ ์กฐ์ •ํ•˜์„ธ์š”.

spring-cloud-starter-circuitbreaker-resilience4j ์˜์กด์„ฑ ์ถ”๊ฐ€๋Š” ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. Resilience4j์™€์˜ Spring Cloud CircuitBreaker ํ†ตํ•ฉ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์ค‘๋ณต ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•œ ํ›„, ์ด ์„ ์–ธ์„ 26๋ฒˆ ๋ผ์ธ ๊ทผ์ฒ˜์˜ ๋‹ค๋ฅธ Spring Cloud ์˜์กด์„ฑ๋“ค๊ณผ ํ•จ๊ป˜ ๋ฐฐ์น˜ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ๋ฐฐ์น˜

์ค‘๋ณต ์ œ๊ฑฐ ํ›„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฐฐ์น˜:

 //Spring Cloud OpenFeign
 implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
+
+//Spring Cloud CircuitBreaker
+implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")
apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingKeyGenerator.java (1)

3-5: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” import ์ œ๊ฑฐ ํ•„์š”

org.springframework.stereotype.Component import๊ฐ€ ์„ ์–ธ๋˜์–ด ์žˆ์ง€๋งŒ ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ตฌํ˜„์ฒด(RedisRankingKeyGenerator)์— @Component๊ฐ€ ์ ์šฉ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ์ด import๋Š” ์ œ๊ฑฐํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
 package com.loopers.domain.rank;
 
-import org.springframework.stereotype.Component;
-
 public interface RankingKeyGenerator {
 
   String generateDailyKey(String date);
 }
apps/commerce-streamer/src/main/java/com/loopers/application/rank/RankingScheduler.java (1)

18-34: ๊ฐ€์ค‘์น˜ ๋งค์ง ๋„˜๋ฒ„๋ฅผ ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”

0.1 ๊ฐ€์ค‘์น˜ ๊ฐ’์ด ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฐ’์„ ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•˜๋ฉด ์˜๋ฏธ๊ฐ€ ๋ช…ํ™•ํ•ด์ง€๊ณ  ์ถ”ํ›„ ์ˆ˜์ •์ด ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, DateTimeFormatter๋Š” ์Šค๋ ˆ๋“œ ์„ธ์ดํ”„ํ•˜๋ฏ€๋กœ ์ •์  ์ƒ์ˆ˜๋กœ ์„ ์–ธํ•˜์—ฌ ๋งค ํ˜ธ์ถœ๋งˆ๋‹ค ์ƒ์„ฑํ•˜์ง€ ์•Š๋„๋ก ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
 @Slf4j
 @Component
 @RequiredArgsConstructor
 public class RankingScheduler {

   private final RankingService rankingService;
+  private static final double CARRY_OVER_WEIGHT = 0.1;
+  private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

   @Scheduled(cron = "0 50 23 * * *")
   public void scheduleRankingCarryOver() {
     LocalDateTime now = LocalDateTime.now();
-    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");

-    String today = now.format(formatter);
-    String tomorrow = now.plusDays(1).format(formatter);
+    String today = now.format(DATE_FORMATTER);
+    String tomorrow = now.plusDays(1).format(DATE_FORMATTER);

     log.info("Starting Ranking Carry-Over: {} -> {}", today, tomorrow);

     try {
-      rankingService.carryOverRanking(today, tomorrow, 0.1);
+      rankingService.carryOverRanking(today, tomorrow, CARRY_OVER_WEIGHT);
       log.info("Ranking Carry-Over completed successfully.");
     } catch (Exception e) {
       log.error("Ranking Carry-Over failed", e);
     }
   }
 }
.github/workflows/main.yml (1)

10-10: ์•ก์…˜ ๋ฒ„์ „์„ ๊ณ ์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

@main ๋ธŒ๋žœ์น˜ ๋Œ€์‹  ํŠน์ • ๋ฆด๋ฆฌ์Šค ํƒœ๊ทธ๋‚˜ ์ปค๋ฐ‹ SHA๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์›Œํฌํ”Œ๋กœ์šฐ์˜ ์•ˆ์ •์„ฑ๊ณผ ์žฌํ˜„์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •
-      - name: PR Agent action step
-        uses: Codium-ai/pr-agent@main
+      - name: PR Agent action step
+        uses: Codium-ai/[email protected]  # ์ตœ์‹  ์•ˆ์ • ๋ฒ„์ „์œผ๋กœ ๊ต์ฒด
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (2)

27-29: ๋กœ๊น… ํ”„๋ ˆ์ž„์›Œํฌ ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค

System.out.println ๋Œ€์‹  Slf4j ๋กœ๊ฑฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ํด๋ž˜์Šค์— ์ด๋ฏธ Lombok์˜ @requiredargsconstructor๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ @slf4j๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  log.info()๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •์•ˆ

ํด๋ž˜์Šค ๋ ˆ๋ฒจ์— @slf4j ์ถ”๊ฐ€:

+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
 @StepScope
 @ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME)
 @RequiredArgsConstructor
 @Component
 public class DemoTasklet implements Tasklet {

execute ๋ฉ”์„œ๋“œ ๋‚ด ๋กœ๊น… ๋ณ€๊ฒฝ:

-        System.out.println("Demo Tasklet ์‹คํ–‰ (์‹คํ–‰ ์ผ์ž : " + requestDate + ")");
+        log.info("Demo Tasklet ์‹คํ–‰ (์‹คํ–‰ ์ผ์ž : {})", requestDate);
         Thread.sleep(1000);
-        System.out.println("Demo Tasklet ์ž‘์—… ์™„๋ฃŒ");
+        log.info("Demo Tasklet ์ž‘์—… ์™„๋ฃŒ");

28-28: Thread.sleep ์‚ฌ์šฉ์— ๋Œ€ํ•œ ๊ฒ€ํ† 

๋ฐ๋ชจ ๋ชฉ์ ์˜ Tasklet์ด์ง€๋งŒ Thread.sleep(1000)์€ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์Šค๋ ˆ๋“œ๋ฅผ ๋ธ”๋กœํ‚นํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ์ž‘์—…์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๋ ค๋Š” ์˜๋„๋ผ๋ฉด ๊ดœ์ฐฎ์ง€๋งŒ, ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์—์„œ๋Š” ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์œผ๋กœ ๋Œ€์ฒดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

43-49: type ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋ฐฉ์–ด์  null ์ฒ˜๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

type.toUpperCase()๋Š” type์ด null์ผ ๊ฒฝ์šฐ NullPointerException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ํ˜„์žฌ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๊ธฐ๋ณธ๊ฐ’์„ ์ œ๊ณตํ•˜์ง€๋งŒ, RankingFacade๋Š” public ์ปดํฌ๋„ŒํŠธ์ด๋ฏ€๋กœ ๋‹ค๋ฅธ ๊ณณ์—์„œ๋„ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋ฐฉ์–ด์  null ์ฒ˜๋ฆฌ ์ œ์•ˆ
 public List<RankingInfo> getRankings(String type, String date, int page, int size) {
-  return switch (type.toUpperCase()) {
+  String normalizedType = (type != null) ? type.toUpperCase() : "DAILY";
+  return switch (normalizedType) {
     case "WEEKLY" -> rankingService.getWeeklyRankings(date, page, size);
     case "MONTHLY" -> rankingService.getMonthlyRankings(date, page, size);
     default -> getTopRankings(date, page, size);
   };
 }

๋˜๋Š” Java Objects ์œ ํ‹ธ๋ฆฌํ‹ฐ ์‚ฌ์šฉ:

+import java.util.Objects;
+
 public List<RankingInfo> getRankings(String type, String date, int page, int size) {
-  return switch (type.toUpperCase()) {
+  return switch (Objects.requireNonNullElse(type, "DAILY").toUpperCase()) {
     case "WEEKLY" -> rankingService.getWeeklyRankings(date, page, size);
     case "MONTHLY" -> rankingService.getMonthlyRankings(date, page, size);
     default -> getTopRankings(date, page, size);
   };
 }
apps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1Controller.java (1)

23-29: ์„ ํƒ ์‚ฌํ•ญ: ํƒ€์ž… ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด enum ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

ํ˜„์žฌ type์„ String์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์–ด ์œ ์—ฐํ•˜์ง€๋งŒ, enum์„ ์‚ฌ์šฉํ•˜๋ฉด ์ปดํŒŒ์ผ ํƒ€์ž„ ์•ˆ์ •์„ฑ๊ณผ ๋ช…ํ™•ํ•œ API ๋ฌธ์„œํ™”๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž Enum ๊ธฐ๋ฐ˜ ๋ฆฌํŒฉํ† ๋ง ์ œ์•ˆ

RankingType enum ์ƒ์„ฑ:

public enum RankingType {
    DAILY, WEEKLY, MONTHLY
}

์ปจํŠธ๋กค๋Ÿฌ ์ˆ˜์ •:

 public ApiResponse<List<RankingResponse>> getRankings(
-    @RequestParam(value = "type", defaultValue = "DAILY") String type,
+    @RequestParam(value = "type", defaultValue = "DAILY") RankingType type,
     @RequestParam(value = "date") String date,
     @RequestParam(value = "page", defaultValue = "1") int page,
     @RequestParam(value = "size", defaultValue = "20") int size
 ) {
-  List<RankingInfo> infos = rankingFacade.getRankings(type, date, page, size);
+  List<RankingInfo> infos = rankingFacade.getRankings(type.name(), date, page, size);

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด:

  • ์ž˜๋ชป๋œ type ๊ฐ’์— ๋Œ€ํ•ด Spring์ด ์ž๋™์œผ๋กœ 400 ์—๋Ÿฌ ๋ฐ˜ํ™˜
  • API ๋ฌธ์„œ์—์„œ ํ—ˆ์šฉ๋œ ๊ฐ’์ด ๋ช…ํ™•ํ•˜๊ฒŒ ํ‘œ์‹œ๋จ
  • IDE ์ž๋™์™„์„ฑ ์ง€์›
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/scheduler/RankingScheduler.java (2)

37-39: ํ–ฅํ›„ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ฐ•ํ™” ๊ณ ๋ ค

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

Slack ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ๋„์™€๋“œ๋ฆด๊นŒ์š”? Webhook ๊ธฐ๋ฐ˜ ์•Œ๋ฆผ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•ด ๋“œ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Also applies to: 55-57


27-40: ๋ฐฐ์น˜ ์žก ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€ ํ™•์ธ ๊ถŒ์žฅ

์Šค์ผ€์ค„๋Ÿฌ์— ๋™์‹œ ์‹คํ–‰ ๋ฐฉ์ง€ ๋กœ์ง์ด ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋ฐฐ์น˜ ์žก์ด ์˜ˆ์ƒ๋ณด๋‹ค ์˜ค๋ž˜ ์‹คํ–‰๋˜์–ด ๋‹ค์Œ ์Šค์ผ€์ค„ ์‹œ๊ฐ„๊ณผ ๊ฒน์น˜๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Spring Batch์˜ ๊ธฐ๋ณธ JobLauncher๋Š” ๋™์ผํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์™„๋ฃŒ๋œ ์žก์˜ ์žฌ์‹คํ–‰์„ ๋ฐฉ์ง€ํ•˜์ง€๋งŒ, ์‹คํ–‰ ์ค‘์ธ ์žก๊ณผ์˜ ๋™์‹œ ์‹คํ–‰์€ ๋ง‰์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”:

  1. Spring์˜ @Scheduled์— fixedDelay ์‚ฌ์šฉ (์ด์ „ ์‹คํ–‰ ์™„๋ฃŒ ํ›„ ๋Œ€๊ธฐ)
  2. ShedLock ๊ฐ™์€ ๋ถ„์‚ฐ ๋ฝ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
  3. ๋ฐฐ์น˜ ์žก ๋‚ด๋ถ€์—์„œ ์ด๋ฏธ ์‹คํ–‰ ์ค‘์ธ ์ธ์Šคํ„ด์Šค ํ™•์ธ

ํ˜„์žฌ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋ฐฐ์น˜๋ผ์„œ ์ค‘๋ณต ์‹คํ–‰ ๊ฐ€๋Šฅ์„ฑ์€ ๋‚ฎ์ง€๋งŒ, ์šด์˜ ํ™˜๊ฒฝ์—์„œ์˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด ๊ฒ€ํ† ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 42-58

apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingTableSwapTasklet.java (1)

29-37: ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ๋ฌธ์ œ ๊ฐ€๋Šฅ์„ฑ

workRepository.findAll()๊ณผ mvRepository.saveAll(newData)๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. ์ƒ์œ„ 100๊ฐœ ์ œํ’ˆ๋งŒ ์ฒ˜๋ฆฌํ•˜๋Š” ํ˜„์žฌ ์„ค๊ณ„์—์„œ๋Š” ๋ฌธ์ œ์—†์ง€๋งŒ, ๋ฐ์ดํ„ฐ๊ฐ€ ์ฆ๊ฐ€ํ•  ๊ฒฝ์šฐ ๋ฐฐ์น˜ ์ฒญํฌ ์ฒ˜๋ฆฌ๋‚˜ ํŽ˜์ด์ง•์„ ๊ณ ๋ คํ•˜์„ธ์š”.

๋˜ํ•œ deleteAllInBatch() ํ›„ saveAll() ์‚ฌ์ด์— ์žฅ์•  ๋ฐœ์ƒ ์‹œ ๋ฐ์ดํ„ฐ ์†์‹ค ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ค‘์š”ํ•œ ๊ฒฝ์šฐ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„ ๋˜๋Š” swap ์ „๋žต(์ž„์‹œ ํ…Œ์ด๋ธ” ํ™œ์šฉ)์„ ๊ฒ€ํ† ํ•˜์„ธ์š”.

apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java (2)

37-45: WeeklyRankingJobConfig์™€ ๋‹ฌ๋ฆฌ JobListener๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

WeeklyRankingJobConfig์—์„œ๋Š” .listener(jobListener)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Job ์‹คํ–‰์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜์ง€๋งŒ, MonthlyRankingJobConfig์—๋Š” ๋ˆ„๋ฝ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ์ถ”๊ฐ€๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
+  private final JobListener jobListener;
+
   @Bean(JOB_NAME)
   public Job monthlyRankingJob() {
     return new JobBuilder(JOB_NAME, jobRepository)
         .incrementer(new RunIdIncrementer())
         .start(monthlyPrepareStep())
         .next(monthlyCalculationStep())
         .next(monthlyTableSwapStep())
+        .listener(jobListener)
         .build();
   }

30-35: ์Šค์ผ€์ค„๋Ÿฌ ํƒ€์ด๋ฐ์œผ๋กœ ํ˜„์žฌ ๋™์‹œ ์‹คํ–‰ ๋ฐฉ์ง€๋˜์–ด ์žˆ์œผ๋‚˜, ์ฝ”๋“œ ์ˆ˜์ค€์˜ ๋ณดํ˜ธ ์—†์ด ์„ค๊ณ„ ์œ„ํ—˜ ์กด์žฌ

ํ˜„์žฌ ์Šค์ผ€์ค„๋Ÿฌ ์„ค์ •(weeklyRankingJob์€ ๋งค์ฃผ ์›”์š”์ผ 02:00, monthlyRankingJob์€ ๋งค์›” 1์ผ 03:00)์œผ๋กœ ์ธํ•ด ๋‘ ์ž‘์—…์˜ ๋™์‹œ ์‹คํ–‰์ด ์‹ค์ œ๋กœ๋Š” ๋ฐฉ์ง€๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ RankingPrepareTasklet๊ณผ MonthlyRankingTableSwapTasklet์ด ๋™์ผํ•œ WeeklyRankingWorkRepository(weekly_ranking_work ํ…Œ์ด๋ธ”)๋ฅผ ๊ณต์œ ํ•˜๋Š” ๊ตฌ์กฐ๋Š” ์—ฌ์ „ํžˆ ์„ค๊ณ„์  ์œ„ํ—˜์„ ์•ˆ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์Šค์ผ€์ค„๋Ÿฌ ์„ค์ • ๋ณ€๊ฒฝ์ด๋‚˜ ์ˆ˜๋™ ์žก ์‹คํ–‰ ์‹œ ๋ฐ์ดํ„ฐ ์ถฉ๋Œ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์œผ๋ฏ€๋กœ, ๋ณ„๋„์˜ Monthly์šฉ ์ž‘์—… ํ…Œ์ด๋ธ” ๋„์ž…์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

modules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMV.java (1)

31-43: WeeklyRankingMV์™€ MonthlyRankingMV์˜ ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ์ ‘๊ทผ ๋ฐฉ์‹ ์ฐจ์ด ๊ฒ€ํ†  ํ•„์š”

MonthlyRankingMV.createFromWork๊ฐ€ productName, price, isSoldOut์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š” ๊ฒƒ์€ ์˜ฌ๋ฐ”๋ฅธ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

๋‹ค๋งŒ WeeklyRankingMV.createFromWork๋Š” ํ˜„์žฌ "์ƒํ’ˆ๋ช… ์ž„์‹œ", 0L, false๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํ”Œ๋ ˆ์ด์Šคํ™€๋”๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค (modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.java:48-50, ์ฝ”๋“œ ์ฃผ์„ "์‹ค์ œ ๊ตฌํ˜„ ์‹œ Product ์ •๋ณด ๊ฒฐํ•ฉ ํ•„์š”").

์ด ๋ถˆ์ผ์น˜๋ฅผ ๊ฐœ์„ ํ•˜๋ ค๋ฉด RankingTableSwapTasklet์—์„œ Product ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ์กฐํšŒํ•˜์—ฌ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” WeeklyRankingWork ๋ฐ์ดํ„ฐ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ, ์›๋ณธ Design์ด ์˜๋„ํ•œ ๊ฒƒ์ธ์ง€ ํ™•์ธํ•˜๊ณ  ํ•„์š”์‹œ productId ๊ธฐ๋ฐ˜ Product ์กฐํšŒ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ฒ€ํ† ํ•ด์ฃผ์„ธ์š”.

apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java (1)

40-47: Step ์ด๋ฆ„๊ณผ ๋ฉ”์„œ๋“œ๋ช… ๋ถˆ์ผ์น˜ ํ™•์ธ ํ•„์š”

Step ๋นˆ ์ด๋ฆ„์€ demoSimpleTask์ด์ง€๋งŒ ๋ฉ”์„œ๋“œ๋ช…์€ categorySyncStep์ž…๋‹ˆ๋‹ค. ๋ฐ๋ชจ ๋ชฉ์ ์˜ ์ฝ”๋“œ๋ผ๋ฉด ํฐ ๋ฌธ์ œ๋Š” ์•„๋‹ˆ์ง€๋งŒ, ๋ฉ”์„œ๋“œ๋ช…์„ demoSimpleTaskStep ๋˜๋Š” ์ด์™€ ์œ ์‚ฌํ•˜๊ฒŒ ๋ณ€๊ฒฝํ•˜๋ฉด ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
     @JobScope
     @Bean(STEP_DEMO_SIMPLE_TASK_NAME)
-    public Step categorySyncStep() {
+    public Step demoSimpleTaskStep() {
         return new StepBuilder(STEP_DEMO_SIMPLE_TASK_NAME, jobRepository)
             .tasklet(demoTasklet, new ResourcelessTransactionManager())
             .listener(stepMonitorListener)
             .build();
     }
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/RankingChunkConfig.java (1)

27-37: ์ฃผ๊ฐ„ ๋žญํ‚น๋„ Job ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋‚ ์งœ๋ฅผ ๋ฐ›๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”

monthlyRankingReader๋Š” startDate Job ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ˜๋ฉด, rankingReader๋Š” LocalDateTime.now().minusDays(7)๋ฅผ ํ•˜๋“œ์ฝ”๋”ฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์žฌ์ฒ˜๋ฆฌ๋‚˜ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋‚ ์งœ๋ฅผ ์™ธ๋ถ€์—์„œ ์ฃผ์ž…๋ฐ›์œผ๋ฉด ๋” ์œ ์—ฐํ•ด์ง‘๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/RankingServiceImpl.java (2)

56-64: DateTimeFormatter ์ธ์Šคํ„ด์Šค๋ฅผ static final๋กœ ์ถ”์ถœํ•˜์„ธ์š”

DateTimeFormatter.ofPattern("yyyyMMdd")๊ฐ€ ๋งค ํ˜ธ์ถœ๋งˆ๋‹ค ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ํด๋ž˜์Šค ๋ ˆ๋ฒจ ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•˜๋ฉด ๋ถˆํ•„์š”ํ•œ ๊ฐ์ฒด ์ƒ์„ฑ์„ ํ”ผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
+  private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
+
   @Override
   public Integer getProductRank(Long productId) {
-    String today = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+    String today = LocalDateTime.now().format(DATE_FORMATTER);
     String key = "ranking:all:" + today;

23-38: Redis ํ‚ค ์ƒ์„ฑ ๋กœ์ง ํ•˜๋“œ์ฝ”๋”ฉ ๋ฐ NumberFormatException ์œ„ํ—˜์„ฑ

getTopRankingIds ๋ฉ”์„œ๋“œ(25ํ–‰)์™€ getProductRank ๋ฉ”์„œ๋“œ(59ํ–‰)์—์„œ "ranking:all:" ํ‚ค ํŒจํ„ด์ด ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋™์ผํ•œ ํŒจํ„ด์ด ๋‹ค๋ฅธ ํŒŒ์ผ(MetricsEventConsumer 66ํ–‰ ๋“ฑ)์—์„œ๋„ ๋ฐ˜๋ณต๋˜๊ณ  ์žˆ์œผ๋ฉฐ, ๋ณ„๋„ ๋ชจ๋“ˆ์˜ RankingKeyGenerator ์ธํ„ฐํŽ˜์ด์Šค(generateDailyKey ๋ฉ”์„œ๋“œ ์ œ๊ณต)์™€ ๋ถˆ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. ์ค‘์•™ํ™”๋œ ํ‚ค ์ƒ์„ฑ ๋กœ์ง์œผ๋กœ ํ†ตํ•ฉํ•˜์‹ญ์‹œ์˜ค.

๋˜ํ•œ 36ํ–‰์˜ Long::valueOf๋Š” ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ์—†์–ด Redis์— ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋œ ๊ฒฝ์šฐ NumberFormatException์„ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณ€ํ™˜ ์ „ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋˜๋Š” ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์‹ญ์‹œ์˜ค.

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

14-19: type ํŒŒ๋ผ๋ฏธํ„ฐ์— Enum ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”

type ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ String์œผ๋กœ ์ •์˜๋˜์–ด ์žˆ์–ด ๋Ÿฐํƒ€์ž„์— ์ž˜๋ชป๋œ ๊ฐ’์ด ์ „๋‹ฌ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. RankingType enum์„ ์ •์˜ํ•˜๋ฉด ์ปดํŒŒ์ผ ํƒ€์ž„ ์•ˆ์ „์„ฑ๊ณผ Swagger ๋ฌธ์„œ ์ž๋™ํ™” ์ด์ ์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž Enum ์ •์˜ ์˜ˆ์‹œ
public enum RankingType {
    DAILY, WEEKLY, MONTHLY
}
   ApiResponse<List<RankingResponse>> getRankings(
-      @Parameter(description = "๋žญํ‚น ํƒ€์ž… (DAILY, WEEKLY, MONTHLY)", example = "WEEKLY") String type,
+      @Parameter(description = "๋žญํ‚น ํƒ€์ž…", example = "WEEKLY") RankingType type,
       @Parameter(description = "์กฐํšŒ ๋‚ ์งœ (yyyyMMdd)", example = "20251225") String date,
       @Parameter(description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ") int page,
       @Parameter(description = "ํŽ˜์ด์ง€ ํฌ๊ธฐ") int size
   );
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/monthly/MonthlyRankingTableSwapTasklet.java (1)

32-34: Redis ํ‚ค ํŒจํ„ด์„ ์ƒ์ˆ˜๋กœ ์ถ”์ถœ ๊ถŒ์žฅ

"product:snapshot:" ํ‚ค ํŒจํ„ด์ด ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๊ณณ์—์„œ๋„ ๋™์ผํ•œ ํŒจํ„ด์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์ƒ์ˆ˜๋‚˜ ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ๋กœ ์ถ”์ถœํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 360a1ac and 84b46b4.

๐Ÿ“’ Files selected for processing (49)
  • .github/workflows/main.yml
  • README.md
  • apps/commerce-api/build.gradle.kts
  • apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderCreatedEvent.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderSalesAggregateListener.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/rank/RankingFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/rank/RankingInfo.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/RankingService.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/RankingServiceImpl.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1Dto.java
  • apps/commerce-api/src/main/resources/application.yml
  • apps/commerce-batch/build.gradle.kts
  • apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/RankingChunkConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/scheduler/RankingScheduler.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingPrepareTasklet.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingTableSwapTasklet.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/monthly/MonthlyRankingTableSwapTasklet.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/weekly/WeeklyRankingProcessor.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
  • apps/commerce-batch/src/main/resources/application.yml
  • apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java
  • apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/rank/RankingScheduler.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingKeyGenerator.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingService.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RedisRankingKeyGenerator.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.java
  • apps/pg-simulator/src/main/resources/application.yml
  • modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.java
  • modules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMV.java
  • modules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMVRepository.java
  • modules/jpa/src/main/java/com/loopers/domain/rank/monthly/ProductSnapshot.java
  • modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.java
  • modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMVRepository.java
  • modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWork.java
  • modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWorkRepository.java
  • modules/kafka/src/main/java/com/loopers/event/ProductStockEvent.java
  • settings.gradle.kts
๐Ÿ’ค Files with no reviewable changes (1)
  • apps/commerce-api/src/main/resources/application.yml
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (8)
๐Ÿ“š 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:

  • modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWorkRepository.java
  • modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMVRepository.java
  • modules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMVRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.java
๐Ÿ“š Learning: 2026-01-01T14:12:04.385Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 76
File: apps/commerce-batch/src/main/kotlin/com/loopers/batch/listener/ChunkListener.kt:4-5
Timestamp: 2026-01-01T14:12:04.385Z
Learning: In Spring Batch 6.x, ChunkListener API changed from using ChunkContext to Chunk<T> parameters. The correct method signatures are: beforeChunk(Chunk<T> chunk), afterChunk(Chunk<T> chunk), and onChunkError(Exception exception, Chunk<T> chunk). The import path is org.springframework.batch.core.listener.ChunkListener.

Applied to files:

  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.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-batch/src/main/resources/application.yml
  • apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.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:

  • modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.java
๐Ÿ“š Learning: 2025-12-19T20:59:57.713Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: docs/week8/round8-detailed-design.md:151-178
Timestamp: 2025-12-19T20:59:57.713Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository's Kafka event pipeline, only 5 domain events are intentionally published to Kafka via CloudEventEnvelopeFactory: OrderPaidEventV1, LikeCreatedEventV1, LikeCanceledEventV1, ProductViewedEventV1, and StockDepletedEventV1. Other domain events (OrderCreatedEventV1, OrderCanceledEventV1, PaymentCreatedEventV1, PaymentPaidEventV1, PaymentFailedEventV1) are internal-only and intentionally not mapped in resolveMetadata(), which correctly returns null for them to exclude them from Outbox publication.

Applied to files:

  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderSalesAggregateListener.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderCreatedEvent.java
  • modules/kafka/src/main/java/com/loopers/event/ProductStockEvent.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-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java
  • modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.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-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java
  • modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.java
๐Ÿ“š Learning: 2026-01-01T14:12:07.943Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 76
File: apps/commerce-batch/src/main/kotlin/com/loopers/batch/listener/JobListener.kt:4-5
Timestamp: 2026-01-01T14:12:07.943Z
Learning: Spring Batch 6.x์—์„œ JobExecution์€ org.springframework.batch.core.job ํŒจํ‚ค์ง€์—, JobExecutionListener๋Š” org.springframework.batch.core.listener ํŒจํ‚ค์ง€์— ์œ„์น˜ํ•ฉ๋‹ˆ๋‹ค. Spring Batch 5.x์™€ ๋‹ฌ๋ฆฌ ํ•˜์œ„ ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Applied to files:

  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
๐Ÿงฌ Code graph analysis (14)
apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RedisRankingKeyGenerator.java (1)
apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingService.java (1)
  • Component (14-50)
apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingKeyGenerator.java (2)
apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingService.java (1)
  • Component (14-50)
apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RedisRankingKeyGenerator.java (1)
  • Component (5-14)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
  • Slf4j (14-53)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
  • Slf4j (13-44)
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/RankingChunkConfig.java (1)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
  • StepScope (14-32)
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingTableSwapTasklet.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
  • StepScope (14-32)
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingPrepareTasklet.java (1)
  • Component (12-24)
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/weekly/WeeklyRankingProcessor.java (1)
  • Component (9-30)
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
  • SpringBootTest (23-76)
modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.java (2)
modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWork.java (1)
  • Entity (12-30)
modules/jpa/src/main/java/com/loopers/domain/rank/monthly/ProductSnapshot.java (1)
  • Getter (8-19)
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingPrepareTasklet.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingTableSwapTasklet.java (1)
  • Component (17-40)
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/weekly/WeeklyRankingProcessor.java (1)
  • Component (9-30)
modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWork.java (1)
modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.java (1)
  • Entity (12-53)
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (2)
apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1)
  • ConfigurationPropertiesScan (10-22)
apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt (1)
  • main (22-24)
modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.java (1)
apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt (1)
  • name (14-87)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
  • Slf4j (9-21)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
  • Slf4j (13-44)
apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
  • SpringBootTest (6-10)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
  • Slf4j (9-21)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
  • Slf4j (14-53)
๐Ÿ”‡ Additional comments (44)
README.md (1)

30-30: ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฐ˜์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด commerce-batch ๋ชจ๋“ˆ ์ถ”๊ฐ€๋ฅผ ๋ฌธ์„œ์— ์ •ํ™•ํ•˜๊ฒŒ ๋ฐ˜์˜ํ–ˆ์Šต๋‹ˆ๋‹ค. settings.gradle.kts์˜ ์‹ค์ œ ๋ชจ๋“ˆ ํฌํ•จ๊ณผ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

settings.gradle.kts (1)

7-7: ๋ชจ๋“ˆ ์ถ”๊ฐ€๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

commerce-batch ๋ชจ๋“ˆ์ด Gradle ๋ฉ€ํ‹ฐ๋ชจ๋“ˆ ํ”„๋กœ์ ํŠธ์— ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํฌํ•จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“ˆ ๊ฒฝ๋กœ ํ˜•์‹๊ณผ ์œ„์น˜๊ฐ€ ๊ธฐ์กด ๊ตฌ์กฐ์™€ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/build.gradle.kts (1)

1-21: LGTM! ์˜์กด์„ฑ ๊ตฌ์„ฑ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

๋ฐฐ์น˜ ๋ชจ๋“ˆ์˜ ์˜์กด์„ฑ ๊ตฌ์„ฑ์ด PR ๋ชฉํ‘œ์™€ ์ž˜ ์ •๋ ฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • Spring Batch ํ”„๋ ˆ์ž„์›Œํฌ (starter ๋ฐ test ์ง€์›)
  • ๋‚ด๋ถ€ ๋ชจ๋“ˆ ์˜์กด์„ฑ (JPA, Redis, Jackson ๋“ฑ)์ด ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ ์ž‘์—…์˜ ์š”๊ตฌ์‚ฌํ•ญ์„ ์ถฉ์กฑ
  • QueryDSL ์„ค์ •์ด Jakarta ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‚ฌ์šฉ (::jakarta classifier)
  • ํ…Œ์ŠคํŠธ ํ”ฝ์Šค์ฒ˜ ์žฌ์‚ฌ์šฉ์œผ๋กœ ์ผ๊ด€์„ฑ ํ™•๋ณด
apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderCreatedEvent.java (2)

48-48: ์ผ๊ด€๋œ null ์ฒ˜๋ฆฌ ๊ตฌํ˜„

productName ์ถ”์ถœ ๋กœ์ง์ด ๋‹ค๋ฅธ ํ•„๋“œ๋“ค(price, stock)๊ณผ ๋™์ผํ•œ ํŒจํ„ด์œผ๋กœ null-safeํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์ผ๊ด€์„ฑ์ด ์œ ์ง€๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ : "Unknown" ๋ฌธ์ž์—ด์ด ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์œผ๋‚˜, ํ•™์Šต๋œ ๋‚ด์šฉ์— ๋”ฐ๋ฅด๋ฉด ์ด ์ด๋ฒคํŠธ๋Š” ๋‚ด๋ถ€ ์ „์šฉ์ด๋ฏ€๋กœ ํ˜„์žฌ ๊ตฌํ˜„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.


24-24: ์ด ์ฝ”๋ฉ˜ํŠธ๋Š” ๊ฒ€ํ†  ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

OrderCreatedEvent ๋‚ด์˜ OrderItemInfo๋Š” OrderInfo.java์˜ OrderItemInfo์™€ ๋ณ„๊ฐœ์˜ ์ค‘์ฒฉ ๋ ˆ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. ๊ฐ๊ฐ ๋‹ค๋ฅธ ๋ชฉ์ ๊ณผ ํ•„๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ, ๊ฐ™์€ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ๊ณต์œ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. OrderCreatedEvent ๋‚ด์˜ ๋ชจ๋“  OrderItemInfo ์ƒ์„ฑ์ž ํ˜ธ์ถœ(46-52์ค„)์€ ์ƒˆ๋กœ์šด 5๊ฐœ ํ•„๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์™€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋ ˆ์ฝ”๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•œ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ๋Š” ์—†์Šต๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RedisRankingKeyGenerator.java (1)

5-13: LGTM!

RankingKeyGenerator ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊น”๋”ํ•œ ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค. ํ‚ค prefix๊ฐ€ ์ƒ์ˆ˜๋กœ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์–ด ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingService.java (2)

19-25: LGTM!

RankingKeyGenerator๋ฅผ ํ†ตํ•œ ํ‚ค ์ƒ์„ฑ ๋กœ์ง ์ถ”์ƒํ™”๊ฐ€ ์ž˜ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜๋“œ์ฝ”๋”ฉ๋œ prefix ๋Œ€์‹  ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ๊ณผ ์œ ์—ฐ์„ฑ์ด ํ–ฅ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


41-49: unionAndStore ๋™์ž‘ ํ™•์ธ ํ•„์š”

unionAndStore๋Š” targetKey์— ์ด๋ฏธ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•  ๊ฒฝ์šฐ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ๋ฎ์–ด์”๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋‹น์ผ ์ž์ • ์ „์— ์ด๋ฏธ ์ต์ผ(targetKey) ๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์ถ•์ ๋˜์–ด ์žˆ๋‹ค๋ฉด, ์ด ๋ฐ์ดํ„ฐ๊ฐ€ ์†์‹ค๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ 23:50์— ์‹คํ–‰๋˜๋ฏ€๋กœ, ์ต์ผ ํ‚ค์— ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๊ฐ€๋Šฅ์„ฑ์€ ๋‚ฎ์ง€๋งŒ, ์˜๋„๋œ ๋™์ž‘์ธ์ง€ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. ๋งŒ์•ฝ ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋ณ‘ํ•ฉํ•ด์•ผ ํ•œ๋‹ค๋ฉด targetKey๋„ union ๋Œ€์ƒ์— ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ณด์กด์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ˆ˜์ • ์ œ์•ˆ
   public void carryOverRanking(String sourceDate, String targetDate, double weight) {
     String sourceKey = rankingKeyGenerator.generateDailyKey(sourceDate);
     String targetKey = rankingKeyGenerator.generateDailyKey(targetDate);

-    redisTemplate.opsForZSet().unionAndStore(sourceKey, List.of(), targetKey,
-        Aggregate.SUM, Weights.of(weight));
+    // targetKey ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋ณ‘ํ•ฉ (source: weight ์ ์šฉ, target: 1.0 ์œ ์ง€)
+    redisTemplate.opsForZSet().unionAndStore(sourceKey, List.of(targetKey), targetKey,
+        Aggregate.SUM, Weights.of(weight, 1.0));

     redisTemplate.expire(targetKey, 2, TimeUnit.DAYS);
   }
apps/pg-simulator/src/main/resources/application.yml (1)

26-32: Feign ํด๋ผ์ด์–ธํŠธ ํƒ€์ž„์•„์›ƒ ์„ค์ •์ด ์ง€๋‚˜์น˜๊ฒŒ ์งง๊ณ  ์ผ๊ด€๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

connectTimeout๊ณผ readTimeout์ด ๊ฐ๊ฐ 1์ดˆ๋กœ ์„ค์ •๋˜์–ด ์žˆ์–ด ์ •์ƒ์ ์ธ ๋„คํŠธ์›Œํฌ ์ง€์—ฐ ์ƒํ™ฉ์—์„œ๋„ ์š”์ฒญ์ด ์‹คํŒจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ Tomcat์˜ connection-timeout์ด 60์ดˆ(Line 7)๋กœ ์„ค์ •๋œ ๊ฒƒ๊ณผ ๋น„๊ตํ•˜๋ฉด ์‹ฌ๊ฐํ•œ ๋ถˆ์ผ์น˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ถ”๊ฐ€ ๋ฌธ์ œ: ๋ถˆ์™„์ „ํ•œ ํšŒ๋ณต๋ ฅ ๋ฉ”์ปค๋‹ˆ์ฆ˜

์ผ๋ถ€ Feign ํด๋ผ์ด์–ธํŠธ ๋ฉ”์„œ๋“œ์—๋งŒ @Retry์™€ @CircuitBreaker๊ฐ€ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด PgClient์˜ requestPayment() ๋ฉ”์„œ๋“œ๋Š” ๋ณดํ˜ธ๋˜์ง€๋งŒ, getTransactionsByOrder() ๋ฉ”์„œ๋“œ๋Š” ์•„๋ฌด๋Ÿฐ ๋ณดํ˜ธ๋„ ์—†์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์™ธ๋ถ€ API ํ˜ธ์ถœ์— ๋Œ€ํ•ด ์ผ๊ด€๋œ ํšŒ๋ณต๋ ฅ ์ „๋žต์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๊ถŒ์žฅ ์กฐ์น˜:

  1. Feign ํƒ€์ž„์•„์›ƒ์„ ์ ์ ˆํ•œ ์ˆ˜์ค€์œผ๋กœ ์ƒํ–ฅ ์กฐ์ • (connectTimeout: 35์ดˆ, readTimeout: 1030์ดˆ)
  2. ๋ชจ๋“  Feign ํด๋ผ์ด์–ธํŠธ ๋ฉ”์„œ๋“œ์— @Retry์™€ @CircuitBreaker ์ ์šฉ
  3. Tomcat๊ณผ Feign ํƒ€์ž„์•„์›ƒ ์„ค์ •์˜ ์ผ๊ด€์„ฑ ๊ฒ€ํ† 
โ›” Skipped due to learnings
Learnt from: HongChangMo
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 156
File: apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt:51-59
Timestamp: 2025-12-05T04:37:02.942Z
Learning: User HongChangMo prefers to skip detailed code reviews for the pg-simulator module (apps/pg-simulator/**) in the loopers-spring-java-template repository.
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (1)

16-20: ํƒ€์ž„์กด ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

@PostConstruct๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ ํƒ€์ž„์กด์„ Asia/Seoul๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์€ ๋ฐฐ์น˜ ์ž‘์—…์˜ ์‹œ๊ฐ„ ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ์ข‹์€ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. CommerceStreamerApplication๊ณผ ๋™์ผํ•œ ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์–ด ์ผ๊ด€์„ฑ๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

.github/workflows/main.yml (1)

1-4: ์›Œํฌํ”Œ๋กœ์šฐ ํŠธ๋ฆฌ๊ฑฐ ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

PR์ด ์—ด๋ฆฌ๊ฑฐ๋‚˜ ๋™๊ธฐํ™”๋  ๋•Œ ์ž๋™์œผ๋กœ ์‹คํ–‰๋˜๋Š” ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)

6-10: ๊ธฐ๋ณธ ์ปจํ…์ŠคํŠธ ๋กœ๋”ฉ ํ…Œ์ŠคํŠธ ํ™•์ธ๋จ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋กœ๋“œ๋˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ‘œ์ค€ ์Šค๋ชจํฌ ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค. DemoJobE2ETest์™€ ํ•จ๊ป˜ ๊ธฐ๋ณธ์ ์ธ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (2)

14-17: ์ปดํฌ๋„ŒํŠธ ์„ค์ • ํ™•์ธ๋จ

@StepScope์™€ @ConditionalOnProperty๋ฅผ ์‚ฌ์šฉํ•œ ์กฐ๊ฑด๋ถ€ ๋นˆ ๋“ฑ๋ก์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. DemoJobConfig.JOB_NAME๊ณผ ์—ฐ๋™๋˜์–ด ํŠน์ • ์žก ์‹คํ–‰ ์‹œ์—๋งŒ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.


19-26: Job ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ ๋กœ์ง ํ™•์ธ๋จ

SpEL์„ ํ†ตํ•œ job parameter ์ฃผ์ž…๊ณผ null ์ฒดํฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. DemoJobE2ETest์˜ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์™€ ์ผ์น˜ํ•˜๋Š” ๊ฒ€์ฆ ๋กœ์ง์ž…๋‹ˆ๋‹ค.

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

7-7: Import ์žฌ์ •๋ ฌ ํ™•์ธ

RankingService import์˜ ์œ„์น˜๋งŒ ๋ณ€๊ฒฝ๋œ ๊ฒƒ์œผ๋กœ, ๊ธฐ๋Šฅ์  ๋ณ€๊ฒฝ์€ ์—†์Šต๋‹ˆ๋‹ค.

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

3-3: ํŒจํ‚ค์ง€ ์žฌ๊ตฌ์„ฑ ํ™•์ธ๋จ

ProductMetrics ์—”ํ‹ฐํ‹ฐ์˜ ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ๊ฐ€ com.loopers.domain.metrics์—์„œ com.loopers.domain์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ธฐ๋Šฅ์  ๋ณ€๊ฒฝ์€ ์—†์œผ๋ฉฐ, PR์˜ ์ „๋ฐ˜์ ์ธ ๋„๋ฉ”์ธ ๊ตฌ์กฐ ์žฌ๊ตฌ์„ฑ๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWorkRepository.java (1)

1-9: LGTM!

ํ‘œ์ค€ Spring Data JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ ์ ˆํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ์ž‘์—…์—์„œ WeeklyRankingWork ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ CRUD ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

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

7-24: API ํ˜ธํ™˜์„ฑ์„ ๊ณ ๋ คํ•œ ๋ณ€๊ฒฝ ์ „๋žต์„ ๊ฒ€ํ† ํ•˜์„ธ์š”.

stock (int) ํ•„๋“œ๋ฅผ isSoldOut (boolean)์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์€ ๊ธฐ์กด API ์†Œ๋น„์ž์—๊ฒŒ breaking change์ž…๋‹ˆ๋‹ค.

์ด ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด V1 API (/api/v1/rankings)์— ์ ์šฉ๋˜๋ฏ€๋กœ, ๋‹ค์Œ์„ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

  • ๊ธฐ์กด ํด๋ผ์ด์–ธํŠธ๊ฐ€ stock ํ•„๋“œ๋ฅผ ์˜ˆ์ƒํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค
  • API ๋ฒ„์ „ ์—…๊ทธ๋ ˆ์ด๋“œ(V2) ๋˜๋Š” deprecation ๊ธฐ๊ฐ„์„ ๊ณ ๋ คํ•˜์„ธ์š”
  • ๋˜๋Š” ๋‘ ํ•„๋“œ๋ฅผ ๋ชจ๋‘ ์ œ๊ณตํ•˜๋Š” ์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต์„ ๊ฒ€ํ† ํ•˜์„ธ์š”

์˜๋„์ ์ธ breaking change๋ผ๋ฉด API ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๋ฌธ์„œํ™”ํ•˜๊ณ  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๊ณต์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

24-24: LGTM! ํ•˜์œ„ ํ˜ธํ™˜์„ฑ์ด ์ž˜ ์œ ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

type ํŒŒ๋ผ๋ฏธํ„ฐ์— defaultValue = "DAILY"๋ฅผ ์ง€์ •ํ•˜์—ฌ ๊ธฐ์กด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ƒ๋žตํ•ด๋„ ๊ธฐ๋ณธ ๋™์ž‘(์ผ๊ฐ„ ๋žญํ‚น)์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ํ•˜์œ„ ํ˜ธํ™˜์„ฑ์„ ๊ณ ๋ คํ•œ ์ข‹์€ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

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

25-31: LGTM! ์ด๋ฒคํŠธ ๊ฐ•ํ™”(Event Enrichment) ํŒจํ„ด์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

ProductStockEvent๊ฐ€ ์ด์ œ productName์„ ํฌํ•จํ•˜๋„๋ก ํ™•์žฅ๋˜์–ด, commerce-batch ๋ชจ๋“ˆ์ด commerce-api ๋„๋ฉ”์ธ์—์„œ ๋ถ„๋ฆฌ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” PR ๋ชฉํ‘œ์— ๋ช…์‹œ๋œ ์ด๋ฒคํŠธ ๊ฐ•ํ™” ์ „๋žต๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)

25-52: afterJob ๊ตฌํ˜„์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.

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

modules/kafka/src/main/java/com/loopers/event/ProductStockEvent.java (1)

6-25: LGTM! ์ด๋ฒคํŠธ ๊ฐ•ํ™”๋ฅผ ์œ„ํ•œ productName ํ•„๋“œ ์ถ”๊ฐ€๊ฐ€ ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

ProductStockEvent์— productName ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ด๋ฒคํŠธ๊ฐ€ ์ œํ’ˆ ์Šค๋ƒ…์ƒท ๋ฐ์ดํ„ฐ(์ด๋ฆ„, ๊ฐ€๊ฒฉ)๋ฅผ ์ „๋‹ฌํ•˜๋„๋ก ํ™•์žฅํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” PR ๋ชฉํ‘œ์— ๋ช…์‹œ๋œ ๋Œ€๋กœ commerce-batch ๋ชจ๋“ˆ์ด commerce-api ๋„๋ฉ”์ธ์—์„œ ๋ถ„๋ฆฌ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์™€ ๋ ˆ์ฝ”๋“œ ๊ตฌ์กฐ๊ฐ€ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMVRepository.java (1)

8-12: LGTM! Spring Data JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

WeeklyRankingMVRepository๋Š” Spring Data JPA ๊ทœ์น™์„ ๋”ฐ๋ฅด๋ฉฐ, ํŒŒ์ƒ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ findByBaseDateOrderByCurrentRankAsc๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์ •์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ•™์Šต๋œ ๋‚ด์šฉ์— ๋”ฐ๋ผ ์ด ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ๋Š” ๋„๋ฉ”์ธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์— Pageable์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ํ—ˆ์šฉ๋œ ์•„ํ‚คํ…์ฒ˜ ๊ฒฐ์ •์ž…๋‹ˆ๋‹ค.

apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)

58-75: LGTM! ์„ฑ๊ณต ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

JobParametersBuilder๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ LocalDate ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๊ณ , ๋ฐฐ์น˜ ์ž‘์—…์˜ ์„ฑ๊ณต์ ์ธ ์™„๋ฃŒ๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ ๊ตฌ์กฐ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/weekly/WeeklyRankingProcessor.java (2)

18-22: ๊ฐ€์ค‘์น˜ ๊ณต์‹๊ณผ 100๊ฐœ ์ œํ•œ ๊ฒ€์ฆ ๊ถŒ์žฅ

์ ์ˆ˜ ๊ณ„์‚ฐ ๊ฐ€์ค‘์น˜(์กฐํšŒ์ˆ˜ 0.1, ์ข‹์•„์š” 0.2, ํŒ๋งค์ˆ˜ 0.6)์™€ ์ƒ์œ„ 100๊ฐœ ์ œํ•œ์ด ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ 100๊ฐœ ์ œํ•œ์ด ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์–ด ํ–ฅํ›„ ๋ณ€๊ฒฝ ์‹œ ์ฝ”๋“œ ์ˆ˜์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์š”๊ตฌ์‚ฌํ•ญ ๋ฌธ์„œ๋‚˜ ๊ด€๋ จ ์ด์Šˆ์—์„œ ๋‹ค์Œ์„ ํ™•์ธํ•˜์„ธ์š”:

  • ๊ฐ€์ค‘์น˜ ๋น„์œจ์ด ์˜ฌ๋ฐ”๋ฅธ์ง€
  • ์ƒ์œ„ ๋žญํ‚น ๊ฐœ์ˆ˜๊ฐ€ 100๊ฐœ๋กœ ๊ณ ์ •๋˜์–ด์•ผ ํ•˜๋Š”์ง€, ์•„๋‹ˆ๋ฉด ์„ค์ • ๊ฐ€๋Šฅํ•ด์•ผ ํ•˜๋Š”์ง€

13-20: ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ TaskExecutor ์„ค์ •์ด ์—†์–ด ๋™์‹œ์„ฑ ๋ฌธ์ œ ์šฐ๋ ค๋Š” ๊ทผ๊ฑฐ๊ฐ€ ์—†์Œ

WeeklyRankingJobConfig์˜ calculationStep() ๋ฉ”์„œ๋“œ๋ฅผ ํ™•์ธํ•œ ๊ฒฐ๊ณผ, .taskExecutor() ํ˜ธ์ถœ์ด ์—†์Šต๋‹ˆ๋‹ค. Spring Batch๋Š” taskExecutor๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ๊ตฌ์„ฑ๋˜์ง€ ์•Š์œผ๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹จ์ผ ์Šค๋ ˆ๋“œ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ rankCounter ํ•„๋“œ์˜ ํ˜„์žฌ ๊ตฌํ˜„์€ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

@StepScope ์–ด๋…ธํ…Œ์ด์…˜์€ ๊ฐ Step ์‹คํ–‰๋งˆ๋‹ค ์ƒˆ๋กœ์šด processor ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๋ฏ€๋กœ, rankCounter๋Š” ๋‹จ์ผ ์Šค๋ ˆ๋“œ์—์„œ๋งŒ ์ ‘๊ทผ๋ฉ๋‹ˆ๋‹ค.

modules/jpa/src/main/java/com/loopers/domain/rank/monthly/ProductSnapshot.java (1)

1-19: LGTM!

๊ฐ„๋‹จํ•˜๊ณ  ๋ช…ํ™•ํ•œ ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. Serializable ๊ตฌํ˜„์€ Redis ์บ์‹ฑ์— ์ ํ•ฉํ•˜๋ฉฐ, Lombok ์‚ฌ์šฉ์œผ๋กœ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ๊ฐ€ ์ตœ์†Œํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)

18-21: LGTM!

์Šคํ… ์‹œ์ž‘ ๋กœ๊น…์ด ๋ช…ํ™•ํ•˜๊ณ  ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/rank/RankingInfo.java (2)

11-11: API ๊ฐœ์„  - stock์—์„œ isSoldOut์œผ๋กœ ๋ณ€๊ฒฝ

stock ์ •์ˆ˜ ๊ฐ’์„ isSoldOut ๋ถˆ๋ฆฐ์œผ๋กœ ๋ณ€๊ฒฝํ•œ ๊ฒƒ์€ ์ข‹์€ ์บก์Аํ™”์ž…๋‹ˆ๋‹ค. API ์†Œ๋น„์ž๋Š” ์žฌ๊ณ  ์ˆ˜๋Ÿ‰ ๋Œ€์‹  ํ’ˆ์ ˆ ์—ฌ๋ถ€๋งŒ ์•Œ๋ฉด ๋˜๋ฏ€๋กœ ์ •๋ณด ๋…ธ์ถœ์ด ์ตœ์†Œํ™”๋ฉ๋‹ˆ๋‹ค.

Also applies to: 20-20


25-43: LGTM!

WeeklyRankingMV์™€ MonthlyRankingMV๋กœ๋ถ€ํ„ฐ RankingInfo๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. MV ๊ธฐ๋ฐ˜ ๋žญํ‚น ์กฐํšŒ ํŒŒ์ดํ”„๋ผ์ธ๊ณผ ์ž˜ ํ†ตํ•ฉ๋ฉ๋‹ˆ๋‹ค.

modules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMVRepository.java (1)

8-12: LGTM!

Spring Data JPA ๊ทœ์น™์„ ์ž˜ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. findByBaseDateOrderByCurrentRankAsc ๋ฉ”์„œ๋“œ๊ฐ€ ์ˆœ์œ„๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ : ํ˜„์žฌ List ๋ฐ˜ํ™˜ ํƒ€์ž…์€ ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(์ด ๊ฐœ์ˆ˜ ๋“ฑ)๊ฐ€ ํ•„์š” ์—†๋Š” ๊ฒฝ์šฐ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•˜๋ฉด Page<MonthlyRankingMV>๋กœ ๋ณ€๊ฒฝ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

modules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMV.java (1)

17-29: ์—”ํ‹ฐํ‹ฐ ๊ตฌ์กฐ๋Š” ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

MV ์—”ํ‹ฐํ‹ฐ๋กœ์„œ ํ•„์š”ํ•œ ํ•„๋“œ๋“ค์ด ์ž˜ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ, baseDate์™€ currentRank ์ปฌ๋Ÿผ์— ๋ณตํ•ฉ ์ธ๋ฑ์Šค ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด๋ณด์„ธ์š” (์ฟผ๋ฆฌ ํŒจํ„ด: findByBaseDateOrderByCurrentRankAsc).

modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWork.java (1)

12-29: LGTM!

๋ฐฐ์น˜ ์ž‘์—…์„ ์œ„ํ•œ ์ž‘์—… ํ…Œ์ด๋ธ” ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. RankingPrepareTasklet์—์„œ deleteAllInBatch()๋กœ ์ •๋ฆฌํ•œ ํ›„ ๋ฐ์ดํ„ฐ๋ฅผ ์ ์žฌํ•˜๋Š” ํ๋ฆ„์ด๋ฏ€๋กœ ์ค‘๋ณต ๋ฐฉ์ง€ ๋กœ์ง์ด ์ ์ ˆํžˆ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ : productId์— unique ์ œ์•ฝ์กฐ๊ฑด์€ ์—†์ง€๋งŒ, ๋ฐฐ์น˜ ์‹œ์ž‘ ์‹œ ํ…Œ์ด๋ธ”์„ ๋น„์šฐ๋Š” ํ˜„์žฌ ์„ค๊ณ„์—์„œ๋Š” ๋ฌธ์ œ์—†์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java (2)

41-50: LGTM!

Job ๊ตฌ์„ฑ์ด ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. RunIdIncrementer๋กœ ์žฌ์‹คํ–‰์„ ์ง€์›ํ•˜๊ณ , 3๋‹จ๊ณ„(prepare โ†’ calculation โ†’ tableSwap) ํ๋ฆ„์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. JobListener๋กœ ๋ชจ๋‹ˆํ„ฐ๋ง๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.


59-67: ์ฒญํฌ ์ฒ˜๋ฆฌ ๊ตฌ์„ฑ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์ฒญํฌ ํฌ๊ธฐ 100์€ ํ•ฉ๋ฆฌ์ ์ธ ์„ ํƒ์ž…๋‹ˆ๋‹ค. WeeklyRankingProcessor์—์„œ ์ƒ์œ„ 100๊ฐœ๋งŒ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง๊ณผ ์ž˜ ๋งž์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingPrepareTasklet.java (1)

12-23: LGTM!

deleteAllInBatch()๋ฅผ ์‚ฌ์šฉํ•œ ํšจ์œจ์ ์ธ ํ…Œ์ด๋ธ” ์ •๋ฆฌ ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค. Weekly์™€ Monthly ์ž‘์—… ๋ชจ๋‘์—์„œ ์žฌ์‚ฌ์šฉ๋˜๋Š” ์„ค๊ณ„๊ฐ€ ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/RankingService.java (1)

6-15: LGTM! ์ธํ„ฐํŽ˜์ด์Šค ์ถ”์ถœ์ด ์ž˜ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ตฌ์ฒด ํด๋ž˜์Šค์—์„œ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ „ํ™˜ํ•˜์—ฌ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ๊ฐ€ ๋ช…ํ™•ํ•ด์กŒ์Šต๋‹ˆ๋‹ค. ์ผ๊ฐ„(Redis), ์ฃผ๊ฐ„/์›”๊ฐ„(MV) ๋žญํ‚น์— ๋Œ€ํ•œ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.java (1)

29-31: ์ดˆ๊ธฐํ™” ์‹œ updatedAt์ด null๋กœ ๋‚จ์Šต๋‹ˆ๋‹ค

์ƒ์„ฑ์ž์—์„œ updatedAt์„ ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š์•„, ์ฒซ ๋ฒˆ์งธ updateLikeCount ํ˜ธ์ถœ ์ „๊นŒ์ง€ updatedAt์ด null์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์˜๋„๋œ ๋™์ž‘์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋ฐฐ์น˜ ์ž‘์—…์—์„œ updatedAt ๊ธฐ๋ฐ˜ ์ฟผ๋ฆฌ ์‹œ ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ๋ฉ”ํŠธ๋ฆญ์ด ๋ˆ„๋ฝ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
   public ProductMetrics(Long productId) {
     this.productId = productId;
+    this.updatedAt = LocalDateTime.now();
   }
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/monthly/MonthlyRankingTableSwapTasklet.java (1)

28-30: ํŠธ๋žœ์žญ์…˜ ๋ณดํ˜ธ๋กœ ์ธํ•ด delete-then-insert ํŒจํ„ด์˜ ๋ฐ์ดํ„ฐ ์†์‹ค ์œ„ํ—˜์€ ์—†์Œ

์ด Tasklet์€ MonthlyRankingJobConfig์—์„œ .tasklet(tableSwapTasklet, transactionManager)๋กœ ์„ค์ •๋˜์–ด ์žˆ์–ด, execute() ๋ฉ”์„œ๋“œ ์ „์ฒด๊ฐ€ Spring Batch์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ณดํ˜ธ๋ฉ๋‹ˆ๋‹ค(67๋ฒˆ ๋ผ์ธ). ๋”ฐ๋ผ์„œ deleteAllInBatch() ์‹คํ–‰ ํ›„ saveAll() ์ „์— ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด delete๋„ ํ•จ๊ป˜ ๋กค๋ฐฑ๋˜๋ฏ€๋กœ ๋ฐ์ดํ„ฐ ์†์‹ค ์œ„ํ—˜์€ ์—†์Šต๋‹ˆ๋‹ค.

๋‹ค๋งŒ, Redis ์กฐํšŒ ์‹คํŒจ(36๋ฒˆ ๋ผ์ธ) ๋˜๋Š” ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๊ณผ์ •(42-58๋ฒˆ ๋ผ์ธ)์—์„œ์˜ ์˜ˆ์™ธ๋Š” ์ „์ฒด Step์„ ์‹คํŒจ์‹œํ‚ค๋ฏ€๋กœ, ์‹คํŒจ ์ฒ˜๋ฆฌ ์ „๋žต๊ณผ ์—๋Ÿฌ ๋กœ๊น…์„ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/resources/application.yml (5)

1-18: ๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ธฐ๋ณธ ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

web-application-type: none ์„ค์ •๊ณผ ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ job.name์€ Spring Batch ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ ํ•ฉํ•˜๋ฉฐ, initialize-schema: never๋Š” ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ์˜ ์ž๋™ ์Šคํ‚ค๋งˆ ์ดˆ๊ธฐํ™”๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ์˜ฌ๋ฐ”๋ฅธ ๊ธฐ๋ณธ๊ฐ’์ž…๋‹ˆ๋‹ค.


25-32: ๋กœ์ปฌ/ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์˜ ์Šคํ‚ค๋งˆ ์ดˆ๊ธฐํ™” ์ „๋žต์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

๋กœ์ปฌ ๋ฐ ํ…Œ์ŠคํŠธ ํ”„๋กœํŒŒ์ผ์—์„œ initialize-schema: always๋กœ ์„ค์ •ํ•˜์—ฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ๋ฐฐ์น˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”์„ ์ž๋™ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์€ ์ ์ ˆํ•œ ๊ตฌ์„ฑ์ž…๋‹ˆ๋‹ค.


34-50: ํ™˜๊ฒฝ๋ณ„ ํ”„๋กœํŒŒ์ผ ์„ ์–ธ์ด ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

dev, qa, prd ํ”„๋กœํŒŒ์ผ์ด ๋ช…์‹œ์  ์˜ค๋ฒ„๋ผ์ด๋“œ ์—†์ด ์„ ์–ธ๋˜์–ด ์žˆ์œผ๋ฉฐ, ์ด๋Š” ๊ธฐ๋ณธ ์„ค์ •์„ ์ƒ์†ํ•˜๊ณ  ํ™˜๊ฒฝ๋ณ„ ์„ค์ •์€ ์ž„ํฌํŠธ๋œ ํŒŒ์ผ(jpa.yml, redis.yml ๋“ฑ)์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ํŒจํ„ด์œผ๋กœ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.


52-54: ๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ SpringDoc ๋น„ํ™œ์„ฑํ™”๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

web-application-type: none๊ณผ ์ผ๊ด€๋˜๊ฒŒ API ๋ฌธ์„œ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ์ œ๊ฑฐํ•œ ๊ฒƒ์€ ๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ ํ•ฉํ•œ ๊ตฌ์„ฑ์ž…๋‹ˆ๋‹ค.


20-23: ํ—ฌ์Šค ์—”๋“œํฌ์ธํŠธ ๋น„ํ™œ์„ฑํ™”๋กœ ์ดํ•ด๋œ ๋ถ€๋ถ„์„ ์žฌ๊ฒ€ํ† ํ•ด์ฃผ์„ธ์š”.

management.health.defaults.enabled: false๋Š” ๊ธฐ๋ณธ ํ—ฌ์Šค ์ง€ํ‘œ(๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๋””์Šคํฌ ๊ณต๊ฐ„ ๋“ฑ)๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜๋Š” ๊ฒƒ์ด๋ฉฐ, ํ—ฌ์Šค ์—”๋“œํฌ์ธํŠธ ์ž์ฒด๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

imported๋œ monitoring.yml์—์„œ ๋‹ค์Œ์ด ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • management.endpoints.web.exposure.include: [health, prometheus] - ํ—ฌ์Šค ์—”๋“œํฌ์ธํŠธ ๋…ธ์ถœ
  • management.endpoint.health.probes.enabled: true - ํ—ฌ์Šค ํ”„๋กœ๋ธŒ ํ™œ์„ฑํ™”
  • management.health.livenessState.enabled: true, readinessState.enabled: true - ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค ํ”„๋กœ๋ธŒ ํ™œ์„ฑํ™”

๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜(web-application-type: none)์˜ ๊ฒฝ์šฐ ๊ธฐ๋ณธ ์ง€ํ‘œ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ์ž‘์—…์˜ ๊ฐ„ํ—์ ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ๋กœ ์ธํ•œ ๋ถˆํ•„์š”ํ•œ ํ—ฌ์Šค ์ฒดํฌ ์‹คํŒจ๋ฅผ ๋ฐฉ์ง€ํ•˜๋ฉด์„œ๋„, /actuator/health ์—”๋“œํฌ์ธํŠธ(ํฌํŠธ 8081)๋Š” ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค readiness/liveness probe๋ฅผ ์œ„ํ•ด ์ •์ƒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

Comment on lines +11 to +13
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.G_TOKEN }}
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

cd .github/workflows && find . -name "main.yml" -exec cat -n {} \;

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

Length of output: 487


๐Ÿ Script executed:

# Check if there are any other references to G_TOKEN or pr-agent in the repo
rg -i "g_token|pr-agent|openai" --type yaml --type md -A 2 -B 2 2>/dev/null || echo "Search completed"

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

Length of output: 104


๐ŸŒ Web query:

Codium-ai pr-agent GitHub action trustworthiness security

๐Ÿ’ก Result:

Short answer: PR-Agent (Codiumโ€‘AI / now Qodo) has had real, documented security issues; two vulnerabilities (CVEโ€‘2024โ€‘51355 and CVEโ€‘2024โ€‘51356) were reported and responsible disclosures and fixes were published โ€” so treat the GitHub Action as potentially risky unless you follow mitigation steps and use patched/pinned images or selfโ€‘host. Sources: independent security research and the project's security notes. [1][2][3]

Key points and what to do

  • History: Multiple serious vulnerabilities were published by Kudelski Security in Augโ€“Nov 2024 (PoC showed secret exfiltration and repo write risks); CVEs were assigned. The researcher showed many repos using the action could be affected. [1]
  • Project response: The PRโ€‘Agent repo/project (now Qodo) has published security guidance, release notes and recommends using fixed releases or pinned Docker digests; they also provide a security contact. Confirm you run a version that includes the fixes. [2][3]
  • Immediate mitigations (minimum best practice):
    1. Do not use unpinned uses: avoid using uses: codiumai/pr-agent@main. Pin to a specific, patched release or to the Docker image digest. [2]
    2. Selfโ€‘host the agent or run the Docker image in your own environment where you control secrets and network egress. [2]
    3. Limit GitHub Actions permissions and tokens: restrict GITHUB_TOKEN permissions (no write unless absolutely needed) and avoid passing highโ€‘privilege secrets into the action. Use least privilege for workflow tokens. (This is standard Actions hardening and directly mitigates the PoC.) [1][2]
    4. Upgrade to a version >= the patched release (check project release notes / changelog) and monitor the repo for new advisories. [2][3]
    5. Scan your repo and workflows for other uses of PRโ€‘Agent and rotated any secrets that may have been exposed if you used vulnerable versions. [1]
  • When to avoid it: do not install the action on sensitive/private repos or orgs that contain production secrets unless you selfโ€‘host or have confirmed the version is patched and permissions are locked down. [1][2]

Want me to:

  • check the exact latest patched release number and a recommended digest right now, or
  • scan a workflow file you have to point out risky config?

๐Ÿ Script executed:

# Check for other workflow files
find .github/workflows -type f -name "*.yml" -o -name "*.yaml" | head -20

# Check for any documentation about tokens
find . -name "README*" -o -name "SECURITY*" -o -name "CONTRIBUTING*" | xargs ls -la 2>/dev/null || echo "No docs found"

# Check git history for when G_TOKEN was introduced
git log --all --full-history -p -- ".github/workflows/main.yml" 2>/dev/null | head -50 || echo "Git history check skipped"

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

Length of output: 1682


๐ŸŒ Web query:

CVE-2024-51355 CVE-2024-51356 pr-agent codium-ai patch fix version

๐Ÿ’ก Result:

CVE-2024-51355 and CVE-2024-51356 affect PRโ€‘Agent (CodiumAI / Qodo Merge). The upstream fix was released in PRโ€‘Agent v0.26 (and included in later releases). [1][2][3]

Sources:

  • Kudelski Security writeup noting the two CVEs and the issue timeline. [1]
  • PRโ€‘Agent GitHub releases (v0.26 release notes). [2]
  • PRโ€‘Agent / Qodo security/release guidance referencing the v0.26 fix and later images. [3]

Codium-ai/pr-agent ์•ก์…˜์˜ ์•Œ๋ ค์ง„ ๋ณด์•ˆ ์ทจ์•ฝ์ ์œผ๋กœ ์ธํ•ด ๊ธด๊ธ‰ ์กฐ์น˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Codium-ai/pr-agent์—์„œ ๋ฐœ๊ฒฌ๋œ CVE-2024-51355์™€ CVE-2024-51356 (๋น„๋ฐ€ ์ •๋ณด ํƒˆ์ทจ ๋ฐ ์ €์žฅ์†Œ ์“ฐ๊ธฐ ๊ถŒํ•œ ์•…์šฉ ์œ„ํ—˜)์„ ๊ณ ๋ คํ•˜๋ฉด, ํ˜„์žฌ ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌ์„ฑ์ด ์œ„ํ—˜ํ•ฉ๋‹ˆ๋‹ค:

  1. @main ์‚ฌ์šฉ ์ค‘๋‹จ: ํŠน์ • ๋ฒ„์ „ (v0.26 ์ด์ƒ์˜ ํŒจ์น˜๋œ ๋ฆด๋ฆฌ์Šค)์œผ๋กœ ๊ณ ์ •ํ•˜์„ธ์š”. ํ˜„์žฌ @main์€ ๋ฏธํŒจ์น˜ ๋ฒ„์ „์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. permissions ๋ธ”๋ก ์ถ”๊ฐ€: PR ์ฝ”๋ฉ˜ํŠธ ์ž‘์„ฑ๋งŒ ํ•„์š”ํ•˜๋ฉด ์ตœ์†Œ ๊ถŒํ•œ์œผ๋กœ ์ œํ•œํ•˜์„ธ์š”:
    permissions:
      pull-requests: write
      contents: read
  3. OPENAI_KEY ๋…ธ์ถœ: ์ด ์•ก์…˜์— ๋ฏผ๊ฐํ•œ API ํ‚ค๋ฅผ ์ „๋‹ฌํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•˜์„ธ์š”. ๊ฐ€๋Šฅํ•˜๋ฉด ํ™˜๊ฒฝ๋ณ„๋กœ ๊ฒฉ๋ฆฌ๋œ ๊ตฌ์„ฑ ์‚ฌ์šฉ์„ ๊ฒ€ํ† ํ•˜์„ธ์š”.
๐Ÿค– Prompt for AI Agents
In .github/workflows/main.yml around lines 11 to 13, the workflow currently uses
the codium-ai/pr-agent action via @main and exposes OPENAI_KEY to the action,
which is vulnerable (CVE-2024-51355/51356); update the action to a specific
patched release (pin to a fixed tag or commit >= v0.26) instead of @main, add a
minimal permissions block restricting rights to only whatโ€™s needed (e.g.,
pull-requests: write and contents: read), and remove or stop passing OPENAI_KEY
into this action (use an isolated, scoped secret or omit the secret for this
job) so the action cannot access sensitive API keys.

Comment on lines +38 to +52
// Resilience4j (Spring Boot 3.x ๊ธฐ์ค€)
implementation("io.github.resilience4j:resilience4j-spring-boot3")

// AOP
implementation("org.springframework.boot:spring-boot-starter-aop")

// actuator
implementation("org.springframework.boot:spring-boot-starter-actuator")

//Micrometer Prometheus
implementation("io.micrometer:micrometer-registry-prometheus")

//Spring Cloud OpenFeign
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")

Copy link

Choose a reason for hiding this comment

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

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

์ค‘๋ณต๋œ ์˜์กด์„ฑ ์„ ์–ธ์„ ์ œ๊ฑฐํ•˜์„ธ์š”.

๋‹ค์Œ ์˜์กด์„ฑ๋“ค์ด ์ด๋ฏธ ํŒŒ์ผ ์ƒ๋‹จ์— ์„ ์–ธ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • resilience4j-spring-boot3 (17๋ฒˆ ๋ผ์ธ)
  • spring-boot-starter-aop (20๋ฒˆ ๋ผ์ธ)
  • spring-boot-starter-actuator (13๋ฒˆ ๋ผ์ธ)
  • micrometer-registry-prometheus (23๋ฒˆ ๋ผ์ธ)
  • spring-cloud-starter-openfeign (26๋ฒˆ ๋ผ์ธ)

Gradle์€ ์ค‘๋ณต ์„ ์–ธ์„ ๋ฌด์‹œํ•˜์ง€๋งŒ, ์œ ์ง€๋ณด์ˆ˜ ๋ถ€๋‹ด๊ณผ ํ˜ผ๋ž€์„ ์•ผ๊ธฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ค‘๋ณต ์ œ๊ฑฐ ์ œ์•ˆ
-
-    // Resilience4j (Spring Boot 3.x ๊ธฐ์ค€)
-    implementation("io.github.resilience4j:resilience4j-spring-boot3")
-
-    // AOP
-    implementation("org.springframework.boot:spring-boot-starter-aop")
-
-    // actuator
-    implementation("org.springframework.boot:spring-boot-starter-actuator")
-
-    //Micrometer Prometheus
-    implementation("io.micrometer:micrometer-registry-prometheus")
-
-    //Spring Cloud OpenFeign
-    implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
-
๐Ÿ“ 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
// Resilience4j (Spring Boot 3.x ๊ธฐ์ค€)
implementation("io.github.resilience4j:resilience4j-spring-boot3")
// AOP
implementation("org.springframework.boot:spring-boot-starter-aop")
// actuator
implementation("org.springframework.boot:spring-boot-starter-actuator")
//Micrometer Prometheus
implementation("io.micrometer:micrometer-registry-prometheus")
//Spring Cloud OpenFeign
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
๐Ÿค– Prompt for AI Agents
In apps/commerce-api/build.gradle.kts around lines 38 to 52, the block
re-declares dependencies that are already defined earlier in the file
(resilience4j-spring-boot3 at line 17, spring-boot-starter-aop at line 20,
spring-boot-starter-actuator at line 13, micrometer-registry-prometheus at line
23, and spring-cloud-starter-openfeign at line 26); remove these duplicate
implementation(...) entries from lines 38โ€“52 so each dependency is declared only
once, keeping the original declarations and deleting the repeated ones to avoid
redundancy and maintenance confusion.

Comment on lines +52 to +64
@Bean
@StepScope
public JpaPagingItemReader<ProductMetrics> monthlyRankingReader(
@Value("#{jobParameters['startDate']}") String startDate
) {
return new JpaPagingItemReaderBuilder<ProductMetrics>()
.name("monthlyRankingReader")
.entityManagerFactory(emf)
.queryString("SELECT m FROM ProductMetrics m WHERE m.updatedAt >= :startDate")
.parameterValues(Map.of("startDate", LocalDateTime.parse(startDate)))
.pageSize(100)
.build();
}
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

startDate ํŒŒ๋ผ๋ฏธํ„ฐ null ์ฒดํฌ ๋ฐ ํŒŒ์‹ฑ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ•„์š”

startDate Job ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ null์ด๊ฑฐ๋‚˜ ์ž˜๋ชป๋œ ํ˜•์‹์ผ ๊ฒฝ์šฐ DateTimeParseException์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. DemoTasklet.java์˜ ํŒจํ„ด์ฒ˜๋Ÿผ null ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
   @Bean
   @StepScope
   public JpaPagingItemReader<ProductMetrics> monthlyRankingReader(
       @Value("#{jobParameters['startDate']}") String startDate
   ) {
+    if (startDate == null || startDate.isBlank()) {
+      throw new IllegalArgumentException("startDate job parameter is required");
+    }
+    LocalDateTime parsedDate;
+    try {
+      parsedDate = LocalDateTime.parse(startDate);
+    } catch (DateTimeParseException e) {
+      throw new IllegalArgumentException("Invalid startDate format: " + startDate, e);
+    }
     return new JpaPagingItemReaderBuilder<ProductMetrics>()
         .name("monthlyRankingReader")
         .entityManagerFactory(emf)
         .queryString("SELECT m FROM ProductMetrics m WHERE m.updatedAt >= :startDate")
-        .parameterValues(Map.of("startDate", LocalDateTime.parse(startDate)))
+        .parameterValues(Map.of("startDate", parsedDate))
         .pageSize(100)
         .build();
   }
๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/RankingChunkConfig.java
around lines 52-64, the job parameter startDate is used directly and can be null
or malformatted causing DateTimeParseException; add a null/blank check for
startDate and parse it inside a try/catch, throwing a clear
IllegalArgumentException (or JobParametersInvalidException) if missing or
unparsable, then pass the successfully parsed LocalDateTime into the reader's
parameterValues instead of parsing inline.


@Scheduled(cron = "0 0 2 * * MON")
public void runWeeklyRankingJob() {
String requestDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# First, examine the RankingScheduler.java file around lines 29 and 44-45
cat -n apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/scheduler/RankingScheduler.java | sed -n '20,55p'

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

Length of output: 1694


๐Ÿ Script executed:

# Check for WeeklyRankingJobConfig and MonthlyRankingJobConfig
fd -t f "RankingJobConfig" apps/commerce-batch/src/main/java/

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

Length of output: 272


๐Ÿ Script executed:

# Check how requestDate and startDate parameters are used in config files
rg -n -C5 'requestDate|startDate' apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/config/ 2>/dev/null || echo "Pattern search in config directory"

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

Length of output: 122


๐Ÿ Script executed:

# Also search more broadly for parameter handling
rg -n -B3 -A3 'requestDate|startDate' apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/ --type java

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

Length of output: 7724


์ผ๊ด€๋˜์ง€ ์•Š์€ ๋‚ ์งœ ํฌ๋งท ์ˆ˜์ • ํ•„์š”

requestDate๋Š” "yyyyMMdd" ํฌ๋งท์œผ๋กœ ์ „๋‹ฌ๋˜์ง€๋งŒ, MonthlyRankingTableSwapTasklet์—์„œ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์„ค์ •๋œ "2026-01" (YYYY-MM ํฌ๋งท)๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ startDate๋Š” ISO_LOCAL_DATE_TIME ํฌ๋งท์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ๋‘ ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ํฌ๋งท์„ ํ†ต์ผํ•˜๊ฑฐ๋‚˜ ๊ฐ ์‚ฌ์šฉ ์œ„์น˜์—์„œ ๋ช…์‹œ์ ์œผ๋กœ ํŒŒ์‹ฑํ•˜์„ธ์š”.

.map(work -> "product:snapshot:" + work.getProductId())
.toList();

List<Object> snapshots = redisTemplate.opsForValue().multiGet(keys);
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

multiGet ๊ฒฐ๊ณผ๊ฐ€ null์ผ ๊ฒฝ์šฐ NPE ๋ฐœ์ƒ ๊ฐ€๋Šฅ

Redis ์—ฐ๊ฒฐ ์‹คํŒจ ๋˜๋Š” ๊ธฐํƒ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ multiGet์ด null์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ Line 45์—์„œ snapshots.get(i) ํ˜ธ์ถœ ์‹œ NullPointerException์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
    List<Object> snapshots = redisTemplate.opsForValue().multiGet(keys);
+   if (snapshots == null) {
+     snapshots = java.util.Collections.nCopies(keys.size(), null);
+   }
๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/monthly/MonthlyRankingTableSwapTasklet.java
around line 36, multiGet can return null (e.g., on Redis failure) causing a
NullPointerException when calling snapshots.get(i); change the code to
null-check the returned List before using it (if null, log an error and either
treat as Collections.emptyList() or fail the step with a clear exception), and
also guard individual snapshot elements when iterating (skip or handle null
entries) so that accessing snapshots.get(i) never throws an NPE.

Comment on lines +11 to +25
@EnableScheduling
@ConfigurationPropertiesScan
@SpringBootApplication
public class CommerceBatchApplication {

@PostConstruct
public void started() {
// set timezone
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
}

public static void main(String[] args) {
int exitCode = SpringApplication.exit(SpringApplication.run(CommerceBatchApplication.class, args));
System.exit(exitCode);
}
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:

#!/bin/bash
# commerce-batch ๋ชจ๋“ˆ์—์„œ @Scheduled ๋ฉ”์„œ๋“œ ๊ฒ€์ƒ‰
rg -n --type=java '@Scheduled' apps/commerce-batch/

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

Length of output: 357


๐Ÿ Script executed:

#!/bin/bash
# commerce-batch ๋ชจ๋“ˆ์˜ ์ „์ฒด Java ํŒŒ์ผ ๋ชฉ๋ก ํ™•์ธ
find apps/commerce-batch/src -name "*.java" -type f | head -20

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

Length of output: 1505


๐Ÿ Script executed:

#!/bin/bash
# commerce-batch ๋ชจ๋“ˆ ๊ตฌ์กฐ ํ™•์ธ
ls -la apps/commerce-batch/src/main/java/com/loopers/

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

Length of output: 328


@EnableScheduling๊ณผ System.exit() ํŒจํ„ด์˜ ์„ค๊ณ„ ๋ชจ์ˆœ์„ ์ฆ‰์‹œ ํ•ด๊ฒฐํ•˜์„ธ์š”.

RankingScheduler์— @Scheduled(cron = "0 0 2 * * MON")๊ณผ @Scheduled(cron = "0 0 3 1 * *")๋กœ ์ •์˜๋œ ์ฃผ๊ธฐ์  ์Šค์ผ€์ค„ ์ž‘์—…์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ CommerceBatchApplication์˜ main() ๋ฉ”์„œ๋“œ์—์„œ System.exit(exitCode)๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹œ์ž‘ ํ›„ ์ฆ‰์‹œ ์ข…๋ฃŒ๋˜์–ด, ์Šค์ผ€์ค„๋œ ์ž‘์—…๋“ค์ด ์‹คํ–‰๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ:

  • @EnableScheduling์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์žฅ๊ธฐ ์‹คํ–‰๋˜์–ด์•ผ ํ•จ์„ ์˜๋ฏธ
  • System.exit(exitCode)๋Š” JVM์„ ์ฆ‰์‹œ ์ข…๋ฃŒ์‹œํ‚ด
  • ๊ฒฐ๊ณผ: ์Šค์ผ€์ค„๋œ ์ž‘์—…์ด ์ •๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋˜์ง€ ์•Š์Œ

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ:

  • main() ๋ฉ”์„œ๋“œ์—์„œ System.exit() ์ œ๊ฑฐ โ†’ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ณ„์† ์‹คํ–‰ ์ƒํƒœ๋กœ ์œ ์ง€
  • ๋˜๋Š” ๋‹ค๋ฅธ ์ข…๋ฃŒ ๋ฉ”์ปค๋‹ˆ์ฆ˜(์˜ˆ: shutdown hook, graceful shutdown) ์ ์šฉ
๐Ÿค– Prompt for AI Agents
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java
around lines 11-25: the main method calls System.exit(...) which immediately
terminates the JVM and prevents @EnableScheduling cron jobs (e.g.,
RankingScheduler) from running; remove the System.exit call (and avoid forcing
SpringApplication.exit on startup) so the application remains running to execute
scheduled tasksโ€”if you need to handle shutdown/exit codes, implement a proper
shutdown hook or call SpringApplication.exit only on explicit termination events
rather than at startup.

Comment on lines +42 to +56
@DisplayName("jobParameter ์ค‘ requestDate ์ธ์ž๊ฐ€ ์ฃผ์–ด์ง€์ง€ ์•Š์•˜์„ ๋•Œ, demoJob ๋ฐฐ์น˜๋Š” ์‹คํŒจํ•œ๋‹ค.")
@Test
void shouldNotSaveCategories_whenApiError() throws Exception {
// arrange
jobLauncherTestUtils.setJob(job);

// act
var jobExecution = jobLauncherTestUtils.launchJob();

// assert
assertAll(
() -> assertThat(jobExecution).isNotNull(),
() -> assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode())
);
}
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

ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ์ด๋ฆ„์ด ํ…Œ์ŠคํŠธ ๋‚ด์šฉ๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

@DisplayName์€ "requestDate ์ธ์ž๊ฐ€ ์ฃผ์–ด์ง€์ง€ ์•Š์•˜์„ ๋•Œ" ์‹คํŒจ๋ฅผ ํ…Œ์ŠคํŠธํ•œ๋‹ค๊ณ  ๋ช…์‹œํ•˜์ง€๋งŒ, ๋ฉ”์„œ๋“œ ์ด๋ฆ„ shouldNotSaveCategories_whenApiError๋Š” API ์˜ค๋ฅ˜์™€ ์นดํ…Œ๊ณ ๋ฆฌ ์ €์žฅ์„ ์–ธ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ์ด๋ฆ„์ด ์‹ค์ œ ๋™์ž‘์„ ๋ฐ˜์˜ํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ๋ฉ”์„œ๋“œ ์ด๋ฆ„ ๋ณ€๊ฒฝ
-    void shouldNotSaveCategories_whenApiError() throws Exception {
+    void shouldFail_whenRequestDateParameterIsMissing() throws Exception {
๐Ÿค– Prompt for AI Agents
In apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java
around lines 42 to 56, the test method name should reflect the scenario
described by the @DisplayName (failure when requestDate job parameter is
missing); rename the method from shouldNotSaveCategories_whenApiError to a name
that clearly indicates the missing requestDate causes the job to fail (e.g.,
shouldFailWhenRequestDateIsMissing or whenRequestDateIsMissing_thenJobFails) and
update any references/imports if necessary.

Comment on lines +54 to +58
metrics.updateProductSnapshot(
event.productName(),
event.price(),
event.currentStock()
);
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

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

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

Length of output: 180


๐Ÿ Script executed:

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

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

Length of output: 240


๐Ÿ Script executed:

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

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

Length of output: 158


๐Ÿ Script executed:

cat -n ./apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java

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

Length of output: 3197


๐Ÿ Script executed:

cat -n ./apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java

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

Length of output: 1501


๐Ÿ Script executed:

cat -n ./modules/kafka/src/main/java/com/loopers/event/ProductStockEvent.java

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

Length of output: 877


๐Ÿ Script executed:

cat -n ./modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.java

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

Length of output: 1881


๐Ÿ Script executed:

rg -A 5 -B 5 "ProductStockEvent\(" --type java | head -100

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

Length of output: 2088


๐Ÿ Script executed:

rg "enrichment|Enrich" -A 3 -B 3 --type java | head -100

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

Length of output: 70


๐Ÿ Script executed:

rg "ProductStockEvent\.of|new ProductStockEvent" -B 5 -A 5 --type java | head -150

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

Length of output: 4042


๐Ÿ Script executed:

cat -n ./apps/commerce-streamer/src/test/java/com/loopers/domain/metrics/IdempotencyIntegrationTest.java | head -80

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

Length of output: 2478


๐Ÿ Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderSalesAggregateListener.java

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

Length of output: 1549


productName null ์•ˆ์ •์„ฑ ๊ฒ€์ฆ ํ•„์š”

ProductMetrics.updateProductSnapshot() ๋ฉ”์„œ๋“œ(modules/jpa)๊ฐ€ productName ๋งค๊ฐœ๋ณ€์ˆ˜์— ๋Œ€ํ•œ null ์ฒดํฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” null ๊ฐ’์ด ์ง์ ‘ ํ•„๋“œ์— ํ• ๋‹น๋˜์–ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ProductStockEvent์˜ productName์ด null์ผ ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด, updateProductSnapshot ๋ฉ”์„œ๋“œ์— null ์ฒดํฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ํ˜ธ์ถœ ์ „์— ๊ฒ€์ฆํ•˜๊ธฐ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java
around lines 54-58, the call to
metrics.updateProductSnapshot(event.productName(), event.price(),
event.currentStock()) can pass a null productName and the downstream method does
not validate it; add a null-safety guard: either validate event.productName()
before calling (log and return/skip processing for null) or update
ProductMetrics.updateProductSnapshot to perform
Objects.requireNonNull(productName, "...") or a null check that throws
IllegalArgumentException or skips persistence so null never gets written to the
DB; ensure the chosen approach logs context (event id) and prevents saving a
null productName.

Comment on lines +41 to +49
public void incrementViewCount() {
this.viewCount += 1;
this.updatedAt = LocalDateTime.now();
}

public void addSalesCount(int quantity) {
this.salesCount += quantity;
this.updatedAt = LocalDateTime.now();
}
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# Find ProductMetrics usage patterns
rg "ProductMetrics" --type java -B 2 -A 2 | head -100

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

Length of output: 11143


๐Ÿ Script executed:

# Check for @Version or lock-related annotations in ProductMetrics file
cat modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.java

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

Length of output: 1482


๐Ÿ Script executed:

# Search for repository/service methods that call incrementViewCount or addSalesCount
rg "incrementViewCount|addSalesCount" --type java -B 3 -A 3

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

Length of output: 4036


๐Ÿ Script executed:

# Check repository patterns for ProductMetrics updates
rg "ProductMetricsRepository|productMetrics.*save|productMetrics.*update" --type java -B 2 -A 5

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

Length of output: 4824


๐Ÿ Script executed:

# Get the full ProductMetricsService implementation
cat apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java

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

Length of output: 2623


๐Ÿ Script executed:

# Check for custom repository methods with lock annotations
rg "findById|findByProductId|Lock|@Query" apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.java -A 3

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

Length of output: 70


๐Ÿ Script executed:

# Check the IdempotencyIntegrationTest to understand concurrency testing
head -150 apps/commerce-streamer/src/test/java/com/loopers/domain/metrics/IdempotencyIntegrationTest.java

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

Length of output: 2030


์ด๋ฒคํŠธ ์ค‘๋ณต์€ ๋ฉฑ๋“ฑ์„ฑ์œผ๋กœ ๋ฐฉ์ง€๋˜์ง€๋งŒ, ๋™์‹œ ์ด๋ฒคํŠธ์—์„œ ์†์‹ค ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ

ํ˜„์žฌ ๊ตฌํ˜„์€ EventHandled ํ…Œ์ด๋ธ”์„ ํ†ตํ•ด ์ด๋ฒคํŠธ ID ์ค‘๋ณต์„ ๊ฐ์ง€ํ•˜๊ณ  ์ค‘๋ณต ์ฒ˜๋ฆฌ๋ฅผ ๋ฐฉ์ง€ํ•˜๋ฏ€๋กœ Kafka ์žฌ์ „์†ก ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ ์„œ๋กœ ๋‹ค๋ฅธ ์ด๋ฒคํŠธ๊ฐ€ ๋™์ผ ์ƒํ’ˆ์„ ๋™์‹œ์— ์—…๋ฐ์ดํŠธํ•  ๊ฒฝ์šฐ findById().save() ํŒจํ„ด์œผ๋กœ๋Š” ์†์‹ค์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์กฐํšŒ ์ด๋ฒคํŠธ์™€ ํŒ๋งค ์ด๋ฒคํŠธ๊ฐ€ ๋™์‹œ์— ์ฒ˜๋ฆฌ๋˜๋ฉด, ๋จผ์ € ๋กœ๋“œ๋œ ์—”ํ‹ฐํ‹ฐ์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ฎ์–ด์”Œ์›Œ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ์˜ ์ตœ์ข… ์ผ๊ด€์„ฑ์ด ์ˆ˜์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด ํ˜„์žฌ ์„ค๊ณ„๋กœ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ, ์ •ํ™•์„ฑ์ด ์ค‘์š”ํ•˜๋‹ค๋ฉด @Version ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋‚™๊ด€์  ๋ฝ์„ ์ ์šฉํ•˜๊ฑฐ๋‚˜, ์›์ž์  ์—…๋ฐ์ดํŠธ ์ฟผ๋ฆฌ(์˜ˆ: UPDATE product_metrics SET view_count = view_count + 1 WHERE product_id = ?)๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”.

Comment on lines +41 to +52
// ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ (์˜๋ฏธ ์žˆ๋Š” ์ƒ์„ฑ ๋ฐฉ์‹ ์ œ๊ณต)
public static WeeklyRankingMV createFromWork(WeeklyRankingWork work, String baseDate) {
return new WeeklyRankingMV(
baseDate,
work.getProductId(),
work.getScore(),
work.getRanking(),
"์ƒํ’ˆ๋ช… ์ž„์‹œ", // ์‹ค์ œ ๊ตฌํ˜„ ์‹œ Product ์ •๋ณด ๊ฒฐํ•ฉ ํ•„์š”
0L, // ์‹ค์ œ ๊ตฌํ˜„ ์‹œ Product ์ •๋ณด ๊ฒฐํ•ฉ ํ•„์š”
false // ์‹ค์ œ ๊ตฌํ˜„ ์‹œ Product ์ •๋ณด ๊ฒฐํ•ฉ ํ•„์š”
);
}
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

์ƒํ’ˆ ์ •๋ณด ํ”Œ๋ ˆ์ด์Šคํ™€๋” ๊ตฌํ˜„ ์™„๋ฃŒ ํ•„์š”

createFromWork ๋ฉ”์„œ๋“œ์—์„œ productName, price, isSoldOut์ด ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ž„์‹œ ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. MV์— ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋˜๋ฉด API ์‘๋‹ต ํ’ˆ์งˆ์— ์˜ํ–ฅ์„ ์ค๋‹ˆ๋‹ค. ProductSnapshot ํด๋ž˜์Šค๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•˜๋ฏ€๋กœ(MonthlyRankingMV ํŒจํ‚ค์ง€ ์ฐธ์กฐ), ์ด๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์‹ค์ œ ์ƒํ’ˆ ์ •๋ณด๋ฅผ ์ฃผ์ž…ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์ด ๊ธฐ๋Šฅ์„ ์™„๋ฃŒํ•˜๊ธฐ ์œ„ํ•œ ๊ตฌํ˜„์„ ์ƒ์„ฑํ•ด ๋“œ๋ฆด๊นŒ์š”, ์•„๋‹ˆ๋ฉด ์ถ”์ ์„ ์œ„ํ•œ ์ƒˆ ์ด์Šˆ๋ฅผ ์—ด์–ด ๋“œ๋ฆด๊นŒ์š”?

๐Ÿค– Prompt for AI Agents
In modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.java
around lines 41 to 52, the createFromWork method currently uses hardcoded
placeholders for productName, price, and isSoldOut; replace these placeholders
by obtaining a ProductSnapshot and populating those fields. Specifically, update
createFromWork to (1) attempt to get a ProductSnapshot from the
WeeklyRankingWork (if the work already carries a snapshot) or, if not present,
fetch the snapshot via the appropriate ProductRepository/ProductService method,
(2) extract productName, price, and isSoldOut from that snapshot and pass them
into the WeeklyRankingMV constructor, and (3) handle null/missing snapshot
defensively (log a warning and use sensible defaults) so the method no longer
stores hardcoded placeholder values.

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.

6 participants