Skip to content

Conversation

@JVHE
Copy link
Collaborator

@JVHE JVHE commented Jan 2, 2026

๐Ÿ“Œ Summary

Spring Batch๋ฅผ ํ™œ์šฉํ•œ ์ฃผ๊ฐ„/์›”๊ฐ„ ์ƒํ’ˆ ๋žญํ‚น ์ง‘๊ณ„ ๋ฐฐ์น˜ ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

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

  • ProductMetricsDaily ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฃผ๊ฐ„/์›”๊ฐ„ TOP 100 ๋žญํ‚น ์ง‘๊ณ„
  • ํ•˜๋‚˜์˜ ํ†ตํ•ฉ Job์œผ๋กœ ์ฃผ๊ฐ„/์›”๊ฐ„ ์ฒ˜๋ฆฌ (์ค‘๋ณต ๋นˆ ๋ฌธ์ œ ํ•ด๊ฒฐ)
  • PriorityQueue๋ฅผ ํ™œ์šฉํ•œ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์ธ TOP 100 ํ•„ํ„ฐ๋ง
  • ExecutionContext ์ง๋ ฌํ™” ๋ฌธ์ œ ํ•ด๊ฒฐ (JSON ๋ณ€ํ™˜ ์ „๋žต)
  • Reader์—์„œ periodType์— ๋”ฐ๋ฅธ ๋‚ ์งœ ๋ฒ”์œ„ ์ž๋™ ๊ณ„์‚ฐ (์ฃผ๊ฐ„: ์ตœ๊ทผ 7์ผ, ์›”๊ฐ„: ์ตœ๊ทผ 30์ผ)

๊ฒฐ๊ณผ:

  • ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ MvProductRankWeekly, MvProductRankMonthly ํ…Œ์ด๋ธ”์— ์ €์žฅ
  • ๋ฐฐ์น˜ API๋ฅผ ํ†ตํ•œ ์ˆ˜๋™ ์‹คํ–‰ ๋ฐ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ํ†ตํ•œ ์ž๋™ ์‹คํ–‰ ์ง€์›

๐Ÿ’ฌ Review Points

(1) ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์ค‘์ ์ ์œผ๋กœ ๋ด์คฌ์œผ๋ฉด ํ•˜๋Š” ๋ถ€๋ถ„

1.1 ํ†ตํ•ฉ Job/Step ๊ตฌ์กฐ ์„ค๊ณ„

  • ์œ„์น˜: RankingAggregationJobConfig, RankingStepConfig
  • ์„ค๋ช…: Weekly/Monthly๋ฅผ ๋ณ„๋„ Job์œผ๋กœ ๋ถ„๋ฆฌํ•˜๋‹ค๊ฐ€ ์ค‘๋ณต ๋นˆ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜์—ฌ, periodType ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•œ ํ†ตํ•ฉ ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ํ™•์ธ ์š”์ฒญ:
    • ํ•˜๋‚˜์˜ Job์œผ๋กœ ํ†ตํ•ฉํ•œ ์„ค๊ณ„๊ฐ€ ์ ์ ˆํ•œ์ง€
    • ํ–ฅํ›„ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋กœ์ง์ด ๋‹ฌ๋ผ์งˆ ๊ฒฝ์šฐ ํ™•์žฅ์„ฑ ๋ฌธ์ œ๊ฐ€ ์—†๋Š”์ง€

1.2 PriorityQueue๋ฅผ ํ†ตํ•œ TOP 100 ํ•„ํ„ฐ๋ง ๋กœ์ง

  • ์œ„์น˜: RankingProcessor.process()
  • ์„ค๋ช…: ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ฆฐ ๋’ค ์ •๋ ฌํ•˜๋Š” ๋Œ€์‹ , Processor์—์„œ PriorityQueue๋กœ ์ƒ์œ„ 100๊ฐœ๋งŒ ์œ ์ง€ํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.

1.3 ProductMetricsDaily ๊ตฌํ˜„

์ผ๊ฐ„ ๋ฐ์ดํ„ฐ์™€ ๋ˆ„๊ณ„๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด daily ์ง‘๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ ๊ธฐ์กด ๋ฉ”ํŠธ๋ฆญ์€ ๋ˆ„๊ณ„๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
์ง€๊ธˆ์ด์•ผ ๋‚ ์งœ๋กœ ๊ทธ๋ฃนํ™”ํ•˜์ง€๋งŒ, ๋งŽ์€ ๋ณ€์ˆ˜(๋ชฐ๋ฆฌ๋Š” ์‹œ๊ฐ„ ๋“ฑ)๋“ค์„ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•์ด ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค.

(2) ๊ณ ๋ฏผํ–ˆ๋˜ ์„ค๊ณ„ ํฌ์ธํŠธ๋‚˜ ๋กœ์ง

2.1 Reader์˜ ๋‚ ์งœ ๋ฒ”์œ„ ์ž๋™ ๊ณ„์‚ฐ

  • ๊ณ ๋ฏผ: Weekly๋Š” Job ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋‚ ์งœ๋ฅผ ๋ฐ›๊ณ , Monthly๋Š” ์ตœ๊ทผ 30์ผ๋กœ ์ž๋™ ๊ณ„์‚ฐํ•ด์•ผ ํ•จ
  • ๊ฒฐ์ •: ProductMetricsDailyReaderFactory์—์„œ periodType์— ๋”ฐ๋ผ ๋‚ ์งœ ๋ฒ”์œ„๋ฅผ ๊ฒฐ์ •
    • Weekly: Job ํŒŒ๋ผ๋ฏธํ„ฐ ์šฐ์„ , ์—†์œผ๋ฉด ์ตœ๊ทผ 7์ผ
    • Monthly: ํ•ญ์ƒ ์ตœ๊ทผ 30์ผ๋กœ ์ž๋™ ๊ณ„์‚ฐ
  • ์ด์œ : ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง(๋‚ ์งœ ๊ณ„์‚ฐ)์„ Reader์— ๋‘์–ด ์œ ์—ฐ์„ฑ ํ™•๋ณด

(3) ๋ฆฌ๋ทฐ์–ด๊ฐ€ ํ™•์ธํ•ด์คฌ์œผ๋ฉด ํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋‚˜ ์˜ˆ์™ธ ์ƒํ™ฉ

(4) ๊ธฐํƒ€ ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์ฐธ๊ณ ํ•ด์•ผ ํ•  ์‚ฌํ•ญ


โœ… Checklist

๊ตฌํ˜„ ์™„๋ฃŒ

  • Spring Batch Job/Step ๊ตฌ์กฐ ์„ค๊ณ„ ๋ฐ ๊ตฌํ˜„
  • Weekly/Monthly ํ†ตํ•ฉ Job ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝ (์ค‘๋ณต ๋นˆ ๋ฌธ์ œ ํ•ด๊ฒฐ)
  • PriorityQueue๋ฅผ ํ™œ์šฉํ•œ TOP 100 ํ•„ํ„ฐ๋ง ๋กœ์ง ๊ตฌํ˜„
  • ExecutionContext ์ง๋ ฌํ™” ๋ฌธ์ œ ํ•ด๊ฒฐ (JSON ๋ณ€ํ™˜)
  • ThreadLocal์„ ํ†ตํ•œ PriorityQueue ๊ด€๋ฆฌ
  • Reader์—์„œ periodType์— ๋”ฐ๋ฅธ ๋‚ ์งœ ๋ฒ”์œ„ ์ž๋™ ๊ณ„์‚ฐ
  • StepExecutionListener์™€ JobExecutionListener ๊ตฌํ˜„
  • ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ ํ•œ๊ธ€ํ™”
  • ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ ๋ฐ ์ฃผ์„ ์ œ๊ฑฐ
  • Monthly ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ๋ฐ ํ†ตํ•ฉ

ํ…Œ์ŠคํŠธ

  • Weekly ๋ฐฐ์น˜ ์‹คํ–‰ ํ…Œ์ŠคํŠธ
  • Monthly ๋ฐฐ์น˜ ์‹คํ–‰ ํ…Œ์ŠคํŠธ
  • PriorityQueue ๋น„์–ด์žˆ์„ ๋•Œ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ
  • ์˜ˆ์™ธ ์ƒํ™ฉ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ
  • ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ

TODO - ํ–ฅํ›„ ๊ฐœ์„  ์‚ฌํ•ญ

  • ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ (Processor, Listener ๋“ฑ)
  • ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ (๋ฐฐ์น˜ ์‹คํ–‰ ์ „์ฒด ํ”Œ๋กœ์šฐ)
  • ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ฐœ์„  (ํ˜„์žฌ๋Š” ๋กœ๊ทธ๋งŒ ์ถœ๋ ฅํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Œ)
  • ๋ฐฐ์น˜ ์‹คํ–‰ ๊ฒฐ๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์•Œ๋ฆผ ๊ธฐ๋Šฅ
  • ๋ฐฐ์น˜ ์‹คํ–‰ ํžˆ์Šคํ† ๋ฆฌ ์กฐํšŒ API

๐Ÿ“Ž References

๊ด€๋ จ ๋ฌธ์„œ

์ฃผ์š” ๋ณ€๊ฒฝ ํŒŒ์ผ

  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/
    • RankingAggregationJobConfig.java: ํ†ตํ•ฉ Job ์ •์˜
    • RankingStepConfig.java: ํ†ตํ•ฉ Step ์ •์˜
    • ProductMetricsDailyReaderFactory.java: Reader (๋‚ ์งœ ๋ฒ”์œ„ ์ž๋™ ๊ณ„์‚ฐ)
    • RankingProcessor.java: Processor (TOP 100 ํ•„ํ„ฐ๋ง)
    • RankingStepExecutionListener.java: Step ๋ฆฌ์Šค๋„ˆ (TOP 100 ์ถ”์ถœ, JSON ๋ณ€ํ™˜)
    • RankingJobExecutionListener.java: Job ๋ฆฌ์Šค๋„ˆ (DB ์ €์žฅ)
    • RankingJsonConverter.java: JSON ๋ณ€ํ™˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/
    • RankingAggregationService.java: ๋ฐฐ์น˜ ์‹คํ–‰ ์„œ๋น„์Šค
    • RankingAggregationScheduler.java: ์Šค์ผ€์ค„๋Ÿฌ
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/
    • RankingBatchV1Controller.java: ๋ฐฐ์น˜ ์‹คํ–‰ API

Summary by CodeRabbit

  • ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ
    • ์ œํ’ˆ ์ˆœ์œ„์˜ ์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ์ž๋™ ์ง‘๊ณ„ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
    • ์ˆœ์œ„ ์ง‘๊ณ„๋ฅผ ์ˆ˜๋™์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” REST API ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€
    • ์ผ์ผ ์ œํ’ˆ ์ง€ํ‘œ(์กฐํšŒ, ์ข‹์•„์š”, ํŒ๋งค๋Ÿ‰) ์ถ”์  ๊ธฐ๋Šฅ ๊ตฌํ˜„
    • ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ธํ”„๋ผ ์ถ”๊ฐ€๋ฅผ ํ†ตํ•œ ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋Šฅ๋ ฅ ํ–ฅ์ƒ

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

์ด PR์€ Spring Batch๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ผ์ผ ์ œํ’ˆ ๋ฉ”ํŠธ๋ฆญ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ์ƒํ’ˆ ์ˆœ์œ„ ์ง‘๊ณ„๋ฅผ ์ž๋™ํ™”ํ•˜๋Š” ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ์„ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘, ๋ฐฐ์น˜ ์žก ๊ตฌ์„ฑ, ์Šค์ผ€์ค„๋ง ๋ฐ REST API ์—”๋“œํฌ์ธํŠธ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

Changes

Cohort / File(s) ์š”์•ฝ
๋ฐฐ์น˜ ์˜์กด์„ฑ
apps/commerce-api/build.gradle.kts
Spring Boot Batch ๋ฐ Batch ํ…Œ์ŠคํŠธ ์˜์กด์„ฑ ์ถ”๊ฐ€
๋ฐฐ์น˜ ์ธํ”„๋ผ ์„ค์ •
apps/commerce-api/src/main/java/com/loopers/infrastructure/batch/BatchConfig.java
JobRepository, JobLauncher, JobExplorer ๋นˆ ๊ตฌ์„ฑ
์ˆœ์œ„ ์ง‘๊ณ„ ์„œ๋น„์Šค
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationService.java,
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationScheduler.java,
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationInfo.java
์ฃผ๊ฐ„/์›”๊ฐ„ ์ˆœ์œ„ ์ง‘๊ณ„ ์‹คํ–‰ ๋ฐ ์Šค์ผ€์ค„๋ง ๋กœ์ง, ๋ฐฐ์น˜ ์‹คํ–‰ ๊ฒฐ๊ณผ ์ •๋ณด ๋ชจ๋ธ
๋ฐฐ์น˜ ์žก ๊ตฌ์„ฑ
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJobConfig.java,
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJob.java
๋žญํ‚น ์ง‘๊ณ„ ๋ฐฐ์น˜ ์žก ๋ฐ ์Šคํ… ์ •์˜
๋ฐฐ์น˜ ๋ฆฌ๋” ๋ฐ ํ”„๋กœ์„ธ์„œ
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/ProductMetricsDailyReaderFactory.java,
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java
์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ํŽ˜์ด์ง• ๋ฆฌ๋”, ์ƒ์œ„ 100๊ฐœ ์ œํ’ˆ ์„ ๋ณ„ ํ”„๋กœ์„ธ์„œ
๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java,
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java
์Šคํ…/์žก ์‹คํ–‰ ์ˆ˜๋ช…์ฃผ๊ธฐ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ, ์ˆœ์œ„ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋ฐ ์ €์žฅ
JSON ๋ณ€ํ™˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJsonConverter.java
์ˆœ์œ„ ๋ฐ์ดํ„ฐ์˜ JSON ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”
REST API
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1ApiSpec.java,
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1Controller.java,
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1Dto.java
๋ฐฐ์น˜ ์‹คํ–‰ API ๋ช…์„ธ, ์ปจํŠธ๋กค๋Ÿฌ, ์‘๋‹ต DTO
๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ - ์ผ์ผ ๋ฉ”ํŠธ๋ฆญ
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java,
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyAggregated.java
์ผ์ผ ์ œํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์—”ํ‹ฐํ‹ฐ ๋ฐ ์ง‘๊ณ„ DTO
๋ฉ”ํŠธ๋ฆญ ์ €์žฅ์†Œ
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyRepository.java,
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyJpaRepository.java,
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java
์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ณ„์ธต (๋„๋ฉ”์ธ ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ ๊ตฌํ˜„)
๋ฉ”ํŠธ๋ฆญ ์„œ๋น„์Šค
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyService.java
์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ/์ƒ์„ฑ/์—…๋ฐ์ดํŠธ ์„œ๋น„์Šค
๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ - ์ˆœ์œ„
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java,
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java
์ฃผ๊ฐ„/์›”๊ฐ„ ์ˆœ์œ„ ๋ทฐ ์—”ํ‹ฐํ‹ฐ
์ˆœ์œ„ ์ €์žฅ์†Œ
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.java,
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java,
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java,
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.java,
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java,
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java
์ฃผ๊ฐ„/์›”๊ฐ„ ์ˆœ์œ„ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ณ„์ธต
์ œํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์ธ๋ฑ์Šค
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.java
์ œํ’ˆ ID ์ปฌ๋Ÿผ์— DB ์ธ๋ฑ์Šค ์ถ”๊ฐ€
์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ํ†ตํ•ฉ
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/CatalogEventListener.java,
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/OrderEventListener.java
ProductMetricsDailyService๋ฅผ ์‚ฌ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as RankingAggregationScheduler
    participant Service as RankingAggregationService
    participant JobLauncher as JobLauncher
    participant Job as rankingJob
    participant Step as rankingChunkStep
    participant Repos as Repositories<br/>(Weekly/Monthly)
    
    Note over Scheduler: ๋งค์ผ ์ž์ • ์‹คํ–‰
    Scheduler->>Service: executeWeeklyAndMonthlyRanking(targetDate)
    
    rect rgb(100, 150, 200)
    Note over Service: Weekly ์ˆœ์œ„ ์ง‘๊ณ„
    Service->>JobLauncher: launch(Job, JobParameters)
    JobLauncher->>Job: execute()
    Job->>Step: execute()
    Step->>Repos: ์ €์žฅ
    Step-->>Job: StepExecution
    end
    
    rect rgb(150, 100, 200)
    Note over Service: Monthly ์ˆœ์œ„ ์ง‘๊ณ„
    Service->>JobLauncher: launch(Job, JobParameters)
    JobLauncher->>Job: execute()
    Job->>Step: execute()
    Step->>Repos: ์ €์žฅ
    Step-->>Job: StepExecution
    end
    
    Service->>Service: combined(weeklyResult, monthlyResult)
    Service-->>Scheduler: RankingAggregationInfo
Loading
sequenceDiagram
    participant Reader as ProductMetricsDailyReader
    participant Processor as RankingProcessor
    participant StepListener as RankingStepExecutionListener
    participant JobListener as RankingJobExecutionListener
    participant Repos as Repositories
    
    Note over Reader,JobListener: ๋ฐฐ์น˜ ์Šคํ… ์‹คํ–‰ ํ๋ฆ„
    
    StepListener->>Reader: beforeStep()
    Reader->>Reader: setDateRange()
    
    loop ํŽ˜์ด์ง€ ๋‹จ์œ„ ์ฒ˜๋ฆฌ
        Reader->>Reader: doPageRead()
        Reader-->>Processor: List<ProductMetricsDailyAggregated>
        Processor->>Processor: process(item)
        Note over Processor: ์ƒ์œ„ 100๊ฐœ ์šฐ์„ ์ˆœ์œ„<br/>ํ ์œ ์ง€
    end
    
    Processor-->>StepListener: afterChunk()
    StepListener->>StepListener: getTop100Queue()
    StepListener->>StepListener: createWeekly/MonthlyRankings()
    StepListener->>StepListener: jsonConverter.toJson()
    StepListener->>StepListener: ExecutionContext ์ €์žฅ
    
    JobListener->>JobListener: afterJob()
    JobListener->>StepListener: ExecutionContext ์กฐํšŒ
    JobListener->>Repos: deleteByRankingDate()
    JobListener->>Repos: saveAll(rankings)
Loading

Estimated code review effort

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

Possibly related PRs

  • [volume-9] Product Ranking with Redisย #211: ๋™์ผํ•œ ์ƒํ’ˆ ์ˆœ์œ„ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋ฉฐ, ์ˆœ์œ„ ์ €์žฅ์†Œ/์„œ๋น„์Šค/API ๋ฐ ์บ์‹œ/DTO ํƒ€์ž…์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.
  • [volume-9] Product Ranking with Redisย #216: ์ŠคํŠธ๋ฆฌ๋จธ์˜ RankingService/์ปจ์Šˆ๋จธ ์ฑ„์ ๊ณผ ์Šค์ผ€์ค„๋Ÿฌ, API ์ธก์˜ RankingCacheService/RankingFacade/์ปจํŠธ๋กค๋Ÿฌ/DTO๋ฅผ ํฌํ•จํ•˜์—ฌ ๋™์ผํ•œ ๊ธฐ๋Šฅ ์˜์—ญ์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.
  • [volume-9] Product Ranking with Redis ย #223: ProductMetricsDaily ์—”ํ‹ฐํ‹ฐ/์ €์žฅ์†Œ, ๋ฐฐ์น˜/์Šค์ผ€์ค„๋Ÿฌ ์ปดํฌ๋„ŒํŠธ, ์ˆœ์œ„ ํŒŒ์‚ฌ๋“œ/์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฐ™์€ ์ˆœ์œ„/๋ฉ”ํŠธ๋ฆญ ์ฝ”๋“œ ๊ฒฝ๋กœ๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.

Suggested labels

enhancement

Poem

๐Ÿฐ ๋ฐฐ์น˜ ์‹œ์Šคํ…œ์ด ๊ฟˆ๊พธ๋Š” ์ˆœ์œ„,
๋‚ ๋งˆ๋‹ค ๋ฉ”ํŠธ๋ฆญ ์Œ“์—ฌ๊ฐ€๋ฉฐ,
์ฃผ๊ฐ„ ์›”๊ฐ„ ์ง‘๊ณ„๋˜๋Š” ์ˆœ๊ฐ„,
์ƒํ’ˆ๋“ค์˜ ์ˆœ์œ„๊ฐ€ ๊ฒฐ์ •๋˜๋„ค!
์Šคํ”„๋ง ๋ฐฐ์น˜ ๋งˆ๋ฒ•์œผ๋กœ โœจ

Pre-merge checks and finishing touches

โŒ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check โš ๏ธ Warning PR ์ œ๋ชฉ '[volume-10] Collect, Stack, Zip'๋Š” ๋งค์šฐ ๋ชจํ˜ธํ•˜๊ณ  ์ถ”์ƒ์ ์ด๋ฉฐ, ์‹ค์ œ ๋ณ€๊ฒฝ์‚ฌํ•ญ(Spring Batch ๊ธฐ๋ฐ˜ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„)๊ณผ ๋ช…ํ™•ํ•œ ์—ฐ๊ด€์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ œ๋ชฉ์„ '[volume-10] Implement Spring Batch ranking aggregation for weekly/monthly rankings' ๊ฐ™์ด ๊ตฌ์ฒด์ ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ณ€๊ฒฝ์‚ฌํ•ญ์˜ ํ•ต์‹ฌ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ „๋‹ฌํ•˜์„ธ์š”.
Docstring Coverage โš ๏ธ Warning Docstring coverage is 31.31% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
โœ… Passed checks (1 passed)
Check name Status Explanation
Description check โœ… Passed PR ์„ค๋ช…์ด ํ…œํ”Œ๋ฆฟ์„ ์ถฉ๋ถ„ํžˆ ๋”ฐ๋ฅด๊ณ  ์žˆ์œผ๋ฉฐ, Summary, Review Points, Checklist, References ์„น์…˜์ด ๋ชจ๋‘ ํฌํ•จ๋˜์–ด ์žˆ๊ณ  ์ƒ์„ธํ•œ ์ •๋ณด๊ฐ€ ๊ธฐ์žฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

โค๏ธ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

๐Ÿงน Nitpick comments (19)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/OrderEventListener.java (1)

205-205: ํƒ€์ž„์กด ์ฒ˜๋ฆฌ ๋ฐ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ ๊ฐœ์„  ๊ถŒ์žฅ

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

ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ๊ณผ ํƒ€์ž„์กด ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด Clock ์ธ์Šคํ„ด์Šค๋ฅผ ์ฃผ์ž…๋ฐ›์•„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž Clock ์ฃผ์ž…์„ ํ†ตํ•œ ๊ฐœ์„  ๋ฐฉ์•ˆ

ํด๋ž˜์Šค์— Clock ํ•„๋“œ ์ถ”๊ฐ€:

 @Slf4j
 @Component
 @RequiredArgsConstructor
 public class OrderEventListener {
 
     private final IdempotencyService idempotencyService;
     private final ProductMetricsService productMetricsService;
     private final ProductMetricsDailyService productMetricsDailyService;
     private final OrderRepository orderRepository;
     private final DlqService dlqService;
     private final ObjectMapper objectMapper;
+    private final Clock clock;

์‚ฌ์šฉ ๋ถ€๋ถ„ ์ˆ˜์ •:

                 try {
                     productMetricsService.incrementSoldCount(productId, Long.valueOf(quantity));
-                    productMetricsDailyService.incrementSoldCount(productId, LocalDate.now(), Long.valueOf(quantity));
+                    productMetricsDailyService.incrementSoldCount(productId, LocalDate.now(clock), Long.valueOf(quantity));
                     log.debug("Incremented sold count for product: productId={}, quantity={}", 
                             productId, quantity);

์„ค์ • ํด๋ž˜์Šค์—์„œ Clock ๋นˆ ๋“ฑ๋ก:

@Bean
public Clock clock() {
    return Clock.system(ZoneId.of("Asia/Seoul")); // ๋˜๋Š” UTC
}
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepConfig.java (1)

28-28: ์ฒญํฌ ํฌ๊ธฐ 1000์— ๋Œ€ํ•œ ์„ฑ๋Šฅ ๊ณ ๋ ค

์ฒญํฌ ํฌ๊ธฐ๊ฐ€ 1000์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ์ ์ ˆํ•œ ํฌ๊ธฐ์ด์ง€๋งŒ, ์‹ค์ œ ๋ฐ์ดํ„ฐ ๋ณผ๋ฅจ๊ณผ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์— ๋”ฐ๋ผ ์กฐ์ •์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

RankingProcessor๊ฐ€ thread-local PriorityQueue๋กœ TOP 100๋งŒ ์œ ์ง€ํ•œ๋‹ค๊ณ  ํ•˜๋”๋ผ๋„, ์ฒญํฌ ๋‹จ์œ„ ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์‹œ ๋ถ€ํ•˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ ํ›„ ํ•„์š”์‹œ ์ฒญํฌ ํฌ๊ธฐ๋ฅผ ์กฐ์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค (์˜ˆ: 500 ๋˜๋Š” 2000). ๋‹ค์Œ ์š”์†Œ๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”:

  • ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ (์ฒญํฌ ํฌ๊ธฐ ร— ์•„์ดํ…œ ํฌ๊ธฐ)
  • ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ๋นˆ๋„
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปค๋„ฅ์…˜ ํƒ€์ž„์•„์›ƒ
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyAggregated.java (1)

18-20: ์ ์ˆ˜ ๊ณ„์‚ฐ ๊ฐ€์ค‘์น˜์— ๋Œ€ํ•œ ๋ฌธ์„œํ™” ๊ถŒ์žฅ

์ ์ˆ˜ ๊ณ„์‚ฐ ๊ณต์‹ ์กฐํšŒ์ˆ˜ * 0.1 + ์ข‹์•„์š” * 0.2 + ํŒ๋งค๋Ÿ‰ * 0.7์˜ ๊ฐ€์ค‘์น˜ ๋น„์œจ์ด ์ฝ”๋“œ์—๋งŒ ์กด์žฌํ•˜์—ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

ํ–ฅํ›„ ๊ฐ€์ค‘์น˜ ์กฐ์ •์ด๋‚˜ ๊ฒ€์ฆ์„ ์œ„ํ•ด ๋‹ค์Œ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค:

  • ๊ฐ€์ค‘์น˜๋ฅผ ์ƒ์ˆ˜๋กœ ๋ถ„๋ฆฌ
  • ๊ฐ€์ค‘์น˜ ์„ ์ • ๊ทผ๊ฑฐ๋ฅผ JavaDoc์— ๋ฌธ์„œํ™”
๐Ÿ”Ž ๋ฌธ์„œํ™” ๊ฐœ์„  ์ œ์•ˆ
+    /**
+     * ๋žญํ‚น ์ ์ˆ˜ ๊ณ„์‚ฐ ๊ฐ€์ค‘์น˜
+     * - ์กฐํšŒ์ˆ˜: 10% - ๊ด€์‹ฌ๋„ ์ง€ํ‘œ
+     * - ์ข‹์•„์š”: 20% - ์„ ํ˜ธ๋„ ์ง€ํ‘œ  
+     * - ํŒ๋งค๋Ÿ‰: 70% - ์‹ค์ œ ๊ตฌ๋งค ์ „ํ™˜ ์ง€ํ‘œ (๊ฐ€์žฅ ์ค‘์š”)
+     */
+    private static final double WEIGHT_VIEW = 0.1;
+    private static final double WEIGHT_LIKE = 0.2;
+    private static final double WEIGHT_SOLD = 0.7;
+
+    /**
+     * ์ œํ’ˆ์˜ ๋žญํ‚น ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
+     * ํŒ๋งค๋Ÿ‰์„ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์ง€ํ‘œ๋กœ ํ•˜์—ฌ ๊ฐ€์ค‘ ํ‰๊ท ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
+     * 
+     * @return ๊ฐ€์ค‘์น˜๊ฐ€ ์ ์šฉ๋œ ๋žญํ‚น ์ ์ˆ˜
+     */
     public double calculateScore() {
         long viewCount = totalViewCount != null ? totalViewCount : 0L;
         long likeCount = totalLikeCount != null ? totalLikeCount : 0L;
         long soldCount = totalSoldCount != null ? totalSoldCount : 0L;
-        return viewCount * 0.1 + likeCount * 0.2 + soldCount * 0.7;
+        return viewCount * WEIGHT_VIEW + likeCount * WEIGHT_LIKE + soldCount * WEIGHT_SOLD;
     }
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationScheduler.java (1)

42-45: ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ „๋žต ๊ฐœ์„  ๊ฒ€ํ† 

๋ชจ๋“  ์˜ˆ์™ธ๋ฅผ catchํ•˜์—ฌ ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์žˆ๋Š”๋ฐ, ์ด๋Š” ๋ฐฐ์น˜ ์‹คํŒจ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ธฐ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

  • ์•Œ๋ฆผ(Slack, ์ด๋ฉ”์ผ ๋“ฑ)์„ ํ†ตํ•œ ์‹คํŒจ ํ†ต์ง€
  • ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘์„ ํ†ตํ•œ ์‹คํŒจ์œจ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ํŠน์ • ์˜ˆ์™ธ(์ผ์‹œ์  ์‹คํŒจ vs ์˜๊ตฌ์  ์˜ค๋ฅ˜)์— ๋Œ€ํ•œ ์ฐจ๋ณ„ํ™”๋œ ์ฒ˜๋ฆฌ
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java (1)

11-15: ์—”ํ‹ฐํ‹ฐ์˜ @Setter ์‚ฌ์šฉ ์žฌ๊ณ 

JPA ์—”ํ‹ฐํ‹ฐ์— @Setter๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฌด๋ถ„๋ณ„ํ•œ ์ƒํƒœ ๋ณ€๊ฒฝ์ด ๊ฐ€๋Šฅํ•ด์ ธ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์šฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค:

  • ํ•„์š”ํ•œ ํ•„๋“œ์—๋งŒ ๊ฐœ๋ณ„ setter ์ œ๊ณต
  • ๋˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ์˜๋ฏธ๊ฐ€ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋กœ ์ƒํƒœ ๋ณ€๊ฒฝ (์˜ˆ: updateRanking(int newRanking))
  • ๋ถˆ๋ณ€์„ฑ์ด ๋ณด์žฅ๋˜์–ด์•ผ ํ•˜๋Š” ํ•„๋“œ๋Š” setter ์ œ๊ฑฐ

๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์—์„œ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์“ฐ๊ธฐ ์œ„ํ•ด setter๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋ผ๋„, ์ตœ์†Œํ•œ ๋ฌธ์„œํ™”ํ•˜์—ฌ ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

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

25-29: ThreadLocal ์‚ฌ์šฉ ์‹œ synchronized ๋ธ”๋ก ๋ถˆํ•„์š”

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

๐Ÿ”Ž ๋‹จ์ผ ์Šค๋ ˆ๋“œ ์ฒ˜๋ฆฌ ์‹œ ๊ฐ„์†Œํ™” ์ œ์•ˆ
-        synchronized (queue) {
-            if (queue.size() < 100) {
-                queue.offer(item);
-                log.info("[RankingProcessor] process: ํ์— ์•„์ดํ…œ ์ถ”๊ฐ€ - productId={}, score={}, queueSize={}",
-                        item.getProductId(), item.calculateScore(), queue.size());
-            } else {
-                ProductMetricsDailyAggregated min = queue.peek();
-                if (min != null && item.calculateScore() > min.calculateScore()) {
-                    ProductMetricsDailyAggregated removed = queue.poll();
-                    queue.offer(item);
-                    log.info("[RankingProcessor] process: ํ์—์„œ ์•„์ดํ…œ ๊ต์ฒด - ์ œ๊ฑฐ๋œ productId={} (score={}), ์ถ”๊ฐ€๋œ productId={} (score={}), queueSize={}",
-                            removed.getProductId(), removed.calculateScore(), item.getProductId(), item.calculateScore(), queue.size());
-                } else {
-                    log.debug("[RankingProcessor] process: ์•„์ดํ…œ ๊ฑฐ๋ถ€๋จ - productId={}, score={}, minScore={}",
-                            item.getProductId(), item.calculateScore(), min != null ? min.calculateScore() : "null");
-                }
+        if (queue.size() < 100) {
+            queue.offer(item);
+            log.info("[RankingProcessor] process: ํ์— ์•„์ดํ…œ ์ถ”๊ฐ€ - productId={}, score={}, queueSize={}",
+                    item.getProductId(), item.calculateScore(), queue.size());
+        } else {
+            ProductMetricsDailyAggregated min = queue.peek();
+            if (min != null && item.calculateScore() > min.calculateScore()) {
+                ProductMetricsDailyAggregated removed = queue.poll();
+                queue.offer(item);
+                log.info("[RankingProcessor] process: ํ์—์„œ ์•„์ดํ…œ ๊ต์ฒด - ์ œ๊ฑฐ๋œ productId={} (score={}), ์ถ”๊ฐ€๋œ productId={} (score={}), queueSize={}",
+                        removed.getProductId(), removed.calculateScore(), item.getProductId(), item.calculateScore(), queue.size());
+            } else {
+                log.debug("[RankingProcessor] process: ์•„์ดํ…œ ๊ฑฐ๋ถ€๋จ - productId={}, score={}, minScore={}",
+                        item.getProductId(), item.calculateScore(), min != null ? min.calculateScore() : "null");
             }
         }

Also applies to: 62-79

apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java (1)

35-42: ํ•„๋“œ ํƒ€์ž… ์ผ๊ด€์„ฑ ๊ณ ๋ ค

likeCount๋Š” Integer์ด๊ณ , viewCount์™€ soldCount๋Š” Long์ž…๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋ฒ”์œ„์— ๋”ฐ๋ฅธ ์˜๋„์  ์„ ํƒ์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ–ฅํ›„ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์œ„ํ•ด ์ผ๊ด€๋œ ํƒ€์ž… ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

18-20: ํƒ€์ž… ์•ˆ์ „์„ฑ ๊ฐœ์„  ํ•„์š”

๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ raw type List๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ œ๋„ค๋ฆญ ํƒ€์ž…์„ ๋ช…์‹œํ•˜์—ฌ ์ปดํŒŒ์ผ ํƒ€์ž„ ํƒ€์ž… ์ฒดํฌ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ๊ฐœ์„ ์•ˆ
-    public String toJson(List items) throws JsonProcessingException {
+    public String toJson(List<?> items) throws JsonProcessingException {
         return objectMapper.writeValueAsString(items);
     }

๋˜๋Š” ์ œ๋„ค๋ฆญ ๋ฉ”์„œ๋“œ๋กœ ๋” ์—„๊ฒฉํ•˜๊ฒŒ ์ œํ•œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

-    public String toJson(List items) throws JsonProcessingException {
+    public <T> String toJson(List<T> items) throws JsonProcessingException {
         return objectMapper.writeValueAsString(items);
     }
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1ApiSpec.java (1)

16-20: ์ค‘๋ณต๋œ ์–ด๋…ธํ…Œ์ด์…˜ ์ •๋ฆฌ ๊ถŒ์žฅ

targetDate ํŒŒ๋ผ๋ฏธํ„ฐ์— @Parameter์™€ @Schema ์–ด๋…ธํ…Œ์ด์…˜์ด ๋™์ผํ•œ ๋‚ด์šฉ์œผ๋กœ ์ค‘๋ณต ์„ ์–ธ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ @Parameter๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•˜๋ฉฐ, @Schema๋Š” ๋ณต์žกํ•œ ํƒ€์ž… ์ •์˜๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

์ด ํŒจํ„ด์ด ๋‹ค๋ฅธ ๋‘ ๋ฉ”์„œ๋“œ์—๋„ ๋ฐ˜๋ณต๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค (lines 27-31, 38-42).

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ๊ฐœ์„ ์•ˆ
     ApiResponse<RankingBatchV1Dto.BatchExecutionResponse> executeWeeklyRanking(
         @Parameter(description = "์ง‘๊ณ„ ๋Œ€์ƒ ๋‚ ์งœ (yyyy-MM-dd ๋˜๋Š” ISO_ZONED_DATE_TIME ํ˜•์‹, ๋ฏธ์ง€์ • ์‹œ ํ˜„์žฌ ์‹œ๊ฐ„)", example = "2024-12-19")
-        @Schema(description = "์ง‘๊ณ„ ๋Œ€์ƒ ๋‚ ์งœ (yyyy-MM-dd ๋˜๋Š” ISO_ZONED_DATE_TIME ํ˜•์‹, ๋ฏธ์ง€์ • ์‹œ ํ˜„์žฌ ์‹œ๊ฐ„)", example = "2024-12-19")
         String targetDate
     );
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyService.java (1)

39-49: ์‹ ๊ทœ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ์‹œ ๋ถˆํ•„์š”ํ•œ ์ค‘๋ณต ์ €์žฅ ๋ฐœ์ƒ

์‹ ๊ทœ ProductMetricsDaily ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ์šฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‘ ๋ฒˆ์˜ ์ €์žฅ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค:

  1. orElseGet ๋‚ด๋ถ€์—์„œ ๋นˆ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ํ›„ ์ €์žฅ (line 44)
  2. ์ฆ๊ฐ€ ํ›„ ๋‹ค์‹œ ์ €์žฅ (line 47)

์ด๋Š” ์„ฑ๋Šฅ์ƒ ๋น„ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. ์‹ ๊ทœ ์—”ํ‹ฐํ‹ฐ์˜ ๊ฒฝ์šฐ ์ดˆ๊ธฐ๊ฐ’์„ 1๋กœ ์„ค์ •ํ•˜์—ฌ ํ•œ ๋ฒˆ๋งŒ ์ €์žฅํ•˜๋Š” ๊ฒƒ์ด ๋” ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.

๋™์ผํ•œ ํŒจํ„ด์ด incrementViewCount (lines 68-77) ๋ฐ incrementSoldCount (lines 83-93) ๋ฉ”์„œ๋“œ์—๋„ ๋ฐ˜๋ณต๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ๊ฐœ์„ ์•ˆ
     @Transactional
     public void incrementLikeCount(Long productId, LocalDate date) {
-        ProductMetricsDaily daily = repository.findByProductIdAndDateForUpdate(productId, date)
-            .orElseGet(() -> {
-                ProductMetricsDaily newDaily = ProductMetricsDaily.create(productId, date);
-                return repository.save(newDaily);
-            });
-        daily.incrementLikeCount();
-        repository.save(daily);
+        ProductMetricsDaily daily = repository.findByProductIdAndDateForUpdate(productId, date)
+            .orElseGet(() -> ProductMetricsDaily.createWithLikeCount(productId, date, 1L));
+        
+        if (daily.getId() == null) {
+            // ์‹ ๊ทœ ์ƒ์„ฑ๋œ ์—”ํ‹ฐํ‹ฐ
+            repository.save(daily);
+        } else {
+            // ๊ธฐ์กด ์—”ํ‹ฐํ‹ฐ ์ฆ๊ฐ€
+            daily.incrementLikeCount();
+            repository.save(daily);
+        }
         log.debug("Incremented daily like count: productId={}, date={}", productId, date);
     }

๋˜๋Š” ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ, ProductMetricsDaily์— ์ดˆ๊ธฐ๊ฐ’์„ ๋ฐ›๋Š” ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

44-46: ๋‚ ์งœ ํŒŒ์‹ฑ ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ˆ„๋ฝ

LocalDate.parse()๋Š” ์ž˜๋ชป๋œ ํ˜•์‹์˜ ๋ฌธ์ž์—ด์— ๋Œ€ํ•ด DateTimeParseException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ Job ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ธํ•ด ๋ฐฐ์น˜๊ฐ€ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋˜๋Š” ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
             // weekly: Job ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ๋‚ ์งœ๊ฐ€ ์ œ๊ณต๋œ ๊ฒฝ์šฐ ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ตœ๊ทผ 7์ผ
             if (startDateStr != null && endDateStr != null) {
-                startDate = LocalDate.parse(startDateStr);
-                endDate = LocalDate.parse(endDateStr);
+                try {
+                    startDate = LocalDate.parse(startDateStr);
+                    endDate = LocalDate.parse(endDateStr);
+                } catch (DateTimeParseException e) {
+                    log.warn("[ProductMetricsDailyReaderFactory] Invalid date format, using default 7 days: startDate={}, endDate={}", startDateStr, endDateStr);
+                    startDate = endDate.minusDays(7);
+                }
                 log.info("[ProductMetricsDailyReaderFactory] Using provided date range for weekly: startDate={}, endDate={}", startDate, endDate);

99-105: CopyOnWriteArrayList ์‚ฌ์šฉ์€ ๋ถˆํ•„์š”ํ•œ ์˜ค๋ฒ„ํ—ค๋“œ ๋ฐœ์ƒ

CopyOnWriteArrayList๋Š” ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ์—๋Š” ์œ ์šฉํ•˜์ง€๋งŒ, ๋ฐฐ์น˜ Reader๋Š” ๋‹จ์ผ ์Šค๋ ˆ๋“œ์—์„œ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰๋˜๋ฏ€๋กœ ์ผ๋ฐ˜ ArrayList๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ๋งค ํŽ˜์ด์ง€๋งˆ๋‹ค ๋ฐฐ์—ด ๋ณต์‚ฌ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
-import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.ArrayList;
             // ๊ฒฐ๊ณผ๋ฅผ results์— ์ €์žฅ (๋ถ€๋ชจ ํด๋ž˜์Šค์˜ ํ•„๋“œ)
             if (results == null) {
-                results = new CopyOnWriteArrayList<>();
+                results = new ArrayList<>();
             } else {
                 results.clear();
             }
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java (2)

67-70: ์ž˜๋ชป๋œ periodType ์ฒ˜๋ฆฌ ํ›„ ๋ช…์‹œ์  ๋ฐ˜ํ™˜ ์ถ”๊ฐ€ ๊ถŒ์žฅ

addFailureException ํ˜ธ์ถœ ํ›„ ๋ฉ”์„œ๋“œ๊ฐ€ ๊ณ„์† ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ฝ”๋“œ ํ๋ฆ„์ƒ ๋ฌธ์ œ๋Š” ์—†์ง€๋งŒ, ๋ช…์‹œ์ ์œผ๋กœ return์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์˜๋„๊ฐ€ ๋” ๋ช…ํ™•ํ•ด์ง‘๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
             } else {
                 log.error("[RankingJobExecutionListener] afterJob: ์ž˜๋ชป๋œ periodType={}, 'weekly' ๋˜๋Š” 'monthly'์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค", periodType);
                 jobExecution.addFailureException(new IllegalArgumentException("Invalid periodType: " + periodType));
+                return;
             }

78-139: ์ฃผ๊ฐ„/์›”๊ฐ„ ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ์˜ ์ค‘๋ณต ์ฝ”๋“œ

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

Also applies to: 141-202

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

142-178: N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ: ๋ฃจํ”„ ๋‚ด ๊ฐœ๋ณ„ ์กฐํšŒ

createWeeklyRankings ๋ฉ”์„œ๋“œ์—์„œ ๊ฐ ์•„์ดํ…œ๋งˆ๋‹ค productService.getProductById์™€ brandService.getBrandById๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. TOP 100 ์ฒ˜๋ฆฌ ์‹œ ์ตœ๋Œ€ 200๊ฐœ์˜ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„ฑ๋Šฅ ๊ฐœ์„ ์„ ์œ„ํ•ด productId ๋ชฉ๋ก์œผ๋กœ ์ผ๊ด„ ์กฐํšŒ ํ›„ Map์œผ๋กœ ์บ์‹ฑํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ๊ฐœ์„  ๋ฐฉํ–ฅ
// ์˜ˆ์‹œ: ์ผ๊ด„ ์กฐํšŒ ๋ฐฉ์‹
List<Long> productIds = top100.stream()
    .map(ProductMetricsDailyAggregated::getProductId)
    .toList();
Map<Long, Product> productMap = productService.getProductsByIds(productIds).stream()
    .collect(Collectors.toMap(Product::getId, Function.identity()));

// ์ดํ›„ ๋ฃจํ”„์—์„œ productMap.get(aggregated.getProductId()) ์‚ฌ์šฉ

187-228: createMonthlyRankings์—๋„ ๋™์ผํ•œ ๊ฐœ์„  ์‚ฌํ•ญ ์ ์šฉ ํ•„์š”

createWeeklyRankings์—์„œ ์–ธ๊ธ‰๋œ N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ์™€ null rankingDate ์ฒ˜๋ฆฌ ์ด์Šˆ๊ฐ€ ์ด ๋ฉ”์„œ๋“œ์—๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java (1)

18-20: @Modifying์— clearAutomatically ์˜ต์…˜ ๊ณ ๋ ค

@Modifying ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๋ฒŒํฌ ์‚ญ์ œ ์‹œ, ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์™€ DB ๊ฐ„ ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋™์ผ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ์‚ญ์ œ ํ›„ ๋‹ค์‹œ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•˜์—ฌ clearAutomatically = true ์˜ต์…˜ ์ถ”๊ฐ€๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
-    @Modifying
+    @Modifying(clearAutomatically = true)
     @Query("DELETE FROM MvProductRankWeekly r WHERE r.rankingDate = :rankingDate")
     void deleteByRankingDate(@Param("rankingDate") ZonedDateTime rankingDate);
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java (1)

48-57: Object[] โ†’ ProductMetricsDailyAggregated ๋ณ€ํ™˜ ๋กœ์ง ์ค‘๋ณต

findAggregatedByDateBetween๊ณผ findAggregatedByDateBetweenPaged์—์„œ ๋™์ผํ•œ ๋งคํ•‘ ๋กœ์ง์ด ๋ฐ˜๋ณต๋ฉ๋‹ˆ๋‹ค. private ํ—ฌํผ ๋ฉ”์„œ๋“œ๋กœ ์ถ”์ถœํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
private ProductMetricsDailyAggregated mapToAggregated(Object[] row) {
    return new ProductMetricsDailyAggregated(
        ((Number) row[0]).longValue(),      // productId
        ((Number) row[1]).longValue(),      // totalLikeCount
        ((Number) row[2]).longValue(),      // totalViewCount
        ((Number) row[3]).longValue()       // totalSoldCount
    );
}

Also applies to: 65-70

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

26-54: ์—”๋“œํฌ์ธํŠธ ๊ตฌํ˜„์ด ์ผ๊ด€๋˜๊ณ  ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

์„ธ ์—”๋“œํฌ์ธํŠธ ๋ชจ๋‘ ๋™์ผํ•œ ํŒจํ„ด์„ ๋”ฐ๋ฅด๋ฉฐ, ์„œ๋น„์Šค ๋ ˆ์ด์–ด์— ์ ์ ˆํžˆ ์œ„์ž„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. targetDate ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์„ ํƒ์ ์œผ๋กœ ๋ฐ›์•„ ์œ ์—ฐ์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์„ ํƒ์  ์ œ์•ˆ: ์ค‘๋ณต ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง

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

private ApiResponse<RankingBatchV1Dto.BatchExecutionResponse> executeRanking(
        String targetDate, 
        Function<ZonedDateTime, RankingAggregationInfo> executor) {
    ZonedDateTime date = parseDateTime(targetDate);
    RankingAggregationInfo result = executor.apply(date);
    return ApiResponse.success(RankingBatchV1Dto.BatchExecutionResponse.from(result));
}

๋‹จ, ํ˜„์žฌ ๊ตฌ์กฐ๋„ ์ถฉ๋ถ„ํžˆ ๋ช…ํ™•ํ•˜๋ฏ€๋กœ ์„ ํƒ์ ์œผ๋กœ ๊ณ ๋ คํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between c725371 and 1969acc.

๐Ÿ“’ Files selected for processing (33)
  • apps/commerce-api/build.gradle.kts
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationScheduler.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationService.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/ProductMetricsDailyReaderFactory.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJob.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJobConfig.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJsonConverter.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepConfig.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/batch/BatchConfig.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1Dto.java
  • apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.java
  • apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java
  • apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyAggregated.java
  • apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyRepository.java
  • apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyService.java
  • apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java
  • apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.java
  • apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java
  • apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.java
  • apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyJpaRepository.java
  • apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java
  • apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java
  • apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java
  • apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java
  • apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/CatalogEventListener.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/OrderEventListener.java
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (10)
๐Ÿ“š Learning: 2025-11-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product ์—”ํ‹ฐํ‹ฐ (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)๋Š” ์œ ์ฆˆ์ผ€์ด์Šค๋ณ„๋กœ ์˜๋„์ ์œผ๋กœ ๋‹ค๋ฅธ ๋ฝ ์ „๋žต์„ ์‚ฌ์šฉํ•œ๋‹ค: ์ข‹์•„์š” ๊ธฐ๋Šฅ์—๋Š” ๋น„๊ด€์  ๋ฝ(findByIdForUpdate)์„, ์žฌ๊ณ  ์ฐจ๊ฐ์—๋Š” ๋‚™๊ด€์  ๋ฝ(Version + ์žฌ์‹œ๋„)์„ ์‚ฌ์šฉํ•œ๋‹ค.

Applied to files:

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

Applied to files:

  • apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java
  • apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java
  • apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java
  • apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyJpaRepository.java
  • apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyRepository.java
  • apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.java
๐Ÿ“š Learning: 2025-11-21T03:38:07.494Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 99
File: apps/commerce-api/src/main/resources/application.yml:26-30
Timestamp: 2025-11-21T03:38:07.494Z
Learning: The batch job implementation for likeCount synchronization in apps/commerce-api is temporary and intended for development environment only. It will be replaced with Event-Driven Architecture (EDA) before production deployment, so production-level configuration concerns (like profile-based initialize-schema settings) are not required.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/infrastructure/batch/BatchConfig.java
๐Ÿ“š Learning: 2025-12-19T03:23:54.756Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 191
File: modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java:24-34
Timestamp: 2025-12-19T03:23:54.756Z
Learning: In the loopers-spring-java-template project, test container configurations (MySQL, Redis, Kafka) use static initialization blocks for consistency across the template. This pattern should be maintained for uniformity, even if alternative lifecycle management approaches exist.

Applied to files:

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

Applied to files:

  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/CatalogEventListener.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/OrderEventListener.java
๐Ÿ“š Learning: 2025-12-19T21:30:16.024Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/outbox/OutboxEventListener.kt:0-0
Timestamp: 2025-12-19T21:30:16.024Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template Kafka event pipeline, Like events (LikeCreatedEventV1, LikeCanceledEventV1) intentionally use aggregateType="Like" with aggregateId=productId. The aggregateId serves as a partitioning/grouping key (not a unique Like entity identifier), ensuring all like events for the same product go to the same partition for ordering guarantees and aligning with ProductStatisticService's product-based aggregation logic. Using individual like_id would scatter events across partitions and break the statistics aggregation pattern.

Applied to files:

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

Applied to files:

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

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java
๐Ÿ“š Learning: 2026-01-01T14:12:07.943Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 76
File: apps/commerce-batch/src/main/kotlin/com/loopers/batch/listener/JobListener.kt:4-5
Timestamp: 2026-01-01T14:12:07.943Z
Learning: Spring Batch 6.x์—์„œ JobExecution์€ org.springframework.batch.core.job ํŒจํ‚ค์ง€์—, JobExecutionListener๋Š” org.springframework.batch.core.listener ํŒจํ‚ค์ง€์— ์œ„์น˜ํ•ฉ๋‹ˆ๋‹ค. Spring Batch 5.x์™€ ๋‹ฌ๋ฆฌ ํ•˜์œ„ ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java
๐Ÿ“š Learning: 2025-11-12T13:04:50.782Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 72
File: core/domain/src/main/java/com/loopers/core/domain/productlike/vo/ProductLikeId.java:5-7
Timestamp: 2025-11-12T13:04:50.782Z
Learning: In the com.loopers codebase, domain entity ID value objects (e.g., ProductLikeId, OrderItemId, ProductId, PaymentId, OrderId, BrandId) are system-generated identifiers and do not require pattern validation (regex, length checks). They are implemented as simple records with a String value and an empty() factory method returning null for unsaved entities. This differs from UserIdentifier, which is a user-supplied login ID that requires format validation. Domain IDs should not be flagged for missing validation logic in the create() method.
<!-- [add_learning]
UserIdentifier์™€ ๊ฐ™์€ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ID์™€ ProductLikeId, OrderItemId ๋“ฑ์˜ ๋„๋ฉ”์ธ ID๋Š” ๊ฒ€์ฆ ํŒจํ„ด์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. UserIdentifier๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•˜๋Š” ๋กœ๊ทธ์ธ ID๋กœ์„œ ์ •๊ทœ์‹, ๊ธธ์ด ๋“ฑ์˜ ํ˜•์‹ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜์ง€๋งŒ, ๋„๋ฉ”์ธ ID๋Š” ์‹œ์Šคํ…œ์—์„œ ์ƒ์„ฑํ•˜๋Š” ์‹๋ณ„์ž(UUID, DB ์ƒ์„ฑ ID)์ด๋ฏ€๋กœ ํŒจํ„ด ๊ฒ€์ฆ์ด ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋„๋ฉ”์ธ ID VO๋Š” ๋‹จ์ˆœํ•œ record์™€ empty() ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

Applied to files:

  • apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java
๐Ÿงฌ Code graph analysis (9)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java (2)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
  • Entity (11-26)
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyAggregated.java (1)
  • Getter (10-21)
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/ProductMetricsDailyReaderFactory.java (1)
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java (1)
  • Component (18-84)
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJsonConverter.java (2)
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
  • Component (13-41)
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (1)
  • Component (13-41)
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyService.java (1)
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java (1)
  • Component (18-84)
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (1)
  • Component (13-41)
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJobConfig.java (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/batch/BatchConfig.java (1)
  • Configuration (18-52)
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (1)
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
  • Component (13-41)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java (1)
  • Entity (11-26)
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java (1)
  • Slf4j (19-120)
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJsonConverter.java (1)
  • Component (12-41)
๐Ÿ”‡ Additional comments (34)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/CatalogEventListener.java (2)

4-4: ์ผ๋ณ„ ๋ฉ”ํŠธ๋ฆญ ์„œ๋น„์Šค ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

ProductMetricsDailyService ๋„์ž…์œผ๋กœ ์ผ๋ณ„ ์ง‘๊ณ„ ๊ธฐ๋Šฅ์„ ๋ˆ„๊ณ„์™€ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ์ฃผ์„๋„ ์ž˜ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Also applies to: 17-17, 28-29


145-167: ๋ˆ„๊ณ„์™€ ์ผ๋ณ„ ๋ฉ”ํŠธ๋ฆญ ์ด์ค‘ ์—…๋ฐ์ดํŠธ ํŒจํ„ด์€ ์•ˆ์ „ํ•˜๊ฒŒ ๊ตฌํ˜„๋จ

๊ฐ ์ด๋ฒคํŠธ ํƒ€์ž…๋งˆ๋‹ค ๋ˆ„๊ณ„์™€ ์ผ๋ณ„ ๋ฉ”ํŠธ๋ฆญ์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์ด์ค‘ ์—…๋ฐ์ดํŠธ ํŒจํ„ด์ด ์ ์šฉ๋˜์—ˆ์œผ๋ฉฐ, @Transactional ๋ฒ”์œ„ ๋‚ด์—์„œ ์‹คํ–‰๋˜๋ฏ€๋กœ ์ผ๊ด€์„ฑ์ด ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค.

ProductUnliked ์ด๋ฒคํŠธ์—์„œ decrementLikeCount ํ˜ธ์ถœ ์‹œ ์Œ์ˆ˜ ๊ฐ’์ด ๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ์šฐ๋ ค๋Š” ์ด๋ฏธ ์ฝ”๋“œ์—์„œ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ProductMetricsDaily ๋ฐ ProductMetrics ์—”ํ‹ฐํ‹ฐ์˜ decrementLikeCount() ๋ฉ”์„œ๋“œ๋Š” likeCount <= 0 ์กฐ๊ฑด์„ ๊ฒ€์ฆํ•˜์—ฌ ์Œ์ˆ˜ ์ƒํƒœ๋ฅผ ๋ฐฉ์ง€ํ•˜๋ฉฐ, ์„œ๋น„์Šค ๊ณ„์ธต์˜ decrementLikeCount๋Š” ๋น„๊ด€์  ๋ฝ(findByProductIdAndDateForUpdate)์„ ์‚ฌ์šฉํ•˜๊ณ  ๋ ˆ์ฝ”๋“œ ๋ฏธ์กด์žฌ ์‹œ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ์ด๋Š” ์ด๋ฒคํŠธ ์ˆœ์„œ ๋ฌธ์ œ๋ฅผ ์ฆ‰์‹œ ๊ฐ์ง€ํ•˜๋Š” ๋ฐฉ์–ด์  ์„ค๊ณ„๋กœ, ์ž˜๋ชป๋œ ์ƒํƒœ ์ง„์ž…์„ ์›์ฒœ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.

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

22-23: Spring Batch ์˜์กด์„ฑ ์ถ”๊ฐ€ ์Šน์ธ

Spring Batch ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ์œ„ํ•œ ํ‘œ์ค€ ์˜์กด์„ฑ์ด ์ ์ ˆํžˆ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Spring Boot BOM์—์„œ ๋ฒ„์ „์ด ๊ด€๋ฆฌ๋˜๋ฏ€๋กœ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ๊ฐ€ ์—†์„ ๊ฒƒ์œผ๋กœ ํŒ๋‹จ๋ฉ๋‹ˆ๋‹ค.

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

31-35: Writer ํŒจํ„ด ํ™•์ธ ์™„๋ฃŒ

์„ค๊ณ„๊ฐ€ ์˜๋„๋œ ๋Œ€๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. RankingStepExecutionListener.afterStep()์—์„œ TOP 100์„ ์ถ”์ถœํ•˜๊ณ  JSON์œผ๋กœ ๋ณ€ํ™˜ํ•œ ํ›„ ExecutionContext์— ์ €์žฅํ•˜๋ฉฐ, RankingJobExecutionListener.afterJob()์˜ processWeeklyRanking() ๋ฐ processMonthlyRanking() ๋ฉ”์„œ๋“œ์—์„œ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค(weeklyRepository.saveAll(), monthlyRepository.saveAll()).

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

15-15: ์ œํ’ˆ๋ณ„ ์กฐํšŒ ์„ฑ๋Šฅ ๊ฐœ์„ ์„ ์œ„ํ•œ ์ธ๋ฑ์Šค

product_id์— ๋Œ€ํ•œ ์ธ๋ฑ์Šค ์ถ”๊ฐ€๋Š” ProductMetricsRepository์˜ findByProductId(), findByProductIdForUpdate() ๋“ฑ ๊ฐœ๋ณ„ ์ œํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ ์‹œ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

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

20-26: ์ž˜ ๊ตฌํ˜„๋œ Spring Batch Job ์„ค์ •์ž…๋‹ˆ๋‹ค.

ํ‘œ์ค€์ ์ธ Spring Batch Job ๊ตฌ์„ฑ ํŒจํ„ด์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋”ฐ๋ฅด๊ณ  ์žˆ์œผ๋ฉฐ, JobRepository, Step, Listener๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

15-38: ์ž˜ ์„ค๊ณ„๋œ DTO์ž…๋‹ˆ๋‹ค.

Java record๋ฅผ ์‚ฌ์šฉํ•œ ๋ถˆ๋ณ€ DTO ๊ตฌ์กฐ์™€ ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ํŒจํ„ด์ด ์ ์ ˆํ•˜๊ฒŒ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Swagger ์–ด๋…ธํ…Œ์ด์…˜๋„ ์ž˜ ์ž‘์„ฑ๋˜์–ด API ๋ฌธ์„œํ™”์— ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/batch/BatchConfig.java (1)

25-34: LGTM!

JobRepository์™€ JobExplorer ๋นˆ ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. MySQL ๋ฐ์ดํ„ฐ ํƒ€์ž…๊ณผ ํ…Œ์ด๋ธ” ์ ‘๋‘์‚ฌ๊ฐ€ ์ผ๊ด€๋˜๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Also applies to: 43-51

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

11-26: LGTM!

MvProductRankWeekly ์—”ํ‹ฐํ‹ฐ์™€ ์ผ๊ด€๋œ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์—์„œ ํ•„๋“œ ์„ค์ •์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋ฅผ ์œ„ํ•ด @Setter ์‚ฌ์šฉ์ด ์ ์ ˆํ•ด ๋ณด์ž…๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.java (1)

7-12: LGTM!

๋„๋ฉ”์ธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๊น”๋”ํ•˜๊ฒŒ ์ •์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. MvProductRankWeeklyRepository์™€ ์ผ๊ด€๋œ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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

10-67: LGTM!

๋ถˆ๋ณ€ ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค๋กœ ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. from() ๋ฉ”์„œ๋“œ์˜ null-safe ์ฒ˜๋ฆฌ์™€ combined() ๋ฉ”์„œ๋“œ์˜ ์ƒํƒœ ํ•ฉ์‚ฐ ๋กœ์ง์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

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

45-82: ์ง‘๊ณ„ ํŒจํ„ด: process()๊ฐ€ ํ•ญ์ƒ null ๋ฐ˜ํ™˜

process() ๋ฉ”์„œ๋“œ๊ฐ€ ํ•ญ์ƒ null์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ Writer๋กœ ์•„์ดํ…œ์ด ์ „๋‹ฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋Š” PriorityQueue์— TOP 100์„ ๋ˆ„์ ํ•˜๊ณ  RankingStepExecutionListener์—์„œ ๊ฒฐ๊ณผ๋ฅผ ์ถ”์ถœํ•˜๋Š” ์˜๋„๋œ ํŒจํ„ด์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

์ด ํŒจํ„ด์ด ์˜๋„๋œ ๊ฒƒ์ธ์ง€ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. Writer๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด ์•„๋ฌด ์ž‘์—…๋„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java (1)

11-65: LGTM!

์—”ํ‹ฐํ‹ฐ ์„ค๊ณ„์™€ ๊ฒ€์ฆ ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ์ธ๋ฑ์Šค์™€ ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ๊ณ , create() ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์˜ ๊ฒ€์ฆ ๋กœ์ง์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 71-93

apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.java (1)

7-27: LGTM!

์ฃผ๊ฐ„ ๋žญํ‚น ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ž˜ ์ •์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. MvProductRankMonthlyRepository์™€ ์ผ๊ด€๋œ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์™€ ๋ช…ํ™•ํ•œ ํ•œ๊ธ€ ๋ฌธ์„œํ™”๊ฐ€ ์ข‹์Šต๋‹ˆ๋‹ค.

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

22-30: ๊ตฌํ˜„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค

TypeFactory๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ œ๋„ค๋ฆญ ํƒ€์ž…์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Jackson์˜ ํƒ€์ž… ์•ˆ์ „ํ•œ ์—ญ์ง๋ ฌํ™” ํŒจํ„ด์„ ์ž˜ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


32-40: ๊ตฌํ˜„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค

์ฃผ๊ฐ„ ๋žญํ‚น ์—ญ์ง๋ ฌํ™” ๋ฉ”์„œ๋“œ์™€ ๋™์ผํ•œ ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์—ฌ ์ผ๊ด€์„ฑ์ด ์œ ์ง€๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java (1)

13-21: ๊ตฌํ˜„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค

Spring Data JPA์˜ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ ๊ทœ์น™์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋”ฐ๋ฅด๊ณ  ์žˆ์œผ๋ฉฐ, ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฆฌํฌ์ง€ํ† ๋ฆฌ(MvProductRankWeeklyJpaRepository)์™€ ์ผ๊ด€๋œ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. findTop100 ๋ฉ”์„œ๋“œ๋Š” TOP 100 ๋žญํ‚น ์กฐํšŒ ์š”๊ตฌ์‚ฌํ•ญ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)

13-41: ๊ตฌํ˜„์ด ์ ์ ˆํ•˜๋ฉฐ ์ผ๊ด€์„ฑ์ด ์ž˜ ์œ ์ง€๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค

์ฃผ๊ฐ„ ๋žญํ‚น ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„(MvProductRankWeeklyRepositoryImpl)๊ณผ ๋™์ผํ•œ ํŒจํ„ด๊ณผ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ์„ค์ •์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ์ผ๊ด€์„ฑ์ด ์šฐ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค. ์ฝ๊ธฐ ์ „์šฉ ์ž‘์—…์—๋Š” readOnly=true๋ฅผ, ์“ฐ๊ธฐ ์ž‘์—…์—๋Š” ์ผ๋ฐ˜ @Transactional์„ ์ ์ ˆํžˆ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyService.java (2)

27-34: ์ž ์žฌ์  ๋™์‹œ์„ฑ ์ด์Šˆ ํ™•์ธ ํ•„์š”

getOrCreate ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฝ ์—†์ด ์กฐํšŒ(findByProductIdAndDate)ํ•œ ํ›„ ์ƒ์„ฑํ•˜๋Š” ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋™์ผํ•œ (productId, date) ์กฐํ•ฉ์— ๋Œ€ํ•ด ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์ด ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์ค‘๋ณต ์ƒ์„ฑ์„ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค๋ฅธ ๋ฉ”์„œ๋“œ๋“ค(incrementLikeCount, incrementViewCount ๋“ฑ)์€ findByProductIdAndDateForUpdate๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฝ์„ ํš๋“ํ•˜๋Š”๋ฐ, ์ด ๋ฉ”์„œ๋“œ๋งŒ ๋ฝ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— (productId, date)์— ๋Œ€ํ•œ unique constraint๊ฐ€ ์žˆ๋‹ค๋ฉด ์ค‘๋ณต ์‚ฝ์ž…์€ ๋ฐฉ์ง€๋˜์ง€๋งŒ, ๋™์‹œ ์ƒ์„ฑ ์‹œ๋„ ์‹œ ํ•œ ์ชฝ์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”:

  1. ProductMetricsDaily ํ…Œ์ด๋ธ”์— (product_id, date) unique constraint๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€
  2. ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ๋™์‹œ ํ˜ธ์ถœ๋  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์žˆ๋Š”์ง€
  3. ์˜๋„์ ์œผ๋กœ ๋ฝ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์€ ๊ฒƒ์ธ์ง€ (์ฝ๊ธฐ ์œ„์ฃผ ์‹œ๋‚˜๋ฆฌ์˜ค ๋“ฑ)

ํ•„์š”ํ•˜๋‹ค๋ฉด ๋‹ค๋ฅธ ๋ฉ”์„œ๋“œ๋“ค์ฒ˜๋Ÿผ findByProductIdAndDateForUpdate๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.


54-62: ์ ์ ˆํ•œ ๊ฒ€์ฆ ๋กœ์ง์ž…๋‹ˆ๋‹ค

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

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

25-62: LGTM!

Factory ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์—ฌ Reader๋ฅผ ์ƒ์„ฑํ•˜๊ณ , @StepScope๋ฅผ ํ†ตํ•ด Job ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ late bindingํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. periodType์— ๋”ฐ๋ฅธ ๋‚ ์งœ ๋ฒ”์œ„ ์ž๋™ ๊ณ„์‚ฐ ๋กœ์ง๋„ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (1)

13-41: LGTM!

Repository ๊ตฌํ˜„์ด ๊น”๋”ํ•˜๊ณ , Monthly ๊ตฌํ˜„์ฒด์™€ ์ผ๊ด€๋œ ํŒจํ„ด์„ ์œ ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ๋ฉ”์„œ๋“œ์— ์ ์ ˆํ•œ @Transactional ์–ด๋…ธํ…Œ์ด์…˜์ด ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java (1)

13-16: LGTM!

Spring Data JPA ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜์„ ์ž˜ ํ™œ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ์ •์˜๊ฐ€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. Based on learnings, ๋„๋ฉ”์ธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์—์„œ Spring Data ํƒ€์ž… ์‚ฌ์šฉ์ด ์ด ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ ํ—ˆ์šฉ๋œ ์„ค๊ณ„ ๊ฒฐ์ •์ž„์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

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

154-169: ์ฃผ๊ฐ„ ์‹คํŒจ ์‹œ ์›”๊ฐ„ ์‹คํ–‰ ์Šคํ‚ต์— ๋Œ€ํ•œ ์ •์ฑ… ํ™•์ธ

ํ˜„์žฌ executeWeeklyAndMonthlyRanking์—์„œ ์ฃผ๊ฐ„ ๋žญํ‚น ์‹คํ–‰์ด ์‹คํŒจํ•˜๋ฉด ์›”๊ฐ„ ๋žญํ‚น์€ ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์˜๋„๋œ ๋™์ž‘์ธ์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ์ง‘๊ณ„๊ฐ€ ๋…๋ฆฝ์ ์ด๋ผ๋ฉด ๊ฐ๊ฐ try-catch๋กœ ๊ฐ์‹ธ์„œ ๋ชจ๋‘ ์‹คํ–‰ ํ›„ ๊ฒฐ๊ณผ๋ฅผ ์ทจํ•ฉํ•˜๋Š” ๋ฐฉ์‹๋„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


23-36: LGTM!

์„œ๋น„์Šค ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ , @Qualifier๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŠน์ • Job Bean์„ ์ฃผ์ž…๋ฐ›๋Š” ๋ฐฉ์‹์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. Spring Batch๊ฐ€ ์ž์ฒด ํŠธ๋žœ์žญ์…˜์„ ๊ด€๋ฆฌํ•˜๋ฏ€๋กœ @Transactional์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์ฃผ์„๋„ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.


64-70: ZonedDateTime ๋ฌธ์ž์—ด ํ˜•์‹์€ ISO-8601 ํ˜ธํ™˜์ด๋ฉฐ ๋ณ€ํ™˜ ๋ฌธ์ œ ์—†์Œ

rankingDate.toString()์€ ISO-8601 ํ™•์žฅ ํ˜•์‹(์˜ˆ: 2024-01-15T23:59:59+09:00[Asia/Seoul])์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. Spring Boot 3.4.4์— ํฌํ•จ๋œ Spring Framework 6.x๋Š” ์ด ํ˜•์‹์„ ZonedDateTime์œผ๋กœ ์ž๋™ ๋ณ€ํ™˜ํ•˜๋ฏ€๋กœ RankingJobExecutionListener์™€ RankingStepExecutionListener์˜ @value ๋ฐ”์ธ๋”ฉ์ด ์ •์ƒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ์ฝ”๋“œ๋Š” ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java (1)

18-40: LGTM!

Repository ๊ตฌํ˜„์ด ์ „๋ฐ˜์ ์œผ๋กœ ๊น”๋”ํ•˜๋ฉฐ, JPA Repository์— ์ ์ ˆํ•˜๊ฒŒ ์œ„์ž„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํŠธ๋žœ์žญ์…˜ ์„ค์ •๋„ ๋ฉ”์„œ๋“œ ํŠน์„ฑ์— ๋งž๊ฒŒ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Based on learnings, ๋„๋ฉ”์ธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์—์„œ Spring Data์˜ Page/Pageable ํƒ€์ž… ์‚ฌ์šฉ์ด ํ—ˆ์šฉ๋œ ์„ค๊ณ„ ๊ฒฐ์ •์ž„์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyRepository.java (3)

11-20: ๊ธฐ๋ณธ ์กฐํšŒ ๋ฉ”์„œ๋“œ ์„ค๊ณ„๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Optional ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๊ณ , ForUpdate ์ ‘๋ฏธ์‚ฌ๋กœ ๋ฝ ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ‘œํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฉ”์„œ๋“œ ๋ช…๋ช…์ด ์ผ๊ด€๋˜๊ณ  ์˜๋„๊ฐ€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.


22-42: ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ง‘๊ณ„ ๋ฉ”์„œ๋“œ ์„ค๊ณ„๊ฐ€ ์šฐ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค.

์›์‹œ ๋ฐ์ดํ„ฐ ์กฐํšŒ(findByDateBetween)์™€ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ(findAggregatedByDateBetween, findAggregatedByDateBetweenPaged)๋ฅผ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ–ˆ์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง• ์ง€์›์œผ๋กœ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.


44-53: ์ €์žฅ ๋ฉ”์„œ๋“œ๊ฐ€ ํ‘œ์ค€ ํŒจํ„ด์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.

๋‹จ๊ฑด ์ €์žฅ๊ณผ ์ผ๊ด„ ์ €์žฅ์„ ๋ชจ๋‘ ์ง€์›ํ•˜๋ฉฐ, Collection ํƒ€์ž…์„ ์‚ฌ์šฉํ•ด ํƒ€์ž… ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.

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

16-24: ์ปจํŠธ๋กค๋Ÿฌ ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Lombok๊ณผ Spring ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•œ ํ‘œ์ค€ ์„ค์ •์ด๋ฉฐ, ๋‚ ์งœ ํŒŒ์‹ฑ์„ ์œ„ํ•œ ํฌ๋งคํ„ฐ ์ƒ์ˆ˜๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


61-80: ๋‚ ์งœ ํŒŒ์‹ฑ ๋กœ์ง์— ์‹œ์Šคํ…œ ํƒ€์ž„์กด ์˜์กด์„ฑ์„ ํ™•์ธํ•˜์„ธ์š”.

๋‚ ์งœ ํŒŒ์‹ฑ ๋กœ์ง์€ ์ „๋ฐ˜์ ์œผ๋กœ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์œผ๋‚˜, Line 73์—์„œ ZoneId.systemDefault()๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰๋˜๋Š” ํ™˜๊ฒฝ์˜ ํƒ€์ž„์กด์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ถ„์‚ฐ ํ™˜๊ฒฝ์ด๋‚˜ ์„œ๋กœ ๋‹ค๋ฅธ ํƒ€์ž„์กด์—์„œ ์‹คํ–‰๋˜๋Š” ์„œ๋ฒ„๊ฐ€ ์žˆ๋‹ค๋ฉด ์ผ๊ด€์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ช…์‹œ์ ์ธ ํƒ€์ž„์กด(์˜ˆ: ZoneId.of("Asia/Seoul") ๋˜๋Š” ZoneOffset.UTC)์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

๋‹ค์Œ ์‚ฌํ•ญ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”:

  1. ๋ฐฐ์น˜ ์ž‘์—…์ด ์—ฌ๋Ÿฌ ์„œ๋ฒ„/์ปจํ…Œ์ด๋„ˆ์—์„œ ์‹คํ–‰๋  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š”์ง€
  2. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ๊ณผ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์˜ ํƒ€์ž„์กด์ด ๋™์ผํ•˜๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€
  3. ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ์ƒ ํŠน์ • ํƒ€์ž„์กด(์˜ˆ: KST)์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š”์ง€
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyJpaRepository.java (2)

18-27: ๋น„๊ด€์  ๋ฝ ์ „๋žต์ด ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

findByProductIdAndDateForUpdate ๋ฉ”์„œ๋“œ์— PESSIMISTIC_WRITE ๋ฝ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋™์‹œ ์—…๋ฐ์ดํŠธ ์‹œ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ๋ช…์‹œ์  @Query๋ฅผ ํ†ตํ•ด ๋ฝ์ด ์ •ํ™•ํžˆ ์ ์šฉ๋˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.


29-45: JPQL ์ง‘๊ณ„ ์ฟผ๋ฆฌ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

productId๋ณ„๋กœ ๋ฉ”ํŠธ๋ฆญ์„ ์ง‘๊ณ„ํ•˜๋Š” ์ฟผ๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ์ •ํ™•ํ•˜๋ฉฐ, SUM ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ์ผ๋ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ํ•ฉ์‚ฐํ•ฉ๋‹ˆ๋‹ค. ๋‚ ์งœ ๋ฒ”์œ„ ํ•„ํ„ฐ๋ง์ด ์–‘์ชฝ ๊ฒฝ๊ณ„๋ฅผ ํฌํ•จ(>=, <=)ํ•˜๋ฏ€๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

Comment on lines 18 to 36
public class RankingAggregationJob {

private final PlatformTransactionManager transactionManager;
private final RankingService rankingService;
private final JobRepository jobRepository;
/**
* ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ์‹คํ–‰
*/
public void execute() {
log.info("์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ๋ฐฐ์น˜ ์‹คํ–‰ ์‹œ์ž‘");
// 1. ์ผ๊ฐ„ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ (tb_product_metrics)
// 2. ์ฃผ๊ฐ„ ๋žญํ‚น ๊ณ„์‚ฐ
// 3. ์›”๊ฐ„ ๋žญํ‚น ๊ณ„์‚ฐ
// 4. ๊ฒฐ๊ณผ๋ฅผ tb_mv_product_rank_weekly, tb_mv_product_rank_month

log.info("์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ ๋ฐฐ์น˜ ์‹คํ–‰ ์™„๋ฃŒ");

}
}
Copy link

Choose a reason for hiding this comment

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

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

๋ฏธ์‚ฌ์šฉ/๋ฏธ์™„์„ฑ ์ฝ”๋“œ ์ œ๊ฑฐ ํ•„์š”

์ด ํด๋ž˜์Šค๋Š” ์‹ค์ œ ๊ตฌํ˜„ ๋กœ์ง ์—†์ด ํ”Œ๋ ˆ์ด์Šคํ™€๋” ์ฃผ์„๋งŒ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ฃผ์ž…๋œ ์˜์กด์„ฑ(transactionManager, rankingService, jobRepository)๋„ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

PR ์š”์•ฝ๊ณผ ๋‹ค๋ฅธ ํŒŒ์ผ๋“ค์„ ๋ณด๋ฉด, ์‹ค์ œ ๋ฐฐ์น˜ ๋กœ์ง์€ ๋‹ค์Œ์— ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • RankingAggregationJobConfig: Job ์ •์˜
  • RankingStepConfig: Step ์ •์˜ (Reader, Processor, Writer)
  • RankingJobExecutionListener, RankingStepExecutionListener: ์‹คํ–‰ ๋กœ์ง

๊ถŒ์žฅ์‚ฌํ•ญ:

  1. ์ด ํด๋ž˜์Šค๊ฐ€ ๋” ์ด์ƒ ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค๋ฉด ์ œ๊ฑฐ
  2. ๋ฏธ๋ž˜์— ์‚ฌ์šฉํ•  ๊ณ„ํš์ด๋ผ๋ฉด TODO ์ฃผ์„์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ค์ œ ์‚ฌ์šฉ ๊ณ„ํš์„ ๋ฌธ์„œํ™”
  3. ์•„๋‹ˆ๋ฉด ์‹ค์ œ ๊ตฌํ˜„์„ ์™„๋ฃŒ

์ด ํด๋ž˜์Šค๋ฅผ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ์‹ค์ œ ๊ตฌํ˜„์„ ์™„๋ฃŒํ•˜๋Š” ๊ฒƒ์„ ๋„์™€๋“œ๋ฆด๊นŒ์š”?

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJob.java
(lines 18-36), the class is a placeholder containing only comments and unused
injected fields; remove or complete it. Option A (preferred): delete this file
and remove any references to it so the project relies on
RankingAggregationJobConfig/RankingStepConfig and listeners for batch execution.
Option B (if keeping): replace placeholder with a minimal, documented TODO and
Javadoc stating planned behavior, mark it with an appropriate annotation if
needed (e.g., @Component) and either implement execute() to delegate to the real
JobLauncher/RankingService or wire and use the injected
transactionManager/rankingService/jobRepository; ensure no unused fields remain
and add unit tests or a TODO ticket reference.

Comment on lines +128 to +134
log.info("[RankingJobExecutionListener] processWeeklyRanking: ๊ธฐ์กด ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์ค‘ - ๋‚ ์งœ: {}", rankingDate);
weeklyRepository.deleteByRankingDate(rankingDate);
log.info("[RankingJobExecutionListener] processWeeklyRanking: ๊ธฐ์กด ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์™„๋ฃŒ - ๋‚ ์งœ: {}", rankingDate);

log.info("[RankingJobExecutionListener] processWeeklyRanking: {}๊ฑด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ ์ค‘...", top100.size());
weeklyRepository.saveAll(top100);
log.info("[RankingJobExecutionListener] processWeeklyRanking: ์ฃผ๊ฐ„ ๋žญํ‚น {}๊ฑด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ ์™„๋ฃŒ", top100.size());
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

find . -name "*.java" -type f | xargs rg -l "RankingJobExecutionListener" | head -5

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

Length of output: 192


๐Ÿ Script executed:

find . -name "*Repository.java" -type f | xargs rg -l "weeklyRepository|Weekly" | head -10

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

Length of output: 292


๐Ÿ Script executed:

rg -nP "@Transactional|class RankingJobExecutionListener" --type=java -A5 apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java 2>/dev/null | head -50

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

Length of output: 370


๐Ÿ Script executed:

cat -n ./apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.java

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

Length of output: 959


๐Ÿ Script executed:

sed -n '120,150p' ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java

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

Length of output: 1963


๐Ÿ Script executed:

rg -nP "processWeeklyRanking|@Transactional" --type=java ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java

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

Length of output: 2375


๐Ÿ Script executed:

cat -n ./apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java

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

Length of output: 1137


๐Ÿ Script executed:

sed -n '1,80p' ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java

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

Length of output: 4004


๐Ÿ Script executed:

rg -nP "class RankingJobExecutionListener|@Transactional|afterJob|void afterJob" --type=java ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java -B2 -A2

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

Length of output: 1677


๐Ÿ Script executed:

rg -nP "@Transactional|PlatformTransactionManager|TransactionTemplate" --type=java ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/ -A2

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

Length of output: 1822


๐Ÿ Script executed:

rg -nP "class.*RankingJobExecutionListener|afterJob|processWeeklyRanking" --type=java ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java | head -20

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

Length of output: 2302


๐Ÿ Script executed:

rg -nP "JobExecutionListener|@Transactional" --type=java ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJob.java -B5 -A5

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

Length of output: 70


๐Ÿ Script executed:

sed -n '78,140p' ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java | cat -n

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

Length of output: 4195


processWeeklyRanking ๋ฉ”์„œ๋“œ์— @Transactional ์ถ”๊ฐ€ ํ•„์š”

deleteByRankingDate์™€ saveAll์ด ๋ณ„๋„์˜ ํŠธ๋žœ์žญ์…˜์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. saveAll ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋Š” ์ด๋ฏธ ์‚ญ์ œ๋œ ์ƒํƒœ๋กœ ๋ฐ์ดํ„ฐ ์†์‹ค์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ์— @Transactional ์• ๋„ˆํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋‘ ์ž‘์—…์ด ๋‹จ์ผ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ์›์ž์„ฑ ์žˆ๊ฒŒ ์‹คํ–‰๋˜๋„๋ก ํ•ด์ฃผ์„ธ์š”.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java
around lines 128 to 134, the deleteByRankingDate and saveAll calls run in
separate transactions which can leave data deleted if saveAll fails; annotate
the processWeeklyRanking method with @Transactional so both operations run in a
single transaction and will roll back together on failure, and add the import
org.springframework.transaction.annotation.Transactional (ensure the class is a
Spring-managed bean and transaction management is enabled).

Comment on lines 106 to 115
public static PriorityQueue<ProductMetricsDailyAggregated> getTop100Queue() {
PriorityQueue<ProductMetricsDailyAggregated> queue = top100Queue.get();
if (queue == null) {
log.warn("[RankingProcessor] getTop100Queue: PriorityQueue๊ฐ€ null์ž…๋‹ˆ๋‹ค!");
} else {
log.info("[RankingProcessor] getTop100Queue: ํ ๋ฐ˜ํ™˜ - size={}, thread={}",
queue.size(), Thread.currentThread().getName());
}
return queue;
}
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

static ๋ฉ”์„œ๋“œ์—์„œ ์ธ์Šคํ„ด์Šค ๋กœ๊ฑฐ ์ ‘๊ทผ ๋ถˆ๊ฐ€

getTop100Queue()๊ฐ€ static ๋ฉ”์„œ๋“œ๋กœ ์„ ์–ธ๋˜์–ด ์žˆ์ง€๋งŒ, @Slf4j๋กœ ์ƒ์„ฑ๋œ log ํ•„๋“œ๋Š” ์ธ์Šคํ„ด์Šค ํ•„๋“œ์ž…๋‹ˆ๋‹ค. ์ด ์ฝ”๋“œ๋Š” ์ปดํŒŒ์ผ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
-public static PriorityQueue<ProductMetricsDailyAggregated> getTop100Queue() {
+public PriorityQueue<ProductMetricsDailyAggregated> getTop100Queue() {
     PriorityQueue<ProductMetricsDailyAggregated> queue = top100Queue.get();
     if (queue == null) {
         log.warn("[RankingProcessor] getTop100Queue: PriorityQueue๊ฐ€ null์ž…๋‹ˆ๋‹ค!");
     } else {
         log.info("[RankingProcessor] getTop100Queue: ํ ๋ฐ˜ํ™˜ - size={}, thread={}",
                 queue.size(), Thread.currentThread().getName());
     }
     return queue;
 }

-public static void clearTop100Queue() {
+public void clearTop100Queue() {
     top100Queue.remove();
 }

๋˜๋Š” static ๋ฉ”์„œ๋“œ๋ฅผ ์œ ์ง€ํ•ด์•ผ ํ•œ๋‹ค๋ฉด ๋ณ„๋„์˜ static ๋กœ๊ฑฐ๋ฅผ ์„ ์–ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

private static final Logger staticLog = LoggerFactory.getLogger(RankingProcessor.class);
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java
around lines 106 to 115: the static method getTop100Queue() uses the instance
logger `log` generated by @Slf4j which causes a compile error; either make the
method non-static or add/use a static logger. Fix by replacing instance logger
usage with a static logger (declare a private static final Logger STATIC_LOG =
LoggerFactory.getLogger(RankingProcessor.class)) and update the log calls inside
getTop100Queue() to use STATIC_LOG, or alternatively remove static from the
method if instance access is acceptable.

Comment on lines +63 to +68
PriorityQueue<ProductMetricsDailyAggregated> queue = RankingProcessor.getTop100Queue();

if (queue == null) {
log.error("[RankingStepExecutionListener] afterStep: PriorityQueue๊ฐ€ null์ž…๋‹ˆ๋‹ค!");
return stepExecution.getExitStatus();
}
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
# First, find the RankingProcessor file to understand ThreadLocal usage
find apps/commerce-api -name "RankingProcessor.java" -type f

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

Length of output: 179


๐Ÿ Script executed:

#!/bin/bash
# Check the ThreadLocal and scope patterns in RankingProcessor
rg -nP 'ThreadLocal|getTop100Queue|@StepScope|@BeforeStep|@AfterStep' --type=java apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/

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

Length of output: 1644


๐Ÿ Script executed:

#!/bin/bash
# Examine the RankingStepExecutionListener file to understand the afterStep context
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java | head -100

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

Length of output: 6148


๐Ÿ Script executed:

#!/bin/bash
# Get the full RankingProcessor.java to understand ThreadLocal usage and initialization
wc -l apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java

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

Length of output: 183


๐Ÿ Script executed:

#!/bin/bash
# Read the relevant parts of RankingProcessor.java focusing on ThreadLocal and @BeforeStep
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java | head -130

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

Length of output: 6480


๐Ÿ Script executed:

#!/bin/bash
# Check the getTop100Queue method implementation
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java | sed -n '106,115p'

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

Length of output: 641


๐Ÿ Script executed:

#!/bin/bash
# Find job configuration files
find apps/commerce-api -name "*JobConfig*" -o -name "*Job.java" | grep -i ranking

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

Length of output: 287


๐Ÿ Script executed:

#!/bin/bash
# Check if clearTop100Queue is called anywhere
rg 'clearTop100Queue|top100Queue\.remove' --type=java apps/commerce-api/

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

Length of output: 620


๐Ÿ Script executed:

#!/bin/bash
# Check RankingStepExecutionListener for where clearTop100Queue is called
rg -n 'clearTop100Queue' --type=java -B3 -A3 apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java

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

Length of output: 389


๐Ÿ Script executed:

#!/bin/bash
# Check job configuration to understand step structure
wc -l apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJobConfig.java

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

Length of output: 193


๐Ÿ Script executed:

#!/bin/bash
# Read the job config to see step sequencing
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJobConfig.java | head -80

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

Length of output: 1220


๐Ÿ Script executed:

#!/bin/bash
# Check the complete afterStep method to understand error handling
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java | sed -n '54,135p'

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

Length of output: 5891


๐Ÿ Script executed:

#!/bin/bash
# Verify Spring Batch behavior for @StepScope and thread context
# Look for how other listeners handle failures
rg 'return ExitStatus.FAILED' --type=java apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/

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

Length of output: 375


๐Ÿ Script executed:

#!/bin/bash
# Check if there are any test files that verify thread behavior
find apps/commerce-api -name "*RankingProcessor*Test*" -o -name "*RankingListener*Test*"

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

Length of output: 70


PriorityQueue๊ฐ€ null ๋˜๋Š” ๋น„์–ด์žˆ์„ ๋•Œ ์Šคํ…์„ ์‹คํŒจ ์ƒํƒœ๋กœ ํ‘œ์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค

ํ˜„์žฌ ์ฝ”๋“œ๋Š” queue๊ฐ€ null์ด๊ฑฐ๋‚˜ ๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  stepExecution.getExitStatus()๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ ์Šคํ…์ด ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ผ๊ด€์„ฑ ์—†๋Š” ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ์ž…๋‹ˆ๋‹ค. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” 122์ค„, 128์ค„๊ณผ ๊ฐ™์ด null ๋˜๋Š” empty ์ฒดํฌ ์‹œ์—๋„ ExitStatus.FAILED๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

if (queue == null) {
    log.error("[RankingStepExecutionListener] afterStep: PriorityQueue๊ฐ€ null์ž…๋‹ˆ๋‹ค!");
    return ExitStatus.FAILED;
}

if (queue.isEmpty()) {
    log.warn("[RankingStepExecutionListener] afterStep: PriorityQueue๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค!");
    return ExitStatus.FAILED;
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java
around lines 63 to 68 (and ensure consistency at the similar checks on lines
~122 and ~128), the code currently logs when the PriorityQueue is null or empty
but returns stepExecution.getExitStatus(), causing the step to be treated as
successful; change these returns to ExitStatus.FAILED so the step fails when the
queue is null or empty, and make both the null-check and the isEmpty-check
return ExitStatus.FAILED (use log.error for null and log.warn for empty) to
mirror the intended failure handling consistently across the file.

Comment on lines +161 to +166
if (weekly.getRankingDate() == null) {
log.error("[RankingStepExecutionListener] afterStep: ์•„์ดํ…œ[{}]์˜ rankingDate๊ฐ€ NULL์ž…๋‹ˆ๋‹ค - productId={}",
i, weekly.getProductId());
}

weeklyRankings.add(weekly);
Copy link

Choose a reason for hiding this comment

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

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

rankingDate๊ฐ€ null์ธ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฒ€ํ†  ํ•„์š”

rankingDate๊ฐ€ null์ธ ๊ฒฝ์šฐ ์—๋Ÿฌ ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  weeklyRankings์— ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค. ์ด ์ƒํƒœ๋กœ DB์— ์ €์žฅ๋˜๋ฉด ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. null์ธ ๊ฒฝ์šฐ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๊ฒƒ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
                 if (weekly.getRankingDate() == null) {
                     log.error("[RankingStepExecutionListener] afterStep: ์•„์ดํ…œ[{}]์˜ rankingDate๊ฐ€ NULL์ž…๋‹ˆ๋‹ค - productId={}", 
                             i, weekly.getProductId());
+                    failureCount++;
+                    continue;
                 }

                 weeklyRankings.add(weekly);
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java
around lines 161 to 166, the code currently logs an error when
weekly.getRankingDate() is null but still adds the entity to weeklyRankings;
change this so null-rankingDate entries are not added (or explicitly throw a
runtime exception) to prevent invalid DB writes: either surround the add with an
else branch so weeklyRankings.add(weekly) only occurs when rankingDate != null,
or after logging throw a descriptive IllegalStateException to stop processing;
ensure any callers/transactional behavior are adjusted accordingly (or update
tests) to reflect the chosen approach.

Comment on lines +18 to +20
public double calculateScore() {
return totalViewCount * 0.1 + totalLikeCount * 0.2 + totalSoldCount * 0.7;
}
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

Null ์•ˆ์ „์„ฑ ๊ฒ€์ฆ ํ•„์š” - NPE ์œ„ํ—˜

calculateScore() ๋ฉ”์„œ๋“œ๊ฐ€ null ์ฒดํฌ ์—†์ด ์‚ฐ์ˆ  ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. totalViewCount, totalLikeCount, totalSoldCount ์ค‘ ํ•˜๋‚˜๋ผ๋„ null์ด๋ฉด NullPointerException์ด ๋ฐœ์ƒํ•˜์—ฌ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ง‘๊ณ„ ์‹œ null ๊ฐ’์ด ๋ฐ˜ํ™˜๋  ๊ฐ€๋Šฅ์„ฑ(์˜ˆ: ํŠน์ • ๋ฉ”ํŠธ๋ฆญ์ด ์—†๋Š” ์ƒํ’ˆ)์ด ์žˆ๋‹ค๋ฉด ๋ฐฉ์–ด ๋กœ์ง์ด ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.

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

๋ฐฉ์•ˆ 1 (๊ถŒ์žฅ): Null์„ 0์œผ๋กœ ์ฒ˜๋ฆฌ

     public double calculateScore() {
-        return totalViewCount * 0.1 + totalLikeCount * 0.2 + totalSoldCount * 0.7;
+        long viewCount = totalViewCount != null ? totalViewCount : 0L;
+        long likeCount = totalLikeCount != null ? totalLikeCount : 0L;
+        long soldCount = totalSoldCount != null ? totalSoldCount : 0L;
+        return viewCount * 0.1 + likeCount * 0.2 + soldCount * 0.7;
     }

๋ฐฉ์•ˆ 2: ์ƒ์„ฑ์ž์—์„œ ๊ฒ€์ฆ

 @Getter
-@AllArgsConstructor
 public class ProductMetricsDailyAggregated {
     private Long productId;
     private Long totalLikeCount;
     private Long totalViewCount;
     private Long totalSoldCount;
+
+    public ProductMetricsDailyAggregated(Long productId, Long totalLikeCount, Long totalViewCount, Long totalSoldCount) {
+        this.productId = productId;
+        this.totalLikeCount = totalLikeCount != null ? totalLikeCount : 0L;
+        this.totalViewCount = totalViewCount != null ? totalViewCount : 0L;
+        this.totalSoldCount = totalSoldCount != null ? totalSoldCount : 0L;
+    }
๐Ÿ“ 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
public double calculateScore() {
return totalViewCount * 0.1 + totalLikeCount * 0.2 + totalSoldCount * 0.7;
}
public double calculateScore() {
long viewCount = totalViewCount != null ? totalViewCount : 0L;
long likeCount = totalLikeCount != null ? totalLikeCount : 0L;
long soldCount = totalSoldCount != null ? totalSoldCount : 0L;
return viewCount * 0.1 + likeCount * 0.2 + soldCount * 0.7;
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyAggregated.java
around lines 18 to 20: calculateScore() performs arithmetic on totalViewCount,
totalLikeCount, and totalSoldCount without null checks which can cause NPEs;
update the method to defensively handle nulls (preferred: treat any null metric
as 0 before computing the weighted sum) or alternatively ensure these fields are
validated/non-null in the constructor/setters so the method can safely assume
non-null values.

Comment on lines +16 to +25
private Long productId;
private String productName;
private Long brandId;
private String brandName;
private Double score;
private Integer likeCount;
private Integer viewCount;
private Integer orderCount;
private Integer ranking;
private ZonedDateTime rankingDate;
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Search for SQL migration and DDL files related to tb_mv_product_rank_weekly table

echo "=== Searching for SQL files with tb_mv_product_rank_weekly reference ==="
fd -e sql | head -20

echo -e "\n=== Searching for tb_mv_product_rank_weekly table definition ==="
fd -e sql -x grep -l "tb_mv_product_rank_weekly" 2>/dev/null

echo -e "\n=== Searching for migration/schema directories ==="
fd -type d -name "*migration*" -o -name "*sql*" -o -name "*schema*" | grep -E "(migration|sql|schema|db)" | head -20

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

Length of output: 466


๐Ÿ Script executed:

#!/bin/bash
# More comprehensive search for database-related files

echo "=== Searching for Flyway migration files ==="
fd -path "*/db/migration/*" -type f | head -30

echo -e "\n=== Searching for Liquibase changelog files ==="
fd -e xml -o -e yaml | xargs grep -l "product_rank_weekly" 2>/dev/null | head -20

echo -e "\n=== Searching in common database locations ==="
find . -type f \( -name "*.sql" -o -name "*.xml" \) 2>/dev/null | grep -iE "(db|migration|schema|changelog)" | head -30

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

Length of output: 532


๐Ÿ Script executed:

#!/bin/bash
# Search for the table definition and index information more broadly

echo "=== Looking for any reference to MvProductRankWeekly or product_rank_weekly ==="
rg -l "mv_product_rank_weekly|MvProductRankWeekly" --max-count 20 2>/dev/null

echo -e "\n=== Looking for CREATE TABLE statements ==="
rg "CREATE TABLE.*product_rank_weekly" -A 20 2>/dev/null | head -50

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

Length of output: 1005


๐Ÿ Script executed:

#!/bin/bash
# Read the entity class to check for @Index annotations
echo "=== MvProductRankWeekly.java entity class ==="
cat -n apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java

echo -e "\n=== MvProductRankWeeklyRepository interface ==="
cat -n apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.java

echo -e "\n=== MvProductRankWeeklyJpaRepository interface ==="
cat -n apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java

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

Length of output: 2987


๐Ÿ Script executed:

#!/bin/bash
# Search for schema definitions in resources
echo "=== Searching for schema files in resources ==="
find . -path "*/resources/*" -name "*schema*" -o -path "*/resources/*" -name "*.sql" | head -20

echo -e "\n=== Searching for application.properties or application.yml ==="
find . -name "application*.properties" -o -name "application*.yml" | xargs grep -l "jpa.hibernate.ddl" 2>/dev/null | head -10

echo -e "\n=== Checking for schema.sql in resources ==="
find . -name "schema.sql" -o -name "schema-*.sql" 2>/dev/null

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

Length of output: 246


๐Ÿ Script executed:

#!/bin/bash
# Search for @Table and @Index annotations in the entity
echo "=== Checking for @Table and @Index annotations ==="
rg -A 5 "@Table|@Index" apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java 2>/dev/null

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

Length of output: 311


๐Ÿ Script executed:

#!/bin/bash
# Check BaseEntity to understand the inheritance
echo "=== BaseEntity class ==="
rg -A 20 "class BaseEntity" --max-count 1 2>/dev/null

echo -e "\n=== Checking for any existing @Index usage elsewhere ==="
rg "@Index" apps/commerce-core/src/main/java/com/loopers/domain/ | head -10

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

Length of output: 3606


@Index ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์Šค ์ •์˜

MvProductRankWeekly ์—”ํ‹ฐํ‹ฐ์— ์ธ๋ฑ์Šค๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์ธ๋ฑ์Šค๋ฅผ @Table ์–ด๋…ธํ…Œ์ด์…˜์˜ indexes ํŒŒ๋ผ๋ฏธํ„ฐ์— ์ถ”๊ฐ€ํ•˜์„ธ์š”:

@Table(name = "tb_mv_product_rank_weekly", indexes = {
    @Index(name = "idx_mv_product_rank_weekly_ranking_date", columnList = "ranking_date"),
    @Index(name = "idx_mv_product_rank_weekly_product_date", columnList = "product_id,ranking_date"),
    @Index(name = "idx_mv_product_rank_weekly_date_ranking", columnList = "ranking_date,ranking")
})

์ด๋Š” ๋‹ค์Œ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ๋“ค์˜ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

  • findByProductIdAndRankingDate(): product_id + ranking_date ๋ณตํ•ฉ ์ธ๋ฑ์Šค ํ•„์š”
  • findTop100ByRankingDateOrderByRankingAsc(): ranking_date + ranking ๋ณตํ•ฉ ์ธ๋ฑ์Šค ํ•„์š”
  • deleteByRankingDate(): ranking_date ์ธ๋ฑ์Šค ํ•„์š”
๐Ÿค– Prompt for AI Agents
In
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java
around lines 16 to 25, the entity lacks DB indexes; update the class-level JPA
@Table annotation to include the provided indexes:
idx_mv_product_rank_weekly_ranking_date on ranking_date,
idx_mv_product_rank_weekly_product_date on product_id,ranking_date, and
idx_mv_product_rank_weekly_date_ranking on ranking_date,ranking so the
repository methods (findByProductIdAndRankingDate,
findTop100ByRankingDateOrderByRankingAsc, deleteByRankingDate) are properly
optimized; add the indexes parameter to @Table (or create a new @Table if
missing) ensuring column names match the entity column mappings.

Comment on lines +47 to +76
/**
* ํŠน์ • ๋‚ ์งœ ๋ฒ”์œ„์˜ ๋ฉ”ํŠธ๋ฆญ์„ product_id๋ณ„๋กœ ์ง‘๊ณ„ํ•˜์—ฌ ํŽ˜์ด์ง• ์กฐํšŒ (๋ฐฐ์น˜์šฉ)
*
* @param startDate ์‹œ์ž‘์ผ
* @param endDate ์ข…๋ฃŒ์ผ
* @param pageable ํŽ˜์ด์ง• ์ •๋ณด
* @return ์ง‘๊ณ„๋œ ๋ฉ”ํŠธ๋ฆญ ํŽ˜์ด์ง€
*/
@Query(
value =
"SELECT " +
" pmd.product_id as productId, " +
" SUM(pmd.like_count) as totalLikeCount, " +
" SUM(pmd.view_count) as totalViewCount, " +
" SUM(pmd.sold_count) as totalSoldCount " +
"FROM tb_product_metrics_daily pmd " +
"WHERE pmd.date >= :startDate AND pmd.date <= :endDate " +
"GROUP BY pmd.product_id " +
"ORDER BY pmd.product_id",
countQuery =
"SELECT COUNT(DISTINCT pmd.product_id) " +
"FROM tb_product_metrics_daily pmd " +
"WHERE pmd.date >= :startDate AND pmd.date <= :endDate",
nativeQuery = true
)
Page<Object[]> findAggregatedByDateBetweenPaged(
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
Pageable pageable
);
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# Search for table definitions in SQL migration files
fd -e sql -e xml | head -20

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

Length of output: 547


๐Ÿ Script executed:

# Find the entity class for ProductMetricsDaily
fd -type f -name "*ProductMetricsDaily*"

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

Length of output: 258


๐Ÿ Script executed:

# Search for @Table annotation related to product metrics
rg -n "@Table|@Column" --type java | grep -i "product.*metric\|metric.*daily" -A 3 -B 3

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

Length of output: 1650


๐Ÿ Script executed:

# Find entity class definitions with field mappings
rg -n "class ProductMetricsDaily" --type java -A 50

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

Length of output: 37490


๐Ÿ Script executed:

# Search for any schema definitions or migration scripts
fd -e sql | xargs grep -l "tb_product_metrics_daily" 2>/dev/null

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

Length of output: 72


๐Ÿ Script executed:

# Get the complete @Table annotation for ProductMetricsDaily
cat apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java | head -20

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

Length of output: 739


๐Ÿ Script executed:

# Search for the table name in the @Table annotation
rg -n "@Table" apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java -A 2

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

Length of output: 159


๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ์˜ ํ…Œ์ด๋ธ”๋ช…๊ณผ ์ปฌ๋Ÿผ๋ช…์ด ์Šคํ‚ค๋งˆ์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ์ด๋ธ”๋ช… tb_product_metrics_daily์™€ ๋ชจ๋“  ์ปฌ๋Ÿผ๋ช…(product_id, like_count, view_count, sold_count)์ด ProductMetricsDaily ์—”ํ‹ฐํ‹ฐ ์ •์˜์™€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋ฉฐ, countQuery๋ฅผ ๋ณ„๋„๋กœ ์ œ๊ณตํ•œ ํŽ˜์ด์ง• ์ตœ์ ํ™”๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

๋‹ค๋งŒ, ORDER BY pmd.product_id๋Š” ์ผ๊ด€๋œ ํŽ˜์ด์ง•์„ ๋ณด์žฅํ•˜์ง€๋งŒ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์˜ ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ๋ชฉํ‘œ(์˜ˆ: ํŒ๋งค๋Ÿ‰ ๋˜๋Š” ์กฐํšŒ์ˆ˜ ๊ธฐ์ค€ ์ˆœ์œ„ ๊ณ„์‚ฐ)์™€ ๋งž๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. ํ•„์š”์‹œ ORDER BY SUM(pmd.sold_count) DESC ๊ฐ™์€ ์˜๋ฏธ ์žˆ๋Š” ์ •๋ ฌ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด ๋” ํšจ์œจ์ ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyJpaRepository.java
around lines 47 to 76, the native query currently orders results by
pmd.product_id which ensures pagination stability but may not reflect the
intended business ordering for the batch (e.g., ranking by sales or views);
change the ORDER BY clause to a business-meaningful aggregate such as ORDER BY
SUM(pmd.sold_count) DESC (or SUM(pmd.view_count) DESC / SUM(pmd.like_count) DESC
as appropriate), and if stability is needed add a secondary tie-breaker like
pmd.product_id; keep the countQuery as-is for paging.

Comment on lines 59 to 71
@Override
public Page<ProductMetricsDailyAggregated> findAggregatedByDateBetweenPaged(LocalDate startDate, LocalDate endDate, Pageable pageable) {
Page<Object[]> results = jpaRepository.findAggregatedByDateBetweenPaged(
startDate, endDate, pageable
);

return results.map(row -> new ProductMetricsDailyAggregated(
((Number) row[0]).longValue(), // productId
((Number) row[1]).longValue(), // totalLikeCount
((Number) row[2]).longValue(), // totalViewCount
((Number) row[3]).longValue() // totalSoldCount
));
}
Copy link

Choose a reason for hiding this comment

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

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

@transactional(readOnly = true) ๋ˆ„๋ฝ

findAggregatedByDateBetweenPaged ๋ฉ”์„œ๋“œ์— @Transactional ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ˆ„๋ฝ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์กฐํšŒ ๋ฉ”์„œ๋“œ๋“ค๊ณผ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด @Transactional(readOnly = true)๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
     @Override
+    @Transactional(readOnly = true)
     public Page<ProductMetricsDailyAggregated> findAggregatedByDateBetweenPaged(LocalDate startDate, LocalDate endDate, Pageable pageable) {
๐Ÿค– Prompt for AI Agents
In
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java
around lines 59 to 71, the method findAggregatedByDateBetweenPaged is missing
the @Transactional(readOnly = true) annotation; add @Transactional(readOnly =
true) immediately above the method signature to match other read-only queries,
and if the import is absent add import
org.springframework.transaction.annotation.Transactional; ensure no other
transactional attributes are changed.

Comment on lines +62 to +63
LocalDate today = LocalDate.now();

Copy link

Choose a reason for hiding this comment

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

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

์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๊ฐ์ด ์•„๋‹Œ ์ฒ˜๋ฆฌ ์‹œ๊ฐ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฌธ์ œ

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

  1. ์ง€์—ฐ๋œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ: Kafka ์ด๋ฒคํŠธ๊ฐ€ ์ง€์—ฐ๋˜๊ฑฐ๋‚˜ ์žฌ์ฒ˜๋ฆฌ๋  ๊ฒฝ์šฐ, ์‹ค์ œ ์ด๋ฒคํŠธ ๋ฐœ์ƒ์ผ์ด ์•„๋‹Œ ์ฒ˜๋ฆฌ์ผ์— ๋ฉ”ํŠธ๋ฆญ์ด ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค.
  2. ํƒ€์ž„์กด ๋ถˆ์ผ์น˜: LocalDate.now()๋Š” ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ํƒ€์ž„์กด์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๋ถ„์‚ฐ ํ™˜๊ฒฝ์—์„œ ์ผ๊ด€์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  3. ์ž์ • ๊ฒฝ๊ณ„ ๋ฌธ์ œ: ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๊ฐ€ ์ž์ •์„ ๋„˜์–ด๊ฐ€๋ฉด ๋™์ผ ๋ฐฐ์น˜ ๋‚ด ์ด๋ฒคํŠธ๋“ค์ด ์„œ๋กœ ๋‹ค๋ฅธ ๋‚ ์งœ์— ๊ธฐ๋ก๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๐Ÿ”Ž ์ด๋ฒคํŠธ ํƒ€์ž„์Šคํƒฌํ”„ ๊ธฐ๋ฐ˜ ์ฒ˜๋ฆฌ๋กœ ๊ฐœ์„ 
-        LocalDate today = LocalDate.now();
-
         for (ConsumerRecord<Object, Object> record : records) {
             try {
+                // ์ด๋ฒคํŠธ ์‹ค์ œ ๋ฐœ์ƒ ์‹œ๊ฐ ์‚ฌ์šฉ (Kafka record timestamp)
+                LocalDate eventDate = Instant.ofEpochMilli(record.timestamp())
+                        .atZone(ZoneId.of("Asia/Seoul")) // ๋˜๋Š” ์„ค์ •์—์„œ ์ฃผ์ž…
+                        .toLocalDate();
+
                 String key = record.key() != null ? record.key().toString() : null;
                 Object value = record.value();
                 
                 // JSON ๋ฌธ์ž์—ด์„ Map์œผ๋กœ ํŒŒ์‹ฑ
                 Map<String, Object> message = parseMessage(value);
                 
                 // ์ด๋ฒคํŠธ ์ •๋ณด ์ถ”์ถœ
                 String eventId = extractEventId(message);
                 String eventType = extractEventType(message);
                 String aggregateId = key != null ? key : extractAggregateId(message);

                 if (eventId == null || eventType == null || aggregateId == null) {
                     log.warn("Missing required fields in message: eventId={}, eventType={}, aggregateId={}", 
                             eventId, eventType, aggregateId);
                     continue;
                 }

                 // ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ
                 if (idempotencyService.isAlreadyHandled(eventId)) {
                     log.warn("Event already handled, skipping: eventId={}, eventType={}", eventId, eventType);
                     continue;
                 }

                 // ์ด๋ฒคํŠธ ํƒ€์ž…๋ณ„ ์ฒ˜๋ฆฌ
-                handleEvent(eventType, aggregateId, today);
+                handleEvent(eventType, aggregateId, eventDate);

์ด ๋ฐฉ์‹์€:

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

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

๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/CatalogEventListener.java
around lines 62-63, replace the use of LocalDate.now() (processing time) with
the event's timestamp: extract the event occurrence instant (e.g. a timestamp
field from the Kafka payload or record metadata), convert it to a LocalDate
using a fixed ZoneId (preferably UTC or a configured ZoneId) via Instant ->
ZonedDateTime/LocalDate, and use that date for metric recording; if the event
timestamp is missing or unparsable, fall back to a safe default and log a
warning so you donโ€™t silently misattribute dates.

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