Skip to content

Conversation

@Kimjipang
Copy link
Collaborator

@Kimjipang Kimjipang commented Jan 2, 2026

๐Ÿ“Œ Summary

Spring Batch

  • ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ batch ํ”„๋กœ๊ทธ๋žจ ๊ตฌํ˜„(ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ฐ˜ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ)
  • chunk ๊ธฐ๋ฐ˜ ์ฒ˜๋ฆฌ
    • reader
    • processor
    • writer
  • ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  ์ฃผ๊ฐ„/์›”๊ฐ„ Materialized View ์„ค๊ณ„

๋žญํ‚น API

  • ์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น API ์ œ๊ณต

๐Ÿ’ฌ Review Points

Q1).

ํšŒ์‚ฌ์—์„œ 1๋…„์— 2๋ฒˆ ์•ฝ๋ฌผ ๋ฐ์ดํ„ฐ(.asc ํ™•์žฅ์ž ํŒŒ์ผ)๋ฅผ ์–ด๋А ์›น์‚ฌ์ดํŠธ์—์„œ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์•„ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•ด์ฃผ๋Š” ์ž‘์—…์„ spring batch๋กœ ๊ตฌํ˜„ํ•ด๋‘” ์ƒํƒœ์ž…๋‹ˆ๋‹ค.

ํ˜„์žฌ ์ž ์‹œ ์—…๋ฌด๊ฐ€ ํ™€๋”ฉ๋œ ์ƒํƒœ์ด๊ธด ํ•˜์ง€๋งŒ ์ถ”ํ›„์— ํ•ด๋‹น ํŒŒ์ผ์„ ์›น์‚ฌ์ดํŠธ์—์„œ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์•„ ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅ์‹œํ‚ค๊ณ  S3์— ํŒŒ์ผ๋“ค์„ ์—…๋กœ๋“œ ํ•ด์•ผ ํ•˜๋Š”๋ฐ์š”!

์ด๋ฅผ ์ž๋™ํ™”ํ•œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”? ์ฐพ์•„๋ณด๋‹ˆ AWS Lambda ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๊ธฐ๋Š” ํ•˜๋˜๋ށ ์–ด๋–ค ๋ฐฉ๋ฒ•์ด ์ข‹์„๊นŒ์š”?!

Q2).

์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ํ”„๋กœ์„ธ์Šค๊ฐ€ ๋‹ค์Œ๊ณผ ๊ฐ™์€๋ฐ ์•Œ๋งž์€ ๋ฐฉ๋ฒ•์ผ๊นŒ์š”?

product_metrics ํ…Œ์ด๋ธ”์— ์ง‘๊ณ„๋ฅผ ํ•  ๋•Œ ์ง‘๊ณ„๋‚ ์งœ(metric_date)๋ฅผ ๊ฐ™์ด ์ €์žฅํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ (product_id, metric_date) ๊ฐ€ ๊ณ ์œ ํ•œ ๊ฐ’์ธ๋ฐ์š”.

Reader

  • product_metrics ํ…Œ์ด๋ธ”์—์„œ productId ๋ณ„๋กœ 7์ผ ๊ฐ„์˜ row๋ฅผ ์กฐํšŒํ•  ๋•Œ view_count, like_count, sales_volume๋ฅผ sum()ํ•ด์„œ read

Processor

  • readํ•œ ๋ฐ์ดํ„ฐ๋“ค์„ ๊ฐ๊ฐ์ด ๊ฐ€์ง„ ๊ฐ€์ค‘์น˜์— ๋”ฐ๋ผ score๋ฅผ ๊ณ„์‚ฐ

Writer

  • ๊ฐ ์กฐํšŒ์šฉ ํ…Œ์ด๋ธ”(mv)์— ์ ์žฌ

Q3).

์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋Š” API์— ๋Œ€ํ•œ ์งˆ๋ฌธ์ž…๋‹ˆ๋‹ค.

์š”๊ตฌ ์‚ฌํ•ญ์ด ์—”๋“œ ํฌ์ธํŠธ๋ฅผ ํ•˜๋‚˜๋กœ ํ†ต์ผํ•˜๊ณ  ๊ฐ๊ฐ์„ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌด์—‡์ธ๊ฐ€๋กœ ์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์ดํ•ดํ•˜๊ณ  ๊ทธ๋ ‡๊ฒŒ ์ง„ํ–‰์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์—”๋“œํฌ์ธํŠธ๊ฐ€ ๋™์ผํ•˜๋‹ค๋ณด๋‹ˆ ์‘๋‹ต ๋ณธ๋ฌธ์„ ๋งž์ถฐ์ฃผ์–ด์•ผ ํ–ˆ๊ณ  ๊ฐ๊ฐ์„ ๊ตฌ๋ถ„ํ•˜๊ณ ์ž period๋ฅผ requestParam์œผ๋กœ ๋„ฃ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  Facade์—์„œ period๊ฐ€ ์–ด๋–ค ๊ฐ’์ธ์ง€์— ๋”ฐ๋ผ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌํ•ด์„œ ๊ฐ๊ฐ์˜ ์„œ๋น„์Šค ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ํ–ˆ๋Š”๋ฐ์š”.

Facade์—์„œ ๊ฐ’์— ๋”ฐ๋ผ ๋ถ„๊ธฐํ•˜์—ฌ ๋‹ค๋ฅธ ์„œ๋น„์Šค ๋กœ์ง์„ ํ˜ธ์ถœํ•˜๋Š” ์ด ํ˜•ํƒœ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ํ˜•ํƒœ์ธ๊ฐ€์š”?

Q4). ์•„ํ‚คํ…์ฒ˜ ๊ด€๋ จ ์งˆ๋ฌธ์ž…๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•  ๋•Œ๋งˆ๋‹ค ์ด๊ฒƒ์ด ์–ด๋А ๊ณณ์— ์œ„์น˜ํ•ด์•ผ ๋˜๋Š”์ง€๊ฐ€ ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด์— ์ œ๊ณต๋˜์—ˆ๋˜ ํ…œํ”Œ๋ฆฟ์— ์กด์žฌํ•˜๋Š” ์œ ํ˜•์˜ ํด๋ž˜์Šค๋“ค์ด๋ผ๋ฉด ๊ทธ๋Œ€๋กœ ๋”ฐ๋ผํ–ˆ์ง€๋งŒ ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ฒฝ์šฐ(kafka publisher, listener, outbox ๋“ฑ๋“ฑ)์—๋Š” ํŒ๋‹จ์ด ์ž˜ ์•ˆ๋˜๋”๋ผ๊ตฌ์š” !

์•„ํ‚คํ…์ฒ˜์— ๋Œ€ํ•œ ์ดํ•ด๋„๋ฅผ ๋†’์ด๋ฉด ํ•ด๊ฒฐ์ด ๋ ๊นŒ ์‹ถ์–ด์„œ ๋ฃจํผ์Šค ์ดํ›„์— ์•„ํ‚คํ…์ฒ˜์— ๋Œ€ํ•ด์„œ ๊ณต๋ถ€ํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ณต๋ถ€๊ฐ€ ๋„์›€์ด ๋  ๊ณต๋ถ€์ธ์ง€์™€ ๋„์›€์ด ๋œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๊ณต๋ถ€ํ•˜๋ฉด ์ข‹์„์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค. (ex. ์„œ์  ์ถ”์ฒœ)

Q5). ๋ฃจํผ์Šค ์ดํ›„ ๋ฐฉํ–ฅ์„ฑ์— ๋Œ€ํ•œ ์งˆ๋ฌธ

๐Ÿง‘โ€๐Ÿ’ป ์ฒซ ๋ฒˆ์งธ ์งˆ๋ฌธ

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

ํ‰์†Œ gpt๋กœ ์ด๋ก ์ ์ธ ํ•™์Šต์„ ๋งŽ์ด ํ•˜๋Š” ํŽธ์ธ๋ฐ์š”.

์•„๋ฌด๋ž˜๋„ gpt๊ฐ€ ๊ฑฐ์ง“์„ ๋งํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ถ„๋ช… ์žˆ๋‹ค๋ณด๋‹ˆ ํ•ด๋‹น ๋ฐฉ๋ฒ•์ด ์ข‹์€ ๊ฒƒ์€ ์•„๋‹๊นŒ ํ•˜๋Š” ์ƒ๊ฐ๋„ ๋“ญ๋‹ˆ๋‹ค. ๋ฉ˜ํ† ๋‹˜์€ ์–ด๋–ป๊ฒŒ ๊ณต๋ถ€ํ•˜์‹œ๋‚˜์š”?

๐Ÿง‘โ€๐Ÿ’ป ๋‘ ๋ฒˆ์งธ ์งˆ๋ฌธ

์ดํ›„์— ๋ฃจํผ์Šค ๋ฐœ์ œ ์ž๋ฃŒ๋ฅผ ์ฐธ๊ณ ํ•˜๋ฉฐ ๋‹ค์‹œ 1์ฃผ์ฐจ๋ถ€ํ„ฐ ๋ฐŸ์•„๋ณด๋ฉฐ ๋ชปํ–ˆ๋˜ ๊ฒƒ๊ณผ ๊ถ๊ธˆํ–ˆ๋˜ ๊ฒƒ์— ๋Œ€ํ•ด ๊นŠ์ด ํ•™์Šตํ•ด๋ณด๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

ํ•˜๋ฉด์„œ ๋„์›€์ด ๋  ๋งŒํ•œ ์„œ์ ์ด๋‚˜ ๋ฐฉ๋ฒ•์— ๋ฌด์—‡์ด ์žˆ์„๊นŒ์š”?

๐Ÿง‘โ€๐Ÿ’ป ์„ธ ๋ฒˆ์งธ ์งˆ๋ฌธ

๋ฉ˜ํ† ๋‹˜์ด ์ƒ๊ฐํ•˜๋Š” ์ €๋Š” ์–ด๋–ค ์‚ฌ๋žŒ์ธ๊ฐ€์š”? (ํƒœ๋„, ์‹ค๋ ฅ ๋“ฑ์„ ์žฅ๋‹จ์ ์œผ๋กœ ๊ฐ€๊ฐ์—†์ด ์†”์งํ•˜๊ฒŒ ๋ง์”€ ๋ถ€ํƒ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ใ…Žใ…Ž.. ๋งค์šด๋ง›๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค!)

์•„์ง์€ ๋ชจ๋ฅด๊ฒ ๋‹ค ์‹ถ์œผ์‹œ๋‹ค๋ฉด ํŽธํžˆ ๋ง์”€ ์ฃผ์…”๋„ ๋ฉ๋‹ˆ๋‹ค.

๐Ÿง‘โ€๐Ÿ’ป ๋งˆ๋ฌด๋ฆฌ..

10์ฃผ๊ฐ„ ์ •๋ง ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค ใ…Žใ…Ž ํ•ญ์ƒ ์œ ์พŒํ•˜๊ฒŒ ๋ฉ˜ํ† ๋ง ๋ฐ›์•˜์—ˆ๋„ค์š”.

2025๋…„๋„ ๊ณ ์ƒ ๋งŽ์œผ์…จ๊ตฌ ๋‹ค๊ฐ€์˜จ ์ƒˆํ•ด์—๋Š” ๋”์šฑ ๋งŽ์€ ํ–‰๋ณต์„ ์ฐพ์•„๊ฐ€์‹œ๊ธธ ์ง„์‹ฌ์œผ๋กœ ๋ฐ”๋ผ๊ฒ ์Šต๋‹ˆ๋‹ค !

โœ… Checklist

๐Ÿงฑ Spring Batch

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

๐Ÿงฉ Ranking API

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

๐Ÿ“Ž References

Summary by CodeRabbit

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

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

    • ์ผ์ผ, ์ฃผ๊ฐ„, ์›”๊ฐ„ ์ƒํ’ˆ ์ˆœ์œ„ ์กฐํšŒ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
    • ์ƒํ’ˆ ์ˆœ์œ„ API ๊ฐœ์„  (๋‚ ์งœ ๋ฐ ๊ธฐ๊ฐ„ ์„ ํƒ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€)
  • ๊ฐœ์„  ์‚ฌํ•ญ

    • ์ˆœ์œ„ ๋ฐ์ดํ„ฐ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ตœ์ ํ™”
  • ์ œ๊ฑฐ๋จ

    • ๋ฐ๋ชจ ์ž‘์—… ๋ฐ ๊ด€๋ จ ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ

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

@Kimjipang Kimjipang self-assigned this Jan 2, 2026
@Kimjipang Kimjipang added the enhancement New feature or request label Jan 2, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

Walkthrough

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

Changes

Cohort / File(s) ์š”์•ฝ
๋žญํ‚น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค
DailyRankingService.java, WeeklyRankingService.java, MonthlyRankingService.java
Redis ZSET๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ๋ถ€ํ„ฐ ์ผ์ผ, ์ฃผ๊ฐ„, ์›”๊ฐ„ ์ƒํ’ˆ ๋žญํ‚น์„ ์กฐํšŒํ•˜๋Š” ์ƒˆ๋กœ์šด ์„œ๋น„์Šค. ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ •๊ทœํ™”, ๋‚ ์งœ ๋ฒ”์œ„ ๊ณ„์‚ฐ, RankingV1Dto ๋งคํ•‘ ์ˆ˜ํ–‰
๋žญํ‚น ๋„๋ฉ”์ธ ๋ชจ๋ธ
Period.java, WeeklyRankingMv.java, MonthlyRankingMv.java
๋žญํ‚น ๊ธฐ๊ฐ„(DAILY, WEEKLY, MONTHLY) ์—ด๊ฑฐํ˜•๊ณผ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ๋งคํ•‘ํ•˜๋Š” JPA ์—”ํ‹ฐํ‹ฐ ์ถ”๊ฐ€
๋žญํ‚น ์ €์žฅ์†Œ ์ธํ„ฐํŽ˜์ด์Šค
WeeklyRankingRepository.java, MonthlyRankingRepository.java
์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ๋ฐ ๊ฐœ์ˆ˜ ์ง‘๊ณ„๋ฅผ ์œ„ํ•œ ๋„๋ฉ”์ธ ์ €์žฅ์†Œ ๊ณ„์•ฝ ์ •์˜
์ €์žฅ์†Œ ๊ตฌํ˜„
WeeklyRankingJpaRepository.java, WeeklyRankingRepositoryImpl.java, MonthlyRankingJpaRepository.java, MonthlyRankingRepositoryImpl.java
QueryDSL ๊ธฐ๋ฐ˜ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ๊ตฌํ˜„ ๋ฐ Spring Data JPA ์ธํ„ฐํŽ˜์ด์Šค
RankingFacade ๋ฐ DTO
RankingFacade.java, RankingV1Dto.java
์„ธ ๊ฐ€์ง€ ๊ธฐ๊ฐ„๋ณ„ ์„œ๋น„์Šค๋ฅผ ์ฃผ์ž…๋ฐ›์•„ ์œ„์ž„ํ•˜๋„๋ก ๋ฆฌํŒฉํ† ๋ง. ProductRankingPageResponse์— period, startDate, endDate ํ•„๋“œ ์ถ”๊ฐ€
API ๊ณ„์ธต
RankingV1ApiSpec.java, RankingV1Controller.java
์—”๋“œํฌ์ธํŠธ๋ฅผ getProductRanking(date, period, page, size)๋กœ ๋ณ€๊ฒฝ. date์™€ period ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
๋ฐฐ์น˜ ์„ค์ •
BatchConfig.java
์ฃผ๊ฐ„/์›”๊ฐ„ ์ƒํ’ˆ ์ง€ํ‘œ ์ง‘๊ณ„๋ฅผ ์œ„ํ•œ Spring Batch ์ž‘์—… ์„ค์ •. ์‚ญ์ œ ๋‹จ๊ณ„ ๋ฐ ์ง‘๊ณ„ ๋‹จ๊ณ„ ํฌํ•จ
๋ฐฐ์น˜ DTO ๋ฐ ์ฒ˜๋ฆฌ
ProductMetricsAgg.java, ProductRankRow.java, ProductMetricProcessor.java
์ง‘๊ณ„ ์ง€ํ‘œ๋ฅผ ๋‹ด๋Š” DTO์™€ ๊ฐ€์ค‘ ์ ์ˆ˜ ๊ณ„์‚ฐ ํ”„๋กœ์„ธ์„œ ์ถ”๊ฐ€
๋ฐฐ์น˜ ์ฝ๊ธฐ ๋ฐ ์“ฐ๊ธฐ
ProductMetricReader.java, ProductMetricWeeklyWriter.java, ProductMetricMonthlyWriter.java
JDBC ๊ธฐ๋ฐ˜ ์ง€ํ‘œ ์ฝ๊ธฐ ๋ฐ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ํ…Œ์ด๋ธ” ์“ฐ๊ธฐ ๊ตฌํ˜„
๋ฐ๋ชจ ์ž‘์—… ์ œ๊ฑฐ
DemoJobConfig.java, DemoTasklet.java, ChunkListener.java, JobListener.java, StepMonitorListener.java
๋ฏธ์‚ฌ์šฉ ๋ฐฐ์น˜ ๋ฐ๋ชจ ๋ฐ ๋ฆฌ์Šค๋„ˆ ์ปดํฌ๋„ŒํŠธ ์ œ๊ฑฐ

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as RankingV1Controller
    participant Facade as RankingFacade
    participant Service as DailyRankingService
    participant Redis as Redis ZSET
    participant DTO as Response DTO

    Client->>Controller: GET /rankings?date=20260101&period=DAILY&page=1&size=20
    Controller->>Facade: getProductRanking(date, DAILY, 1, 20)
    Facade->>Service: getDailyRanking(20260101, DAILY, 1, 20)
    Service->>Redis: reverseRangeWithScores(ranking:all:20260101, 0, 19)
    Redis-->>Service: List<ZSetOperations.TypedTuple>
    Service->>Service: ์ ์ˆ˜๋ณ„ ์ •๋ ฌ ๋ฐ ์ˆœ์œ„ ๊ณ„์‚ฐ
    Service->>DTO: ProductRankingPageResponse ์ƒ์„ฑ
    DTO-->>Service: ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ
    Service-->>Facade: ProductRankingPageResponse
    Facade-->>Controller: ProductRankingPageResponse
    Controller-->>Client: ApiResponse ๋ฐ˜ํ™˜
Loading
sequenceDiagram
    participant Batch as Batch Job
    participant Config as BatchConfig
    participant Reader as ProductMetricReader
    participant Processor as ProductMetricProcessor
    participant DB as Database
    participant Writer as ProductMetricWeeklyWriter
    participant Target as mv_product_rank_weekly

    Batch->>Config: weeklyProductRankingJob ์‹คํ–‰
    Config->>Config: deleteWeeklyMvStep ์‹คํ–‰
    Config->>DB: DELETE FROM mv_product_rank_weekly
    DB-->>Config: ์‚ญ์ œ ์™„๋ฃŒ
    
    Config->>Config: aggregateWeeklyRankStep ์‹คํ–‰
    Config->>Reader: ์ฃผ๊ฐ„ ์ง€ํ‘œ ์ฝ๊ธฐ (JdbcPagingItemReader)
    Reader->>DB: SELECT ์ง‘๊ณ„ ์ง€ํ‘œ
    DB-->>Reader: ProductMetricsAgg ์ฒญํฌ
    Reader-->>Processor: ์ฒญํฌ ์ „๋‹ฌ
    
    Processor->>Processor: ๊ฐ€์ค‘ ์ ์ˆ˜ ๊ณ„์‚ฐ<br/>(view: 0.1, like: 0.3, sales: 0.6)
    Processor-->>Writer: ProductRankRow ๋ณ€ํ™˜
    
    Writer->>Writer: MapSqlParameterSource ๋ฐฐ์น˜ ๊ตฌ์„ฑ
    Writer->>Target: batchUpdate (INSERT)
    Target-->>Writer: ์‚ฝ์ž… ์™„๋ฃŒ
    Writer-->>Batch: ๋ฐฐ์น˜ ์“ฐ๊ธฐ ์™„๋ฃŒ
Loading

Estimated code review effort

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

Possibly related PRs

  • [volume-9] Product Ranking with Redis ย #219: ์ƒํ’ˆ ๋žญํ‚น ๊ธฐ๋Šฅ์„ Daily/Weekly/Monthly ์„œ๋น„์Šค๋กœ ํ™•์žฅํ•˜๊ณ  RankingFacade ๋ฐ DTO๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฉ”์ธ PR๊ณผ ๋™์ผํ•œ ๋žญํ‚น API ๋ฐ ๋กœ์ง์„ ์ˆ˜์ •
  • [volume-9] Product Ranking with Redisย #211: ์ƒˆ๋กœ์šด Daily/Weekly/Monthly ๋žญํ‚น ์„œ๋น„์Šค๊ฐ€ "ranking:all:{date}" Redis ZSET์„ ์ฝ๊ณ  ํŽ˜์ด์ง€๋ณ„ ์‘๋‹ต์„ ๋นŒ๋“œํ•˜๋Š” ๋ฐฉ์‹๊ณผ, ์ด์ „ Redis ๊ธฐ๋ฐ˜ RankingFacade ๋ฐ ๊ด€๋ จ ๋žญํ‚น ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋™์ผํ•œ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์ฝ”๋“œ ์ˆ˜์ค€์—์„œ ์ง์ ‘ ์—ฐ๊ด€
  • [volume-9] Product Ranking with Redis ย #227: RankingFacade ๋ฐ ๋žญํ‚น API/DTO๋ฅผ ์ˆ˜์ •ํ•˜์—ฌ ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ ๋ฐฉ์‹์„ ๋ณ€๊ฒฝ(Redis ๊ธฐ๋ฐ˜ ๋กœ์ง์„ Daily/Weekly/Monthly ์„œ๋น„์Šค ์œ„์ž„์œผ๋กœ ๊ต์ฒด)

Poem

๐Ÿฐ ์‚ฐํ† ๋ผ ํŽ„์ฉํŽ„์ฉ ๋›ฐ๋ฉฐ
์ผ์ฃผ์›” ๋žญํ‚น ์ƒˆ๋กœ ์ •๋ ฌ,
Redis์—์„œ DB๋กœ ์ถค์„ ์ถ˜๋‹ค๋„ค.
๋ฐฐ์น˜๋กœ ๋ฌต์€ ๋ฐ์ดํ„ฐ ์”ป๊ณ ,
API๋Š” ๋” ์šฐ์•„ํ•˜๊ฒŒ! โœจ
๋ ˆ์ด์–ด๋งˆ๋‹ค ์ฑ…์ž„ ๋‚˜๋ˆ„๋‹ˆ,
์ฝ”๋“œ ์ •์› ํ™œ์ง ํ”ผ์—ˆ๋‹ค.

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check โ“ Inconclusive PR ์ œ๋ชฉ '[volume-10] Collect, Stack, Zip'์€ ๋ชจํ˜ธํ•˜๊ณ  ์ผ๋ฐ˜์ ์ด์–ด์„œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์˜ ํ•ต์‹ฌ์„ ๋ช…ํ™•ํžˆ ์ „๋‹ฌํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. Spring Batch ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ๊ธฐ๋Šฅ๊ณผ ํ†ตํ•ฉ ๋žญํ‚น API ๊ตฌํ˜„์„ ๊ตฌ์ฒด์ ์œผ๋กœ ๋ช…์‹œํ•˜๋Š” ์ œ๋ชฉ์œผ๋กœ ์ˆ˜์ •ํ•˜์„ธ์š”. ์˜ˆ: '[volume-10] Implement weekly/monthly ranking batch and unified ranking API'
โœ… 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: 11

โ™ป๏ธ Duplicate comments (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (1)

21-89: MonthlyRankingService์™€ ์ฝ”๋“œ ๊ตฌ์กฐ ๋™์ผ

์ด ๋ฉ”์„œ๋“œ๋Š” MonthlyRankingService์˜ getMonthlyTop100๊ณผ ๊ตฌ์กฐ์ ์œผ๋กœ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ์•ž์„œ MonthlyRankingService ๋ฆฌ๋ทฐ(Lines 21-89)์—์„œ ์–ธ๊ธ‰ํ–ˆ๋“ฏ์ด, ๊ณตํ†ต ๋กœ์ง์„ ์ถ”์ƒํ™”ํ•˜์—ฌ ๋‘ ์„œ๋น„์Šค ๊ฐ„ ์ค‘๋ณต์„ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

Based on learnings, ์ด๋ฏธ MonthlyRankingService์—์„œ ๋™์ผํ•œ ๋ฆฌํŒฉํ† ๋ง ์ œ์•ˆ์„ ํ–ˆ์œผ๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ๋Š” ๊ฐ„๋‹จํžˆ ์ฐธ์กฐ๋งŒ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿงน Nitpick comments (14)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (1)

19-21: ์„œ๋น„์Šค ๊ณ„์ธต๊ณผ ์ค‘๋ณต๋œ ๊ฒ€์ฆ ๋กœ์ง

safePage์™€ safeSize ๊ฒ€์ฆ์ด WeeklyRankingService์˜ 9-10๋ฒˆ ๋ผ์ธ์—์„œ๋„ ๋™์ผํ•˜๊ฒŒ ์ˆ˜ํ–‰๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ณ„์ธต์—์„œ์˜ ๋ฐฉ์–ด์  ๊ฒ€์ฆ์€ ์œ ์šฉํ•˜์ง€๋งŒ, ์„œ๋น„์Šค์—์„œ ์ด๋ฏธ ์ •๊ทœํ™”๋œ ๊ฐ’์„ ๋ฐ›๋Š”๋‹ค๋ฉด ์ค‘๋ณต ์ฒ˜๋ฆฌ์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ: ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋Š” ์ด๋ฏธ ๊ฒ€์ฆ๋œ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‹ ๋ขฐ
     @Override
     public List<WeeklyRankingMv> getWeeklyTop100(String endDate, String startDate, int page, int size) {
-        int safePage = Math.max(page, 1);
-        int safeSize = Math.max(size, 1);
-
-        long offset = (long) (safePage - 1) * safeSize;
+        long offset = (long) (page - 1) * size;
 
         return queryFactory
                 .selectFrom(weeklyRankingMv)
                 .where(
                         weeklyRankingMv.startDate.eq(startDate)
                                 .and(weeklyRankingMv.endDate.eq(endDate)))
                 .orderBy(
                         weeklyRankingMv.score.desc(),
                         weeklyRankingMv.productId.asc()
                 )
                 .offset(offset)
-                .limit(safeSize)
+                .limit(size)
                 .fetch();
     }
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.java (1)

49-50: ์ŠคํŠธ๋ฆผ ๋‚ด๋ถ€์—์„œ ๋ฐ˜๋ณต์ ์ธ ZonedDateTime.now() ํ˜ธ์ถœ

๊ฐ ์•„์ดํ…œ๋งˆ๋‹ค ZonedDateTime.now()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ๋Œ€์šฉ๋Ÿ‰ ๋ฐฐ์น˜์—์„œ ๋ถˆํ•„์š”ํ•œ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ŠคํŠธ๋ฆผ ์™ธ๋ถ€์—์„œ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด ๋” ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ: ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ํ•œ ๋ฒˆ๋งŒ ์ƒ์„ฑ
     @Override
     public void write(Chunk<? extends ProductRankRow> chunk) {
+        ZonedDateTime now = ZonedDateTime.now();
         MapSqlParameterSource[] batch = chunk.getItems().stream()
                 .map(item -> new MapSqlParameterSource()
                         .addValue("startDate", startDate)
                         .addValue("endDate", endDate)
                         .addValue("productId", item.productId())
                         .addValue("viewCountSum", item.viewCountSum())
                         .addValue("likeCountSum", item.likeCountSum())
                         .addValue("salesVolumeSum", item.salesVolumeSum())
                         .addValue("score", item.score())
-                        .addValue("createdAt", ZonedDateTime.now())
-                        .addValue("updatedAt", ZonedDateTime.now()))
+                        .addValue("createdAt", now)
+                        .addValue("updatedAt", now))
                 .toArray(MapSqlParameterSource[]::new);
 
         jdbcTemplate.batchUpdate(SQL, batch);
     }
apps/commerce-batch/src/main/java/com/loopers/batch/dto/ProductMetricsAgg.java (1)

3-8: productId ํƒ€์ž… ์ผ๊ด€์„ฑ ๊ฒ€ํ† 

productId๋Š” Long ๋ž˜ํผ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ํ•„๋“œ๋“ค์€ long ๊ธฐ๋ณธํ˜•์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ProductMetricReader(line 36-40)์—์„œ rs.getLong("ref_product_id")๋กœ ๋งคํ•‘๋˜๋ฉฐ, SQL ๊ฒฐ๊ณผ๊ฐ€ NULL์ผ ๊ฒฝ์šฐ 0์œผ๋กœ ๋ฐ˜ํ™˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. productId๊ฐ€ ์‹ค์ œ๋กœ NULL์ผ ์ˆ˜ ์žˆ๋‹ค๋ฉด Long์ด ์ ์ ˆํ•˜์ง€๋งŒ, ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด long์œผ๋กœ ํ†ต์ผํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ: NULL์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด ๊ธฐ๋ณธํ˜•์œผ๋กœ ํ†ต์ผ
 public record ProductMetricsAgg(
-        Long productId,
+        long productId,
         long sumLike,
         long sumView,
         long sumSales
 ) {
 }
apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingRepository.java (1)

6-6: ๋ฉ”์„œ๋“œ ์ด๋ฆ„ "Top100"์ด page/size ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ๋ชจ์ˆœ๋ฉ๋‹ˆ๋‹ค

๋ฉ”์„œ๋“œ ์ด๋ฆ„์ด getWeeklyTop100์ด์ง€๋งŒ page์™€ size ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์•„ 100๊ฐœ ์ด์ƒ ๋˜๋Š” ์ดํ•˜์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ํ˜ผ๋ž€์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, getWeeklyTop100(date, lastWeek, 0, 50)์„ ํ˜ธ์ถœํ•˜๋ฉด 50๊ฐœ๋งŒ ๋ฐ˜ํ™˜๋˜๊ณ , getWeeklyTop100(date, lastWeek, 1, 100)์€ 101~200๋ฒˆ์งธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋ช…ํ™•ํ•œ ๋ฉ”์„œ๋“œ ์ด๋ฆ„์œผ๋กœ ๋ณ€๊ฒฝ ์ œ์•ˆ
-    List<WeeklyRankingMv> getWeeklyTop100(String date, String lastWeek, int page, int size);
+    List<WeeklyRankingMv> getWeeklyRankings(LocalDate endDate, LocalDate startDate, int page, int size);
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)

21-27: date ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • ๊ณ ๋ ค

date ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์ˆ˜๊ฐ’์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์œผ๋‚˜ ํ˜•์‹ ๊ฒ€์ฆ์ด ์—†์Šต๋‹ˆ๋‹ค. DailyRankingService, WeeklyRankingService, MonthlyRankingService์—์„œ BASIC_ISO_DATE ํ˜•์‹(์˜ˆ: "20260102")์„ ๊ธฐ๋Œ€ํ•˜๊ณ  ์žˆ์–ด, ์ž˜๋ชป๋œ ํ˜•์‹์˜ ๋‚ ์งœ๊ฐ€ ์ „๋‹ฌ๋˜๋ฉด DateTimeParseException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ size์™€ page ํŒŒ๋ผ๋ฏธํ„ฐ์— ๊ธฐ๋ณธ๊ฐ’์ด ์—†์–ด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ƒ๋žตํ•˜๋ฉด ์š”์ฒญ์ด ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ธฐ๋ณธ๊ฐ’ ์ถ”๊ฐ€ ๋ฐ ๋‚ ์งœ ํ˜•์‹ ๊ฒ€์ฆ ์ œ์•ˆ
     public ApiResponse<RankingV1Dto.ProductRankingPageResponse> getProductRanking(
-            @RequestParam String date,
+            @RequestParam(defaultValue = "#{T(java.time.LocalDate).now().format(T(java.time.format.DateTimeFormatter).BASIC_ISO_DATE)}") String date,
             @RequestParam(defaultValue = "DAILY") Period period,
-            @RequestParam int size,
-            @RequestParam int page
+            @RequestParam(defaultValue = "20") int size,
+            @RequestParam(defaultValue = "1") int page
     ) {
apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java (1)

18-18: Period ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ

period ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ฉ”์„œ๋“œ์— ์ „๋‹ฌ๋˜์ง€๋งŒ, ์‘๋‹ต์—์„œ๋Š” ํ•ญ์ƒ Period.DAILY๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค (๋ผ์ธ 29, 37, 60). ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ์ „๋‹ฌ๋œ ๊ฐ’์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ผ๊ด€์„ฑ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java (1)

49-50: ๋ฐฐ์น˜ ๋‚ด ํƒ€์ž„์Šคํƒฌํ”„ ์ผ๊ด€์„ฑ ๊ณ ๋ ค

ZonedDateTime.now()๊ฐ€ ๊ฐ ์•„์ดํ…œ ๋งคํ•‘ ์‹œ ํ˜ธ์ถœ๋˜์–ด ๋ฐฐ์น˜ ๋‚ด ๋ ˆ์ฝ”๋“œ ๊ฐ„ ๋ฏธ์„ธํ•œ ์‹œ๊ฐ„ ์ฐจ์ด๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๊ด€๋œ ํƒ€์ž„์Šคํƒฌํ”„๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ๋ฐฐ์น˜ ์‹œ์ž‘ ์‹œ์ ์˜ ์‹œ๊ฐ„์„ ์บก์ฒ˜ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ผ๊ด€๋œ ํƒ€์ž„์Šคํƒฌํ”„ ์‚ฌ์šฉ ์˜ˆ์‹œ
     @Override
     public void write(Chunk<? extends ProductRankRow> chunk) {
+        ZonedDateTime now = ZonedDateTime.now();
         MapSqlParameterSource[] batch = chunk.getItems().stream()
                 .map(item -> new MapSqlParameterSource()
                         .addValue("startDate", startDate)
                         .addValue("endDate", endDate)
                         .addValue("productId", item.productId())
                         .addValue("viewCountSum", item.viewCountSum())
                         .addValue("likeCountSum", item.likeCountSum())
                         .addValue("salesVolumeSum", item.salesVolumeSum())
                         .addValue("score", item.score())
-                        .addValue("createdAt", ZonedDateTime.now())
-                        .addValue("updatedAt", ZonedDateTime.now()))
+                        .addValue("createdAt", now)
+                        .addValue("updatedAt", now))
                 .toArray(MapSqlParameterSource[]::new);

         jdbcTemplate.batchUpdate(SQL, batch);
     }
apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.java (1)

20-24: Exception ๋Œ€์‹  ๊ตฌ์ฒด์ ์ธ ์˜ˆ์™ธ ํƒ€์ž… ์‚ฌ์šฉ ๊ถŒ์žฅ

throws Exception์€ ๋„ˆ๋ฌด ๊ด‘๋ฒ”์œ„ํ•ฉ๋‹ˆ๋‹ค. provider.getObject()์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์ฒด์ ์ธ ์˜ˆ์™ธ ํƒ€์ž…์„ ๋ช…์‹œํ•˜๋ฉด ํ˜ธ์ถœ์ž๊ฐ€ ๋” ๋ช…ํ™•ํ•˜๊ฒŒ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

18-19: ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ ํ˜ผ๋ž€ ๋ฐ ๋ฉ”์„œ๋“œ๋ช… ์˜คํ•ด ์†Œ์ง€

๋‘ ๊ฐ€์ง€ ๊ฐœ์„  ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ: getMonthlyTop100(String endDate, String startDate, ...) ์ˆœ์„œ๊ฐ€ ์ง๊ด€์ ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ startDate๊ฐ€ ๋จผ์ € ์˜ค๋Š” ๊ฒƒ์ด ์ž์—ฐ์Šค๋Ÿฝ์Šต๋‹ˆ๋‹ค. WeeklyRankingRepositoryImpl๋„ ๋™์ผํ•œ ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด ์ผ๊ด€์„ฑ์€ ์žˆ์ง€๋งŒ, ํ˜ธ์ถœ๋ถ€์—์„œ ์‹ค์ˆ˜ํ•˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค.

  2. ๋ฉ”์„œ๋“œ๋ช…: getMonthlyTop100์ด๋ผ๋Š” ์ด๋ฆ„์ด์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” size ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋”ฐ๋ผ ๊ฐœ์ˆ˜๊ฐ€ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค. getMonthlyRankings ๋˜๋Š” getMonthlyRankingPage๊ฐ€ ๋” ์ •ํ™•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ ์ˆ˜์ • ์ œ์•ˆ
     @Override
-    public List<MonthlyRankingMv> getMonthlyTop100(String endDate, String startDate, int page, int size) {
+    public List<MonthlyRankingMv> getMonthlyTop100(String startDate, String endDate, int page, int size) {

์ฐธ๊ณ : ์ด ๋ณ€๊ฒฝ์€ ์ธํ„ฐํŽ˜์ด์Šค MonthlyRankingRepository์™€ ํ˜ธ์ถœ๋ถ€ MonthlyRankingService์—๋„ ์ ์šฉ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

21-89: WeeklyRankingService์™€์˜ ์ฝ”๋“œ ์ค‘๋ณต ์ œ๊ฑฐ ๊ถŒ์žฅ

์ด ๋ฉ”์„œ๋“œ๋Š” WeeklyRankingService์˜ getWeeklyTop100๊ณผ ๊ตฌ์กฐ๊ฐ€ ๊ฑฐ์˜ ๋™์ผํ•ฉ๋‹ˆ๋‹ค(ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋กœ์ง, ๋นˆ ์‘๋‹ต ์ฒ˜๋ฆฌ, ๋žญํ‚น ๋งคํ•‘ ๋“ฑ). ๊ณตํ†ต ๋กœ์ง์„ ์ถ”์ƒํ™”ํ•˜์—ฌ ์ค‘๋ณต์„ ์ œ๊ฑฐํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ๋ฆฌํŒฉํ† ๋ง ๋ฐฉํ–ฅ ์ œ์•ˆ

๊ณตํ†ต ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฐ ๋žญํ‚น ๋งคํ•‘ ๋กœ์ง์„ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

// ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋˜๋Š” ์ถ”์ƒ ๋ฒ ์ด์Šค ํด๋ž˜์Šค์— ์ถ”๊ฐ€
protected <T extends RankingMv> RankingV1Dto.ProductRankingPageResponse 
    buildRankingResponse(
        String date, 
        Period period,
        String startDate, 
        String endDate,
        int page, 
        int size,
        long totalElements,
        List<T> rows,
        Function<T, Long> productIdExtractor,
        Function<T, BigDecimal> scoreExtractor
    ) {
    // ๊ณตํ†ต ๋กœ์ง ๊ตฌํ˜„
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Weekly/Monthly ์„œ๋น„์Šค์—์„œ ์ค‘๋ณต๋œ ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฐ DTO ๋งคํ•‘ ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

19-23: ๋‚ ์งœ ํ•„๋“œ๋ฅผ String์œผ๋กœ ์ €์žฅ

startDate์™€ endDate๊ฐ€ String ํƒ€์ž…์ž…๋‹ˆ๋‹ค. BASIC_ISO_DATE ํฌ๋งท(yyyyMMdd)์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ด์ง€๋งŒ, ํƒ€์ž… ์•ˆ์ •์„ฑ๊ณผ DB ์ˆ˜์ค€์˜ ๋‚ ์งœ ์—ฐ์‚ฐ์„ ์œ„ํ•ด LocalDate ๋˜๋Š” DB์˜ DATE ํƒ€์ž… ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌ์กฐ์˜ ์žฅ์ : String ๊ธฐ๋ฐ˜ ๋‚ ์งœ๋Š” ์ •๋ ฌ๊ณผ ๋น„๊ต๊ฐ€ ๊ฐ„๋‹จํ•˜๊ณ  ํฌ๋งท ์ œ์–ด๊ฐ€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

๋Œ€์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ: ๋งŒ์•ฝ ํ–ฅํ›„ ๋‚ ์งœ ๋ฒ”์œ„ ์ฟผ๋ฆฌ๋‚˜ ๋‚ ์งœ ์—ฐ์‚ฐ์ด ๋ณต์žกํ•ด์ง„๋‹ค๋ฉด, LocalDate๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด ๋” ์•ˆ์ „ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ํ˜„์žฌ ์‚ฌ์šฉ ํŒจํ„ด(๋‹จ์ˆœ equality ๋น„๊ต)์—์„œ๋Š” String๋„ ์ถฉ๋ถ„ํžˆ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/config/BatchConfig.java (3)

57-74: ์‚ญ์ œ ์ž‘์—…์— ๋Œ€ํ•œ ๋กœ๊น… ์ถ”๊ฐ€๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

DELETE ์ž‘์—…์˜ ์˜ํ–ฅ์„ ๋ฐ›์€ ํ–‰ ์ˆ˜๋ฅผ ๋กœ๊น…ํ•˜๋ฉด ์šด์˜ ์‹œ ๋ฌธ์ œ ์ถ”์ ์— ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค. jdbcTemplate.update()๋Š” ์˜ํ–ฅ๋ฐ›์€ ํ–‰ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋กœ๊น… ์ถ”๊ฐ€ ์ œ์•ˆ
     @Bean
     @StepScope
     public Tasklet deleteWeeklyMvTasklet(
             @Value("#{jobParameters['weeklyStartDate']}") String startDate,
             @Value("#{jobParameters['weeklyEndDate']}") String endDate
     ) {
         return (contribution, chunkContext) -> {
             String sql = """
             DELETE FROM mv_product_rank_weekly
             WHERE start_date = :weeklyStartDate AND end_date = :weeklyEndDate
             """;
-            jdbcTemplate.update(sql, Map.of(
+            int deletedCount = jdbcTemplate.update(sql, Map.of(
                     "weeklyStartDate", startDate,
                     "weeklyEndDate", endDate
             ));
+            // log.info("Deleted {} rows from mv_product_rank_weekly for period {} ~ {}", deletedCount, startDate, endDate);
             return RepeatStatus.FINISHED;
         };
     }

76-84: Fault tolerance ์ •์ฑ… ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

ํ˜„์žฌ chunk ์ฒ˜๋ฆฌ ์ค‘ ๋‹จ์ผ ์•„์ดํ…œ ์‹คํŒจ ์‹œ ์ „์ฒด Step์ด ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜(๋„คํŠธ์›Œํฌ, ๋ฐ๋“œ๋ฝ ๋“ฑ)์— ๋Œ€ํ•œ retry ์ •์ฑ…์ด๋‚˜, ํŠน์ • ์˜ˆ์™ธ์— ๋Œ€ํ•œ skip ์ •์ฑ… ์ถ”๊ฐ€๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž Fault tolerance ์„ค์ • ์˜ˆ์‹œ
     @Bean
     public Step aggregateWeeklyRankStep(@Qualifier("weeklyProductMetricsAggReader") JdbcPagingItemReader<ProductMetricsAgg> weeklyProductMetricsAggReader) {
         return new StepBuilder("aggregateWeeklyRankStep", jobRepository)
                 .<ProductMetricsAgg, ProductRankRow>chunk(1000, transactionManager)
                 .reader(weeklyProductMetricsAggReader)
                 .processor(productMetricProcessor)
                 .writer(productMetricWeeklyWriter)
+                .faultTolerant()
+                .retryLimit(3)
+                .retry(Exception.class)  // ํ•„์š”์— ๋”ฐ๋ผ ํŠน์ • ์˜ˆ์™ธ๋กœ ์ œํ•œ
                 .build();
     }

96-148: ์ฝ”๋“œ ์ค‘๋ณต ๊ฐœ์„ ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

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

๐Ÿ”Ž ๋ฆฌํŒฉํ† ๋ง ์•„์ด๋””์–ด
// ์˜ˆ์‹œ: ๊ณตํ†ต Step ์ƒ์„ฑ ๋ฉ”์„œ๋“œ
private Step createDeleteMvStep(String stepName, String tableName, String startDateParam, String endDateParam) {
    Tasklet tasklet = (contribution, chunkContext) -> {
        String sql = String.format("DELETE FROM %s WHERE start_date = :startDate AND end_date = :endDate", tableName);
        jdbcTemplate.update(sql, Map.of(
            "startDate", chunkContext.getStepContext().getJobParameters().get(startDateParam),
            "endDate", chunkContext.getStepContext().getJobParameters().get(endDateParam)
        ));
        return RepeatStatus.FINISHED;
    };
    return new StepBuilder(stepName, jobRepository)
            .tasklet(tasklet, transactionManager)
            .build();
}

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

๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 5a4975e and 52e3752.

๐Ÿ“’ Files selected for processing (28)
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRankingMv.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRankingRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/Period.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingMv.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/config/BatchConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/dto/ProductMetricsAgg.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/dto/ProductRankRow.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/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/java/com/loopers/batch/processor/ProductMetricProcessor.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java
๐Ÿ’ค Files with no reviewable changes (5)
  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.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/job/demo/step/DemoTasklet.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (3)
๐Ÿ“š Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingRepository.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-batch/src/main/java/com/loopers/batch/dto/ProductRankRow.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-batch/src/main/java/com/loopers/batch/dto/ProductMetricsAgg.java
๐Ÿงฌ Code graph analysis (12)
apps/commerce-batch/src/main/java/com/loopers/batch/processor/ProductMetricProcessor.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.java (1)
  • Component (15-60)
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.java (1)
  • Component (15-55)
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java (1)
  • Component (15-56)
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/processor/ProductMetricProcessor.java (1)
  • Component (12-35)
apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.java (1)
  • Component (15-60)
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.java (1)
  • Component (15-55)
apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (4)
apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java (1)
  • Component (13-62)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
  • Component (9-31)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.java (1)
  • Component (13-52)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (1)
  • Component (13-52)
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/processor/ProductMetricProcessor.java (1)
  • Component (12-35)
apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.java (1)
  • Component (15-60)
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java (1)
  • Component (15-56)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingMv.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRankingMv.java (1)
  • Entity (13-38)
apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java (5)
apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java (1)
  • Component (13-62)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
  • Component (9-31)
apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (1)
  • Component (14-91)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.java (1)
  • Component (13-52)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (1)
  • Component (13-52)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.java (4)
apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java (1)
  • Component (14-90)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
  • Component (9-31)
apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (1)
  • Component (14-91)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (1)
  • Component (13-52)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (5)
apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java (1)
  • Component (13-62)
apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java (1)
  • Component (14-90)
apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (1)
  • Component (14-91)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.java (1)
  • Component (13-52)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (1)
  • Component (13-52)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingV1Dto.java (1)
  • RankingV1Dto (7-25)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
  • Component (9-31)
apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (1)
  • Component (14-91)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRankingMv.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingMv.java (1)
  • Entity (13-40)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingV1Dto.java (1)
  • RankingV1Dto (7-25)
๐Ÿ”‡ Additional comments (18)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (2)

28-29: LGTM!

๋‚ ์งœ ๋ฒ”์œ„ ํ•„ํ„ฐ๋ง๊ณผ null ์•ˆ์ „ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 45-46


19-19: ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ์‹œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ ํ™•์ธ ํ•„์š”

WeeklyRankingRepositoryImpl ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ(endDate, startDate)์™€ WeeklyRankingService์—์„œ์˜ ํ˜ธ์ถœ ํŒจํ„ด์„ ๊ฒ€์ฆํ•˜์—ฌ ์˜๋„ํ•œ ์ˆœ์„œ๊ฐ€ ๋งž๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  startDate/endDate๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋งคํ•‘๋˜๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

1-7: LGTM!

๊ฐ„๋‹จํ•˜๊ณ  ๋ช…ํ™•ํ•œ enum ์ •์˜์ž…๋‹ˆ๋‹ค. ๋„๋ฉ”์ธ ๊ณ„์ธต์— ์ ์ ˆํ•˜๊ฒŒ ์œ„์น˜ํ•˜๋ฉฐ ์„œ๋น„์Šค์™€ API ๊ณ„์ธต์—์„œ ์ž˜ ํ™œ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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

6-7: LGTM!

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

apps/commerce-batch/src/main/java/com/loopers/batch/dto/ProductRankRow.java (1)

5-11: productId ํƒ€์ž…์€ ์‹ค์ œ๋กœ๋Š” ๋„ ๋ถˆ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ๊ฒ€ํ†  ๋ถˆํ•„์š”

ProductMetricReader์˜ rowMapper์—์„œ rs.getLong("ref_product_id")๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, productId๋Š” ์‹ค์ œ๋กœ null ๊ฐ’์„ ๋ฐ›์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปฌ๋Ÿผ์ด NULL์ด๋ฉด rs.getLong()์€ null์ด ์•„๋‹Œ 0์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ Long ํƒ€์ž… ์„ ์–ธ์€ ํ˜ผ๋™์„ ์ค„ ์ˆ˜ ์žˆ์ง€๋งŒ, NullPointerException์ด๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ œ์•ฝ ์กฐ๊ฑด ์œ„๋ฐ˜์˜ ์‹ค์ œ ์œ„ํ—˜์€ ์—†์Šต๋‹ˆ๋‹ค. Writers์˜ MapSqlParameterSource๋„ null ๊ฐ’์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ํƒ€์ž… ์ผ๊ด€์„ฑ ๊ฐœ์„  ์‚ฌํ•ญ์ด์ง€ ๊ธฐ๋Šฅ์  ๊ฒฐํ•จ์ด ์•„๋‹™๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

apps/commerce-batch/src/main/java/com/loopers/batch/processor/ProductMetricProcessor.java (1)

14-34: LGTM! ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง์ด ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

BigDecimal์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ถ€๋™์†Œ์ˆ˜์  ์ •๋ฐ€๋„ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ , ๊ฐ€์ค‘์น˜ ํ•ฉ๊ณ„๊ฐ€ 1.0(0.1 + 0.3 + 0.6)์œผ๋กœ ์ •ํ™•ํžˆ ๋งž์Šต๋‹ˆ๋‹ค. HALF_UP ๋ฐ˜์˜ฌ๋ฆผ ๋ชจ๋“œ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

ํ–ฅํ›„ ๊ฐ€์ค‘์น˜ ์กฐ์ •์ด ํ•„์š”ํ•  ๊ฒฝ์šฐ, @Value ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด properties์—์„œ ์ฃผ์ž…๋ฐ›๋„๋ก ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

1-25: LGTM! DTO ๊ตฌ์กฐ ๊ฐœ์„ ์ด ์ž˜ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

ํŒจํ‚ค์ง€๊ฐ€ interfaces.api.ranking์—์„œ application.ranking์œผ๋กœ ์ด๋™๋˜์–ด ๊ณ„์ธต ์•„ํ‚คํ…์ฒ˜์— ๋” ์ ํ•ฉํ•ด์กŒ์Šต๋‹ˆ๋‹ค. ProductRankingPageResponse์— period, startDate, endDate ํ•„๋“œ๊ฐ€ ์ถ”๊ฐ€๋˜์–ด ๋‹ค์–‘ํ•œ ๋žญํ‚น ๊ธฐ๊ฐ„์„ ์ง€์›ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

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

39-51: LGTM! countMonthly ๊ตฌํ˜„์ด ์•ˆ์ „ํ•˜๊ฒŒ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

fetchOne() ๊ฒฐ๊ณผ๊ฐ€ null์ผ ๊ฒฝ์šฐ๋ฅผ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌํ•˜์—ฌ 0L์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.java (1)

52-57: RowMapper ํ•„๋“œ ์ˆœ์„œ ํ™•์ธ ์™„๋ฃŒ

ProductMetricsAgg ์ƒ์„ฑ์ž์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ (productId, sumLike, sumView, sumSales)๊ฐ€ rowMapper์—์„œ ์ถ”์ถœํ•˜๋Š” ์ˆœ์„œ (ref_product_id, sum_like, sum_view, sum_sales)์™€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ’์˜ ์ˆœ์„œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๋ฏ€๋กœ ์ถ”๊ฐ€ ์ˆ˜์ •์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

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

28-28: ์›”๊ฐ„ ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ ๋ฐฉ์‹ ๊ฒ€ํ†  ํ•„์š”

minusDays(30)์„ ์‚ฌ์šฉํ•˜๋ฉด ํ•ญ์ƒ 30์ผ ๊ธฐ๊ฐ„์ด ๋˜์–ด ์‹ค์ œ ๋‹ฌ๋ ฅ ์›”(28~31์ผ)๊ณผ ์ฐจ์ด๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ์ด "์ง€๋‚œ 30์ผ"์ธ์ง€ "ํ•ด๋‹น ์›”"์ธ์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋‹ฌ๋ ฅ ์›” ๊ธฐ์ค€์ด๋ผ๋ฉด minusMonths(1)์„ ๊ณ ๋ คํ•˜์„ธ์š”.

์ฐธ๊ณ : WeeklyRankingService์—์„œ๋Š” minusDays(6)์„ ์‚ฌ์šฉํ•˜์—ฌ ์ •ํ™•ํžˆ 7์ผ ์œˆ๋„์šฐ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค(Line 28). Monthly๋„ ๋™์ผํ•œ ํŒจํ„ด(๊ณ ์ • ์ผ์ˆ˜)์„ ๋”ฐ๋ฅด๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ด์ง€๋งŒ, ์›”๊ฐ„ ๋žญํ‚น์˜ ๊ฒฝ์šฐ ๋‹ฌ๋ ฅ ์›” ๊ธฐ์ค€์ด ๋” ์ง๊ด€์ ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

16-29: ์„œ๋น„์Šค ์œ„์ž„ ํŒจํ„ด์ด ์ž˜ ๊ตฌํ˜„๋จ

RankingFacade๊ฐ€ Period์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์„œ๋น„์Šค๋กœ ์œ„์ž„ํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. Switch expression์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ€๋…์„ฑ๋„ ์ข‹์Šต๋‹ˆ๋‹ค.

Minor suggestion: Java์˜ sealed class๋‚˜ exhaustive switch๋ฅผ ํ™œ์šฉํ•˜๋ฉด default ์ผ€์ด์Šค๊ฐ€ ๋ถˆํ•„์š”ํ•ด์งˆ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ˜„์žฌ ๊ตฌ์กฐ๋„ ์ถฉ๋ถ„ํžˆ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. Period enum์— ์ƒˆ๋กœ์šด ๊ฐ’์ด ์ถ”๊ฐ€๋˜๋ฉด ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ๊ฒฝ๊ณ ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ์ƒˆ๋กœ์šด Period ์ถ”๊ฐ€ ์‹œ ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์„ ์žŠ์ง€ ๋งˆ์„ธ์š”.


27-27: Period enum ๊ฐ’ ๊ฒ€์ฆ ๋กœ์ง์€ ์ ์ ˆํ•˜๋ฉฐ default ์ผ€์ด์Šค๋Š” ์ด๋ก ์ƒ ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค

Period enum์ด DAILY, WEEKLY, MONTHLY ์„ธ ๊ฐ€์ง€ ๊ฐ’๋งŒ ์ •์˜๋˜์–ด ์žˆ๊ณ , ๋ชจ๋“  ์ผ€์ด์Šค๊ฐ€ switch ๋ฌธ์—์„œ ์ฒ˜๋ฆฌ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ RankingV1Controller์˜ @RequestParam(defaultValue = "DAILY") ์„ค์ •์œผ๋กœ ์ธํ•ด null ๊ฐ’์ด ์—…์ŠคํŠธ๋ฆผ์—์„œ ์ฐจ๋‹จ๋˜๋ฉฐ, Spring์˜ enum ๋ฐ”์ธ๋”ฉ ๊ฒ€์ฆ์ด ์œ ํšจํ•˜์ง€ ์•Š์€ Period ๊ฐ’์„ ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ default ์ผ€์ด์Šค๋Š” ์ด๋ก ์ƒ ์‹คํ–‰๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ๋ฐฉ์–ด์  ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ฐจ์›์—์„œ default ์ผ€์ด์Šค์˜ ์กด์žฌ๋Š” ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

28-28: ์ฃผ๊ฐ„ ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ์ด ์˜ฌ๋ฐ”๋ฆ„

minusDays(6)์„ ์‚ฌ์šฉํ•˜์—ฌ endDate ํฌํ•จ 7์ผ ์œˆ๋„์šฐ๋ฅผ ์ •ํ™•ํžˆ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ: endDate๊ฐ€ 2024-01-07์ด๋ฉด startDate๋Š” 2024-01-01์ด ๋˜์–ด 7์ผ ๊ธฐ๊ฐ„์ด ๋ฉ๋‹ˆ๋‹ค.

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

13-38: WeeklyRankingMv์™€ ์ผ๊ด€๋œ ๊ตฌ์กฐ๋กœ ๊ตฌํ˜„๋จ

MonthlyRankingMv๋Š” WeeklyRankingMv์™€ ๋™์ผํ•œ ํŒจํ„ด์„ ๋”ฐ๋ฅด๋ฉฐ, ํ…Œ์ด๋ธ” ์ด๋ฆ„(mv_product_rank_monthly)๋งŒ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ผ๊ด€์„ฑ์€ ์œ ์ง€๋ณด์ˆ˜์— ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ : WeeklyRankingMv ๋ฆฌ๋ทฐ(Lines 13-40)์—์„œ ์–ธ๊ธ‰ํ•œ ์‚ฌํ•ญ๋“ค(batch writer ๊ฒ€์ฆ, String ๋‚ ์งœ ํ•„๋“œ)์ด ์ด ์—”ํ‹ฐํ‹ฐ์—๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

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

13-40: ๋ฐฐ์น˜ ์“ฐ๊ธฐ๋Š” JPA๊ฐ€ ์•„๋‹Œ ์ง์ ‘ SQL์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์—”ํ‹ฐํ‹ฐ ๊ตฌ์กฐ ๋ณ€๊ฒฝ ๋ถˆํ•„์š”

๊ฒ€์ฆ ๊ฒฐ๊ณผ, ProductMetricWeeklyWriter์™€ ProductMetricMonthlyWriter๋Š” NamedParameterJdbcTemplate.batchUpdate()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ProductRankRow DTO ๊ฐ์ฒด๋ฅผ ์ง์ ‘ SQL INSERT ๋ฌธ์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ์“ฐ๊ธฐ ๊ณผ์ •์—์„œ WeeklyRankingMv ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด๊ฐ€ ์ธ์Šคํ„ด์Šคํ™”๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ, setter๋‚˜ ๋นŒ๋”๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ด ์—”ํ‹ฐํ‹ฐ๋Š” Materialized View๋ฅผ ๋‚˜ํƒ€๋‚ด๋ฉฐ ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์„ค๊ณ„๋œ ๊ฒƒ์ด ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ตฌ์กฐ(protected no-arg ์ƒ์„ฑ์ž, @Getter๋งŒ ํฌํ•จ)๋Š” JPA ํ•˜์ด๋“œ๋ ˆ์ด์…˜๊ณผ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ๋งคํ•‘์— ์ตœ์ ํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/config/BatchConfig.java (3)

29-40: LGTM!

Spring Batch ์„ค์ •์„ ์œ„ํ•œ ์˜์กด์„ฑ ์ฃผ์ž…์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Lombok @RequiredArgsConstructor๋ฅผ ํ™œ์šฉํ•œ ์ƒ์„ฑ์ž ์ฃผ์ž… ๋ฐฉ์‹์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.


41-48: LGTM!

@StepScope๋ฅผ ํ™œ์šฉํ•œ job parameter ์ฃผ์ž…์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. Step ์‹คํ–‰ ์‹œ์ ์— ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ฐ”์ธ๋”ฉ๋˜์–ด ๋™์  ๋‚ ์งœ ๋ฒ”์œ„ ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.


86-93: LGTM!

RunIdIncrementer๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋™์ผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Job ์žฌ์‹คํ–‰์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. Delete โ†’ Aggregate ์ˆœ์„œ์˜ Step ์‹คํ–‰ ํ๋ฆ„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Comment on lines +46 to +56
for (var t : tuples) {
String member = t.getValue();
Double score = t.getScore();
if (member == null || score == null) continue;

items.add(new RankingV1Dto.ProductRankingResponse(
rank++,
Long.parseLong(member),
score
));
}
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

Long.parseLong(member)์—์„œ ์˜ˆ์™ธ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ

Redis์—์„œ ๊ฐ€์ ธ์˜จ member ๊ฐ’์ด ์œ ํšจํ•œ ์ˆซ์ž๊ฐ€ ์•„๋‹ ๊ฒฝ์šฐ NumberFormatException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ์ด ๋ณด์žฅ๋œ๋‹ค๋ฉด ๋ฌธ์ œ๊ฐ€ ์—†์ง€๋งŒ, ๋ฐฉ์–ด์  ์ฝ”๋”ฉ์„ ์œ„ํ•ด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”.

๐Ÿ”Ž ๋ฐฉ์–ด์  ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ œ์•ˆ
             for (var t : tuples) {
                 String member = t.getValue();
                 Double score = t.getScore();
                 if (member == null || score == null) continue;

+                Long productId;
+                try {
+                    productId = Long.parseLong(member);
+                } catch (NumberFormatException e) {
+                    continue; // ๋˜๋Š” ๋กœ๊น… ํ›„ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
+                }
+
                 items.add(new RankingV1Dto.ProductRankingResponse(
                         rank++,
-                        Long.parseLong(member),
+                        productId,
                         score
                 ));
             }
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java
around lines 46 to 56, parsing the Redis member string with
Long.parseLong(member) can throw NumberFormatException for non-numeric or
malformed values; wrap the parse in a defensive check (e.g., trim and validate
with a numeric check or regex) or a try-catch around Long.parseLong, log a
warning including the offending member and skip adding that entry (do not
increment rank for skipped items), and ensure null/blank members are also
handled consistently so the loop continues safely without crashing.


String startDate = end.minusDays(30).format(BASIC);

System.out.println("startDate = " + startDate);
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

ํ”„๋กœ๋•์…˜ ๋กœ๊น… ๋ฐฉ์‹์œผ๋กœ ๊ต์ฒด ํ•„์š”

System.out.println์€ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ์ ํ•ฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ ์ ˆํ•œ ๋กœ๊น… ํ”„๋ ˆ์ž„์›Œํฌ(SLF4J)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ๊ทธ ๋ ˆ๋ฒจ๊ณผ ์ถœ๋ ฅ ์œ„์น˜๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณ€๊ฒฝํ•˜์„ธ์š”.

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

ํด๋ž˜์Šค ์ƒ๋‹จ์— logger ์„ ์–ธ์„ ์ถ”๊ฐ€:

+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
 @Component
 @RequiredArgsConstructor
 public class MonthlyRankingService {

๊ทธ๋ฆฌ๊ณ  System.out.println์„ ๋กœ๊ฑฐ๋กœ ๊ต์ฒด:

-        System.out.println("startDate = " + startDate);
+        log.debug("Monthly ranking calculation - startDate: {}, endDate: {}", startDate, endDate);

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

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java
around line 30, replace the System.out.println call with SLF4J logging: add a
private static final Logger logger =
LoggerFactory.getLogger(MonthlyRankingService.class) declaration at the top of
the class (ensure org.slf4j.Logger and org.slf4j.LoggerFactory are imported),
then change the println to logger.debug or logger.info("startDate = {}",
startDate) as appropriate for the message severity so logs are managed by the
configured logging framework.

);
}

List<MonthlyRankingMv> rows = monthlyRankingRepository.getMonthlyTop100(endDate, startDate, safePage, safeSize);
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:

#!/bin/bash
# Find repository interfaces and service implementation
echo "=== ํŒŒ์ผ ์œ„์น˜ ํ™•์ธ ==="
find . -type f -name "*RankingRepository*" -o -name "MonthlyRankingService.java" | head -20

echo -e "\n=== MonthlyRankingService ํŒŒ์ผ ์ฐพ๊ธฐ ==="
fd MonthlyRankingService.java

echo -e "\n=== ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํŒŒ์ผ๋“ค ์ฐพ๊ธฐ ==="
fd "RankingRepository" --type f

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

Length of output: 1111


๐Ÿ Script executed:

#!/bin/bash
# MonthlyRankingService์˜ line 32์™€ 65 ํ™•์ธ
echo "=== MonthlyRankingService.java ๋‚ด์šฉ (๋ผ์ธ 25-75) ==="
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java | sed -n '25,75p'

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

Length of output: 2190


๐Ÿ Script executed:

#!/bin/bash
# ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ํ™•์ธ
echo "=== MonthlyRankingRepository getMonthlyTop100 ๋ฉ”์„œ๋“œ ==="
rg "getMonthlyTop100" -A 2 -B 1

echo -e "\n=== MonthlyRankingRepository countMonthly ๋ฉ”์„œ๋“œ ==="
rg "countMonthly" -A 2 -B 1

echo -e "\n=== WeeklyRankingRepository getWeeklyTop100 ๋ฉ”์„œ๋“œ ==="
rg "getWeeklyTop100" -A 2 -B 1

echo -e "\n=== WeeklyRankingRepository countWeekly ๋ฉ”์„œ๋“œ ==="
rg "countWeekly" -A 2 -B 1

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

Length of output: 9011


๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ๊ฐ„ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ ์ผ๊ด€์„ฑ ํ†ต์ผ ํ•„์š”

Line 65์˜ getMonthlyTop100(endDate, startDate, ...)์™€ Line 32์˜ countMonthly(startDate, endDate) ํ˜ธ์ถœ์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ๊ฐ€ ๋ฐ˜๋Œ€์ž…๋‹ˆ๋‹ค. MonthlyRankingRepository ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ๋‘ ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ๊ฐ€ ์ผ๊ด€๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ (getMonthlyTop100์€ endDate ๋จผ์ €, countMonthly๋Š” startDate ๋จผ์ €), WeeklyRankingRepository์™€ ๋™์ผํ•œ ํŒจํ„ด์œผ๋กœ ํ†ต์ผํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ๊ฐ™์€ ๋…ผ๋ฆฌ๋ฅผ ๋‹ค๋ฃจ๋Š” ๋ฉ”์„œ๋“œ๋“ค์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ๋ฅผ ์ผ๊ด€๋˜๊ฒŒ ์œ ์ง€ํ•˜๋ฉด ํ˜ผ๋™์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java
around line 65, the call getMonthlyTop100(endDate, startDate, ...) uses the
opposite parameter order to countMonthly(startDate, endDate) (called at line
32); make the parameter ordering consistent with WeeklyRankingRepository and
other methods by changing getMonthlyTop100's signature and all its usages to
accept (startDate, endDate, ...) instead of (endDate, startDate), then update
the repository implementation and any callers (including this line 65) to pass
startDate first and endDate second so both methods use the same
startDate,endDate ordering.


String startDate = end.minusDays(6).format(BASIC);

System.out.println("startDate = " + startDate);
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

ํ”„๋กœ๋•์…˜ ๋กœ๊น… ๋ฐฉ์‹์œผ๋กœ ๊ต์ฒด ํ•„์š”

MonthlyRankingService์™€ ๋™์ผํ•œ ์ด์Šˆ: System.out.println์€ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ์ ํ•ฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. SLF4J ๋กœ๊ฑฐ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •์‚ฌํ•ญ
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
 @Component
 @RequiredArgsConstructor
 public class WeeklyRankingService {
-        System.out.println("startDate = " + startDate);
+        log.debug("Weekly ranking calculation - startDate: {}, endDate: {}", startDate, endDate);

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

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java
around line 30, replace the System.out.println call with SLF4J logging: add a
private static final org.slf4j.Logger logger =
org.slf4j.LoggerFactory.getLogger(WeeklyRankingService.class); import the SLF4J
classes, then change the print statement to logger.info("startDate = {}",
startDate); ensuring you use parameterized logging and remove the
System.out.println.

Comment on lines +6 to +7
List<MonthlyRankingMv> getMonthlyTop100(String endDate, String startDate, int safePage, int safeSize);
long countMonthly(String startDate, String endDate);
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

์น˜๋ช…์ : ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ ๋ถˆ์ผ์น˜๋กœ ์ธํ•œ ์ž ์žฌ์  ๋ฒ„๊ทธ ์œ„ํ—˜

getMonthlyTop100์€ endDate๋ฅผ startDate๋ณด๋‹ค ๋จผ์ € ๋ฐ›์ง€๋งŒ, countMonthly๋Š” startDate๋ฅผ endDate๋ณด๋‹ค ๋จผ์ € ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ์˜ ๋ถˆ์ผ์น˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ํ˜ธ์ถœ ์‹œ ๋‚ ์งœ๋ฅผ ์ž˜๋ชป๋œ ์ˆœ์„œ๋กœ ์ „๋‹ฌํ•˜์—ฌ ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ์‹ฌ๊ฐํ•œ ๋ฒ„๊ทธ๋ฅผ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ WeeklyRankingRepository.getWeeklyTop100๊ณผ๋„ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ผ๊ด€๋œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ๋กœ ์ˆ˜์ • ์ œ์•ˆ
-    List<MonthlyRankingMv> getMonthlyTop100(String endDate, String startDate, int safePage, int safeSize);
+    List<MonthlyRankingMv> getMonthlyTop100(String startDate, String endDate, int safePage, int safeSize);
     long countMonthly(String startDate, String endDate);

๋” ๋‚˜์€ ๊ฐœ์„ ์•ˆ (String ๋‚ ์งœ ๋ฌธ์ œ๋„ ํ•จ๊ป˜ ํ•ด๊ฒฐ):

+import java.time.LocalDate;
+
 public interface MonthlyRankingRepository {
-    List<MonthlyRankingMv> getMonthlyTop100(String endDate, String startDate, int safePage, int safeSize);
-    long countMonthly(String startDate, String endDate);
+    List<MonthlyRankingMv> getMonthlyRankings(LocalDate startDate, LocalDate endDate, int page, int size);
+    long countMonthly(LocalDate startDate, LocalDate endDate);
 }
๐Ÿ“ 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
List<MonthlyRankingMv> getMonthlyTop100(String endDate, String startDate, int safePage, int safeSize);
long countMonthly(String startDate, String endDate);
List<MonthlyRankingMv> getMonthlyTop100(String startDate, String endDate, int safePage, int safeSize);
long countMonthly(String startDate, String endDate);

import java.util.List;

public interface WeeklyRankingRepository {
List<WeeklyRankingMv> getWeeklyTop100(String date, String lastWeek, int page, int size);
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

String ํƒ€์ž… ๋‚ ์งœ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํƒ€์ž… ์•ˆ์ •์„ฑ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐํ•ฉ๋‹ˆ๋‹ค

๋‚ ์งœ๋ฅผ String ํƒ€์ž…์œผ๋กœ ๋ฐ›์œผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค:

  • ๋Ÿฐํƒ€์ž„ ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ๊ฐ€๋Šฅ์„ฑ
  • ๋‚ ์งœ ํ˜•์‹ ๋ถˆ์ผ์น˜ (์˜ˆ: "2025-01-02" vs "20250102")
  • ์ปดํŒŒ์ผ ํƒ€์ž„ ํƒ€์ž… ๊ฒ€์ฆ ๋ถˆ๊ฐ€
  • ๋‚ ์งœ ์—ฐ์‚ฐ ์‹œ ๋งค๋ฒˆ ํŒŒ์‹ฑ ํ•„์š”

๋˜ํ•œ, ํŒŒ๋ผ๋ฏธํ„ฐ ๋ช…๋ช…์ด ์ผ๊ด€๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: date์™€ lastWeek๋Š” ์˜๋ฏธ๊ฐ€ ๋ช…ํ™•ํ•˜์ง€ ์•Š์œผ๋ฉฐ, countWeekly์˜ startDate/endDate์™€๋„ ๋ถˆ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž LocalDate ์‚ฌ์šฉ์œผ๋กœ ๊ฐœ์„  ์ œ์•ˆ
-    List<WeeklyRankingMv> getWeeklyTop100(String date, String lastWeek, int page, int size);
-    long countWeekly(String startDate, String endDate);
+    List<WeeklyRankingMv> getWeeklyTop100(LocalDate endDate, LocalDate startDate, int page, int size);
+    long countWeekly(LocalDate startDate, LocalDate endDate);

ํŒŒ์ผ ์ƒ๋‹จ์— import ์ถ”๊ฐ€:

 import java.util.List;
+import java.time.LocalDate;

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

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingRepository.java
around line 6, replace the two String date parameters with java.time.LocalDate
and standardize names to match other APIs (e.g., startDate and endDate) to
ensure type safety; update the method signature to use LocalDate startDate,
LocalDate endDate (keeping page and size), add the necessary import
(java.time.LocalDate) at the top, and then propagate this change to all
implementations, callers, and unit/integration tests to parse/convert input
strings to LocalDate at the boundary (e.g., controller) rather than inside the
repository.

Comment on lines +6 to +8
public interface WeeklyRankingJpaRepository extends JpaRepository<WeeklyRankingMv, Long> {

}
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
# Search for usage of WeeklyRankingJpaRepository
rg -nP --type=java 'WeeklyRankingJpaRepository' .

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

Length of output: 281


๐Ÿ Script executed:

#!/bin/bash
# Search for WeeklyRankingRepositoryImpl and related classes
rg -nP --type=java 'WeeklyRankingRepository' .

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

Length of output: 883


WeeklyRankingJpaRepository ์ œ๊ฑฐ ํ•„์š”

WeeklyRankingJpaRepository๋Š” ์„ ์–ธ๋˜์–ด ์žˆ์ง€๋งŒ ์ฝ”๋“œ๋ฒ ์ด์Šค ์–ด๋””์—์„œ๋„ ์ฃผ์ž…๋˜๊ฑฐ๋‚˜ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ์€ WeeklyRankingRepository ๋„๋ฉ”์ธ ์ธํ„ฐํŽ˜์ด์Šค์ด๋ฉฐ, WeeklyRankingRepositoryImpl์ด ์ด๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ WeeklyRankingJpaRepository๋Š” ์ œ๊ฑฐํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingJpaRepository.java
around lines 6 to 8, remove the unused JpaRepository interface declaration:
WeeklyRankingJpaRepository is never injected or referenced (the code uses
WeeklyRankingRepository and its implementation WeeklyRankingRepositoryImpl), so
delete this file and any imports referencing it; run a project-wide search to
confirm no remaining usages and update build/config if needed.

public interface RankingV1ApiSpec {

ApiResponse<RankingV1Dto.ProductRankingPageResponse> getDailyProductRanking(int size, int page);
ApiResponse<RankingV1Dto.ProductRankingPageResponse> getProductRanking(String date, Period period, int size, int page);
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

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

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

Length of output: 178


๐Ÿ Script executed:

find . -type f -path "*commerce-api*" -name "*.java" | grep -i ranking | head -20

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

Length of output: 1807


๐Ÿ Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java

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

Length of output: 526


๐Ÿ Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java

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

Length of output: 1518


๐Ÿ Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java

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

Length of output: 1492


๐Ÿ Script executed:

rg -A 10 "getProductRanking" ./apps/commerce-api/src/main/java/com/loopers/ --type java

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

Length of output: 3461


๐Ÿ Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java

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

Length of output: 2833


๐Ÿ Script executed:

rg "LocalDate|DateTimeFormatter|parse" ./apps/commerce-api/src/main/java/com/loopers/application/ranking/ -A 2 --type java

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

Length of output: 3076


๐Ÿ Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java

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

Length of output: 3570


๐Ÿ Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java

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

Length of output: 3575


Controller์˜ date ํŒŒ๋ผ๋ฏธํ„ฐ์— @DateTimeFormat ์• ๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€ ๋ฐ ํ˜•์‹ ๋ฌธ์„œํ™” ํ•„์š”

@RequestParam String date์— ํ˜•์‹ ์ง€์ •์ด ์—†์–ด ๋‹ค์Œ ๋ฌธ์ œ ๋ฐœ์ƒ:

  • WEEKLY์™€ MONTHLY ๊ธฐ๊ฐ„ ์กฐํšŒ ์‹œ LocalDate.parse(date, BASIC_ISO_DATE) ํ˜ธ์ถœ๋กœ "yyyyMMdd" ํ˜•์‹ ๊ฐ•์ œ๋˜์ง€๋งŒ ๋ฌธ์„œํ™”๋˜์ง€ ์•Š์Œ
  • ์ž˜๋ชป๋œ ํ˜•์‹(์˜ˆ: "yyyy-MM-dd") ์ž…๋ ฅ ์‹œ DateTimeParseException ๋ฐœ์ƒ
  • OpenAPI/Swagger ์ŠคํŽ™์—์„œ ๋‚ ์งœ ํ˜•์‹ ๋ช…์‹œ ๋ถˆ๊ฐ€๋Šฅ

RankingV1Controller์˜ getProductRanking ๋ฉ”์„œ๋“œ์—์„œ @DateTimeFormat(pattern = "yyyyMMdd")๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , JavaDoc์œผ๋กœ "yyyyMMdd ํ˜•์‹์˜ ๋‚ ์งœ" ๋ช…์‹œ ํ•„์š”.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java
around line 9, the date request parameter lacks a declared format causing
LocalDate.parse(date, BASIC_ISO_DATE) to fail for inputs like "yyyy-MM-dd" and
leaving the OpenAPI spec without a date pattern; add @DateTimeFormat(pattern =
"yyyyMMdd") to the controller/interface method parameter for date and update the
method JavaDoc to state "date must be in yyyyMMdd format" so the runtime
parsing, exception behavior and generated OpenAPI documentation clearly reflect
the expected format.

Comment on lines +18 to +54
public class ProductMetricMonthlyWriter implements ItemWriter<ProductRankRow> {
private final NamedParameterJdbcTemplate jdbcTemplate;

@Value("#{jobParameters['monthlyStartDate']}")
private String startDate;

@Value("#{jobParameters['monthlyEndDate']}")
private String endDate;

private static final String SQL = """
INSERT INTO mv_product_rank_monthly
(start_date, end_date, ref_product_id,
view_count_sum, like_count_sum, sales_volume_sum,
score, created_at, updated_at, deleted_at)
VALUES
(:startDate, :endDate, :productId,
:viewCountSum, :likeCountSum, :salesVolumeSum,
:score, :createdAt, :updatedAt, NULL)
""";

@Override
public void write(Chunk<? extends ProductRankRow> chunk) {
MapSqlParameterSource[] batch = chunk.getItems().stream()
.map(item -> new MapSqlParameterSource()
.addValue("startDate", startDate)
.addValue("endDate", endDate)
.addValue("productId", item.productId())
.addValue("viewCountSum", item.viewCountSum())
.addValue("likeCountSum", item.likeCountSum())
.addValue("salesVolumeSum", item.salesVolumeSum())
.addValue("score", item.score())
.addValue("createdAt", ZonedDateTime.now())
.addValue("updatedAt", ZonedDateTime.now()))
.toArray(MapSqlParameterSource[]::new);

jdbcTemplate.batchUpdate(SQL, batch);
}
Copy link

Choose a reason for hiding this comment

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

๐Ÿ› ๏ธ Refactor suggestion | ๐ŸŸ  Major

ProductMetricWeeklyWriter์™€ ๋†’์€ ์ฝ”๋“œ ์ค‘๋ณต

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

๐Ÿ”Ž ์ œ์•ˆ: ์ถ”์ƒ ๋ฒ ์ด์Šค ํด๋ž˜์Šค ์ถ”์ถœ
// ์ƒˆ ํŒŒ์ผ: AbstractProductMetricWriter.java
@RequiredArgsConstructor
public abstract class AbstractProductMetricWriter implements ItemWriter<ProductRankRow> {
    protected final NamedParameterJdbcTemplate jdbcTemplate;
    
    protected abstract String getStartDateParam();
    protected abstract String getEndDateParam();
    protected abstract String getTableName();
    
    @Override
    public void write(Chunk<? extends ProductRankRow> chunk) {
        ZonedDateTime now = ZonedDateTime.now();
        String sql = String.format("""
            INSERT INTO %s
              (start_date, end_date, ref_product_id,
               view_count_sum, like_count_sum, sales_volume_sum,
               score, created_at, updated_at, deleted_at)
            VALUES
              (:startDate, :endDate, :productId,
               :viewCountSum, :likeCountSum, :salesVolumeSum,
               :score, :createdAt, :updatedAt, NULL)
            """, getTableName());
        
        MapSqlParameterSource[] batch = chunk.getItems().stream()
                .map(item -> new MapSqlParameterSource()
                        .addValue("startDate", getStartDateParam())
                        .addValue("endDate", getEndDateParam())
                        .addValue("productId", item.productId())
                        .addValue("viewCountSum", item.viewCountSum())
                        .addValue("likeCountSum", item.likeCountSum())
                        .addValue("salesVolumeSum", item.salesVolumeSum())
                        .addValue("score", item.score())
                        .addValue("createdAt", now)
                        .addValue("updatedAt", now))
                .toArray(MapSqlParameterSource[]::new);

        jdbcTemplate.batchUpdate(sql, batch);
    }
}

// ProductMetricMonthlyWriter๋Š” ๊ฐ„์†Œํ™”
@Component
@StepScope
public class ProductMetricMonthlyWriter extends AbstractProductMetricWriter {
    @Value("#{jobParameters['monthlyStartDate']}")
    private String startDate;
    
    @Value("#{jobParameters['monthlyEndDate']}")
    private String endDate;
    
    public ProductMetricMonthlyWriter(NamedParameterJdbcTemplate jdbcTemplate) {
        super(jdbcTemplate);
    }
    
    @Override
    protected String getStartDateParam() { return startDate; }
    
    @Override
    protected String getEndDateParam() { return endDate; }
    
    @Override
    protected String getTableName() { return "mv_product_rank_monthly"; }
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.java
(lines 18-54) and its Weekly counterpart, there is high duplication; extract a
new AbstractProductMetricWriter base class that holds the
NamedParameterJdbcTemplate, the generic write(Chunk) implementation (builds SQL
using an abstract getTableName(), constructs the batch params using
ZonedDateTime.now() once, and calls jdbcTemplate.batchUpdate), and declares
abstract getters getStartDateParam(), getEndDateParam(), and getTableName();
make jdbcTemplate protected final and move common SQL/template logic into the
base class; update ProductMetricMonthlyWriter to be a thin subclass (keep
@Component and @StepScope, keep @Value fields for
monthlyStartDate/monthlyEndDate, call super(jdbcTemplate) in constructor, and
implement the three abstract getters returning the injected fields and
"mv_product_rank_monthly"); apply the same refactor to ProductMetricWeeklyWriter
(return weekly params and table) so both classes only provide parameter values
and table name.

Comment on lines +27 to +36
private static final String SQL = """
INSERT INTO mv_product_rank_weekly
(start_date, end_date, ref_product_id,
view_count_sum, like_count_sum, sales_volume_sum,
score, created_at, updated_at, deleted_at)
VALUES
(:startDate, :endDate, :productId,
:viewCountSum, :likeCountSum, :salesVolumeSum,
:score, :createdAt, :updatedAt, NULL)
""";
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

๋ฐฐ์น˜ ์žฌ์‹คํ–‰ ์‹œ ์ค‘๋ณต ๋ฐ์ดํ„ฐ ์‚ฝ์ž… ์œ„ํ—˜

INSERT ๋ฌธ์— ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋กœ์ง(ON CONFLICT ๋˜๋Š” ON DUPLICATE KEY UPDATE)์ด ์—†์–ด, ๋™์ผํ•œ ๊ธฐ๊ฐ„์— ๋Œ€ํ•ด ๋ฐฐ์น˜๊ฐ€ ์žฌ์‹คํ–‰๋˜๋ฉด ์ค‘๋ณต ๋ ˆ์ฝ”๋“œ๊ฐ€ ์‚ฝ์ž…๋ฉ๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ์ž‘์—…์˜ ๋ฉฑ๋“ฑ์„ฑ(idempotency)์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด upsert ํŒจํ„ด์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”Ž PostgreSQL์˜ ๊ฒฝ์šฐ ON CONFLICT ์‚ฌ์šฉ ์˜ˆ์‹œ
     private static final String SQL = """
         INSERT INTO mv_product_rank_weekly
           (start_date, end_date, ref_product_id,
            view_count_sum, like_count_sum, sales_volume_sum,
            score, created_at, updated_at, deleted_at)
         VALUES
           (:startDate, :endDate, :productId,
            :viewCountSum, :likeCountSum, :salesVolumeSum,
            :score, :createdAt, :updatedAt, NULL)
+        ON CONFLICT (start_date, end_date, ref_product_id)
+        DO UPDATE SET
+          view_count_sum = EXCLUDED.view_count_sum,
+          like_count_sum = EXCLUDED.like_count_sum,
+          sales_volume_sum = EXCLUDED.sales_volume_sum,
+          score = EXCLUDED.score,
+          updated_at = EXCLUDED.updated_at
         """;

์ฐธ๊ณ : ProductMetricMonthlyWriter.java์—๋„ ๋™์ผํ•œ ์ˆ˜์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java
around lines 27-36, the SQL INSERT lacks conflict handling so re-running the
batch can insert duplicate rows for the same period/product; update the
statement to an upsert by adding an ON CONFLICT clause keyed on the unique
constraint (probably start_date, end_date, ref_product_id) that performs DO
UPDATE setting the aggregate columns (view_count_sum, like_count_sum,
sales_volume_sum, score) and timestamps (updated_at) from the excluded values,
ensure created_at remains unchanged on update, and mirror the same change in
ProductMetricMonthlyWriter.java.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant