Skip to content

Conversation

@sieun0322
Copy link
Collaborator

@sieun0322 sieun0322 commented Jan 1, 2026

๐Ÿ“Œ Summary

์ฃผ๊ฐ„/์›”๊ฐ„ ์ƒํ’ˆ ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌํ˜„

  • Redis ๊ธฐ๋ฐ˜ ์บ์‹ฑ๊ณผ ๋ฐฐ์น˜ ์ง‘๊ณ„๋ฅผ ํ†ตํ•ด ๊ณ ์„ฑ๋Šฅ ๋žญํ‚น ์กฐํšŒ API ์ œ๊ณต
  • ์บ์‹œ ๋ฏธ์Šค ์ƒํ™ฉ์—์„œ๋„ ์•ˆ์ •์ ์ธ ์‘๋‹ต์„ ์œ„ํ•œ DB Fallback ๋กœ์ง ๊ตฌํ˜„
  • ์ผ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ฃผ๊ฐ„ยท์›”๊ฐ„ ๋žญํ‚น์„ ์ƒ์„ฑํ•˜๋Š” ์ง‘๊ณ„ ๊ตฌ์กฐ ์„ค๊ณ„

๐Ÿ’ฌ Review Points

1. ๋žญํ‚น ์ง‘๊ณ„ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ (commerce-batch)

Materialized View (MV) ํ…Œ์ด๋ธ”

  • mv_product_rank_weekly : ์ฃผ๊ฐ„ TOP 100
  • mv_product_rank_monthly : ์›”๊ฐ„ TOP 100

๊ณตํ†ต ์ปฌ๋Ÿผ

  • product_id
  • ranking
  • like_count
  • order_count
  • view_count
  • period_yyyyww / period_yyyymm

๋ฆฌ๋ทฐ ํฌ์ธํŠธ

  • ์ฃผ๊ฐ„/์›”๊ฐ„ MV ํ…Œ์ด๋ธ” ๋ถ„๋ฆฌ๊ฐ€ ์ ์ ˆํ•œ์ง€
  • ๊ธฐ๊ฐ„ ์ปฌ๋Ÿผ(period_yyyyww / period_yyyymm) ์„ค๊ณ„์˜ ํ™•์žฅ์„ฑ

2. ๋žญํ‚น ์ง‘๊ณ„ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํ๋ฆ„

์ „์ฒด ์ง‘๊ณ„ ํŒŒ์ดํ”„๋ผ์ธ

  1. 10๋ถ„ ๋‹จ์œ„ ์›์ฒœ ๋ฐ์ดํ„ฐ ์ ์žฌ

    • product_metrics ํ…Œ์ด๋ธ”์— 10๋ถ„ ๋‹จ์œ„ ์ง€ํ‘œ ์ €์žฅ
  2. ์ผ๊ฐ„ ์ง‘๊ณ„

    • ์›์ฒœ ๋ฐ์ดํ„ฐ๋ฅผ ์ผ ๋‹จ์œ„๋กœ ์ง‘๊ณ„ํ•˜์—ฌ product_metrics_daily ์ƒ์„ฑ
  3. ์ฃผ๊ฐ„/์›”๊ฐ„ ์ง‘๊ณ„

    • product_metrics_daily ๊ธฐ์ค€์œผ๋กœ TOP 100 ์‚ฐ์ถœ
    • MV ํ…Œ์ด๋ธ” ๊ฐฑ์‹  ๋ฐ Redis ์บ์‹œ ๋ฌดํšจํ™”

๋ฆฌ๋ทฐ ํฌ์ธํŠธ

  • 10๋ถ„ โ†’ ์ผ๊ฐ„ โ†’ ์ฃผ๊ฐ„/์›”๊ฐ„ ๊ตฌ์กฐ์˜ ์ ์ ˆ์„ฑ
  • ์ผ๊ฐ„ ํ…Œ์ด๋ธ”์„ ์ค‘๊ฐ„ ๊ธฐ์ค€ ํ…Œ์ด๋ธ”๋กœ ๋‘” ์„ค๊ณ„ ํŒ๋‹จ

3. ๋ฐฐ์น˜ ์‹คํ–‰ ๋ช…๋ น ๋ฐ Job ๊ตฌ์„ฑ

์ผ๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜

java -jar commerce-batch.jar \
  --job.name=dailyRankingDataProcessingJob \
  --date=20260101

Step ๊ตฌ์„ฑ

  1. clearDailyWorkingTablesStep

    • ์ผ๊ฐ„ ์ง‘๊ณ„๋ฅผ ์œ„ํ•œ working ํ…Œ์ด๋ธ” ์ดˆ๊ธฐํ™”
  2. dailyRankingStep

    • product_metrics ๊ธฐ์ค€ ์ƒํ’ˆ๋ณ„ ์ผ๊ฐ„ ์ง€ํ‘œ ์ง‘๊ณ„
    • ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ working ํ…Œ์ด๋ธ”์— ์ €์žฅ
  3. dailyDataValidationStep

    • ์ง‘๊ณ„ ๊ฑด์ˆ˜ ๋ฐ ํ•„์ˆ˜ ์ปฌ๋Ÿผ NULL ์—ฌ๋ถ€ ๊ฒ€์ฆ
  4. dailyTableSwapStep

    • working ํ…Œ์ด๋ธ”์„ product_metrics_daily๋กœ ์›์ž์  ๊ต์ฒด

์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜

java -jar commerce-batch.jar \
  --job.name=weeklyRankingDataProcessingJob \
  --date=20260101

Step ๊ตฌ์„ฑ

  1. weeklyDataProcessingStep

    • ์ผ๊ฐ„ ์ง‘๊ณ„ ํ…Œ์ด๋ธ” ๊ธฐ์ค€ ์ฃผ์ฐจ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„
  2. weeklyTop100MVUpdateStep

    • ๊ธฐ์กด MV ๋ฐ์ดํ„ฐ ์‚ญ์ œ ํ›„ TOP 100 INSERT
    • Redis ์ฃผ๊ฐ„ ๋žญํ‚น ์บ์‹œ ์‚ญ์ œ
      (ranking:weekly:{YYYYWW}W:*)

์›”๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜

java -jar commerce-batch.jar \
  --job.name=monthlyRankingDataProcessingJob \
  --date=20260101

Step ๊ตฌ์„ฑ

  1. monthlyDataProcessingStep

    • ์ผ๊ฐ„ ์ง‘๊ณ„ ํ…Œ์ด๋ธ” ๊ธฐ์ค€ ์›”๊ฐ„ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„
  2. monthlyTop100MVUpdateStep

    • ๊ธฐ์กด MV ๋ฐ์ดํ„ฐ ์‚ญ์ œ ํ›„ TOP 100 INSERT
    • Redis ์›”๊ฐ„ ๋žญํ‚น ์บ์‹œ ์‚ญ์ œ
      (ranking:monthly:{YYYYMM}M:*)

4. Redis ์บ์‹œ ๊ตฌ์กฐ (commerce-api)

  • ์ฃผ๊ฐ„

    • ranking:weekly:{YYYYWW}W
    • TTL: 7์ผ
  • ์›”๊ฐ„

    • ranking:monthly:{YYYYMM}M
    • TTL: 30์ผ

๋ฆฌ๋ทฐ ํฌ์ธํŠธ

  • ์บ์‹œ ํ‚ค ๋„ค์ด๋ฐ ๋ฐ TTL ์ •์ฑ…์˜ ์ ์ ˆ์„ฑ

5. ๋žญํ‚น API ์„ค๊ณ„

API Endpoint

GET /api/v1/rankings?date={date}&page={page}&size={size}

๋‚ ์งœ ํฌ๋งท๋ณ„ ์ฒ˜๋ฆฌ

  • YYYYMMDD โ†’ ์ผ๊ฐ„

  • YYYYWW โ†’ ์ฃผ๊ฐ„

  • YYYYMM โ†’ ์›”๊ฐ„

  • RankingDateParser๋ฅผ ํ†ตํ•ด ๋‚ ์งœ ํ˜•์‹ ๊ฒ€์ฆ ๋ฐ ๋ถ„๊ธฐ

๋ฆฌ๋ทฐ ํฌ์ธํŠธ

  • ๋‹จ์ผ API๋กœ ์ผ/์ฃผ/์›” ๋žญํ‚น์„ ์ œ๊ณตํ•˜๋Š” ์„ค๊ณ„์˜ ์ ์ ˆ์„ฑ

6. Redis Fallback ์ „๋žต

  1. Redis ์บ์‹œ ์กฐํšŒ
  2. ์บ์‹œ ๋ฏธ์Šค ์‹œ MV ํ…Œ์ด๋ธ” ์กฐํšŒ
  3. MV ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ์ผ๊ฐ„ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐ
  4. ์กฐํšŒ ๊ฒฐ๊ณผ Redis ์žฌ์ ์žฌ

โ“ ์งˆ๋ฌธ

Q1. ์ง‘๊ณ„ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๊ด€๋ จ

์ดˆ๊ธฐ ์ง€ํ‘œ ํ…Œ์ด๋ธ”์„ 10๋ถ„ ๋‹จ์œ„๋กœ ์„ค๊ณ„ํ•˜๋ฉด์„œ, ์ฃผ๊ฐ„ยท์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹œ ๊ธฐ์ค€์œผ๋กœ ์‚ฌ์šฉํ•  ์ผ๊ฐ„ ๋งคํŠธ๋ฆญ์Šค ํ…Œ์ด๋ธ”(product_metrics_daily)์„ ์ถ”๊ฐ€๋กœ ์ƒ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์ง‘๊ณ„ ๋‹จ๊ณ„๊ฐ€ 10๋ถ„ โ†’ ์ผ๊ฐ„ โ†’ ์ฃผ๊ฐ„/์›”๊ฐ„์œผ๋กœ ๋‚˜๋‰˜์—ˆ๋Š”๋ฐ, ํ˜„์žฌ ๊ตฌ์กฐ๊ฐ€ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๋งŽ์€ ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”์ง€ ๊ณ ๋ฏผ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ 10๋ถ„ ๋‹จ์œ„ ํ…Œ์ด๋ธ”์„ ๊ธฐ์ค€์œผ๋กœ ๋ฐ”๋กœ ์ฃผ๊ฐ„ยท์›”๊ฐ„ ๋žญํ‚น์„ ์‚ฐ์ถœํ•œ๋‹ค๋ฉด, ์ผ๋ฐ˜์ ์œผ๋กœ๋Š” ์–ด๋–ค ๋ฐฉ์‹์˜ ์„ค๊ณ„๊ฐ€ ๋” ์ ์ ˆํ•œ์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค.


Q2. ๋žญํ‚น API ํ†ตํ•ฉ ์„ค๊ณ„ ๊ด€๋ จ

๊ณผ์ œ ์ •๋ณด์ƒ "๊ธฐ์กด Ranking ์„ ์ œ๊ณตํ•˜๋Š” API ๋กœ ์ผ๊ฐ„, ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค" ๋ผ๋Š” ๋ฌธ๊ฐ€๊ฐ€ ์žˆ์–ด ๋‹จ์ผ ์—”๋“œํฌ์ธํŠธ๋กœ ์ผ๊ฐ„ยท์ฃผ๊ฐ„ยท์›”๊ฐ„ ๋žญํ‚น์„ ๋ชจ๋‘ ์ œ๊ณตํ•˜๋„๋ก ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น์„ ํ•˜๋‚˜์˜ API๋กœ ํ†ตํ•ฉํ•ด์•ผ ํ•  ์ด์œ ๊ฐ€ ์žˆ์—ˆ๋Š”์ง€, ์ผยท์ฃผยท์›” ๋žญํ‚น์„ ๋ถ„๋ฆฌ๋œ API๋กœ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ๋„ ์ƒ๊ด€์—†๋Š” ๋ถ€๋ถ„์ด์—ˆ๋Š”์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค.


Q3. Argo Workflow ์ ์šฉ ์—ฌ๋ถ€ ๊ด€๋ จ

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

Argo Workflow๊ฐ€ ์‹ค์ œ๋กœ ๊ฐ•์ ์„ ๋ฐœํœ˜ํ•˜๋Š” ๋ฌธ์ œ ์œ ํ˜•์ด๋‚˜, ๋ฐ˜๋“œ์‹œ ๋„์ž…์„ ๊ณ ๋ คํ•ด๋ณผ ๋งŒํ•œ ํ™˜๊ฒฝ์ด ์–ด๋–ค ๊ฒฝ์šฐ์ธ์ง€ ๋“ฃ๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

โœ… Checklist

๐Ÿงฑ Spring Batch

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

๐Ÿงฉ Ranking API

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

๐Ÿ“Ž References

Summary by CodeRabbit

Release Notes

  • New Features

    • ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ ๊ฐœ์„ : ์ผ๋ณ„, ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋‹ค์–‘ํ•œ ๊ธฐ๊ฐ„๋ณ„ ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
    • ๋‚ ์งœ ํ˜•์‹ ์œ ์—ฐํ™”: ๋‹ค์ค‘ ํ˜•์‹ ๋‚ ์งœ ์ž…๋ ฅ ์ง€์›์œผ๋กœ ์‚ฌ์šฉ์ž ํŽธ์˜์„ฑ ํ–ฅ์ƒ
  • Improvements

    • ์ฃผ๋ฌธ ๋ฐ ๊ฒฐ์ œ ํ”„๋กœ์„ธ์Šค ์•ˆ์ •ํ™”๋กœ ์‹œ์Šคํ…œ ์‹ ๋ขฐ์„ฑ ๊ฐ•ํ™”
    • ์ฟ ํฐ ์‚ฌ์šฉ ํ๋ฆ„ ๊ฐ„์†Œํ™” ๋ฐ ์„ฑ๋Šฅ ์ตœ์ ํ™”

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 1, 2026

Walkthrough

Order ์‹๋ณ„์ž ํƒ€์ž…์„ Long์—์„œ String์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ฐœ์„ ํ•˜๋ฉฐ, ์ œํ’ˆ ๋žญํ‚น ์‹œ์Šคํ…œ์„ ํ™•์žฅํ•˜๊ณ , ์ƒˆ๋กœ์šด ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

Changes

์ฝ”ํ˜ธํŠธ / ํŒŒ์ผ(๋“ค) ๋ณ€๊ฒฝ ์‚ฌํ•ญ
์ฃผ๋ฌธ ๋ฐ ๊ฒฐ์ œ ๋„๋ฉ”์ธ ํƒ€์ž… ๋ณ€๊ฒฝ
apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java, apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentRepository.java
orderId๋ฅผ Long์—์„œ String์œผ๋กœ ๋ณ€๊ฒฝ; Order์— generateOrderId() ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€; ์ƒˆ๋กœ์šด findByOrderId() ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
์ฃผ๋ฌธ/๊ฒฐ์ œ ์„œ๋น„์Šค ๋ฐ ํŒŒ์‚ฌ๋“œ
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java, apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java, apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java
๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ String orderId๋กœ ์—…๋ฐ์ดํŠธ; OutboxService ์˜์กด์„ฑ ์ œ๊ฑฐ; ์ง์ ‘ ์ด๋ฒคํŠธ ๋ฐœํ–‰์œผ๋กœ ๋ณ€๊ฒฝ; OrderPaidEvent, PaymentSuccessEvent, PaymentFailureEvent ๋ฐœํ–‰ ์ถ”๊ฐ€
์ด๋ฒคํŠธ ํด๋ž˜์Šค ๋ฐ ํ•ธ๋“ค๋Ÿฌ ์žฌ๊ตฌ์„ฑ
apps/commerce-api/src/main/java/com/loopers/application/event/BusinessActionEvent.java, apps/commerce-api/src/main/java/com/loopers/application/event/CouponUsedEvent.java, apps/commerce-api/src/main/java/com/loopers/application/event/OrderCreatedEvent.java, apps/commerce-api/src/main/java/com/loopers/application/event/OrderCancelledEvent.java, apps/commerce-api/src/main/java/com/loopers/application/event/PaymentCallbackEvent.java
orderId ํ•„๋“œ๋ฅผ Long์—์„œ String์œผ๋กœ ๋ณ€๊ฒฝ
์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ์ „์†ก ์ด๋ฒคํŠธ ๋ฐ ํ•ธ๋“ค๋Ÿฌ
apps/commerce-api/src/main/java/com/loopers/application/event/OrderDataTransferEvent.java, apps/commerce-api/src/main/java/com/loopers/application/event/PaymentDataTransferEvent.java, apps/commerce-api/src/main/java/com/loopers/application/event/OrderPaidEvent.java, apps/commerce-api/src/main/java/com/loopers/application/event/PaymentSuccessEvent.java, apps/commerce-api/src/main/java/com/loopers/application/event/PaymentFailureEvent.java, apps/commerce-api/src/main/java/com/loopers/application/event/DataTransferEventHandler.java
orderId๋ฅผ String์œผ๋กœ, amount๋ฅผ BigDecimal์—์„œ Money๋กœ ๋ณ€๊ฒฝ; OrderPaidEvent, PaymentSuccessEvent, PaymentFailureEvent ์‹ ๊ทœ ์ถ”๊ฐ€; DataTransferEventHandler ์‹ ๊ทœ ์ถ”๊ฐ€
์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ œ๊ฑฐ ๋ฐ ๊ฐœ์„ 
apps/commerce-api/src/main/java/com/loopers/application/event/DataPlatformEventHandler.java, apps/commerce-api/src/main/java/com/loopers/application/event/CouponEventHandler.java, apps/commerce-api/src/main/java/com/loopers/application/event/OrderEventHandler.java, apps/commerce-api/src/main/java/com/loopers/application/event/PaymentEventHandler.java
DataPlatformEventHandler ์ œ๊ฑฐ; CouponEventHandler์— try-catch ์ถ”๊ฐ€; OrderEventHandler ๋กœ๊น… ๋ฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ฐœ์„ ; PaymentEventHandler ์˜์กด์„ฑ ์ œ๊ฑฐ ๋ฐ ์ƒˆ๋กœ์šด ์ด๋ฒคํŠธ ํƒ€์ž… ์ฒ˜๋ฆฌ
Outbox ๋ฐ ์ œํ’ˆ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
apps/commerce-api/src/main/java/com/loopers/application/event/OutboxEventHandler.java, apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java, apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java, apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
OutboxEventHandler ์‹ ๊ทœ ์ถ”๊ฐ€; Outbox ๊ด€๋ จ ์˜์กด์„ฑ ์ œ๊ฑฐ; ProductViewedEvent ๋ฐ LikeEvent ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์ถ”๊ฐ€
๊ฒฐ์ œ ํด๋ผ์ด์–ธํŠธ ๋ฐ DTO
apps/commerce-api/src/main/java/com/loopers/application/payment/CreatePaymentCommand.java, apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentInfo.java, apps/commerce-api/src/main/java/com/loopers/application/payment/PgClient.java, apps/commerce-api/src/main/java/com/loopers/application/payment/PgPayRequest.java, apps/commerce-api/src/main/java/com/loopers/application/payment/PgPaymentInfoResponse.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/feign/PgClientImpl.java
๋ชจ๋“  ๊ฒฐ์ œ ๊ด€๋ จ ํด๋ž˜์Šค์—์„œ orderId๋ฅผ Long์—์„œ String์œผ๋กœ ๋ณ€๊ฒฝ; ํด๋ฐฑ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์—…๋ฐ์ดํŠธ
ํฌ์ธํŠธ ๋ฐ ๊ฒฐ์ œ ์ฝœ๋ฐฑ DTO
apps/commerce-api/src/main/java/com/loopers/application/point/EarnPointFromPaymentCommand.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/client/PaymentCallbackV1Dto.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/client/PaymentCreateV1Dto.java
orderId๋ฅผ Long์—์„œ String์œผ๋กœ ๋ณ€๊ฒฝ
API ๋ ˆ์ด์–ด ์—…๋ฐ์ดํŠธ
apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java
getOrderDetail ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ํƒ€์ž…์„ Long์—์„œ String์œผ๋กœ ๋ณ€๊ฒฝ
์ƒˆ๋กœ์šด ๋žญํ‚น ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java, apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java, apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java, apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java
์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ์ œํ’ˆ ๋žญํ‚น/๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ ์‹ ๊ทœ ์ถ”๊ฐ€; ๊ฐ€์ค‘ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง ํฌํ•จ
๋žญํ‚น ์„œ๋น„์Šค ๋ฐ ํŒŒ์‚ฌ๋“œ ํ™•์žฅ
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/MvProductRankWeeklyService.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/MvProductRankMonthlyService.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/ProductMetricsDailyService.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingPeriod.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingDateParser.java
์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์„œ๋น„์Šค ์ถ”๊ฐ€; Redis ์บ์‹ฑ ๋กœ์ง ํฌํ•จ; ๋‚ ์งœ ํŒŒ์‹ฑ ๋ฐ ๊ธฐ๊ฐ„ ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€
๋žญํ‚น ์ €์žฅ์†Œ ๋ฐ API
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepository.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepository.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductMetricsDailyRepository.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
์ƒˆ๋กœ์šด MV ์ €์žฅ์†Œ ๋ฐ ์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ์ €์žฅ์†Œ ์ถ”๊ฐ€; API ์‚ฌ์–‘ ์—…๋ฐ์ดํŠธ๋กœ ๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์กฐํšŒ ์ง€์›
๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹ ๊ทœ ๋ชจ๋“ˆ
apps/commerce-batch/build.gradle.kts, apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java, apps/commerce-batch/src/main/resources/application.yml
์ƒˆ๋กœ์šด ๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ชจ๋“ˆ ์ถ”๊ฐ€; Spring Batch, Redis, JPA ๋“ฑ ์˜์กด์„ฑ ์ถ”๊ฐ€
๋ฐฐ์น˜ ์ž‘์—… ์„ค์ •
apps/commerce-batch/src/main/java/com/loopers/batch/job/DailyRankingJobConfig.java, apps/commerce-batch/src/main/java/com/loopers/batch/job/WeeklyRankingJobConfig.java, apps/commerce-batch/src/main/java/com/loopers/batch/job/MonthlyRankingJobConfig.java
์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋ฐฐ์น˜ ์ž‘์—… ์‹ ๊ทœ ์ถ”๊ฐ€; ํ…Œ์ด๋ธ” ์Šค์™‘, ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ, Redis ์บ์‹œ ์ดˆ๊ธฐํ™” ๋กœ์ง ํฌํ•จ
๋ฐฐ์น˜ ์‹คํ–‰ ๋ฐ ์Šค์ผ€์ค„๋Ÿฌ
apps/commerce-batch/src/main/java/com/loopers/batch/runner/BatchJobRunner.java, apps/commerce-batch/src/main/java/com/loopers/batch/scheduler/RankingBatchScheduler.java, apps/commerce-batch/src/main/java/com/loopers/config/BatchJobProperties.java
๋ฐฐ์น˜ ์ž‘์—… ๋Ÿฐ์ฒ˜, ์Šค์ผ€์ค„๋Ÿฌ, ์„ค์ • ์†์„ฑ ํด๋ž˜์Šค ์‹ ๊ทœ ์ถ”๊ฐ€
๋ฐฐ์น˜ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ
apps/commerce-batch/src/main/java/com/loopers/domain/BaseEntity.java, apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java, apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductMetricsDaily.java, apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductMetricsWeekly.java, apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductMetricsMonthly.java, apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
๋ฐฐ์น˜์šฉ ๊ธฐ๋ณธ ์—”ํ‹ฐํ‹ฐ, ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ, ์ €์žฅ์†Œ ์‹ ๊ทœ ์ถ”๊ฐ€
Argo Workflows ์„ค์ •
argo-workflows/weekly-ranking-workflow.yaml
Argo Workflows ํ…œํ”Œ๋ฆฟ ์‹ ๊ทœ ์ถ”๊ฐ€๋กœ ์ผ์ผ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์›Œํฌํ”Œ๋กœ์šฐ ์ •์˜
์ŠคํŠธ๋ฆฌ๋จธ ๋ฐ ํ”„๋กœ์ ํŠธ ๊ตฌ์„ฑ
apps/commerce-streamer/src/main/java/com/loopers/application/batch/OrderEventProcessor.java, settings.gradle.kts
OrderEventProcessor์—์„œ orderId ์ถ”์ถœ์„ asLong()์—์„œ asText()๋กœ ๋ณ€๊ฒฝ; commerce-batch ๋ชจ๋“ˆ ํฌํ•จ ์ถ”๊ฐ€
์ฃผ๋ฌธ ๋ฐ ๊ฒฐ์ œ ์ €์žฅ์†Œ ๊ตฌํ˜„
apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentJpaRepository.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentRepositoryImpl.java
findByOrderId() ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€; ๋งค๊ฐœ๋ณ€์ˆ˜ ํƒ€์ž…์„ Long์—์„œ String์œผ๋กœ ๋ณ€๊ฒฝ
๋ฉ”ํŠธ๋ฆญ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ (API)
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetrics.java
์ œํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ ์‹ ๊ทœ ์ถ”๊ฐ€; ์กฐํšŒ/์ข‹์•„์š”/ํŒ๋งค ์ง‘๊ณ„ ๋ฉ”์„œ๋“œ ํฌํ•จ

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Order as OrderEventHandler
    participant Payment as PaymentService
    participant Event as ApplicationEventPublisher
    participant Cache as RedisCache
    
    rect rgb(200, 220, 255)
    Note over Client,Cache: ์ฃผ๋ฌธ ์ƒ์„ฑ ๋ฐ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ
    end
    
    Client->>Order: OrderCreatedEvent ๋ฐœํ–‰
    activate Order
    Order->>Payment: requestPayment(orderId)
    activate Payment
    Payment->>Event: publishEvent(PaymentSuccessEvent)
    deactivate Payment
    Event->>Event: ๊ฒฐ์ œ ์„ฑ๊ณต ์ฒ˜๋ฆฌ
    Event->>Event: publishEvent(OrderPaidEvent)
    Event->>Cache: updateRankingCache()
    deactivate Order
    
    rect rgb(200, 255, 200)
    Note over Client,Cache: ๊ฒฐ์ œ ์‹คํŒจ ์‹œ๋‚˜๋ฆฌ์˜ค
    end
    
    Order->>Payment: processPaymentCallback(orderId, FAILED)
    activate Payment
    Payment->>Event: publishEvent(PaymentFailureEvent)
    deactivate Payment
    Order->>Order: cancelPayment(orderId)
    Event->>Event: publishEvent(OrderCancelledEvent)
Loading
sequenceDiagram
    participant Batch as DailyRankingJob
    participant DB as Database
    participant Cache as Redis
    participant Validation as DataValidator
    
    rect rgb(255, 200, 200)
    Note over Batch,Cache: ๋ฐฐ์น˜ ์ž‘์—… - ์ผ์ผ ๋žญํ‚น ์ฒ˜๋ฆฌ
    end
    
    Batch->>DB: TRUNCATE product_metrics_daily_working
    Batch->>DB: SELECT daily metrics by date
    activate DB
    DB-->>Batch: metrics data
    deactivate DB
    
    Batch->>Batch: Transform & Process data
    Batch->>DB: INSERT INTO product_metrics_daily_working
    
    Batch->>Validation: validateData()
    activate Validation
    Validation->>DB: COUNT records
    Validation-->>Batch: validation result
    deactivate Validation
    
    alt ๊ฒ€์ฆ ์„ฑ๊ณต
    Batch->>DB: RENAME daily tables (swap)
    Batch->>Cache: invalidatePattern(ranking:daily:*)
    else ๊ฒ€์ฆ ์‹คํŒจ
    Batch->>Batch: rollback & error
    end
Loading

Estimated code review effort

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

Possibly related PRs

  • PR #217: ์ œํ’ˆ ๋žญํ‚น ๋ฐ ๋ฉ”ํŠธ๋ฆญ ์‹œ์Šคํ…œ ์ถ”๊ฐ€ โ€” ๋ณธ PR์—์„œ ๊ตฌํ˜„๋œ Redis ๊ธฐ๋ฐ˜ ๋žญํ‚น, ๋ฐฐ์น˜ ์ž‘์—… ๋ฐ ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ์™€ ์ง์ ‘์ ์œผ๋กœ ๊ฒน์น˜๋Š” ํ•ต์‹ฌ ๊ธฐ๋Šฅ๋“ค์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • PR #225: ์ด๋ฒคํŠธ/Outbox ํ๋ฆ„ ๋ฐ ๋žญํ‚น ์ง‘๊ณ„ ๊ฐœ์„  โ€” ๋ณธ PR์ด ์ œ๊ฑฐํ•œ OutboxService ๋Œ€์‹  ์ง์ ‘ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฐฉ์‹์œผ๋กœ์˜ ์ „ํ™˜๊ณผ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ ๋กœ์ง์ด ๊ฐ™์€ ์˜์—ญ์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

  • PR #165: orderId ํƒ€์ž… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐ PgClient ์ˆ˜์ • โ€” ๋ณธ PR์—์„œ ๊ด‘๋ฒ”์œ„ํ•˜๊ฒŒ ์ˆ˜ํ–‰๋œ orderId Longโ†’String ๋ณ€ํ™˜๊ณผ PgClient ์ธํ„ฐํŽ˜์ด์Šค ๋ณ€๊ฒฝ์ด ์ด PR๊ณผ ์ง์ ‘ ์—ฐ๊ด€๋ฉ๋‹ˆ๋‹ค.

Suggested labels

enhancement

Poem

๐Ÿฐ ์ฃผ๋ฌธ๋ถ€ํ„ฐ ๊ฒฐ์ œ๊นŒ์ง€ ์ˆœ์„œ๋Œ€๋กœ,
๊ธด ๋ฌธ์ž์—ด๋กœ ๋ณ€์‹ ํ•œ ID๊ฐ€ ์šฐ๋ฅด๋ฅด๋ฅด~
๋ฐฐ์น˜ ์ž‘์—…์€ ๋งค์ผ ๋ฐค ์—ด์‹ฌํžˆ ๊ณ„์‚ฐํ•˜๊ณ ,
๋ ˆ๋””์Šค ์บ์‹œ์— ๋žญํ‚น์„ ์˜์˜ฅ! ์ €์žฅํ•˜๋„ค์š”.
๊ตฌ์กฐ ์ •๋ˆ๋œ ์ด๋ฒคํŠธ๋“ค์ด ์ถค์„ ์ถ˜๋‹ค! ๐ŸŽ‰

Pre-merge checks and finishing touches

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

โค๏ธ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

๐ŸŸ  Major comments (15)
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java-40-48 (1)

40-48: ์ข‹์•„์š” ์ทจ์†Œ ์ด๋ฒคํŠธ์˜ ๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•˜์„ธ์š”.

remove() ๋ฉ”์„œ๋“œ๋Š” ์ข‹์•„์š”๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•˜์ง€ ์•Š๊ณ  ๋ฌด์กฐ๊ฑด UNLIKE ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์ข‹์•„์š”๊ฐ€ ์ด๋ฏธ ์‚ญ์ œ๋˜์—ˆ๊ฑฐ๋‚˜ ์• ์ดˆ์— ์กด์žฌํ•˜์ง€ ์•Š์•˜๋˜ ๊ฒฝ์šฐ์—๋„ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋˜์–ด ๋ฐฐ์น˜ ์ง‘๊ณ„ ์‹œ์Šคํ…œ์—์„œ ์Œ์ˆ˜ ์นด์šดํŠธ๋‚˜ ๋ถ€์ •ํ™•ํ•œ ์ง‘๊ณ„๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

LikeFacade์—์„œ ์บ์‹œ ๊ธฐ๋ฐ˜์œผ๋กœ ์ผ์ฐจ์ ์ธ ์ œ์–ด๋ฅผ ํ•˜๊ณ  ์žˆ์ง€๋งŒ, DB ์ƒํƒœ์™€ ์บ์‹œ ๊ฐ„ ๋ถˆ์ผ์น˜ ์ƒํ™ฉ์—์„œ๋Š” ์—ฌ์ „ํžˆ ์ค‘๋ณต ์ด๋ฒคํŠธ ๋ฐœํ–‰์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ ํ›„ ์ด๋ฒคํŠธ ๋ฐœํ–‰ํ•˜๋Š” ์ˆ˜์ •์•ˆ
 @Transactional
 public void remove(Long userId, Long productId) {
-
+  // ์ข‹์•„์š”๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
+  Optional<Like> liked = likeRepository.findById(userId, productId);
+  if (!liked.isPresent()) {
+    log.debug("์ข‹์•„์š”๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์•„ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์ƒ๋žต - ์‚ฌ์šฉ์žID: {}, ์ƒํ’ˆID: {}", userId, productId);
+    return;
+  }
+
   likeRepository.remove(userId, productId);
-
+
   // ์ข‹์•„์š” ์ทจ์†Œ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์šฉ)
   LikeEvent unlikeEvent = new LikeEvent(userId, productId, LikeEvent.LikeAction.UNLIKE);
   eventPublisher.publishEvent(unlikeEvent);
   log.debug("์ข‹์•„์š” ์ทจ์†Œ ์ด๋ฒคํŠธ ๋ฐœํ–‰ - ์‚ฌ์šฉ์žID: {}, ์ƒํ’ˆID: {}", userId, productId);
 }
argo-workflows/weekly-ranking-workflow.yaml-49-68 (1)

49-68: JAVA_OPTS ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์‹ค์ œ๋กœ ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

JAVA_OPTS ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ–ˆ์ง€๋งŒ, java ๋ช…๋ น์–ด์—์„œ ํ•ด๋‹น ๊ฐ’์„ ์ฐธ์กฐํ•˜์ง€ ์•Š์•„ JVM ์˜ต์…˜์ด ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

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

command์™€ args๋ฅผ ์‰˜์„ ํ†ตํ•ด ์‹คํ–‰ํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜, entrypoint ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”:

     container:
       image: your-registry/commerce-batch:latest
-      command: ["java"]
-      args: 
-      - "-jar"
-      - "commerce-batch.jar"
-      - "--spring.batch.job.names=dailyRankingDataProcessingJob"
-      - "--date={{ inputs.parameters.date }}"
+      command: ["/bin/sh", "-c"]
+      args:
+      - |
+        java $JAVA_OPTS -jar commerce-batch.jar \
+          --spring.batch.job.names=dailyRankingDataProcessingJob \
+          --date={{ inputs.parameters.date }}

๋™์ผํ•œ ํŒจํ„ด์„ cache-update-step๊ณผ cache-retry-step์—๋„ ์ ์šฉํ•˜์„ธ์š”.

apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java-10-11 (1)

10-11: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Java ์ฝ”๋“œ์—์„œ orderId๋Š” ์ด๋ฏธ ๋ชจ๋“  ๊ณ„์ธต(Order ์—”ํ‹ฐํ‹ฐ, Payment ์—”ํ‹ฐํ‹ฐ, ์„œ๋น„์Šค, API, ์ด๋ฒคํŠธ)์—์„œ String์œผ๋กœ ์ผ๊ด€๋˜๊ฒŒ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ ๋‹ค์Œ์„ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ: Flyway/Liquibase ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์ด๋‚˜ SQL ์Šคํฌ๋ฆฝํŠธ์—์„œ orders ํ…Œ์ด๋ธ”๊ณผ commerce_payments ํ…Œ์ด๋ธ”์˜ orderId/order_id ์ปฌ๋Ÿผ์„ Long/BIGINT์—์„œ VARCHAR๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.
  • ๊ธฐ์กด ๋ฐ์ดํ„ฐ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ฃผ๋ฌธ ๋ฐ์ดํ„ฐ์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต์ด ์ •์˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.
  • ์ธ๋ฑ์Šค: orderId ์ปฌ๋Ÿผ์˜ unique ์ธ๋ฑ์Šค๊ฐ€ ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ํ›„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์œ ์ง€๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

์ด๋ฒคํŠธ ์ง๋ ฌํ™”, API ํ˜ธํ™˜์„ฑ, ์„ฑ๋Šฅ์€ ์ฝ”๋“œ ์ˆ˜์ค€์—์„œ ์ด๋ฏธ String ํƒ€์ž…์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java-56-58 (1)

56-58: generateOrderId() ๋ฉ”์„œ๋“œ๋Š” ์ถฉ๋Œ ์œ„ํ—˜์ด ์žˆ๋Š” ID ์ƒ์„ฑ ์ „๋žต์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

UUID์˜ ์ฒซ 8์ž๋งŒ ์‚ฌ์šฉํ•˜๋ฉด 32๋น„ํŠธ ์—”ํŠธ๋กœํ”ผ๋งŒ ์ œ๊ณต๋˜์–ด, ์•ฝ 65,000๊ฐœ ์ฃผ๋ฌธ์—์„œ ์ถฉ๋Œ ํ™•๋ฅ ์ด ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์ถฉ๋Œ ์‹œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์œ ๋‹ˆํฌ ์ œ์•ฝ์กฐ๊ฑด์— ์˜ํ•ด DataIntegrityViolationException์ด ๋ฐœ์ƒํ•˜์ง€๋งŒ, ํ˜„์žฌ ์ฝ”๋“œ์—๋Š” ์ด๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง์ด ์—†์–ด ํŠธ๋žœ์žญ์…˜์ด ๋กค๋ฐฑ๋ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ ์˜ต์…˜์„ ๊ณ ๋ คํ•˜์„ธ์š”:

  • ULIDGenerator ์‚ฌ์šฉ (์ด๋ฏธ ์ฝ”๋“œ๋ฒ ์ด์Šค์— ๊ตฌํ˜„๋จ): ํƒ€์ž„์Šคํƒฌํ”„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ถฉ๋Œ ์œ„ํ—˜์„ ๊ฑฐ์˜ ์ œ๊ฑฐ
  • ๋” ๊ธด ID ๊ธธ์ด (์˜ˆ: 12-16์ž)
  • CouponIssue์ฒ˜๋Ÿผ ์ „์ฒด UUID ์‚ฌ์šฉ
apps/commerce-batch/src/main/java/com/loopers/batch/job/DailyRankingJobConfig.java-229-248 (1)

229-248: ํ…Œ์ด๋ธ” ์Šค์™‘ ํ›„ Working ํ…Œ์ด๋ธ” ์žฌ์ƒ์„ฑ ๋ˆ„๋ฝ

ํ…Œ์ด๋ธ” ์Šค์™‘ ํ›„ product_metrics_daily_working ํ…Œ์ด๋ธ”์ด ์กด์žฌํ•˜์ง€ ์•Š๊ฒŒ ๋˜์–ด, ๋‹ค์Œ Job ์‹คํ–‰ ์‹œ Step 1์˜ TRUNCATE TABLE product_metrics_daily_working๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
 try {
   // 1. ์ด์ „ ๋ฐฑ์—… ์‚ญ์ œ
   jdbcTemplate.execute("DROP TABLE IF EXISTS product_metrics_daily_backup");

   // 2. ์›์ž์  ํ…Œ์ด๋ธ” ๊ต์ฒด
   jdbcTemplate.execute("""
           RENAME TABLE 
               product_metrics_daily TO product_metrics_daily_backup,
               product_metrics_daily_working TO product_metrics_daily
       """);

+  // 3. Working ํ…Œ์ด๋ธ” ์žฌ์ƒ์„ฑ (backup ๊ตฌ์กฐ ๋ณต์‚ฌ)
+  jdbcTemplate.execute("CREATE TABLE product_metrics_daily_working LIKE product_metrics_daily");

   log.info("Daily table swap completed successfully");
   return RepeatStatus.FINISHED;
apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java-47-49 (1)

47-49: v1 API์˜ Breaking Change: orderId ํŒŒ๋ผ๋ฏธํ„ฐ ํƒ€์ž… ๋ณ€๊ฒฝ

orderId ๊ฒฝ๋กœ ๋ณ€์ˆ˜์˜ ํƒ€์ž…์ด Long์—์„œ String์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” /api/v1/orders/{orderId} ์—”๋“œํฌ์ธํŠธ์˜ ๊ณ„์•ฝ์„ ๋ณ€๊ฒฝํ•˜๋Š” breaking change์ž…๋‹ˆ๋‹ค.

์ฝ”๋“œ ๊ตฌํ˜„์€ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ฒ˜๋ฆฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค (OrderV1Controller, OrderV1ApiSpec, OrderFacade ๋ชจ๋‘ String ํƒ€์ž… ์‚ฌ์šฉ). ๋‹ค๋งŒ ๋‹ค์Œ ์‚ฌํ•ญ์„ ํ™•์ธํ•˜๊ณ  ์กฐ์น˜ํ•˜์„ธ์š”:

  • API ๋ฒ„์ „ ๊ด€๋ฆฌ: ์ด breaking change๊ฐ€ v1์— ์ ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด ํด๋ผ์ด์–ธํŠธ ํ˜ธํ™˜์„ฑ ๋ฐ ๋ฒ„์ „ ๊ด€๋ฆฌ ์ „๋žต์„ ์žฌ๊ฒ€ํ† ํ•˜์„ธ์š”.
  • ๋ฌธ์„œํ™” ๋ฐ ๋ณ€๊ฒฝ ๋กœ๊ทธ: API ๋ฌธ์„œ์™€ ๋ณ€๊ฒฝ ๋กœ๊ทธ์— ์ด breaking change๋ฅผ ๋ช…์‹œํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ๋ฅผ ์ œ๊ณตํ•˜์„ธ์š”.
  • ํด๋ผ์ด์–ธํŠธ ์˜ํ–ฅ๋„: ๋ชจ๋“  API ์†Œ๋น„์ž(ํ”„๋ก ํŠธ์—”๋“œ, ๋ชจ๋ฐ”์ผ ์•ฑ, ์™ธ๋ถ€ ํ†ตํ•ฉ)์˜ ์—…๋ฐ์ดํŠธ ๊ณ„ํš์„ ํ™•์ธํ•˜์„ธ์š”.
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductMetricsMonthly.java-10-13 (1)

10-13: ์›”๋ณ„ ์—”ํ‹ฐํ‹ฐ์— ๋ณตํ•ฉ ์ธ๋ฑ์Šค ๋ฐ ์œ ๋‹ˆํฌ ์ œ์•ฝ์กฐ๊ฑด ๋ˆ„๋ฝ

ProductMetricsWeekly ์—”ํ‹ฐํ‹ฐ๋Š” (productId, period_yyyyww)์— ๋Œ€ํ•œ ๋ณตํ•ฉ ์ธ๋ฑ์Šค์™€ ์œ ๋‹ˆํฌ ์ œ์•ฝ์กฐ๊ฑด์„ ๊ฐ€์ง€๊ณ  ์žˆ์ง€๋งŒ, ์ด ProductMetricsMonthly ์—”ํ‹ฐํ‹ฐ์—๋Š” ๋™์ผํ•œ ํŒจํ„ด์ด ์ ์šฉ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์ค‘๋ณต ๋ฐ์ดํ„ฐ ์‚ฝ์ž…์ด ๊ฐ€๋Šฅํ•˜๊ณ  ์ฟผ๋ฆฌ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ผ๊ด€์„ฑ์„ ์œ„ํ•œ ์ˆ˜์ • ์ œ์•ˆ
 @Entity
 @Table(name = "product_metrics_monthly_working",
     indexes = {
         @Index(name = "idx_monthly_period_yyyymm", columnList = "period_yyyymm"),
+        @Index(name = "idx_monthly_product_month", columnList = "productId, period_yyyymm")
+    },
+    uniqueConstraints = {
+        @UniqueConstraint(name = "uk_product_month", columnNames = {"productId", "period_yyyymm"})
     })
apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java-22-26 (1)

22-26: ๋‚ ์งœ ๋ฒ”์œ„ ์ƒํ•œ ์กฐ๊ฑด ๋ˆ„๋ฝ

WHERE pm.bucket_time_key >= :startTimeKey๋งŒ ์‚ฌ์šฉํ•˜๋ฉด ์‹œ์ž‘ ์‹œ์  ์ดํ›„์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ์ฃผ๊ฐ„/์›”๊ฐ„ ์ง‘๊ณ„๋ฅผ ์ •ํ™•ํžˆ ํ•˜๋ ค๋ฉด ์ƒํ•œ ์กฐ๊ฑด(bucket_time_key < :endTimeKey ๋˜๋Š” BETWEEN)์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
     @Query(value = """
         SELECT 
             pm.product_id as productId,
             COALESCE(SUM(pm.like_count), 0) as likeCount,
             COALESCE(SUM(pm.sales_revenue), 0) as orderCount,
             COALESCE(SUM(pm.view_count), 0) as viewCount,
             :period as yearMonthWeek
         FROM product_metrics pm
-        WHERE pm.bucket_time_key >= :startTimeKey
+        WHERE pm.bucket_time_key >= :startTimeKey AND pm.bucket_time_key < :endTimeKey
         GROUP BY pm.product_id
         ORDER BY pm.product_id
         """, nativeQuery = true)
-    Page<ProductMetricsWeekly> findWeeklyMetrics(@Param("startTimeKey") String startTimeKey, @Param("period") String period, Pageable pageable);
+    Page<ProductMetricsWeekly> findWeeklyMetrics(@Param("startTimeKey") String startTimeKey, @Param("endTimeKey") String endTimeKey, @Param("period") String period, Pageable pageable);

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

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

15-27: sales_revenue๋ฅผ orderCount๋กœ ๋งคํ•‘ํ•˜๋Š” ๊ฒƒ์€ ์˜๋ฏธ์  ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค.

์ฟผ๋ฆฌ์—์„œ SUM(pm.sales_revenue) (๋งค์ถœ์•ก)๋ฅผ orderCount๋กœ aliasํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ProductMetricsWeekly์™€ ProductMetricsMonthly ์—”ํ‹ฐํ‹ฐ์˜ orderCount ํ•„๋“œ๋Š” ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰(Integer)์„ ๋‚˜ํƒ€๋‚ด์•ผ ํ•˜๋ฏ€๋กœ, ๋งค์ถœ์•ก(revenue)๊ณผ ์˜๋ฏธ์ ์œผ๋กœ ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

product_metrics ํ…Œ์ด๋ธ”์—๋Š” salesRevenue ์ปฌ๋Ÿผ๋งŒ ์žˆ๊ณ  orderCount ์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ์ฃผ๋ฌธ ๊ฑด์ˆ˜๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ๋ณ„๋„์˜ order_count ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ํ•„๋“œ๋ช… ๋ฐ ์‚ฌ์šฉ์ฒ˜๋ฅผ ์žฌ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋Š” findWeeklyMetrics(19์ค„)๊ณผ findMonthlyMetrics(33์ค„) ๋‘ ์ฟผ๋ฆฌ์— ๋ชจ๋‘ ์˜ํ–ฅ์„ ๋ฏธ์นฉ๋‹ˆ๋‹ค.

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

10-17: @Index columnList์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปฌ๋Ÿผ๋ช… ์‚ฌ์šฉ ํ•„์š”

JPA ๋ช…์„ธ์— ๋”ฐ๋ฅด๋ฉด @Index์˜ columnList๋Š” ๋ฐ˜๋“œ์‹œ ๋ฌผ๋ฆฌ์  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปฌ๋Ÿผ๋ช…์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ฝ”๋“œ๋Š” Java ํ•„๋“œ๋ช…(productId, bucketTimeKey)์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”๋ฐ, @Column ์• ๋„ˆํ…Œ์ด์…˜์— name ์†์„ฑ์ด ์—†์œผ๋ฏ€๋กœ Spring Data์˜ ๋ช…๋ช… ๊ทœ์น™(camelCase โ†’ snake_case)์— ์˜ํ•ด product_id, bucket_time_key๋กœ ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•˜์„ธ์š”:

์ˆ˜์ • ๋ฐฉ์•ˆ
@Index(name = "idx_product_bucket", columnList = "product_id, bucket_time_key"),
@Index(name = "idx_bucket_time", columnList = "bucket_time_key")

๋˜๋Š” ํ•„๋“œ์— ๋ช…์‹œ์ ์œผ๋กœ ์ปฌ๋Ÿผ๋ช…์„ ์ง€์ •:

@Column(name = "productId", nullable = false)
private Long productId;

@Column(name = "bucketTimeKey", nullable = false)
private String bucketTimeKey;

ํ˜„์žฌ ์„ค์ •์œผ๋กœ๋Š” ์ธ๋ฑ์Šค๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ƒ์„ฑ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

83-88: Locale ์˜์กด์  ์ฃผ์ฐจ ๊ณ„์‚ฐ์œผ๋กœ ์ธํ•œ ๋ถˆ์ผ์น˜ ๊ฐ€๋Šฅ์„ฑ

WeekFields.of(Locale.getDefault())๋Š” ์„œ๋ฒ„ ๋กœ์ผ€์ผ์— ๋”ฐ๋ผ ์ฃผ์ฐจ ๊ณ„์‚ฐ์ด ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ฏธ๊ตญ(์ผ์š”์ผ ์‹œ์ž‘)๊ณผ ํ•œ๊ตญ/์œ ๋Ÿฝ(์›”์š”์ผ ์‹œ์ž‘)์—์„œ ๋™์ผ ๋‚ ์งœ์˜ ์ฃผ์ฐจ๊ฐ€ ๋‹ค๋ฅด๊ฒŒ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •์•ˆ: ISO ์ฃผ์ฐจ ์‚ฌ์šฉ
 private String convertDateToYearMonthWeek(String date) {
     LocalDate localDate = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyyMMdd"));
-    WeekFields weekFields = WeekFields.of(Locale.getDefault());
+    WeekFields weekFields = WeekFields.ISO; // ISO-8601: ์›”์š”์ผ ์‹œ์ž‘, ์ฒซ ์ฃผ๋Š” ์ตœ์†Œ 4์ผ ํฌํ•จ
     int weekOfYear = localDate.get(weekFields.weekOfYear());
     return localDate.format(DateTimeFormatter.ofPattern("yyyy")) + String.format("%02d", weekOfYear);
 }
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingDateParser.java-147-171 (1)

147-171: ์ฃผ์ฐจโ†’๋‚ ์งœ ๋ณ€ํ™˜ ๋กœ์ง์ด ISO ์ฃผ์ฐจ ํ‘œ์ค€๊ณผ ๋‹ค๋ฆ„

convertWeekToDate์˜ ๊ณ„์‚ฐ์‹ (week - 1) * 7 + 4๋Š” 1์›” 1์ผ์ด ์ฒซ์งธ ์ฃผ ์‹œ์ž‘์ด๋ผ๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค. ISO-8601 ์ฃผ์ฐจ๋Š” 1์›” 4์ผ์„ ํฌํ•จํ•˜๋Š” ์ฃผ๋ฅผ ์ฒซ์งธ ์ฃผ๋กœ ์ •์˜ํ•˜๋ฏ€๋กœ, ์—ฐ์ดˆ/์—ฐ๋ง ๊ฒฝ๊ณ„ ์ฃผ์ฐจ์—์„œ ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •์•ˆ: Java Time API ํ™œ์šฉ
+import java.time.LocalDate;
+import java.time.temporal.WeekFields;
+
 private static String convertWeekToDate(int year, int week) {
-    // ํ•ด๋‹น ์ฃผ์ฐจ์˜ ๋Œ€ํ‘œ ๋‚ ์งœ ๊ณ„์‚ฐ (๊ฐ„๋‹จํ•œ ๊ทผ์‚ฌ์น˜)
-    // 1์›” ์ฒซ์งธ ์ฃผ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐ
-    int dayOfYear = (week - 1) * 7 + 4; // ํ•ด๋‹น ์ฃผ์˜ ๋ชฉ์š”์ผ
-    
-    // ์œค๋…„ ๊ณ ๋ ค
-    int[] monthDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
-    if (isLeapYear(year)) {
-        monthDays[1] = 29;
-    }
-    
-    int month = 1;
-    while (month <= 12 && dayOfYear > monthDays[month - 1]) {
-        dayOfYear -= monthDays[month - 1];
-        month++;
-    }
-    
-    // ๋ฒ”์œ„ ์ดˆ๊ณผ ์‹œ 12์›” ๋งˆ์ง€๋ง‰ ๋‚ ๋กœ ์กฐ์ •
-    if (month > 12) {
-        month = 12;
-        dayOfYear = monthDays[11];
-    }
-    
-    return String.format("%04d%02d%02d", year, month, dayOfYear);
+    // ISO-8601 ์ฃผ์ฐจ ๊ธฐ์ค€์œผ๋กœ ํ•ด๋‹น ์ฃผ์˜ ๋ชฉ์š”์ผ ๋ฐ˜ํ™˜
+    LocalDate date = LocalDate.of(year, 1, 4) // 1์›” 4์ผ์€ ํ•ญ์ƒ ์ฒซ์งธ ์ฃผ์— ํฌํ•จ
+        .with(WeekFields.ISO.weekOfWeekBasedYear(), week)
+        .with(WeekFields.ISO.dayOfWeek(), 4); // ๋ชฉ์š”์ผ
+    return date.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
 }
apps/commerce-api/src/main/java/com/loopers/application/event/DataTransferEventHandler.java-29-29 (1)

29-29: ์—”ํ‹ฐํ‹ฐ ์กฐํšŒ ์‹œ null ์ฒดํฌ๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ paymentService.findPaymentByOrderId() ๋ฐ orderService.getOrder()๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ์ง€๋งŒ, ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ์— ๋Œ€ํ•œ null ์ฒดํฌ๋‚˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ํŠธ๋žœ์žญ์…˜ ํƒ€์ด๋ฐ ์ด์Šˆ๋‚˜ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜๋กœ ์ธํ•ด ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ฐพ์ง€ ๋ชปํ•˜๋ฉด NPE๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์˜ˆ์‹œ
 Payment payment = paymentService.findPaymentByOrderId(event.orderId());
+if (payment == null) {
+    log.error("Payment not found for orderId: {}", event.orderId());
+    return;
+}

Also applies to: 59-59, 79-79, 96-96

apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductMetricsWeekly.java-1-49 (1)

1-49: ์ค‘๋ณต๋œ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค๋ฅผ ํ†ตํ•ฉํ•ด์ฃผ์„ธ์š”.

ProductMetricsWeekly ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์—ฌ๋Ÿฌ ๋ชจ๋“ˆ๊ณผ ํŒจํ‚ค์ง€์— ์ค‘๋ณต ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  1. apps/commerce-batch/.../domain/ranking/ProductMetricsWeekly.java (ํ…Œ์ด๋ธ”: product_metrics_weekly_working)
  2. apps/commerce-api/.../domain/ranking/ProductMetricsWeekly.java (ํ…Œ์ด๋ธ”: product_metrics_weekly)
  3. apps/commerce-api/.../domain/metrics/ProductMetricsWeekly.java (ํ…Œ์ด๋ธ”: product_metrics_weekly)

์ด๋Ÿฌํ•œ ์ค‘๋ณต์€ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ์ €ํ•ดํ•˜๊ณ  ๋ถˆ์ผ์น˜๋ฅผ ์•ผ๊ธฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ด๋ธ” ์ด๋ฆ„์ด ๋‹ค๋ฅธ ๊ฒƒ์€ ์˜๋„์ ์ผ ์ˆ˜ ์žˆ์œผ๋‚˜(working vs production), ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค๋Š” ๊ณตํ†ต ๋ชจ๋“ˆ๋กœ ์ถ”์ถœํ•˜๊ฑฐ๋‚˜ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

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

102-103: ์Šค์ฝ”์–ด ๊ณ„์‚ฐ ๊ฐ€์ค‘์น˜๊ฐ€ ๋ฐฐ์น˜ ์ž‘์—…๊ณผ ๋ถˆ์ผ์น˜

์ด ์„œ๋น„์Šค์˜ ๊ฐ€์ค‘์น˜(like*3.0 + order*10.0 + view*1.0)๊ฐ€ WeeklyRankingJobConfig์™€ MonthlyRankingJobConfig์˜ ๊ฐ€์ค‘์น˜(view*0.1 + like*0.2 + order*0.6)์™€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ๋™์ผํ•œ ๋žญํ‚น ๊ธฐ์ค€์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ

๊ฐ€์ค‘์น˜๋ฅผ ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜์„ธ์š”:

// ๊ณตํ†ต ์ƒ์ˆ˜ ํด๋ž˜์Šค ๋˜๋Š” ์„ค์ •
public static final double VIEW_WEIGHT = 0.1;
public static final double LIKE_WEIGHT = 0.2;
public static final double ORDER_WEIGHT = 0.6;
-        double score = metric.getLikeCount() * 3.0 + metric.getOrderCount() * 10.0 + metric.getViewCount() * 1.0;
+        double score = metric.getViewCount() * VIEW_WEIGHT + metric.getLikeCount() * LIKE_WEIGHT + metric.getOrderCount() * ORDER_WEIGHT;

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

๐ŸŸก Minor comments (9)
apps/commerce-batch/src/main/resources/application.yml-63-82 (1)

63-82: ํ…Œ์ŠคํŠธ ํ”„๋กœํŒŒ์ผ์— format_sql ์„ค์ • ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”.

๋กœ์ปฌ ํ”„๋กœํŒŒ์ผ์—๋Š” format_sql: true๊ฐ€ ์žˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ ํ”„๋กœํŒŒ์ผ์—๋Š” ๋ˆ„๋ฝ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋„ SQL ๋””๋ฒ„๊น…์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
   jpa:
     show-sql: true
     hibernate:
       ddl-auto: none
     properties:
       hibernate:
+        format_sql: true
         jdbc:
           batch_size: 1000
argo-workflows/weekly-ranking-workflow.yaml-1-5 (1)

1-5: ํŒŒ์ผ๋ช…๊ณผ ๋‚ด์šฉ์ด ๋ถˆ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

ํŒŒ์ผ๋ช…์€ weekly-ranking-workflow.yaml์ด์ง€๋งŒ ์‹ค์ œ ๋‚ด์šฉ์€ daily-ranking-workflow์™€ daily-ranking-cron์„ ์ •์˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ์œ„ํ•ด ํŒŒ์ผ๋ช…์„ daily-ranking-workflow.yaml๋กœ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜, ์ฃผ๊ฐ„ ๋žญํ‚น ์›Œํฌํ”Œ๋กœ์šฐ ๋‚ด์šฉ์œผ๋กœ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductMetricsDaily.java-25-32 (1)

25-32: ํ•„๋“œ ๊ธฐ๋ณธ๊ฐ’์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ˆ˜์ค€์—์„œ๋Š” ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์—”ํ‹ฐํ‹ฐ ํ•„๋“œ์— = 0 ๊ธฐ๋ณธ๊ฐ’์„ ์„ค์ •ํ–ˆ์ง€๋งŒ, ์ด๋Š” Java ๊ฐ์ฒด ๋ ˆ๋ฒจ์—์„œ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. SQL์„ ํ†ตํ•ด ์ง์ ‘ ์‚ฝ์ž…๋˜๋Š” ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—๋Š” ๊ธฐ๋ณธ๊ฐ’์ด ์—†์–ด NULL์ด ์ €์žฅ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์„ ๊ณ ๋ คํ•˜์„ธ์š”:

  • JPA @Column ์–ด๋…ธํ…Œ์ด์…˜์— columnDefinition = "INTEGER DEFAULT 0" ์ถ”๊ฐ€
  • ๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ์—์„œ DEFAULT ์ œ์•ฝ์กฐ๊ฑด ๋ช…์‹œ
๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
-@Column(nullable = false)
+@Column(nullable = false, columnDefinition = "INTEGER DEFAULT 0")
 private Integer likeCount = 0;

-@Column(nullable = false)
+@Column(nullable = false, columnDefinition = "INTEGER DEFAULT 0")
 private Integer orderCount = 0;

-@Column(nullable = false)
+@Column(nullable = false, columnDefinition = "INTEGER DEFAULT 0")
 private Integer viewCount = 0;
apps/commerce-batch/src/main/java/com/loopers/batch/runner/BatchJobRunner.java-49-52 (1)

49-52: ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ ์˜ค๋ฅ˜ ์ˆ˜์ • ํ•„์š”

Line 49์˜ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๊ฐ€ "Batch job completed"๋กœ ๋˜์–ด ์žˆ์œผ๋‚˜, ์ด ์‹œ์ ์—์„œ๋Š” ์•„์ง job์ด ์‹คํ–‰๋˜๊ธฐ ์ „์ž…๋‹ˆ๋‹ค. ์˜๋„ํ•œ ๋‚ด์šฉ์€ job parameters ์ถœ๋ ฅ์ธ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
-    log.info("Batch job completed: {}", builder.toJobParameters().toString());
+    log.info("Job parameters: {}", builder.toJobParameters().toString());
     rankingBatchScheduler.run(jobKey, builder.toJobParameters());

     log.info("Batch job completed: {}", jobKey);
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java-9-45 (1)

9-45: ์—”ํ‹ฐํ‹ฐ ์ฝ”๋“œ ์ค‘๋ณต: ์„œ๋กœ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์„ ๋งคํ•‘ํ•˜๋‚˜ ๊ตฌ์กฐ ์žฌ์‚ฌ์šฉ ๊ฒ€ํ†  ํ•„์š”

commerce-batch ๋ชจ๋“ˆ์˜ ProductMetricsMonthly์™€ ์ฝ”๋“œ ๊ตฌ์กฐ๊ฐ€ ์™„์ „ํžˆ ๋™์ผํ•˜๋‚˜, ์„œ๋กœ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์„ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค:

  • commerce-batch: product_metrics_monthly_working (์ค‘๊ฐ„ ์ž‘์—… ํ…Œ์ด๋ธ”)
  • commerce-api: product_metrics_monthly (์ตœ์ข… ๋ฉ”ํŠธ๋ฆญ ํ…Œ์ด๋ธ”)

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

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

116-139: ์ผ๊ฐ„ ๋‚ ์งœ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ถˆ์™„์ „: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋‚ ์งœ ํ—ˆ์šฉ

validateDailyDate์—์„œ ์ผ(day)์„ 1-31 ๋ฒ”์œ„๋กœ๋งŒ ๊ฒ€์ฆํ•˜์—ฌ 20250231(2์›” 31์ผ) ๊ฐ™์€ ์œ ํšจํ•˜์ง€ ์•Š์€ ๋‚ ์งœ๊ฐ€ ํ†ต๊ณผ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •์•ˆ: LocalDate ํŒŒ์‹ฑ์œผ๋กœ ๊ฒ€์ฆ
 private static void validateDailyDate(String date) {
     try {
-        String year = date.substring(0, 4);
-        String month = date.substring(4, 6);
-        String day = date.substring(6, 8);
-        
-        int yearInt = Integer.parseInt(year);
-        int monthInt = Integer.parseInt(month);
-        int dayInt = Integer.parseInt(day);
-        
-        validateYear(yearInt);
-        
-        if (monthInt < 1 || monthInt > 12) {
-            throw new IllegalArgumentException("์œ ํšจํ•˜์ง€ ์•Š์€ ์›”์ž…๋‹ˆ๋‹ค: " + monthInt);
-        }
-        
-        if (dayInt < 1 || dayInt > 31) {
-            throw new IllegalArgumentException("์œ ํšจํ•˜์ง€ ์•Š์€ ์ผ์ž…๋‹ˆ๋‹ค: " + dayInt);
-        }
-        
-    } catch (NumberFormatException e) {
+        LocalDate parsedDate = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyyMMdd"));
+        validateYear(parsedDate.getYear());
+    } catch (DateTimeParseException e) {
         throw new IllegalArgumentException("์ผ๊ฐ„ ๋‚ ์งœ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค: " + date);
     }
 }
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java-286-288 (1)

286-288: ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์นด์šดํŠธ ํด๋ฐฑ ์‹œ 0 ๋ฐ˜ํ™˜์œผ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ •๋ณด ๋ถˆ์ผ์น˜

MV๊ฐ€ ์—†์„ ๋•Œ getTotalWeeklyRankingCountWithMV์™€ getTotalMonthlyRankingCountWithMV ๋ฉ”์„œ๋“œ๋Š” 0L์„ ๋ฐ˜ํ™˜ํ•˜์ง€๋งŒ, ๋ฐ์ดํ„ฐ ์กฐํšŒ ๋ฉ”์„œ๋“œ(getWeeklyRankingProductIdsWithMV, getMonthlyRankingProductIdsWithMV)๋Š” ProductMetricsDailyService๋กœ ํด๋ฐฑํ•˜์—ฌ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฐ›๋Š” ์ƒํ’ˆ ๋ฐ์ดํ„ฐ์™€ ์ด ๊ฐœ์ˆ˜๊ฐ€ ๋ถˆ์ผ์น˜ํ•˜์—ฌ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ •๋ณด๊ฐ€ ์ •ํ™•ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ProductMetricsDailyRepository์˜ findWeeklyAggregateByDateRange์™€ findMonthlyAggregateByYearMonth ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ์—์„œ Page.getTotalElements()๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์นด์šดํŠธ ํด๋ฐฑ ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

44-44: ๋””๋ฒ„๊ทธ์šฉ System.out.print ๋ฌธ ์ œ๊ฑฐ ํ•„์š”

ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์— System.out.print ๋ฌธ์ด ๋‚จ์•„ ์žˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊น…์ด ํ•„์š”ํ•˜๋ฉด ์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ log.info()๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
     log.info("Week range: {} to {}", startDate, endDate);
-    System.out.print(startDate + endDate);

     // ProductMetricsDaily์—์„œ ์ฃผ๊ฐ„ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ
     Page<ProductMetricsWeekly> weeklyMetrics = productMetricsDailyRepository
         .findWeeklyAggregateByDateRange(startDate, endDate, yearMonthWeek, Pageable.unpaged());
-    System.out.print(weeklyMetrics.getTotalElements());
+    log.debug("Weekly metrics count: {}", weeklyMetrics.getTotalElements());

Also applies to: 49-49

apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java-40-54 (1)

40-54: getOrder์™€ getOrderByOrderId ๊ฐ„ ์ผ๊ด€์„ฑ ์—†๋Š” ์—๋Ÿฌ ์ฒ˜๋ฆฌ

getOrder๋Š” ์ฃผ๋ฌธ์„ ์ฐพ์ง€ ๋ชปํ•˜๋ฉด null์„ ๋ฐ˜ํ™˜ํ•˜๊ณ , getOrderByOrderId๋Š” ์˜ˆ์™ธ๋ฅผ ๋˜์ง‘๋‹ˆ๋‹ค. ๋™์ผํ•œ ์กฐํšŒ ๋กœ์ง์— ๋Œ€ํ•ด ์ผ๊ด€๋œ ๋™์ž‘์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
  @Transactional(readOnly = true)
  public Order getOrder(String id) {
    if (id == null) {
      throw new CoreException(ErrorType.BAD_REQUEST, "ID๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.");
    }
-    return orderRepository.findByOrderId(id).orElse(null);
+    return orderRepository.findByOrderId(id)
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: " + id));
  }

๋˜๋Š” getOrderByOrderId๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  getOrder๋กœ ํ†ตํ•ฉํ•˜์„ธ์š”.

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

๐Ÿงน Nitpick comments (31)
apps/commerce-batch/src/main/resources/application.yml (3)

1-2: ํ™˜๊ฒฝ๋ณ„ ํฌํŠธ ์„ค์ •์„ ๊ณ ๋ คํ•˜์„ธ์š”.

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


28-35: ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์˜ ํ—ฌ์Šค ์ฒดํฌ ์ƒ์„ธ ์ •๋ณด ๋…ธ์ถœ์„ ๊ฒ€ํ† ํ•˜์„ธ์š”.

show-details: always๋Š” ๋ชจ๋“  ํ™˜๊ฒฝ์—์„œ ํ—ฌ์Šค ์ฒดํฌ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” when-authorized ๋˜๋Š” ํ”„๋กœํŒŒ์ผ๋ณ„ ์„ค์ •์„ ๊ณ ๋ คํ•˜์—ฌ ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋…ธ์ถœ์„ ์ œํ•œํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.


11-15: ๋ฐฐ์น˜ ์„ค์ •์ด ๋žญํ‚น ์‹œ์Šคํ…œ ์š”๊ตฌ์‚ฌํ•ญ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

PR ๋ชฉํ‘œ์—์„œ ์„ค๋ช…ํ•œ ๊ณ„์ธต์  ์ง‘๊ณ„ ํŒŒ์ดํ”„๋ผ์ธ(10๋ถ„ ์›์‹œ ๋ฉ”ํŠธ๋ฆญ โ†’ ์ผ์ผ ์ง‘๊ณ„ โ†’ ์ฃผ๊ฐ„/์›”๊ฐ„ MV)์„ ์ง€์›ํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฐ์น˜ ์„ค์ •์ด ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • job.enabled: false๋กœ ์ˆ˜๋™ ์‹คํ–‰ ์ œ์–ด
  • Hibernate ๋ฐฐ์น˜ ์ตœ์ ํ™”(batch_size: 1000, order_inserts/updates)๋กœ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ ํ™•๋ณด

๋‹ค๋งŒ, ์ด๋Ÿฌํ•œ ์ตœ์ ํ™” ์„ค์ •์ด dev/qa/prd ํ™˜๊ฒฝ์—๋„ ์ ์šฉ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ถ”๊ฐ€ ๊ณ ๋ ค์‚ฌํ•ญ:

  • ๋ฐฐ์น˜ ์ž‘์—…์˜ ๋™์‹œ์„ฑ ์ œ์–ด๋ฅผ ์œ„ํ•ด ๋ถ„์‚ฐ ๋ฝ(Redis ๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ธฐ๋ฐ˜) ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•˜์„ธ์š”.
  • ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ ์ฒญํฌ ํฌ๊ธฐ(chunk)์™€ Hibernate batch_size๋ฅผ ํ•จ๊ป˜ ํŠœ๋‹ํ•˜์„ธ์š”.
  • PR์—์„œ ์–ธ๊ธ‰ํ•œ Argo Workflows ์‚ฌ์šฉ ์‹œ, ์ž‘์—… ์žฌ์‹œ์ž‘์„ฑ(idempotency)์„ ๋ณด์žฅํ•˜๋„๋ก ์„ค๊ณ„ํ•˜์„ธ์š”.

Also applies to: 54-61

apps/commerce-batch/src/main/java/com/loopers/batch/scheduler/RankingBatchScheduler.java (2)

7-9: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” import ์ œ๊ฑฐ ๊ถŒ์žฅ

JobParametersBuilder(Line 7)์™€ @Qualifier(Line 9)๋ฅผ importํ•˜์ง€๋งŒ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •์‚ฌํ•ญ
-import org.springframework.batch.core.JobParametersBuilder;
 import org.springframework.batch.core.launch.JobLauncher;
-import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Component;

22-34: ๋ฐฐ์น˜ ์ž‘์—… ์‹คํ–‰ ๋กœ๊น… ์ถ”๊ฐ€ ๊ถŒ์žฅ

๋ฐฐ์น˜ ์ž‘์—…์˜ ์‹œ์ž‘, ์™„๋ฃŒ, ์‹คํŒจ๋ฅผ ๋กœ๊น…ํ•˜๋ฉด ์šด์˜ ์ค‘ ๋ฌธ์ œ ์ถ”์ ๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง์ด ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค. @Slf4j๋ฅผ ์ด๋ฏธ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ ๋กœ๊ทธ ์ถ”๊ฐ€๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •์‚ฌํ•ญ
 public void run(String jobKey, JobParameters params) {
+  log.info("Starting batch job: {}, params: {}", jobKey, params);
   Job job = jobs.get(jobKey);
 
   if (job == null) {
+    log.error("Job not found: {}", jobKey);
     throw new IllegalArgumentException("No such job: " + jobKey);
   }
 
   try {
-    jobLauncher.run(job, params);
+    var execution = jobLauncher.run(job, params);
+    log.info("Batch job completed: {}, status: {}", jobKey, execution.getStatus());
   } catch (Exception e) {
+    log.error("Failed to run batch job: {}", jobKey, e);
     throw new RuntimeException("Failed to run job: " + jobKey, e);
   }
 }
apps/commerce-api/src/main/java/com/loopers/application/event/PaymentSuccessEvent.java (1)

1-12: ์ด๋ฒคํŠธ ๋ ˆ์ฝ”๋“œ ๊ตฌํ˜„ ์Šน์ธ

PaymentSuccessEvent ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. String ํƒ€์ž…์˜ orderId๋Š” PR์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๊ณผ ์ผ๊ด€๋˜๋ฉฐ, ๋ถˆ๋ณ€์„ฑ์ด ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค. ์„ ํƒ์ ์œผ๋กœ ์ด ์ด๋ฒคํŠธ๊ฐ€ ์–ธ์ œ ๋ฐœํ–‰๋˜๋Š”์ง€ ๋ช…์‹œํ•˜๋Š” JavaDoc์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ฐ€๋…์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

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

1-11: ์ด๋ฒคํŠธ ๋ ˆ์ฝ”๋“œ ๊ตฌํ˜„ ์Šน์ธ, ์„ ํƒ์  ๊ฐœ์„ ์‚ฌํ•ญ ์ œ์•ˆ

PaymentFailureEvent ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. String ํƒ€์ž…์˜ orderId๋Š” PR์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๊ณผ ์ผ๊ด€๋˜๋ฉฐ ๋ถˆ๋ณ€์„ฑ์ด ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค.

์„ ํƒ์ ์œผ๋กœ reason ํ•„๋“œ๋ฅผ String ๋Œ€์‹  enum์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด ํƒ€์ž… ์•ˆ์ •์„ฑ์ด ํ–ฅ์ƒ๋˜๊ณ  ์‹คํŒจ ์‚ฌ์œ ๋ฅผ ํ‘œ์ค€ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

56-65: ๊ฐ€์ค‘์น˜ ์ƒ์ˆ˜๋ฅผ ๊ณต์œ  ์œ„์น˜๋กœ ์ถ”์ถœํ•˜์„ธ์š”.

MvProductRankMonthly.java์™€ ๋™์ผํ•œ ๊ฐ€์ค‘์น˜ ์ƒ์ˆ˜(VIEW_WEIGHT=0.1, LIKE_WEIGHT=0.2, ORDER_WEIGHT=0.6)์™€ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง์ด ์ค‘๋ณต๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์„ ๊ณ ๋ คํ•˜์„ธ์š”:

  • ๊ฐ€์ค‘์น˜ ์ƒ์ˆ˜๋ฅผ ๊ณต์œ  ์„ค์ • ํด๋ž˜์Šค๋กœ ์ถ”์ถœ (์˜ˆ: RankingWeightConfig)
  • ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง์„ ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฉ”์„œ๋“œ๋กœ ์ถ”์ถœ
  • ํ–ฅํ›„ ๊ฐ€์ค‘์น˜ ๋ณ€๊ฒฝ ์‹œ ๋‹จ์ผ ์ง€์ ์—์„œ ์ˆ˜์ • ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ฐœ์„ 
๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ๋ฆฌํŒฉํ† ๋ง

๊ณต์œ  ์„ค์ • ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”:

public class RankingScoreCalculator {
    private static final double VIEW_WEIGHT = 0.1;
    private static final double LIKE_WEIGHT = 0.2;
    private static final double ORDER_WEIGHT = 0.6;
    
    public static double calculateScore(int viewCount, int likeCount, int orderCount) {
        return (VIEW_WEIGHT * viewCount) + (LIKE_WEIGHT * likeCount) + (ORDER_WEIGHT * orderCount);
    }
}

๊ทธ ํ›„ create() ๋ฉ”์„œ๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์„ธ์š”:

 public static MvProductRankWeekly create(Long productId, String yearMonthWeek, Integer ranking,
                                        Integer likeCount, Integer orderCount, Integer viewCount) {
-    final double VIEW_WEIGHT = 0.1;
-    final double LIKE_WEIGHT = 0.2;
-    final double ORDER_WEIGHT = 0.6;
-    
-    double score = (VIEW_WEIGHT * viewCount) + (LIKE_WEIGHT * likeCount) + (ORDER_WEIGHT * orderCount);
+    double score = RankingScoreCalculator.calculateScore(viewCount, likeCount, orderCount);
     
     return new MvProductRankWeekly(productId, yearMonthWeek, ranking, score, likeCount, orderCount, viewCount);
 }

MvProductRankMonthly์—๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉํ•˜์„ธ์š”.

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

26-26: ๋ฏธ์‚ฌ์šฉ ์˜์กด์„ฑ eventPublisher

OrderDataTransferEvent ๋ฐœํ–‰ ๋กœ์ง์ด ์ œ๊ฑฐ๋˜๋ฉด์„œ ApplicationEventPublisher eventPublisher๊ฐ€ ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
 @RequiredArgsConstructor
 @Component
 public class CouponEventHandler {
   private final CouponService couponService;
   private final PgClient pgClient;
   private final OrderService orderService;
-  private final ApplicationEventPublisher eventPublisher;

ApplicationEventPublisher import๋„ ํ•จ๊ป˜ ์ œ๊ฑฐํ•ด ์ฃผ์„ธ์š”.

apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java (1)

14-15: orderId ํƒ€์ž… ๋ณ€๊ฒฝ์ด ์ผ๊ด€๋˜๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•„๋“œ, ์ƒ์„ฑ์ž, ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ๋ชจ๋‘ String ํƒ€์ž…์œผ๋กœ ์ผ๊ด€๋˜๊ฒŒ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. PR ์ „๋ฐ˜์˜ Long โ†’ String ์ „ํ™˜๊ณผ ์ž˜ ๋งž์Šต๋‹ˆ๋‹ค.

์„ ํƒ์ ์œผ๋กœ, create() ๋ฉ”์„œ๋“œ์—์„œ orderId์— ๋Œ€ํ•œ null/๋นˆ ๋ฌธ์ž์—ด ๊ฒ€์ฆ ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”. ํ˜„์žฌ๋Š” nullable=false ์ œ์•ฝ๋งŒ DB ๋ ˆ๋ฒจ์—์„œ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

Also applies to: 39-39, 49-51

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

6-6: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” import์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

totalAmount๊ฐ€ BigDecimal์—์„œ Money๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์œผ๋ฏ€๋กœ, java.math.BigDecimal import๊ฐ€ ๋” ์ด์ƒ ํ•„์š”ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/payment/PgPayRequest.java (1)

12-14: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ฝœ๋ฐฑ URL์€ ํ™˜๊ฒฝ ์„ค์ •์œผ๋กœ ์™ธ๋ถ€ํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

localhost:8080 URL์ด ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์–ด ๋กœ์ปฌ ํ™˜๊ฒฝ ์™ธ์—์„œ๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฐ’์€ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋‚˜ application.yml ์„ค์ •์œผ๋กœ ์ฃผ์ž…๋ฐ›๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์„ค์ • ์™ธ๋ถ€ํ™” ์˜ˆ์‹œ
// application.yml์—์„œ ์ฃผ์ž…๋ฐ›๋Š” ๋ฐฉ์‹ ์˜ˆ์‹œ
// @Value("${payment.callback.url}") ๋˜๋Š” ConfigurationProperties ํ™œ์šฉ
apps/commerce-batch/src/main/java/com/loopers/config/BatchJobProperties.java (1)

9-16: ์„ ํƒ์  ๊ฐœ์„ : jobName์— ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ถ”๊ฐ€ ๊ฒ€ํ† 

BatchJobProperties๋Š” CommerceBatchApplication์—์„œ @EnableConfigurationProperties(BatchJobProperties.class)๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋“ฑ๋ก๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค๋งŒ, jobName ์†์„ฑ์— ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์ž˜๋ชป๋œ ์„ค์ •๊ฐ’์œผ๋กœ ์ธํ•œ ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜๋ฅผ ์กฐ๊ธฐ์— ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, @Pattern ๋˜๋Š” ์ปค์Šคํ…€ @Validated ๊ฒ€์ฆ์„ ํ†ตํ•ด ํ—ˆ์šฉ๋˜๋Š” ๋ฐฐ์น˜ ์žก ์ด๋ฆ„(dailyRankingDataProcessingJob, weeklyRankingMVUpdateJob, monthlyRankingMVUpdateJob)๋งŒ ์ˆ˜์šฉํ•˜๋„๋ก ์ œํ•œํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

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

56-58: null ์ž…๋ ฅ์— ๋Œ€ํ•œ ๋ฐฉ์–ด ์ฝ”๋“œ ์ถ”๊ฐ€ ๊ถŒ์žฅ

incrementSalesRevenue(Long revenue) ๋ฉ”์„œ๋“œ์—์„œ revenue๊ฐ€ null์ธ ๊ฒฝ์šฐ NullPointerException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
   public void incrementSalesRevenue(Long revenue) {
+    if (revenue == null || revenue <= 0) {
+      return;
+    }
     this.salesRevenue += revenue;
   }
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (2)

1-59: commerce-batch ๋ชจ๋“ˆ๊ณผ ์ค‘๋ณต๋œ ์—”ํ‹ฐํ‹ฐ

apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java์™€ ๋™์ผํ•œ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๋ชจ๋“ˆ์—์„œ ๊ฐ™์€ ํ…Œ์ด๋ธ”์„ ๋งคํ•‘ํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ฐ๊ฐ ๊ด€๋ฆฌํ•˜๋ฉด ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณตํ†ต ๋„๋ฉ”์ธ ๋ชจ๋“ˆ๋กœ ์ถ”์ถœํ•˜๊ฑฐ๋‚˜, ์˜๋„๋œ ์„ค๊ณ„์ธ ๊ฒฝ์šฐ ๋‘ ์—”ํ‹ฐํ‹ฐ์˜ ๋™๊ธฐํ™” ๋ฐฉ์•ˆ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.


56-58: null ์ž…๋ ฅ์— ๋Œ€ํ•œ ๋ฐฉ์–ด ์ฝ”๋“œ ์ถ”๊ฐ€ ๊ถŒ์žฅ

batch ๋ชจ๋“ˆ๊ณผ ๋™์ผํ•˜๊ฒŒ revenue๊ฐ€ null์ธ ๊ฒฝ์šฐ NullPointerException ๋ฐฉ์ง€๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
   public void incrementSalesRevenue(Long revenue) {
+    if (revenue == null || revenue <= 0) {
+      return;
+    }
     this.salesRevenue += revenue;
   }
apps/commerce-api/src/main/java/com/loopers/application/ranking/MvProductRankMonthlyService.java (1)

30-42: ์„ ํƒ์  ๊ฐœ์„ : ์ค‘๋ณต DB ํ˜ธ์ถœ ์ œ๊ฑฐ ๊ณ ๋ ค

existsByYearMonth ์ฒดํฌ ํ›„ findByYearMonthOrderByRanking ํ˜ธ์ถœ๋กœ ๋‘ ๋ฒˆ์˜ DB ์ ‘๊ทผ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. MV ๋ฐ์ดํ„ฐ๋Š” ๋ฐฐ์น˜๋กœ ๊ฐฑ์‹ ๋˜์–ด ์‹ค์งˆ์  ๋ฌธ์ œ๋Š” ์—†์ง€๋งŒ, ์กฐํšŒ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ๋Š”์ง€ ์ง์ ‘ ํ™•์ธํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์„ ํƒ์  ๊ฐœ์„ ์•ˆ
 public List<Long> getMonthlyRankingProductIds(String yearMonth, Pageable pageable) {
-    if (!existsByYearMonth(yearMonth)) {
-        log.debug("Monthly MV data not found for period: {}", yearMonth);
-        return List.of();
-    }
-
     Page<MvProductRankMonthly> mvResult = mvMonthlyRepository
         .findByYearMonthOrderByRanking(yearMonth, pageable);
     
+    if (mvResult.isEmpty()) {
+        log.debug("Monthly MV data not found for period: {}", yearMonth);
+        return List.of();
+    }
+    
     return mvResult.getContent().stream()
         .map(MvProductRankMonthly::getProductId)
         .toList();
 }
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (1)

193-271: ์ฝ”๋“œ ์ค‘๋ณต: ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ๋ฉ”์„œ๋“œ

getWeeklyRankingProductIdsWithMV์™€ getMonthlyRankingProductIdsWithMV ๊ฐ๊ฐ 2๊ฐœ์˜ ์˜ค๋ฒ„๋กœ๋“œ๊ฐ€ ์žˆ์œผ๋ฉฐ, ๋Œ€๋ถ€๋ถ„์˜ ๋กœ์ง์ด ์ค‘๋ณต๋ฉ๋‹ˆ๋‹ค. periodKey๋ฅผ ๋ฐ›๋Š” ๋ฒ„์ „์œผ๋กœ ํ†ต์ผํ•˜๊ณ  ๋‹ค๋ฅธ ๋ฒ„์ „์—์„œ ๋ณ€ํ™˜ ํ›„ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ค‘๋ณต ์ œ๊ฑฐ ์˜ˆ์‹œ (์ฃผ๊ฐ„)
 private List<Long> getWeeklyRankingProductIdsWithMV(String date, Pageable pageable) {
-    // 1. Redis ์บ์‹œ์—์„œ ์กฐํšŒ ์‹œ๋„
-    List<Long> cachedProductIds = getWeeklyRankingFromCache(date, pageable);
-    if (cachedProductIds != null && !cachedProductIds.isEmpty()) {
-      log.debug("Weekly ranking found in cache for date: {}, size: {}", date, cachedProductIds.size());
-      return cachedProductIds;
-    }
-
-    // 2. MV์—์„œ ์กฐํšŒ ์‹œ๋„ (์บ์‹œ miss ์‹œ)
     String yearMonthWeek = convertDateToYearMonthWeek(date);
-    if (mvWeeklyService.existsByYearMonthWeek(yearMonthWeek)) {
-      log.debug("Weekly ranking found in MV for period: {}", yearMonthWeek);
-      return mvWeeklyService.getWeeklyRankingProductIds(yearMonthWeek, pageable);
-    }
-
-    // 3. ProductMetricsDaily์—์„œ ์ง‘๊ณ„ํ•˜์—ฌ Redis์— ์ €์žฅ ํ›„ ๋ฐ˜ํ™˜ (MV miss ์‹œ)
-    log.warn("Weekly ranking MV miss for period: {}, falling back to ProductMetricsDaily aggregation", yearMonthWeek);
-    return dailyService.calculateAndCacheWeeklyRanking(date, yearMonthWeek, pageable);
+    return getWeeklyRankingProductIdsWithMV(date, yearMonthWeek, pageable);
 }
apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java (1)

46-72: PENDING ์ƒํƒœ์ผ ๋•Œ ๋ถˆํ•„์š”ํ•œ Order ์กฐํšŒ ๋ฐœ์ƒ

status๊ฐ€ PENDING์ผ ๋•Œ๋„ Line 53์—์„œ orderService.getOrderByOrderId(orderId)๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ๋ฐœํ–‰์ด ํ•„์š” ์—†๋Š” ๊ฒฝ์šฐ์ด๋ฏ€๋กœ ์กฐ๊ฑด๋ถ€ ์กฐํšŒ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ถˆํ•„์š”ํ•œ DB ํ˜ธ์ถœ์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์„ ํƒ์  ๊ฐœ์„ ์•ˆ
 @Transactional
 public void processPaymentCallback(String orderId, TransactionStatus status, String reason) {
     Payment payment = findPaymentByOrderId(orderId);
     payment.processCallbackStatus(status, reason);
     paymentRepository.save(payment);

-    // ์ƒํƒœ์— ๋”ฐ๋ฅธ ์ด๋ฒคํŠธ ๋ฐœํ–‰
-    Order order = orderService.getOrderByOrderId(orderId);
-
     if (status == TransactionStatus.SUCCESS) {
+        Order order = orderService.getOrderByOrderId(orderId);
         eventPublisher.publishEvent(new PaymentSuccessEvent(
             order.getOrderId(),
             order.getRefUserId(),
             payment.getAmount(),
             reason,
             order.getTotalPrice()
         ));
     } else if (status == TransactionStatus.FAILED) {
+        Order order = orderService.getOrderByOrderId(orderId);
         eventPublisher.publishEvent(new PaymentFailureEvent(
             order.getOrderId(),
             order.getRefUserId(),
             payment.getAmount(),
             reason
         ));
     }
     // PENDING ์ƒํƒœ๋Š” ๋ณ„๋„ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Œ
 }
apps/commerce-api/src/main/java/com/loopers/application/event/OrderEventHandler.java (1)

57-61: ์—๋Ÿฌ ๋กœ๊น… ์‹œ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ํฌํ•จ ๊ถŒ์žฅ

e.getMessage()๋งŒ ๋กœ๊น…ํ•˜๋ฉด ์˜ˆ์™ธ์˜ ๊ทผ๋ณธ ์›์ธ ์ถ”์ ์ด ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๋ฅผ ํฌํ•จํ•˜์—ฌ ๋””๋ฒ„๊น…์„ ์šฉ์ดํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •์•ˆ
     } catch (Exception e) {
-      log.error("์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ์‹คํŒจ - orderId: {}, userId: {}, error: {}",
-          event.orderId(), event.userId(), e.getMessage());
+      log.error("์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ์‹คํŒจ - orderId: {}, userId: {}",
+          event.orderId(), event.userId(), e);
       orderService.cancelPayment(event.orderId());
     }
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepository.java (1)

19-19: @param ์–ด๋…ธํ…Œ์ด์…˜์ด ํŒŒ์ƒ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ์—์„œ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Spring Data JPA์˜ ํŒŒ์ƒ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ(findByYearMonthOrderByRanking)๋Š” ๋ฉ”์„œ๋“œ ์ด๋ฆ„์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๋ก ํ•˜๋ฏ€๋กœ @Param ์–ด๋…ธํ…Œ์ด์…˜์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ช…์‹œ์  @Query ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ(Line 24, 30)์—๋งŒ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •์‚ฌํ•ญ
-    Page<MvProductRankMonthly> findByYearMonthOrderByRanking(@Param("yearMonth") String yearMonth, Pageable pageable);
+    Page<MvProductRankMonthly> findByYearMonthOrderByRanking(String yearMonth, Pageable pageable);
apps/commerce-api/src/main/java/com/loopers/application/event/DataTransferEventHandler.java (1)

14-14: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” import๋ฅผ ์ œ๊ฑฐํ•ด์ฃผ์„ธ์š”.

BigDecimal์ด ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

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

56-65: ๋žญํ‚น ์ ์ˆ˜ ๊ฐ€์ค‘์น˜๋ฅผ ๊ณตํ†ต ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

MvProductRankMonthly์™€ MvProductRankWeekly์—์„œ ๋™์ผํ•œ ๊ฐ€์ค‘์น˜(VIEW_WEIGHT=0.1, LIKE_WEIGHT=0.2, ORDER_WEIGHT=0.6)๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฐ€์ค‘์น˜๊ฐ€ ๋ณ€๊ฒฝ๋  ๊ฒฝ์šฐ ๋‘ ๊ณณ์„ ๋ชจ๋‘ ์ˆ˜์ •ํ•ด์•ผ ํ•˜๋ฉฐ, ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณตํ†ต ์ƒ์ˆ˜ ํด๋ž˜์Šค๋‚˜ ์„ค์ • ํŒŒ์ผ๋กœ ์ถ”์ถœํ•˜์—ฌ ์ค‘์•™ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ๋ฆฌํŒฉํ† ๋ง ์˜ˆ์‹œ

๊ณตํ†ต ์ƒ์ˆ˜ ํด๋ž˜์Šค ์ƒ์„ฑ:

public class RankingWeights {
    public static final double VIEW_WEIGHT = 0.1;
    public static final double LIKE_WEIGHT = 0.2;
    public static final double ORDER_WEIGHT = 0.6;
    
    public static double calculateScore(int viewCount, int likeCount, int orderCount) {
        return (VIEW_WEIGHT * viewCount) + (LIKE_WEIGHT * likeCount) + (ORDER_WEIGHT * orderCount);
    }
}

๊ทธ๋ฆฌ๊ณ  ์–‘์ชฝ ์—”ํ‹ฐํ‹ฐ์—์„œ ํ™œ์šฉ:

 public static MvProductRankMonthly create(Long productId, String yearMonth, Integer ranking,
                                           Integer likeCount, Integer orderCount, Integer viewCount) {
-    final double VIEW_WEIGHT = 0.1;
-    final double LIKE_WEIGHT = 0.2;
-    final double ORDER_WEIGHT = 0.6;
-
-    double score = (VIEW_WEIGHT * viewCount) + (LIKE_WEIGHT * likeCount) + (ORDER_WEIGHT * orderCount);
+    double score = RankingWeights.calculateScore(viewCount, likeCount, orderCount);
     
     return new MvProductRankMonthly(productId, yearMonth, ranking, score, likeCount, orderCount, viewCount);
 }
apps/commerce-api/src/main/java/com/loopers/application/ranking/ProductMetricsDailyService.java (2)

47-60: ๋น„ํšจ์œจ์ ์ธ ํŽ˜์ด์ง€๋„ค์ด์…˜ ํŒจํ„ด

Pageable.unpaged()๋กœ ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ ํ›„ skip/limit์œผ๋กœ ์ˆ˜๋™ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜๋Š” ๋ฐฉ์‹์€ ๋ฉ”๋ชจ๋ฆฌ ๋น„ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์—์„œ ๋ฌธ์ œ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฐœ์„  ๋ฐฉ์•ˆ

Redis ์บ์‹ฑ์šฉ ์ „์ฒด ์กฐํšŒ์™€ API ์‘๋‹ต์šฉ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ๋ถ„๋ฆฌํ•˜๊ฑฐ๋‚˜, DB ๋ ˆ๋ฒจ์—์„œ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์ ์šฉํ•˜์„ธ์š”:

-    Page<ProductMetricsWeekly> weeklyMetrics = productMetricsDailyRepository
-        .findWeeklyAggregateByDateRange(startDate, endDate, yearMonthWeek, Pageable.unpaged());
+    // ์บ์‹ฑ์šฉ TOP N ์กฐํšŒ (๋ณ„๋„ ์ฟผ๋ฆฌ)
+    List<ProductMetricsWeekly> allMetrics = productMetricsDailyRepository
+        .findWeeklyAggregateByDateRange(startDate, endDate, yearMonthWeek, PageRequest.of(0, 100)).getContent();
+    
+    cacheWeeklyRankingToRedis(rankingKey, allMetrics);
+    
+    // ํŽ˜์ด์ง€๋„ค์ด์…˜๋œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
+    return allMetrics.stream()
+        .skip((long) pageable.getPageNumber() * pageable.getPageSize())
+        .limit(pageable.getPageSize())
+        .map(ProductMetricsWeekly::getProductId)
+        .toList();

96-112: Redis ์บ์‹œ ๊ฐฑ์‹  ์‹œ ์›์ž์„ฑ ๋ฌธ์ œ

delete ํ›„ ์ˆœ์ฐจ์ ์œผ๋กœ addํ•˜๋Š” ํŒจํ„ด์€ ์›์ž์ ์ด์ง€ ์•Š์•„, ๊ฐฑ์‹  ์ค‘ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋นˆ ๋ฐ์ดํ„ฐ๋‚˜ ๋ถ€๋ถ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฐœ์„  ๋ฐฉ์•ˆ

์ƒˆ ํ‚ค์— ๋จผ์ € ๋ฐ์ดํ„ฐ๋ฅผ ์“ด ํ›„ RENAME ๋ช…๋ น์œผ๋กœ ๊ต์ฒดํ•˜๊ฑฐ๋‚˜, Redis ํŒŒ์ดํ”„๋ผ์ด๋‹์„ ์‚ฌ์šฉํ•˜์„ธ์š”:

private void cacheWeeklyRankingToRedis(String rankingKey, List<ProductMetricsWeekly> weeklyMetrics) {
    try {
        String tempKey = rankingKey + ":temp:" + System.currentTimeMillis();
        
        // ์ž„์‹œ ํ‚ค์— ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
        for (ProductMetricsWeekly metric : weeklyMetrics) {
            double score = /* ... */;
            redisTemplate.opsForZSet().add(tempKey, metric.getProductId().toString(), score);
        }
        
        // ์›์ž์ ์œผ๋กœ ํ‚ค ๊ต์ฒด
        redisTemplate.rename(tempKey, rankingKey);
        redisTemplate.expire(rankingKey, Duration.ofDays(7));
        
        log.info("Cached {} weekly ranking items to Redis key: {}", weeklyMetrics.size(), rankingKey);
    } catch (Exception e) {
        log.error("Failed to cache weekly ranking to Redis: {}", rankingKey, e);
    }
}
apps/commerce-batch/src/main/java/com/loopers/batch/job/WeeklyRankingJobConfig.java (2)

120-129: ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ Redis KEYS ๋ช…๋ น ์‚ฌ์šฉ ์ฃผ์˜

redisTemplate.keys(pattern)๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ KEYS ๋ช…๋ น์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์ด๋Š” O(N) ๋ณต์žก๋„๋กœ Redis๋ฅผ ๋ธ”๋กœํ‚นํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜์—์„œ๋Š” SCAN ๊ธฐ๋ฐ˜ ์‚ญ์ œ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฐœ์„  ๋ฐฉ์•ˆ
private void deleteRedisCacheByPattern(String pattern) {
    try {
        ScanOptions scanOptions = ScanOptions.scanOptions().match(pattern).count(100).build();
        Set<String> keysToDelete = new HashSet<>();
        
        try (Cursor<String> cursor = redisTemplate.scan(scanOptions)) {
            while (cursor.hasNext()) {
                keysToDelete.add(cursor.next());
            }
        }
        
        if (!keysToDelete.isEmpty()) {
            redisTemplate.delete(keysToDelete);
            log.debug("Deleted {} cache keys matching pattern: {}", keysToDelete.size(), pattern);
        }
    } catch (Exception e) {
        log.warn("Failed to clear Redis cache for pattern: {}", pattern, e);
    }
}

36-36: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์˜์กด์„ฑ ์ œ๊ฑฐ

entityManagerFactory๊ฐ€ ์ฃผ์ž…๋˜์—ˆ์ง€๋งŒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
   private final DataSource dataSource;
-  private final EntityManagerFactory entityManagerFactory;
   private final RedisTemplate<String, String> redisTemplate;
apps/commerce-batch/src/main/java/com/loopers/batch/job/MonthlyRankingJobConfig.java (3)

59-61: ์›”๋ง ๋‚ ์งœ ๊ณ„์‚ฐ ๊ฐœ์„  ๊ถŒ์žฅ

endDate = yearMonth + "31"์€ ๋Œ€๋ถ€๋ถ„์˜ DB์—์„œ ๋™์ž‘ํ•˜์ง€๋งŒ, ์ฝ”๋“œ ์˜๋„๊ฐ€ ๋ถˆ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ์›”๋ง ๋‚ ์งœ๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์ด ๋” ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
-      String startDate = yearMonth + "01"; // ํ•ด๋‹น ์›” 1์ผ
-      String endDate = yearMonth + "31";   // ํ•ด๋‹น ์›” 31์ผ (31์ผ์ด ์—†๋Š” ๋‹ฌ๋„ ํฌํ•จ)
+      LocalDate firstDay = LocalDate.parse(yearMonth + "01", DateTimeFormatter.ofPattern("yyyyMMdd"));
+      LocalDate lastDay = firstDay.withDayOfMonth(firstDay.lengthOfMonth());
+      String startDate = firstDay.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+      String endDate = lastDay.format(DateTimeFormatter.ofPattern("yyyyMMdd"));

35-35: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์˜์กด์„ฑ ์ œ๊ฑฐ

WeeklyRankingJobConfig์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ entityManagerFactory๊ฐ€ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


110-120: Redis ์บ์‹œ ์‚ญ์ œ ๋กœ์ง ์ค‘๋ณต ๋ฐ KEYS ๋ช…๋ น ์‚ฌ์šฉ

WeeklyRankingJobConfig์™€ ๋™์ผํ•œ deleteRedisCacheByPattern ๋ฉ”์„œ๋“œ๊ฐ€ ์ค‘๋ณต๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ๋กœ ์ถ”์ถœํ•˜๊ณ , KEYS ๋Œ€์‹  SCAN ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

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

30-31: findByYearMonthWeekAndProductId ๋ฐ˜ํ™˜ ํƒ€์ž…์„ Optional๋กœ ๋ณ€๊ฒฝ ๊ถŒ์žฅ

๊ฒฐ๊ณผ๊ฐ€ ์—†์„ ๋•Œ null์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด NPE ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค. Optional<MvProductRankWeekly>๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์ด ๋” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
-    @Query("SELECT mv FROM MvProductRankWeekly mv WHERE mv.yearMonthWeek = :yearMonthWeek AND mv.productId = :productId")
-    MvProductRankWeekly findByYearMonthWeekAndProductId(@Param("yearMonthWeek") String yearMonthWeek, @Param("productId") Long productId);
+    @Query("SELECT mv FROM MvProductRankWeekly mv WHERE mv.yearMonthWeek = :yearMonthWeek AND mv.productId = :productId")
+    Optional<MvProductRankWeekly> findByYearMonthWeekAndProductId(@Param("yearMonthWeek") String yearMonthWeek, @Param("productId") Long productId);

Import ์ถ”๊ฐ€:

import java.util.Optional;


public record CouponUsedEvent(
Long orderId,
String orderId,
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Find CouponUsedEvent consumers
echo "=== CouponUsedEvent ์†Œ๋น„์ž ๊ฒ€์ƒ‰ ==="
rg -n -C3 --type=java 'CouponUsedEvent' 

echo -e "\n=== orderId ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๊ฒ€์ƒ‰ ==="
fd -e sql -e xml | head -20

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

Length of output: 4738


๐Ÿ Script executed:

#!/bin/bash
# Get full CouponService implementation
echo "=== CouponService.publishCouponUsedEvent ์ „์ฒด ์ฝ”๋“œ ==="
rg -n -A10 'private void publishCouponUsedEvent' --type=java

echo -e "\n=== CouponEventHandler.handleCouponUsed ์ „์ฒด ์ฝ”๋“œ ==="
rg -n -A15 'public void handleCouponUsed' --type=java

echo -e "\n=== BusinessActionEvent.couponUsed ์ •์˜ ์ฐพ๊ธฐ ==="
rg -n -B5 -A10 'couponUsed\(' --type=java | grep -A15 'static.*couponUsed'

echo -e "\n=== ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ํŒŒ์ผ ํ™•์ธ ==="
fd -e sql | xargs rg -l 'orders' -i

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

Length of output: 5305


๐Ÿ Script executed:

#!/bin/bash
# Find where CouponUsedEvent is instantiated
echo "=== CouponUsedEvent ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ์œ„์น˜ ==="
rg -n 'new CouponUsedEvent' --type=java

echo -e "\n=== CouponUsedEvent ๋ ˆ์ฝ”๋“œ ์ „์ฒด ์ •์˜ ==="
rg -n -A10 'public record CouponUsedEvent' --type=java

echo -e "\n=== eventPublisher.publishEvent ํ˜ธ์ถœ ์œ„์น˜ ==="
rg -n 'publishEvent\(' --type=java | head -20

echo -e "\n=== CouponEventHandler ์ „์ฒด ํด๋ž˜์Šค ==="
cat apps/commerce-api/src/main/java/com/loopers/application/event/CouponEventHandler.java

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

Length of output: 5058


๐Ÿ Script executed:

#!/bin/bash
# Search for all places where CouponUsedEvent is published or created
echo "=== CouponUsedEvent๋ฅผ ๋ฐœํ–‰ํ•˜๋Š” ์ฝ”๋“œ ์ฐพ๊ธฐ ==="
rg -n 'publishEvent.*CouponUsedEvent|eventPublisher.*new CouponUsedEvent' --type=java

# Check if CouponUsedEvent is used anywhere
echo -e "\n=== CouponUsedEvent ๋ชจ๋“  ์‚ฌ์šฉ ์œ„์น˜ ==="
rg -n 'CouponUsedEvent' --type=java

# Check OrderService.cancelPayment to see what type orderId should be
echo -e "\n=== OrderService.cancelPayment ๋ฉ”์„œ๋“œ ์„œ๋ช… ==="
rg -n -A5 'public.*cancelPayment' apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java

# Check PgClient.requestPayment to see what type orderId should be
echo -e "\n=== PgClient.requestPayment ๋ฉ”์„œ๋“œ ์„œ๋ช… ==="
rg -n -A5 'requestPayment' --type=java | head -20

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

Length of output: 3639


๐Ÿ Script executed:

#!/bin/bash
# Check git history or comments that might explain CouponUsedEvent
echo "=== CouponUsedEvent ๊ด€๋ จ ์ฝ”๋“œ ๋งฅ๋ฝ ==="
cat apps/commerce-api/src/main/java/com/loopers/application/event/CouponUsedEvent.java

echo -e "\n=== CouponService ์ „์ฒด publishCouponUsedEvent ๋ฉ”์„œ๋“œ ==="
rg -n -B2 -A8 'private void publishCouponUsedEvent' apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java

echo -e "\n=== Order ์—”ํ‹ฐํ‹ฐ์—์„œ orderId ํƒ€์ž… ํ™•์ธ ==="
rg -n 'orderId|orderId' --type=java | grep -i 'order.*class\|private.*orderId\|String orderId\|Long orderId' | head -20

echo -e "\n=== Order ์—”ํ‹ฐํ‹ฐ ํŒŒ์ผ ์ฐพ๊ธฐ ==="
fd -n 'Order.java' --type f | head -5

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

Length of output: 4091


๐Ÿ Script executed:

#!/bin/bash
# Look at Order.java to confirm orderId type
echo "=== Order.java ํŒŒ์ผ ๋‚ด์šฉ ==="
cat apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java | head -50

echo -e "\n=== CouponService ์ „์ฒด ๋‚ด์šฉ ํ™•์ธ ==="
cat apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java

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

Length of output: 5391


CouponUsedEvent๊ฐ€ ๋ฐœํ–‰๋˜์ง€ ์•Š์•„ ํ•ธ๋“ค๋Ÿฌ ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์Œ

CouponUsedEvent๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์ง€๋งŒ ์‹ค์ œ๋กœ ๋ฐœํ–‰๋˜์ง€ ์•Š๋Š” ๊ตฌ์กฐ์  ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

  • CouponService.publishCouponUsedEvent() ๋ฉ”์„œ๋“œ๋Š” BusinessActionEvent๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉฐ, CouponUsedEvent๋Š” ์ „ํ˜€ ๋ฐœํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
  • CouponEventHandler.handleCouponUsed() ๋ฆฌ์Šค๋„ˆ๋Š” ๋„๋‹ฌ ๋ถˆ๊ฐ€๋Šฅํ•œ ๋ฐ๋“œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค
  • ์‹ค์ œ ๋ฐœํ–‰๋˜๋Š” BusinessActionEvent๋Š” orderId๋ฅผ null๋กœ ์ „๋‹ฌํ•˜๋ฏ€๋กœ, ์˜๋„ํ•œ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ ๋กœ์ง(pgClient.requestPayment(), orderService.cancelPayment())์ด ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค

CouponUsedEvent๋ฅผ ์‹ค์ œ๋กœ ๋ฐœํ–‰ํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜, CouponEventHandler๋ฅผ ์ œ๊ฑฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/event/CouponUsedEvent.java
around line 7, CouponUsedEvent is never actually published so
CouponEventHandler.handleCouponUsed is dead code and BusinessActionEvent is
published with a null orderId; fix by changing
CouponService.publishCouponUsedEvent() to construct and publish a
CouponUsedEvent (copying the orderId and other needed fields out of the
BusinessActionEvent payload) instead of publishing BusinessActionEvent, ensure
the event payload includes a non-null orderId, and keep the existing handler
intact (or if you prefer to keep BusinessActionEvent as the canonical event,
remove the unused CouponUsedEvent class and its handler). Ensure the publish
call uses the correct event type and that downstream handler wiring remains
unchanged.

Comment on lines 26 to 36
public void save(Long userId, Long productId) {
Optional<Like> liked = likeRepository.findById(userId, productId);
if (!liked.isPresent()) {
likeRepository.save(userId, productId);
}

// ์ข‹์•„์š” ์ด๋ฒคํŠธ ๋ฐœํ–‰ (๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์šฉ)
LikeEvent likeEvent = new LikeEvent(userId, productId, LikeEvent.LikeAction.LIKE);
eventPublisher.publishEvent(likeEvent);
log.debug("์ข‹์•„์š” ์ด๋ฒคํŠธ ๋ฐœํ–‰ - ์‚ฌ์šฉ์žID: {}, ์ƒํ’ˆID: {}", userId, productId);
}
Copy link

Choose a reason for hiding this comment

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

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

์ค‘๋ณต ์ด๋ฒคํŠธ ๋ฐœํ–‰์œผ๋กœ ์ธํ•œ ์ง‘๊ณ„ ์˜ค๋ฅ˜ ๋ฐœ์ƒ.

ํ˜„์žฌ ๊ตฌํ˜„์€ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ข‹์•„์š”์— ๋Œ€ํ•ด์„œ๋„ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค:

  • Line 28-30: ์ข‹์•„์š”๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•˜๋ฉด ์ €์žฅ์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.
  • Line 33-35: ํ•˜์ง€๋งŒ ์ด๋ฒคํŠธ๋Š” ๋ฌด์กฐ๊ฑด ๋ฐœํ–‰๋ฉ๋‹ˆ๋‹ค (if ๋ธ”๋ก ๋ฐ–์— ์œ„์น˜).

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

๐Ÿ”Ž ์ด๋ฒคํŠธ ๋ฐœํ–‰์„ ์กฐ๊ฑด๋ถ€ ๋ธ”๋ก ์•ˆ์œผ๋กœ ์ด๋™ํ•˜๋Š” ์ˆ˜์ •์•ˆ
 @Transactional
 public void save(Long userId, Long productId) {
   Optional<Like> liked = likeRepository.findById(userId, productId);
   if (!liked.isPresent()) {
     likeRepository.save(userId, productId);
+
+    // ์ข‹์•„์š” ์ด๋ฒคํŠธ ๋ฐœํ–‰ (๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์šฉ)
+    LikeEvent likeEvent = new LikeEvent(userId, productId, LikeEvent.LikeAction.LIKE);
+    eventPublisher.publishEvent(likeEvent);
+    log.debug("์ข‹์•„์š” ์ด๋ฒคํŠธ ๋ฐœํ–‰ - ์‚ฌ์šฉ์žID: {}, ์ƒํ’ˆID: {}", userId, productId);
   }
-
-  // ์ข‹์•„์š” ์ด๋ฒคํŠธ ๋ฐœํ–‰ (๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์šฉ)
-  LikeEvent likeEvent = new LikeEvent(userId, productId, LikeEvent.LikeAction.LIKE);
-  eventPublisher.publishEvent(likeEvent);
-  log.debug("์ข‹์•„์š” ์ด๋ฒคํŠธ ๋ฐœํ–‰ - ์‚ฌ์šฉ์žID: {}, ์ƒํ’ˆID: {}", userId, productId);
 }
๐Ÿค– Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
around lines 26 to 36, the method currently publishes a LikeEvent regardless of
whether a new Like was actually saved, causing duplicate events and incorrect
batch aggregation; change the logic so that the event creation, publishEvent
call, and debug log only occur when likeRepository.save(userId, productId) is
executed (i.e., move those lines inside the branch that runs when the like did
not previously exist), ensuring no event is emitted for existing likes.

void ์‹คํŒจ_์กด์žฌํ•˜์ง€_์•Š๋Š”_์ƒํ’ˆID() {
// arrange
Long orderId = (long) -1;
String orderId = (long) -1;
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

์ปดํŒŒ์ผ ์˜ค๋ฅ˜: ํƒ€์ž… ๋ถˆ์ผ์น˜

(long) -1์„ String ๋ณ€์ˆ˜์— ํ• ๋‹นํ•˜๊ณ  ์žˆ์–ด ์ปดํŒŒ์ผ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. String ๋ฆฌํ„ฐ๋Ÿด๋กœ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
-      String orderId = (long) -1;
+      String orderId = "invalid-order-id";
๐Ÿ“ 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
String orderId = (long) -1;
String orderId = "invalid-order-id";
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java
around line 136, the code assigns (long) -1 to a String variable causing a
compile-time type mismatch; replace the numeric cast with a String literal
(e.g., "-1") or use String.valueOf(-1L) so the variable receives a valid String
value.

void ์‹คํŒจ_์กด์žฌํ•˜์ง€_์•Š๋Š”_์ฃผ๋ฌธID() {
// arrange
Long orderId = (long) -1;
String orderId = (long) -1;
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

์ปดํŒŒ์ผ ์˜ค๋ฅ˜: long์„ String์— ํ• ๋‹นํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

(long) -1์„ String ๋ณ€์ˆ˜์— ์ง์ ‘ ํ• ๋‹นํ•˜๋Š” ๊ฒƒ์€ Java์—์„œ ํ—ˆ์šฉ๋˜์ง€ ์•Š์•„ ์ปดํŒŒ์ผ์ด ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
-      String orderId = (long) -1;
+      String orderId = "-1";
๐Ÿ“ 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
String orderId = (long) -1;
String orderId = "-1";
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java
around line 119, the test attempts to assign (long) -1 to a String variable
causing a compile error; replace the invalid assignment with a proper String
representation of -1 (e.g., use a string literal "-1" or convert the long via
String.valueOf(-1L) / Long.toString(-1L)) so orderId is a valid String.

Comment on lines +94 to +104
queryProvider.setSelectClause("pm.product_id, COALESCE(SUM(pm.like_count), 0) as like_count, COALESCE(SUM(pm.sales_revenue), 0) as order_count, COALESCE(SUM(pm.view_count), 0) as view_count");
queryProvider.setFromClause("product_metrics pm");

// ํŠน์ • ์ผ์˜ ๋ฐ์ดํ„ฐ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ (00:00๋ถ€ํ„ฐ 23:59๊นŒ์ง€)
String currentDate = date != null ? date : LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String startKey = currentDate + "0000"; // ํ•ด๋‹น ์ผ 00:00
String endKey = currentDate + "2359"; // ํ•ด๋‹น ์ผ 23:59

queryProvider.setWhereClause("pm.bucket_time_key >= '" + startKey + "' AND pm.bucket_time_key <= '" + endKey + "'");
queryProvider.setGroupClause("pm.product_id");
queryProvider.setSortKeys(Map.of("pm.product_id", Order.ASCENDING));
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

SQL Injection ์ทจ์•ฝ์  ๋ฐ ์ปฌ๋Ÿผ ๋งคํ•‘ ์˜ค๋ฅ˜

๋‘ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. SQL Injection: setWhereClause์—์„œ ๋ฌธ์ž์—ด ์—ฐ๊ฒฐ๋กœ SQL์„ ๊ตฌ์„ฑํ•˜๊ณ  ์žˆ์–ด ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
  2. ์ปฌ๋Ÿผ ๋งคํ•‘ ์˜ค๋ฅ˜: sales_revenue๋ฅผ order_count๋กœ ๋งคํ•‘ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํŒ๋งค ์ˆ˜์ต๊ณผ ์ฃผ๋ฌธ ์ˆ˜๋Š” ๋‹ค๋ฅธ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค.
๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
 MySqlPagingQueryProvider queryProvider = new MySqlPagingQueryProvider();
-queryProvider.setSelectClause("pm.product_id, COALESCE(SUM(pm.like_count), 0) as like_count, COALESCE(SUM(pm.sales_revenue), 0) as order_count, COALESCE(SUM(pm.view_count), 0) as view_count");
+queryProvider.setSelectClause("pm.product_id, COALESCE(SUM(pm.like_count), 0) as like_count, COALESCE(COUNT(*), 0) as order_count, COALESCE(SUM(pm.view_count), 0) as view_count");
 queryProvider.setFromClause("product_metrics pm");

 String currentDate = date != null ? date : LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
 String startKey = currentDate + "0000";
 String endKey = currentDate + "2359";

-queryProvider.setWhereClause("pm.bucket_time_key >= '" + startKey + "' AND pm.bucket_time_key <= '" + endKey + "'");
+queryProvider.setWhereClause("pm.bucket_time_key >= :startKey AND pm.bucket_time_key <= :endKey");

ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์œ„ํ•ด JdbcPagingItemReaderBuilder์— parameterValues ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

return new JdbcPagingItemReaderBuilder<DailyMetricsDto>()
    .name("dailyMetricsReader")
    .dataSource(dataSource)
    .queryProvider(queryProvider)
    .parameterValues(Map.of("startKey", startKey, "endKey", endKey))
    .pageSize(1000)
    .rowMapper(new BeanPropertyRowMapper<>(DailyMetricsDto.class))
    .build();

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

๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/job/DailyRankingJobConfig.java
around lines 94 to 104, the code builds the WHERE clause via string
concatenation (SQL injection risk) and incorrectly aliases sales_revenue as
order_count; change the WHERE clause to use named parameters (e.g.
pm.bucket_time_key >= :startKey AND pm.bucket_time_key <= :endKey) and pass a
parameterValues map into the JdbcPagingItemReaderBuilder with startKey and
endKey, and fix the SELECT clause so sales_revenue is aliased to sales_revenue
(or add SUM(pm.order_count) as order_count if you intend to include order count
separately), ensuring the resulting column aliases match the fields of
DailyMetricsDto.

Comment on lines +17 to +26
data:
redis:
host: localhost
port: 6379
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
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

Redis ์„ค์ •์„ ํ™˜๊ฒฝ๋ณ„๋กœ ๋ถ„๋ฆฌํ•˜์„ธ์š”.

๋ชจ๋“  ํ™˜๊ฒฝ์—์„œ localhost:6379๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฐํฌ๋œ ํ™˜๊ฒฝ(dev/qa/prd)์—์„œ๋Š” ์‹ค์ œ Redis ์ธ์Šคํ„ด์Šค์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์–ด ๋žญํ‚น ์บ์‹œ ์‹œ์Šคํ…œ์ด ๋™์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋˜๋Š” ํ”„๋กœํŒŒ์ผ๋ณ„ ์„ค์ • ์ œ์•ˆ

์˜ต์…˜ 1: ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์‚ฌ์šฉ (๊ถŒ์žฅ)

  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}

์˜ต์…˜ 2: ํ”„๋กœํŒŒ์ผ๋ณ„ ์„ค์ •
๊ฐ ํ”„๋กœํŒŒ์ผ(dev/qa/prd)์— ํ•ด๋‹น ํ™˜๊ฒฝ์˜ Redis ์„ค์ • ์ถ”๊ฐ€

๐Ÿค– Prompt for AI Agents
In apps/commerce-batch/src/main/resources/application.yml around lines 17-26 the
Redis host/port are hardcoded to localhost:6379 which breaks non-local
deployments; replace these literals with environment-variable placeholders (e.g.
REDIS_HOST, REDIS_PORT, optional REDIS_PASSWORD) or move Redis settings into
profile-specific files (application-dev.yml, application-qa.yml,
application-prod.yml) and reference them via spring.profiles.active; update
configuration to use the placeholders/defaults and add the profile files for
each environment, then verify the app reads the env vars or active profile
during startup.

Comment on lines +84 to +95
spring.config.activate.on-profile: dev

spring:
datasource:
url: jdbc:mysql://localhost:3306/loopers
username: application
password: application
driver-class-name: com.mysql.cj.jdbc.Driver

jpa:
hibernate:
ddl-auto: none
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

dev ํ”„๋กœํŒŒ์ผ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •์„ ์ˆ˜์ •ํ•˜์„ธ์š”.

๋‹ค์Œ ๋ฌธ์ œ๋“ค์ด ํ™•์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

  1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค URL: localhost:3306์€ ๋ฐฐํฌ๋œ ํ™˜๊ฒฝ์—์„œ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  2. ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ž๊ฒฉ์ฆ๋ช…: username/password๊ฐ€ ์†Œ์Šค ์ฝ”๋“œ์— ๋…ธ์ถœ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  3. ๋ˆ„๋ฝ๋œ Hibernate ๋ฐฐ์น˜ ์ตœ์ ํ™”: ๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์„ฑ๋Šฅ์— ์ค‘์š”ํ•œ ์„ค์ •๋“ค์ด ์—†์Šต๋‹ˆ๋‹ค.
๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
 spring:
   datasource:
-    url: jdbc:mysql://localhost:3306/loopers
-    username: application
-    password: application
+    url: ${DB_URL:jdbc:mysql://localhost:3306/loopers}
+    username: ${DB_USERNAME}
+    password: ${DB_PASSWORD}
     driver-class-name: com.mysql.cj.jdbc.Driver
 
   jpa:
     hibernate:
       ddl-auto: none
+    properties:
+      hibernate:
+        jdbc:
+          batch_size: 1000
+          order_inserts: true
+          order_updates: true
+        batch_versioned_data: true
๐Ÿค– Prompt for AI Agents
In apps/commerce-batch/src/main/resources/application.yml around lines 84-95,
the dev profile currently uses a hardcoded localhost URL and credentials and
lacks Hibernate batch optimization settings; change the datasource URL and
credentials to environment-backed placeholders (e.g. spring.datasource.url:
${SPRING_DATASOURCE_URL:jdbc:mysql://db:3306/loopers},
spring.datasource.username: ${SPRING_DATASOURCE_USERNAME:application},
spring.datasource.password: ${SPRING_DATASOURCE_PASSWORD:application}) so
deployment can supply real values rather than localhost/hardcoded secrets, and
add JPA/Hibernate batch properties suitable for batch jobs (e.g.
hibernate.jdbc.batch_size, hibernate.order_inserts, hibernate.order_updates,
hibernate.batch_versioned_data and the JDBC rewriteBatchedStatements option via
datasource URL) while keeping ddl-auto as none or validate; ensure placeholders
use secure environment variables and document required env names.

Comment on lines +97 to +102
spring.config.activate.on-profile: qa
spring:
datasource:
url: jdbc:mysql://localhost:3306/loopers
username: application
password: application
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

qa ํ”„๋กœํŒŒ์ผ์˜ ์„ค์ •์„ ์™„์„ฑํ•˜์„ธ์š”.

๋‹ค์Œ ๋ฌธ์ œ๋“ค์ด ํ™•์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

  1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค URL: localhost:3306์€ ๋ฐฐํฌ๋œ ํ™˜๊ฒฝ์—์„œ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  2. ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ž๊ฒฉ์ฆ๋ช…: ๋ณด์•ˆ ์ทจ์•ฝ์ ์ž…๋‹ˆ๋‹ค.
  3. ๋ˆ„๋ฝ๋œ driver-class-name: ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜๋ฅผ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  4. ๋ˆ„๋ฝ๋œ Hibernate ๋ฐฐ์น˜ ์ตœ์ ํ™”: ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค๋‹ˆ๋‹ค.
๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
 spring:
   datasource:
-    url: jdbc:mysql://localhost:3306/loopers
-    username: application
-    password: application
+    url: ${DB_URL}
+    username: ${DB_USERNAME}
+    password: ${DB_PASSWORD}
+    driver-class-name: com.mysql.cj.jdbc.Driver
+
+  jpa:
+    hibernate:
+      ddl-auto: none
+    properties:
+      hibernate:
+        jdbc:
+          batch_size: 1000
+          order_inserts: true
+          order_updates: true
+        batch_versioned_data: true
๐Ÿ“ 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
spring.config.activate.on-profile: qa
spring:
datasource:
url: jdbc:mysql://localhost:3306/loopers
username: application
password: application
spring.config.activate.on-profile: qa
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: none
properties:
hibernate:
jdbc:
batch_size: 1000
order_inserts: true
order_updates: true
batch_versioned_data: true
๐Ÿค– Prompt for AI Agents
In apps/commerce-batch/src/main/resources/application.yml around lines 97-102,
the qa profile uses localhost and hardcoded credentials and is missing
driver-class-name and Hibernate batch settings; change the datasource URL to
reference an environment-configurable host/port/database (do not hardcode
localhost), replace username/password with environment property placeholders or
a credentials store reference, add the proper driver-class-name for MySQL, and
add Hibernate batch tuning properties (e.g.,
spring.jpa.properties.hibernate.jdbc.batch_size and related
order_inserts/order_updates flags) to enable batch optimization.

Comment on lines +104 to +109
spring.config.activate.on-profile: prd
spring:
datasource:
url: jdbc:mysql://localhost:3306/loopers
username: application
password: application
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

prd ํ”„๋กœํŒŒ์ผ์˜ ์„ค์ •์„ ์™„์„ฑํ•˜๊ณ  ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•˜์„ธ์š”.

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋‹ค์Œ์˜ ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค URL: localhost:3306์€ ํ”„๋กœ๋•์…˜์—์„œ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  2. ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ž๊ฒฉ์ฆ๋ช…: ํ”„๋กœ๋•์…˜ ์ž๊ฒฉ์ฆ๋ช…์ด ์†Œ์Šค ์ฝ”๋“œ์— ๋…ธ์ถœ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋“œ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋‚˜ ์‹œํฌ๋ฆฟ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  3. ๋ˆ„๋ฝ๋œ driver-class-name: ํ•„์ˆ˜ ์„ค์ •์ž…๋‹ˆ๋‹ค.
  4. ๋ˆ„๋ฝ๋œ Hibernate ๋ฐฐ์น˜ ์ตœ์ ํ™”: ๋ฐฐ์น˜ ์ž‘์—… ์„ฑ๋Šฅ์— ์‹ฌ๊ฐํ•œ ์˜ํ–ฅ์„ ์ค๋‹ˆ๋‹ค.
  5. ๋ˆ„๋ฝ๋œ JPA ddl-auto ์„ค์ •: ๋ช…์‹œ์ ์œผ๋กœ none์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
 spring:
   datasource:
-    url: jdbc:mysql://localhost:3306/loopers
-    username: application
-    password: application
+    url: ${DB_URL}
+    username: ${DB_USERNAME}
+    password: ${DB_PASSWORD}
+    driver-class-name: com.mysql.cj.jdbc.Driver
+
+  jpa:
+    hibernate:
+      ddl-auto: none
+    properties:
+      hibernate:
+        jdbc:
+          batch_size: 1000
+          order_inserts: true
+          order_updates: true
+        batch_versioned_data: true

๋˜ํ•œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” AWS Secrets Manager, HashiCorp Vault, ๋˜๋Š” Kubernetes Secrets ๋“ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž๊ฒฉ์ฆ๋ช…์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ฐ•๋ ฅํžˆ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ 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
spring.config.activate.on-profile: prd
spring:
datasource:
url: jdbc:mysql://localhost:3306/loopers
username: application
password: application
spring.config.activate.on-profile: prd
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: none
properties:
hibernate:
jdbc:
batch_size: 1000
order_inserts: true
order_updates: true
batch_versioned_data: true
๐Ÿค– Prompt for AI Agents
In apps/commerce-batch/src/main/resources/application.yml around lines 104-109,
the prd profile currently uses localhost DB URL and hardcoded credentials and is
missing driver-class-name, Hibernate batch optimizations, and JPA ddl-auto;
update the prd profile to point to the real production DB host/port (do not use
localhost), remove hardcoded username/password and read them from environment
variables or a secrets manager (e.g., AWS Secrets Manager/HashiCorp
Vault/Kubernetes Secrets), add the appropriate driver-class-name for MySQL,
enable Hibernate batch settings (e.g., hibernate.jdbc.batch_size and
hibernate.order_inserts/order_updates) to optimize batch jobs, and explicitly
set spring.jpa.hibernate.ddl-auto: none.

Comment on lines +139 to +145
arguments:
parameters:
- name: date
# ํ˜„์žฌ ๋‚ ์งœ๋ฅผ YYYYMMDD ํ˜•์‹์œผ๋กœ ์ƒ์„ฑ (์˜ˆ: 20240115)
value: "{{ workflow.creationTimestamp.strftime('%Y%m%d') }}"
workflowTemplateRef:
name: daily-ranking-workflow No newline at end of file
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐ŸŒ Web query:

Argo Workflows template functions date formatting strftime

๐Ÿ’ก Result:

Short answer โ€” two ways:

  1. Built-in workflow variables (strftime-style parts)
  • Argo exposes workflow.creationTimestamp and per-format-character fields like workflow.creationTimestamp.Y, .m, .d, .H, .M, .S etc (these are populated for each supported strftime character). Use those to build parts directly. [1]
  1. Templating with Sprig (recommended for full formatting)
  • Use Sprig template functions to format/modify dates (sprig.date, sprig.dateModify, sprig.now(), sprig.unixEpoch, sprig.toDate). Note Sprig uses Goโ€™s time layout (the reference date 2006-01-02 15:04:05) โ€” not strftime. Example: '{{=sprig.date("2006-01-02", workflow.creationTimestamp)}}'. [2][3]

References

  • Argo workflow variables (creationTimestamp and per-character fields). [1]
  • Examples and Sprig usage in Argo (date, dateModify, Go-layout example). [2][3]

๐ŸŒ Web query:

Argo Workflows sprig template functions date format

๐Ÿ’ก Result:

Short answer โ€” use Sprig's date functions (Go layouts) inside Argo templates. Key functions: sprig.date, sprig.dateInZone, sprig.dateModify (aka date_modify), sprig.now, sprig.toDate, sprig.unixEpoch. Sprig uses Go's reference time (Mon Jan 2 15:04:05 MST 2006) for format strings.

Examples:

  • Format workflow creation timestamp as YYYY-MM-DD:
    value: '{{=sprig.date("2006-01-02", workflow.creationTimestamp)}}'. [1][2]

  • Get yesterday's date (YYYY-MM-DD):
    value: '{{=sprig.date("2006-01-02", sprig.dateModify("-24h", sprig.now()))}}'. [1][4]

  • Format in UTC:
    value: '{{=sprig.dateInZone("2006-01-02T15:04:05Z07:00", sprig.now(), "UTC")}}'. [1]

  • Parse a string then reformat:
    value: '{{=sprig.toDate("2006-01-02T15:04:05Z07:00", inputs.parameters.dt) | sprig.date("02/01/2006")}}'. [1]

Common tokens: "2006"=year, "01"=month, "02"=day, "15:04:05"=24h time; RFC3339 layout is "2006-01-02T15:04:05Z07:00". [1][3]

References: Sprig date functions and layouts documentation. [1][3][2]


๐ŸŒ Web query:

Argo Workflows creationTimestamp date format syntax

๐Ÿ’ก Result:

Short answer:

  • Workflow.creationTimestamp is provided in RFC 3339 (UTC) (example: 2018-08-23T05:42:49Z). [1][2]

How to use/format it in Argo:

  • Raw variable: {{workflow.creationTimestamp}}. [1]
  • RFC3339 explicit: {{workflow.creationTimestamp.RFC3339}}. [1]
  • Use Sprig/Go-style formatting: {{=sprig.date("2006-01-02 15:04:05", workflow.creationTimestamp)}} (Go reference date/layout semantics: 2006-01-02 15:04:05). [3]

Sources:

  • Argo Workflow variables / creationTimestamp docs. [1]
  • Argo ObjectMeta / creationTimestamp field (RFC3339, UTC). [2]
  • Example showing Sprig/Go-layout formatting of workflow.creationTimestamp. [3]

Argo Workflows์—์„œ ์œ ํšจํ•˜์ง€ ์•Š์€ ํ…œํ”Œ๋ฆฟ ๋ฌธ๋ฒ•์ž…๋‹ˆ๋‹ค.

workflow.creationTimestamp.strftime('%Y%m%d') ๋ฌธ๋ฒ•์€ Argo Workflows์—์„œ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Argo๋Š” .strftime() ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ Sprig์˜ date ํ•จ์ˆ˜๋กœ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ • ์‚ฌํ•ญ
     arguments:
       parameters:
       - name: date
-        # ํ˜„์žฌ ๋‚ ์งœ๋ฅผ YYYYMMDD ํ˜•์‹์œผ๋กœ ์ƒ์„ฑ (์˜ˆ: 20240115)
-        value: "{{ workflow.creationTimestamp.strftime('%Y%m%d') }}"
+        # ํ˜„์žฌ ๋‚ ์งœ๋ฅผ YYYYMMDD ํ˜•์‹์œผ๋กœ ์ƒ์„ฑ (์˜ˆ: 20240115)
+        value: "{{ sprig.date('20060102', workflow.creationTimestamp) }}"

์ฐธ๊ณ : Go์˜ date ํฌ๋งท์€ ๋ ˆํผ๋Ÿฐ์Šค ์‹œ๊ฐ„ Mon Jan 2 15:04:05 MST 2006์„ ๊ธฐ์ค€์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค. 20060102๋Š” YYYYMMDD ํ˜•์‹์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ 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
arguments:
parameters:
- name: date
# ํ˜„์žฌ ๋‚ ์งœ๋ฅผ YYYYMMDD ํ˜•์‹์œผ๋กœ ์ƒ์„ฑ (์˜ˆ: 20240115)
value: "{{ workflow.creationTimestamp.strftime('%Y%m%d') }}"
workflowTemplateRef:
name: daily-ranking-workflow
arguments:
parameters:
- name: date
# ํ˜„์žฌ ๋‚ ์งœ๋ฅผ YYYYMMDD ํ˜•์‹์œผ๋กœ ์ƒ์„ฑ (์˜ˆ: 20240115)
value: "{{ sprig.date('20060102', workflow.creationTimestamp) }}"
workflowTemplateRef:
name: daily-ranking-workflow
๐Ÿค– Prompt for AI Agents
In argo-workflows/weekly-ranking-workflow.yaml around lines 139 to 145, the
parameter value uses unsupported Python-like .strftime() on
workflow.creationTimestamp; replace it with Argo/Sprig date formatting. Change
the value to use Sprig: value: "{{ workflow.creationTimestamp | toDate | date
\"20060102\" }}" so the timestamp is converted to a date and formatted as
YYYYMMDD (Go reference format 20060102).

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