Skip to content

Conversation

@minor7295
Copy link
Collaborator

@minor7295 minor7295 commented Jan 1, 2026

๐Ÿ“Œ Summary

Spring Batch๋ฅผ ํ™œ์šฉํ•˜์—ฌ product_metrics ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜์œผ๋กœ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„์˜ ์ •ํ™•์„ฑ๊ณผ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด 2-Step ๊ตฌ์กฐ๋กœ ์ง‘๊ณ„์™€ ๋žญํ‚น์„ ๋ถ„๋ฆฌํ•˜๊ณ , Materialized View์— TOP 100 ๋žญํ‚น์„ ์ €์žฅํ•˜์—ฌ ์กฐํšŒ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ๊ตฌํ˜„ ๋‚ด์šฉ:

  • Spring Batch Job ๊ตฌํ˜„: product_metrics ํ…Œ์ด๋ธ”์„ ์ฝ์–ด Chunk-Oriented Processing์œผ๋กœ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„
  • 2-Step ๊ตฌ์กฐ: Step 1์—์„œ ์ ์ˆ˜ ์ง‘๊ณ„ โ†’ Step 2์—์„œ TOP 100 ์„ ์ • ๋ฐ ๋žญํ‚น ๋ถ€์—ฌ
  • Materialized View ์„ค๊ณ„: ํ•˜๋‚˜์˜ ํ…Œ์ด๋ธ”(mv_product_rank)์— period_type์œผ๋กœ ์ฃผ๊ฐ„/์›”๊ฐ„ ๊ตฌ๋ถ„ํ•˜์—ฌ TOP 100 ์ €์žฅ
  • Ranking API ํ™•์žฅ: ๊ธฐ์กด API์— period ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ํ•˜์—ฌ ์ผ๊ฐ„(Redis), ์ฃผ๊ฐ„/์›”๊ฐ„(Materialized View) ๋žญํ‚น ์ œ๊ณต
  • commerce-batch ๋ชจ๋“ˆ ๋ถ„๋ฆฌ: ์‹คํ–‰ ์ฃผ๊ธฐ, ํŠธ๋žœ์žญ์…˜ ์„ฑ๊ฒฉ, ์žฅ์•  ๋Œ€์‘ ๋ฐฉ์‹์˜ ์ฐจ์ด๋ฅผ ๊ณ ๋ คํ•˜์—ฌ API์™€ ๋ฐฐ์น˜๋ฅผ ๋…๋ฆฝ์ ์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๋ถ„๋ฆฌ (์ง€์† ์‹คํ–‰ vs ๋‹จ๋ฐœ์„ฑ ์‹คํ–‰, ์งง์€ ํŠธ๋žœ์žญ์…˜ vs ๊ธด ํŠธ๋žœ์žญ์…˜, ์ฆ‰์‹œ ์‘๋‹ต vs ์žฌ์‹œ์ž‘ ๊ฐ€๋Šฅ)
  • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ค‘์‹ฌ ํ…Œ์ŠคํŠธ: ๋ฐฐ์น˜ ์ „์ฒด ์‹คํ–‰ ๋Œ€์‹  Reader/Processor/Writer์˜ ํ•ต์‹ฌ ๋กœ์ง๋งŒ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ ๊ฒ€์ฆ

๊ตฌํ˜„๋œ ๊ธฐ๋Šฅ:

  • GET /api/v1/rankings?date=yyyyMMdd&period=WEEKLY&size=20&page=1: ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ
  • Spring Batch Job ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ฐ˜ ์‹คํ–‰: periodType=WEEKLY targetDate=20241215

๐Ÿ’ฌ Review Points

1. 2-Step ๊ตฌ์กฐ๋กœ ์ง‘๊ณ„์™€ ๋žญํ‚น ๋ถ„๋ฆฌ: ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์ •ํ™•ํ•œ TOP 100 ์„ ์ •

๋ฐฐ๊ฒฝ ๋ฐ ์„ค๊ณ„ ์˜๋„:
๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ๋ฅผ Chunk ๋‹จ์œ„๋กœ ์ฒ˜๋ฆฌํ•  ๋•Œ, ๊ฐ Chunk๋งˆ๋‹ค TOP 100์„ ๊ณ„์‚ฐํ•˜๋ฉด ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ์ •ํ™•ํ•œ TOP 100์„ ์„ ์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ฒซ ๋ฒˆ์งธ Chunk์—์„œ ์ ์ˆ˜๊ฐ€ ๋†’์€ ์ƒํ’ˆ 100๊ฐœ๋ฅผ ์„ ์ •ํ–ˆ์ง€๋งŒ, ์ดํ›„ Chunk์—์„œ ๋” ๋†’์€ ์ ์ˆ˜๋ฅผ ๊ฐ€์ง„ ์ƒํ’ˆ์ด ๋‚˜ํƒ€๋‚  ์ˆ˜ ์žˆ์–ด ๊ฒฐ๊ณผ๊ฐ€ ๋ถ€์ •ํ™•ํ•ด์ง‘๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Step์„ ์‹คํŒจ ๊ฒฉ๋ฆฌ์™€ ์žฌ์‹œ์ž‘ ๋‹จ์œ„๋กœ ์‚ฌ์šฉํ•˜์—ฌ ์ง‘๊ณ„ ๊ณ„์‚ฐ๊ณผ ๋žญํ‚น ์ ์žฌ๋ฅผ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ๋ถ„๋ฆฌํ•˜๋ฉด:

  • ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์ •ํ™•ํ•œ TOP 100 ์„ ์ •: Step 1์—์„œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์ง‘๊ณ„ํ•œ ํ›„, Step 2์—์„œ ์ „์ฒด ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋žญํ‚น ๊ณ„์‚ฐ
  • ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๋ช…ํ™•ํ™”: ๊ฐ Step์ด ๋…๋ฆฝ์ ์ธ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๋ฅผ ๊ฐ€์ง€๋ฏ€๋กœ, ์ง‘๊ณ„ ๊ณ„์‚ฐ๊ณผ ๋žญํ‚น ์ ์žฌ์˜ ํŠธ๋žœ์žญ์…˜ ์„ฑ๊ฒฉ ์ฐจ์ด๋ฅผ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„
  • ์žฌ์‹œ์ž‘ ๊ฐ€๋Šฅ์„ฑ: Step 1์ด ์™„๋ฃŒ๋˜๋ฉด Step 2๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์žฌ์‹œ์ž‘ ๊ฐ€๋Šฅํ•˜์—ฌ, ์ง‘๊ณ„ ๊ณ„์‚ฐ์€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ๋žญํ‚น ์ ์žฌ๋งŒ ์‹คํŒจํ•œ ๊ฒฝ์šฐ Step 2๋งŒ ์žฌ์‹คํ–‰ ๊ฐ€๋Šฅ
  • ์˜์กด์„ฑ ๋ถ„๋ฆฌ: Step 1์˜ ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์ž„์‹œ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜์—ฌ Step 2์™€์˜ ์˜์กด์„ฑ์„ ๋ช…ํ™•ํžˆ ๋ถ„๋ฆฌ

๊ตฌ์กฐ:

Step 1: scoreAggregationStep
  โ”œโ”€ Reader: product_metrics ํ…Œ์ด๋ธ” ํŽ˜์ด์ง• ์กฐํšŒ (Chunk ๋‹จ์œ„)
  โ”œโ”€ Processor: Pass-through
  โ””โ”€ Writer: product_id๋ณ„ ๋ฉ”ํŠธ๋ฆญ ์ง‘๊ณ„ โ†’ tmp_product_rank_score ์ €์žฅ
      โ†“ (์ž„์‹œ ํ…Œ์ด๋ธ”์„ ํ†ตํ•œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ)
Step 2: rankingCalculationStep
  โ”œโ”€ Reader: tmp_product_rank_score ์ „์ฒด ์กฐํšŒ (์ ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ)
  โ”œโ”€ Processor: TOP 100 ์„ ์ • ๋ฐ ๋žญํ‚น ๋ฒˆํ˜ธ ๋ถ€์—ฌ
  โ””โ”€ Writer: mv_product_rank ์ €์žฅ (delete + insert)

๊ด€๋ จ ์ฝ”๋“œ:

@Bean
public Job productRankAggregationJob(
    Step scoreAggregationStep,
    Step rankingCalculationStep
) {
    return new JobBuilder("productRankAggregationJob", jobRepository)
        .start(scoreAggregationStep)        // Step 1 ๋จผ์ € ์‹คํ–‰
        .next(rankingCalculationStep)        // Step 1 ์™„๋ฃŒ ํ›„ Step 2 ์‹คํ–‰
        .build();
}

๊ณ ๋ฏผํ•œ ์  ๋ฐ ์˜์‚ฌ๊ฒฐ์ •:

  1. Step ๋ถ„๋ฆฌ vs StepListener ์‚ฌ์šฉ

    • ๊ณ ๋ฏผ: StepListener๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•˜๋‚˜์˜ Step ๋‚ด์—์„œ ์ง‘๊ณ„์™€ ์ €์žฅ์„ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฐฉ์•ˆ๋„ ๊ณ ๋ คํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์„ ํƒ: Step์„ ๋ถ„๋ฆฌํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๊ณ , ์žฌ์‹œ์ž‘ ๊ฐ€๋Šฅ์„ฑ์„ ํ™•๋ณดํ•˜๋Š” ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์ด์œ : ์ง‘๊ณ„ ๊ณ„์‚ฐ(Step 1)๊ณผ ๋žญํ‚น ์ ์žฌ(Step 2)๋Š” ํŠธ๋žœ์žญ์…˜ ์„ฑ๊ฒฉ๊ณผ ์ž์› ์‚ฌ์šฉ ํŠน์„ฑ์ด ๋‹ค๋ฅด๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค. ์ง‘๊ณ„ ๊ณ„์‚ฐ์€ ์žฌ์‹œ์ž‘ ๊ฐ€๋Šฅ์„ฑ์„ ์šฐ์„  ๊ณ ๋ คํ•˜๊ณ , ๋žญํ‚น ์ ์žฌ๋Š” ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ๊ณผ ์›์ž์„ฑ์„ ์šฐ์„  ๊ณ ๋ คํ•ฉ๋‹ˆ๋‹ค.
  2. ์ž„์‹œ ํ…Œ์ด๋ธ” ๋„์ž…

    • ๊ณ ๋ฏผ: Step ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์„ ์œ„ํ•ด ์ž„์‹œ ํ…Œ์ด๋ธ”(tmp_product_rank_score)์„ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์„ ํƒ: ์ž„์‹œ ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜์—ฌ Step 1๊ณผ Step 2๋ฅผ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์ด์œ :
      • Step 1๊ณผ Step 2๊ฐ€ ๋…๋ฆฝ์ ์ธ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์–ด ์‹คํŒจ ๊ฒฉ๋ฆฌ๊ฐ€ ๋ช…ํ™•ํ•จ
      • Step 1์ด ์™„๋ฃŒ๋˜๋ฉด Step 2๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์žฌ์‹œ์ž‘ ๊ฐ€๋Šฅ
      • ๋‹ค์Œ ๋ฐฐ์น˜ ์‹คํ–‰ ์‹œ ์ž๋™์œผ๋กœ ๋ฎ์–ด์“ฐ๊ธฐ๋˜๋ฏ€๋กœ ๋ณ„๋„ ์ •๋ฆฌ ๋กœ์ง ๋ถˆํ•„์š”
    • ํŠธ๋ ˆ์ด๋“œ์˜คํ”„: ์ค‘๊ฐ„ ์ €์žฅ์†Œ ๊ด€๋ฆฌ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์žˆ์ง€๋งŒ, ์žฌ์‹œ์ž‘ ๊ฐ€๋Šฅ์„ฑ๊ณผ ์‹คํŒจ ๊ฒฉ๋ฆฌ ์ธก๋ฉด์—์„œ ์ด์ ์ด ๋” ํผ
  3. ์ฃผ๊ฐ„/์›”๊ฐ„ ์ฒ˜๋ฆฌ ๋ฐฉ์‹

    • ๊ณ ๋ฏผ: ์ฃผ๊ฐ„ ๋žญํ‚น๊ณผ ์›”๊ฐ„ ๋žญํ‚น์„ ๋ณ„๋„ Step์œผ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฐฉ์•ˆ์„ ๊ณ ๋ คํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์„ ํƒ: Job ํŒŒ๋ผ๋ฏธํ„ฐ(periodType)๋กœ ๋ถ„๊ธฐํ•˜์—ฌ ๋ณ„๋„ ์‹คํ–‰ํ•˜๋Š” ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์ด์œ :
      • ์ฃผ๊ฐ„ ๋žญํ‚น๊ณผ ์›”๊ฐ„ ๋žญํ‚น์€ ์„œ๋กœ ๋…๋ฆฝ์ ์ธ ๊ฒฐ๊ณผ ์Šค๋ƒ…์ƒท์ด๋ฏ€๋กœ ๋ณ„๋„ ์‹คํ–‰์ด ์ž์—ฐ์Šค๋Ÿฌ์›€
      • ์‹คํ–‰ ์ฃผ๊ธฐ๊ฐ€ ๋‹ค๋ฅด๋ฏ€๋กœ(์ฃผ๊ฐ„์€ ๋งค์ฃผ, ์›”๊ฐ„์€ ๋งค์›”) ๋ณ„๋„ ์‹คํ–‰์ด ๋” ์ ํ•ฉ
      • ํ•˜๋‚˜์˜ Job์—์„œ ์ฃผ๊ฐ„๊ณผ ์›”๊ฐ„์„ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋ถˆํ•„์š”ํ•œ ์˜์กด์„ฑ๊ณผ ๋ณต์žก๋„๊ฐ€ ์ฆ๊ฐ€
    • ํŠธ๋ ˆ์ด๋“œ์˜คํ”„: Step ๋‹จ์œ„ ์žฌ์‹œ์ž‘์€ ๋ถˆ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, Job ๋‹จ์œ„ ์žฌ์‹œ์ž‘์œผ๋กœ ์ถฉ๋ถ„ํ•˜๋ฉฐ ๊ตฌ์กฐ๊ฐ€ ๋‹จ์ˆœํ•จ
  4. Chunk ๋‹จ์œ„ ์ฒ˜๋ฆฌ์™€ ์ „์ฒด ๋ฐ์ดํ„ฐ ์ง‘๊ณ„

    • ๊ณ ๋ฏผ: Step 1์—์„œ Chunk ๋‹จ์œ„๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด์„œ๋„ ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ง‘๊ณ„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    • ์„ ํƒ: Chunk ๋‹จ์œ„๋กœ ์ง‘๊ณ„ํ•˜๋˜, ๊ฐ™์€ product_id๊ฐ€ ์—ฌ๋Ÿฌ Chunk์— ๊ฑธ์ณ ์žˆ์„ ๊ฒฝ์šฐ ์ž„์‹œ ํ…Œ์ด๋ธ”(tmp_product_rank_score)์— UPSERT ๋ฐฉ์‹์œผ๋กœ ๋ˆ„์ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์ด์œ :
      • ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ์„ ์œ„ํ•ด Chunk ๋‹จ์œ„๋กœ ์ฒ˜๋ฆฌ
      • ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๊ธฐ ์ „์— ์ง‘๊ณ„๋ฅผ ์™„๋ฃŒํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ, ์ž„์‹œ ํ…Œ์ด๋ธ”์— ๋ˆ„์  ์ €์žฅ
      • Step 2์—์„œ ์ „์ฒด ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด TOP 100 ์„ ์ •
    • ๊ตฌํ˜„: Step 1์˜ Writer์—์„œ ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜์—ฌ ๋ˆ„์ ํ•œ ํ›„ entityManager.merge()๋กœ ์ €์žฅ
  5. Materialized View ์ €์žฅ ๋ฐฉ์‹: delete+insert

    • ๊ณ ๋ฏผ: Step 2์—์„œ Materialized View์— ์ €์žฅํ•  ๋•Œ upsert, delete+insert, staging ๊ธฐ๋ฐ˜ ๊ต์ฒด ๋ฐฉ์‹์„ ๊ณ ๋ คํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์„ ํƒ: delete+insert ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์ด์œ :
      • ๋‹จ์ˆœํ•˜๊ณ  ์˜๋„๊ฐ€ ๋ช…ํ™•ํ•จ (๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ์™„์ „ํžˆ ๊ต์ฒด)
      • ๋žญํ‚น์„ ๊ธฐ๊ฐ„ ์ข…๋ฃŒ ์‹œ์ ์˜ ์Šค๋ƒ…์ƒท์œผ๋กœ ๋‹ค๋ฃจ๋Š” ์„ค๊ณ„ ์›์น™๊ณผ ์ผ์น˜
      • ๊ณผ์ œ ๋ฒ”์œ„์™€ ์šด์˜ ๋ณต์žก๋„๋ฅผ ๊ณ ๋ คํ–ˆ์„ ๋•Œ ๊ฐ€์žฅ ์ ์ ˆ
    • ๊ตฌํ˜„: saveRanks() ๋ฉ”์„œ๋“œ์—์„œ deleteByPeriod() ํ˜ธ์ถœ ํ›„ entityManager.persist()๋กœ ์ €์žฅ

2. Materialized View ์„ค๊ณ„: ํ•˜๋‚˜์˜ ํ…Œ์ด๋ธ”์— period_type์œผ๋กœ ๊ตฌ๋ถ„

๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ œ ์ƒํ™ฉ:
์š”๊ตฌ์‚ฌํ•ญ์—์„œ๋Š” mv_product_rank_weekly์™€ mv_product_rank_monthly๋ฅผ ๋ณ„๋„ ํ…Œ์ด๋ธ”๋กœ ์„ค๊ณ„ํ•˜๋ผ๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ํ•˜๋‚˜์˜ ํ…Œ์ด๋ธ”(mv_product_rank)์— period_type ์ปฌ๋Ÿผ์œผ๋กœ ์ฃผ๊ฐ„/์›”๊ฐ„์„ ๊ตฌ๋ถ„ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ:
๋…ผ๋ฆฌ์ ์œผ๋กœ๋Š” ๋ณ„๋„ ํ…Œ์ด๋ธ”์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜์ง€๋งŒ, ๋ฌผ๋ฆฌ์ ์œผ๋กœ๋Š” ํ•˜๋‚˜์˜ ํ…Œ์ด๋ธ”์— period_type์œผ๋กœ ๊ตฌ๋ถ„ํ•˜๋Š” ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค:

  • ํ…Œ์ด๋ธ” ๊ตฌ์กฐ: mv_product_rank ํ…Œ์ด๋ธ”์— period_type (WEEKLY/MONTHLY) ์ปฌ๋Ÿผ์œผ๋กœ ๊ตฌ๋ถ„
  • ์ธ๋ฑ์Šค ์ „๋žต: (period_type, period_start_date, rank) ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋กœ ๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์กฐํšŒ ์ตœ์ ํ™”
  • ์กฐํšŒ ๋กœ์ง: period_type๊ณผ period_start_date๋กœ ํ•„ํ„ฐ๋งํ•˜์—ฌ ์กฐํšŒ

์ด ๋ฐฉ์‹์˜ ์žฅ์ :

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

๊ด€๋ จ ์ฝ”๋“œ:

// apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.java
@Entity
@Table(
    name = "mv_product_rank",
    indexes = {
        @Index(name = "idx_period_type_start_date_rank", columnList = "period_type, period_start_date, rank"),
        @Index(name = "idx_period_type_start_date_product_id", columnList = "period_type, period_start_date, product_id")
    }
)
public class ProductRank {
    @Enumerated(EnumType.STRING)
    @Column(name = "period_type", nullable = false, length = 20)
    private PeriodType periodType; // WEEKLY ๋˜๋Š” MONTHLY
    
    @Column(name = "period_start_date", nullable = false)
    private LocalDate periodStartDate;
    // ...
}

๊ณ ๋ฏผํ•œ ์ :

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

3. ๋ฐฐ์น˜ ๋ชจ๋“ˆ ๋ถ„๋ฆฌ: API์™€ ๋ฐฐ์น˜๋ฅผ ๋…๋ฆฝ์ ์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๋ถ„๋ฆฌ

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

๋ถ„๋ฆฌ์˜ ํ•ต์‹ฌ ์ด์œ :

  1. ์‹คํ–‰ ์ฃผ๊ธฐ์˜ ์ฐจ์ด

    • API: ์ง€์† ์‹คํ–‰ (Long-running) - HTTP ์š”์ฒญ ๋Œ€๊ธฐ ์ƒํƒœ๋กœ ๊ณ„์† ์‹คํ–‰
    • ๋ฐฐ์น˜: ๋‹จ๋ฐœ์„ฑ ์‹คํ–‰ (Short-lived) - Job ์‹คํ–‰ ํ›„ ์ž๋™ ์ข…๋ฃŒ
    # API: ์„œ๋ฒ„ ์‹œ์ž‘ ํ›„ ๊ณ„์† ์‹คํ–‰
    java -jar commerce-api.jar
    
    # ๋ฐฐ์น˜: Job ์™„๋ฃŒ ํ›„ ์ž๋™ ์ข…๋ฃŒ
    java -jar commerce-batch.jar \
      --spring.batch.job.names=productRankAggregationJob \
      periodType=WEEKLY targetDate=20241215
  2. ํŠธ๋žœ์žญ์…˜ ์„ฑ๊ฒฉ์˜ ์ฐจ์ด

    • API: ์งง์€ ํŠธ๋žœ์žญ์…˜ (์ˆ˜๋ฐฑ ms ~ ์ˆ˜์ดˆ), ๋‹ค์ค‘ ์š”์ฒญ ๋™์‹œ ์ฒ˜๋ฆฌ
    • ๋ฐฐ์น˜: ๊ธด ํŠธ๋žœ์žญ์…˜ (์ˆ˜๋ถ„ ~ ์ˆ˜์‹œ๊ฐ„), Chunk ๋‹จ์œ„ ์ˆœ์ฐจ ์ฒ˜๋ฆฌ, ์žฌ์‹œ์ž‘ ๊ฐ€๋Šฅ
  3. ์žฅ์•  ๋Œ€์‘ ๋ฐฉ์‹์˜ ์ฐจ์ด

    • API: ์ฆ‰์‹œ ์‘๋‹ต (Circuit Breaker, Retry, Fallback)
    • ๋ฐฐ์น˜: ์žฌ์‹œ์ž‘ ๊ฐ€๋Šฅ (Chunk ๋‹จ์œ„ ์žฌ์‹œ์ž‘, Spring Batch ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋กœ ์žฌ์‹œ์ž‘ ์ง€์  ์ถ”์ )
  4. ๋…๋ฆฝ์  ์‹คํ–‰, ์žฌ์‹คํ–‰, ๊ด€์ธก

    • ๋…๋ฆฝ ์‹คํ–‰: ๋ฐฐ์น˜ ์‹คํ–‰ ์‹œ API ์„œ๋ฒ„ ๋ถˆํ•„์š”
    • ๋…๋ฆฝ ์žฌ์‹คํ–‰: ์‹คํŒจ ์‹œ ๋งˆ์ง€๋ง‰ ์™„๋ฃŒ๋œ Chunk๋ถ€ํ„ฐ ์žฌ์‹œ์ž‘, ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ
    • ๊ด€์ธก ๊ฐ€๋Šฅ: Spring Batch ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋กœ Job/Step/Chunk ๋‹จ์œ„ ์ถ”์ 

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ:
commerce-batch ๋ชจ๋“ˆ์„ ๋ณ„๋„๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๋…๋ฆฝ์ ์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค:

  • ๋…๋ฆฝ ์‹คํ–‰: BatchApplication์„ ํ†ตํ•ด ๋ฐฐ์น˜๋งŒ ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰ ๊ฐ€๋Šฅ
  • ์„ค์ • ๋ถ„๋ฆฌ: application.yml์—์„œ ๋ฐฐ์น˜ ์ „์šฉ ์„ค์ • ๊ด€๋ฆฌ (์›น ์„œ๋ฒ„ ๋น„ํ™œ์„ฑํ™”, Job ์ž๋™ ์‹คํ–‰ ๋น„ํ™œ์„ฑํ™”)
  • ์˜์กด์„ฑ ์ตœ์†Œํ™”: Kafka, Feign Client, Resilience4j ๋“ฑ ๋ถˆํ•„์š”ํ•œ ์˜์กด์„ฑ ์ œ๊ฑฐ
  • ๋„๋ฉ”์ธ ๊ณต์œ : com.loopers.domain ํŒจํ‚ค์ง€์˜ ๋„๋ฉ”์ธ์€ ๊ณต์œ ํ•˜๋˜, Repository ๊ตฌํ˜„์€ ๋ชจ๋“ˆ๋ณ„๋กœ ๋ถ„๋ฆฌ
  • ํ…Œ์ŠคํŠธ ์ „๋žต ๋ถ„๋ฆฌ: ๋ฐฐ์น˜ ํ…Œ์ŠคํŠธ๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ค‘์‹ฌ์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ ๊ตฌ์„ฑ

๊ตฌ์กฐ:

commerce-api/
  โ””โ”€ API ์š”์ฒญ ์ฒ˜๋ฆฌ, ์‹ค์‹œ๊ฐ„ ๋žญํ‚น ์กฐํšŒ (Redis)
      โ”œโ”€ ์›น ์„œ๋ฒ„ ํ™œ์„ฑํ™” (Servlet)
      โ”œโ”€ Feign Client, Resilience4j
      โ””โ”€ HTTP ์š”์ฒญ ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ
  
commerce-batch/
  โ””โ”€ ๋ฐฐ์น˜ ์ง‘๊ณ„, Materialized View ์ ์žฌ
      โ”œโ”€ BatchApplication (๋…๋ฆฝ ์‹คํ–‰, Job ์™„๋ฃŒ ํ›„ ์ž๋™ ์ข…๋ฃŒ)
      โ”œโ”€ ProductRankJobConfig (Job/Step ๊ตฌ์„ฑ)
      โ”œโ”€ ์›น ์„œ๋ฒ„ ๋น„ํ™œ์„ฑํ™” (web-application-type: none)
      โ”œโ”€ Spring Batch ์ „์šฉ ์˜์กด์„ฑ
      โ””โ”€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ค‘์‹ฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

๊ด€๋ จ ์ฝ”๋“œ:

// apps/commerce-batch/src/main/java/com/loopers/BatchApplication.java
@SpringBootApplication(scanBasePackages = "com.loopers")
@EnableJpaRepositories(basePackages = "com.loopers.infrastructure")
@EntityScan(basePackages = "com.loopers.domain")
public class BatchApplication {
    public static void main(String[] args) {
        // Job ์™„๋ฃŒ ํ›„ ์ž๋™ ์ข…๋ฃŒ
        System.exit(SpringApplication.exit(SpringApplication.run(BatchApplication.class, args)));
    }
}

// apps/commerce-batch/src/main/resources/application.yml
spring:
  main:
    web-application-type: none # ๋ฐฐ์น˜ ์ „์šฉ์ด๋ฏ€๋กœ ์›น ์„œ๋ฒ„ ๋ถˆํ•„์š”
  batch:
    jdbc:
      initialize-schema: always # Spring Batch ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์ž๋™ ์ƒ์„ฑ
    job:
      enabled: false # ๋ช…๋ น์ค„์—์„œ ์ˆ˜๋™ ์‹คํ–‰ํ•˜๋ฏ€๋กœ ์ž๋™ ์‹คํ–‰ ๋น„ํ™œ์„ฑํ™”

๋ถ„๋ฆฌ์˜ ํšจ๊ณผ:

  • โœ… ๊ด€๋ฆฌ ๋ณต์žก๋„ ๊ฐ์†Œ: ์„ค์ •, Job/Step ๊ตฌ์„ฑ, ํ…Œ์ŠคํŠธ ์ „๋žต ๋ถ„๋ฆฌ
  • โœ… ์˜์กด์„ฑ ์ตœ์†Œํ™”: ๋ฐฐ์น˜ ๋ชจ๋“ˆ์— ๋ถˆํ•„์š”ํ•œ ์˜์กด์„ฑ ์ œ๊ฑฐ (Kafka, Feign Client, Resilience4j)
  • โœ… ๋ฐฐํฌ ์ „๋žต ๋ถ„๋ฆฌ: API๋Š” ์ˆ˜ํ‰ ํ™•์žฅ, ๋ฐฐ์น˜๋Š” ์ˆ˜์ง ํ™•์žฅ
  • โœ… ๋ชจ๋‹ˆํ„ฐ๋ง ๋ถ„๋ฆฌ: API๋Š” Actuator, ๋ฐฐ์น˜๋Š” Spring Batch ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
  • โœ… ์žฅ์•  ๊ฒฉ๋ฆฌ: ๋ฐฐ์น˜ ์ž‘์—… ์‹คํŒจ๊ฐ€ API ์„œ๋น„์Šค์— ์˜ํ–ฅ ์—†์Œ

๊ณ ๋ฏผํ•œ ์ :

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

4. ๋ฐฐ์น˜ ํ…Œ์ŠคํŠธ ์ „๋žต: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ค‘์‹ฌ์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

๋ฐฐ๊ฒฝ ๋ฐ ์„ค๊ณ„ ์˜๋„:
๋ฉ˜ํ† ๋ง ์„ธ์…˜์—์„œ ๋ฐฐ์น˜ ์ „์ฒด๋ฅผ execํ•ด์„œ ์ž˜ ์‹คํ–‰๋˜๋Š”์ง€๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๊ทธ ์•ˆ์— ์žˆ๋Š” processor๊ฐ™์€ ์˜๋ฏธ์žˆ๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ๋‚ซ๋‹ค๋Š” ์กฐ์–ธ์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋ฐฐ์น˜ ์ „์ฒด ์‹คํ–‰ ํ…Œ์ŠคํŠธ ๋Œ€์‹ , ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์— ์ดˆ์ ์„ ๋‘์—ˆ์Šต๋‹ˆ๋‹ค:

  • Reader/Processor/Writer ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง(์ง‘๊ณ„, ์ ์ˆ˜ ๊ณ„์‚ฐ, ๋žญํ‚น ๋ถ€์—ฌ ๋“ฑ)์„ Mock์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒฉ๋ฆฌ๋œ ํ™˜๊ฒฝ์—์„œ ๊ฒ€์ฆ
  • ๋ฐฐ์น˜ ์ „์ฒด ์‹คํ–‰ ํ…Œ์ŠคํŠธ๋Š” ์ œ์™ธ: ๋ฐฐ์น˜ ์ „์ฒด๋ฅผ ์‹คํ–‰ํ•˜๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” ์ž‘์„ฑํ•˜์ง€ ์•Š์Œ
  • ํ•ต์‹ฌ ๋กœ์ง ๊ฒ€์ฆ: ๋ฉ”ํŠธ๋ฆญ ์ง‘๊ณ„, ์ ์ˆ˜ ๊ณ„์‚ฐ, TOP 100 ํ•„ํ„ฐ๋ง ๋“ฑ ํ•ต์‹ฌ ๋กœ์ง๋งŒ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฒ€์ฆ

ํ…Œ์ŠคํŠธ ์˜ˆ์‹œ:

// apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriterTest.java
@ExtendWith(MockitoExtension.class)
class ProductRankScoreAggregationWriterTest {
    @Mock
    private ProductRankScoreRepository productRankScoreRepository;
    
    @InjectMocks
    private ProductRankScoreAggregationWriter writer;
    
    @Test
    void aggregatesMetricsByProductId() throws Exception {
        // Chunk ๋‚ด์—์„œ ๊ฐ™์€ product_id๋ฅผ ๊ฐ€์ง„ ๋ฉ”ํŠธ๋ฆญ์„ ์ง‘๊ณ„ํ•˜๋Š” ๋กœ์ง ๊ฒ€์ฆ
        // ...
    }
    
    @Test
    void calculatesScoreWithCorrectWeights() throws Exception {
        // ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง ๊ฒ€์ฆ (๊ฐ€์ค‘์น˜: ์ข‹์•„์š” 0.3, ํŒ๋งค๋Ÿ‰ 0.5, ์กฐํšŒ์ˆ˜ 0.2)
        // ...
    }
}

๊ณ ๋ฏผํ•œ ์ :

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

โœ… Checklist

Spring Batch

  • Spring Batch Job์„ ์ž‘์„ฑํ•˜๊ณ , ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค

    • Job ํŒŒ๋ผ๋ฏธํ„ฐ: periodType(WEEKLY/MONTHLY), targetDate(yyyyMMdd)
    • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java
  • Chunk Oriented Processing (Reader/Processor/Writer) ๊ธฐ๋ฐ˜์˜ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค

    • Chunk ํฌ๊ธฐ: 100
    • Step 1: Reader(ํŽ˜์ด์ง• ์กฐํšŒ) โ†’ Processor(Pass-through) โ†’ Writer(์ง‘๊ณ„ ๋ฐ ์ €์žฅ)
    • Step 2: Reader(์ „์ฒด ์กฐํšŒ) โ†’ Processor(TOP 100 ์„ ์ •) โ†’ Writer(์ €์žฅ)
    • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java
  • ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  Materialized View์˜ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์žฌํ–ˆ๋‹ค

    • ํ…Œ์ด๋ธ”: mv_product_rank (period_type์œผ๋กœ ์ฃผ๊ฐ„/์›”๊ฐ„ ๊ตฌ๋ถ„)
    • ์ €์žฅ ๋ฐฉ์‹: delete + insert (TOP 100๋งŒ ์ €์žฅ)
    • apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.java

Ranking API

  • API๊ฐ€ ์ผ๊ฐ„, ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น์„ ์ œ๊ณตํ•˜๋ฉฐ ์กฐํšŒํ•ด์•ผ ํ•˜๋Š” ํ˜•ํƒœ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋žญํ‚น์„ ์ œ๊ณตํ•œ๋‹ค
    • ์ผ๊ฐ„: Redis ZSET์—์„œ ์กฐํšŒ
    • ์ฃผ๊ฐ„/์›”๊ฐ„: Materialized View์—์„œ ์กฐํšŒ
    • GET /api/v1/rankings?date=yyyyMMdd&period=WEEKLY&size=20&page=1
    • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
    • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java

๐Ÿ“Ž References

Summary by CodeRabbit

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

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

    • ์ƒํ’ˆ ์ˆœ์œ„ ์กฐํšŒ์— ๊ธฐ๊ฐ„ ์„ ํƒ ์˜ต์…˜ ์ถ”๊ฐ€ (์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„)
    • ์ผ๊ฐ„ ์ˆœ์œ„๋Š” Redis์—์„œ, ์ฃผ๊ฐ„/์›”๊ฐ„ ์ˆœ์œ„๋Š” ์ตœ์ ํ™”๋œ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ์—์„œ ์ œ๊ณต
    • ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์ง€์›ํ•˜๋Š” ์ƒํ’ˆ ์ˆœ์œ„ ์กฐํšŒ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
  • ๊ฐœ์„ 

    • ์ˆœ์œ„ ์กฐํšŒ ์‹คํŒจ ์‹œ ์ž๋™์œผ๋กœ ์ด์ „ ๋ฐ์ดํ„ฐ๋กœ ์•ˆ์ •์  ์ œ๊ณต
    • ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๊ธฐ๋ฐ˜ ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ ๋ฐ ์ˆœ์œ„ ๊ณ„์‚ฐ ์‹œ์Šคํ…œ ๊ตฌ์ถ•

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

* feat: batch ์ฒ˜๋ฆฌ ๋ชจ๋“‡ ๋ถ„๋ฆฌ

* feat: batch ๋ชจ๋“ˆ์— ProductMetrics ๋„๋ฉ”์ธ ์ถ”๊ฐ€

* feat: ProudctMetrics์˜ Repository ์ถ”๊ฐ€

* test: Product Metrics ๋ฐฐ์น˜ ์ž‘์—…์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€

* feat: ProductMetrics ๋ฐฐ์น˜ ์ž‘์—… ๊ตฌํ˜„

* test: Product Rank์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€

* feat: Product Rank ๋„๋ฉ”์ธ ๊ตฌํ˜„

* feat: Product Rank Repository ์ถ”๊ฐ€

* test: Product Rank ๋ฐฐ์น˜์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€

* feat: Product Rank ๋ฐฐ์น˜ ์ž‘์—… ์ถ”๊ฐ€

* feat: ์ผ๊ฐ„, ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น์„ ์ œ๊ณตํ•˜๋Š” api ์ถ”๊ฐ€

* refractor: ๋žญํ‚น ์ง‘๊ณ„ ๋กœ์ง์„ ์—ฌ๋Ÿฌ step์œผ๋กœ ๋ถ„๋ฆฌํ•จ

* chore: db ์ดˆ๊ธฐํ™” ๋กœ์ง์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜ ์ˆ˜์ •

* test: ๋žญํ‚น ์ง‘๊ณ„์˜ ๊ฐ step์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€
@coderabbitai
Copy link

coderabbitai bot commented Jan 1, 2026

Walkthrough

์ƒˆ๋กœ์šด ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋ชจ๋“ˆ(commerce-batch)์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์ƒํ’ˆ ์ˆœ์œ„๋ฅผ ์ผ์ผ(Redis), ์ฃผ๊ฐ„/์›”๊ฐ„(๊ตฌ์ฒดํ™”๋œ ๋ทฐ)์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ™•์žฅํ–ˆ์Šต๋‹ˆ๋‹ค. RankingService์— ๋‹ค์ค‘ ๊ธฐ๊ฐ„ ์ฟผ๋ฆฌ ์ง€์›, ProductMetrics ๋ฐ ProductRank ์—”ํ‹ฐํ‹ฐ, ๋ฐฐ์น˜ ์ž‘์—… ๋ฐ ๊ด€๋ จ ์ €์žฅ์†Œ๋ฅผ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค.

Changes

์ฝ”ํ˜ธํŠธ / ํŒŒ์ผ ๋ณ€๊ฒฝ ์š”์•ฝ
๊ธฐ์กด ์˜์กด์„ฑ ์ œ๊ฑฐ
apps/commerce-api/build.gradle.kts, apps/commerce-api/src/main/resources/application.yml
Spring Boot Batch ์˜์กด์„ฑ ๋ฐ ์„ค์ • ์ œ๊ฑฐ; ๋ฐฐ์น˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ๋น„ํ™œ์„ฑํ™”
API ์ˆœ์œ„ ๊ธฐ๋Šฅ ํ™•์žฅ
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
PeriodType(DAILY/WEEKLY/MONTHLY) ์—ด๊ฑฐํ˜• ์ถ”๊ฐ€; getRankings์— ๋‹ค์ค‘ ๊ธฐ๊ฐ„ ๋ผ์šฐํŒ… ๋กœ์ง ์ถ”๊ฐ€; ๊ตฌ์ฒดํ™”๋œ ๋ทฐ ์ง€์› ๋ฉ”์„œ๋“œ ๋ฐ ์ ์ˆ˜ ๊ณ„์‚ฐ ์ถ”๊ฐ€; ์„ ํƒ์  period ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ปจํŠธ๋กค๋Ÿฌ ์—…๋ฐ์ดํŠธ
์ƒํ’ˆ ์ˆœ์œ„ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ(API)
apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.java, apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRankRepository.java
๊ตฌ์ฒดํ™”๋œ ๋ทฐ(mv_product_rank)์— ๋งคํ•‘๋œ ProductRank ์—”ํ‹ฐํ‹ฐ ์ถ”๊ฐ€; ๊ธฐ๊ฐ„๋ณ„ ์กฐํšŒ๋ฅผ ์œ„ํ•œ ์ธ๋ฑ์Šค ๋ฐ ProductRankRepository ์ธํ„ฐํŽ˜์ด์Šค ์ถ”๊ฐ€
์ƒํ’ˆ ์ˆœ์œ„ ์ €์žฅ์†Œ ๊ตฌํ˜„(API)
apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.java
JPA EntityManager๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ProductRankRepository ๊ตฌํ˜„ ์ถ”๊ฐ€
๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ง„์ž…์ 
apps/commerce-batch/src/main/java/com/loopers/BatchApplication.java, apps/commerce-batch/build.gradle.kts, apps/commerce-batch/src/main/resources/application.yml
์ƒˆ๋กœ์šด ๋ฐฐ์น˜ ๋ชจ๋“ˆ ์ƒ์„ฑ; ์˜์กด์„ฑ ์ถ”๊ฐ€(JPA, Redis, Jackson, Batch); ๋ฐฐ์น˜ ์„ค์ • ํŒŒ์ผ ์ถ”๊ฐ€
๋ฉ”ํŠธ๋ฆญ ์ง‘๊ณ„ ๋„๋ฉ”์ธ
apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java, apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
๋ฐฐ์น˜ ์ง‘๊ณ„์šฉ ProductMetrics ์—”ํ‹ฐํ‹ฐ ์ถ”๊ฐ€; ์ข‹์•„์š”/ํŒ๋งค/์กฐํšŒ ์ˆ˜ ์ฆ๊ฐ ๋ฉ”์„œ๋“œ ํฌํ•จ; ๋ฒ„์ „ ๊ด€๋ฆฌ ์ง€์›
๋ฉ”ํŠธ๋ฆญ ์ €์žฅ์†Œ ๊ตฌํ˜„
apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java
JPA ์ €์žฅ์†Œ ๋ฐ ๊ตฌํ˜„ ์ถ”๊ฐ€; ๊ธฐ๊ฐ„ ๋ฒ”์œ„ ์ฟผ๋ฆฌ(findByUpdatedAtBetween) ํฌํ•จ
๋ฉ”ํŠธ๋ฆญ ๋ฐฐ์น˜ ์ž‘์—…
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReader.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessor.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriter.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsJobConfig.java
์ฒญํฌ ๊ธฐ๋ฐ˜ ๋ฉ”ํŠธ๋ฆญ ์ฝ๊ธฐ/์ฒ˜๋ฆฌ/์“ฐ๊ธฐ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€; targetDate ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฒ”์œ„ ์ฟผ๋ฆฌ ๊ตฌ์„ฑ
์ˆœ์œ„ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋„๋ฉ”์ธ
apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScore.java, apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScoreRepository.java
์ž„์‹œ ์ ์ˆ˜ ์ง‘๊ณ„์šฉ ProductRankScore ์—”ํ‹ฐํ‹ฐ ์ถ”๊ฐ€(tmp_product_rank_score ํ…Œ์ด๋ธ”); ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ์ ์ˆ˜ ๊ณ„์‚ฐ ์ง€์›
์ˆœ์œ„ ๊ณ„์‚ฐ ๋„๋ฉ”์ธ
apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.java, apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankRepository.java
๋ฐฐ์น˜์šฉ ProductRank ์—”ํ‹ฐํ‹ฐ ์ถ”๊ฐ€; updateRank ๋ฉ”์„œ๋“œ ํฌํ•จ; UPSERT ์˜๋ฏธ๋ก ์˜ ์ €์žฅ์†Œ ์ธํ„ฐํŽ˜์ด์Šค ์ถ”๊ฐ€
์ˆœ์œ„ ์ง‘๊ณ„ ๋ฐฐ์น˜ ์ž‘์—…
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessor.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReader.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.java
์ฃผ๊ฐ„/์›”๊ฐ„ ๊ธฐ๊ฐ„ ๊ด€๋ฆฌ ํ”„๋กœ์„ธ์„œ ์ถ”๊ฐ€; ๋ฒ”์œ„๋ณ„ ๋ฉ”ํŠธ๋ฆญ ์ฝ๊ธฐ ์ถ”๊ฐ€; ์ง‘๊ณ„๋œ ์ ์ˆ˜ ์ €์žฅ์†Œ์— ์“ฐ๊ธฐ ์ถ”๊ฐ€
์ˆœ์œ„ ๊ณ„์‚ฐ ๋ฐฐ์น˜ ์ž‘์—…
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessor.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationReader.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationWriter.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java
ProductRankScore๋ฅผ ProductRank๋กœ ๋ณ€ํ™˜; TOP 100 ์„ ํƒ; ๊ตฌ์ฒดํ™”๋œ ๋ทฐ์— ์ˆœ์œ„ ์ €์žฅ
์ €์žฅ์†Œ ๊ตฌํ˜„(์ˆœ์œ„)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankScoreRepositoryImpl.java
ProductRank ๋ฐ ProductRankScore ์ €์žฅ์†Œ์˜ JPA ๊ธฐ๋ฐ˜ ๊ตฌํ˜„ ์ถ”๊ฐ€
ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€
apps/commerce-batch/src/test/java/com/loopers/domain/metrics/*, apps/commerce-batch/src/test/java/com/loopers/domain/rank/*, apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/*/*
ProductMetrics ์—”ํ‹ฐํ‹ฐ, ProductRank ์—”ํ‹ฐํ‹ฐ, ๋ฐฐ์น˜ ์ปดํฌ๋„ŒํŠธ(Reader/Processor/Writer) ๋ฐ ์ง‘๊ณ„ ๋กœ์ง์— ๋Œ€ํ•œ ํฌ๊ด„์  ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
์ธํ”„๋ผ ์œ ํ‹ธ
modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java
TRUNCATE TABLE ์ „์— ํ…Œ์ด๋ธ” ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ ์ถ”๊ฐ€; ํ…Œ์ŠคํŠธ ์•ˆ์ •์„ฑ ๊ฐœ์„ 
๊ทธ๋ž˜๋“ค ์„ค์ •
settings.gradle.kts
๋ชจ๋“ˆ ๋ชฉ๋ก์— :apps:commerce-batch ์ถ”๊ฐ€

Sequence Diagrams

sequenceDiagram
    participant Client
    participant RankingV1Controller
    participant RankingService
    participant Redis as Redis<br/>(DAILY)
    participant MV as Materialized View<br/>(WEEKLY/MONTHLY)
    participant ProductDB as Product<br/>Repository

    Client->>RankingV1Controller: getRankings(date, period, page, size)
    RankingV1Controller->>RankingV1Controller: parsePeriodType(period)
    RankingV1Controller->>RankingService: getRankings(date, PeriodType, page, size)

    alt period == DAILY
        RankingService->>Redis: Redis ZSET ์กฐํšŒ
        Redis-->>RankingService: ์ˆœ์œ„ ๋ฐ์ดํ„ฐ
    else period == WEEKLY or MONTHLY
        RankingService->>MV: periodStartDate, PeriodType๋กœ ์กฐํšŒ
        MV-->>RankingService: TOP 100 ์ƒํ’ˆ ์ˆœ์œ„
    end

    RankingService->>ProductDB: ์ƒํ’ˆ/๋ธŒ๋žœ๋“œ ์ •๋ณด ์กฐํšŒ
    ProductDB-->>RankingService: ์ƒํ’ˆ ๋ฐ์ดํ„ฐ
    RankingService->>RankingService: ์ˆœ์œ„ ํ•ญ๋ชฉ ๊ตฌ์„ฑ
    RankingService-->>RankingV1Controller: RankingsResponse (ํŽ˜์ด์ง€๋„ค์ด์…˜)
    RankingV1Controller-->>Client: ApiResponse<RankingsResponse>
Loading
sequenceDiagram
    participant Scheduler as Job Scheduler
    participant BatchApp as Batch<br/>Application
    participant MetricsJob as ProductMetrics<br/>Aggregation Job
    participant RankJob as ProductRank<br/>Aggregation Job
    participant MetricsDB as ProductMetrics<br/>Table
    participant ScoreTable as ProductRankScore<br/>(Temp)
    participant RankMV as ProductRank MV<br/>(mv_product_rank)

    Scheduler->>BatchApp: productMetricsAggregationJob(targetDate)
    BatchApp->>MetricsJob: Step 1: ๋ฉ”ํŠธ๋ฆญ ์ฝ๊ธฐ/์ฒ˜๋ฆฌ/์“ฐ๊ธฐ
    MetricsJob->>MetricsDB: updatedAt ๋ฒ”์œ„๋กœ ์กฐํšŒ
    MetricsDB-->>MetricsJob: ProductMetrics ์ฒญํฌ
    MetricsJob->>MetricsJob: pass-through (์ฒ˜๋ฆฌ)
    MetricsJob->>MetricsJob: ๋กœ๊น…

    Scheduler->>BatchApp: productRankAggregationJob(periodType, targetDate)
    BatchApp->>RankJob: Step 1: ๋ฉ”ํŠธ๋ฆญ -> ์ ์ˆ˜ ์ง‘๊ณ„
    RankJob->>MetricsDB: ๋ฒ”์œ„๋ณ„ ๋ฉ”ํŠธ๋ฆญ ์ฝ๊ธฐ
    MetricsDB-->>RankJob: ProductMetrics
    RankJob->>RankJob: ์ œํ’ˆ๋ณ„ ๊ทธ๋ฃนํ™”<br/>๊ฐ€์ค‘์น˜ ์ ์ˆ˜ ๊ณ„์‚ฐ<br/>(์ข‹์•„์š” 0.3, ํŒ๋งค 0.5, ์กฐํšŒ 0.2)
    RankJob->>ScoreTable: ProductRankScore ์ €์žฅ (UPSERT)
    ScoreTable-->>RankJob: ์ €์žฅ ์™„๋ฃŒ

    BatchApp->>RankJob: Step 2: ์ ์ˆ˜ -> ์ˆœ์œ„ ๋ณ€ํ™˜
    RankJob->>ScoreTable: ์ ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ ์กฐํšŒ
    ScoreTable-->>RankJob: ProductRankScore (๋ชจ๋“  ํ•ญ๋ชฉ)
    RankJob->>RankJob: TOP 100 ์„ ํƒ<br/>1-100 ์ˆœ์œ„ ํ• ๋‹น<br/>(ThreadLocal ์นด์šดํ„ฐ)
    RankJob->>RankMV: ProductRank ์ €์žฅ<br/>(๊ธฐ๊ฐ„๋ณ„ UPSERT)
    RankMV-->>RankJob: ์ €์žฅ ์™„๋ฃŒ

    RankJob->>ScoreTable: ์ž„์‹œ ํ…Œ์ด๋ธ” ์ •๋ฆฌ
Loading

Estimated code review effort

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

Possibly related PRs

  • [volume - 9] What is Popularity?ย #210: ์ดˆ๊ธฐ Redis ๊ธฐ๋ฐ˜ ์ˆœ์œ„ ์ง€์ • ๊ตฌํ˜„ ์œ„์— ๊ตฌ์ฒดํ™”๋œ ๋ทฐ ๋ฐ ๋ฐฐ์น˜ ์ง€์›์„ ์ถ”๊ฐ€ํ•œ ๊ฒƒ์œผ๋กœ, ๋™์ผ RankingService ๋ฐ ๊ด€๋ จ ์ˆœ์œ„ ๋„๋ฉ”์ธ ํด๋ž˜์Šค๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
  • [volume-9] Product Ranking with Redis ย #217: ๋™์ผ ์ˆœ์œ„ ์„œ๋ธŒ์‹œ์Šคํ…œ์„ ์ˆ˜์ •ํ•˜๋ฉฐ, ProductRank ๊ตฌ์ฒดํ™”๋œ ๋ทฐ/์—”ํ‹ฐํ‹ฐ/์ €์žฅ์†Œ ๋ฐ ๋ฉ”ํŠธ๋ฆญ/์ˆœ์œ„ ์ง‘๊ณ„ ๋ฐฐ์น˜ ์ž‘์—…์„ ์ถ”๊ฐ€/ํ™•์žฅํ•ฉ๋‹ˆ๋‹ค.
  • [volume-9] Product Ranking with Redisย #216: commerce-api ์ˆœ์œ„ ๊ธฐ๋Šฅ์„ ์ˆ˜์ •ํ•˜๋ฉฐ, RankingV1Controller ๋ฐ ์ˆœ์œ„ ์„œ๋น„์Šค/์ง‘๊ณ„/Redis ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ ์ฝ”๋“œ ๊ฒฝ๋กœ๋ฅผ ๊ฑด๋“œ๋ฆฝ๋‹ˆ๋‹ค.

Suggested labels

enhancement

Poem

๐Ÿฐ ๋ฐฐ์น˜ ๋ชจ๋“ˆ์„ ๊ฐ–์ถฐ์ง„ ํ† ๋ผ,
์ฃผ๊ฐ„๊ณผ ์›”๊ฐ„ ์ˆœ์œ„๋ฅผ ๋ฌถ์–ด,
๊ตฌ์ฒดํ™”๋œ ๋ทฐ์— ์ ์ˆ˜ ์ ์žฌ,
TOP 100์ด ๋ฐ˜์ง์ด๋ฉฐ,
์ƒํ’ˆ๋“ค์ด ์ถค์ถ˜๋‹ค๋„ค! โœจ

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 46.36% 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 ๊ธฐ๋ฐ˜ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌํ˜„์ด์ง€๋งŒ, ์ œ๋ชฉ์—์„œ๋Š” ์ด๋ฅผ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ œ๋ชฉ์„ ๋” ๊ตฌ์ฒด์ ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์„ธ์š”. ์˜ˆ: '[Feature] Spring Batch๋ฅผ ํ™œ์šฉํ•œ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌํ˜„' ๋˜๋Š” '[Feature] Materialized View ๊ธฐ๋ฐ˜ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„'
โœ… Passed checks (1 passed)
Check name Status Explanation
Description check โœ… Passed PR ์„ค๋ช…์ด ๊ตฌ์กฐํ™”๋œ ํ…œํ”Œ๋ฆฟ์„ ๋”ฐ๋ฅด๊ณ  ์ถฉ๋ถ„ํ•œ ์ปจํ…์ŠคํŠธ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
โœจ 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.

@minor7295
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 1, 2026

โœ… Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@minor7295 minor7295 self-assigned this Jan 1, 2026
@minor7295 minor7295 changed the title Feature/batch (#40) [volume-10] Collect, Stack, Zip Jan 1, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

๐Ÿงน Nitpick comments (24)
modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java (1)

43-57: ๋ชจ๋“  ์˜ˆ์™ธ๋ฅผ ๋ฌด์‹œํ•˜๋Š” ๊ฒƒ์€ ๋””๋ฒ„๊น…์„ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ด๋ธ” ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ๋จผ์ € ํ™•์ธํ•˜๋Š” ๋กœ์ง์€ ์ข‹์ง€๋งŒ, ๋ชจ๋“  ์˜ˆ์™ธ๋ฅผ ๋กœ๊น… ์—†์ด ์‚ผํ‚ค๋Š” ๊ฒƒ์€ ์‹ค์ œ ๋ฌธ์ œ(DB ์—ฐ๊ฒฐ ์˜ค๋ฅ˜, ๊ถŒํ•œ ๋ฌธ์ œ ๋“ฑ)๋ฅผ ์ˆจ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์‹คํŒจ ์›์ธ ํŒŒ์•…์ด ์–ด๋ ค์›Œ์งˆ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ตœ์†Œํ•œ์˜ ๋””๋ฒ„๊ทธ ๋กœ๊น…์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”.

๋˜ํ•œ, ํ…Œ์ด๋ธ” ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•œ ํ›„์—๋„ try-catch๊ฐ€ ํ•„์š”ํ•œ ์ด์œ ๊ฐ€ ๋ถˆ๋ถ„๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์กด์žฌ ํ™•์ธ์ด ์„ฑ๊ณตํ•˜๋ฉด TRUNCATE๋„ ์„ฑ๊ณตํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์œผ๋ฏ€๋กœ, ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ์›์ธ์„ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋กœ๊น…์ด ๋”์šฑ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ตœ์†Œ ๋กœ๊น… ์ถ”๊ฐ€ ์ œ์•ˆ
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 @Component
 public class DatabaseCleanUp implements InitializingBean {
+    private static final Logger log = LoggerFactory.getLogger(DatabaseCleanUp.class);
 
     @PersistenceContext
     private EntityManager entityManager;
             } catch (Exception e) {
-                // ํ…Œ์ด๋ธ”์ด ์—†๊ฑฐ๋‚˜ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋ฌด์‹œํ•˜๊ณ  ๊ณ„์† ์ง„ํ–‰
-                // ๋กœ๊ทธ๋Š” ๋‚จ๊ธฐ์ง€ ์•Š์Œ (ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ์ •์ƒ์ ์ธ ์ƒํ™ฉ์ผ ์ˆ˜ ์žˆ์Œ)
+                // ํ…Œ์ด๋ธ”์ด ์—†๊ฑฐ๋‚˜ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋ฌด์‹œํ•˜๊ณ  ๊ณ„์† ์ง„ํ–‰
+                log.debug("ํ…Œ์ด๋ธ” {} ์ •๋ฆฌ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ (๋ฌด์‹œ๋จ): {}", table, e.getMessage());
             }
apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (2)

64-71: ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ์œ„ํ•ด ์‹œ๊ฐ„ ์ฃผ์ž… ๊ณ ๋ ค

์ƒ์„ฑ์ž์—์„œ LocalDateTime.now()๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์ด ์–ด๋ ต๊ณ  ์‹œ์Šคํ…œ ์‹œ๊ฐ„์— ๊ฒฐํ•ฉ๋ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ๊ณผ ๋„๋ฉ”์ธ ์ˆœ์ˆ˜์„ฑ์„ ๊ฐœ์„ ํ•˜๋ ค๋ฉด ๋‹ค์Œ์„ ๊ณ ๋ คํ•˜์„ธ์š”:

  • ์˜ต์…˜ 1: updatedAt์„ ์ƒ์„ฑ์ž ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๊ธฐ
  • ์˜ต์…˜ 2: Clock ๊ฐ์ฒด๋ฅผ ์ฃผ์ž…๋ฐ›์•„ ์‚ฌ์šฉ
๐Ÿ”Ž ์‹œ๊ฐ„ ์ฃผ์ž… ์˜ˆ์‹œ
-public ProductMetrics(Long productId) {
+public ProductMetrics(Long productId, LocalDateTime now) {
     this.productId = productId;
     this.likeCount = 0L;
     this.salesCount = 0L;
     this.viewCount = 0L;
     this.version = 0L;
-    this.updatedAt = LocalDateTime.now();
+    this.updatedAt = now;
 }

76-113: ๋ฉ”์„œ๋“œ ๋‚ด ์‹œ๊ฐ„ ์ƒ์„ฑ ์ผ๊ด€์„ฑ

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

๊ธ์ •์ ์ธ ์ :

  • decrementLikeCount()์˜ ์Œ์ˆ˜ ๋ฐฉ์ง€ ๊ฐ€๋“œ ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค
  • incrementSalesCount()์˜ ์ˆ˜๋Ÿ‰ ๊ฒ€์ฆ์ด ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessorTest.java (1)

38-50: ์ค‘๋ณต ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค

processesNonNullItem ํ…Œ์ŠคํŠธ๋Š” processesItem_andReturnsSameItem ํ…Œ์ŠคํŠธ์™€ ์ค‘๋ณต๋ฉ๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ๊ฐ€ ์ด๋ฏธ ๋™์ผํ•œ ๊ฒ€์ฆ(๋™์ผ ๊ฐ์ฒด ๋ฐ˜ํ™˜, non-null)์„ ๋” ํฌ๊ด„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ด ์ค‘๋ณต ํ…Œ์ŠคํŠธ๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReaderTest.java (2)

13-15: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” import ๋ฌธ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

LocalDateTime๊ณผ LocalTime์ด import๋˜์—ˆ์ง€๋งŒ ์‹ค์ œ๋กœ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
 import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;

94-114: ํ…Œ์ŠคํŠธ ๊ฒ€์ฆ์ด ๋ถˆ์™„์ „ํ•ฉ๋‹ˆ๋‹ค.

parsesDateCorrectly_andSetsDateTimeRange ํ…Œ์ŠคํŠธ์—์„œ expectedStart์™€ expectedEnd ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธํ–ˆ์ง€๋งŒ ์‹ค์ œ ๊ฒ€์ฆ์— ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ฃผ์„์—์„œ "๊ฐ„์ ‘์ ์œผ๋กœ ๊ฒ€์ฆ"์ด๋ผ๊ณ  ์–ธ๊ธ‰ํ•˜๊ณ  ์žˆ์œผ๋‚˜, Reader ๋‚ด๋ถ€ ์ƒํƒœ๋ฅผ ์ง์ ‘ ๊ฒ€์ฆํ•˜๊ฑฐ๋‚˜ Repository ํ˜ธ์ถœ ์‹œ ์ „๋‹ฌ๋œ ์ธ์ž๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ์ด ๋” ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„์—์„œ๋Š” Repository ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ์‹œ ArgumentCaptor๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, Reader์˜ ๋‚ด๋ถ€ ์ƒํƒœ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ํ•ด๋‹น ๊ฐ’์„ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ํ˜„์žฌ ๊ตฌ์กฐ์—์„œ ๊ฒ€์ฆ์ด ์–ด๋ ต๋‹ค๋ฉด, ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜๋Š” ์ œ๊ฑฐํ•˜๊ณ  ํ…Œ์ŠคํŠธ ์ด๋ฆ„๊ณผ ์ฃผ์„์„ ์‹ค์ œ ๊ฒ€์ฆ ๋‚ด์šฉ์— ๋งž๊ฒŒ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/test/java/com/loopers/domain/rank/ProductRankTest.java (1)

54-82: ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ์—์„œ Thread.sleep ์‚ฌ์šฉ

Thread.sleep(1)์„ ์‚ฌ์šฉํ•˜์—ฌ ํƒ€์ž„์Šคํƒฌํ”„ ์ฐจ์ด๋ฅผ ๋ณด์žฅํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์ ‘๊ทผ ๋ฐฉ์‹์€ ๋™์ž‘ํ•˜์ง€๋งŒ, ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ„ํ—์ ์œผ๋กœ ์‹คํŒจํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋” ๊ฒฐ์ •์ ์ธ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด java.time.Clock์„ ์ฃผ์ž…ํ•˜์—ฌ ์‹œ๊ฐ„์„ ์ œ์–ดํ•˜๋Š” ๋ฐฉ์‹์„ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ, ํ˜„์žฌ 1ms sleep์€ ์‹ค์šฉ์ ์œผ๋กœ ์ถฉ๋ถ„ํžˆ ์•ˆ์ •์ ์ž…๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessor.java (1)

3-15: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” import ๋ฌธ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

ProductMetrics, ItemProcessor, Comparator, List, Map, Collectors, IntStream ๋“ฑ์˜ import๊ฐ€ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฆฌํŒฉํ† ๋ง ํ›„ ๋‚จ์€ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ •๋ฆฌ ์ œ์•ˆ
 package com.loopers.infrastructure.batch.rank;
 
-import com.loopers.domain.metrics.ProductMetrics;
 import com.loopers.domain.rank.ProductRank;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.batch.item.ItemProcessor;
 import org.springframework.stereotype.Component;
 
 import java.time.LocalDate;
 import java.time.temporal.TemporalAdjusters;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReaderTest.java (1)

100-150: ๋‚ ์งœ ๋ฒ”์œ„ ๊ณ„์‚ฐ ๋กœ์ง ๊ฒ€์ฆ์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

weeklyReaderCalculatesCorrectWeekRange_forAnyDayInWeek์™€ monthlyReaderCalculatesCorrectMonthRange_forAnyDayInMonth ํ…Œ์ŠคํŠธ๋Š” "๋ชจ๋‘ ๊ฐ™์€ ์ฃผ/์›”์˜ ์‹œ์ž‘์ผ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์•ผ ํ•จ"์ด๋ผ๊ณ  ์ฃผ์„์œผ๋กœ ๋ช…์‹œํ•˜์ง€๋งŒ, ์ด๋ฅผ ๊ฒ€์ฆํ•˜๋Š” assertion์ด ์—†์Šต๋‹ˆ๋‹ค.

๋‚ ์งœ ๋ฒ”์œ„ ๊ณ„์‚ฐ ๋กœ์ง์„ ๋ณ„๋„ ํ—ฌํผ ๋ฉ”์„œ๋“œ๋กœ ์ถ”์ถœํ•˜๋ฉด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ์šฉ์ดํ•ด์ง‘๋‹ˆ๋‹ค:

// ProductRankAggregationReader์— ์ถ”๊ฐ€
public LocalDate calculateWeekStart(LocalDate targetDate) {
    return targetDate.with(DayOfWeek.MONDAY);
}

public LocalDate calculateMonthStart(LocalDate targetDate) {
    return targetDate.with(TemporalAdjusters.firstDayOfMonth());
}

๊ทธ๋Ÿฐ ๋‹ค์Œ ์ด ๋ฉ”์„œ๋“œ๋“ค์„ ์ง์ ‘ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReader.java (1)

69-72: ๋ถˆ๋ณ€ Map ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”.

๋‹จ์ผ ํ•ญ๋ชฉ๋งŒ ํฌํ•จํ•˜๋Š” ์ •๋ ฌ ๊ธฐ์ค€์— HashMap์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Java 9+์˜ Map.of()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ๊ฐ„๊ฒฐํ•˜๊ณ  ๋ถˆ๋ณ€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
-        // ์ •๋ ฌ ๊ธฐ์ค€ ์„ค์ • (product_id ๊ธฐ์ค€ ์˜ค๋ฆ„์ฐจ์ˆœ)
-        Map<String, Sort.Direction> sorts = new HashMap<>();
-        sorts.put("productId", Sort.Direction.ASC);
+        // ์ •๋ ฌ ๊ธฐ์ค€ ์„ค์ • (product_id ๊ธฐ์ค€ ์˜ค๋ฆ„์ฐจ์ˆœ)
+        Map<String, Sort.Direction> sorts = Map.of("productId", Sort.Direction.ASC);

์ด ๊ฒฝ์šฐ java.util.HashMap import๋„ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScore.java (2)

34-41: ์ธ๋ฑ์Šค ์ •์˜ ๋ฐฉ์‹์— ๋Œ€ํ•œ ์ฐธ๊ณ ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค.

@Index์˜ unique = true์™€ @Column์˜ unique = true๊ฐ€ ๋ชจ๋‘ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค (Line 38๊ณผ Line 54). ๋‘ ์„ค์ • ๋ชจ๋‘ ์œ ๋‹ˆํฌ ์ œ์•ฝ์กฐ๊ฑด์„ ์ƒ์„ฑํ•˜๋ฏ€๋กœ ์ค‘๋ณต ์ •์˜์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”. ์ผ๋ฐ˜์ ์œผ๋กœ @Column(unique = true)๋Š” ๋‹จ์ผ ์ปฌ๋Ÿผ ์œ ๋‹ˆํฌ ์ œ์•ฝ์—, @Index(unique = true)๋Š” ๋ณตํ•ฉ ์ธ๋ฑ์Šค์˜ ์œ ๋‹ˆํฌ ์ œ์•ฝ์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.


95-101: setMetrics() ๋ฉ”์„œ๋“œ๊ฐ€ public์œผ๋กœ ๋…ธ์ถœ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

Javadoc์—์„œ "Repository์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ"๋ผ๊ณ  ๋ช…์‹œํ–ˆ์ง€๋งŒ ์ ‘๊ทผ ์ œ์–ด์ž๊ฐ€ public์ž…๋‹ˆ๋‹ค. ์˜๋„์น˜ ์•Š์€ ์™ธ๋ถ€ ์ ‘๊ทผ์„ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด package-private ๋˜๋Š” protected๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
-    public void setMetrics(Long likeCount, Long salesCount, Long viewCount, Double score) {
+    void setMetrics(Long likeCount, Long salesCount, Long viewCount, Double score) {
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationWriter.java (1)

76-79: ๋งค Chunk๋งˆ๋‹ค ์ „์ฒด ๋ฐ์ดํ„ฐ ์‚ญ์ œ+์‚ฝ์ž…์€ ๋น„ํšจ์œจ์ 

ํ˜„์žฌ ๊ตฌํ˜„์€ ๋งค Chunk๋งˆ๋‹ค delete + insert๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. Step ์™„๋ฃŒ ์‹œ์ ์— ํ•œ ๋ฒˆ๋งŒ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์ด ๋” ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. StepExecutionListener.afterStep()์—์„œ ์ตœ์ข… ์ €์žฅ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.java (1)

146-168: Java record๋กœ ๊ฐ„์†Œํ™” ๊ฐ€๋Šฅ

AggregatedMetrics ๋‚ด๋ถ€ ํด๋ž˜์Šค๋ฅผ Java record๋กœ ๋Œ€์ฒดํ•˜๋ฉด ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž record ์‚ฌ์šฉ ์ œ์•ˆ
-    private static class AggregatedMetrics {
-        private final Long likeCount;
-        private final Long salesCount;
-        private final Long viewCount;
-
-        public AggregatedMetrics(Long likeCount, Long salesCount, Long viewCount) {
-            this.likeCount = likeCount;
-            this.salesCount = salesCount;
-            this.viewCount = viewCount;
-        }
-
-        public Long getLikeCount() {
-            return likeCount;
-        }
-
-        public Long getSalesCount() {
-            return salesCount;
-        }
-
-        public Long getViewCount() {
-            return viewCount;
-        }
-    }
+    private record AggregatedMetrics(Long likeCount, Long salesCount, Long viewCount) {}
apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankScoreRepositoryImpl.java (1)

35-42: ๊ด€๋ฆฌ ์ƒํƒœ ์—”ํ‹ฐํ‹ฐ์— merge() ํ˜ธ์ถœ ๋ถˆํ•„์š”

existingScore๋Š” findByProductId์—์„œ ์กฐํšŒ๋œ ๊ด€๋ฆฌ(managed) ์ƒํƒœ ์—”ํ‹ฐํ‹ฐ์ž…๋‹ˆ๋‹ค. @Transactional ์ปจํ…์ŠคํŠธ ๋‚ด์—์„œ setMetrics()๋กœ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๋ฉด JPA dirty checking์— ์˜ํ•ด ์ž๋™์œผ๋กœ flush๋ฉ๋‹ˆ๋‹ค. merge() ํ˜ธ์ถœ์€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
             ProductRankScore existingScore = existing.get();
             existingScore.setMetrics(
                 score.getLikeCount(),
                 score.getSalesCount(),
                 score.getViewCount(),
                 score.getScore()
             );
-            entityManager.merge(existingScore);
             log.debug("ProductRankScore ์—…๋ฐ์ดํŠธ: productId={}", score.getProductId());

Based on learnings, ์ด ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ๋Š” ํŠธ๋žœ์žญ์…˜ ์ปจํ…์ŠคํŠธ ๋‚ด์—์„œ JPA dirty checking์„ ํ†ตํ•œ ์ž๋™ ์˜์†ํ™”๋ฅผ ์„ ํ˜ธํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.java (1)

102-109: JPA ๋ผ์ดํ”„์‚ฌ์ดํด ์ฝœ๋ฐฑ์œผ๋กœ ํƒ€์ž„์Šคํƒฌํ”„ ๊ด€๋ฆฌ ๊ถŒ์žฅ

createdAt๊ณผ updatedAt์„ ์ˆ˜๋™์œผ๋กœ ์„ค์ •ํ•˜๋Š” ๋Œ€์‹  @PrePersist์™€ @PreUpdate๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ผ๊ด€์„ฑ ์žˆ๋Š” ํƒ€์ž„์Šคํƒฌํ”„ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž JPA ์ฝœ๋ฐฑ ์‚ฌ์šฉ ์ œ์•ˆ
+    @PrePersist
+    protected void onCreate() {
+        this.createdAt = LocalDateTime.now();
+        this.updatedAt = LocalDateTime.now();
+    }
+
+    @PreUpdate
+    protected void onUpdate() {
+        this.updatedAt = LocalDateTime.now();
+    }
+
     public ProductRank(
         PeriodType periodType,
         LocalDate periodStartDate,
         Long productId,
         Integer rank,
         Long likeCount,
         Long salesCount,
         Long viewCount
     ) {
         this.periodType = periodType;
         this.periodStartDate = periodStartDate;
         this.productId = productId;
         this.rank = rank;
         this.likeCount = likeCount;
         this.salesCount = salesCount;
         this.viewCount = viewCount;
-        this.createdAt = LocalDateTime.now();
-        this.updatedAt = LocalDateTime.now();
     }

     public void updateRank(Integer rank, Long likeCount, Long salesCount, Long viewCount) {
         this.rank = rank;
         this.likeCount = likeCount;
         this.salesCount = salesCount;
         this.viewCount = viewCount;
-        this.updatedAt = LocalDateTime.now();
     }

Also applies to: 138-139, 155-155

apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.java (2)

36-38: ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ €์žฅ ์‹œ ์„ฑ๋Šฅ ๊ฐœ์„  ๊ณ ๋ ค

๊ฐœ๋ณ„ persist() ํ˜ธ์ถœ ๋Œ€์‹  ์ผ์ • ๊ฐ„๊ฒฉ์œผ๋กœ flush()์™€ clear()๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฉด ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค. TOP 100์œผ๋กœ ์ œํ•œ๋˜์–ด ํ˜„์žฌ๋Š” ๋ฌธ์ œ์—†์ง€๋งŒ, ํ–ฅํ›„ ํ™•์žฅ ์‹œ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”Ž ๋ฐฐ์น˜ ํ”Œ๋Ÿฌ์‹œ ํŒจํ„ด ์ œ์•ˆ
     public void saveRanks(ProductRank.PeriodType periodType, LocalDate periodStartDate, List<ProductRank> ranks) {
         // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ
         deleteByPeriod(periodType, periodStartDate);

         // ์ƒˆ ๋ฐ์ดํ„ฐ ์ €์žฅ
-        for (ProductRank rank : ranks) {
-            entityManager.persist(rank);
-        }
+        for (int i = 0; i < ranks.size(); i++) {
+            entityManager.persist(ranks.get(i));
+            if (i > 0 && i % 50 == 0) {
+                entityManager.flush();
+                entityManager.clear();
+            }
+        }
+        entityManager.flush();

         log.info("ProductRank ์ €์žฅ ์™„๋ฃŒ: periodType={}, periodStartDate={}, count={}",
             periodType, periodStartDate, ranks.size());
     }

68-77: getResultList()๋ฅผ ์‚ฌ์šฉํ•œ ๊ฐ„๊ฒฐํ•œ ๊ตฌํ˜„ ๊ณ ๋ ค

getSingleResult()์™€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋Œ€์‹  getResultList()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ๋” ๊ฐ„๊ฒฐํ•ด์ง‘๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฐ„๊ฒฐํ•œ ๊ตฌํ˜„ ์ œ์•ˆ
-        try {
-            ProductRank rank = entityManager.createQuery(jpql, ProductRank.class)
-                .setParameter("periodType", periodType)
-                .setParameter("periodStartDate", periodStartDate)
-                .setParameter("productId", productId)
-                .getSingleResult();
-            return Optional.of(rank);
-        } catch (jakarta.persistence.NoResultException e) {
-            return Optional.empty();
-        }
+        return entityManager.createQuery(jpql, ProductRank.class)
+            .setParameter("periodType", periodType)
+            .setParameter("periodStartDate", periodStartDate)
+            .setParameter("productId", productId)
+            .getResultList()
+            .stream()
+            .findFirst();
apps/commerce-batch/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.java (1)

46-52: Thread.sleep() ๋Œ€์‹  ๋” ์•ˆ์ •์ ์ธ ์‹œ๊ฐ„ ๊ฒ€์ฆ ๋ฐฉ๋ฒ• ๊ณ ๋ ค

Thread.sleep(1)์€ ๊ฐ„ํ—์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ์‹คํŒจ๋ฅผ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ ๋ถ€ํ•˜์— ๋”ฐ๋ผ 1ms ๋‚ด์— ๋‘ ์ž‘์—…์ด ๋™์ผํ•œ ์‹œ๊ฐ์— ์™„๋ฃŒ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Clock ์ฃผ์ž… ํŒจํ„ด์ด๋‚˜ isAfterOrEqualTo() ๊ฐ™์€ ์™„ํ™”๋œ ๊ฒ€์ฆ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”Ž ๋Œ€์•ˆ ์ œ์•ˆ
-        Thread.sleep(1); // ์‹œ๊ฐ„ ์ฐจ์ด๋ฅผ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•œ ์ž‘์€ ์ง€์—ฐ
         metrics.incrementLikeCount();

         // assert
         assertThat(metrics.getLikeCount()).isEqualTo(initialLikeCount + 1);
         assertThat(metrics.getVersion()).isEqualTo(initialVersion + 1);
-        assertThat(metrics.getUpdatedAt()).isAfter(initialUpdatedAt);
+        assertThat(metrics.getUpdatedAt()).isAfterOrEqualTo(initialUpdatedAt);
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReader.java (1)

104-106: Map.of() ์‚ฌ์šฉ์œผ๋กœ ๊ฐ„๊ฒฐํ™” ๊ฐ€๋Šฅ

๋‹จ์ผ ์—”ํŠธ๋ฆฌ์˜ ๊ฒฝ์šฐ Map.of()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ๊ฐ„๊ฒฐํ•˜๊ณ  ๋ถˆ๋ณ€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ
-        // ์ •๋ ฌ ๊ธฐ์ค€ ์„ค์ • (product_id ๊ธฐ์ค€ ์˜ค๋ฆ„์ฐจ์ˆœ)
-        Map<String, Sort.Direction> sorts = new HashMap<>();
-        sorts.put("productId", Sort.Direction.ASC);
+        // ์ •๋ ฌ ๊ธฐ์ค€ ์„ค์ • (product_id ๊ธฐ์ค€ ์˜ค๋ฆ„์ฐจ์ˆœ)
+        Map<String, Sort.Direction> sorts = Map.of("productId", Sort.Direction.ASC);
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java (1)

187-188: Reader Bean์—์„œ Processor ์ƒํƒœ ์„ค์ •์€ ๊ฒฐํ•ฉ๋„๊ฐ€ ๋†’์Œ

Reader bean ์ƒ์„ฑ ์‹œ productRankAggregationProcessor.setPeriod()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์€ ์•”์‹œ์  ์˜์กด์„ฑ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. Step ๋ฆฌ์Šค๋„ˆ๋‚˜ ๋ณ„๋„์˜ ์ดˆ๊ธฐํ™” ๋นˆ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ๋ช…์‹œ์ ์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (3)

49-49: ์™„์ „ํ•œ ํด๋ž˜์Šค๋ช…(FQCN) ๋Œ€์‹  import ๋ฌธ ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

com.loopers.domain.rank.ProductRankRepository์™€ com.loopers.domain.rank.ProductRank ํด๋ž˜์Šค๊ฐ€ ์—ฌ๋Ÿฌ ๊ณณ(๋ผ์ธ 375-378, 380, 396, 399-400, 422)์—์„œ ์™„์ „ํ•œ ํด๋ž˜์Šค๋ช…์œผ๋กœ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด ์ƒ๋‹จ์— import ๋ฌธ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

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

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

import com.loopers.domain.rank.ProductRank;
import com.loopers.domain.rank.ProductRankRepository;

๊ทธ ํ›„ ํ•ด๋‹น ํด๋ž˜์Šค๋ช…์„ ๊ฐ„๋žตํ•˜๊ฒŒ ์ˆ˜์ •:

-    private final com.loopers.domain.rank.ProductRankRepository productRankRepository;
+    private final ProductRankRepository productRankRepository;

480-487: PeriodType enum ์ค‘๋ณต ์ •์˜ ๊ฒ€ํ† .

RankingService.PeriodType๊ณผ ProductRank.PeriodType ๋‘ ๊ฐœ์˜ enum์ด ์กด์žฌํ•˜๋ฉฐ, ๋ผ์ธ 375-378์—์„œ ์ˆ˜๋™์œผ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ตฌ์กฐ๋Š” ํ–ฅํ›„ ์ƒˆ๋กœ์šด ๊ธฐ๊ฐ„ ํƒ€์ž… ์ถ”๊ฐ€ ์‹œ ๋™๊ธฐํ™” ๋ˆ„๋ฝ ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ๋ฐฉ์•ˆ์„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋„๋ฉ”์ธ enum ์žฌ์‚ฌ์šฉ: ProductRank.PeriodType์„ API ๊ณ„์ธต์—์„œ๋„ ์‚ฌ์šฉ
  • ๊ณตํ†ต enum ์ถ”์ถœ: ๋ณ„๋„ ๊ณตํ†ต ๋ชจ๋“ˆ์— enum ์ •์˜ ํ›„ ์–‘์ชฝ์—์„œ ์ฐธ์กฐ

ํ˜„์žฌ ๊ธฐ๋Šฅ ๋™์ž‘์—๋Š” ๋ฌธ์ œ ์—†์œผ๋ฏ€๋กœ ํ–ฅํ›„ ๋ฆฌํŒฉํ† ๋ง์œผ๋กœ ๊ณ ๋ คํ•˜์…”๋„ ๋ฉ๋‹ˆ๋‹ค.


380-396: ํŽ˜์ด์ง€๋„ค์ด์…˜์„ DB ์ฟผ๋ฆฌ๋กœ ์œ„์ž„ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ 100๊ฑด ์ „์ฒด๋ฅผ ์กฐํšŒํ•œ ํ›„ ๋ฉ”๋ชจ๋ฆฌ์—์„œ subList๋กœ ํŽ˜์ด์ง•ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. TOP 100 ์ œํ•œ์ด ์žˆ์–ด ์„ฑ๋Šฅ์ƒ ํฐ ๋ฌธ์ œ๋Š” ์—†์ง€๋งŒ, ProductRankRepository.findByPeriod์— offset/limit ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ์ „์†ก์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„๋„ ๋™์ž‘์— ๋ฌธ์ œ์—†์œผ๋ฏ€๋กœ ํ•„์š”์‹œ ์ตœ์ ํ™”๋กœ ๊ฒ€ํ† ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between f8db897 and d4ceccc.

๐Ÿ“’ Files selected for processing (42)
  • apps/commerce-api/build.gradle.kts
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.java
  • apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRankRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
  • apps/commerce-api/src/main/resources/application.yml
  • apps/commerce-batch/build.gradle.kts
  • apps/commerce-batch/src/main/java/com/loopers/BatchApplication.java
  • apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java
  • apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
  • apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.java
  • apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankRepository.java
  • apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScore.java
  • apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScoreRepository.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessor.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReader.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriter.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsJobConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessor.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReader.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessor.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationReader.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationWriter.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankScoreRepositoryImpl.java
  • apps/commerce-batch/src/main/resources/application.yml
  • apps/commerce-batch/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.java
  • apps/commerce-batch/src/test/java/com/loopers/domain/rank/ProductRankTest.java
  • apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessorTest.java
  • apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReaderTest.java
  • apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriterTest.java
  • apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessorTest.java
  • apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReaderTest.java
  • apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessorTest.java
  • apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriterTest.java
  • modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java
  • settings.gradle.kts
๐Ÿ’ค Files with no reviewable changes (2)
  • apps/commerce-api/src/main/resources/application.yml
  • apps/commerce-api/build.gradle.kts
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (8)
๐Ÿ““ Common learnings
Learnt from: junoade
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 0
File: :0-0
Timestamp: 2025-12-02T08:12:06.383Z
Learning: ProductQueryService์—์„œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ์‹œ Redis ์บ์‹œ๋ฅผ ์ ์šฉํ–ˆ์œผ๋ฉฐ, ์บ์‹œ ํ‚ค๋Š” brandId, sortType, pageNumber, pageSize์˜ ์กฐํ•ฉ์œผ๋กœ ๊ตฌ์„ฑ๋˜๊ณ  TTL์€ 5๋ถ„์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ๋‹ค.
๐Ÿ“š Learning: 2025-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/domain/rank/ProductRank.java
  • apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScore.java
  • apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.java
  • apps/commerce-batch/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.java
  • apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java
๐Ÿ“š Learning: 2025-11-12T13:04:50.782Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 72
File: core/domain/src/main/java/com/loopers/core/domain/productlike/vo/ProductLikeId.java:5-7
Timestamp: 2025-11-12T13:04:50.782Z
Learning: In the com.loopers codebase, domain entity ID value objects (e.g., ProductLikeId, OrderItemId, ProductId, PaymentId, OrderId, BrandId) are system-generated identifiers and do not require pattern validation (regex, length checks). They are implemented as simple records with a String value and an empty() factory method returning null for unsaved entities. This differs from UserIdentifier, which is a user-supplied login ID that requires format validation. Domain IDs should not be flagged for missing validation logic in the create() method.
<!-- [add_learning]
UserIdentifier์™€ ๊ฐ™์€ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ID์™€ ProductLikeId, OrderItemId ๋“ฑ์˜ ๋„๋ฉ”์ธ ID๋Š” ๊ฒ€์ฆ ํŒจํ„ด์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. UserIdentifier๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•˜๋Š” ๋กœ๊ทธ์ธ ID๋กœ์„œ ์ •๊ทœ์‹, ๊ธธ์ด ๋“ฑ์˜ ํ˜•์‹ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜์ง€๋งŒ, ๋„๋ฉ”์ธ ID๋Š” ์‹œ์Šคํ…œ์—์„œ ์ƒ์„ฑํ•˜๋Š” ์‹๋ณ„์ž(UUID, DB ์ƒ์„ฑ ID)์ด๋ฏ€๋กœ ํŒจํ„ด ๊ฒ€์ฆ์ด ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋„๋ฉ”์ธ ID VO๋Š” ๋‹จ์ˆœํ•œ record์™€ empty() ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

Applied to files:

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

Applied to files:

  • apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java
  • apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScoreRepository.java
๐Ÿ“š Learning: 2025-12-19T09:30:12.459Z
Learnt from: HongChangMo
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 198
File: apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventListener.java:0-0
Timestamp: 2025-12-19T09:30:12.459Z
Learning: In the loopers-spring-java-template repository's commerce-api module, when entities are managed within a transactional context (e.g., Transactional methods), prefer relying on JPA dirty checking for automatic persistence rather than explicit save() calls. Both Payment and Order entities in PaymentEventListener use this pattern, with state changes automatically flushed on transaction commit.

Applied to files:

  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankScoreRepositoryImpl.java
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.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/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java
  • apps/commerce-batch/src/main/resources/application.yml
๐Ÿ“š 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/domain/metrics/ProductMetrics.java
๐Ÿ“š Learning: 2025-12-19T03:23:51.674Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 191
File: modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java:24-34
Timestamp: 2025-12-19T03:23:51.674Z
Learning: Maintain the use of static initialization blocks in test fixtures for testcontainers configurations (e.g., MySQL, Redis, Kafka) to ensure consistency and predictable initialization across the loopers-spring-java-template project. Apply this pattern to all test-fixture Java files under src/testFixtures/java to preserve uniform lifecycle behavior, unless there's a compelling, well-justified reason to diverge.

Applied to files:

  • modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java
๐Ÿงฌ Code graph analysis (10)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriter.java (2)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessor.java (1)
  • Slf4j (25-44)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
  • Component (30-72)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessor.java (3)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessor.java (1)
  • Slf4j (30-86)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationWriter.java (1)
  • Slf4j (33-81)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java (1)
  • Slf4j (65-256)
apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.java (1)
apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.java (1)
  • Entity (35-165)
apps/commerce-batch/src/main/java/com/loopers/BatchApplication.java (1)
apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt (1)
  • main (22-24)
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReaderTest.java (1)
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReaderTest.java (1)
  • ExtendWith (21-151)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt (1)
  • result (7-19)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java (5)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessor.java (1)
  • Slf4j (27-73)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReader.java (1)
  • Slf4j (36-122)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessor.java (1)
  • Slf4j (30-86)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationWriter.java (1)
  • Slf4j (33-81)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.java (1)
  • Slf4j (36-169)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReader.java (1)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
  • Component (30-72)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.java (1)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankScoreRepositoryImpl.java (1)
  • Slf4j (20-99)
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessorTest.java (4)
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReaderTest.java (1)
  • ExtendWith (23-133)
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReaderTest.java (1)
  • ExtendWith (21-151)
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriterTest.java (1)
  • ExtendWith (27-250)
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt (1)
  • result (7-19)
๐Ÿ”‡ Additional comments (40)
modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java (1)

45-49: ํ…Œ์ด๋ธ” ์กด์žฌ ํ™•์ธ ์ฟผ๋ฆฌ๋Š” ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

information_schema.tables ์กฐํšŒ ์‹œ ํŒŒ๋ผ๋ฏธํ„ฐํ™”๋œ ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ SQL ์ธ์ ์…˜์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, DATABASE() ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ˜„์žฌ ์Šคํ‚ค๋งˆ ๋‚ด์—์„œ๋งŒ ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ฒƒ์ด ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ์ž‘์—…์œผ๋กœ ์ธํ•œ ์ถ”๊ฐ€ ํ…Œ์ด๋ธ”(mv_product_rank ๋“ฑ)์ด ์ผ๋ถ€ ํ…Œ์ŠคํŠธ ์ปจํ…์ŠคํŠธ์—์„œ๋งŒ ์กด์žฌํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์—์„œ ์ด ๋ฐฉ์–ด์  ์ ‘๊ทผ์€ ํ•ฉ๋ฆฌ์ ์ž…๋‹ˆ๋‹ค.

settings.gradle.kts (1)

5-5: ๋ชจ๋“ˆ ๋“ฑ๋ก์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ˆ˜ํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด commerce-batch ๋ชจ๋“ˆ์ด ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ์— ์ •ํ™•ํ•˜๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

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

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

์›น ์„œ๋ฒ„ ๋น„ํ™œ์„ฑํ™”(web-application-type: none)์™€ ํ•„์š”ํ•œ ์„ค์ • ํŒŒ์ผ ์ž„ํฌํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


14-18: ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์Šคํ‚ค๋งˆ ์ž๋™ ์ดˆ๊ธฐํ™” ์„ค์ •์„ ๊ฒ€ํ† ํ•˜์„ธ์š”.

initialize-schema: always ์„ค์ •์ด ๋ชจ๋“  ํ™˜๊ฒฝ์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ด ์„ค์ •์ด ์˜ˆ๊ธฐ์น˜ ์•Š์€ ์Šคํ‚ค๋งˆ ์žฌ์ƒ์„ฑ์„ ์œ ๋ฐœํ•˜๊ฑฐ๋‚˜ ๊ธฐ์กด ๋ฐฐ์น˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์†์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ”„๋กœํ•„๋ณ„๋กœ ์„ค์ •์„ ๋ถ„๋ฆฌํ•˜์—ฌ local/test ํ™˜๊ฒฝ์—์„œ๋งŒ always๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , dev/qa/prd ํ™˜๊ฒฝ์—์„œ๋Š” never๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ํ”„๋กœํ•„๋ณ„ ์„ค์ • ๋ถ„๋ฆฌ ์˜ˆ์‹œ
-  batch:
-    jdbc:
-      initialize-schema: always # Spring Batch ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์ž๋™ ์ƒ์„ฑ
-    job:
-      enabled: false # ๋ช…๋ น์ค„์—์„œ ์ˆ˜๋™ ์‹คํ–‰ํ•˜๋ฏ€๋กœ ์ž๋™ ์‹คํ–‰ ๋น„ํ™œ์„ฑํ™”
+  batch:
+    job:
+      enabled: false # ๋ช…๋ น์ค„์—์„œ ์ˆ˜๋™ ์‹คํ–‰ํ•˜๋ฏ€๋กœ ์ž๋™ ์‹คํ–‰ ๋น„ํ™œ์„ฑํ™”

 ---
 spring:
   config:
     activate:
       on-profile: local, test
+  batch:
+    jdbc:
+      initialize-schema: always

 ---
 spring:
   config:
     activate:
       on-profile: dev
+  batch:
+    jdbc:
+      initialize-schema: never

 ---
 spring:
   config:
     activate:
       on-profile: qa
+  batch:
+    jdbc:
+      initialize-schema: never

 ---
 spring:
   config:
     activate:
       on-profile: prd
+  batch:
+    jdbc:
+      initialize-schema: never
โ›” Skipped due to learnings
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.
apps/commerce-batch/build.gradle.kts (1)

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

๋ฐฐ์น˜ ๋ชจ๋“ˆ์— ํ•„์š”ํ•œ Spring Batch, QueryDSL, ํ…Œ์ŠคํŠธ ํ”ฝ์Šค์ฒ˜ ๋“ฑ์˜ ์˜์กด์„ฑ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

33-67: ๋„๋ฉ”์ธ ์ €์žฅ์†Œ ๋ฉ”์„œ๋“œ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

save, findByProductId, findByUpdatedAtBetween ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์š”๊ตฌ์‚ฌํ•ญ์„ ์ž˜ ๋ฐ˜์˜ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, Spring Data์˜ Page/Pageable ์‚ฌ์šฉ๋„ ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ์•„ํ‚คํ…์ฒ˜ ๊ฒฐ์ •์— ๋ถ€ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

Based on learnings, Spring Data์˜ Page/Pageable ํƒ€์ž… ์‚ฌ์šฉ์€ ์ด ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ ํ—ˆ์šฉ๋œ ์•„ํ‚คํ…์ฒ˜ ๊ฒฐ์ •์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRankRepository.java (1)

13-38: ์ธํ„ฐํŽ˜์ด์Šค๋Š” ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค - ๋ชจ๋“ˆ๋ณ„ ์ฑ…์ž„ ๋ถ„๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

๊ฒ€์ฆ ๊ฒฐ๊ณผ, ์ด ์ธํ„ฐํŽ˜์ด์Šค(apps/commerce-api)๋Š” ์˜๋„์ ์œผ๋กœ ์กฐํšŒ ๋ฉ”์„œ๋“œ๋งŒ ํฌํ•จํ•˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ €์žฅ ๋ฐ ์‚ญ์ œ ๋ฉ”์„œ๋“œ(saveRanks, deleteByPeriod)๋Š” ๋ฐฐ์น˜ ๋ชจ๋“ˆ(apps/commerce-batch)์˜ ๋ณ„๋„ ์ธํ„ฐํŽ˜์ด์Šค์—๋งŒ ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉฐ, ์‹ค์ œ๋กœ ๋ฐฐ์น˜ ์ž‘์—…์—์„œ๋งŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” DIP ์œ„๋ฐ˜์ด ์•„๋‹ˆ๋ผ ๋ชจ๋“ˆ๋ณ„ ์ฑ…์ž„ ๋ถ„๋ฆฌ์˜ ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ์ž…๋‹ˆ๋‹ค:

  • API ๋ชจ๋“ˆ: ์ฝ๊ธฐ ์ „์šฉ (findByPeriod, findByPeriodAndProductId)
  • ๋ฐฐ์น˜ ๋ชจ๋“ˆ: ์ฝ๊ธฐ + ์“ฐ๊ธฐ (๋ชจ๋“  4๊ฐœ ๋ฉ”์„œ๋“œ ํฌํ•จ)

AI ์š”์•ฝ์ด ๋‘ ๋ชจ๋“ˆ์˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ˜ผ๋™ํ–ˆ์œผ๋‚˜, ์ฝ”๋“œ ์„ค๊ณ„ ์ž์ฒด๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (1)

125-132: ๋ฒ„์ „ ๊ธฐ๋ฐ˜ ์—…๋ฐ์ดํŠธ ์ฒดํฌ ๋กœ์ง ํ™•์ธ

์ด๋ฒคํŠธ ๋ฒ„์ „ ๋น„๊ต ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. null ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•œ ํ•˜์œ„ ํ˜ธํ™˜์„ฑ๋„ ์ข‹์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ : version ํ•„๋“œ๊ฐ€ JPA์˜ @Version(๋‚™๊ด€์  ๋ฝ)๊ณผ ํ˜ผ๋™๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํ•„๋“œ๋Š” ์ด๋ฒคํŠธ ๋ฒ„์ „ ๊ด€๋ฆฌ์šฉ์ด๋ฏ€๋กœ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„๋ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessorTest.java (1)

14-86: Pass-through ํ”„๋กœ์„ธ์„œ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€

Pass-through ๋กœ์ง์„ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. PR ๋ชฉํ‘œ์— ๋ช…์‹œ๋œ ๋Œ€๋กœ ์ „์ฒด Job ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๊ฐ€ ์•„๋‹Œ ํ•ต์‹ฌ ๋กœ์ง์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์— ์ง‘์ค‘ํ•˜๋Š” ์ „๋žต๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ํ”„๋กœ์„ธ์„œ์˜ ์˜ˆ์ƒ ๋™์ž‘์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

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

56-64: ๊ธฐ๊ฐ„ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ณธ๊ฐ’ ์ฒ˜๋ฆฌ

@RequestParam์˜ defaultValue = "DAILY"์™€ parsePeriodType์˜ DAILY ๊ธฐ๋ณธ๊ฐ’์ด ์ค‘๋ณต๋˜์ง€๋งŒ ๋ฐฉ์–ด์ ์ด๊ณ  ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„์€ ์ž˜๋ชป๋œ period ๊ฐ’์„ ์กฐ์šฉํžˆ DAILY๋กœ ํด๋ฐฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์‚ฌ์šฉ์ž ์นœํ™”์ ์ด์ง€๋งŒ, API ์‚ฌ์šฉ์ž๊ฐ€ ์˜คํƒ€๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ธฐ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ์˜ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐฉ์นจ์— ๋”ฐ๋ผ ๋‹ค์Œ์„ ๊ณ ๋ คํ•˜์„ธ์š”:

  • ํ˜„์žฌ ๋ฐฉ์‹ ์œ ์ง€: ๊ด€๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋กœ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 
  • ๋˜๋Š” 400 ์—๋Ÿฌ ๋ฐ˜ํ™˜: ๋ช…์‹œ์  ํ”ผ๋“œ๋ฐฑ์œผ๋กœ API ๊ณ„์•ฝ ๊ฐ•ํ™”

ํ˜„์žฌ API ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐฉ์นจ(๊ด€๋Œ€ํ•œ ๊ธฐ๋ณธ๊ฐ’ vs ๋ช…์‹œ์  ์—๋Ÿฌ)์„ ํ™•์ธํ•˜์„ธ์š”.


112-123: ๊ธฐ๊ฐ„ ํƒ€์ž… ํŒŒ์‹ฑ ๋กœ์ง ๊ตฌํ˜„

parsePeriodType ๋ฉ”์„œ๋“œ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

  • null/blank ์ฒ˜๋ฆฌ
  • ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†๋Š” ํŒŒ์‹ฑ (toUpperCase())
  • ์•ˆ์ „ํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

์ฝ”๋“œ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ๊ฒฌ๊ณ ํ•ฉ๋‹ˆ๋‹ค.

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

25-32: ๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ง„์ž…์  ๊ตฌํ˜„

๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

  • @SpringBootApplication์˜ scanBasePackages๊ฐ€ ์ ์ ˆํžˆ ์„ค์ •๋จ
  • @EnableJpaRepositories์™€ @EntityScan์ด ์ธํ”„๋ผ ๋ฐ ๋„๋ฉ”์ธ ํŒจํ‚ค์ง€๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ง€์ •
  • SpringApplication.exit ํŒจํ„ด์ด ๋ฐฐ์น˜ ์ž‘์—… ์™„๋ฃŒ ํ›„ ์ ์ ˆํ•œ ์ข…๋ฃŒ ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๋ณด์žฅ

Javadoc์˜ ์‹คํ–‰ ์˜ˆ์‹œ๋„ ๋ช…ํ™•ํ•˜๊ณ  ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

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

28-38: ๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์กฐํšŒ ์ฟผ๋ฆฌ ๊ตฌํ˜„

JPQL ์ฟผ๋ฆฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

  • ์ ์ ˆํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ
  • ORDER BY pr.rank ASC๋กœ ์ˆœ์œ„ ์ •๋ ฌ
  • setMaxResults๋กœ ๊ฒฐ๊ณผ ์ œํ•œ

PR ๋ชฉํ‘œ์— ๋ช…์‹œ๋œ ๋ณตํ•ฉ ์ธ๋ฑ์Šค(period_type, period_start_date, rank)๊ฐ€ ์žˆ์œผ๋ฉด ์ด ์ฟผ๋ฆฌ์˜ ์„ฑ๋Šฅ์ด ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค.


40-61: ํŠน์ • ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ ๊ตฌํ˜„

๊ฐœ๋ณ„ ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

  • NoResultException ์˜ˆ์™ธ๋ฅผ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌ
  • Optional ํŒจํ„ด์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‚ฌ์šฉ
  • ๋ช…ํ™•ํ•œ ์ฟผ๋ฆฌ ๋กœ์ง
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessorTest.java (1)

1-120: LGTM! ํ…Œ์ŠคํŠธ ๊ตฌ์„ฑ์ด ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

ProductRankAggregationProcessor์˜ ๊ธฐ๊ฐ„ ์„ค์ • ๋กœ์ง์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ์ฒด๊ณ„์ ์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ฃผ๊ฐ„/์›”๊ฐ„ ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ, ๋‹ค์–‘ํ•œ ๋‚ ์งœ ์ž…๋ ฅ์— ๋Œ€ํ•œ ๊ฒฝ๊ณ„ ์ผ€์ด์Šค, ๊ทธ๋ฆฌ๊ณ  ์—ฌ๋Ÿฌ ๋ฒˆ ์„ค์ • ์‹œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ž˜ ๊ฒ€์ฆ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriterTest.java (1)

100-116: LGTM! ํ…Œ์ŠคํŠธ ํ—ฌํผ ๋ฉ”์„œ๋“œ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

createProductMetricsList ํ—ฌํผ ๋ฉ”์„œ๋“œ๊ฐ€ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ์„ ํšจ๊ณผ์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessor.java (1)

25-44: LGTM! ํ™•์žฅ ํฌ์ธํŠธ๋กœ์„œ์˜ ์—ญํ• ์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ pass-through ๊ตฌํ˜„์ด์ง€๋งŒ, Javadoc์—์„œ ํ–ฅํ›„ ์ง‘๊ณ„/๋ณ€ํ™˜/ํ•„ํ„ฐ๋ง ๋กœ์ง ์ถ”๊ฐ€๋ฅผ ์œ„ํ•œ ํ™•์žฅ ํฌ์ธํŠธ์ž„์„ ์ž˜ ์„ค๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

@Slf4j ์–ด๋…ธํ…Œ์ด์…˜์ด ์„ ์–ธ๋˜์–ด ์žˆ์ง€๋งŒ ํ˜„์žฌ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ–ฅํ›„ ๋กœ์ง ์ถ”๊ฐ€ ์‹œ ์‚ฌ์šฉ๋  ๊ฒƒ์œผ๋กœ ๋ณด์ด๋ฏ€๋กœ ์œ ์ง€ํ•ด๋„ ๋ฌด๋ฐฉํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/test/java/com/loopers/domain/rank/ProductRankTest.java (1)

182-233: LGTM! PeriodType enum๊ณผ ๋žญํ‚น ๋ฒ”์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

PeriodType enum ๊ฒ€์ฆ๊ณผ TOP 100/1์œ„ ๋žญํ‚น ๊ฒฝ๊ณ„ ํ…Œ์ŠคํŠธ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessorTest.java (2)

92-129: ThreadLocal ์ •๋ฆฌ ๋™์ž‘ ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ์ฐธ๊ณ  ์‚ฌํ•ญ

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

์‹ค์ œ ๋ฐฐ์น˜ ์‹คํ–‰ ์‹œ์—๋Š” 100๊ฐœ ์ดํ›„ ํ•ญ๋ชฉ์ด ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ์ด ํ…Œ์ŠคํŠธ๊ฐ€ ๊ตฌํ˜„ ๋ณ€๊ฒฝ ์‹œ ๊นจ์งˆ ์ˆ˜ ์žˆ์Œ์„ ์ธ์ง€ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค. ์ฃผ์„์ด ์ด๋ฏธ ์ด ์ ์„ ์ž˜ ์„ค๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


255-261: LGTM! ํ…Œ์ŠคํŠธ ํ—ฌํผ ๋ฉ”์„œ๋“œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

createProductRankScore ํ—ฌํผ๊ฐ€ ์ ์ˆ˜ ๊ณ„์‚ฐ ๊ณต์‹(๊ฐ€์ค‘์น˜ 0.3, 0.5, 0.2)์„ ProductRankScoreAggregationWriterTest์˜ ๊ณต์‹๊ณผ ์ผ์น˜ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriter.java (1)

41-56: ํ˜„์žฌ ์ƒํƒœ๋Š” ์˜๋„์ ์ธ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

ProductMetricsItemWriter๊ฐ€ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์€ ํ™•์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ProductMetricsJobConfig์˜ Javadoc์—์„œ "Writer: ์ง‘๊ณ„ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ (ํ˜„์žฌ๋Š” ๋กœ๊น…, ํ–ฅํ›„ MV ์ €์žฅ)"์ด๋ผ๊ณ  ๋ช…์‹œ๋œ ๋Œ€๋กœ ์ด๋Š” ์˜๋„์ ์ธ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ ์œ ์‹ค ์šฐ๋ ค๋Š” ์—†์Šต๋‹ˆ๋‹ค. ProductRankScoreAggregationWriter๋Š” ๋ณ„๊ฐœ์˜ Step์—์„œ ProductMetrics๋ฅผ ์ฝ์–ด ProductRankScore๋ฅผ ๊ณ„์‚ฐํ•˜์—ฌ productRankScoreRepository.saveAll()๋กœ ์‹ค์ œ DB์— ์ €์žฅํ•˜๋ฏ€๋กœ, ์—ญํ•  ๊ตฌ๋ถ„์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค:

  • ProductMetricsItemWriter: ProductMetrics ๋กœ๊น… (ํ˜„์žฌ ์ƒํƒœ), ํ–ฅํ›„ Materialized View ์ €์žฅ ์˜ˆ์ •
  • ProductRankScoreAggregationWriter: ProductMetrics๋ฅผ ProductRankScore๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์‹ค์ œ DB ์ €์žฅ (์ด๋ฏธ ๊ตฌํ˜„)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessor.java (1)

27-53: ๋™์‹œ Job ์‹คํ–‰ ์‹œ ์Šค๋ ˆ๋“œ ์•ˆ์ „์„ฑ ๊ฒ€ํ† ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์ด ํด๋ž˜์Šค๋Š” ์‹ฑ๊ธ€ํ†ค @Component์ด์ง€๋งŒ ๊ฐ€๋ณ€ ์ธ์Šคํ„ด์Šค ํ•„๋“œ(periodType, periodStartDate)๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋™์ผํ•œ Job์ด ๋‹ค๋ฅธ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋™์‹œ์— ์‹คํ–‰๋  ๊ฒฝ์šฐ ๊ฒฝ์Ÿ ์ƒํƒœ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌ์กฐ์—์„œ๋Š” productRankReader๊ฐ€ @StepScope์ด์ง€๋งŒ ์ด Processor๋Š” ์‹ฑ๊ธ€ํ†ค์ด๋ฏ€๋กœ, ๋™์‹œ ์‹คํ–‰ ์‹œ ํ•œ Job์˜ setPeriod() ํ˜ธ์ถœ์ด ๋‹ค๋ฅธ Job์˜ ๊ธฐ๊ฐ„ ์ •๋ณด๋ฅผ ๋ฎ์–ด์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋™์‹œ ๋ฐฐ์น˜ Job ์‹คํ–‰์ด ์˜ˆ์ƒ๋˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ํ˜„์žฌ ๊ตฌํ˜„์œผ๋กœ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ, ๋™์‹œ ์‹คํ–‰์ด ํ•„์š”ํ•˜๋‹ค๋ฉด @StepScope๋กœ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ThreadLocal์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”.

apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.java (1)

35-118: LGTM!

์—”ํ‹ฐํ‹ฐ ๊ตฌ์กฐ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. API ๋ชจ๋“ˆ์—์„œ๋Š” ์กฐํšŒ ์ „์šฉ์œผ๋กœ ์‚ฌ์šฉ๋˜๋ฏ€๋กœ ์ƒ์„ฑ์ž ์—†์ด @NoArgsConstructor(access = AccessLevel.PROTECTED)๋งŒ ์žˆ๋Š” ๊ฒƒ์ด ์˜๋„๋œ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค. ์ธ๋ฑ์Šค ์ „๋žต๋„ ๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์กฐํšŒ์™€ ํŠน์ • ์ƒํ’ˆ ์กฐํšŒ์— ์ตœ์ ํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriterTest.java (3)

36-93: LGTM!

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


95-159: LGTM!

์ ์ˆ˜ ๊ฐ€์ค‘์น˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ์™€ ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ˆ„์  ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€์ค‘์น˜ ๊ณต์‹(like * 0.3 + sales * 0.5 + view * 0.2)์ด ๋ช…ํ™•ํ•˜๊ฒŒ ๊ฒ€์ฆ๋ฉ๋‹ˆ๋‹ค.


161-249: LGTM!

๋นˆ Chunk ์ฒ˜๋ฆฌ, ๋‹ค์ค‘ product_id ์ฒ˜๋ฆฌ, ์ƒˆ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ๊ฐ€ edge case๋ฅผ ์ž˜ ์ปค๋ฒ„ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReader.java (1)

97-109: ๋‚ ์งœ ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์˜ค๋Š˜ ๋‚ ์งœ๋กœ ๋Œ€์ฒดํ•˜๋Š” ๋™์ž‘์„ ์žฌ๊ณ ํ•ด์ฃผ์„ธ์š”.

์ž˜๋ชป๋œ ๋‚ ์งœ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์ „๋‹ฌ๋˜๋ฉด ๊ฒฝ๊ณ ๋งŒ ๋กœ๊น…ํ•˜๊ณ  ์˜ค๋Š˜ ๋‚ ์งœ๋กœ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์Šค์ผ€์ค„๋ง๋œ ๋ฐฐ์น˜ ์ž‘์—…์—์„œ ์ด๋Š” ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜๋„์ ์ธ ์„ค๊ณ„๋ผ๋ฉด ํ˜„์žฌ ์ƒํƒœ๋กœ ์œ ์ง€ํ•ด๋„ ๋˜์ง€๋งŒ, ์—„๊ฒฉํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”:

private LocalDate parseDate(String dateStr) {
    if (dateStr == null || dateStr.isEmpty()) {
        throw new IllegalArgumentException("๋‚ ์งœ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.");
    }
    try {
        return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyyMMdd"));
    } catch (DateTimeParseException e) {
        throw new IllegalArgumentException("์ž˜๋ชป๋œ ๋‚ ์งœ ํ˜•์‹: " + dateStr, e);
    }
}
apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScore.java (1)

124-138: LGTM!

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

apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankRepository.java (1)

7-58: LGTM!

Repository ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์กฐํšŒ, ํŠน์ • ์ƒํ’ˆ ์กฐํšŒ, ์ €์žฅ/์‚ญ์ œ ๋ฉ”์„œ๋“œ๊ฐ€ Materialized View ํŒจํ„ด์— ์ ํ•ฉํ•˜๊ฒŒ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Javadoc๋„ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScoreRepository.java (1)

12-67: LGTM!

์ธํ„ฐํŽ˜์ด์Šค ์„ค๊ณ„๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ , UPSERT ๋™์ž‘ ๋ฐ ์šฉ๋„๊ฐ€ Javadoc์— ์ž˜ ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankScoreRepositoryImpl.java (1)

59-71: NoResultException ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์ ์ ˆ

์˜ˆ์™ธ ๊ธฐ๋ฐ˜ Optional ์ฒ˜๋ฆฌ๊ฐ€ ์ •ํ™•ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.java (1)

35-42: ์ธ๋ฑ์Šค ์„ค๊ณ„ ์ ์ ˆ

๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์กฐํšŒ ๋ฐ ํŠน์ • ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ ์ฟผ๋ฆฌ ํŒจํ„ด์— ๋งž๋Š” ๋ณตํ•ฉ ์ธ๋ฑ์Šค๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)

30-72: LGTM!

JPA ๋ ˆํฌ์ง€ํ† ๋ฆฌ์— ๋Œ€ํ•œ ์œ„์ž„ ํŒจํ„ด์ด ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. getJpaRepository()๋Š” Spring Batch์˜ RepositoryItemReader API ์š”๊ตฌ์‚ฌํ•ญ์„ ์ถฉ์กฑํ•˜๊ธฐ ์œ„ํ•œ ์ ์ ˆํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

apps/commerce-batch/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.java (1)

162-215: shouldUpdate ๋กœ์ง ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ์šฐ์ˆ˜

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

apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsJobConfig.java (2)

74-109: Job ๋ฐ Step ๊ตฌ์„ฑ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค

Chunk ํฌ๊ธฐ 100, StepScope Reader ํ™œ์šฉ, ๋ช…ํ™•ํ•œ ๋ฌธ์„œํ™” ๋“ฑ Spring Batch ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


120-126: ๋ฌธ์ œ ์—†์Œ - null ํŒŒ๋ผ๋ฏธํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ์ด๋ฏธ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค

ProductMetricsItemReader.createReader() ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์˜ parseDate() ๋ฉ”์„œ๋“œ(๋ผ์ธ 97-109)์—์„œ ์ด๋ฏธ null ๋ฐ ๋นˆ ๋ฌธ์ž์—ด์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. null์ธ ๊ฒฝ์šฐ ์˜ค๋Š˜ ๋‚ ์งœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ์—๋„ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ProductRankJobConfig์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํŒจํ„ด๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReader.java (1)

52-65: ์ฃผ๊ฐ„ ๋‚ ์งœ ๋ฒ”์œ„ ๊ณ„์‚ฐ ์ •ํ™•

์›”์š”์ผ๋ถ€ํ„ฐ ๋‹ค์Œ ์ฃผ ์›”์š”์ผ 00:00:00๊นŒ์ง€์˜ ๋ฒ”์œ„๊ฐ€ ์ •ํ™•ํ•˜๊ฒŒ ๊ณ„์‚ฐ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. exclusive end date ํŒจํ„ด์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java (1)

93-102: 2-Step Job ๊ตฌ์กฐ ์ ์ ˆ

Step 1์—์„œ ์ ์ˆ˜ ์ง‘๊ณ„ ํ›„ Step 2์—์„œ ๋žญํ‚น ๊ณ„์‚ฐํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. start().next() ์ฒด์ธ์œผ๋กœ ์ˆœ์ฐจ ์‹คํ–‰์ด ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค.

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

79-87: LGTM!

๊ธฐ๊ฐ„ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ , ๊ธฐ์กด Redis ๊ธฐ๋ฐ˜ ์ผ๊ฐ„ ๋žญํ‚น๊ณผ ์ƒˆ๋กœ์šด Materialized View ๊ธฐ๋ฐ˜ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น์„ ๊น”๋”ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.


474-478: ๋ฐฐ์น˜ ์ž‘์—…๊ณผ์˜ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง์€ ์ด๋ฏธ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

API์˜ calculateScore ๋ฉ”์„œ๋“œ๋Š” ๋ฐฐ์น˜ ์ž‘์—…์˜ ProductRankScoreAggregationWriter์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•œ ๊ฐ€์ค‘์น˜(์ข‹์•„์š” 0.3, ํŒ๋งค๋Ÿ‰ 0.5, ์กฐํšŒ์ˆ˜ 0.2)๋ฅผ ์ ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์œ ์ผํ•œ ์ฐจ์ด๋Š” API์—์„œ null ๊ฐ’์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ผํ•ญ ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ธ๋ฐ, ์ด๋Š” Materialized View ์กฐํšŒ ๊ฒฐ๊ณผ์— null ๊ฐ’์ด ์˜ฌ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

Comment on lines +420 to +452
// ๋žญํ‚น ํ•ญ๋ชฉ ์ƒ์„ฑ
List<RankingItem> rankingItems = new ArrayList<>();
for (com.loopers.domain.rank.ProductRank rank : pagedRanks) {
Long productId = rank.getProductId();
Product product = productMap.get(productId);

if (product == null) {
log.warn("๋žญํ‚น์— ํฌํ•จ๋œ ์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: productId={}", productId);
continue;
}

Brand brand = brandMap.get(product.getBrandId());
if (brand == null) {
log.warn("์ƒํ’ˆ์˜ ๋ธŒ๋žœ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: productId={}, brandId={}",
productId, product.getBrandId());
continue;
}

ProductDetail productDetail = ProductDetail.from(
product,
brand.getName(),
rank.getLikeCount()
);

// ์ข…ํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ (Materialized View์—๋Š” ์ €์žฅ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๊ณ„์‚ฐ)
double score = calculateScore(rank.getLikeCount(), rank.getSalesCount(), rank.getViewCount());

rankingItems.add(new RankingItem(
rank.getRank().longValue(),
score,
productDetail
));
}
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

์ƒํ’ˆ/๋ธŒ๋žœ๋“œ ๋ˆ„๋ฝ ์‹œ ์ˆœ์œ„ ๊ฐ„๊ฒฉ(gap) ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ ํ™•์ธ ํ•„์š”.

์ƒํ’ˆ์ด๋‚˜ ๋ธŒ๋žœ๋“œ๊ฐ€ ์กฐํšŒ๋˜์ง€ ์•Š์„ ๊ฒฝ์šฐ continue๋กœ ๊ฑด๋„ˆ๋›ฐ์ง€๋งŒ, ์‘๋‹ต์—์„œ๋Š” DB์— ์ €์žฅ๋œ rank ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด API ์‘๋‹ต์—์„œ ์ˆœ์œ„๊ฐ€ 1, 2, 5, 6๊ณผ ๊ฐ™์ด ๋ถˆ์—ฐ์†์ ์œผ๋กœ ๋‚˜ํƒ€๋‚  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜๋„๋œ ๋™์ž‘์ด๋ผ๋ฉด ๋ฌด์‹œํ•ด๋„ ๋˜์ง€๋งŒ, ์—ฐ์†์ ์ธ ์ˆœ์œ„๋ฅผ ๊ธฐ๋Œ€ํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด์Šˆ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‘ ๊ฐ€์ง€ ์ ‘๊ทผ ๋ฐฉ์‹์„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ํ˜„์žฌ ๋ฐฉ์‹ ์œ ์ง€: ์›๋ž˜ ์ˆœ์œ„ ๋ณด์กด (๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ์šฐ์„ )
  2. ์ˆœ์œ„ ์žฌ๊ณ„์‚ฐ: ๋ˆ„๋ฝ๋œ ํ•ญ๋ชฉ ์ œ์™ธ ํ›„ ์—ฐ์† ์ˆœ์œ„ ๋ถ€์—ฌ (UX ์šฐ์„ )
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java
around lines 420 to 452, the loop skips missing products/brands but keeps the
original DB rank value, causing gaps in returned ranks; update the construction
so that after filtering out missing items you assign continuous ranks to
rankingItems (e.g., maintain a local counter starting at 1 and increment per
added item) instead of using rank.getRank(), while preserving score calculation
and product details; optionally make this behavior conditional via a flag/config
if you need to support preserving original DB ranks in some cases.

Comment on lines +17 to +23
* <b>Materialized View ์„ค๊ณ„:</b>
* <ul>
* <li>์ฃผ๊ฐ„ ๋žญํ‚น: `mv_product_rank_weekly` (period_type = WEEKLY)</li>
* <li>์›”๊ฐ„ ๋žญํ‚น: `mv_product_rank_monthly` (period_type = MONTHLY)</li>
* <li>TOP 100๋งŒ ์ €์žฅํ•˜์—ฌ ์กฐํšŒ ์„ฑ๋Šฅ ์ตœ์ ํ™”</li>
* </ul>
* </p>
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

Javadoc๊ณผ ์‹ค์ œ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Javadoc์—์„œ๋Š” mv_product_rank_weekly, mv_product_rank_monthly ๋‘ ๊ฐœ์˜ ํ…Œ์ด๋ธ”์„ ์–ธ๊ธ‰ํ•˜๊ณ  ์žˆ์ง€๋งŒ, ์‹ค์ œ ๊ตฌํ˜„์€ period_type ์ปฌ๋Ÿผ์„ ๊ฐ€์ง„ ๋‹จ์ผ ํ…Œ์ด๋ธ” mv_product_rank๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
  * <b>Materialized View ์„ค๊ณ„:</b>
  * <ul>
- *   <li>์ฃผ๊ฐ„ ๋žญํ‚น: `mv_product_rank_weekly` (period_type = WEEKLY)</li>
- *   <li>์›”๊ฐ„ ๋žญํ‚น: `mv_product_rank_monthly` (period_type = MONTHLY)</li>
+ *   <li>์ฃผ๊ฐ„ ๋žญํ‚น: period_type = WEEKLY</li>
+ *   <li>์›”๊ฐ„ ๋žญํ‚น: period_type = MONTHLY</li>
  *   <li>TOP 100๋งŒ ์ €์žฅํ•˜์—ฌ ์กฐํšŒ ์„ฑ๋Šฅ ์ตœ์ ํ™”</li>
  * </ul>
๐Ÿค– Prompt for AI Agents
apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.java lines
17-23: the Javadoc incorrectly describes two separate materialized views
(`mv_product_rank_weekly`, `mv_product_rank_monthly`) while the implementation
uses a single table `mv_product_rank` with a `period_type` column; update the
Javadoc to describe the single materialized view `mv_product_rank`, explain that
`period_type` distinguishes WEEKLY vs MONTHLY, and retain the note about storing
TOP 100 for performance (or alternatively, if you intended separate tables,
change the implementation to match the two-view description).

Comment on lines +20 to +22
@Slf4j
@Repository
public class ProductRankRepositoryImpl implements ProductRankRepository {
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

ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ๋ˆ„๋ฝ

Repository ๊ตฌํ˜„์ฒด์— @Transactional ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค. EntityManager๋ฅผ ์‚ฌ์šฉํ•˜๋Š” JPA ์ž‘์—…์—๋Š” ํŠธ๋žœ์žญ์…˜ ์ปจํ…์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ์ฝ”๋“œ๋Š” ์ฝ๊ธฐ ์ „์šฉ ๋ฉ”์„œ๋“œ๋งŒ ํ‘œ์‹œ๋˜์–ด ์žˆ์ง€๋งŒ, AI ์š”์•ฝ์—๋Š” saveRanks์™€ deleteByPeriod ๊ฐ™์€ ์“ฐ๊ธฐ ์ž‘์—…์ด ์–ธ๊ธ‰๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ž‘์—…์—๋Š” ํŠธ๋žœ์žญ์…˜์ด ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž ํŠธ๋žœ์žญ์…˜ ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€

ํด๋ž˜์Šค ๋ ˆ๋ฒจ์— ์ฝ๊ธฐ ์ „์šฉ ํŠธ๋žœ์žญ์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ , ์“ฐ๊ธฐ ๋ฉ”์„œ๋“œ์—๋Š” ๊ฐœ๋ณ„์ ์œผ๋กœ ์ ์šฉ:

 @Slf4j
 @Repository
+@Transactional(readOnly = true)
 public class ProductRankRepositoryImpl implements ProductRankRepository {

์“ฐ๊ธฐ ๋ฉ”์„œ๋“œ(saveRanks, deleteByPeriod ๋“ฑ)์—๋Š”:

@Transactional
public void saveRanks(...) {
    // implementation
}
๐Ÿ“ 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
@Slf4j
@Repository
public class ProductRankRepositoryImpl implements ProductRankRepository {
@Slf4j
@Repository
@Transactional(readOnly = true)
public class ProductRankRepositoryImpl implements ProductRankRepository {
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.java
around lines 20-22, the repository implementation lacks transaction management:
add class-level @Transactional(readOnly = true) to ensure a transactional
context for read operations and then annotate mutating methods (e.g., saveRanks,
deleteByPeriod) individually with @Transactional (without readOnly) so write
operations run in a proper transaction; also import
org.springframework.transaction.annotation.Transactional and place the
annotations on the class and the specific write methods.

Comment on lines +83 to +85
@SuppressWarnings("rawtypes")
org.springframework.data.repository.PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository();
}
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

ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๊ฐœ์„ ํ•˜์„ธ์š”.

getJpaRepository() ๋ฉ”์„œ๋“œ๊ฐ€ raw type์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  @SuppressWarnings("rawtypes")๋กœ ๊ฒฝ๊ณ ๋ฅผ ์–ต์ œํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Spring Batch์˜ RepositoryItemReader๊ฐ€ PagingAndSortingRepository๋ฅผ ์š”๊ตฌํ•˜๋”๋ผ๋„, ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ๋„ค๋ฆญ ํƒ€์ž…์„ ๋ช…์‹œํ•œ ๊ฐœ์„ ์•ˆ
     /**
      * Spring Batch์˜ RepositoryItemReader์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ JPA Repository๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
      * <p>
      * RepositoryItemReader๋Š” PagingAndSortingRepository๋ฅผ ์ง์ ‘ ์š”๊ตฌํ•˜๋ฏ€๋กœ,
      * ๊ธฐ์ˆ ์  ์ œ์•ฝ์œผ๋กœ ์ธํ•ด JPA Repository์— ๋Œ€ํ•œ ์ ‘๊ทผ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
      * </p>
      * <p>
      * <b>์ฃผ์˜:</b> ์ด ๋ฉ”์„œ๋“œ๋Š” Spring Batch์˜ ๊ธฐ์ˆ ์  ์š”๊ตฌ์‚ฌํ•ญ์œผ๋กœ ์ธํ•ด ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.
      * ์ผ๋ฐ˜์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ๋Š” ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ ,
      * ์œ„์˜ ๋„๋ฉ”์ธ ๋ฉ”์„œ๋“œ๋“ค์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      * </p>
      *
      * @return PagingAndSortingRepository๋ฅผ ๊ตฌํ˜„ํ•œ JPA Repository
      */
-    @SuppressWarnings("rawtypes")
-    org.springframework.data.repository.PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository();
+    org.springframework.data.repository.PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository();

๊ตฌํ˜„์ฒด(ProductMetricsRepositoryImpl)์—์„œ๋„ ์ œ๋„ค๋ฆญ ํƒ€์ž…์„ ๋ช…์‹œํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๊ฒฝ๊ณ ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿ“ 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
@SuppressWarnings("rawtypes")
org.springframework.data.repository.PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository();
}
/**
* Spring Batch์˜ RepositoryItemReader์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ JPA Repository๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
* <p>
* RepositoryItemReader๋Š” PagingAndSortingRepository๋ฅผ ์ง์ ‘ ์š”๊ตฌํ•˜๋ฏ€๋กœ,
* ๊ธฐ์ˆ ์  ์ œ์•ฝ์œผ๋กœ ์ธํ•ด JPA Repository์— ๋Œ€ํ•œ ์ ‘๊ทผ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
* </p>
* <p>
* <b>์ฃผ์˜:</b> ์ด ๋ฉ”์„œ๋“œ๋Š” Spring Batch์˜ ๊ธฐ์ˆ ์  ์š”๊ตฌ์‚ฌํ•ญ์œผ๋กœ ์ธํ•ด ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.
* ์ผ๋ฐ˜์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ๋Š” ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ ,
* ์œ„์˜ ๋„๋ฉ”์ธ ๋ฉ”์„œ๋“œ๋“ค์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
* </p>
*
* @return PagingAndSortingRepository๋ฅผ ๊ตฌํ˜„ํ•œ JPA Repository
*/
org.springframework.data.repository.PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository();
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
around lines 83-85, the method currently suppresses raw-type warnings; change
the signature to return a parameterized repository type instead of a raw type
and remove the @SuppressWarnings: declare the method as
PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository(); then update
the implementing class ProductMetricsRepositoryImpl to match that exact generic
return type and adjust imports so the code compiles without raw-type warnings.

Comment on lines +36 to +37
private final ThreadLocal<Integer> currentRank = ThreadLocal.withInitial(() -> 0);
private static final int TOP_RANK_LIMIT = 100;
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

ThreadLocal ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐ ๋žญํ‚น ์˜ค๋ฅ˜ ๊ฐ€๋Šฅ์„ฑ

currentRank์˜ remove()๊ฐ€ rank == 100์ผ ๋•Œ๋งŒ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ์ƒํ™ฉ์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค:

  1. ์ƒํ’ˆ์ด 100๊ฐœ ๋ฏธ๋งŒ์ธ ๊ฒฝ์šฐ ThreadLocal์ด ์ •๋ฆฌ๋˜์ง€ ์•Š์Œ
  2. Job ์‹คํŒจ ์‹œ ๋‹ค์Œ ์‹คํ–‰์—์„œ ์ด์ „ ๊ฐ’์ด ๋‚จ์•„์žˆ์Œ
  3. ์Šค๋ ˆ๋“œ ์žฌ์‚ฌ์šฉ ์‹œ ์ž˜๋ชป๋œ ๋žญํ‚น ๋ฒˆํ˜ธ ๋ถ€์—ฌ
๐Ÿ”Ž StepExecutionListener๋กœ ์•ˆ์ „ํ•œ ์ดˆ๊ธฐํ™”/์ •๋ฆฌ ์ œ์•ˆ
 @Slf4j
 @Component
 @RequiredArgsConstructor
-public class ProductRankCalculationProcessor implements ItemProcessor<ProductRankScore, ProductRank> {
+public class ProductRankCalculationProcessor implements ItemProcessor<ProductRankScore, ProductRank>, StepExecutionListener {

     private final ProductRankAggregationProcessor productRankAggregationProcessor;
     private final ThreadLocal<Integer> currentRank = ThreadLocal.withInitial(() -> 0);
     private static final int TOP_RANK_LIMIT = 100;

+    @Override
+    public void beforeStep(StepExecution stepExecution) {
+        currentRank.set(0);
+    }
+
+    @Override
+    public ExitStatus afterStep(StepExecution stepExecution) {
+        currentRank.remove();
+        return stepExecution.getExitStatus();
+    }
+
     @Override
     public ProductRank process(ProductRankScore score) throws Exception {
         int rank = currentRank.get() + 1;
         currentRank.set(rank);

         if (rank > TOP_RANK_LIMIT) {
             return null;
         }
         // ...
-        if (rank == TOP_RANK_LIMIT) {
-            currentRank.remove();
-        }
         return productRank;
     }

Also applies to: 79-82

Comment on lines +184 to +185
LocalDate date = parseDate(targetDate);
ProductRank.PeriodType period = ProductRank.PeriodType.valueOf(periodType.toUpperCase());
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

periodType null ์ฒดํฌ ํ•„์š”

periodType์ด Job ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ์ฃผ์ž…๋˜์ง€ ์•Š์œผ๋ฉด null์ด ๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ, toUpperCase() ํ˜ธ์ถœ ์‹œ NullPointerException์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž null ๊ฒ€์ฆ ์ถ”๊ฐ€ ์ œ์•ˆ
     public ItemReader<ProductMetrics> productRankReader(
         @Value("#{jobParameters['periodType']}") String periodType,
         @Value("#{jobParameters['targetDate']}") String targetDate
     ) {
+        if (periodType == null || periodType.isEmpty()) {
+            throw new IllegalArgumentException("periodType ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. (WEEKLY ๋˜๋Š” MONTHLY)");
+        }
+
         LocalDate date = parseDate(targetDate);
         ProductRank.PeriodType period = ProductRank.PeriodType.valueOf(periodType.toUpperCase());
๐Ÿ“ 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
LocalDate date = parseDate(targetDate);
ProductRank.PeriodType period = ProductRank.PeriodType.valueOf(periodType.toUpperCase());
public ItemReader<ProductMetrics> productRankReader(
@Value("#{jobParameters['periodType']}") String periodType,
@Value("#{jobParameters['targetDate']}") String targetDate
) {
if (periodType == null || periodType.isEmpty()) {
throw new IllegalArgumentException("periodType ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. (WEEKLY ๋˜๋Š” MONTHLY)");
}
LocalDate date = parseDate(targetDate);
ProductRank.PeriodType period = ProductRank.PeriodType.valueOf(periodType.toUpperCase());
๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java
around lines 184-185, the code calls periodType.toUpperCase() without checking
for null which can cause a NullPointerException; add a null/blank check before
calling toUpperCase() (e.g., if periodType is null or blank, either set a
sensible default PeriodType or throw an IllegalArgumentException with a clear
message), then use periodType.trim().toUpperCase() to create the
ProductRank.PeriodType via valueOf; ensure the error path logs or surfaces a
descriptive message indicating the missing/invalid job parameter.

Comment on lines +84 to +102
List<ProductRankScore> scores = chunkAggregatedMap.entrySet().stream()
.map(entry -> {
Long productId = entry.getKey();
AggregatedMetrics chunkAggregated = entry.getValue();

// ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์กฐํšŒ
java.util.Optional<ProductRankScore> existing = productRankScoreRepository.findByProductId(productId);

// ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋ˆ„์ 
Long totalLikeCount = chunkAggregated.getLikeCount();
Long totalSalesCount = chunkAggregated.getSalesCount();
Long totalViewCount = chunkAggregated.getViewCount();

if (existing.isPresent()) {
ProductRankScore existingScore = existing.get();
totalLikeCount += existingScore.getLikeCount();
totalSalesCount += existingScore.getSalesCount();
totalViewCount += existingScore.getViewCount();
}
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

N+1 ์ฟผ๋ฆฌ ํŒจํ„ด์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์ €ํ•˜ ์šฐ๋ ค

Chunk ๋‚ด ๊ฐ productId๋งˆ๋‹ค findByProductId๋ฅผ ๊ฐœ๋ณ„ ํ˜ธ์ถœํ•˜์—ฌ N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. Chunk ํฌ๊ธฐ๊ฐ€ ํฌ๊ฑฐ๋‚˜ ๊ณ ์œ  ์ƒํ’ˆ์ด ๋งŽ์„ ๊ฒฝ์šฐ ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ผ๊ด„ ์กฐํšŒ ๋ฐฉ์‹์œผ๋กœ ๊ฐœ์„  ์ œ์•ˆ
+        // Chunk ๋‚ด ๋ชจ๋“  productId๋ฅผ ํ•œ ๋ฒˆ์— ์กฐํšŒ
+        Set<Long> productIds = chunkAggregatedMap.keySet();
+        Map<Long, ProductRankScore> existingScores = productRankScoreRepository
+            .findAllByProductIdIn(productIds)
+            .stream()
+            .collect(Collectors.toMap(ProductRankScore::getProductId, Function.identity()));
+
         List<ProductRankScore> scores = chunkAggregatedMap.entrySet().stream()
             .map(entry -> {
                 Long productId = entry.getKey();
                 AggregatedMetrics chunkAggregated = entry.getValue();
                 
-                // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์กฐํšŒ
-                java.util.Optional<ProductRankScore> existing = productRankScoreRepository.findByProductId(productId);
+                // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์กฐํšŒ (์ผ๊ด„ ์กฐํšŒ ๊ฒฐ๊ณผ์—์„œ)
+                ProductRankScore existing = existingScores.get(productId);
                 
                 // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋ˆ„์ 
                 Long totalLikeCount = chunkAggregated.getLikeCount();
                 Long totalSalesCount = chunkAggregated.getSalesCount();
                 Long totalViewCount = chunkAggregated.getViewCount();
                 
-                if (existing.isPresent()) {
-                    ProductRankScore existingScore = existing.get();
-                    totalLikeCount += existingScore.getLikeCount();
+                if (existing != null) {
+                    totalLikeCount += existing.getLikeCount();

ProductRankScoreRepository์— findAllByProductIdIn(Set<Long> productIds) ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

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

๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.java
around lines 84-102, the code calls productRankScoreRepository.findByProductId
for each productId causing an N+1 query problem; instead add a repository method
findAllByProductIdIn(Set<Long> productIds) (or equivalent) and before mapping
collect the set of productIds from chunkAggregatedMap, call the new batch method
once, convert the returned list to a Map<Long, ProductRankScore>, and then use
that map to look up existing scores while computing
totalLikeCount/totalSalesCount/totalViewCount so all existing records are
fetched in a single query.

Comment on lines +44 to +56
* @param startDateTime ์กฐํšŒ ์‹œ์ž‘ ์‹œ๊ฐ (ํ•ด๋‹น ๋‚ ์งœ์˜ 00:00:00)
* @param endDateTime ์กฐํšŒ ์ข…๋ฃŒ ์‹œ๊ฐ (ํ•ด๋‹น ๋‚ ์งœ์˜ 23:59:59.999999999)
* @param pageable ํŽ˜์ด์ง• ์ •๋ณด
* @return ์กฐํšŒ๋œ ๋ฉ”ํŠธ๋ฆญ ํŽ˜์ด์ง€
*/
@Query("SELECT pm FROM ProductMetrics pm " +
"WHERE pm.updatedAt >= :startDateTime AND pm.updatedAt < :endDateTime " +
"ORDER BY pm.productId")
Page<ProductMetrics> findByUpdatedAtBetween(
@Param("startDateTime") LocalDateTime startDateTime,
@Param("endDateTime") LocalDateTime endDateTime,
Pageable pageable
);
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

Javadoc๊ณผ ์ฟผ๋ฆฌ ์กฐ๊ฑด ๋ถˆ์ผ์น˜

Javadoc์—์„œ endDateTime์„ "23:59:59.999999999"๋กœ ์„ค๋ช…ํ•˜์ง€๋งŒ, ์ฟผ๋ฆฌ๋Š” < :endDateTime์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ํ˜ธ์ถœ ์‹œ ๋‹ค์Œ ๋‚  00:00:00์„ ์ „๋‹ฌํ•ด์•ผ ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. Javadoc์„ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜ ํ˜ธ์ถœ๋ถ€์™€ ์ผ๊ด€์„ฑ์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java
around lines 44 to 56, the Javadoc claims endDateTime represents
"23:59:59.999999999" but the JPQL uses a strict less-than (< :endDateTime),
causing an off-by-one-day behavior; fix by making them consistent: either update
the Javadoc to state endDateTime is exclusive (e.g., the caller should pass
nextDay at 00:00:00) or change the query to use a non-strict comparison (<=
:endDateTime) if you intend endDateTime to be inclusive, and ensure any callers
are adjusted accordingly.

Comment on lines +82 to +98
@DisplayName("Chunk์˜ ๋ชจ๋“  ํ•ญ๋ชฉ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค")
@Test
void writesChunk_processesAllItems() throws Exception {
// arrange
int itemCount = 10;
List<ProductMetrics> items = createProductMetricsList(itemCount);
Chunk<ProductMetrics> chunk = new Chunk<>(items);

// act
writer.write(chunk);

// assert
// ํ˜„์žฌ๋Š” ๋กœ๊น…๋งŒ ์ˆ˜ํ–‰ํ•˜๋ฏ€๋กœ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฉด ์„ฑ๊ณต
// ํ–ฅํ›„ Materialized View ์ €์žฅ ๋กœ์ง ์ถ”๊ฐ€ ์‹œ ์ถ”๊ฐ€ ๊ฒ€์ฆ ํ•„์š”
assertThatCode(() -> writer.write(chunk))
.doesNotThrowAnyException();
}
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

์ค‘๋ณต๋œ write ํ˜ธ์ถœ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

writesChunk_processesAllItems ํ…Œ์ŠคํŠธ์—์„œ writer.write(chunk)๊ฐ€ ๋‘ ๋ฒˆ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค (Line 91๊ณผ Line 96). ์ฒซ ๋ฒˆ์งธ ํ˜ธ์ถœ ํ›„ ๋‘ ๋ฒˆ์งธ ํ˜ธ์ถœ์€ ์ถ”๊ฐ€์ ์ธ ๊ฒ€์ฆ ๊ฐ€์น˜๊ฐ€ ์—†์œผ๋ฉฐ, ํ…Œ์ŠคํŠธ ์˜๋„๊ฐ€ ๋ถˆ๋ช…ํ™•ํ•ด์ง‘๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
     @DisplayName("Chunk์˜ ๋ชจ๋“  ํ•ญ๋ชฉ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค")
     @Test
     void writesChunk_processesAllItems() throws Exception {
         // arrange
         int itemCount = 10;
         List<ProductMetrics> items = createProductMetricsList(itemCount);
         Chunk<ProductMetrics> chunk = new Chunk<>(items);

-        // act
-        writer.write(chunk);
-
-        // assert
+        // act & assert
         // ํ˜„์žฌ๋Š” ๋กœ๊น…๋งŒ ์ˆ˜ํ–‰ํ•˜๋ฏ€๋กœ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฉด ์„ฑ๊ณต
         // ํ–ฅํ›„ Materialized View ์ €์žฅ ๋กœ์ง ์ถ”๊ฐ€ ์‹œ ์ถ”๊ฐ€ ๊ฒ€์ฆ ํ•„์š”
         assertThatCode(() -> writer.write(chunk))
             .doesNotThrowAnyException();
     }
๐Ÿ“ 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
@DisplayName("Chunk์˜ ๋ชจ๋“  ํ•ญ๋ชฉ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค")
@Test
void writesChunk_processesAllItems() throws Exception {
// arrange
int itemCount = 10;
List<ProductMetrics> items = createProductMetricsList(itemCount);
Chunk<ProductMetrics> chunk = new Chunk<>(items);
// act
writer.write(chunk);
// assert
// ํ˜„์žฌ๋Š” ๋กœ๊น…๋งŒ ์ˆ˜ํ–‰ํ•˜๋ฏ€๋กœ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฉด ์„ฑ๊ณต
// ํ–ฅํ›„ Materialized View ์ €์žฅ ๋กœ์ง ์ถ”๊ฐ€ ์‹œ ์ถ”๊ฐ€ ๊ฒ€์ฆ ํ•„์š”
assertThatCode(() -> writer.write(chunk))
.doesNotThrowAnyException();
}
@DisplayName("Chunk์˜ ๋ชจ๋“  ํ•ญ๋ชฉ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค")
@Test
void writesChunk_processesAllItems() throws Exception {
// arrange
int itemCount = 10;
List<ProductMetrics> items = createProductMetricsList(itemCount);
Chunk<ProductMetrics> chunk = new Chunk<>(items);
// act & assert
// ํ˜„์žฌ๋Š” ๋กœ๊น…๋งŒ ์ˆ˜ํ–‰ํ•˜๋ฏ€๋กœ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฉด ์„ฑ๊ณต
// ํ–ฅํ›„ Materialized View ์ €์žฅ ๋กœ์ง ์ถ”๊ฐ€ ์‹œ ์ถ”๊ฐ€ ๊ฒ€์ฆ ํ•„์š”
assertThatCode(() -> writer.write(chunk))
.doesNotThrowAnyException();
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriterTest.java
around lines 82 to 98, the test calls writer.write(chunk) twice (once before the
assertion and once inside assertThatCode), creating a redundant invocation;
remove the standalone writer.write(chunk) before the assertion so the test
invokes write only within the assertThatCode (or alternatively call write once
and assert around that single call) to keep the test intent clear and eliminate
the duplicate call.

Comment on lines +47 to +63
@DisplayName("์ฃผ๊ฐ„ Reader๋Š” ํ•ด๋‹น ์ฃผ์˜ ์›”์š”์ผ๋ถ€ํ„ฐ ๋‹ค์Œ ์ฃผ ์›”์š”์ผ๊นŒ์ง€์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ๋‹ค")
@Test
void weeklyReaderQueriesFromMondayToNextMonday() {
// arrange
LocalDate targetDate = LocalDate.of(2024, 12, 15); // ์ผ์š”์ผ
when(productMetricsRepository.getJpaRepository()).thenReturn(jpaRepository);

ProductRankAggregationReader reader = new ProductRankAggregationReader(productMetricsRepository);

// act
RepositoryItemReader<ProductMetrics> itemReader = reader.createWeeklyReader(targetDate);

// assert
assertThat(itemReader).isNotNull();
// ์ฃผ๊ฐ„ ์‹œ์ž‘์ผ์€ ํ•ด๋‹น ์ฃผ์˜ ์›”์š”์ผ์ด์–ด์•ผ ํ•จ
// 2024-12-15(์ผ) -> 2024-12-09(์›”)์ด ์‹œ์ž‘์ผ
}
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

ํ…Œ์ŠคํŠธ assertion์ด ๋ถˆ์™„์ „ํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์ด๋ฆ„๊ณผ ์ฃผ์„์—์„œ๋Š” "ํ•ด๋‹น ์ฃผ์˜ ์›”์š”์ผ๋ถ€ํ„ฐ ๋‹ค์Œ ์ฃผ ์›”์š”์ผ๊นŒ์ง€์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ๋‹ค"๊ณ  ๋ช…์‹œํ•˜์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” reader๊ฐ€ null์ด ์•„๋‹Œ์ง€๋งŒ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. ๋‚ ์งœ ๋ฒ”์œ„๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” assertion์ด ์—†์Šต๋‹ˆ๋‹ค.

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

๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReaderTest.java
around lines 47-63, the test only asserts the reader is non-null but does not
verify the computed date range; update the test to assert the reader queries
from the expected Monday to next Monday. Either: 1) assert readerโ€™s internal
properties that hold start/end dates (or the query parameters) to equal
2024-12-09 (start) and 2024-12-16 (end), or 2) refactor
ProductRankAggregationReader to extract the weekly date-range calculation into a
package-visible method (e.g., calculateWeeklyRange(LocalDate)) and add a unit
test that calls that method with 2024-12-15 and asserts the returned start is
2024-12-09 and end is 2024-12-16. Ensure mocks/stubs expose whatever
fields/methods are needed so assertions can be made.

@minor7295 minor7295 marked this pull request as ready for review January 2, 2026 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant