Skip to content

Conversation

@junoade
Copy link
Collaborator

@junoade junoade commented Jan 1, 2026

๐Ÿ“Œ Summary

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

๐Ÿ’ฌ Review Points

  1. ๊ณผ์ œ ์„ค๊ณ„
image
  • Chunk-Oriented ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด์„œ Reader, Processor, Writer ์˜ ๊ตฌ์กฐ๋ฅผ ์ ์šฉํ•ด๋ณด๊ณ , Input (score ๋ฏธ์กด์žฌ) ๊ณผ Output (score ์กด์žฌ)์˜ dto๋ฅผ ๋‹ค๋ฅด๊ฒŒ ํ•ด๋ณด๋Š” ๋“ฑ ์ ์šฉํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.
  • 9์ฃผ์ฐจ ๊ณผ์ œ ๊ตฌ์„ฑ์— ๋ฐฐ์น˜ ๋ชจ๋“ˆ์„ ๋”ํ•˜๋ฉด์„œ ์ ์  ๊ตฌ์„ฑ์ด ๋ณต์žกํ•ด์ง€๊ณ  ์žˆ๋Š”๊ฒŒ ๋А๊ปด์กŒ์Šต๋‹ˆ๋‹ค..! ํ˜น์‹œ ๋ณต์žกํ•œ ์‹œ์Šคํ…œ ์š”์†Œ๋“ค์„ ๋‹ค๋ฃจ์‹œ๋Š” ํŒ ๊ฐ™์€๊ฒŒ ์žˆ์„๊นŒ์š”?
  1. ํ…Œ์ด๋ธ” ์„ค๊ณ„
  • product_like_metrics ์€ ์ผ๋ณ„๋กœ ๋‹ค์–‘ํ•œ ์ƒํ’ˆ ๊ด€๋ จ ๋ฉ”ํŠธ๋ฆญ ์ •๋ณด๋ฅผ ์ง‘๊ณ„ํ•  ์ˆ˜ ์žˆ๋„๋ก product_metrics๋กœ ๋ฆฌํŒฉํ† ๋ง ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • MV ํ…Œ์ด๋ธ”์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค๊ณ„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

    • ํ…Œ์ด๋ธ”์— ๋Œ€ํ•œ ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด๋Š” commerce-api ๋‚ด ์กด์žฌํ•˜๋Š” api์—์„œ๋งŒ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๋‹น ๊ด€์‹ฌ์‚ฌ๊ฐ€ commerce-api์— ์ ํ•ฉํ•˜๋‹ค ๋ณด๊ณ  ๊ทธ์ชฝ์— ์œ„์น˜์‹œ์ผฐ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ ranking ๊ด€๋ จ ์†Œ์Šค๋“ค์ด ์‚ฐ์žฌํ•ด์žˆ์–ด ๋ณ„๋„์˜ ๋ชจ๋“ˆ๋กœ ๋นผ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์€๋ฐ ์ง„ํ–‰ํ•ด๋ณผ ์ƒ๊ฐ์ž…๋‹ˆ๋‹ค.
erDiagram 
MV_PRODUCT_RANK_WEEKLY {
        varchar year_week PK
        bigint  product_id PK
        bigint  view_count
        bigint  like_count
        bigint  order_count
        double score
        datetime created_at
        datetime updated_at
    }

    MV_PRODUCT_RANK_MONTHLY {
        varchar year_month PK
        bigint  product_id PK
        bigint  view_count
        bigint  like_count
        bigint  order_count
        double score
        datetime created_at
        datetime updated_at
    }
Loading
  1. ๋ฐฐ์น˜ ๋ชจ๋“ˆ ๊ตฌ์„ฑ
  • ๋ฐฐ์น˜ ๋ชจ๋“ˆ์„ ์ฒด๋ฆฌํ”ฝํ•ด์™€์„œ ๊ตฌ์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ์Šคํ”„๋ง๋ฐฐ์น˜์— ๋Œ€ํ•ด ํ•™์Šตํ•˜๊ณ  JobConfig, Reader, Processor, Writer, Listener ๊ตฌ์กฐ๋ฅผ ์ตํ˜€์„œ ์ ์šฉํ•ด๋ณผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  1. ๋ฐฐ์น˜๋ฅผ ์ด์šฉํ•œ ์ฃผ๊ฐ„/์›”๋ณ„ ๊ฐฑ์‹  ์ „๋žต
  • ํ˜„์žฌ๋Š” ๋‹จ์ˆœํ•˜๊ฒŒ ์ผํšŒ์„ฑ ์ผ๊ด„ ์ง‘๊ณ„๋งŒ ํ•˜๋„๋ก ํ•˜์˜€์ง€๋งŒ ์ถ”ํ›„ ์ฆ๋ถ„ + ์ฃผ๊ธฐ์  ์ „์ฒด ์žฌ์ง‘๊ณ„(๋ณด์ •)์œผ๋กœ ํ™•์žฅํ•ด๋ณผ ์ˆ˜๋„ ์žˆ์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.
  1. ์กฐํšŒ API ๊ตฌํ˜„
  • ๊ธฐ์กด ์ผ๋ณ„ ๋ ˆ๋””์Šค๋ฅผ ํ†ตํ•œ ๋žญํ‚น ์กฐํšŒ ์ฝ”๋“œ์™€์˜ ์ˆ˜์ • ๋ฒ”์œ„๋ฅผ ๊ณ ๋ คํ•˜์—ฌ
  • ์ „๋žต ํŒจํ„ด์„ ์ ์šฉํ•ด์„œ Ranking ์ •๋ณด๋ฅผ ๋‹ค๋ฅธ ์ €์žฅ์†Œ์—์„œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
period=DAILY โ†’ Redis(ZSET)
period=WEEKLY|MONTHLY โ†’ MV ํ…Œ์ด๋ธ”

โœ… Checklist

๐Ÿงฑ Spring Batch

  • Spring Batch Job ์„ ์ž‘์„ฑํ•˜๊ณ , ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.
java -jar apps/commerce-batch/build/libs/commerce-batch.jar \
  --spring.profiles.active=local \
  --spring.batch.job.enabled=true \
  --spring.batch.job.name=rankWeeklyMvJob \
  yearWeek=2026-W01

java -jar apps/commerce-batch/build/libs/commerce-batch.jar \
  --spring.profiles.active=local \
  --spring.batch.job.enabled=true \
  --spring.batch.job.name=rankMonthlyMvJob \
  yearMonth=202601 startDate=20260101 endDate=20260131
  • Chunk Oriented Processing (Reader/Processor/Writer or Tasklet) ๊ธฐ๋ฐ˜์˜ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค.
  • ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  Materialized View ์˜ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์žฌํ–ˆ๋‹ค.

๐Ÿงฉ Ranking API

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

๐Ÿ“Ž References

์Šคํ”„๋ง๋ฐฐ์น˜ https://youtu.be/gTQSKYL_AGw?si=MD0uBmLYBsnDVoRH

hubtwork and others added 14 commits December 31, 2025 19:26
์ฃผ๊ฐ„/์›”๊ฐ„ MV ๋žญํ‚น ๋ฉ”ํŠธ๋ฆญ ์ •๋ณด ์ง‘๊ณ„๋ฅผ ์œ„ํ•ด
์•„๋ž˜์˜ ํ…Œ์ด๋ธ”๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ฅด๋„๋ก ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.
```mermaid
erDiagram

    PRODUCT_METRICS {
        BIGINT product_id PK
        VARCHAR2(8) metrics_date PK
        VARCHAR2(50) METRICS_TYPE PK
        BIGINT count
        TIMESTAMP updated_at
        TIMESTAMP created_at
    }

```
```mermaid
erDiagram {
    MV_PRODUCT_RANK_WEEKLY {
        varchar year_week PK
        bigint  product_id PK
        bigint  view_count
        bigint  like_count
        bigint  order_count
        datetime created_at
        datetime updated_at
    }

    MV_PRODUCT_RANK_MONTHLY {
        varchar year_month PK
        bigint  product_id PK
        bigint  view_count
        bigint  like_count
        bigint  order_count
        datetime created_at
        datetime updated_at
    }

}
```
- Instant / ZonedDateTime / LocalDateTime ํ•™์Šต
์ฒญํฌ ๊ธฐ๋ฐ˜ / jdbc / ๊ฐ€์žฅ ๊ฐ„๋‹จํ•˜๊ฒŒ

```sh
java -jar apps/commerce-batch/build/libs/commerce-batch-8472ca24.jar \
  --spring.profiles.active=local \
  --spring.batch.job.enabled=true \
  --spring.batch.job.name=rankWeeklyMvJob \
  yearWeek=2026-W01
```
๋‹ค์Œ๊ณผ ๊ฐ™์€ ์‹์œผ๋กœ score๋ฅผ ๊ณ„์‚ฐํ•œ๋‹ค

```md
score = 0.1*view + 0.2*like + 0.7*order
```
๋‹ค์Œ๊ณผ ๊ฐ™์€ ์‹์œผ๋กœ score๋ฅผ ๊ณ„์‚ฐํ•œ๋‹ค

```md
score = 0.1*view + 0.2*like + 0.7*order
```
๋””๋ฒ„๊น…์šฉ์—์„œ StepMonitorListener๋ฅผ ํ™œ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝ
streamer ๋ชจ๋“ˆ์—์„œ api ๋ชจ๋“ˆ ์œ„์น˜๋กœ ๋ณ€๊ฒฝ

streamer ๋ชจ๋“ˆ์—์„œ ์ง์ ‘์ ์œผ๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , metric ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ์ง‘๊ณ„๋˜๋ฉฐ, api ์—์„œ ์‚ฌ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ
[volume-10] Collect, Stack, Zip
@coderabbitai
Copy link

coderabbitai bot commented Jan 1, 2026

Walkthrough

์ˆœ์œ„ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์ „๋žต ํŒจํ„ด์œผ๋กœ ๋ฆฌํŒฉํ† ๋งํ•˜๊ณ , ์›”๊ฐ„/์ฃผ๊ฐ„ ์ˆœ์œ„ ์ง‘๊ณ„๋ฅผ ์œ„ํ•œ ์ƒˆ๋กœ์šด ๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜(commerce-batch)์„ ๋„์ž…ํ•˜๋ฉฐ, ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ๋ชจ๋ธ์„ ProductLikeMetrics์—์„œ ๋ณตํ•ฉ ํ‚ค ๊ธฐ๋ฐ˜์˜ ProductMetrics๋กœ ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค.

Changes

Cohort / File(s) ๋ณ€๊ฒฝ ์š”์•ฝ
Ranking API ๋ฐ ์„œ๋น„์Šค ๊ณ„์ธต
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingPeriod.java, RankingQuery.java, RankingQueryService.java
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
RankingPeriod ์—ด๊ฑฐํ˜• ์ถ”๊ฐ€, RankingQuery ๋ ˆ์ฝ”๋“œ ์ •์˜, ์„œ๋น„์Šค์—์„œ period ํŒŒ๋ผ๋ฏธํ„ฐ ์ง€์›ํ•˜๋„๋ก ๋ฆฌํŒฉํ† ๋ง. API ์ปจํŠธ๋กค๋Ÿฌ์— period ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ ๋ฐ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ๋ณ€๊ฒฝ
Ranking ์ „๋žต ํŒจํ„ด ๊ตฌํ˜„
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/RankingFetchStrategy.java, RankingFetchStrategyResolver.java, DailyRankingFetcher.java, WeeklyRankingFetcher.java, MonthlyRankingFetcher.java
RankingFetchStrategy ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜, ์ „๋žต ์„ ํƒ์ž(Resolver) ๊ตฌํ˜„, ์ผ/์ฃผ/์›”๋ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์ „๋žต ํด๋ž˜์Šค ์„ธ ๊ฐœ ์ถ”๊ฐ€
Product Ranking ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java, ProductRankMonthlyId.java, ProductRankWeekly.java, ProductRankWeeklyId.java
์›”๊ฐ„/์ฃผ๊ฐ„ ์ˆœ์œ„ ํ…Œ์ด๋ธ” ๋งคํ•‘ ์—”ํ‹ฐํ‹ฐ ๋ฐ ๋ณตํ•ฉ ํ‚ค ํด๋ž˜์Šค ๋„ค ๊ฐœ ์ถ”๊ฐ€
Product Ranking ์ €์žฅ์†Œ
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankMonthlyRepository.java, ProductRankWeeklyRepository.java
JPA ์ €์žฅ์†Œ ๋‘ ๊ฐœ ์ถ”๊ฐ€, ์ปค์Šคํ…€ @Query๋กœ RankingEntry ๊ฐ์ฒด ์ƒ์„ฑํ•˜์—ฌ ๋ฐ˜ํ™˜
Commerce Batch ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ - ๊ธฐ๋ณธ ์„ค์ •
apps/commerce-batch/build.gradle.kts, src/main/java/com/loopers/CommerceBatchApplication.java, src/main/resources/application.yml
์ƒˆ๋กœ์šด ๋ฐฐ์น˜ ๋ชจ๋“ˆ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ, Spring Batch ๋ฐ ์˜์กด์„ฑ ์ถ”๊ฐ€, ๋ฐฐ์น˜ ์ „์šฉ ๋นˆ ํŒฉํ† ๋ฆฌ ์„ค์ •(DataSource ๋ณ„์นญ), ๋‹ค์ค‘ ํ”„๋กœํ•„ YAML ๊ตฌ์„ฑ
Batch ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”
apps/commerce-batch/src/main/java/com/loopers/batch/infrastructure/db/ProductMetricDataInitializer.java, src/main/resources/db/fixtures/large-product-metric-data.sql
๋กœ์ปฌ ํ”„๋กœํ•„ ์‹คํ–‰ ์‹œ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์ดˆ๊ธฐํ™”๊ธฐ, SQL ํ”ฝ์Šค์ฒ˜ ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€
Demo ๋ฐฐ์น˜ ์ž‘์—…
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java, step/DemoTasklet.java, src/test/java/com/loopers/job/demo/DemoJobE2ETest.java
์ƒ˜ํ”Œ ๋ฐฐ์น˜ ์ž‘์—… ๋ฐ ํƒœ์Šคํฌ ์ •์˜, E2E ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
Monthly Ranking ๋ฐฐ์น˜ ์ž‘์—…
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyJobConfig.java, RankMonthlyProcessor.java, RankMonthlyProcessorConfig.java, RankMonthlyReaderConfig.java, RankMonthlyWriterConfig.java, dto/MonthlyAggRow.java, dto/MonthlyRankRow.java
์›”๊ฐ„ ์ˆœ์œ„ ์ง‘๊ณ„ ๋ฐฐ์น˜ ์ž‘์—… ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ(์ฝ๊ธฐ-์ฒ˜๋ฆฌ-์“ฐ๊ธฐ) ๋ฐ DTO ์ •์˜
Weekly Ranking ๋ฐฐ์น˜ ์ž‘์—…
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/RankWeeklyJobConfig.java, step/RankWeeklyProcessor.java, step/RankWeeklyReaderConfig.java, step/RankWeeklyWriterConfig.java, step/dto/WeeklyAggRow.java, step/dto/WeeklyRankRow.java
์ฃผ๊ฐ„ ์ˆœ์œ„ ์ง‘๊ณ„ ๋ฐฐ์น˜ ์ž‘์—… ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ(์ฝ๊ธฐ-์ฒ˜๋ฆฌ-์“ฐ๊ธฐ) ๋ฐ DTO ์ •์˜
Batch ๋ฆฌ์Šค๋„ˆ
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java, JobListener.java, StepMonitorListener.java
๋ฐฐ์น˜ ์ž‘์—… ๋ฐ ์Šคํ… ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฆฌ์Šค๋„ˆ ์„ธ ๊ฐœ ์ถ”๊ฐ€, ๋กœ๊น… ๋ฐ ์ƒํƒœ ์ถ”์ 
Batch ํ…Œ์ŠคํŠธ
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java
์ปจํ…์ŠคํŠธ ๋กœ๋“œ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
Product Metrics ๋„๋ฉ”์ธ ๋ฆฌํŒฉํ† ๋ง
apps/commerce-streamer/src/main/java/com/loopers/domain/MetricsType.java, ProductMetricsId.java, ProductMetricsModel.java
(์ œ๊ฑฐ) ProductLikeMetricsModel.java
MetricsType ์—ด๊ฑฐํ˜• ์ถ”๊ฐ€, ๋ณตํ•ฉ ํ‚ค ๊ธฐ๋ฐ˜ ProductMetricsId/ProductMetricsModel ๋„์ž…, ๊ธฐ์กด ProductLikeMetricsModel ์ œ๊ฑฐ
Product Metrics ์ €์žฅ์†Œ
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.java
(์ œ๊ฑฐ) ProductLikeMetricsRepository.java
ProductMetricsRepository ์‹ ๊ทœ ์ถ”๊ฐ€, ProductLikeMetricsRepository ์ œ๊ฑฐ
Metrics ์„œ๋น„์Šค ๋ฐ ํ…Œ์ŠคํŠธ
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.java
src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
์ €์žฅ์†Œ ํƒ€์ž… ๋ฐ ๋ชจ๋ธ ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ๋กœ ์ƒˆ๋กœ์šด Metrics ๊ตฌ์กฐ ๋ฐ˜์˜, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋„ ํ•จ๊ป˜ ์ˆ˜์ •
๊ธฐํƒ€
README.md, apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java, settings.gradle.kts
README์— commerce-batch ๋””๋ ‰ํ† ๋ฆฌ ์ถ”๊ฐ€, ๋ถˆํ•„์š”ํ•œ import ์ œ๊ฑฐ, Gradle ๋ชจ๋“ˆ include ์ถ”๊ฐ€

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant RankingV1Controller
    participant RankingQueryService
    participant RankingFetchStrategyResolver
    participant RankingFetchStrategy
    participant Repository
    
    Client->>RankingV1Controller: GET /ranking?period=WEEKLY&date=...&size=20
    RankingV1Controller->>RankingQueryService: getDailyPopularProducts(WEEKLY, date, 20)
    RankingQueryService->>RankingFetchStrategyResolver: resolve(WEEKLY, date, 20)
    
    rect rgb(200, 220, 240)
    Note over RankingFetchStrategyResolver: ํ•ด์„(Parse) ๋ฐ ์ •๊ทœํ™”
    RankingFetchStrategyResolver->>RankingFetchStrategyResolver: initLocalDate(date)<br/>normalizeSize(20)<br/>yearWeekKey(LocalDate)<br/>build RankingQuery
    end
    
    RankingFetchStrategyResolver->>RankingFetchStrategyResolver: ์ „๋žต ์„ ํƒ (WEEKLY โ†’ WeeklyRankingFetcher)
    RankingFetchStrategyResolver-->>RankingQueryService: Resolved(RankingQuery, WeeklyFetcher)
    
    rect rgb(220, 240, 220)
    Note over RankingQueryService: ์กฐํšŒ ์‹คํ–‰
    RankingQueryService->>RankingFetchStrategy: fetchRankingEntries(key, limit)
    RankingFetchStrategy->>Repository: findTopByYearWeek(key, PageRequest(0, 20))
    Repository-->>RankingFetchStrategy: List<RankingEntry>
    RankingFetchStrategy-->>RankingQueryService: List<RankingEntry>
    end
    
    RankingQueryService-->>RankingV1Controller: List<RankingEntry>
    RankingV1Controller-->>Client: HTTP 200 + JSON
Loading
sequenceDiagram
    participant Batch as Spring Batch Scheduler
    participant JobConfig
    participant Reader as JDBC Reader
    participant Processor
    participant Writer as JDBC Writer
    participant DB as Database
    
    Batch->>JobConfig: ์›”๊ฐ„ ์ˆœ์œ„ ๋ฐฐ์น˜ ์ž‘์—… ์‹œ์ž‘ (rankMonthlyMvJob)
    JobConfig->>Reader: ์ฒญํฌ ํฌ๊ธฐ 1000์œผ๋กœ ์‹œ์ž‘
    
    rect rgb(255, 240, 200)
    Note over Reader,DB: ์ฝ๊ธฐ ๋‹จ๊ณ„
    Reader->>DB: product_metrics ์ฟผ๋ฆฌ<br/>(startDate, endDate)<br/>์›”๋ณ„ ์ง‘๊ณ„
    DB-->>Reader: MonthlyAggRow ๋ฆฌ์ŠคํŠธ
    Reader-->>Processor: MonthlyAggRow ๋ฐฐ์น˜ (์ตœ๋Œ€ 1000๊ฐœ)
    end
    
    rect rgb(240, 220, 240)
    Note over Processor: ์ฒ˜๋ฆฌ ๋‹จ๊ณ„
    Processor->>Processor: ๊ฐ ํ–‰๋งˆ๋‹ค<br/>score = 0.1*view<br/>        + 0.2*like<br/>        + 0.7*order
    Processor-->>Writer: MonthlyRankRow ๋ฐฐ์น˜
    end
    
    rect rgb(220, 240, 240)
    Note over Writer,DB: ์“ฐ๊ธฐ ๋‹จ๊ณ„
    Writer->>DB: INSERT ... ON DUPLICATE<br/>KEY UPDATE<br/>mv_product_rank_monthly
    DB-->>Writer: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ
    Writer-->>JobConfig: ์ฒญํฌ ์™„๋ฃŒ
    end
    
    Note over JobConfig: ๋ชจ๋“  ์ฒญํฌ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ํ›„ ์ž‘์—… ์ข…๋ฃŒ
Loading

Estimated code review effort

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

Possibly related PRs

  • PR #216: ๋™์ผํ•œ ์ˆœ์œ„ ๊ธฐ๋Šฅ์˜ API ํ‘œ๋ฉด๊ณผ ์„œ๋น„์Šค(RankingV1Controller, ์ˆœ์œ„ ๊ฒ€์ƒ‰ ํ๋ฆ„, ์ˆœ์œ„ ๊ด€๋ จ ์„œ๋น„์Šค ํด๋ž˜์Šค)๋ฅผ ์ˆ˜์ •ํ•˜๋ฏ€๋กœ ์ง์ ‘ ๊ด€๋ จ
  • PR #197: ๋™์ผํ•œ ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ๋ฐ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋„๋ฉ”์ธ(ProductMetrics/ProductMetricsId/ProductMetricsModel, EventHandled ๋ฐ ๊ด€๋ จ ์ €์žฅ์†Œ)์„ ์ˆ˜์ •ํ•˜๋ฏ€๋กœ ์ง์ ‘ ๊ด€๋ จ
  • PR #199: ์ด๋ฒคํŠธ/์•„์›ƒ๋ฐ•์Šค/๋ฉฑ๋“ฑ์„ฑ ๋ฐ ๋ฉ”ํŠธ๋ฆญ ๋„๋ฉ”์ธ(EventHandledService/ํ…Œ์ŠคํŠธ, ๋ฉ”ํŠธ๋ฆญ ๋ชจ๋ธ/์ €์žฅ์†Œ, ๊ด€๋ จ ์ปจ์Šˆ๋จธ/์•„์›ƒ๋ฐ•์Šค)์˜ ๋™์ผ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ˆ˜์ •ํ•˜๋ฏ€๋กœ ์ง์ ‘ ๊ด€๋ จ

Suggested labels

enhancement

Poem

๐Ÿฐ ๋ฐฐ์น˜ ์ž‘์—…์ด ์Œ“์—ฌ ๊ฐ€๊ณ ,
์ „๋žต ํŒจํ„ด์œผ๋กœ ์ˆœ์œ„ ํ๋ฆ„์ด ๋ง‘์•„์ง€๋‹ˆ,
๋ฉ”ํŠธ๋ฆญ ๋ณตํ•ฉํ‚ค๋กœ ๋”์šฑ ๋‹จ๋‹จํ•ด์ง€๋„ค!
์ผ์ฃผ์›”์˜ ์ˆœ์œ„๋ฅผ ์ž๋™์œผ๋กœ ๋ชจ์œผ๊ณ ,
๋“ฃ๋Š” ์ด๋“ค์˜ ์ด์•ผ๊ธฐ๋ฅผ ํ•œ๊ณณ์— ๋‹ด๋„ค. ๐ŸŒŸ

Pre-merge checks and finishing touches

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

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

โค๏ธ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Note

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

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.java (2)

31-45: ๋ฉ”์„œ๋“œ๋ช…์ด ๊ธฐ๋Šฅ๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ฉ”์„œ๋“œ๋ช…์ด getDailyPopularProducts์ด์ง€๋งŒ ์ด์ œ RankingPeriod ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์•„ ์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„ ๋ชจ๋“  ๊ธฐ๊ฐ„์˜ ๋žญํ‚น์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”์„œ๋“œ๋ช…์ด ์‹ค์ œ ๊ธฐ๋Šฅ์„ ๋ฐ˜์˜ํ•˜์ง€ ๋ชปํ•˜๊ณ  ์žˆ์–ด ํ˜ผ๋ž€์„ ์•ผ๊ธฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฉ”์„œ๋“œ๋ช…์„ getPopularProducts ๋˜๋Š” getPopularProductsByPeriod๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋ฉ”์„œ๋“œ๋ช… ๋ณ€๊ฒฝ ์ œ์•ˆ
     @Transactional(readOnly = true)
-    public RankingQueryResponse getDailyPopularProducts(RankingPeriod period, String date, int size) {
+    public RankingQueryResponse getPopularProducts(RankingPeriod period, String date, int size) {
         log.debug("Get {} popular products for {}", period, date);

์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋„ ํ˜ธ์ถœ ๋ถ€๋ถ„์„ ํ•จ๊ป˜ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


53-69: null ๊ฐ’์ด API ์‘๋‹ต์— ํฌํ•จ๋˜๋Š” ๋ฌธ์ œ - ์ˆ˜์ • ํ•„์š”

findProductSummaryFrom ๋ฉ”์„œ๋“œ์—์„œ ์ œํ’ˆ์„ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ null์„ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•˜๋Š”๋ฐ, ์ด ๋ฆฌ์ŠคํŠธ๊ฐ€ ํ•„ํ„ฐ๋ง ์—†์ด RankingQueryResponse์— ์ „๋‹ฌ๋˜๊ณ  JSON์œผ๋กœ ์ง๋ ฌํ™”๋ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ Jackson ์„ค์ •์—์„œ null ๊ฐ’์„ ์ œ์™ธํ•˜๋Š” ์˜ต์…˜์ด ์—†์œผ๋ฏ€๋กœ, API ์‘๋‹ต์˜ productLikeSummary ๋ฐฐ์—ด์— null ์š”์†Œ๊ฐ€ ํฌํ•จ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ์ˆ˜ํ–‰ํ•˜์„ธ์š”:

  • null ๊ฐ’์„ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๋ฆฌ์ŠคํŠธ์—์„œ ์ œ๊ฑฐ
  • ๋˜๋Š” ๋นˆ summary ๊ฐ์ฒด๋‚˜ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋Œ€์ฒด
  • ๋˜๋Š” @JsonInclude(JsonInclude.Include.NON_NULL) ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€ (๋น„๊ถŒ์žฅ)
๐ŸŸก Minor comments (4)
apps/commerce-batch/src/main/resources/db/fixtures/large-product-metric-data.sql-1-3 (1)

1-3: ์ฃผ์„์˜ ๋‚ ์งœ ๋ฒ”์œ„๊ฐ€ ์‹ค์ œ ๋ฐ์ดํ„ฐ์™€ ๋ถˆ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์„์—๋Š” [2025-01-01, 2026-01-31] ๋ฒ”์œ„๋ผ๊ณ  ๋˜์–ด ์žˆ์ง€๋งŒ, n.n BETWEEN 0 AND 395๋Š” 396์ผ์„ ์ƒ์„ฑํ•˜์—ฌ ์‹ค์ œ๋กœ๋Š” 2025-01-01 + 395์ผ = 2026-02-01๊นŒ์ง€์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ฃผ์„ ์ˆ˜์ • ์ œ์•ˆ
 -- PRODUCT_METRICS bulk dummy data
--- ๋ฒ”์œ„: [2025-01-01, 2026-01-31]
+-- ๋ฒ”์œ„: [2025-01-01, 2026-02-01] (396์ผ)
 DELETE FROM product_metrics;
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.java-16-17 (1)

16-17: yearWeek ์ปฌ๋Ÿผ ๊ธธ์ด๋ฅผ 8๋กœ ์ˆ˜์ •ํ•˜์„ธ์š”

yearWeek ํ•„๋“œ๋Š” ISO 8601 ์ฃผ๊ฐ„ ํ˜•์‹์ธ "YYYY-Www"์œผ๋กœ๋งŒ ์‚ฌ์šฉ๋˜๋ฉฐ, ์ด๋Š” ์ •ํ™•ํžˆ 8์ž์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ ์ง€์ •๋œ length = 10์€ ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ธธ์ด๋ณด๋‹ค 2์ž ํฌ๋ฏ€๋กœ, ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

- @Column(name = "year_week_key", length = 10, nullable = false)
+ @Column(name = "year_week_key", length = 8, nullable = false)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessor.java-5-5 (1)

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

WeeklyAggRow๊ฐ€ import๋˜์—ˆ์ง€๋งŒ ์ด ํŒŒ์ผ์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
 import com.loopers.batch.job.rankMonthly.dto.MonthlyAggRow;
 import com.loopers.batch.job.rankMonthly.dto.MonthlyRankRow;
-import com.loopers.batch.job.rankWeekly.step.dto.WeeklyAggRow;
 import lombok.RequiredArgsConstructor;
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyJobConfig.java-5-5 (1)

5-5: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” import๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

WeeklyAggRow๋Š” ์ฃผ๊ฐ„ ์ง‘๊ณ„์šฉ DTO๋กœ, ์›”๊ฐ„ ์ง‘๊ณ„ Job์—์„œ๋Š” ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. RankWeeklyJobConfig์—์„œ ๋ณต์‚ฌํ•  ๋•Œ ๋‚จ์€ ๋ถˆํ•„์š”ํ•œ import์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ๊ฑฐ ์ œ์•ˆ
 package com.loopers.batch.job.rankMonthly;
 
 import com.loopers.batch.job.rankMonthly.dto.MonthlyAggRow;
 import com.loopers.batch.job.rankMonthly.dto.MonthlyRankRow;
-import com.loopers.batch.job.rankWeekly.step.dto.WeeklyAggRow;
 import com.loopers.batch.listener.JobListener;
 import com.loopers.batch.listener.StepMonitorListener;
๐Ÿงน Nitpick comments (30)
apps/commerce-batch/src/main/resources/application.yml (2)

20-23: ๋ฐฐ์น˜ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์œ„ํ•œ ํ—ฌ์Šค ์ฒดํฌ ๊ณ ๋ ค ์‚ฌํ•ญ

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


52-54: ์ค‘๋ณต๋œ ์„ค์ • (์„ ํƒ ์‚ฌํ•ญ)

web-application-type: none์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์–ด ์›น ๋ ˆ์ด์–ด๊ฐ€ ์—†์œผ๋ฏ€๋กœ Springdoc ๋น„ํ™œ์„ฑํ™” ์„ค์ •์€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ์„ค์ •์€ ๋ฌดํ•ดํ•˜๋ฉฐ ๋ช…์‹œ์  ์˜๋„๋ฅผ ํ‘œํ˜„ํ•œ๋‹ค๋ฉด ์œ ์ง€ํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค.

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

31-41: ๋ถ„์‚ฐ ํ™˜๊ฒฝ์—์„œ ZoneId.systemDefault() ์‚ฌ์šฉ ์‹œ ์‹œ๊ฐ„๋Œ€ ๋ถˆ์ผ์น˜ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๋Ÿฌ ์„œ๋ฒ„์—์„œ ๋‹ค๋ฅธ ์‹œ์Šคํ…œ ์‹œ๊ฐ„๋Œ€๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ createdAt/updatedAt ๊ฐ’์ด ์ผ๊ด€๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ZoneOffset.UTC ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž UTC ์‚ฌ์šฉ ์ œ์•ˆ
+import java.time.ZoneOffset;
+
 @PrePersist
 protected void onCreate() {
-    LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault());
+    LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
     this.createdAt = now;
     this.updatedAt = now;
 }

 @PreUpdate
 protected void onUpdate() {
-    this.updatedAt = LocalDateTime.now(ZoneId.systemDefault());
+    this.updatedAt = LocalDateTime.now(ZoneOffset.UTC);
 }

50-52: ๋™์‹œ์„ฑ ์ œ์–ด ์—†์ด count ์ฆ๊ฐ€ ์‹œ lost update ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋™์ผ ์ƒํ’ˆ์— ๋Œ€ํ•ด ๋™์‹œ์— ์—ฌ๋Ÿฌ ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๋˜๋ฉด ์ผ๋ถ€ ์ฆ๊ฐ€๊ฐ€ ์œ ์‹ค๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‚™๊ด€์  ๋ฝ(@Version) ๋˜๋Š” ๋น„๊ด€์  ๋ฝ ์ ์šฉ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”Ž ๋‚™๊ด€์  ๋ฝ ์ถ”๊ฐ€ ์˜ˆ์‹œ
+    @Version
+    private Long version;
+
     public void increase() {
         this.count += 1;
     }
apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsId.java (2)

38-49: ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜„์žฌ ๋‚ ์งœ๋งŒ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

of(Long, MetricsType) ๋ฉ”์„œ๋“œ๋Š” ํ•ญ์ƒ ์˜ค๋Š˜ ๋‚ ์งœ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋‚˜ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์žฌ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ๋‚ ์งœ๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š” ์˜ค๋ฒ„๋กœ๋“œ ๋ฉ”์„œ๋“œ๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋‚ ์งœ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›๋Š” ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
 public static ProductMetricsId of(Long productId, MetricsType metricsType) {
     String nowDate = convertDate(Instant.now());
     return new ProductMetricsId(productId, nowDate, metricsType);
 }

+public static ProductMetricsId of(Long productId, String metricsDate, MetricsType metricsType) {
+    return new ProductMetricsId(productId, metricsDate, metricsType);
+}

44-49: ProductMetricsModel๊ณผ ๋™์ผํ•˜๊ฒŒ ZoneId.systemDefault() ๋Œ€์‹  UTC ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๋ถ„์‚ฐ ํ™˜๊ฒฝ์—์„œ์˜ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ๋™์ผํ•œ ์‹œ๊ฐ„๋Œ€ ์ •์ฑ…์„ ์ ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.java (1)

16-16: ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋ณ€์ˆ˜๋ช…์ด ์‹ค์ œ ์—ญํ• ๊ณผ ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ProductMetricsRepository๋Š” ์ด์ œ ๋ชจ๋“  ๋ฉ”ํŠธ๋ฆญ ํƒ€์ž…(VIEW, LIKE, ORDER_SUCCESS ๋“ฑ)์„ ์ฒ˜๋ฆฌํ•˜์ง€๋งŒ, ๋ณ€์ˆ˜๋ช…์ด likeMetricsRepository๋กœ ๋˜์–ด ์žˆ์–ด ํ˜ผ๋ž€์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋ณ€์ˆ˜๋ช… ์ˆ˜์ • ์ œ์•ˆ
-    private final ProductMetricsRepository likeMetricsRepository;
+    private final ProductMetricsRepository metricsRepository;

๊ทธ๋ฆฌ๊ณ  ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ๋„ ํ•ด๋‹น ๋ณ€์ˆ˜ ์‚ฌ์šฉ์„ ์—…๋ฐ์ดํŠธํ•ด ์ฃผ์„ธ์š”.

apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java (1)

81-87: ์นด์šดํŠธ ์กฐํšŒ ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๋ฉฐ, ๋ฉฑ๋“ฑ์„ฑ ๊ฒ€์ฆ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด ๋ชจ๋ธ์˜ getCount() ์‚ฌ์šฉ์ด ์˜ฌ๋ฐ”๋ฅด๊ณ , ๋กœ๊น… ์ถ”๊ฐ€๊ฐ€ ๋””๋ฒ„๊น…์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ฉฑ๋“ฑ์„ฑ ๊ฒ€์ฆ ๋กœ์ง(์ค‘๋ณต ๋ฉ”์‹œ์ง€ ์ „์†ก ํ›„ ์นด์šดํŠธ +1, ์ฒ˜๋ฆฌ ๊ธฐ๋ก 1๊ฑด)์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.

์„ ํƒ์  ๊ฐœ์„ : Line 86์˜ "product-like-metrics" ๋ฌธ์ž์—ด์„ ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์„ ํƒ์  ๋ฆฌํŒฉํ† ๋ง: ์ปจ์Šˆ๋จธ ์ด๋ฆ„ ์ƒ์ˆ˜ํ™”
+    private static final String CONSUMER_NAME = "product-like-metrics";
+
     @Test
     @DisplayName("๋ฉฑ๋“ฑ์„ฑ ํ…Œ์ŠคํŠธ")
     void duplicate_message_should_be_applied_once() throws Exception {
         // ... existing code ...
         
-        long handledCount = handledRepo.countByConsumerNameAndEventId("product-like-metrics", "06b9f00c-04bb-40fb-a96c-a9da5d0ede53");
+        long handledCount = handledRepo.countByConsumerNameAndEventId(CONSUMER_NAME, "06b9f00c-04bb-40fb-a96c-a9da5d0ede53");
         assertThat(handledCount).isEqualTo(1);
     }
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (2)

19-23: ํƒ€์ž„์กด ์„ค์ • ์‹œ์ ์„ main ๋ฉ”์„œ๋“œ๋กœ ์ด๋™ ๊ถŒ์žฅ

TimeZone.setDefault()๋Š” JVM ์ „์—ญ ์„ค์ •์ด๋ฏ€๋กœ ๊ฐ€๋Šฅํ•œ ํ•œ ๋นจ๋ฆฌ ์„ค์ •๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. @PostConstruct์—์„œ ์„ค์ •ํ•˜๋ฉด ๋‹ค๋ฅธ ๋นˆ๋“ค์ด ์ดˆ๊ธฐํ™”๋  ๋•Œ ์ด๋ฏธ ๋‚ ์งœ/์‹œ๊ฐ„ ๊ด€๋ จ ๊ฐ’๋“ค์ด ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์–ด ์ผ๊ด€์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •์‚ฌํ•ญ
-    @PostConstruct
-    public void started() {
-        // set timezone
-        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
-    }
-
     public static void main(String[] args) {
+        // set timezone before Spring context initialization
+        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
         int exitCode = SpringApplication.exit(SpringApplication.run(CommerceBatchApplication.class, args));
         System.exit(exitCode);
     }

๋˜๋Š” JVM ์ธ์ž๋กœ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

-Duser.timezone=Asia/Seoul

30-41: DataSource ๋ช…์‹œ์  ์„ค์ • ๋ฐฉ์‹ ๊ฒ€ํ†  ๊ถŒ์žฅ

ํ˜„์žฌ BeanFactoryPostProcessor๋ฅผ ํ†ตํ•ด DataSource ๋ณ„์นญ์„ ๋“ฑ๋กํ•˜๋Š” ๊ฒƒ์€ Spring Batch๊ฐ€ "dataSource"๋ผ๋Š” ์ด๋ฆ„์˜ ๋นˆ์„ ๊ธฐ๋Œ€ํ•˜๊ธฐ ๋•Œ๋ฌธ์ธ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ ๋™์ž‘ํ•˜์ง€๋งŒ ์šฐํšŒ์ ์ธ ์ ‘๊ทผ์ž…๋‹ˆ๋‹ค.

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

  1. application.yml์—์„œ spring.batch.jdbc.data-source๋กœ ๋ช…์‹œ์ ์œผ๋กœ DataSource ๋นˆ ์ด๋ฆ„ ์ง€์ •
  2. BatchConfigurer๋ฅผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜์—ฌ DataSource๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •

ํ˜„์žฌ ๊ตฌํ˜„์€ ๋ฐฉ์–ด์ ์ด๊ณ  ์•ˆ์ „ํ•˜์ง€๋งŒ, ์„ค์ • ๊ธฐ๋ฐ˜ ์ ‘๊ทผ์ด ๋” ๋ช…ํ™•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

14-14: QueryDSL ์˜์กด์„ฑ์˜ ๋ฒ„์ „ ๊ด€๋ฆฌ๋Š” ์ •์ƒ์ด๋‚˜, ๋ช…์‹œ์  ์„ ์–ธ ์ถ”์ฒœ

querydsl-apt::jakarta ๊ตฌ๋ฌธ์€ Spring Boot์˜ ์•”์‹œ์  ์˜์กด์„ฑ ๊ด€๋ฆฌ๋ฅผ ํ†ตํ•ด ๋ฒ„์ „์ด ๊ด€๋ฆฌ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ์—์„œ Spring Boot 3.4.4๋ฅผ ์‚ฌ์šฉ ์ค‘์ด๋ฉฐ, io.spring.dependency-management ํ”Œ๋Ÿฌ๊ทธ์ธ์ด QueryDSL ๋ฒ„์ „์„ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ ํ˜„์žฌ ๊ตฌ์„ฑ์€ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

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

  1. jakarta classifier๋Š” ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค - Spring Boot 3.x๋Š” Jakarta EE๋ฅผ ํ‘œ์ค€์œผ๋กœ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ํ˜„์žฌ ์„ค์ •์ด ๋งž์Šต๋‹ˆ๋‹ค.
  2. ๋ฒ„์ „ ๊ด€๋ฆฌ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์„ ์–ธํ•˜๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค - ๋ฃจํŠธ build.gradle.kts์—์„œ Spring Cloud BOM๋งŒ importํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ, ๋” ๋ช…ํ™•ํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด Spring Boot BOM์„ ๋ช…์‹œ์ ์œผ๋กœ importํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”:
    dependencyManagement {
        imports {
            mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}")
        }
    }

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

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

40-47: Step ๋ฉ”์„œ๋“œ๋ช…๊ณผ ์‹ค์ œ ์šฉ๋„๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

categorySyncStep() ๋ฉ”์„œ๋“œ๋ช…์€ ์นดํ…Œ๊ณ ๋ฆฌ ๋™๊ธฐํ™”๋ฅผ ์•”์‹œํ•˜์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” demoSimpleTask๋ผ๋Š” ์ด๋ฆ„์˜ ๋ฐ๋ชจ์šฉ Step์ž…๋‹ˆ๋‹ค. ํ˜ผ๋™์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ฉ”์„œ๋“œ๋ช…์„ Step์˜ ์‹ค์ œ ์šฉ๋„์— ๋งž๊ฒŒ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
     @JobScope
     @Bean(STEP_DEMO_SIMPLE_TASK_NAME)
-    public Step categorySyncStep() {
+    public Step demoSimpleTaskStep() {
         return new StepBuilder(STEP_DEMO_SIMPLE_TASK_NAME, jobRepository)
                 .tasklet(demoTasklet, new ResourcelessTransactionManager())
                 .listener(stepMonitorListener)
                 .build();
     }

๊ทธ๋ฆฌ๊ณ  demoJob() ๋ฉ”์„œ๋“œ์—์„œ๋„ ์ฐธ์กฐ๋ฅผ ์—…๋ฐ์ดํŠธ:

     @Bean(JOB_NAME)
     public Job demoJob() {
         return new JobBuilder(JOB_NAME, jobRepository)
                 .incrementer(new RunIdIncrementer())
-                .start(categorySyncStep())
+                .start(demoSimpleTaskStep())
                 .listener(jobListener)
                 .build();
     }
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (2)

24-26: ๋” ๊ตฌ์ฒด์ ์ธ ์˜ˆ์™ธ ํƒ€์ž… ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

RuntimeException ๋Œ€์‹  IllegalArgumentException์ด๋‚˜ ์ปค์Šคํ…€ ๋ฐฐ์น˜ ์˜ˆ์™ธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์˜ˆ์™ธ์˜ ์›์ธ์„ ๋” ๋ช…ํ™•ํ•˜๊ฒŒ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
         if (requestDate == null) {
-            throw new RuntimeException("requestDate is null");
+            throw new IllegalArgumentException("Required job parameter 'requestDate' is null");
         }

27-29: System.out.println ๋Œ€์‹  SLF4J ๋กœ๊ฑฐ ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

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

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
 @StepScope
 @ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME)
 @RequiredArgsConstructor
 @Component
 public class DemoTasklet implements Tasklet {
     @Value("#{jobParameters['requestDate']}")
     private String requestDate;

     @Override
     public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
         if (requestDate == null) {
             throw new RuntimeException("requestDate is null");
         }
-        System.out.println("Demo Tasklet ์‹คํ–‰ (์‹คํ–‰ ์ผ์ž : " + requestDate + ")");
+        log.info("Demo Tasklet ์‹คํ–‰ (์‹คํ–‰ ์ผ์ž : {})", requestDate);
         Thread.sleep(1000);
-        System.out.println("Demo Tasklet ์ž‘์—… ์™„๋ฃŒ");
+        log.info("Demo Tasklet ์ž‘์—… ์™„๋ฃŒ");
         return RepeatStatus.FINISHED;
     }
 }
apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (3)

4-4: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” @RequiredArgsConstructor ์–ด๋…ธํ…Œ์ด์…˜์„ ์ œ๊ฑฐํ•˜์„ธ์š”.

์ด ํด๋ž˜์Šค์—๋Š” final ํ•„๋“œ๊ฐ€ ์—†๊ณ  ๋ชจ๋“  ์˜์กด์„ฑ์ด @Autowired๋กœ ์ฃผ์ž…๋˜๋ฏ€๋กœ @RequiredArgsConstructor๊ฐ€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
 import com.loopers.batch.job.demo.DemoJobConfig;
-import lombok.RequiredArgsConstructor;
 import org.junit.jupiter.api.BeforeEach;

๊ทธ๋ฆฌ๊ณ  ํด๋ž˜์Šค ์„ ์–ธ์—์„œ:

 @SpringBootTest
 @SpringBatchTest
 @TestPropertySource(properties = "spring.batch.job.name=" + DemoJobConfig.JOB_NAME)
-@RequiredArgsConstructor
 class DemoJobE2ETest {

37-40: ๋น„์–ด ์žˆ๋Š” @BeforeEach ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.

์•„๋ฌด๋Ÿฐ ์„ค์ •์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๋Š” ๋นˆ ๋ฉ”์„œ๋“œ๋Š” ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์„ ์ €ํ•ดํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
-    @BeforeEach
-    void beforeEach() {
-
-    }
-
     @DisplayName("jobParameter ์ค‘ requestDate ์ธ์ž๊ฐ€ ์ฃผ์–ด์ง€์ง€ ์•Š์•˜์„ ๋•Œ, demoJob ๋ฐฐ์น˜๋Š” ์‹คํŒจํ•œ๋‹ค.")

42-56: ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๋ช…์ด ์‹ค์ œ ํ…Œ์ŠคํŠธ ๋™์ž‘๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

shouldNotSaveCategories_whenApiError๋Š” ์นดํ…Œ๊ณ ๋ฆฌ ์ €์žฅ๊ณผ API ์—๋Ÿฌ๋ฅผ ์•”์‹œํ•˜์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” requestDate ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์—†์„ ๋•Œ Job์ด ์‹คํŒจํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. @DisplayName์˜ ๋‚ด์šฉ๊ณผ ์ผ์น˜ํ•˜๋„๋ก ๋ฉ”์„œ๋“œ๋ช…์„ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
     @DisplayName("jobParameter ์ค‘ requestDate ์ธ์ž๊ฐ€ ์ฃผ์–ด์ง€์ง€ ์•Š์•˜์„ ๋•Œ, demoJob ๋ฐฐ์น˜๋Š” ์‹คํŒจํ•œ๋‹ค.")
     @Test
-    void shouldNotSaveCategories_whenApiError() throws Exception {
+    void shouldFail_whenRequestDateParameterIsMissing() throws Exception {
         // arrange
         jobLauncherTestUtils.setJob(job);
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/dto/WeeklyAggRow.java (1)

3-9: LGTM - productId ํƒ€์ž… ์ผ๊ด€์„ฑ ๊ณ ๋ ค ์ œ์•ˆ

๋ ˆ์ฝ”๋“œ ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ์ฃผ์„๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

๋‹ค๋งŒ ์„ ํƒ์ ์œผ๋กœ productId๋ฅผ Long ๋Œ€์‹  long์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ count ํ•„๋“œ๋“ค๊ณผ ํƒ€์ž… ์ผ๊ด€์„ฑ์„ ๋งž์ถ”๊ณ  null ์•ˆ์ „์„ฑ์„ ๋†’์ด๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ปจํ…์ŠคํŠธ์—์„œ productId๊ฐ€ null์ผ ๊ฐ€๋Šฅ์„ฑ์€ ์—†์œผ๋ฏ€๋กœ, primitive ํƒ€์ž… ์‚ฌ์šฉ์ด ๋” ์ ํ•ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessorConfig.java (1)

10-16: LGTM - job parameter ์ผ๊ด€์„ฑ ํ™•์ธ ์ œ์•ˆ

Spring Batch์˜ step-scoped bean ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. yearMonth ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ†ตํ•ด processor์— job ์ปจํ…์ŠคํŠธ๋ฅผ ์ฃผ์ž…ํ•˜๋Š” ํŒจํ„ด์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์„ ํƒ์ ์œผ๋กœ RankMonthlyJobConfig์—์„œ ์ด job parameter๋“ค(yearMonth, startDate, endDate)์ด ๋ชจ๋‘ ์ผ๊ด€๋˜๊ฒŒ ์ „๋‹ฌ๋˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

#!/bin/bash
# RankMonthlyJobConfig์—์„œ job parameter ์„ค์ • ํ™•์ธ
ast-grep --pattern 'class RankMonthlyJobConfig {
  $$$
}'
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQuery.java (1)

5-10: ์ž…๋ ฅ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”.

RankingQuery๋Š” ๋ฐ์ดํ„ฐ ์ „์†ก ๊ฐ์ฒด๋กœ ์ ์ ˆํ•˜์ง€๋งŒ, ํ•„๋“œ์— ๋Œ€ํ•œ ๊ฒ€์ฆ์ด ์—†์Šต๋‹ˆ๋‹ค:

  • period, key, date๋Š” null์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
  • limit์€ ์Œ์ˆ˜๋‚˜ 0์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

RankingFetchStrategyResolver.resolve()์—์„œ ์ผ๋ถ€ ์ •๊ทœํ™”๊ฐ€ ์ด๋ฃจ์–ด์ง€๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ด์ง€๋งŒ, ์ด record๊ฐ€ ๋‹ค๋ฅธ ๊ณณ์—์„œ ์ง์ ‘ ์ƒ์„ฑ๋  ๊ฒฝ์šฐ ์œ ํšจํ•˜์ง€ ์•Š์€ ์ฟผ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Java Bean Validation (@NotNull, @Positive ๋“ฑ) ๋˜๋Š” ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•œ ๊ฒ€์ฆ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

๐Ÿ”Ž ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€ ์˜ˆ์‹œ

์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒ€์ฆ:

+import java.util.Objects;
+
 public record RankingQuery(
         RankingPeriod period,
         String key,
         LocalDate date,
         int limit
-) { }
+) {
+    public RankingQuery {
+        Objects.requireNonNull(period, "period must not be null");
+        Objects.requireNonNull(key, "key must not be null");
+        Objects.requireNonNull(date, "date must not be null");
+        if (limit <= 0) {
+            throw new IllegalArgumentException("limit must be positive");
+        }
+    }
+}
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessor.java (1)

30-35: ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง์ด PR ๋ช…์„ธ์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

๊ณ„์‚ฐ ๊ณต์‹ (0.1view + 0.2like + 0.7*order)์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. TODO ์ฝ”๋ฉ˜ํŠธ์— ์–ธ๊ธ‰๋œ ์ •์ฑ… ๋ถ„๋ฆฌ๋Š” ํ–ฅํ›„ ๊ฐœ์„ ์‚ฌํ•ญ์œผ๋กœ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

TODO ํ•ญ๋ชฉ์ธ ์ ์ˆ˜ ๊ณ„์‚ฐ ์ •์ฑ… ๋ถ„๋ฆฌ๋ฅผ ์œ„ํ•œ ์ „๋žต ํŒจํ„ด ๊ตฌํ˜„ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜, ๋ณ„๋„ ์ด์Šˆ๋กœ ์ƒ์„ฑํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?

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

31-38: ๋กœ๊ทธ ๋ ˆ๋ฒจ์„ error๋กœ ๋ณ€๊ฒฝ ๊ถŒ์žฅ

์‹คํŒจ ์˜ˆ์™ธ ์ •๋ณด๋ฅผ ๋กœ๊น…ํ•  ๋•Œ log.info ๋Œ€์‹  log.error๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ์—์„œ ์—๋Ÿฌ ๋กœ๊ทธ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ณ  ์•Œ๋ฆผ์„ ์„ค์ •ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋กœ๊ทธ ๋ ˆ๋ฒจ ๋ณ€๊ฒฝ ์ œ์•ˆ
-            log.info(
+            log.error(
                 """
                    [์—๋Ÿฌ ๋ฐœ์ƒ]
                    jobName: {}
                    exceptions:
                    {}
                """.trim(), jobName, exceptions
             );

13-16: ๋ถˆํ•„์š”ํ•œ @RequiredArgsConstructor ์–ด๋…ธํ…Œ์ด์…˜

ํ˜„์žฌ ํด๋ž˜์Šค์— final ํ•„๋“œ๊ฐ€ ์—†์œผ๋ฏ€๋กœ @RequiredArgsConstructor๋Š” ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ œ๊ฑฐํ•ด๋„ ๋™์ž‘์— ์˜ํ–ฅ์ด ์—†์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.java (1)

14-21: year_month_key ์ปฌ๋Ÿผ ๊ธธ์ด ์กฐ์ • ๊ฒ€ํ† 

yearMonth ํ•„๋“œ๋Š” ๋ฐฐ์น˜ Writer์™€ Repository์—์„œ ์ผ๊ด€๋˜๊ฒŒ YYYYMM ํ˜•์‹(6์ž๋ฆฌ, ์˜ˆ: "202601")์œผ๋กœ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์„ ์–ธ๋œ ์ปฌ๋Ÿผ ๊ธธ์ด length = 8์€ ์‹ค์ œ ํ˜•์‹๋ณด๋‹ค ์•ฝ๊ฐ„ ํฌ๋ฏ€๋กœ, ์ •ํ™•์„ฑ์„ ์œ„ํ•ด length = 6์œผ๋กœ ์กฐ์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyReaderConfig.java (1)

45-74: ์„œ๋ธŒ์ฟผ๋ฆฌ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ๊ตฌ์กฐ ํ™•์ธ ์™„๋ฃŒ - ์ธ๋ฑ์Šค ์ตœ์ ํ™” ๊ถŒ์žฅ

์„œ๋ธŒ์ฟผ๋ฆฌ ๊ธฐ๋ฐ˜ ์ง‘๊ณ„ ์ฟผ๋ฆฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ, product_metrics ํ…Œ์ด๋ธ”์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์ฆ๊ฐ€ํ•  ๊ฒฝ์šฐ ์ฟผ๋ฆฌ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด (metrics_date, product_id, metrics_type) ๋ณตํ•ฉ ์ธ๋ฑ์Šค ์ƒ์„ฑ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ฟผ๋ฆฌ๋Š” WHERE pm.metrics_date between :startDate and :endDate๋กœ metrics_date๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ณ  GROUP BY pm.product_id๋กœ ์ง‘๊ณ„ํ•˜๋ฏ€๋กœ, ์ด ์ธ๋ฑ์Šค๋Š” ์ฟผ๋ฆฌ ์‹คํ–‰ ๊ณ„ํš์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

23-28: ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง์„ ์ „๋žต ํŒจํ„ด ๋˜๋Š” ์„ค์ • ๊ธฐ๋ฐ˜์œผ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

TODO ์ฃผ์„์—์„œ ์–ธ๊ธ‰ํ•œ ๋Œ€๋กœ, ๊ฐ€์ค‘์น˜๊ฐ€ ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์–ด ์ •์ฑ… ๋ณ€๊ฒฝ ์‹œ ์ฝ”๋“œ ์ˆ˜์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ์ ‘๊ทผ ๋ฐฉ์‹์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”:

  • Option 1: ScoreCalculationStrategy ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋„์ž…ํ•˜๊ณ  ๊ฐ€์ค‘์น˜๋ฅผ ์™ธ๋ถ€ํ™”
  • Option 2: application.yml์— ๊ฐ€์ค‘์น˜๋ฅผ ์„ค์ •ํ•˜๊ณ  @ConfigurationProperties๋กœ ์ฃผ์ž…
๐Ÿ”Ž ์ „๋žต ํŒจํ„ด ๊ธฐ๋ฐ˜ ๋ฆฌํŒฉํ† ๋ง ์˜ˆ์‹œ
// ScoreCalculationStrategy ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
public interface ScoreCalculationStrategy {
    double calculate(long viewCount, long likeCount, long orderCount);
}

// ์„ค์ • ๊ธฐ๋ฐ˜ ๊ตฌํ˜„
@Component
@ConfigurationProperties(prefix = "ranking.score.weights")
public class WeightedScoreCalculator implements ScoreCalculationStrategy {
    private double view = 0.1;
    private double like = 0.2;
    private double order = 0.7;
    
    @Override
    public double calculate(long viewCount, long likeCount, long orderCount) {
        return view * viewCount + like * likeCount + order * orderCount;
    }
    
    // getters and setters
}

// Processor์—์„œ ์‚ฌ์šฉ
@Component
@RequiredArgsConstructor
public class RankWeeklyProcessor implements ItemProcessor<WeeklyAggRow, WeeklyRankRow> {
    private final ScoreCalculationStrategy scoreCalculator;
    
    private double calculateScore(WeeklyAggRow item) {
        return scoreCalculator.calculate(item.viewCount(), item.likeCount(), item.orderCount());
    }
}
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java (1)

46-47: ํƒ€์ž„์กด ์ฒ˜๋ฆฌ์˜ ์ผ๊ด€์„ฑ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

ProductRankWeekly.java์—์„œ๋Š” LocalDateTime.now(ZoneId.systemDefault())์„ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์—ฌ๊ธฐ์„œ๋Š” LocalDateTime.now()๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋™์ผํ•œ ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ผ๊ด€์„ฑ ์ธก๋ฉด์—์„œ ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ผ๊ด€์„ฑ์„ ์œ„ํ•œ ์ˆ˜์ •
-    e.createdAt = LocalDateTime.now();
-    e.updatedAt = LocalDateTime.now();
+    e.createdAt = LocalDateTime.now(ZoneId.systemDefault());
+    e.updatedAt = LocalDateTime.now(ZoneId.systemDefault());

overwrite() ๋ฉ”์„œ๋“œ์˜ 55๋ผ์ธ๋„ ๋™์ผํ•˜๊ฒŒ ์ˆ˜์ •ํ•˜๋ฉด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyWriterConfig.java (1)

36-36: assertUpdates(false) ์„ค์ •์ด ์—…๋ฐ์ดํŠธ ์‹คํŒจ๋ฅผ ๊ฐ์ง€ํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

assertUpdates(false)๋กœ ์„ค์ •ํ•˜๋ฉด ์‹ค์ œ๋กœ ํ–‰์ด ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์•„๋„ ๋ฐฐ์น˜๊ฐ€ ์‹คํŒจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Upsert ์ž‘์—…์˜ ํŠน์„ฑ์ƒ ์˜๋„์ ์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ์‹ค์ œ ์˜ค๋ฅ˜ ์ƒํ™ฉ๋„ ๋ฌด์‹œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์—…๋ฐ์ดํŠธ ์‹คํŒจ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•(๋กœ๊น…, ๋ฉ”ํŠธ๋ฆญ ๋“ฑ)์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java (1)

46-47: ํƒ€์ž„์Šคํƒฌํ”„ ์ƒ์„ฑ ๋ฐฉ์‹์ด ์ผ๊ด€๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Line 46-47์—์„œ๋Š” LocalDateTime.now(ZoneId.systemDefault())๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ, Line 55์—์„œ๋Š” LocalDateTime.now()๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. LocalDateTime.now()๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ZoneId๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๋™์ž‘์€ ๋™์ผํ•˜์ง€๋งŒ, ์ฝ”๋“œ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ๋™์ผํ•œ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ ๋กœ apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java์—์„œ๋Š” ๋ชจ๋“  ํƒ€์ž„์Šคํƒฌํ”„ ์ƒ์„ฑ์— LocalDateTime.now()๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ผ๊ด€์„ฑ ๊ฐœ์„  ์ œ์•ˆ

์˜ต์…˜ 1: ZoneId ๋ช…์‹œ ์ œ๊ฑฐ (ProductRankMonthly์™€ ์ผ๊ด€์„ฑ ์œ ์ง€)

-    e.createdAt = LocalDateTime.now(ZoneId.systemDefault());
-    e.updatedAt = LocalDateTime.now(ZoneId.systemDefault());
+    e.createdAt = LocalDateTime.now();
+    e.updatedAt = LocalDateTime.now();

์˜ต์…˜ 2: ZoneId ๋ช…์‹œ ์ถ”๊ฐ€

-    this.updatedAt = LocalDateTime.now();
+    this.updatedAt = LocalDateTime.now(ZoneId.systemDefault());

Also applies to: 55-55

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

62-77: ์ฝ”๋“œ ์ค‘๋ณต ๋ฐ ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ ๊ฐœ์„ ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

initLocalDate์™€ hasValidDate ๋ฉ”์„œ๋“œ๊ฐ€ apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/DailyRankingFetcher.java (lines 19-26)์—๋„ ๋™์ผํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ hasValidDate ๋ฉ”์„œ๋“œ๋ช…์ด ํ˜ผ๋ž€์Šค๋Ÿฝ์Šต๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋Š” date๊ฐ€ null์ด๊ฑฐ๋‚˜ blank์ผ ๋•Œ true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ, "์œ ํšจํ•œ ๋‚ ์งœ๊ฐ€ ์žˆ๋Š”๊ฐ€?"๋ผ๋Š” ์˜๋ฏธ์™€ ๋ฐ˜๋Œ€๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

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

1. ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค๋กœ ์ถ”์ถœ:

์ƒˆ๋กœ์šด ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค ์ƒ์„ฑ:

public class RankingDateUtils {
    private static final DateTimeFormatter BASIC_ISO_DATE = DateTimeFormatter.BASIC_ISO_DATE;
    
    public static LocalDate parseOrToday(String date) {
        return (date == null || date.isBlank())
                ? LocalDate.now(ZoneId.systemDefault())
                : LocalDate.parse(date, BASIC_ISO_DATE);
    }
}

์ดํ›„ RankingFetchStrategyResolver์™€ DailyRankingFetcher์—์„œ ์ด ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝ:

-private LocalDate initLocalDate(String date) {
-    return (hasValidDate(date))
-            ? LocalDate.now(ZoneId.systemDefault())
-            : LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE);
-}
-
-private boolean hasValidDate(String date) {
-    return date == null || date.isBlank();
-}
+private LocalDate initLocalDate(String date) {
+    return RankingDateUtils.parseOrToday(date);
+}

2. ๋˜๋Š” ๋ฉ”์„œ๋“œ๋ช… ๊ฐœ์„  (์ตœ์†Œํ•œ์˜ ๋ณ€๊ฒฝ):

-private boolean hasValidDate(String date) {
+private boolean isDateEmpty(String date) {
     return date == null || date.isBlank();
 }
 
 private LocalDate initLocalDate(String date) {
-    return (hasValidDate(date))
+    return (isDateEmpty(date))
             ? LocalDate.now(ZoneId.systemDefault())
             : LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE);
 }
๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 0f9f582 and ca9b019.

๐Ÿ“’ Files selected for processing (51)
  • README.md
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingPeriod.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQuery.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/DailyRankingFetcher.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/MonthlyRankingFetcher.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/RankingFetchStrategy.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/RankingFetchStrategyResolver.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/WeeklyRankingFetcher.java
  • apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java
  • apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.java
  • apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java
  • apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankMonthlyRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankWeeklyRepository.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
  • apps/commerce-batch/build.gradle.kts
  • apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/infrastructure/db/ProductMetricDataInitializer.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyJobConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessor.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessorConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyReaderConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyWriterConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/dto/MonthlyAggRow.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/dto/MonthlyRankRow.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/RankWeeklyJobConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyProcessor.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyReaderConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyWriterConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/dto/WeeklyAggRow.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/dto/WeeklyRankRow.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
  • apps/commerce-batch/src/main/resources/application.yml
  • apps/commerce-batch/src/main/resources/db/fixtures/large-product-metric-data.sql
  • apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java
  • apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/MetricsType.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ProductLikeMetricsModel.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsId.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsModel.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductLikeMetricsRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
  • settings.gradle.kts
๐Ÿ’ค Files with no reviewable changes (2)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductLikeMetricsRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ProductLikeMetricsModel.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-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java
  • apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java
  • apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsId.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsModel.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-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java
  • apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java
  • apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsId.java
  • apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.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-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankWeeklyRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankMonthlyRepository.java
๐Ÿ“š Learning: 2025-11-21T03:38:07.494Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 99
File: apps/commerce-api/src/main/resources/application.yml:26-30
Timestamp: 2025-11-21T03:38:07.494Z
Learning: The batch job implementation for likeCount synchronization in apps/commerce-api is temporary and intended for development environment only. It will be replaced with Event-Driven Architecture (EDA) before production deployment, so production-level configuration concerns (like profile-based initialize-schema settings) are not required.

Applied to files:

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

Applied to files:

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

Applied to files:

  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.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/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
๐Ÿ“š Learning: 2025-12-19T23:39:20.851Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-streamer/src/main/kotlin/com/loopers/interfaces/consumer/product/ProductEventConsumer.kt:0-0
Timestamp: 2025-12-19T23:39:20.851Z
Learning: In Loopers-dev-lab/loopers-spring-kotlin-template, toongri prefers pragmatic idempotency handling in Kafka consumers: when idempotency key persistence fails after successful business logic execution, log a warning and continue (accepting low risk of duplicates on retry) rather than rolling back business data. This keeps business logic decoupled from idempotency store (which might be Redis/external system, not RDB), avoids over-engineering for rare edge cases, and prioritizes core business value over auxiliary metadata. Transaction-based atomicity between business data and idempotency keys is considered excessive when duplicate event probability is very low.

Applied to files:

  • apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.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-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/infrastructure/db/ProductMetricDataInitializer.java
  • apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java
๐Ÿงฌ Code graph analysis (17)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java (1)
  • Entity (12-65)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/RankWeeklyJobConfig.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyWriterConfig.java (1)
  • Configuration (12-39)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyReaderConfig.java (1)
  • Configuration (16-80)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java (1)
  • ConditionalOnProperty (19-48)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
  • Slf4j (9-21)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
  • Slf4j (14-53)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyWriterConfig.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessorConfig.java (1)
  • Configuration (8-17)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyReaderConfig.java (1)
  • Configuration (16-77)
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/RankingFetchStrategyResolver.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/DailyRankingFetcher.java (1)
  • Component (14-39)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
  • Slf4j (14-53)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
  • Slf4j (13-44)
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/WeeklyRankingFetcher.java (3)
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/MonthlyRankingFetcher.java (1)
  • Slf4j (13-29)
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/DailyRankingFetcher.java (1)
  • Component (14-39)
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/RankingFetchStrategyResolver.java (1)
  • Component (16-78)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java (1)
  • Entity (13-65)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyReaderConfig.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
  • StepScope (14-32)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyWriterConfig.java (1)
  • Configuration (12-39)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.java (1)
  • Embeddable (9-21)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyReaderConfig.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
  • StepScope (14-32)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessorConfig.java (1)
  • Configuration (8-17)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyWriterConfig.java (1)
  • Configuration (12-39)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessorConfig.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
  • StepScope (14-32)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyReaderConfig.java (1)
  • Configuration (16-77)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyWriterConfig.java (1)
  • Configuration (12-39)
apps/commerce-batch/src/main/java/com/loopers/batch/infrastructure/db/ProductMetricDataInitializer.java (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/config/KafkaTopicsConfig.java (1)
  • Profile (9-37)
apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
  • SpringBootTest (6-10)
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
  • SpringBootTest (23-76)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.java (1)
  • Embeddable (9-21)
๐Ÿ”‡ Additional comments (36)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)

9-9: ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์Šน์ธ!

UserActionType import ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ์ด import๋Š” ๊ธฐ์กด @TrackUserAction ์–ด๋…ธํ…Œ์ด์…˜(28๋ฒˆ, 42๋ฒˆ ๋ผ์ธ)์—์„œ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ฝ”๋“œ์˜ ๋ช…์‹œ์„ฑ์„ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.

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

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

web-application-type: none๊ณผ job.name: ${job.name:NONE} ์„ค์ •์ด ๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ ํ•ฉํ•˜๋ฉฐ, ๊ธฐ๋ณธ๊ฐ’ NONE์„ ํ†ตํ•ด ์˜๋„ํ•˜์ง€ ์•Š์€ ์ž‘์—… ์‹คํ–‰์„ ๋ฐฉ์ง€ํ•˜๋Š” ์•ˆ์ „ ์žฅ์น˜๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


17-18: Spring Batch ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ๊ด€๋ฆฌ ์ „๋žต์„ ๋ช…ํ™•ํžˆ ํ•˜์„ธ์š”.

local/test ํ”„๋กœํŒŒ์ผ์€ initialize-schema: always๋กœ ์„ค์ •๋˜์ง€๋งŒ, dev/qa/prd ํ”„๋กœํŒŒ์ผ์€ ๊ธฐ๋ณธ๊ฐ’ never๋ฅผ ์ƒ์†ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—์„œ Flyway๋‚˜ Liquibase ๊ฐ™์€ ์Šคํ‚ค๋งˆ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋„๊ตฌ๊ฐ€ ๊ตฌ์„ฑ๋˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์—, ์šด์˜ ํ™˜๊ฒฝ์—์„œ Spring Batch ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”(BATCH_JOB_INSTANCE, BATCH_JOB_EXECUTION ๋“ฑ)์ด ์–ด๋–ป๊ฒŒ ์ƒ์„ฑ๋˜๊ณ  ๊ด€๋ฆฌ๋˜๋Š”์ง€ ๋ช…์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ•ด๋‹น ํ…Œ์ด๋ธ”์ด ๋ฏธ๋ฆฌ ์ƒ์„ฑ๋˜์–ด ์žˆ๊ฑฐ๋‚˜, ํ–ฅํ›„ ์Šคํ‚ค๋งˆ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋„๊ตฌ ๋„์ž… ๊ณ„ํš์ด ์žˆ๋‹ค๋ฉด ์ฝ”๋“œ๋‚˜ ์„ค์ • ํŒŒ์ผ์— ๋ช…์‹œํ•˜์„ธ์š”.

โ›” Skipped due to learnings
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 99
File: apps/commerce-api/src/main/resources/application.yml:26-30
Timestamp: 2025-11-21T03:38:07.494Z
Learning: The batch job implementation for likeCount synchronization in apps/commerce-api is temporary and intended for development environment only. It will be replaced with Event-Driven Architecture (EDA) before production deployment, so production-level configuration concerns (like profile-based initialize-schema settings) are not required.
apps/commerce-batch/src/main/resources/db/fixtures/large-product-metric-data.sql (1)

5-44: Fixture ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Cross join์„ ํ™œ์šฉํ•œ ๋ฒŒํฌ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํŒจํ„ด์ด ํšจ์œจ์ ์ด๋ฉฐ, metrics_type๋ณ„๋กœ ํ˜„์‹ค์ ์ธ count ๋ฒ”์œ„๋ฅผ ์„ค์ •ํ•œ ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. Local ํ”„๋กœํŒŒ์ผ์—์„œ๋งŒ ์‹คํ–‰๋˜๋ฏ€๋กœ ์šด์˜ ํ™˜๊ฒฝ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

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

1-8: LGTM!

enum ์ƒ์ˆ˜๋“ค์ด SQL fixture ๋ฐ ๋„๋ฉ”์ธ ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ์ผ์น˜ํ•˜๋ฉฐ, ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

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

1-8: LGTM!

๋ณตํ•ฉ ํ‚ค(ProductMetricsId)๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ Spring Data JPA ๋ ˆํฌ์ง€ํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. ํ•„์š”์‹œ ์ปค์Šคํ…€ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.java (1)

18-28: ๋ณตํ•ฉ ํ‚ค ๊ธฐ๋ฐ˜ ๋ฉ”ํŠธ๋ฆญ ์ฒ˜๋ฆฌ ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

findById().orElseGet() ํŒจํ„ด์œผ๋กœ ์กฐํšŒ ๋˜๋Š” ์ƒ์„ฑ ํ›„ increase()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์ด JPA dirty checking๊ณผ ํ•จ๊ป˜ ์ •์ƒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. @Transactional ๋‚ด์—์„œ ์—”ํ‹ฐํ‹ฐ ๋ณ€๊ฒฝ์ด ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ ์‹œ ์ž๋™์œผ๋กœ flush๋ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java (4)

3-7: ์ƒˆ๋กœ์šด ProductMetrics ๋„๋ฉ”์ธ ๋ชจ๋ธ๋กœ์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฐ˜์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ž„ํฌํŠธ ๋ฐ ๋กœ๊น… ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. MetricsType, ProductMetricsId, ProductMetricsModel๋กœ์˜ ์ „ํ™˜์ด PR ๋ชฉํ‘œ์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 10-10, 26-26


38-38: ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํƒ€์ž… ์—…๋ฐ์ดํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

ProductMetricsRepository๋กœ์˜ ์ „ํ™˜์ด ํ†ตํ•ฉ๋œ ๋ฉ”ํŠธ๋ฆญ ๋ชจ๋ธ๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.


68-73: ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๋Œ€๊ธฐ๋ฅผ ์œ„ํ•œ Thread.sleep ์‚ฌ์šฉ์— ๋Œ€ํ•œ TODO๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

Thread.sleep์„ ์‚ฌ์šฉํ•œ ๋™๊ธฐํ™”๋Š” ํ…Œ์ŠคํŠธ ์•ˆ์ •์„ฑ ์ธก๋ฉด์—์„œ ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ์œผ๋‚˜, ์ด๋ฏธ TODO ์ฃผ์„(line 72)์œผ๋กœ ๊ฐœ์„  ํ•„์š”์„ฑ์ด ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ–ฅํ›„ Awaitility ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ TestContainers์˜ ์ปจ์Šˆ๋จธ ๋™๊ธฐํ™” ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ๊ณ ๋ คํ•ด๋ณด์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.


57-62: ๋ณตํ•ฉ ํ‚ค ํŒจํ„ด์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

ProductMetricsId์— MetricsType.LIKE๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒƒ์ด ์ ์ ˆํ•˜๋ฉฐ, ๋กœ๊น… ์ถ”๊ฐ€๊ฐ€ ํ…Œ์ŠคํŠธ ๋””๋ฒ„๊น…์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ProductMetricsId.of(productId, metricsType) ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ์ž๋™์œผ๋กœ metricsDate๋ฅผ ํ˜„์žฌ ๋‚ ์งœ์˜ yyyyMMdd ํ˜•์‹์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•˜๋ฏ€๋กœ, ๋ณตํ•ฉ ํ‚ค์˜ ์„ธ ํ•„๋“œ(productId, metricsDate, metricsType) ๋ชจ๋‘๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ NOT NULL ์ œ์•ฝ ์กฐ๊ฑด๋„ ๋งŒ์กฑํ•ฉ๋‹ˆ๋‹ค.

README.md (1)

30-30: LGTM! ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

settings.gradle.kts (1)

6-6: LGTM! ๋ชจ๋“ˆ ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

commerce-batch ๋ชจ๋“ˆ์ด Gradle ๋ฉ€ํ‹ฐ๋ชจ๋“ˆ ํ”„๋กœ์ ํŠธ์— ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

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

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

ํ•„์š”ํ•œ ๋ชจ๋“ˆ๋“ค(jpa, redis, jackson, logging, monitoring)๊ณผ Spring Batch ์˜์กด์„ฑ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. test-fixtures ์‚ฌ์šฉ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

6-9: LGTM! ํ‘œ์ค€ ์Šค๋ชจํฌ ํ…Œ์ŠคํŠธ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋กœ๋“œ๋˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ธฐ๋ณธ ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค. ๋นˆ ํ…Œ์ŠคํŠธ ๋ณธ๋ฌธ์€ ์˜๋„๋œ ๊ฒƒ์œผ๋กœ, ์„ค์ • ์˜ค๋ฅ˜๋ฅผ ์กฐ๊ธฐ์— ๋ฐœ๊ฒฌํ•˜๋Š” ๋ฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

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

25-28: LGTM! ๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ ํ•ฉํ•œ ์ข…๋ฃŒ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

SpringApplication.exit()์™€ System.exit()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐฐ์น˜ ์ž‘์—… ์™„๋ฃŒ ํ›„ ์ ์ ˆํ•˜๊ฒŒ ์ข…๋ฃŒ ์ฝ”๋“œ๋ฅผ ์ „ํŒŒํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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

58-75: ํ…Œ์ŠคํŠธ ๊ตฌ์กฐ์™€ ๊ฒ€์ฆ ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์„ฑ๊ณต ์ผ€์ด์Šค์—์„œ JobParametersBuilder๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ requestDate๋ฅผ ์„ค์ •ํ•˜๊ณ , ExitStatus.COMPLETED๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ์‹์ด ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.java (1)

9-14: LGTM!

JPA ์ž„๋ฒ ๋””๋“œ ID ํด๋ž˜์Šค ๊ตฌ์กฐ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Serializable ๊ตฌํ˜„, equals/hashCode ์˜ค๋ฒ„๋ผ์ด๋“œ, ๊ทธ๋ฆฌ๊ณ  protected ๊ธฐ๋ณธ ์ƒ์„ฑ์ž๊ฐ€ ๋ชจ๋‘ ์ ์ ˆํ•˜๊ฒŒ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

3-7: LGTM!

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

apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/dto/MonthlyRankRow.java (2)

3-10: ๊ฐ„๊ฒฐํ•˜๊ณ  ๋ช…ํ™•ํ•œ DTO ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

Record๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ถˆ๋ณ€ ๋ฐ์ดํ„ฐ ์ „์†ก ๊ฐ์ฒด๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฒƒ์€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ํ•„๋“œ๋ช…๊ณผ ํƒ€์ž…์ด ๋ช…ํ™•ํ•˜๋ฉฐ ๋ฐฐ์น˜ ์ž‘์—…์˜ ์ถœ๋ ฅ ๋ชจ๋ธ๋กœ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.


5-5: productId๊ฐ€ null์ด ๋  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ๋กœ๋ฅผ ๊ฒ€ํ† ํ•˜์„ธ์š”.

Reader์˜ SQL ์ฟผ๋ฆฌ์—์„œ GROUP BY pm.product_id๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์›๋ณธ ๋ฐ์ดํ„ฐ์— null product_id๊ฐ€ ์กด์žฌํ•˜๋ฉด, SQL GROUP BY๋Š” null์„ ๋ณ„๋„์˜ ๊ทธ๋ฃน์œผ๋กœ ํฌํ•จ์‹œํ‚ต๋‹ˆ๋‹ค. ํ˜„์žฌ ์ฝ”๋“œ๋Š”:

  • MonthlyAggRow์™€ MonthlyRankRow ๋ชจ๋‘ Long productId (nullable)
  • Reader: rs.getLong() ์‚ฌ์šฉ (nullable ์ปฌ๋Ÿผ์— ๋Œ€ํ•ด ๋ถ€์ ์ ˆํ•  ์ˆ˜ ์žˆ์Œ)
  • Processor: null ์ฒดํฌ ์—†์ด ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ
  • Writer: :productId๋ฅผ SQL์— ์‚ฌ์šฉ

๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•˜๋ ค๋ฉด, Reader ๋‹จ๊ณ„์—์„œ null product_id ํ–‰์„ ํ•„ํ„ฐ๋งํ•˜๊ฑฐ๋‚˜, productId๋ฅผ primitive long์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

โ›” Skipped due to learnings
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() ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.java (1)

28-28: ์ „๋žต ํŒจํ„ด์„ ์œ„ํ•œ ์ ์ ˆํ•œ ์˜์กด์„ฑ ์ถ”๊ฐ€์ž…๋‹ˆ๋‹ค.

RankingFetchStrategyResolver ์˜์กด์„ฑ์„ ์ฃผ์ž…ํ•˜์—ฌ ๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์กฐํšŒ ์ „๋žต์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ ๊ฒƒ์€ ์ข‹์€ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/dto/MonthlyAggRow.java (2)

3-8: ๊ฐ„๊ฒฐํ•œ ์ž…๋ ฅ DTO ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

๋ฐฐ์น˜ Reader์˜ ์ž…๋ ฅ ๋ชจ๋ธ๋กœ ์ ์ ˆํ•œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. MonthlyRankRow์™€์˜ ๋ช…ํ™•ํ•œ ๊ตฌ๋ถ„(yearMonth์™€ score ์—†์Œ)์ด Reader-Processor-Writer ํŒŒ์ดํ”„๋ผ์ธ์˜ ์ฑ…์ž„ ๋ถ„๋ฆฌ๋ฅผ ์ž˜ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.


4-4: productId๋Š” NULL ๊ฐ’์„ ๊ฐ€์งˆ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ์ถ”๊ฐ€ ์กฐ์น˜๊ฐ€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

SQL ์ฟผ๋ฆฌ์˜ GROUP BY pm.product_id ์ ˆ ๋•Œ๋ฌธ์— NULL productId๋Š” ๊ฒฐ๊ณผ ์ง‘ํ•ฉ์— ํฌํ•จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. GROUP BY๋Š” NULL ๊ฐ’์„ ํ•„ํ„ฐ๋งํ•˜๋ฏ€๋กœ Reader๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ชจ๋“  productId๋Š” NULL์ด ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ˜„์žฌ ์ฝ”๋“œ๋Š” NULL ์ฒ˜๋ฆฌ ๊ด€์ ์—์„œ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค๋งŒ, ํ–ฅํ›„ ์ฟผ๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜๊ฑฐ๋‚˜ LEFT JOIN ๋“ฑ์ด ์ถ”๊ฐ€๋  ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด rs.getLong() ๋Œ€์‹  rs.getObject("product_id", Long.class) ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•˜๊ฑฐ๋‚˜, productId๋ฅผ primitive long์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

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

13-29: ์ „๋žต ํŒจํ„ด ๊ตฌํ˜„์ด ์ผ๊ด€๋˜๊ณ  ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

WeeklyRankingFetcher๊ฐ€ RankingFetchStrategy ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„ํ–ˆ์œผ๋ฉฐ, DailyRankingFetcher ๋ฐ MonthlyRankingFetcher์™€ ์ผ๊ด€๋œ ํŒจํ„ด์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•˜๊ณ  ์ฑ…์ž„์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.


24-28: ๊ธ€๋กœ๋ฒŒ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์ด๋ฏธ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

weeklyRankingRepository.findTopByYearWeek() ํ˜ธ์ถœ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋Š” ApiControllerAdvice์˜ ์ผ๋ฐ˜์ ์ธ Throwable ํ•ธ๋“ค๋Ÿฌ(line 118-122)์— ์˜ํ•ด ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์˜ค๋ฅ˜๋‚˜ ์ฟผ๋ฆฌ ์˜ค๋ฅ˜๋Š” ErrorType.INTERNAL_ERROR๋กœ ์‘๋‹ต๋ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌ์กฐ๋Š” ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์„ค๊ณ„๋˜์—ˆ์œผ๋‚˜, ๋‹ค์Œ์„ ๊ณ ๋ คํ•˜์„ธ์š”:

  • ์ €์žฅ์†Œ ์˜ค๋ฅ˜์— ๋Œ€ํ•œ ํŠน์ • ์—๋Ÿฌ ์‘๋‹ต์ด ํ•„์š”ํ•˜๋ฉด CoreException์„ ๋ช…์‹œ์ ์œผ๋กœ ๋˜์ง€๊ฑฐ๋‚˜ ApiControllerAdvice์— ๋ณ„๋„ ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€
  • yearWeek ํ˜•์‹ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜๋ฉด ์ฟผ๋ฆฌ ์ „์— ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ถ”๊ฐ€
  • ํ˜„์žฌ๋Š” ๋‹ค๋ฅธ fetcher์™€ ๋™์ผํ•œ ํŒจํ„ด์ด๋ฏ€๋กœ ์ผ๊ด€์„ฑ์€ ์œ ์ง€๋จ
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)

25-52: ์‹œ๊ฐ„ ๊ณ„์‚ฐ ๋ฐ ๋กœ๊น… ๋กœ์ง์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.

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

apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/dto/WeeklyRankRow.java (1)

1-10: Record ์ •์˜๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์— ์‚ฌ์šฉ๋˜๋Š” DTO๋กœ์„œ ๋ถˆ๋ณ€ record ํƒ€์ž…์ด ์ ํ•ฉํ•˜๊ฒŒ ์„ ํƒ๋˜์—ˆ๊ณ , ํ•„๋“œ ํƒ€์ž…๋„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

1-12: ์ „๋žต ํŒจํ„ด ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

period๋ณ„ ranking ์กฐํšŒ ์ „๋žต์„ ์ถ”์ƒํ™”ํ•œ ์ธํ„ฐํŽ˜์ด์Šค ์„ค๊ณ„๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

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

13-25: LGTM!

๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์™€ JPQL ์ฟผ๋ฆฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. RankingEntry ์ƒ์„ฑ์ž ํ‘œํ˜„์‹๊ณผ Pageable์„ ํ™œ์šฉํ•œ ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

13-25: LGTM!

ProductRankWeeklyRepository์™€ ์ผ๊ด€๋œ ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์›”๊ฐ„ ์ˆœ์œ„ ์กฐํšŒ๋ฅผ ์œ„ํ•œ ์ฟผ๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

13-29: LGTM!

์ „๋žต ํŒจํ„ด์„ ํ™œ์šฉํ•œ ๊น”๋”ํ•œ ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค. WeeklyRankingFetcher์™€ ์ผ๊ด€๋œ ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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

8-21: ๊ตฌํ˜„์ด ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

๋ฐฐ์น˜ ํ”„๋กœ์„ธ์„œ๊ฐ€ WeeklyAggRow๋ฅผ WeeklyRankRow๋กœ ์ •ํ™•ํ•˜๊ฒŒ ๋ณ€ํ™˜ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง๋„ ํ†ตํ•ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java (1)

12-37: ์—”ํ‹ฐํ‹ฐ ๊ตฌ์กฐ๊ฐ€ ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

๋ณตํ•ฉ ํ‚ค์™€ ํ•„๋“œ ๋งคํ•‘์ด ์ ์ ˆํ•˜๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

14-28: ์ „๋žต ํŒจํ„ด ๊ตฌํ˜„์ด ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

RankingFetchStrategy ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ผ์ผ ์ˆœ์œ„ ์กฐํšŒ ๋กœ์ง์ด ์ ์ ˆํ•˜๊ฒŒ ์บก์Аํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/RankWeeklyJobConfig.java (1)

23-56: Spring Batch ๊ตฌ์„ฑ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

chunk ํฌ๊ธฐ 1000์œผ๋กœ ์„ค์ •๋œ ํ‘œ์ค€์ ์ธ chunk-oriented step ๊ตฌ์„ฑ์ด๋ฉฐ, Reader/Processor/Writer๊ฐ€ ์ ์ ˆํžˆ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Job๊ณผ Step ์ˆ˜์ค€์˜ ๋ฆฌ์Šค๋„ˆ๋„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Comment on lines +30 to +38
private LocalDate initLocalDate(String date) {
return (hasValidDate(date))
? LocalDate.now(ZoneId.systemDefault())
: LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE);
}

private boolean hasValidDate(String date) {
return date == null || date.isBlank();
}
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

๋ฉ”์„œ๋“œ ์ด๋ฆ„์ด ์‹ค์ œ ๋™์ž‘๊ณผ ๋ฐ˜๋Œ€์ž…๋‹ˆ๋‹ค.

hasValidDate() ๋ฉ”์„œ๋“œ๋Š” date๊ฐ€ null์ด๊ฑฐ๋‚˜ blank์ผ ๋•Œ true๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” "์œ ํšจํ•œ ๋‚ ์งœ๊ฐ€ ์žˆ๋Š”์ง€"๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ "๋‚ ์งœ๊ฐ€ ์—†๋Š”์ง€"๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ, ๋ฉ”์„œ๋“œ ์ด๋ฆ„์ด ๋กœ์ง๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์•„ ํ˜ผ๋ž€์„ ์•ผ๊ธฐํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ ๋‚ ์งœ ํŒŒ์‹ฑ ์‹œ DateTimeParseException์— ๋Œ€ํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์—†์–ด, ์ž˜๋ชป๋œ ํ˜•์‹์˜ ๋‚ ์งœ๊ฐ€ ์ „๋‹ฌ๋  ๊ฒฝ์šฐ ์ ์ ˆํ•œ ์—๋Ÿฌ ์‘๋‹ต ์—†์ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ๊ฐœ์„ ์‚ฌํ•ญ
 private LocalDate initLocalDate(String date) {
-    return (hasValidDate(date))
+    return (isDateMissing(date))
             ? LocalDate.now(ZoneId.systemDefault())
-            : LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE);
+            : parseDate(date);
 }

-private boolean hasValidDate(String date) {
+private boolean isDateMissing(String date) {
     return date == null || date.isBlank();
 }

+private LocalDate parseDate(String date) {
+    try {
+        return LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE);
+    } catch (DateTimeParseException e) {
+        throw new IllegalArgumentException(
+            "Invalid date format. Expected format: yyyyMMdd, but got: " + date, e);
+    }
+}
๐Ÿ“ 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
private LocalDate initLocalDate(String date) {
return (hasValidDate(date))
? LocalDate.now(ZoneId.systemDefault())
: LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE);
}
private boolean hasValidDate(String date) {
return date == null || date.isBlank();
}
private LocalDate initLocalDate(String date) {
return (isDateMissing(date))
? LocalDate.now(ZoneId.systemDefault())
: parseDate(date);
}
private boolean isDateMissing(String date) {
return date == null || date.isBlank();
}
private LocalDate parseDate(String date) {
try {
return LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(
"Invalid date format. Expected format: yyyyMMdd, but got: " + date, e);
}
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/DailyRankingFetcher.java
around lines 30-38, the helper method name and logic are inverted and there is
no parsing error handling: rename or flip hasValidDate so its name matches its
behavior (e.g., isBlankOrNull -> retains current boolean) or change its logic to
return true when the input is a valid non-blank date, then update initLocalDate
to use the corrected predicate; additionally wrap LocalDate.parse in a try/catch
for DateTimeParseException and handle it by throwing a controlled application
exception or returning a clear error value (or fallback) with a descriptive
message so malformed dates do not propagate raw exceptions.

Comment on lines +39 to +49
public static ProductRankMonthly of(String yearMonth, Long productId,
long viewCount, long likeCount, long orderCount) {
ProductRankMonthly e = new ProductRankMonthly();
e.id = new ProductRankMonthlyId(yearMonth, productId);
e.viewCount = viewCount;
e.likeCount = likeCount;
e.orderCount = orderCount;
e.createdAt = LocalDateTime.now();
e.updatedAt = LocalDateTime.now();
return e;
}
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

์น˜๋ช…์ : score ํ•„๋“œ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

score ํ•„๋“œ๋Š” nullable = false๋กœ ์„ ์–ธ๋˜์–ด ์žˆ์ง€๋งŒ of() ๋ฉ”์„œ๋“œ์—์„œ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์—”ํ‹ฐํ‹ฐ ์ €์žฅ ์‹œ ์ œ์•ฝ์กฐ๊ฑด ์œ„๋ฐ˜(constraint violation)์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

๋ฐฐ์น˜ ์ž‘์—…์˜ RankMonthlyProcessor์—์„œ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋ฏ€๋กœ, ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ score ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ฐ›์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •์‚ฌํ•ญ
 public static ProductRankMonthly of(String yearMonth, Long productId,
-                                    long viewCount, long likeCount, long orderCount) {
+                                    long viewCount, long likeCount, long orderCount, double score) {
     ProductRankMonthly e = new ProductRankMonthly();
     e.id = new ProductRankMonthlyId(yearMonth, productId);
     e.viewCount = viewCount;
     e.likeCount = likeCount;
     e.orderCount = orderCount;
+    e.score = score;
     e.createdAt = LocalDateTime.now();
     e.updatedAt = LocalDateTime.now();
     return e;
 }

๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ProductRankWeekly.java์—๋„ ์กด์žฌํ•˜๋ฏ€๋กœ ํ•จ๊ป˜ ์ˆ˜์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ 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 static ProductRankMonthly of(String yearMonth, Long productId,
long viewCount, long likeCount, long orderCount) {
ProductRankMonthly e = new ProductRankMonthly();
e.id = new ProductRankMonthlyId(yearMonth, productId);
e.viewCount = viewCount;
e.likeCount = likeCount;
e.orderCount = orderCount;
e.createdAt = LocalDateTime.now();
e.updatedAt = LocalDateTime.now();
return e;
}
public static ProductRankMonthly of(String yearMonth, Long productId,
long viewCount, long likeCount, long orderCount, double score) {
ProductRankMonthly e = new ProductRankMonthly();
e.id = new ProductRankMonthlyId(yearMonth, productId);
e.viewCount = viewCount;
e.likeCount = likeCount;
e.orderCount = orderCount;
e.score = score;
e.createdAt = LocalDateTime.now();
e.updatedAt = LocalDateTime.now();
return e;
}
๐Ÿค– Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java
around lines 39-49, the factory method of(...) fails to initialize the
non-nullable score field causing constraint violations; update the method
signature to accept a score parameter (e.g., double or appropriate numeric
type), set e.score = score before returning, and ensure callers (including
RankMonthlyProcessor batch code) pass the computed score; apply the identical
change to ProductRankWeekly to keep both factory methods consistent.

Comment on lines +51 to +56
public void overwrite(long viewCount, long likeCount, long orderCount) {
this.viewCount = viewCount;
this.likeCount = likeCount;
this.orderCount = orderCount;
this.updatedAt = LocalDateTime.now();
}
Copy link

Choose a reason for hiding this comment

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

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

์น˜๋ช…์ : score ํ•„๋“œ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

overwrite() ๋ฉ”์„œ๋“œ์—์„œ ์นด์šดํŠธ ํ•„๋“œ๋“ค์„ ์—…๋ฐ์ดํŠธํ•˜์ง€๋งŒ score ํ•„๋“œ๋Š” ์—…๋ฐ์ดํŠธํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์นด์šดํŠธ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์ ์ˆ˜๋„ ์žฌ๊ณ„์‚ฐ๋˜์–ด์•ผ ํ•˜๋ฏ€๋กœ, score ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •์‚ฌํ•ญ
-public void overwrite(long viewCount, long likeCount, long orderCount) {
+public void overwrite(long viewCount, long likeCount, long orderCount, double score) {
     this.viewCount = viewCount;
     this.likeCount = likeCount;
     this.orderCount = orderCount;
+    this.score = score;
     this.updatedAt = LocalDateTime.now();
 }

๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ProductRankWeekly.java์˜ overwrite() ๋ฉ”์„œ๋“œ์—๋„ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ 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 void overwrite(long viewCount, long likeCount, long orderCount) {
this.viewCount = viewCount;
this.likeCount = likeCount;
this.orderCount = orderCount;
this.updatedAt = LocalDateTime.now();
}
public void overwrite(long viewCount, long likeCount, long orderCount, double score) {
this.viewCount = viewCount;
this.likeCount = likeCount;
this.orderCount = orderCount;
this.score = score;
this.updatedAt = LocalDateTime.now();
}
๐Ÿค– Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java
around lines 51โ€“56, the overwrite(...) method updates viewCount, likeCount,
orderCount and updatedAt but does not update the score; change the method
signature to accept a score parameter (e.g., double or long matching the score
field type), assign this.score = score inside the method, and update updatedAt
as now; also search for and update all callsites to pass the recalculated score,
and apply the same change to ProductRankWeekly.java's overwrite(...) to keep
behavior consistent.

Comment on lines +39 to +49
public static ProductRankWeekly of(String yearWeek, Long productId,
long viewCount, long likeCount, long orderCount) {
ProductRankWeekly e = new ProductRankWeekly();
e.id = new ProductRankWeeklyId(yearWeek, productId);
e.viewCount = viewCount;
e.likeCount = likeCount;
e.orderCount = orderCount;
e.createdAt = LocalDateTime.now(ZoneId.systemDefault());
e.updatedAt = LocalDateTime.now(ZoneId.systemDefault());
return e;
}
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

score ํ•„๋“œ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

factory ๋ฉ”์„œ๋“œ์—์„œ viewCount, likeCount, orderCount๋Š” ์„ค์ •ํ•˜์ง€๋งŒ score ํ•„๋“œ๋Š” ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•„ ๊ธฐ๋ณธ๊ฐ’ 0.0์œผ๋กœ ๋‚จ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. PR ์š”์•ฝ์— ๋”ฐ๋ฅด๋ฉด score = 0.1view + 0.2like + 0.7*order๋กœ ๊ณ„์‚ฐ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž score ๊ณ„์‚ฐ ๋กœ์ง ์ถ”๊ฐ€ ์ œ์•ˆ
 public static ProductRankWeekly of(String yearWeek, Long productId,
                                    long viewCount, long likeCount, long orderCount) {
     ProductRankWeekly e = new ProductRankWeekly();
     e.id = new ProductRankWeeklyId(yearWeek, productId);
     e.viewCount = viewCount;
     e.likeCount = likeCount;
     e.orderCount = orderCount;
+    e.score = 0.1 * viewCount + 0.2 * likeCount + 0.7 * orderCount;
     e.createdAt = LocalDateTime.now(ZoneId.systemDefault());
     e.updatedAt = LocalDateTime.now(ZoneId.systemDefault());
     return e;
 }
๐Ÿค– Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java
around lines 39 to 49, the factory method does not set the score field so it
remains default 0.0; compute score using the formula score = 0.1 * viewCount +
0.2 * likeCount + 0.7 * orderCount (ensuring proper numeric types/casting to the
field type) and assign it to e.score before returning the instance.

Comment on lines +51 to +56
public void overwrite(long viewCount, long likeCount, long orderCount) {
this.viewCount = viewCount;
this.likeCount = likeCount;
this.orderCount = orderCount;
this.updatedAt = LocalDateTime.now();
}
Copy link

Choose a reason for hiding this comment

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

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

overwrite ๋ฉ”์„œ๋“œ์—์„œ score๊ฐ€ ๊ฐฑ์‹ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ฉ”ํŠธ๋ฆญ ์นด์šดํŠธ๊ฐ€ ์—…๋ฐ์ดํŠธ๋  ๋•Œ score๋„ ํ•จ๊ป˜ ์žฌ๊ณ„์‚ฐ๋˜์–ด์•ผ ํ•˜์ง€๋งŒ ํ˜„์žฌ๋Š” ์ด์ „ ๊ฐ’์„ ์œ ์ง€ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž score ์žฌ๊ณ„์‚ฐ ๋กœ์ง ์ถ”๊ฐ€ ์ œ์•ˆ
 public void overwrite(long viewCount, long likeCount, long orderCount) {
     this.viewCount = viewCount;
     this.likeCount = likeCount;
     this.orderCount = orderCount;
+    this.score = 0.1 * viewCount + 0.2 * likeCount + 0.7 * orderCount;
     this.updatedAt = LocalDateTime.now();
 }
๐Ÿ“ 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 void overwrite(long viewCount, long likeCount, long orderCount) {
this.viewCount = viewCount;
this.likeCount = likeCount;
this.orderCount = orderCount;
this.updatedAt = LocalDateTime.now();
}
public void overwrite(long viewCount, long likeCount, long orderCount) {
this.viewCount = viewCount;
this.likeCount = likeCount;
this.orderCount = orderCount;
this.score = 0.1 * viewCount + 0.2 * likeCount + 0.7 * orderCount;
this.updatedAt = LocalDateTime.now();
}
๐Ÿค– Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java
around lines 51-56, the overwrite method updates viewCount/likeCount/orderCount
and updatedAt but does not recompute score; modify the method to recalculate and
assign the score after updating the counts (e.g., call the existing score
calculation helper or compute score from the new view/like/order values and set
this.score = ...), then update updatedAtโ€”if no helper exists, implement the same
score formula used elsewhere and assign it here.

Comment on lines +53 to +70
qp.setFromClause("""
from (
select
CONCAT(
SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 1, 4),
'-W',
LPAD(SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 5, 2), 2, '0')
) as year_week,
pm.product_id as product_id,
SUM(CASE WHEN pm.metrics_type = 'VIEW' THEN pm.count ELSE 0 END) as view_count,
SUM(CASE WHEN pm.metrics_type = 'LIKE' THEN pm.count ELSE 0 END) as like_count,
SUM(CASE WHEN pm.metrics_type = 'ORDER_SUCCESS' THEN pm.count ELSE 0 END) as order_count
from product_metrics pm
group by year_week, pm.product_id
) t
""");

qp.setWhereClause("where t.year_week = :yearWeek");
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

์„ฑ๋Šฅ ์ด์Šˆ: WHERE ์ ˆ์„ ์„œ๋ธŒ์ฟผ๋ฆฌ ๋‚ด๋ถ€๋กœ ์ด๋™ ํ•„์š”

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

๐Ÿ”Ž ์„ฑ๋Šฅ ๊ฐœ์„ ์„ ์œ„ํ•œ ์ฟผ๋ฆฌ ์ˆ˜์ • ์ œ์•ˆ

yearWeek ํŒŒ๋ผ๋ฏธํ„ฐ ๋Œ€์‹  ํ•ด๋‹น ์ฃผ์˜ ์‹œ์ž‘์ผ/์ข…๋ฃŒ์ผ์„ ๊ณ„์‚ฐํ•˜์—ฌ ์„œ๋ธŒ์ฟผ๋ฆฌ ๋‚ด๋ถ€์—์„œ ํ•„ํ„ฐ๋งํ•˜๊ฑฐ๋‚˜, ์•„๋ž˜์™€ ๊ฐ™์ด ์„œ๋ธŒ์ฟผ๋ฆฌ ๋‚ด๋ถ€์— WHERE ์ ˆ ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”:

         qp.setFromClause("""
             from (
               select
                 CONCAT(
                   SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 1, 4),
                   '-W',
                   LPAD(SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 5, 2), 2, '0')
                 ) as year_week,
                 pm.product_id as product_id,
                 SUM(CASE WHEN pm.metrics_type = 'VIEW' THEN pm.count ELSE 0 END) as view_count,
                 SUM(CASE WHEN pm.metrics_type = 'LIKE' THEN pm.count ELSE 0 END) as like_count,
                 SUM(CASE WHEN pm.metrics_type = 'ORDER_SUCCESS' THEN pm.count ELSE 0 END) as order_count
               from product_metrics pm
+              where CONCAT(
+                SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 1, 4),
+                '-W',
+                LPAD(SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 5, 2), 2, '0')
+              ) = :yearWeek
               group by year_week, pm.product_id
             ) t
             """);
-
-        qp.setWhereClause("where t.year_week = :yearWeek");

๋˜๋Š” ์›”๊ฐ„ ๋ฆฌ๋”์ฒ˜๋Ÿผ startDate, endDate ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด ๋” ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyReaderConfig.java
lines 53-70: the current query groups the entire product_metrics table then
filters by year_week in the outer WHERE, causing large unnecessary aggregation;
move the filtering into the subquery so aggregation only runs on the target
periodโ€”either add a WHERE inside the subquery that filters metrics_date
(recommended: switch the reader to accept startDate and endDate parameters and
filter metrics_date BETWEEN :startDate AND :endDate inside the subquery) or, if
keeping yearWeek param, convert it to a date range inside Java and apply
STR_TO_DATE/metrics_date comparison inside the subquery; update setFromClause to
include that WHERE and remove or adjust the outer setWhereClause accordingly.

Comment on lines +19 to +30
String sql = """
INSERT INTO mv_product_rank_weekly
(year_week_key, product_id, view_count, like_count, order_count, score, created_at, updated_at)
VALUES
(:yearWeek, :productId, :viewCount, :likeCount, :orderCount, :score, NOW(6), NOW(6))
ON DUPLICATE KEY UPDATE
view_count = VALUES(view_count),
like_count = VALUES(like_count),
order_count = VALUES(order_count),
score=VALUES(score),
updated_at = NOW(6)
""";
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

๐ŸŒ Web query:

MySQL 8.0.20 VALUES function deprecated ON DUPLICATE KEY UPDATE

๐Ÿ’ก Result:

Yes โ€” starting in MySQL 8.0.20 the VALUES() function used inside INSERT ... ON DUPLICATE KEY UPDATE is deprecated and will be removed in a future release. Use a row (and optional column) alias for the VALUES() row instead.

  • Deprecation announced in MySQL 8.0.20 release notes (Apr 27, 2020). [1]
  • Reference manual: use VALUES(...) AS alias (or AS alias(col1,col2,...)) and then refer to alias.col in the UPDATE clause. Examples also show using a derived-subquery or SET form to avoid warnings. [2]

Example replacements

  • Old (deprecated):
    INSERT INTO t1 (a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c = VALUES(a)+VALUES(b);
  • New (use row alias):
    INSERT INTO t1 (a,b,c) VALUES (1,2,3) AS new ON DUPLICATE KEY UPDATE c = new.a + new.b;
  • Or with column aliases:
    INSERT INTO t1 (a,b,c) VALUES (1,2,3) AS new(m,n,p) ON DUPLICATE KEY UPDATE c = m + n. [2]

Sources:
[1] MySQL 8.0.20 release notes (Changes in MySQL 8.0.20).
[2] MySQL Reference Manual โ€” INSERT ... ON DUPLICATE KEY UPDATE.


MySQL 8.0.20+ ์ด์ƒ์—์„œ VALUES() ํ•จ์ˆ˜๊ฐ€ deprecated๋˜์—ˆ์œผ๋ฏ€๋กœ ์ƒˆ๋กœ์šด ๋ฌธ๋ฒ•์œผ๋กœ ์ˆ˜์ •ํ•˜์„ธ์š”.

๋ผ์ธ 25-28์—์„œ ์‚ฌ์šฉ ์ค‘์ธ ON DUPLICATE KEY UPDATE์˜ VALUES() ํ•จ์ˆ˜๋Š” MySQL 8.0.20๋ถ€ํ„ฐ deprecated๋˜์—ˆ์œผ๋ฉฐ ํ–ฅํ›„ ๋ฒ„์ „์—์„œ ์ œ๊ฑฐ๋  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. ํ–‰ ๋ณ„์นญ(row alias)์„ ์‚ฌ์šฉํ•˜๋Š” ์ƒˆ๋กœ์šด ๋ฌธ๋ฒ•์œผ๋กœ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ๋ฐฉ๋ฒ•

VALUES ์ ˆ ์งํ›„์— AS new_values ๋ณ„์นญ์„ ์ถ”๊ฐ€ํ•˜๊ณ , UPDATE ์ ˆ์—์„œ VALUES(column_name) ๋Œ€์‹  new_values.column_name ํ˜•์‹์œผ๋กœ ์ฐธ์กฐํ•˜์„ธ์š”:

         String sql = """
             INSERT INTO mv_product_rank_weekly
               (year_week_key, product_id, view_count, like_count, order_count, score, created_at, updated_at)
             VALUES
               (:yearWeek, :productId, :viewCount, :likeCount, :orderCount, :score, NOW(6), NOW(6))
+            AS new_values
             ON DUPLICATE KEY UPDATE
-              view_count = VALUES(view_count),
-              like_count = VALUES(like_count),
-              order_count = VALUES(order_count),
-              score=VALUES(score),
+              view_count = new_values.view_count,
+              like_count = new_values.like_count,
+              order_count = new_values.order_count,
+              score = new_values.score,
               updated_at = NOW(6)
             """;
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String sql = """
INSERT INTO mv_product_rank_weekly
(year_week_key, product_id, view_count, like_count, order_count, score, created_at, updated_at)
VALUES
(:yearWeek, :productId, :viewCount, :likeCount, :orderCount, :score, NOW(6), NOW(6))
ON DUPLICATE KEY UPDATE
view_count = VALUES(view_count),
like_count = VALUES(like_count),
order_count = VALUES(order_count),
score=VALUES(score),
updated_at = NOW(6)
""";
String sql = """
INSERT INTO mv_product_rank_weekly
(year_week_key, product_id, view_count, like_count, order_count, score, created_at, updated_at)
VALUES
(:yearWeek, :productId, :viewCount, :likeCount, :orderCount, :score, NOW(6), NOW(6))
AS new_values
ON DUPLICATE KEY UPDATE
view_count = new_values.view_count,
like_count = new_values.like_count,
order_count = new_values.order_count,
score = new_values.score,
updated_at = NOW(6)
""";
๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyWriterConfig.java
around lines 19 to 30, the SQL uses the deprecated VALUES() function in the ON
DUPLICATE KEY UPDATE clause; change the INSERT so the VALUES(...) row is given
an alias (e.g., AS new_values) immediately after the VALUES (...) list and then
replace each VALUES(column_name) in the UPDATE clause with
new_values.column_name for view_count, like_count, order_count and score,
keeping updated_at = NOW(6) as-is.

Comment on lines +9 to +12
@Slf4j
@RequiredArgsConstructor
@Component
public class ChunkListener {
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

Spring Batch 6.x ChunkListener ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ ํ•„์š”

Spring Batch 6.x์—์„œ๋Š” org.springframework.batch.core.listener.ChunkListener ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
+import org.springframework.batch.core.listener.ChunkListener;
+
 @Slf4j
 @RequiredArgsConstructor
 @Component
-public class ChunkListener {
+public class ChunkListener implements org.springframework.batch.core.listener.ChunkListener {

Based on learnings, Spring Batch 6.x requires implementing the ChunkListener interface.

๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Slf4j
@RequiredArgsConstructor
@Component
public class ChunkListener {
@Slf4j
@RequiredArgsConstructor
@Component
public class ChunkListener implements org.springframework.batch.core.listener.ChunkListener {
๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java
around lines 9 to 12, the class is annotated but does not implement the Spring
Batch 6.x ChunkListener interface; update the class declaration to implement
org.springframework.batch.core.listener.ChunkListener and add the required
@Override methods (beforeChunk, afterChunk, and afterChunkError) with
appropriate signatures and simple no-op or logging bodies so the class satisfies
the interface; ensure proper imports and keep existing annotations (@Slf4j,
@RequiredArgsConstructor, @Component).

Comment on lines +14 to +20
@AfterChunk
void afterChunk(ChunkContext chunkContext) {
log.info(
"์ฒญํฌ ์ข…๋ฃŒ: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " +
"writeCount: ${chunkContext.stepContext.stepExecution.writeCount}"
);
}
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

๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์™€ ๋ฌธ์ž์—ด ๋ณด๊ฐ„ ๊ตฌ๋ฌธ ์ˆ˜์ • ํ•„์š”

๋‹ค์Œ ๋‘ ๊ฐ€์ง€ critical ์ด์Šˆ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. Spring Batch 6.x API ๋ณ€๊ฒฝ: ChunkContext ๋Œ€์‹  Chunk<?> ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  2. Java ๋ฌธ์ž์—ด ๋ณด๊ฐ„ ๊ตฌ๋ฌธ ์˜ค๋ฅ˜: ${...} ๊ตฌ๋ฌธ์€ Kotlin ๋ฌธ๋ฒ•์ด๋ฉฐ Java์—์„œ๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. Java์—์„œ๋Š” ๋ฌธ์ž์—ด ์—ฐ๊ฒฐ ๋˜๋Š” String.format์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
+import org.springframework.batch.core.scope.context.ChunkContext;
+import org.springframework.batch.item.Chunk;
+
 @Slf4j
 @RequiredArgsConstructor
 @Component
-public class ChunkListener {
+public class ChunkListener implements org.springframework.batch.core.listener.ChunkListener {

     @AfterChunk
-    void afterChunk(ChunkContext chunkContext) {
+    public void afterChunk(ChunkContext context, Chunk<?> chunk) {
         log.info(
-            "์ฒญํฌ ์ข…๋ฃŒ: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " +
-                    "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}"
+            "์ฒญํฌ ์ข…๋ฃŒ: readCount: {}, writeCount: {}",
+            context.getStepContext().getStepExecution().getReadCount(),
+            context.getStepContext().getStepExecution().getWriteCount()
         );
     }
 }

Based on learnings, Spring Batch 6.x changed ChunkListener API to use Chunk parameters with signature: afterChunk(ChunkContext context, Chunk<T> chunk).

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

๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java
around lines 14-20, the method uses the old Spring Batch signature and Kotlin
string interpolation; change the method signature to accept the current
parameters (e.g. afterChunk(ChunkContext chunkContext, Chunk<?> chunk) or the
exact Spring Batch 6.x signature used in the project) and replace the
Kotlin-style "${...}" with Java getters and string construction โ€” use
chunkContext.getStepContext().getStepExecution().getReadCount() and
getWriteCount() and build the log message with String.format(...) or string
concatenation so the log prints the readCount and writeCount correctly.

Comment on lines +19 to +23
@BeforeJob
void beforeJob(JobExecution jobExecution) {
log.info("Job '${jobExecution.jobInstance.jobName}' ์‹œ์ž‘");
jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis());
}
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

Java ๋ฌธ์ž์—ด ๋ณด๊ฐ„ ๊ตฌ๋ฌธ ์˜ค๋ฅ˜๋ฅผ ์ˆ˜์ •ํ•˜์„ธ์š”.

Line 21์—์„œ "${jobExecution.jobInstance.jobName}"๋Š” Java์—์„œ ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฌธ์ž์—ด ๋ณด๊ฐ„ ๊ตฌ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด ์ฝ”๋“œ๋Š” ์‹ค์ œ Job ์ด๋ฆ„ ๋Œ€์‹  ๋ฆฌํ„ฐ๋Ÿด ๋ฌธ์ž์—ด์„ ๋กœ๊ทธ์— ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
     @BeforeJob
     void beforeJob(JobExecution jobExecution) {
-        log.info("Job '${jobExecution.jobInstance.jobName}' ์‹œ์ž‘");
+        log.info("Job '{}' ์‹œ์ž‘", jobExecution.getJobInstance().getJobName());
         jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis());
     }
๐Ÿ“ 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
@BeforeJob
void beforeJob(JobExecution jobExecution) {
log.info("Job '${jobExecution.jobInstance.jobName}' ์‹œ์ž‘");
jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis());
}
@BeforeJob
void beforeJob(JobExecution jobExecution) {
log.info("Job '{}' ์‹œ์ž‘", jobExecution.getJobInstance().getJobName());
jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis());
}
๐Ÿค– Prompt for AI Agents
In apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
around lines 19 to 23, the log call uses a Java-invalid string interpolation
("${jobExecution.jobInstance.jobName}") which prints the literal rather than the
job name; replace it with a proper Java expressionโ€”either use the SLF4J
parameterized logging placeholder and pass
jobExecution.getJobInstance().getJobName() as an argument, or build the string
with + (or String.format) using jobExecution.getJobInstance().getJobName();
ensure you call the correct getter methods to obtain the job name.

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.

2 participants