Skip to content

Conversation

@HongChangMo
Copy link
Collaborator

@HongChangMo HongChangMo commented Jan 2, 2026

๐Ÿ“Œ Summary

  • Spring Batch + MV ๊ธฐ๋ฐ˜ ์›”๊ฐ„, ์ฃผ๊ฐ„ ์ƒํ’ˆ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
  • Chunk Oriented Processing ๊ธฐ๋ฐ˜ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
  • ์ผ๊ฐ„, ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น ์ œ๊ณต API ๊ตฌํ˜„

๐Ÿ’ฌ Review Points

์ปค์Šคํ…€ ItemReader๊ฐ€ ์•„๋‹Œ RepositoryItemReader๋ฅผ ์‚ฌ์šฉ

์ฒ˜์Œ ๊ตฌํ˜„ํ• ๋•Œ, ์ปค์Šคํ…€ ItemReader๋ฅผ ํ†ตํ•ด์„œ ๋งŒ์•ฝ ์ข‹์•„์š” ๋ฐ์ดํ„ฐ๊ฐ€ 100๋งŒ ๊ฑด์ด๋ผ๋ฉด,
100๋งŒ๊ฑด์„ ๋ชจ๋‘ ์ฝ์–ด์™€ ์ฒ˜๋ฆฌ๋ฅผ ํ•œ๋‹ค๋ฉด ๋ฐ์ดํ„ฐ ์–‘์ด ๋” ๋งŽ์•„์ง€๋ฉด OOM ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์•„,
Spring Batch ์—์„œ ์ง€์›ํ•˜๋Š” RepositoryItemReader๋ฅผ ์‚ฌ์šฉํ•ด์„œ chunk size์™€ ๋™์ผํ•œ
ํŽ˜์ด์ง• ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
๊ถ๊ธˆํ•œ ์ ์€ RepositoryItemReader ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์‹ค๋ฌด์—์„œ ๋งŽ์€์ง€ ๊ถ๊ธˆํ•˜๊ณ ,
์ •๋‹ต์€ ์—†๋‹ค๊ณ  ํ•˜์…จ์ง€๋งŒ, ๊ตฌํ˜„ํ•œ ๋ฐฉ์‹์ด ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์„ฑ์ด ๋งž๋Š”์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค.


MV๋ฅผ ํ™œ์šฉํ•œ ์ฃผ๊ฐ„ ์›”๊ฐ„ ๋žญํ‚น ๊ฐ€์ค‘์น˜ ์ ์šฉ ์งˆ๋ฌธ

Redis์˜ ZSET์œผ๋กœ ์ผ๊ฐ„ ๋žญํ‚น์„ ๊ตฌํ˜„ํ• ๋•Œ์—๋Š” ZSET score ๊ธฐ๋ฐ˜ top ๋žญํ‚น์„ ๋ฝ‘๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์—ˆ๋Š”๋ฐ, ์ด๋ฒˆ ๊ณผ์ œ๋Š” Spring Batch + MV ๋ฅผ ํ™œ์šฉํ•ด ์ฃผ๊ฐ„, ์›”๊ฐ„ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ , commerce-api์—์„œ top 100 ๋žญํ‚น์„ ์กฐํšŒํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๋Š”๋ฐ,

์ œ๊ฐ€ ๊ถ๊ธˆํ•œ ์ ์€ MV์— ์ €์žฅ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ์ง‘๊ณ„์— ๋Œ€ํ•œ ๋‚ด์šฉ๋ฟ์ด๋ผ, top N ์„ ๋ฝ‘์•„์•ผํ•˜๋Š” ๊ฒฝ์šฐ ๋ณ„๋„์˜ score ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”๊ฒŒ ์ข‹์„์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค.

์ €๋Š” ์ด๋ฒˆ ๊ณผ์ œ์—์„œ api๋ฅผ ์กฐํšŒํ•˜๋Š” commerce-api ์— ์ฝ๊ธฐ ์ „์šฉ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋งŒ๋“ค๊ณ  MV์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ db์—์„œ ์กฐํšŒํ•  ๋•Œ score๋ฅผ ๊ณ„์‚ฐํ•ด์„œ ์กฐํšŒํ•˜๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ œ๊ฐ€ ์ƒ๊ฐํ•œ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” ๋‚ด๋ถ€์— score๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๋„๋ฉ”์ธ ๋กœ์ง์„ ํ†ตํ•ด ์ข…ํ•ฉ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋„๋ก ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์„ ๊ฒƒ ๊ฐ™์•„๋ณด์ด๋Š”๋ฐ, commerce-collector ์ง‘๊ณ„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„์—์„œ ๋ฏธ๋ฆฌ score๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์ด ๋” ์ข‹์€ ๋ฐฉ๋ฒ•์ธ์ง€, ์•„๋‹ˆ๋ฉด ์ œ๊ฐ€ ๊ตฌํ˜„ํ•œ ๋ถ€๋ถ„๋„ ๊ดœ์ฐฎ์€ ๋ฐฉ๋ฒ•์ธ์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค.

์Šค์ฝ”์–ด ๊ณ„์‚ฐ์„ repository์—์„œ ์ฒ˜๋ฆฌ

@Query("""
        SELECT w FROM ProductMetricsWeekly w
        WHERE w.year = :year AND w.week = :week
        ORDER BY (w.totalLikeCount * 0.2 + w.totalViewCount * 0.1 + w.totalOrderCount * 0.6) DESC
        """)
    List<ProductMetricsWeekly> findByYearAndWeekOrderByCompositeScoreDesc(
            @Param("year") int year,
            @Param("week") int week,
            Pageable pageable
    );

๋„๋ฉ”์ธ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ

/**
     * ์ข…ํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ (๊ฐ€์ค‘์น˜ ์ ์šฉ)
     * Score = (like * 0.2) + (view * 0.1) + (order * 0.6)
     */
    public double calculateCompositeScore() {
        return (totalLikeCount * 0.2) + (totalViewCount * 0.1) + (totalOrderCount * 0.6);
    }

โœ… Checklist

๐Ÿงฑ Spring Batch

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

๐Ÿงฉ Ranking API

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

Summary by CodeRabbit

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

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

    • ๊ธฐ๊ฐ„๋ณ„ ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ ์ถ”๊ฐ€ (์ผ๊ฐ„, ์ฃผ๊ฐ„, ์›”๊ฐ„)
    • ์—ฌ๋Ÿฌ ์ˆœ์œ„ ๊ธฐ์ค€ ์ง€์›: ์ข‹์•„์š”, ์กฐํšŒ์ˆ˜, ์ฃผ๋ฌธ ๊ธฐ๋ฐ˜ ์ˆœ์œ„ ๋ฐ ๋ณตํ•ฉ ์ ์ˆ˜
    • ์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ์ƒํ’ˆ ์ง€ํ‘œ ์ถ”์  ๊ธฐ๋Šฅ ๊ตฌํ˜„
    • ํŽ˜์ด์ง• ์ง€์›์œผ๋กœ ๋Œ€๊ทœ๋ชจ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐœ์„ 
  • ํ…Œ์ŠคํŠธ

    • ์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ์ง€ํ‘œ ์ง‘๊ณ„ ๋ฐฐ์น˜ ์ž‘์—… ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€

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

- Chunk Oriented Processing ๊ธฐ๋ฐ˜ ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
- ์ƒํ’ˆ ์ง‘๊ณ„ ๊ฒฐ๊ณผ MV ํ™œ์šฉ
- Chunk Oriented Processing ๊ธฐ๋ฐ˜ ์›”๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
- ์ƒํ’ˆ ์ง‘๊ณ„ ๊ฒฐ๊ณผ MV ํ™œ์šฉ
- ์˜ค๋ž˜๋œ ์ผ์ž๋ณ„ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ 10์ผ ๊ธฐ์ค€ -> 30์ผ ์ˆ˜์ •
- PeriodType๋ณ„ ์ผ๊ฐ„, ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น ์ œ๊ณต
- ์ผ๊ฐ„์€ Redis ZSET ์œผ๋กœ ์ œ๊ณต
- ์ฃผ๊ฐ„, ์›”๊ฐ„ MV๋ฅผ ํ™œ์šฉํ•œ ๋žญํ‚น ์ œ๊ณต
@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

์›Œํฌ์Šค๋ฃจ

์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ์ƒํ’ˆ ์ˆœ์œ„ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด PeriodType ์—ด๊ฑฐํ˜•๊ณผ PeriodRankingService๋ฅผ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฃผ๊ฐ„/์›”๊ฐ„ ์ œํ’ˆ ์ง€ํ‘œ๋ฅผ ์œ„ํ•œ ์ƒˆ๋กœ์šด JPA ์—”ํ‹ฐํ‹ฐ, ์ €์žฅ์†Œ ๋ฐ ๋ฐฐ์น˜ ํ”„๋กœ์„ธ์„œ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ , RankingFacade ๋ฐ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ๊ธฐ๊ฐ„ ์œ ํ˜• ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋ฉฐ, ๋งค์›” ์ง€ํ‘œ ์ •๋ฆฌ ๊ธฐ๊ฐ„์„ 10์ผ์—์„œ 30์ผ๋กœ ์—ฐ์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ณ€๊ฒฝ ์‚ฌํ•ญ

์ฝ”ํ˜ธํŠธ / ํŒŒ์ผ ๋ณ€๊ฒฝ ์š”์•ฝ
๋„๋ฉ”์ธ ๋ชจ๋ธ - ์ˆœ์œ„
apps/commerce-api/src/main/java/com/loopers/domain/ranking/*
PeriodType ์—ด๊ฑฐํ˜• ์ถ”๊ฐ€ (DAILY, WEEKLY, MONTHLY); Ranking ์—”ํ‹ฐํ‹ฐ์— totalLikeCount, totalViewCount, totalOrderCoun ํ•„๋“œ ๋ฐ ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ of() ์ถ”๊ฐ€
์ฃผ๊ฐ„/์›”๊ฐ„ ์ง€ํ‘œ ์—”ํ‹ฐํ‹ฐ ๋ฐ ์ €์žฅ์†Œ (commerce-api)
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java, ProductMetricsMonthly.java
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetrics*Repository.java
์ฝ๊ธฐ ์ „์šฉ JPA ์—”ํ‹ฐํ‹ฐ ProductMetricsWeekly, ProductMetricsMonthly ์ถ”๊ฐ€; ๋ณตํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฉ”์„œ๋“œ ํฌํ•จ; ๊ฐ๊ฐ์— ๋Œ€ํ•œ ๋„๋ฉ”์ธ ์ €์žฅ์†Œ ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ JPA ์ €์žฅ์†Œ ์ถ”๊ฐ€
์ฃผ๊ฐ„/์›”๊ฐ„ ์ˆœ์œ„ ์„œ๋น„์Šค
apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodRankingService.java
์ƒˆ๋กœ์šด ์„œ๋น„์Šค ์ถ”๊ฐ€: getTopWeeklyRanking(), getTopMonthlyRanking(), getWeeklyRankingWithPaging(), getMonthlyRankingWithPaging() ๋ฉ”์„œ๋“œ ๊ตฌํ˜„
RankingFacade ๋ฐ ์ปจํŠธ๋กค๋Ÿฌ
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java
RankingFacade์— PeriodRankingService ์ฃผ์ž…; getTopRanking(), getRankingWithPaging() ๋ฉ”์„œ๋“œ์— PeriodType ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€; DTO์— periodType, product ํ•„๋“œ ์ถ”๊ฐ€
๋ฐฐ์น˜ ์˜์กด์„ฑ
apps/commerce-collector/build.gradle.kts
Spring Batch ๊ด€๋ จ ์˜์กด์„ฑ ์ถ”๊ฐ€: spring-boot-starter-batch, spring-batch-test
๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ
apps/commerce-collector/src/main/java/com/loopers/batch/listener/*
์„ธ ๊ฐœ์˜ ๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€: JobListener (์ž‘์—… ์‹œ์ž‘/์ข…๋ฃŒ ๋กœ๊น…), StepMonitorListener (์Šคํ… ์‹คํŒจ ๊ฐ์ง€), ChunkListener (์ฒญํฌ ๋‹จ์œ„ ํ†ต๊ณ„ ๋กœ๊น…)
์ฃผ๊ฐ„/์›”๊ฐ„ ์ง€ํ‘œ ์—”ํ‹ฐํ‹ฐ ๋ฐ ์ €์žฅ์†Œ (commerce-collector)
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java, ProductMetricsMonthly.java
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetrics*Repository.java
์ฝ๊ธฐ/์“ฐ๊ธฐ JPA ์—”ํ‹ฐํ‹ฐ ์ถ”๊ฐ€; ์ •์  ํŒฉํ† ๋ฆฌ create(), updateMetrics(), reset() ๋ฉ”์„œ๋“œ ํฌํ•จ; ์ €์žฅ, ์‚ญ์ œ ์ž‘์—…์„ ์œ„ํ•œ ์ €์žฅ์†Œ ์ธํ„ฐํŽ˜์ด์Šค ์ถ”๊ฐ€
์ง‘๊ณ„ DTO
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/WeeklyAggregationDto.java, MonthlyAggregationDto.java
์ฃผ๊ฐ„/์›”๊ฐ„ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ์œ„ํ•œ DTO ํด๋ž˜์Šค ์ถ”๊ฐ€
๋ฐฐ์น˜ ํ”„๋กœ์„ธ์„œ ๋ฐ ๋ผ์ดํ„ฐ
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsProcessor.java, MonthlyMetricsProcessor.java
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsWriter.java, MonthlyMetricsWriter.java
์ฃผ๊ฐ„/์›”๊ฐ„ ์ง€ํ‘œ ๋ณ€ํ™˜ ๋ฐ ์ €์žฅ์„ ์œ„ํ•œ ๋ฐฐ์น˜ ํ”„๋กœ์„ธ์„œ ๋ฐ ๋ผ์ดํ„ฐ ์ถ”๊ฐ€
๋ฐฐ์น˜ ์ž‘์—… ์„ค์ •
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobConfig.java, ProductMetricsMonthlyJobConfig.java
๋ฆฌ๋”-ํ”„๋กœ์„ธ์„œ-๋ผ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ, ์ฒญํฌ ๊ธฐ๋ฐ˜ ์ฒ˜๋ฆฌ (ํฌ๊ธฐ 100), ๋ฆฌ์Šค๋„ˆ ์—ฐ๊ฒฐ์„ ํฌํ•จํ•œ ๋ฐฐ์น˜ ์ž‘์—… ์„ค์ •
์ €์žฅ์†Œ ๊ตฌํ˜„
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetrics*RepositoryImpl.java
JPA ์ €์žฅ์†Œ์— ์œ„์ž„ํ•˜๊ณ  JDBC ๊ธฐ๋ฐ˜ ๋ฒŒํฌ UPSERT (MySQL ON DUPLICATE KEY UPDATE) ๊ตฌํ˜„
JPA ์ €์žฅ์†Œ ํ™•์žฅ
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyJpaRepository.java
์ฃผ๊ฐ„/์›”๊ฐ„ ์ง‘๊ณ„ ์กฐํšŒ ๋ฉ”์„œ๋“œ findWeeklyAggregation(), findMonthlyAggregation() ์ถ”๊ฐ€
๋ฐฐ์น˜ ํ…Œ์ŠคํŠธ
apps/commerce-collector/src/test/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobTest.java, ProductMetricsMonthlyJobTest.java
์„ฑ๊ณต ๊ฒฝ๋กœ, ๋ฐ์ดํ„ฐ ์—†์Œ ์‹œ๋‚˜๋ฆฌ์˜ค, UPSERT ๋™์ž‘, ํƒ€๊ฒŸํŒ…๋œ ์Šคํ… ์‹คํ–‰์— ๋Œ€ํ•œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
์Šค์ผ€์ค„๋Ÿฌ ์œ ์ง€๋ณด์ˆ˜
apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingScheduler.java
์ผ์ผ ๋ฉ”ํŠธ๋ฆญ ์ •๋ฆฌ ์œˆ๋„์šฐ๋ฅผ 10์ผ์—์„œ 30์ผ๋กœ ํ™•์žฅ

์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ

sequenceDiagram
    participant Client
    participant RankingController as RankingV1<br/>Controller
    participant RankingFacade as RankingFacade
    participant PeriodService as PeriodRanking<br/>Service
    participant WeeklyRepo as Weekly<br/>Repository
    participant MonthlyRepo as Monthly<br/>Repository
    participant Enrichment as Enrichment<br/>Logic

    Client->>RankingController: GET /ranking<br/>(rankingType, periodType,<br/>date, limit)
    RankingController->>RankingFacade: getTopRanking(type,<br/>periodType, date, limit)
    
    alt periodType == WEEKLY
        RankingFacade->>PeriodService: getTopWeeklyRanking(type,<br/>date, limit)
        PeriodService->>WeeklyRepo: findByYearAndWeek<br/>OrderBy[Type]Desc(year,<br/>week, limit)
        WeeklyRepo-->>PeriodService: List<ProductMetricsWeekly>
        PeriodService->>PeriodService: convertWeeklyToRanking()<br/>calculateScore()
        PeriodService-->>RankingFacade: List<Ranking>
    else periodType == MONTHLY
        RankingFacade->>PeriodService: getTopMonthlyRanking(type,<br/>date, limit)
        PeriodService->>MonthlyRepo: findByYearAndMonth<br/>OrderBy[Type]Desc(year,<br/>month, limit)
        MonthlyRepo-->>PeriodService: List<ProductMetricsMonthly>
        PeriodService->>PeriodService: convertMonthlyToRanking()<br/>calculateScore()
        PeriodService-->>RankingFacade: List<Ranking>
    else periodType == DAILY (default)
        RankingFacade->>RankingFacade: getTopRanking(type,<br/>date, limit)<br/>๊ธฐ์กด ๋กœ์ง
    end
    
    RankingFacade->>Enrichment: enrichWithProductInfo(rankings)
    Enrichment-->>RankingFacade: List<RankingInfo>
    RankingFacade-->>RankingController: List<RankingInfo>
    RankingController-->>Client: JSON Response
Loading

์˜ˆ์ƒ ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋…ธ๋ ฅ

๐ŸŽฏ 4 (๋ณต์žกํ•จ) | โฑ๏ธ ~60๋ถ„

๊ด€๋ จ ๊ฐ€๋Šฅ์„ฑ ์žˆ๋Š” PR

  • [volume-9] Product Ranking with Redisย #216: ๋™์ผํ•œ RankingFacade, DTO, ์ˆœ์œ„ ๋„๋ฉ”์ธ ํƒ€์ž… ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜๋ฉฐ, ์ˆœ์œ„ ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ์ฝ”๋“œ ์ˆ˜์ค€์˜ ์—ฐ๊ด€์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค (์ฃผ์š” PR์€ ๊ธฐ๊ฐ„ ๊ธฐ๋ฐ˜ ์ˆœ์œ„ ๋ฐ PeriodRankingService ์ถ”๊ฐ€, ๊ฒ€์ƒ‰๋œ PR์€ Redis ๊ธฐ๋ฐ˜ ์ˆœ์œ„/์บ์‹œ ๋ฐ RankingFacade ์—ฐ๊ฒฐ ๊ตฌํ˜„).

์ œ์•ˆ๋œ ๋ ˆ์ด๋ธ”

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 37.60% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check โ“ Inconclusive ์ œ๋ชฉ '[volume-10] Collect, Stack, Zip'์€ ๋ชจํ˜ธํ•˜๊ณ  ๊ตฌ์ฒด์ ์ธ ์ •๋ณด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ ์‚ฌํ•ญ์˜ ํ•ต์‹ฌ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ „๋‹ฌํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ์ œ๋ชฉ์„ ๋” ๊ตฌ์ฒด์ ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์„ธ์š”. ์˜ˆ: '[batch] Implement Spring Batch weekly/monthly ranking aggregation with materialized views' ๋˜๋Š” '[ranking] Add period-based ranking support for daily/weekly/monthly'
โœ… Passed checks (1 passed)
Check name Status Explanation
Description check โœ… Passed PR ์„ค๋ช…์€ ์ œ์‹œ๋œ ํ…œํ”Œ๋ฆฟ์˜ ์ฃผ์š” ์„น์…˜(Summary, Review Points, Checklist)์„ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๊ตฌํ˜„ ๋‚ด์šฉ๊ณผ ๊ฒ€ํ†  ํฌ์ธํŠธ๊ฐ€ ๋ช…ํ™•ํžˆ ๊ธฐ์ˆ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
โœจ Finishing touches
  • ๐Ÿ“ Generate docstrings

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

โค๏ธ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

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/interfaces/api/ranking/RankingV1Controller.java (2)

37-37: getTotalRankingCount์— periodType ์ „๋‹ฌ ๋ˆ„๋ฝ

getRankingWithPaging์—๋Š” periodType์„ ์ „๋‹ฌํ•˜์ง€๋งŒ, getTotalRankingCount์—๋Š” ์ „๋‹ฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น์˜ ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ๋‹ค๋ฅด๋ฏ€๋กœ(์ผ๊ฐ„: Redis, ์ฃผ๊ฐ„/์›”๊ฐ„: MV), ์ด ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ๋„ periodType์„ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋กœ ์ธํ•ด ํŽ˜์ด์ง€๋„ค์ด์…˜์˜ ์ด ํŽ˜์ด์ง€ ์ˆ˜๊ฐ€ ์ž˜๋ชป ๊ณ„์‚ฐ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
-        long totalCount = rankingFacade.getTotalRankingCount(request.type(), date);
+        long totalCount = rankingFacade.getTotalRankingCount(request.type(), request.periodType(), date);

63-63: ๋™์ผ ์ด์Šˆ: getTopRanking์—์„œ๋„ periodType ๋ˆ„๋ฝ

getTopRanking ์—”๋“œํฌ์ธํŠธ์—์„œ๋„ getTotalRankingCount ํ˜ธ์ถœ ์‹œ periodType์„ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
-        long totalCount = rankingFacade.getTotalRankingCount(request.type(), date);
+        long totalCount = rankingFacade.getTotalRankingCount(request.type(), request.periodType(), date);
โ™ป๏ธ Duplicate comments (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)

38-65: ๋™์ผํ•œ null ์ฒ˜๋ฆฌ ๊ฒ€์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

GetTopRankingRequest์™€ ๋™์ผํ•˜๊ฒŒ periodType ํ•„๋“œ์˜ null ์ฒ˜๋ฆฌ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

๐Ÿงน Nitpick comments (16)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java (1)

32-47: ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ๊ฐœ์„  ์ œ์•ˆ

  • ํŒŒ๋ผ๋ฏธํ„ฐ๋ช… i๋ฅผ rank๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๋†’์ด์„ธ์š”.
  • Line 40์˜ new Ranking ์‚ฌ์ด ์ด์ค‘ ๊ณต๋ฐฑ์„ ์ˆ˜์ •ํ•˜์„ธ์š”.
๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
     public static Ranking of(
-            int i,
+            int rank,
             Long productId,
             double score,
             Long totalLikeCount,
             Long totalViewCount,
             Long totalOrderCount
     ) {
-        return new  Ranking(
-                i,
+        return new Ranking(
+                rank,
                 productId,
                 score,
                 totalLikeCount,
                 totalViewCount,
                 totalOrderCount
         );
     }
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java (1)

164-188: FQN ๋Œ€์‹  import ๋ฌธ ์‚ฌ์šฉ ๊ถŒ์žฅ

org.springframework.data.domain.Page, org.springframework.data.domain.Pageable, ๊ทธ๋ฆฌ๊ณ  DTO ํด๋ž˜์Šค๋“ค์— ๋Œ€ํ•ด ์™„์ „ํ•œ ํŒจํ‚ค์ง€๋ช…(FQN)์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด ํŒŒ์ผ ์ƒ๋‹จ์— import ๋ฌธ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

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

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

import com.loopers.domain.metrics.dto.MonthlyAggregationDto;
import com.loopers.domain.metrics.dto.WeeklyAggregationDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

๊ทธ ํ›„ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๊ฐ„์†Œํ™”:

     @Override
-    public org.springframework.data.domain.Page<com.loopers.domain.metrics.dto.WeeklyAggregationDto> findWeeklyAggregation(
+    public Page<WeeklyAggregationDto> findWeeklyAggregation(
             Integer year,
             Integer week,
             LocalDate startDate,
             LocalDate endDate,
-            org.springframework.data.domain.Pageable pageable
+            Pageable pageable
     ) {
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (1)

63-64: ๋ณตํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ฐ€์ค‘์น˜ ๊ณ„์‚ฐ ๊ณต์‹(์ข‹์•„์š” 0.2 + ์กฐํšŒ์ˆ˜ 0.1 + ์ฃผ๋ฌธ์ˆ˜ 0.6)์ด ProductMetricsWeekly์™€ ์ผ๊ด€๋˜๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋ฐฉ์–ด์  null ์ฒ˜๋ฆฌ ์ถ”๊ฐ€ ๊ณ ๋ ค

์Šคํ‚ค๋งˆ์—์„œ nullable = false๋กœ ์„ค์ •๋˜์–ด ์žˆ์–ด ์•ˆ์ „ํ•˜์ง€๋งŒ, ๋ฐฉ์–ด์  ์ฝ”๋”ฉ์„ ์œ„ํ•ด null ์ฒดํฌ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

 public double calculateCompositeScore() {
-    return (totalLikeCount * 0.2) + (totalViewCount * 0.1) + (totalOrderCount * 0.6);
+    long likeCount = totalLikeCount != null ? totalLikeCount : 0L;
+    long viewCount = totalViewCount != null ? totalViewCount : 0L;
+    long orderCount = totalOrderCount != null ? totalOrderCount : 0L;
+    return (likeCount * 0.2) + (viewCount * 0.1) + (orderCount * 0.6);
 }
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/MonthlyMetricsWriter.java (1)

22-26: ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Line 26์—์„œ List<? extends ProductMetricsMonthly>๋ฅผ List<ProductMetricsMonthly>๋กœ unchecked castํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Spring Batch ์ปจํ…์ŠคํŠธ์—์„œ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์•ˆ์ „ํ•˜์ง€๋งŒ, ์ปดํŒŒ์ผ๋Ÿฌ ๊ฒฝ๊ณ ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉฐ ์ด๋ก ์ ์œผ๋กœ๋Š” ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ฌธ์ œ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ํƒ€์ž… ์•ˆ์ „ํ•œ ๋Œ€์•ˆ
 @Override
 public void write(Chunk<? extends ProductMetricsMonthly> chunk) {
-    List<? extends ProductMetricsMonthly> items = chunk.getItems();
-
-    // Bulk Insert/Update (UPSERT)
-    monthlyRepository.saveAll((List<ProductMetricsMonthly>) items);
+    List<ProductMetricsMonthly> items = new ArrayList<>();
+    for (ProductMetricsMonthly item : chunk.getItems()) {
+        items.add(item);
+    }
+    
+    // Bulk Insert/Update (UPSERT)
+    monthlyRepository.saveAll(items);

     log.info("์›”๊ฐ„ ์ง‘๊ณ„ ์ €์žฅ ์™„๋ฃŒ: {} ๊ฑด", items.size());
 }

๋˜๋Š” Java 10+ ์ŠคํŠธ๋ฆผ ์‚ฌ์šฉ:

 @Override
 public void write(Chunk<? extends ProductMetricsMonthly> chunk) {
-    List<? extends ProductMetricsMonthly> items = chunk.getItems();
-
-    // Bulk Insert/Update (UPSERT)
-    monthlyRepository.saveAll((List<ProductMetricsMonthly>) items);
+    List<ProductMetricsMonthly> items = chunk.getItems().stream()
+        .map(ProductMetricsMonthly.class::cast)
+        .toList();
+    
+    monthlyRepository.saveAll(items);

     log.info("์›”๊ฐ„ ์ง‘๊ณ„ ์ €์žฅ ์™„๋ฃŒ: {} ๊ฑด", items.size());
 }
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/MonthlyMetricsProcessor.java (1)

12-30: ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ํŒจํ„ด ๊ฒ€ํ†  ์ œ์•ˆ.

ํ˜„์žฌ ProductMetricsMonthly.create()๋กœ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•œ ํ›„ ์ฆ‰์‹œ updateMetrics()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํŒจํ„ด์€ ๋™์ž‘ํ•˜์ง€๋งŒ, ๋‘ ๋‹จ๊ณ„๋กœ ๋‚˜๋‰˜์–ด ์žˆ์–ด ๋‹ค์†Œ ๋น„์ง๊ด€์ ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”:

  1. create() ๋ฉ”์„œ๋“œ๊ฐ€ ๋ชจ๋“  ๋งค๊ฐœ๋ณ€์ˆ˜(metrics ํฌํ•จ)๋ฅผ ๋ฐ›๋„๋ก ํ™•์žฅ
  2. ๋นŒ๋” ํŒจํ„ด ์‚ฌ์šฉ
  3. ํ˜„์žฌ ํŒจํ„ด์ด ์˜๋„์ ์ธ ์„ค๊ณ„๋ผ๋ฉด ๊ทธ๋Œ€๋กœ ์œ ์ง€

ํ˜„์žฌ ๊ตฌํ˜„๋„ ์ •์ƒ ๋™์ž‘ํ•˜๋ฏ€๋กœ ํŒ€์˜ ์„ค๊ณ„ ์ฒ ํ•™์— ๋”ฐ๋ผ ์„ ํƒํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeeklyRepository.java (1)

8-10: ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ง€์› ๋ฐ saveAll ๋ฐ˜ํ™˜ ํƒ€์ž… ๊ฐœ์„  ๊ถŒ์žฅ

ํ˜„์žฌ ๊ตฌํ˜„์—์„œ ๋‘ ๊ฐ€์ง€ ๊ฐœ์„  ์‚ฌํ•ญ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”:

  1. findAll() ๋ฉ”์„œ๋“œ๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜ ์—†์ด ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ๋กœ ๋กœ๋“œํ•˜๋ฏ€๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์„ ๊ฒฝ์šฐ ์„ฑ๋Šฅ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. saveAll()์ด void๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ, ์ผ๋ฐ˜์ ์ธ Spring Data ํŒจํ„ด์—์„œ๋Š” ์ €์žฅ๋œ ์—”ํ‹ฐํ‹ฐ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์ƒ์„ฑ๋œ ID ๋“ฑ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
๐Ÿ”Ž ๊ฐœ์„ ๋œ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์ œ์•ˆ
 public interface ProductMetricsWeeklyRepository {
 
     ProductMetricsWeekly save(ProductMetricsWeekly metrics);
-    void saveAll(List<ProductMetricsWeekly> metricsList);
+    List<ProductMetricsWeekly> saveAll(List<ProductMetricsWeekly> metricsList);
     int deleteByYearAndWeekBefore(Integer year, Integer week);
-    List<ProductMetricsWeekly> findAll();
+    List<ProductMetricsWeekly> findAll(Pageable pageable);
 }
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsProcessor.java (1)

12-30: DTO ์ž…๋ ฅ๊ฐ’์— ๋Œ€ํ•œ null ๊ฒ€์ฆ ์ถ”๊ฐ€ ๊ถŒ์žฅ

ํ˜„์žฌ ํ”„๋กœ์„ธ์„œ๋Š” DTO์˜ ํ•„๋“œ๊ฐ’์— ๋Œ€ํ•œ null ์ฒดํฌ ์—†์ด ๋ฐ”๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Spring Batch์—์„œ DTO ์ƒ์„ฑ ์‹œ null ๊ฐ’์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ๋ฐฉ์–ด์  ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์œ„ํ•ด ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

๐Ÿ”Ž null ๊ฒ€์ฆ ์ถ”๊ฐ€ ์ œ์•ˆ
 public class WeeklyMetricsProcessor implements ItemProcessor<WeeklyAggregationDto, ProductMetricsWeekly> {
     @Override
     public ProductMetricsWeekly process(WeeklyAggregationDto dto) {
+        // ํ•„์ˆ˜ ํ•„๋“œ ๊ฒ€์ฆ
+        if (dto.getProductId() == null || dto.getYear() == null || dto.getWeek() == null) {
+            throw new IllegalArgumentException("Required fields cannot be null");
+        }
+        
         // DTO๋ฅผ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜
         ProductMetricsWeekly metrics = ProductMetricsWeekly.create(
                 dto.getProductId(),
                 dto.getYear(),
                 dto.getWeek(),
                 dto.getPeriodStartDate(),
                 dto.getPeriodEndDate()
         );
 
         // ์ง‘๊ณ„ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ
         metrics.updateMetrics(
-                dto.getTotalLikeCount(),
-                dto.getTotalViewCount(),
-                dto.getTotalOrderCount()
+                dto.getTotalLikeCount() != null ? dto.getTotalLikeCount() : 0L,
+                dto.getTotalViewCount() != null ? dto.getTotalViewCount() : 0L,
+                dto.getTotalOrderCount() != null ? dto.getTotalOrderCount() : 0L
         );
 
         return metrics;
     }
 }
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthlyRepository.java (1)

7-9: ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ง€์› ๋ฐ saveAll ๋ฐ˜ํ™˜ ํƒ€์ž… ๊ฐœ์„  ๊ถŒ์žฅ

ProductMetricsWeeklyRepository์™€ ๋™์ผํ•œ ๊ฐœ์„  ์‚ฌํ•ญ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”:

  1. findAll() ๋ฉ”์„œ๋“œ์— ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ง€์› ์ถ”๊ฐ€๋กœ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ๋ฌธ์ œ ๋ฐฉ์ง€
  2. saveAll()์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ List<ProductMetricsMonthly>๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ €์žฅ๋œ ์—”ํ‹ฐํ‹ฐ์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ฐœ์„ 
๐Ÿ”Ž ๊ฐœ์„ ๋œ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์ œ์•ˆ
 public interface ProductMetricsMonthlyRepository {
     ProductMetricsMonthly save(ProductMetricsMonthly metrics);
-    void saveAll(List<ProductMetricsMonthly> metricsList);
+    List<ProductMetricsMonthly> saveAll(List<ProductMetricsMonthly> metricsList);
     int deleteByYearAndMonthBefore(Integer year, Integer month);
-    List<ProductMetricsMonthly> findAll();
+    List<ProductMetricsMonthly> findAll(Pageable pageable);
 }
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (1)

91-100: ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ์œ„ํ•ด Clock ์ฃผ์ž… ๊ณ ๋ ค

Line 99์—์„œ ZonedDateTime.now()๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์‹œ ์‹œ๊ฐ„์„ ์ œ์–ดํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฌธ์ œ์—†์ง€๋งŒ, ํ…Œ์ŠคํŠธ ์‹œ ํŠน์ • ์‹œ๊ฐ„์„ ๊ฒ€์ฆํ•ด์•ผ ํ•  ๊ฒฝ์šฐ ์–ด๋ ค์›€์ด ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

ํ•„๋“œ์— Clock ์ถ”๊ฐ€:

+import java.time.Clock;
+
 @Entity
 @Table(...)
 @Getter
 @NoArgsConstructor
 public class ProductMetricsMonthly extends BaseEntity {
+    
+    private Clock clock = Clock.systemDefaultZone();
+    
+    // For testing
+    public void setClock(Clock clock) {
+        this.clock = clock;
+    }

updateMetrics ๋ฉ”์„œ๋“œ ์ˆ˜์ •:

     public void updateMetrics(
             Long likeCount,
             Long viewCount,
             Long orderCount
     ) {
         this.totalLikeCount = likeCount;
         this.totalViewCount = viewCount;
         this.totalOrderCount = orderCount;
-        this.aggregatedAt = ZonedDateTime.now();
+        this.aggregatedAt = ZonedDateTime.now(clock);
     }
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (1)

91-100: ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ์œ„ํ•ด Clock ์ฃผ์ž… ๊ณ ๋ ค

Line 99์˜ ZonedDateTime.now() ์ง์ ‘ ํ˜ธ์ถœ์€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์‹œ๊ฐ„ ์ œ์–ด๋ฅผ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ProductMetricsMonthly์™€ ๋™์ผํ•˜๊ฒŒ Clock ์ฃผ์ž…์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

๐Ÿ”Ž Clock ์ฃผ์ž…์„ ํ†ตํ•œ ๊ฐœ์„  ์ œ์•ˆ
+import java.time.Clock;
+
 @Entity
 @Table(...)
 @Getter
 @NoArgsConstructor
 public class ProductMetricsWeekly extends BaseEntity {
+    
+    private Clock clock = Clock.systemDefaultZone();
+    
+    // For testing
+    public void setClock(Clock clock) {
+        this.clock = clock;
+    }
     
     // ... existing fields ...
     
     public void updateMetrics(
             Long likeCount,
             Long viewCount,
             Long orderCount
     ) {
         this.totalLikeCount = likeCount;
         this.totalViewCount = viewCount;
         this.totalOrderCount = orderCount;
-        this.aggregatedAt = ZonedDateTime.now();
+        this.aggregatedAt = ZonedDateTime.now(clock);
     }
 }
apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodRankingService.java (2)

61-78: ์ธ๋ฉ”๋ชจ๋ฆฌ ํŽ˜์ด์ง•์€ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์—์„œ ๋น„ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„์€ (page + 1) * size ๋งŒํผ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ ํ›„ ์ธ๋ฉ”๋ชจ๋ฆฌ์—์„œ skip/limit์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๊ฐ€ ์ปค์งˆ์ˆ˜๋ก ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋งŽ์ด ์กฐํšŒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

Repository์—์„œ ์ง์ ‘ OFFSET/LIMIT์„ ์‚ฌ์šฉํ•˜๋Š” ํŽ˜์ด์ง• ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”.

๐Ÿ”Ž ๊ฐœ์„  ์ œ์•ˆ
// Repository์— ํŽ˜์ด์ง• ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
List<ProductMetricsWeekly> findByYearAndWeekOrderByLikeCountDesc(
    int year, int week, int offset, int limit);

// Service์—์„œ ์ง์ ‘ ์‚ฌ์šฉ
public List<Ranking> getWeeklyRankingWithPaging(RankingType type, LocalDate date, int page, int size) {
    LocalDate targetDate = date != null ? date : LocalDate.now();
    int year = targetDate.getYear();
    int week = targetDate.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR);
    
    List<ProductMetricsWeekly> weeklyMetrics = switch (type) {
        case LIKE -> weeklyRepository.findByYearAndWeekOrderByLikeCountDescWithPaging(
            year, week, page * size, size);
        // ... other cases
    };
    
    return convertWeeklyToRanking(weeklyMetrics, type, page * size);
}

81-130: ์ค‘๋ณต ์ฝ”๋“œ ์ถ”์ถœ์„ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

convertWeeklyToRanking๊ณผ convertMonthlyToRanking ๋ฉ”์„œ๋“œ๊ฐ€ ๊ฑฐ์˜ ๋™์ผํ•œ ๋กœ์ง์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค๋‚˜ ํ•จ์ˆ˜ํ˜• ์ ‘๊ทผ๋ฒ•์œผ๋กœ ์ถ”์ถœํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ํ–ฅ์ƒ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋ฆฌํŒฉํ† ๋ง ์˜ˆ์‹œ
// ๊ณตํ†ต ๋ณ€ํ™˜ ๋ฉ”์„œ๋“œ ์ถ”์ถœ
private <T> List<Ranking> convertToRanking(
        List<T> metrics,
        RankingType type,
        Function<T, Long> productIdGetter,
        Function<T, Long> likeCountGetter,
        Function<T, Long> viewCountGetter,
        Function<T, Long> orderCountGetter
) {
    int rank = 1;
    List<Ranking> rankings = new ArrayList<>();
    
    for (T metric : metrics) {
        double score = calculateScore(type,
                likeCountGetter.apply(metric),
                viewCountGetter.apply(metric),
                orderCountGetter.apply(metric));
        
        rankings.add(Ranking.of(
                rank++,
                productIdGetter.apply(metric),
                score,
                likeCountGetter.apply(metric),
                viewCountGetter.apply(metric),
                orderCountGetter.apply(metric)
        ));
    }
    return rankings;
}
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobConfig.java (2)

73-91: ISO ์ฃผ์ฐจ ๋‚ ์งœ ๊ณ„์‚ฐ ์‹œ ์—ฐ๋„ ๊ฒฝ๊ณ„ ์—ฃ์ง€ ์ผ€์ด์Šค ์ฃผ์˜

ISO 8601 ์ฃผ์ฐจ ๊ธฐ์ค€์œผ๋กœ, 1์›” 1์ผ์ด ํ•ญ์ƒ ํ•ด๋‹น ์—ฐ๋„์˜ ์ฒซ ์ฃผ์— ์†ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, 2026๋…„ 1์›” 1์ผ์€ ISO ๊ธฐ์ค€ 2025๋…„์˜ 53๋ฒˆ์งธ ์ฃผ์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„์—์„œ LocalDate.of(year, 1, 1)์„ ์‹œ์ž‘์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋ฉด, ํŠน์ • ์—ฐ๋„/์ฃผ์ฐจ ์กฐํ•ฉ์—์„œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋‚ ์งœ๊ฐ€ ๊ณ„์‚ฐ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋” ์•ˆ์ •์ ์ธ ISO ์ฃผ์ฐจ ๋‚ ์งœ ๊ณ„์‚ฐ ๋ฐฉ์‹ ์ œ์•ˆ
     @Bean
     @StepScope
     public RepositoryItemReader<WeeklyAggregationDto> weeklyMetricsReader(
             @Value("#{jobParameters['year']}") Integer year,
             @Value("#{jobParameters['week']}") Integer week
     ) {
         // ์ฃผ๊ฐ„ ์‹œ์ž‘์ผ/์ข…๋ฃŒ์ผ ๊ณ„์‚ฐ
-        LocalDate startDate = LocalDate.of(year, 1, 1)
-                .with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, week)
-                .with(DayOfWeek.MONDAY);
+        LocalDate startDate = LocalDate.ofYearDay(year, 1)
+                .with(IsoFields.WEEK_BASED_YEAR, year)
+                .with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, week)
+                .with(DayOfWeek.MONDAY);
         LocalDate endDate = startDate.plusDays(6);

93-103: Processor์™€ Writer์˜ ๋ถˆํ•„์š”ํ•œ @StepScope ์ œ๊ฑฐ ๊ณ ๋ ค

weeklyMetricsProcessor()์™€ weeklyMetricsWriter()๋Š” job parameter๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ @StepScope๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. @StepScope๋Š” ๋งค Step ์‹คํ–‰๋งˆ๋‹ค ์ƒˆ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์•ฝ๊ฐ„์˜ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž @StepScope ์ œ๊ฑฐ ์ œ์•ˆ
     @Bean
-    @StepScope
     public ItemProcessor<WeeklyAggregationDto, ProductMetricsWeekly> weeklyMetricsProcessor() {
         return new WeeklyMetricsProcessor();
     }

     @Bean
-    @StepScope
     public ItemWriter<ProductMetricsWeekly> weeklyMetricsWriter() {
         return new WeeklyMetricsWriter(weeklyRepository);
     }
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsMonthlyJobConfig.java (1)

89-99: Processor์™€ Writer์˜ ๋ถˆํ•„์š”ํ•œ @StepScope ์ œ๊ฑฐ ๊ณ ๋ ค

์ฃผ๊ฐ„ ์„ค์ •๊ณผ ๋™์ผํ•˜๊ฒŒ, monthlyMetricsProcessor()์™€ monthlyMetricsWriter()๋Š” job parameter๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ @StepScope๊ฐ€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž @StepScope ์ œ๊ฑฐ ์ œ์•ˆ
     @Bean
-    @StepScope
     public ItemProcessor<MonthlyAggregationDto, ProductMetricsMonthly> monthlyMetricsProcessor() {
         return new MonthlyMetricsProcessor();
     }

     @Bean
-    @StepScope
     public ItemWriter<ProductMetricsMonthly> monthlyMetricsWriter() {
         return new MonthlyMetricsWriter(monthlyRepository);
     }
apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (1)

16-18: @component ๋Œ€์‹  @repository ์‚ฌ์šฉ ๊ณ ๋ ค

์ด ํด๋ž˜์Šค๋Š” ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ณ„์ธต์„ ๋‹ด๋‹นํ•˜๋ฏ€๋กœ, @Component ๋Œ€์‹  @Repository๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์˜๋ฏธ์ ์œผ๋กœ ๋” ๋ช…ํ™•ํ•˜๊ณ , Spring์˜ ์˜ˆ์™ธ ๋ณ€ํ™˜(exception translation) ๊ธฐ๋Šฅ๋„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž @repository ์‚ฌ์šฉ ์ œ์•ˆ
-import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Repository;

 /**
  * ์ฃผ๊ฐ„ ์ƒํ’ˆ ์ง‘๊ณ„ Repository ๊ตฌํ˜„ (์ฝ๊ธฐ ์ „์šฉ)
  * commerce-collector์—์„œ ์ƒ์„ฑํ•œ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ
  */
-@Component
+@Repository
 @RequiredArgsConstructor
 public class ProductMetricsWeeklyRepositoryImpl implements ProductMetricsWeeklyRepository {
๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 0129d10 and d5068dd.

๐Ÿ“’ Files selected for processing (40)
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
  • apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java
  • apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthlyRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java
  • apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeeklyRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodRankingService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodType.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java
  • apps/commerce-collector/build.gradle.kts
  • apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingScheduler.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/listener/ChunkListener.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/metrics/MonthlyMetricsProcessor.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/metrics/MonthlyMetricsWriter.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsProcessor.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsWriter.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsMonthlyJobConfig.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobConfig.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDailyRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthlyRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeeklyRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/MonthlyAggregationDto.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/WeeklyAggregationDto.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyJpaRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyJpaRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyJpaRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java
  • apps/commerce-collector/src/test/java/com/loopers/batch/metrics/job/ProductMetricsMonthlyJobTest.java
  • apps/commerce-collector/src/test/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobTest.java
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (5)
๐Ÿ“š 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-collector/src/main/java/com/loopers/batch/listener/ChunkListener.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.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-collector/src/main/java/com/loopers/batch/listener/JobListener.java
  • apps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.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-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDailyRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyJpaRepository.java
  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyJpaRepository.java
๐Ÿ“š Learning: 2025-12-19T09:30:12.459Z
Learnt from: HongChangMo
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 198
File: apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventListener.java:0-0
Timestamp: 2025-12-19T09:30:12.459Z
Learning: In the loopers-spring-java-template repository's commerce-api module, when entities are managed within a transactional context (e.g., Transactional methods), prefer relying on JPA dirty checking for automatic persistence rather than explicit save() calls. Both Payment and Order entities in PaymentEventListener use this pattern, with state changes automatically flushed on transaction commit.

Applied to files:

  • apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java
๐Ÿ“š Learning: 2025-11-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product ์—”ํ‹ฐํ‹ฐ (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)๋Š” ์œ ์ฆˆ์ผ€์ด์Šค๋ณ„๋กœ ์˜๋„์ ์œผ๋กœ ๋‹ค๋ฅธ ๋ฝ ์ „๋žต์„ ์‚ฌ์šฉํ•œ๋‹ค: ์ข‹์•„์š” ๊ธฐ๋Šฅ์—๋Š” ๋น„๊ด€์  ๋ฝ(findByIdForUpdate)์„, ์žฌ๊ณ  ์ฐจ๊ฐ์—๋Š” ๋‚™๊ด€์  ๋ฝ(Version + ์žฌ์‹œ๋„)์„ ์‚ฌ์šฉํ•œ๋‹ค.

Applied to files:

  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java
  • apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java
  • apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java
  • apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java
๐Ÿงฌ Code graph analysis (10)
apps/commerce-collector/src/main/java/com/loopers/batch/listener/ChunkListener.java (2)
apps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.java (1)
  • Slf4j (14-53)
apps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
  • Slf4j (14-45)
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (2)
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsWriter.java (1)
  • Slf4j (12-25)
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java (1)
  • Slf4j (20-189)
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/MonthlyMetricsWriter.java (1)
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java (1)
  • Slf4j (17-84)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodRankingService.java (3)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
  • Component (16-154)
apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java (1)
  • Component (16-67)
apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (1)
  • Component (16-67)
apps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (6)
apps/commerce-collector/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
  • Slf4j (9-21)
apps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.java (1)
  • Slf4j (14-53)
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsWriter.java (1)
  • Slf4j (12-25)
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java (1)
  • Slf4j (20-189)
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (1)
  • Slf4j (17-84)
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt (1)
  • log (22-119)
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (1)
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/WeeklyAggregationDto.java (1)
  • Getter (13-26)
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java (1)
  • Getter (6-49)
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (1)
  • Entity (15-66)
apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt (1)
  • name (14-87)
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java (1)
  • Getter (6-49)
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (1)
  • Entity (15-66)
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobConfig.java (1)
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsProcessor.java (1)
  • WeeklyMetricsProcessor (10-31)
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (1)
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/MonthlyAggregationDto.java (1)
  • Getter (13-26)
๐Ÿ”‡ Additional comments (46)
apps/commerce-collector/build.gradle.kts (2)

36-36: Spring Batch ํ…Œ์ŠคํŠธ ์ข…์†์„ฑ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

spring-batch-test๊ฐ€ testImplementation ์Šค์ฝ”ํ”„๋กœ ์ ์ ˆํ•˜๊ฒŒ ์ถ”๊ฐ€๋˜์–ด ๋ฐฐ์น˜ ์ž‘์—… ํ…Œ์ŠคํŠธ(JobLauncherTestUtils, StepScopeTestExecutionListener ๋“ฑ)๋ฅผ ์ง€์›ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


14-14: Spring Batch ์ข…์†์„ฑ์ด ์ ์ ˆํžˆ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

spring-boot-starter-batch์™€ spring-batch-test ์ข…์†์„ฑ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ข…์†์„ฑ์€ ์ ์ ˆํžˆ ์Šค์ฝ”ํ”„ ์ฒ˜๋ฆฌ๋˜์—ˆ์œผ๋ฉฐ(implementation vs testImplementation), Spring Boot 3.4.4์˜ ์˜์กด์„ฑ ๊ด€๋ฆฌ๋ฅผ ํ†ตํ•ด ๋ฒ„์ „์ด ๊ด€๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

2025~2026๋…„ ํ˜„์žฌ๊นŒ์ง€ Spring Batch ๊ด€๋ จ ๋ณด์•ˆ ์ทจ์•ฝ์ (CVE)์ด ๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ ๋ณด์•ˆ์ƒ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingScheduler.java (1)

135-141: LGTM! ๋ณด๊ด€ ๊ธฐ๊ฐ„ ๋ณ€๊ฒฝ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์ฃผ๊ฐ„/์›”๊ฐ„ ์ง‘๊ณ„ ๋ฐฐ์น˜ ์ž‘์—…์„ ์ง€์›ํ•˜๊ธฐ ์œ„ํ•ด 10์ผ์—์„œ 30์ผ๋กœ ๋ณด๊ด€ ๊ธฐ๊ฐ„์„ ํ™•์žฅํ•œ ๊ฒƒ์€ ํ•ฉ๋ฆฌ์ ์ž…๋‹ˆ๋‹ค. ์›”๊ฐ„ ์ง‘๊ณ„์—๋Š” ์ตœ์†Œ 30์ผ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyJpaRepository.java (2)

31-54: GROUP BY์™€ Page ์กฐํ•ฉ ์‹œ count ์ฟผ๋ฆฌ ๊ฒ€์ฆ ํ•„์š”

Spring Data JPA์—์„œ GROUP BY๊ฐ€ ํฌํ•จ๋œ ์ฟผ๋ฆฌ์— Page๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, ์ž๋™ ์ƒ์„ฑ๋˜๋Š” count ์ฟผ๋ฆฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. GROUP BY ๊ฒฐ๊ณผ์˜ ํ–‰ ์ˆ˜๊ฐ€ ์•„๋‹Œ ์ „์ฒด ๋ ˆ์ฝ”๋“œ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

count ์ฟผ๋ฆฌ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค:

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
     @Query(value = """
         SELECT new com.loopers.domain.metrics.dto.WeeklyAggregationDto(
             p.productId,
             :year,
             :week,
             :startDate,
             :endDate,
             SUM(p.likeDelta),
             SUM(p.viewDelta),
             SUM(p.orderDelta),
             0L
         )
         FROM ProductMetricsDaily p
         WHERE p.metricDate BETWEEN :startDate AND :endDate
         GROUP BY p.productId
         ORDER BY p.productId
-    """)
+    """,
+    countQuery = """
+        SELECT COUNT(DISTINCT p.productId)
+        FROM ProductMetricsDaily p
+        WHERE p.metricDate BETWEEN :startDate AND :endDate
+    """)
     Page<WeeklyAggregationDto> findWeeklyAggregation(

56-82: ์›”๊ฐ„ ์ง‘๊ณ„ ์ฟผ๋ฆฌ๋„ ๋™์ผํ•˜๊ฒŒ count ์ฟผ๋ฆฌ ๋ช…์‹œ ํ•„์š”

์ฃผ๊ฐ„ ์ง‘๊ณ„์™€ ๋™์ผํ•œ ์ด์œ ๋กœ, ์›”๊ฐ„ ์ง‘๊ณ„ ์ฟผ๋ฆฌ์—๋„ ๋ช…์‹œ์ ์ธ count ์ฟผ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ • ์‚ฌํ•ญ
     @Query(value = """
         SELECT new com.loopers.domain.metrics.dto.MonthlyAggregationDto(
             p.productId,
             :year,
             :month,
             :startDate,
             :endDate,
             SUM(p.likeDelta),
             SUM(p.viewDelta),
             SUM(p.orderDelta),
             0L
         )
         FROM ProductMetricsDaily p
         WHERE p.metricDate BETWEEN :startDate AND :endDate
         GROUP BY p.productId
         ORDER BY p.productId
-    """)
+    """,
+    countQuery = """
+        SELECT COUNT(DISTINCT p.productId)
+        FROM ProductMetricsDaily p
+        WHERE p.metricDate BETWEEN :startDate AND :endDate
+    """)
     Page<MonthlyAggregationDto> findMonthlyAggregation(
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDailyRepository.java (1)

27-66: LGTM! ์ธํ„ฐํŽ˜์ด์Šค ์„ค๊ณ„๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ฃผ๊ฐ„/์›”๊ฐ„ ์ง‘๊ณ„ ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•œ Javadoc์ด ๋ช…ํ™•ํ•˜๊ณ , ํŽ˜์ด์ง• ์ง€์›์ด Spring Batch Job๊ณผ์˜ ํ†ตํ•ฉ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ Page/Pageable ์‚ฌ์šฉ ํŒจํ„ด๊ณผ ์ผ๊ด€์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

1-15: LGTM! ๊น”๋”ํ•œ enum ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

PeriodType enum์ด ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉฐ, ํ•œ๊ตญ์–ด ๋ ˆ์ด๋ธ”์ด ์‚ฌ์šฉ์ž ์นœํ™”์ ์ž…๋‹ˆ๋‹ค. Lombok์„ ํ™œ์šฉํ•˜์—ฌ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ๋ฅผ ์ตœ์†Œํ™”ํ•œ ์ ๋„ ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (2)

15-57: ์ฝ๊ธฐ ์ „์šฉ ์—”ํ‹ฐํ‹ฐ ์„ค๊ณ„๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Materialized View์— ๋งคํ•‘๋˜๋Š” ์ฝ๊ธฐ ์ „์šฉ ์—”ํ‹ฐํ‹ฐ๋กœ @Immutable ์–ด๋…ธํ…Œ์ด์…˜์ด ์ ์ ˆํ•˜๊ฒŒ ์‚ฌ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. JPA๋ฅผ ํ†ตํ•ด ์กฐํšŒ๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์‹ค์ œ ๋ฐ์ดํ„ฐ๋Š” commerce-collector์˜ ๋ฐฐ์น˜ ์ž‘์—…์—์„œ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.


15-65: AI ์š”์•ฝ๊ณผ ์‹ค์ œ ์ฝ”๋“œ ๊ฐ„์˜ ๋ถˆ์ผ์น˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

AI ์š”์•ฝ์—์„œ ์–ธ๊ธ‰๋œ ๋‹ค์Œ ๋ฉ”์„œ๋“œ๋“ค์ด ์‹ค์ œ ์ฝ”๋“œ์—๋Š” ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค:

  • static factory create(productId, year, month, periodStartDate, periodEndDate)
  • updateMetrics(likeCount, viewCount, orderCount)
  • reset()

์ฝ๊ธฐ ์ „์šฉ ์—”ํ‹ฐํ‹ฐ์ด๋ฏ€๋กœ ์ด๋Ÿฌํ•œ ๋ฉ”์„œ๋“œ๊ฐ€ ์—†๋Š” ๊ฒƒ์ด ์˜ฌ๋ฐ”๋ฅธ ์„ค๊ณ„์ด๋ฉฐ, ์‹ค์ œ ์ฝ”๋“œ๊ฐ€ ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/WeeklyAggregationDto.java (1)

13-26: DTO ์„ค๊ณ„๊ฐ€ ๊น”๋”ํ•˜๊ณ  ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Spring Batch ItemReader์—์„œ ์‚ฌ์šฉ๋  ์ฃผ๊ฐ„ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ DTO๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ •์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Lombok ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeeklyRepository.java (1)

6-36: ์ €์žฅ์†Œ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

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

apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthlyRepository.java (1)

6-36: ์ €์žฅ์†Œ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์›”๊ฐ„ ๋ฉ”ํŠธ๋ฆญ์Šค ์กฐํšŒ๋ฅผ ์œ„ํ•œ ๋‹ค์–‘ํ•œ ์ •๋ ฌ ๊ธฐ์ค€์„ ์ œ๊ณตํ•˜๋ฉฐ, ProductMetricsWeeklyRepository์™€ ์ผ๊ด€๋œ ์„ค๊ณ„ ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฉ”์„œ๋“œ ๋ช…๋ช…๊ณผ ๋ฌธ์„œํ™”๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)

14-15: ์ฝ”๋“œ๋Š” ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์˜ Spring Batch ๋ฒ„์ „๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

ํ”„๋กœ์ ํŠธ๋Š” Spring Boot 3.4.4๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ, ์ด๋Š” Spring Batch 5.2.2๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. Spring Batch 5.2.2์—์„œ ChunkListener์˜ ์˜ฌ๋ฐ”๋ฅธ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋Š” ChunkContext ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค:

@AfterChunk
void afterChunk(ChunkContext chunkContext)

ํ˜„์žฌ ์ฝ”๋“œ๋Š” Spring Batch 5.2.2 API ์‚ฌ์–‘์„ ์ •ํ™•ํžˆ ๋”ฐ๋ฅด๊ณ  ์žˆ์œผ๋ฏ€๋กœ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. Chunk<T> ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ์˜ ๋ณ€๊ฒฝ์€ Spring Batch 6.x ์ด์ƒ์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œํ•  ๋•Œ๋งŒ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyJpaRepository.java (1)

11-20: ์‚ญ์ œ ์ฟผ๋ฆฌ ๋กœ์ง ํ™•์ธ ์™„๋ฃŒ.

๋…„๋„์™€ ์ฃผ์ฐจ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ด์ „ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (m.year < :year) OR (m.year = :year AND m.week < :week) ์กฐ๊ฑด์€ ์ •ํ™•ํžˆ ์˜๋„ํ•œ ๋Œ€๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (2)

15-57: ์ฝ๊ธฐ ์ „์šฉ ์—”ํ‹ฐํ‹ฐ ๊ตฌ์กฐ๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

@Immutable ์–ด๋…ธํ…Œ์ด์…˜๊ณผ ํ•จ๊ป˜ ์ฝ๊ธฐ ์ „์šฉ ์—”ํ‹ฐํ‹ฐ๋กœ ๋ช…ํ™•ํžˆ ์ •์˜๋˜์–ด ์žˆ์–ด, Materialized View์˜ ํŠน์„ฑ์„ ์ฝ”๋“œ ๋ ˆ๋ฒจ์—์„œ ์ž˜ ํ‘œํ˜„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ฃผ๊ฐ„ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ์œ„ํ•œ ํ•„๋“œ ๊ตฌ์„ฑ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.


59-65: ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง์˜ ์ผ๊ด€์„ฑ์ด ํ™•๋ณด๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

calculateCompositeScore() ๋ฉ”์„œ๋“œ๊ฐ€ ProductMetricsMonthly(apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java Line 57-61)์™€ ๋™์ผํ•œ ๊ฐ€์ค‘์น˜(์ข‹์•„์š” 0.2, ์กฐํšŒ 0.1, ์ฃผ๋ฌธ 0.6)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyJpaRepository.java (1)

11-20: ์‚ญ์ œ ์ฟผ๋ฆฌ ๋กœ์ง ํ™•์ธ ์™„๋ฃŒ.

๋…„๋„์™€ ์›”์„ ๊ธฐ์ค€์œผ๋กœ ์ด์ „ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (m.year < :year) OR (m.year = :year AND m.month < :month) ์กฐ๊ฑด์€ ์ •ํ™•ํžˆ ์˜๋„ํ•œ ๋Œ€๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-collector/src/test/java/com/loopers/batch/metrics/job/ProductMetricsMonthlyJobTest.java (5)

26-61: ํ…Œ์ŠคํŠธ ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Spring Batch ํ…Œ์ŠคํŠธ ์„ค์ •๊ณผ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. DatabaseCleanUp์„ ์‚ฌ์šฉํ•œ ํ…Œ์ด๋ธ” ์ •๋ฆฌ ๋ฐฉ์‹๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.


63-123: ์›”๊ฐ„ ์ง‘๊ณ„ ์„ฑ๊ณต ์ผ€์ด์Šค๊ฐ€ ์ž˜ ๊ฒ€์ฆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

3๊ฐœ ์ƒํ’ˆ์— ๋Œ€ํ•œ 31์ผ๊ฐ„ ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ๋ฐ ๊ฒ€์ฆ ๋กœ์ง์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. Job/Step ์ƒํƒœ์™€ ์ง‘๊ณ„ ๊ฒฐ๊ณผ(310 likes, 3100 views, 155 orders)๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ฒ€์ฆ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


125-151: ๋ฐ์ดํ„ฐ ๋ถ€์žฌ ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์ ์ ˆํžˆ ๊ฒ€์ฆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Daily ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๋•Œ Job์ด ์„ฑ๊ณตํ•˜์ง€๋งŒ ์ฒ˜๋ฆฌ ๊ฑด์ˆ˜๊ฐ€ 0์ด๊ณ  Monthly ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


153-206: UPSERT ๋™์ž‘์ด ์ •ํ™•ํžˆ ๊ฒ€์ฆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋™์ผ ์›”์— ๋Œ€ํ•ด ์žฌ์‹คํ–‰ ์‹œ ์ƒˆ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š๊ณ  ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋Š” ๊ฒƒ(10 โ†’ 30)์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


208-272: Step ์‹คํ–‰๊ณผ ์›”๋ณ„ ์ผ์ˆ˜ ์ฒ˜๋ฆฌ๊ฐ€ ์ž˜ ๊ฒ€์ฆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

ํŠน์ • Step๋งŒ ์‹คํ–‰ํ•˜๋Š” ํ…Œ์ŠคํŠธ์™€ 2์›”(28์ผ)๊ณผ 12์›”(31์ผ)์˜ ์ผ์ˆ˜ ์ฐจ์ด๋ฅผ ์ •ํ™•ํžˆ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ๋ชจ๋‘ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. 2025๋…„์ด ํ‰๋…„์ž„์„ ๊ณ ๋ คํ•œ 28์ผ ๊ฒ€์ฆ๋„ ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.

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

67-88: ์ƒํ’ˆ ์ •๋ณด ํ•„๋“œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋งคํ•‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

RankingItem์— product ํ•„๋“œ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ๊ณ  from() ๋ฉ”์„œ๋“œ์—์„œ info.product()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ •ํ™•ํžˆ ๋งคํ•‘ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


16-36: ์ด ๊ฒ€ํ†  ์˜๊ฒฌ์€ ๊ทผ๊ฑฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. periodType์ด null์ผ ๋•Œ NPE๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์šฐ๋ ค๋Š” ํƒ€๋‹นํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

RankingFacade ํด๋ž˜์Šค์˜ getTopRanking() ๋ฐ getRankingWithPaging() ๋ฉ”์„œ๋“œ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด null ์ฒ˜๋ฆฌ๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

PeriodType period = periodType != null ? periodType : PeriodType.DAILY;

๋”ฐ๋ผ์„œ null๋กœ ์ „๋‹ฌ๋˜๋”๋ผ๋„ ๊ธฐ๋ณธ๊ฐ’(DAILY)์œผ๋กœ ์ž๋™ ์„ค์ •๋˜๋ฏ€๋กœ ์„œ๋น„์Šค ๋ ˆ์ด์–ด์—์„œ NPE ์œ„ํ—˜์€ ์—†์Šต๋‹ˆ๋‹ค. DTO์— @NotNull ์ถ”๊ฐ€๋‚˜ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •์€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsWriter.java (2)

18-19: unchecked cast๋Š” Spring Batch ํŒจํ„ด์—์„œ ๋ถˆ๊ฐ€ํ”ผํ•ฉ๋‹ˆ๋‹ค.

Line 19์˜ unchecked cast๋Š” Spring Batch์˜ ์ œ๋„ค๋ฆญ ์ œ์•ฝ์œผ๋กœ ์ธํ•ด ๋ถˆ๊ฐ€ํ”ผํ•˜๋ฉฐ, ๋Ÿฐํƒ€์ž„์— ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.


12-25: @component ์–ด๋…ธํ…Œ์ด์…˜์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

WeeklyMetricsWriter๋Š” ProductMetricsWeeklyJobConfig์—์„œ @Bean ๋ฉ”์„œ๋“œ๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์ธ์Šคํ„ด์Šคํ™”๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” Spring Batch์˜ ํ‘œ์ค€ ํŒจํ„ด์œผ๋กœ, ์ž๋™ ์Šค์บ”์ด ์•„๋‹Œ ์„ค์ • ํด๋ž˜์Šค์—์„œ ์ง์ ‘ ๋นˆ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. @Component ์–ด๋…ธํ…Œ์ด์…˜ ์—†์ด ํ˜„์žฌ ๊ตฌํ˜„์ด ์ •์ƒ ์ž‘๋™ํ•˜๋ฏ€๋กœ ์ถ”๊ฐ€ ์กฐ์น˜๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

apps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.java (1)

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

์‹คํ–‰ ์‹œ๊ฐ„ ๊ณ„์‚ฐ๊ณผ ํ•œ๊ตญ์–ด ์‹œ๊ฐ„ ๋‹จ์œ„(์‹œ๊ฐ„/๋ถ„/์ดˆ) ํฌ๋งคํŒ…์ด ์ •ํ™•ํ•˜๋ฉฐ, ๋กœ๊น… ๋กœ์ง์— ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

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

19-22: Step ์‹œ์ž‘ ๋กœ๊น…์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Step ์‹œ์ž‘ ์‹œ Step ์ด๋ฆ„์„ ์˜ฌ๋ฐ”๋ฅธ SLF4J ๊ตฌ๋ฌธ์œผ๋กœ ๋กœ๊น…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (1)

16-28: ํ…Œ์ด๋ธ” ์„ค๊ณ„๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค

Unique constraint์™€ ์ธ๋ฑ์Šค ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค:

  • uk_product_year_month๋Š” ์ค‘๋ณต ๋ฐ์ดํ„ฐ ๋ฐฉ์ง€
  • idx_year_month๋Š” ๊ธฐ๊ฐ„๋ณ„ ์กฐํšŒ ์ตœ์ ํ™”
  • idx_product_id๋Š” ์ƒํ’ˆ๋ณ„ ์กฐํšŒ ์ตœ์ ํ™”

์ด๋Š” ๋ฐฐ์น˜ upsert ๋ฐ ๋žญํ‚น ์กฐํšŒ ํŒจํ„ด์— ์ž˜ ๋งž์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (2)

16-28: ํ…Œ์ด๋ธ” ์„ค๊ณ„๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค

Unique constraint์™€ ์ธ๋ฑ์Šค ๊ตฌ์„ฑ์ด ์ฃผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ ํŒจํ„ด์— ์ž˜ ๋งž์Šต๋‹ˆ๋‹ค:

  • uk_product_year_week๋กœ ์ค‘๋ณต ๋ฐฉ์ง€
  • idx_year_week๋กœ ๊ธฐ๊ฐ„๋ณ„ ์กฐํšŒ ์ตœ์ ํ™”
  • idx_product_id๋กœ ์ƒํ’ˆ๋ณ„ ์กฐํšŒ ์ตœ์ ํ™”

1-111: AI ์š”์•ฝ์˜ ๋ชจํ˜ธํ•จ: calculateCompositeScore() ๋ฉ”์„œ๋“œ ์œ„์น˜ ๋ช…ํ™•ํ™” ํ•„์š”

๊ฒ€ํ†  ๋Œ€์ƒ์ธ apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java ํŒŒ์ผ์—๋Š” ์‹ค์ œ๋กœ calculateCompositeScore() ๋ฉ”์„œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ, ์ด๋Š” ์ฝ”๋“œ ๊ฒฐํ•จ์ด ์•„๋‹ˆ๋ผ ์˜๋„์ ์ธ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

์ฝ”๋“œ๋ฒ ์ด์Šค์—๋Š” ๋™์ผํ•œ ์ด๋ฆ„์˜ ProductMetricsWeekly ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋‘ ๋ชจ๋“ˆ์— ์กด์žฌํ•ฉ๋‹ˆ๋‹ค:

  • commerce-collector: ๋ฐฐ์น˜ ์ง‘๊ณ„์šฉ ์“ฐ๊ธฐ ์—”ํ‹ฐํ‹ฐ (BaseEntity ์ƒ์†) - ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฉ”์„œ๋“œ ์—†์Œ
  • commerce-api: ์ฝ๊ธฐ ์ „์šฉ ์—”ํ‹ฐํ‹ฐ (@immutable) - calculateCompositeScore() ๋ฉ”์„œ๋“œ ๊ตฌํ˜„๋จ (๊ฐ€์ค‘์น˜: likeร—0.2 + viewร—0.1 + orderร—0.6)

commerce-collector ๋ชจ๋“ˆ์—์„œ๋Š” ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ค‘ ๋ฉ”ํŠธ๋ฆญ ์ง‘๊ณ„๋งŒ ๋‹ด๋‹นํ•˜๋ฉฐ, ์ ์ˆ˜ ๊ณ„์‚ฐ์€ ์ฝ๊ธฐ ์‹œ์ ์— commerce-api์˜ ์—”ํ‹ฐํ‹ฐ ๋ฉ”์„œ๋“œ๋‚˜ PeriodRankingService ๋“ฑ์—์„œ ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ˜„์žฌ ์ฝ”๋“œ๋Š” ์ •์ƒ์ด๋ฉฐ, AI ์š”์•ฝ์ด ์ปจํ…์ŠคํŠธ ์—†์ด ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€๋ฅผ ์–ธ๊ธ‰ํ•œ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/MonthlyAggregationDto.java (1)

1-26: LGTM!

DTO ๊ตฌ์กฐ๊ฐ€ ์›”๊ฐ„ ์ง‘๊ณ„ ์šฉ๋„์— ์ ํ•ฉํ•˜๋ฉฐ, JPA ํ”„๋กœ์ ์…˜ ๋งคํ•‘์„ ์œ„ํ•œ @NoArgsConstructor์™€ ํŽธ์˜์„ฑ์„ ์œ„ํ•œ @AllArgsConstructor๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/test/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobTest.java (2)

125-232: LGTM!

๋‚˜๋จธ์ง€ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋“ค์ด ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋นˆ ๋ฐ์ดํ„ฐ ์‹œ๋‚˜๋ฆฌ์˜ค ์ฒ˜๋ฆฌ
  • UPSERT ๋ฉฑ๋“ฑ์„ฑ ๊ฒ€์ฆ (10 + 20 = 30)
  • Step ๋‹จ์œ„ ์‹คํ–‰ ๊ฒ€์ฆ

ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ๋ฐฐ์น˜ Job์˜ ์ฃผ์š” ๋™์ž‘์„ ํฌ๊ด„์ ์œผ๋กœ ๋‹ค๋ฃจ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


63-123: ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ฃผ๊ฐ„ ์ง‘๊ณ„ ๋ฐฐ์น˜ Job์˜ ์„ฑ๊ณต ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ ์ ˆํžˆ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, 7์ผ๊ฐ„์˜ Daily ๋ฐ์ดํ„ฐ๊ฐ€ Weekly๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ง‘๊ณ„๋ฉ๋‹ˆ๋‹ค. ISO ์ฃผ์ฐจ ๊ณ„์‚ฐ๋„ ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค: 2025-12-01์€ ์›”์š”์ผ์ด๋ฉฐ ISO ์ฃผ์ฐจ 49์˜ ์ฒซ ๋‚ ์ž…๋‹ˆ๋‹ค(49์ฃผ = 2025-12-01 ์›” ~ 2025-12-07 ์ผ).

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

132-139: ๋ณตํ•ฉ ์ ์ˆ˜ ๊ฐ€์ค‘์น˜ ํ•ฉ๊ณ„๊ฐ€ 1.0์ด ์•„๋‹™๋‹ˆ๋‹ค.

ALL ํƒ€์ž…์˜ ๊ฐ€์ค‘์น˜ ํ•ฉ๊ณ„๊ฐ€ 0.9์ž…๋‹ˆ๋‹ค (0.2 + 0.1 + 0.6). ์˜๋„๋œ ๊ฒƒ์ธ์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์ •๊ทœํ™”๋œ ์ ์ˆ˜๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ๊ฐ€์ค‘์น˜ ์กฐ์ •์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”.

apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (1)

25-28: LGTM!

๋‹จ์ผ ์ €์žฅ ๋ฐ ์กฐํšŒ/์‚ญ์ œ ๋ฉ”์„œ๋“œ๊ฐ€ JPA Repository์— ์ ์ ˆํžˆ ์œ„์ž„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

Also applies to: 75-83

apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java (1)

1-67: LGTM!

์ฝ๊ธฐ ์ „์šฉ Repository ๊ตฌํ˜„์ด JPA Repository์— ์ ์ ˆํžˆ ์œ„์ž„๋˜์–ด ์žˆ์œผ๋ฉฐ, commerce-api ๋ชจ๋“ˆ์˜ ProductMetricsWeeklyRepositoryImpl๊ณผ ์ผ๊ด€๋œ ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java (1)

25-28: LGTM!

๋‹จ์ผ ์ €์žฅ ๋ฐ ์กฐํšŒ/์‚ญ์ œ ๋ฉ”์„œ๋“œ๊ฐ€ JPA Repository์— ์ ์ ˆํžˆ ์œ„์ž„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

Also applies to: 75-83

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

37-41: DAILY ํƒ€์ž…์—์„œ date ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค.

DAILY ์ผ€์ด์Šค์—์„œ ์ „๋‹ฌ๋œ date ํŒŒ๋ผ๋ฏธํ„ฐ ๋Œ€์‹  LocalDate.now()๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์˜๋„๋œ ๋™์ž‘์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, API ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ๋‚ ์งœ์˜ ์ผ์ผ ๋žญํ‚น์„ ์กฐํšŒํ•˜๋ ค ํ•  ๋•Œ ํ˜ผ๋ž€์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜๋„๋œ ๋™์ž‘์ด๋ผ๋ฉด Javadoc์— ์ด ๋™์ž‘์„ ๋ช…์‹œํ•˜๊ฑฐ๋‚˜, ํŒŒ๋ผ๋ฏธํ„ฐ๋ช…์„ weeklyOrMonthlyDate๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”.

Also applies to: 64-68


21-21: LGTM!

PeriodRankingService ํ†ตํ•ฉ์ด ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. switch ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•œ ๊ธฐ๊ฐ„๋ณ„ ๋ผ์šฐํŒ… ๋กœ์ง์ด ๋ช…ํ™•ํ•˜๊ณ , null ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•œ DAILY ๊ธฐ๋ณธ๊ฐ’ ์ ์šฉ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 28-48

apps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsMonthlyJobConfig.java (1)

46-64: LGTM!

์›”๊ฐ„ ๋ฐฐ์น˜ Job ๊ตฌ์„ฑ์ด ์ฃผ๊ฐ„ ๊ตฌ์„ฑ๊ณผ ์ผ๊ด€๋œ ํŒจํ„ด์œผ๋กœ ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Job/Step/Chunk ๋ฆฌ์Šค๋„ˆ ์—ฐ๊ฒฐ๊ณผ chunk ํฌ๊ธฐ(100)๊ฐ€ pageSize์™€ ์ผ์น˜ํ•˜์—ฌ ํšจ์œจ์ ์ธ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyJpaRepository.java (2)

33-42: ๋ณตํ•ฉ ์ ์ˆ˜ ๊ฐ€์ค‘์น˜ ํ•ฉ๊ณ„ ํ™•์ธ ํ•„์š”

ํ˜„์žฌ ๋ณตํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ์‹์˜ ๊ฐ€์ค‘์น˜ ํ•ฉ์ด 0.9์ž…๋‹ˆ๋‹ค (0.2 + 0.1 + 0.6 = 0.9). ์ด๊ฒƒ์ด ์˜๋„๋œ ์„ค๊ณ„์ธ์ง€, ์•„๋‹ˆ๋ฉด ๊ฐ€์ค‘์น˜๊ฐ€ 1.0์ด ๋˜์–ด์•ผ ํ•˜๋Š”์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ, ์ด ์ฟผ๋ฆฌ๊ฐ€ ์ž์ฃผ ํ˜ธ์ถœ๋œ๋‹ค๋ฉด (year, month) ๋ณตํ•ฉ ์ธ๋ฑ์Šค๊ฐ€ ์„ฑ๋Šฅ์— ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


12-53: LGTM!

Repository ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ Spring Data JPA ๋ช…๋ช… ๊ทœ์น™์„ ์ž˜ ๋”ฐ๋ฅด๊ณ  ์žˆ์œผ๋ฉฐ, ๋‹ค์–‘ํ•œ ์ •๋ ฌ ๊ธฐ์ค€(์ข‹์•„์š”, ์กฐํšŒ์ˆ˜, ์ฃผ๋ฌธ์ˆ˜, ๋ณตํ•ฉ ์ ์ˆ˜)์— ๋Œ€ํ•œ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Based on learnings, Pageable ์‚ฌ์šฉ์€ ์ด ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ํ—ˆ์šฉ๋œ ์•„ํ‚คํ…์ฒ˜ ๊ฒฐ์ •์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (1)

22-66: LGTM!

JPA Repository๋กœ์˜ ์œ„์ž„ ํŒจํ„ด์ด ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. PageRequest.of(0, limit)๋ฅผ ์‚ฌ์šฉํ•œ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ฒ˜๋ฆฌ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyJpaRepository.java (2)

33-42: ๋ณตํ•ฉ ์ ์ˆ˜ ๊ฐ€์ค‘์น˜ ํ•ฉ๊ณ„ - Monthly Repository์™€ ๋™์ผํ•œ ํ™•์ธ ํ•„์š”

Monthly Repository์™€ ๋™์ผํ•˜๊ฒŒ ๊ฐ€์ค‘์น˜ ํ•ฉ์ด 0.9์ž…๋‹ˆ๋‹ค. ๋‘ repository ๊ฐ„ ์ผ๊ด€์„ฑ์€ ์œ ์ง€๋˜์–ด ์žˆ์œผ๋‚˜, ์˜๋„๋œ ์„ค๊ณ„์ธ์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.


12-53: LGTM!

์ฃผ๊ฐ„ Repository๊ฐ€ ์›”๊ฐ„ Repository์™€ ์ผ๊ด€๋œ ํŒจํ„ด์œผ๋กœ ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ : AI ์š”์•ฝ์—์„œ deleteByYearAndWeekBefore ๋ฉ”์„œ๋“œ๊ฐ€ ์–ธ๊ธ‰๋˜์—ˆ์œผ๋‚˜ ์‹ค์ œ ์ฝ”๋“œ์—๋Š” ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ๋ฉ”์„œ๋“œ๊ฐ€ ๋‹ค๋ฅธ ํŒŒ์ผ์— ์žˆ๊ฑฐ๋‚˜ ์š”์•ฝ์ด ๋ถ€์ •ํ™•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

- ์˜คํƒ€ ์ˆ˜์ •
- NPE ๋ฐฉ์ง€ ์ฝ”๋“œ ์ ์šฉ
@HongChangMo HongChangMo changed the title Round 10 [volume-10] Collect, Stack, Zip Jan 2, 2026
@HongChangMo
Copy link
Collaborator Author

@coderabbitai ์•„๋ž˜์˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธํ•ด์ค˜

๐Ÿงฑ Spring Batch

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

๐Ÿงฉ Ranking API

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

Tip

For best results, initiate chat on the files or code changes.

@HongChangMo ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ๋“ค์„ ํ™•์ธํ•œ ๊ฒฐ๊ณผ, ๋ชจ๋“  ์š”๊ตฌ์‚ฌํ•ญ์ด ์ถฉ์กฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! โœ…

๐Ÿงฑ Spring Batch

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

    • ProductMetricsWeeklyJobConfig์™€ ProductMetricsMonthlyJobConfig๊ฐ€ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
    • @StepScope๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Job Parameter๋ฅผ ์ฃผ์ž…๋ฐ›์Šต๋‹ˆ๋‹ค:
      • ์ฃผ๊ฐ„ ์ง‘๊ณ„: year, week ํŒŒ๋ผ๋ฏธํ„ฐ
      • ์›”๊ฐ„ ์ง‘๊ณ„: year, month ํŒŒ๋ผ๋ฏธํ„ฐ
    • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ JobParameters๋ฅผ ํ†ตํ•ด ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ฐ˜ ์‹คํ–‰์„ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
  • โœ… Chunk Oriented Processing (Reader/Processor/Writer) ๊ธฐ๋ฐ˜์˜ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค.

    • ์ฃผ๊ฐ„ ์ง‘๊ณ„:
      • Reader: RepositoryItemReader<WeeklyAggregationDto>
      • Processor: WeeklyMetricsProcessor
      • Writer: WeeklyMetricsWriter
    • ์›”๊ฐ„ ์ง‘๊ณ„:
      • Reader: RepositoryItemReader<MonthlyAggregationDto>
      • Processor: MonthlyMetricsProcessor
      • Writer: MonthlyMetricsWriter
    • ๋‘ Job ๋ชจ๋‘ chunk size 100์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • โœ… ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  Materialized View์˜ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์žฌํ–ˆ๋‹ค.

    • ์ฃผ๊ฐ„ MV: ProductMetricsWeekly (ํ…Œ์ด๋ธ”: mv_product_metrics_weekly)
      • productId, year, week, period dates, ์ง‘๊ณ„ ์นด์šดํŠธ(like/view/order)
      • unique constraint: (product_id, year, week)
    • ์›”๊ฐ„ MV: ProductMetricsMonthly (ํ…Œ์ด๋ธ”: mv_product_metrics_monthly)
      • productId, year, month, period dates, ์ง‘๊ณ„ ์นด์šดํŠธ(like/view/order)
      • unique constraint: (product_id, year, month)
    • JDBC์˜ bulk UPSERT (ON DUPLICATE KEY UPDATE)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํšจ์œจ์ ์œผ๋กœ ์ ์žฌํ•ฉ๋‹ˆ๋‹ค.

๐Ÿงฉ Ranking API

  • โœ… API๊ฐ€ ์ผ๊ฐ„, ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น์„ ์ œ๊ณตํ•˜๋ฉฐ ์กฐํšŒํ•ด์•ผ ํ•˜๋Š” ํ˜•ํƒœ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋žญํ‚น์„ ์ œ๊ณตํ•œ๋‹ค.
    • PeriodType enum (DAILY, WEEKLY, MONTHLY)์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
    • RankingFacade๊ฐ€ PeriodType์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋กœ ๋ผ์šฐํŒ…ํ•ฉ๋‹ˆ๋‹ค:
      • DAILY: Redis ZSET (๊ธฐ์กด ๋กœ์ง)
      • WEEKLY: ProductMetricsWeekly MV (PeriodRankingService)
      • MONTHLY: ProductMetricsMonthly MV (PeriodRankingService)
    • API ์—”๋“œํฌ์ธํŠธ์—์„œ periodType ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋“  ์š”๊ตฌ์‚ฌํ•ญ์ด ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์œผ๋ฉฐ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ๋„ ๊ฒ€์ฆ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰

@HongChangMo HongChangMo self-assigned this Jan 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant