Skip to content

Conversation

@sylee6529
Copy link
Collaborator

@sylee6529 sylee6529 commented Jan 2, 2026

๐Ÿ“Œ Summary

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

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

  • Chunk-Oriented Processing์œผ๋กœ ProductMetrics ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ์ ์ˆ˜ ๊ณ„์‚ฐ
  • ์ „์ฒด ์ƒํ’ˆ ์ค‘ TOP 100 ๋žญํ‚น์„ Materialized View ํ…Œ์ด๋ธ”์— ์ €์žฅ
  • ์ฃผ๊ฐ„/์›”๊ฐ„ ๊ธฐ๊ฐ„๋ณ„ ๋žญํ‚น ์กฐํšŒ API ์ œ๊ณต (ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ง€์›)

๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํ”Œ๋กœ์šฐ:

ProductMetrics โ†’ JdbcPagingItemReader โ†’ RankingScoreProcessor
โ†’ InMemoryCollector โ†’ StepListener(์ •๋ ฌ & TOP 100 ์ €์žฅ) โ†’ Materialized View

๐Ÿ’ฌ Review Points

1.QueryDSL ๋ฒŒํฌ DELETE vs JPA deleteBy ์„ ํƒ

ํ˜„์žฌ ๊ตฌํ˜„ (QueryDSL ๋ฒŒํฌ DELETE):

queryFactory.delete(weeklyRanking)
    .where(weeklyRanking.weekStartDate.eq(weekStartDate))
    .execute();  // 1๊ฐœ ์ฟผ๋ฆฌ

JPA deleteBy ๋ฐฉ์‹:

void deleteByWeekStartDate(LocalDate weekStartDate);
// SELECT 1๊ฐœ + DELETE N๊ฐœ = N+1 ์ฟผ๋ฆฌ

์งˆ๋ฌธ:

  • QueryDSL ๋ฒŒํฌ DELETE๋Š” ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š์•„ ๋™๊ธฐํ™” ๋ฌธ์ œ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ๋Š”๋ฐ, ๋ฐฐ์น˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์•ˆ์ „ํ•œ๊ฐ€์š”?
    @Query + @Modifying(clearAutomatically = true)๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์•ˆ์ „ํ•œ๊ฐ€์š”?

2.StepExecutionListener์—์„œ DELETE + SAVE ํŠธ๋žœ์žญ์…˜ ๋ถ„๋ฆฌ ๋ฌธ์ œ

ํ˜„์žฌ ๊ตฌํ˜„:

// Repository ๋ฉ”์„œ๋“œ๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ํŠธ๋žœ์žญ์…˜
@Override
@Transactional
public void deleteByWeekStartDate(LocalDate weekStartDate) { ... }

@Override
@Transactional
public void saveAll(List<WeeklyRanking> rankings) { ... }

StepExecutionListener์—์„œ ํ˜ธ์ถœ:

weeklyRankingRepository.deleteByWeekStartDate(weekStartDate);  // ํŠธ๋žœ์žญ์…˜ 1
weeklyRankingRepository.saveAll(rankedList);  // ํŠธ๋žœ์žญ์…˜ 2

์งˆ๋ฌธ:

  • DELETE ์„ฑ๊ณต ํ›„ SAVE ์‹คํŒจ ์‹œ โ†’ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์†์‹ค๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. DELETE + SAVE๋ฅผ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์–ด์•ผ ํ• ๊นŒ์š”?
  • ์„œ๋น„์Šค ๋ ˆ์ด์–ด๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ @Transactional replaceWeeklyRanking() ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ๋‚˜์„๊นŒ์š”?

3. ๋žญํ‚น๋ฐ์ดํ„ฐ๋ฅผ ์Œ“์„ ๋•Œ, ๋ชจ๋“  ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๋žญํ‚น์„ ์Œ“์ง€๋Š” ์•Š์„ ๊ฒƒ ๊ฐ™์€๋ฐ, Top-100์ด๋ฉด 100๊ฐœ๋งŒ ์ €์žฅํ•˜๋Š” ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜์‹œ๋‚˜์š”?


โœ… Checklist

๐Ÿงฑ Spring Batch

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

๐Ÿงฉ Ranking API

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

Summary by CodeRabbit

์ถœ์‹œ ๋…ธํŠธ

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

    • ์กฐํšŒ์ˆ˜, ์ข‹์•„์š” ๋ฐ ํŒ๋งค ๋ฉ”ํŠธ๋ฆญ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ์ƒ์œ„ 100๊ฐœ ์ƒํ’ˆ ๋žญํ‚น์„ ๊ณ„์‚ฐํ•˜๋Š” ๋ฐฐ์น˜ ์ž‘์—… ์ถ”๊ฐ€
    • ์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ๋žญํ‚น ๊ณ„์‚ฐ์„ ์ˆ˜๋™์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๊ด€๋ฆฌ์ž ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€
    • ์ผ๋ณ„, ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น์„ ๊ธฐ๊ฐ„๋ณ„๋กœ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋„๋ก ๋žญํ‚น API ํ™•์žฅ
  • ์ธํ”„๋ผ

    • Spring Batch ๊ตฌ์„ฑ ๋ฐ ์ž๋™ ์Šคํ‚ค๋งˆ ์ดˆ๊ธฐํ™”๋ฅผ ํฌํ•จํ•œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”

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

Spring Boot 3.x Auto-configuration ์‚ฌ์šฉ์„ ์œ„ํ•ด @EnableBatchProcessing ์ œ๊ฑฐ
Batch ๋ชจ๋“ˆ์„ ๋ณ„๋„ ๋ชจ๋“ˆ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์žฌ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ
์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น์„ ์œ„ํ•œ Entity ๋ฐ Repository ์ธํ„ฐํŽ˜์ด์Šค ์ถ”๊ฐ€
๊ธฐ๊ฐ„ ํƒ€์ž…(DAILY/WEEKLY/MONTHLY) ๋ฐ ๋‚ ์งœ ๊ณ„์‚ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๊ตฌํ˜„
QueryDSL ๊ธฐ๋ฐ˜ Repository ๊ตฌํ˜„์œผ๋กœ N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ
์ฃผ๊ฐ„/์›”๊ฐ„ TOP 100 ๋žญํ‚น ์ €์žฅ์„ ์œ„ํ•œ MV ํ…Œ์ด๋ธ” DDL ์ถ”๊ฐ€
ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ๋ฐ QueryDSL delete ์ฟผ๋ฆฌ ์ตœ์ ํ™”
ProductMetricsDto: ๋ฐฐ์น˜ Reader๊ฐ€ ์ฝ์–ด์˜ฌ DTO
RankedProductDto: ์ ์ˆ˜ ๊ณ„์‚ฐ ํ›„ ์ •๋ ฌ์šฉ DTO
RankingScoreProcessor: ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ์ ์ˆ˜ ๊ณ„์‚ฐ (view 0.1, like 0.2, order 0.6)
InMemoryRankingCollector: Step ์™„๋ฃŒ ํ›„ ์ •๋ ฌ์„ ์œ„ํ•œ ๋ฉ”๋ชจ๋ฆฌ ์ˆ˜์ง‘๊ธฐ
Chunk-Oriented Processing (CHUNK_SIZE=100) ์ ์šฉ
JdbcPagingItemReader๋กœ product_metrics ํŽ˜์ด์ง• ์กฐํšŒ
StepExecutionListener์—์„œ ์ •๋ ฌ ํ›„ TOP 100 ์„ ํƒ ๋ฐ ์ €์žฅ
Reader ์ดˆ๊ธฐํ™”๋ฅผ ์œ„ํ•œ afterPropertiesSet() ํ˜ธ์ถœ ์ถ”๊ฐ€
RankingService: ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ๋ฐ N+1 ํ•ด๊ฒฐ
RankingFacade: PeriodType์— ๋”ฐ๋ผ DAILY/WEEKLY/MONTHLY ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ
๋ฐฐ์น˜ ์กฐํšŒ๋กœ ์ƒํ’ˆ ๋ฐ ๋ธŒ๋žœ๋“œ ์ •๋ณด ํšจ์œจ์  ์กฐํšŒ
BatchJobController: ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ Job ์ˆ˜๋™ ์‹คํ–‰ API
RankingV1Controller: period ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ผ๊ฐ„/์ฃผ๊ฐ„/์›”๊ฐ„ ์„ ํƒ ์ง€์›
batch.yml ์„ค์ • import ์ถ”๊ฐ€
WeeklyRankingJobTest: ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ 3๊ฐœ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ
MonthlyRankingJobTest: ์›”๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ 4๊ฐœ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ
TestContainers ๊ธฐ๋ฐ˜ ์‹ค์ œ MySQL ํ™˜๊ฒฝ ํ…Œ์ŠคํŠธ
init-ranking-tables.sql: ํ…Œ์ŠคํŠธ์šฉ DDL ์Šคํฌ๋ฆฝํŠธ
@sylee6529 sylee6529 self-assigned this Jan 2, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

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

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

๊ฐœ์š”

์ด PR์€ Spring Batch๋ฅผ ํ™œ์šฉํ•œ ์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ์ƒํ’ˆ ๋žญํ‚น ๋ฐฐ์น˜ ์ž‘์—…์„ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ๋ฐฐ์น˜ ๋ชจ๋“ˆ, ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ, ์ €์žฅ์†Œ, ํ”„๋กœ์„ธ์„œ, ๊ทธ๋ฆฌ๊ณ  RankingFacade๋ฅผ ํ†ตํ•œ ๋‹ค์ค‘ ๊ธฐ๊ฐ„ ์กฐํšŒ ๊ธฐ๋Šฅ(์ผ์ผ, ์ฃผ๊ฐ„, ์›”๊ฐ„)์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

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

๊ทธ๋ฃน / ํŒŒ์ผ(๋“ค) ๋ณ€๊ฒฝ ์š”์•ฝ
๋ฐฐ์น˜ ๋ชจ๋“ˆ ์„ค์ •
modules/batch/build.gradle.kts, modules/batch/src/main/java/com/loopers/config/batch/BatchConfig.java, modules/batch/src/main/resources/batch.yml, apps/commerce-api/build.gradle.kts, settings.gradle.kts
๋ฐฐ์น˜ ๋ชจ๋“ˆ ์ถ”๊ฐ€, Spring Batch ์Šคํƒ€ํ„ฐ ์˜์กด์„ฑ ๊ตฌ์„ฑ, ์ž๋™ ์‹คํ–‰ ๋น„ํ™œ์„ฑํ™” ์„ค์ •, ์ƒ์œ„ build ํŒŒ์ผ์—์„œ ๋ฐฐ์น˜ ๋ชจ๋“ˆ ์ฐธ์กฐ
๋ฐฐ์น˜ ์ž‘์—… ๊ตฌ์„ฑ
apps/commerce-api/src/main/java/com/loopers/application/batch/WeeklyRankingJobConfig.java, apps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.java
์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ ์ž‘์—… ์ •์˜, JDBC Paging Reader, ๋žญํ‚น ์Šค์ฝ”์–ด ํ”„๋กœ์„ธ์„œ, ๋ฉ”๋ชจ๋ฆฌ ์ˆ˜์ง‘๊ธฐ, StepExecutionListener๋ฅผ ํ†ตํ•œ ํ›„์ฒ˜๋ฆฌ(์ •๋ ฌ, TOP 100 ์„ ์ •, ๋žญํ‚น ํ• ๋‹น, ์ €์žฅ)
๋ฐฐ์น˜ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๊ฐ์ฒด ๋ฐ ํ”„๋กœ์„ธ์„œ
apps/commerce-api/src/main/java/com/loopers/application/batch/dto/ProductMetricsDto.java, apps/commerce-api/src/main/java/com/loopers/application/batch/dto/RankedProductDto.java, apps/commerce-api/src/main/java/com/loopers/application/batch/processor/RankingScoreProcessor.java
ProductMetricsDto ๋ ˆ์ฝ”๋“œ, ์ •๋ ฌ ๊ฐ€๋Šฅํ•œ RankedProductDto ๋ ˆ์ฝ”๋“œ, ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ์ด์  ๊ณ„์‚ฐ ํ”„๋กœ์„ธ์„œ(์กฐํšŒยท์ข‹์•„์š”ยท์ฃผ๋ฌธ ๊ฐ€์ค‘์น˜ ๋ฐ ๋กœ๊ทธ ์ •๊ทœํ™”)
๋ฐฐ์น˜ ์“ฐ๊ธฐ ๊ตฌ์„ฑ ์š”์†Œ
apps/commerce-api/src/main/java/com/loopers/application/batch/writer/InMemoryRankingCollector.java
์ฒญํฌ๋ณ„ ์•„์ดํ…œ์„ ๋™๊ธฐํ™”๋œ ๋ฆฌ์ŠคํŠธ์— ์ˆ˜์ง‘ํ•˜๋Š” ItemWriter ๊ตฌํ˜„, ์ˆ˜์ง‘ ํฌ๊ธฐ ๋ฐ ์ •๋ณด ์กฐํšŒ ๋ฉ”์„œ๋“œ ์ œ๊ณต
๋„๋ฉ”์ธ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ ๋ฐ ์ €์žฅ์†Œ
apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRanking.java, apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRankingRepository.java, apps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRanking.java, apps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRankingRepository.java
JPA ์—”ํ‹ฐํ‹ฐ(์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น), ์กฐํšŒยทํŽ˜์ด์ง€๋„ค์ด์…˜ยท์‚ญ์ œยท์ผ๊ด„ ์ €์žฅ ๋ฉ”์„œ๋“œ๊ฐ€ ํฌํ•จ๋œ ์ €์žฅ์†Œ ์ธํ„ฐํŽ˜์ด์Šค
๋„๋ฉ”์ธ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฐ ์„œ๋น„์Šค
apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodType.java, apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodUtils.java, apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java
PeriodType ์—ด๊ฑฐํ˜•(DAILY, WEEKLY, MONTHLY), ์ฃผ๊ฐ„/์›”๊ฐ„ ๋ฒ”์œ„ ๊ณ„์‚ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ, ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์‚ฌ์šฉํ•œ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ๋ฐ N+1 ํšŒํ”ผ๋ฅผ ์œ„ํ•œ ๋ฐฐ์น˜ ์ƒํ’ˆยท๋ธŒ๋žœ๋“œ ๋กœ๋“œ
์ธํ”„๋ผ ์ €์žฅ์†Œ ๊ตฌํ˜„
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingJpaRepository.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingRepositoryImpl.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingJpaRepository.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingRepositoryImpl.java
JPA ์ €์žฅ์†Œ ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ QueryDSL ๊ธฐ๋ฐ˜ ๊ตฌํ˜„, ์ฃผ๊ฐ„/์›”๊ฐ„ ์กฐํšŒ ๋ฐ ์‚ญ์ œ ๊ธฐ๋Šฅ
API ๋ ˆ์ด์–ด
apps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
๋ฐฐ์น˜ ์ž‘์—… ์‹คํ–‰์„ ์œ„ํ•œ /admin/batch ์—”๋“œํฌ์ธํŠธ(์ฃผ๊ฐ„/์›”๊ฐ„), RankingV1Controller์— PeriodType ๋งค๊ฐœ๋ณ€์ˆ˜ ์ถ”๊ฐ€, RankingFacade๋ฅผ ์ผ์ผ/์ฃผ๊ฐ„/์›”๊ฐ„ ์กฐํšŒ๋กœ ํ™•์žฅ
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ
apps/commerce-api/src/test/resources/db/init-ranking-tables.sql, docker/mysql/init/03-ranking-materialized-views.sql
mv_product_rank_weekly, mv_product_rank_monthly, product_metrics ํ…Œ์ด๋ธ” ์ƒ์„ฑ(์ธ๋ฑ์Šค, ์ฃผ์„ ํฌํ•จ)
ํ…Œ์ŠคํŠธ
apps/commerce-api/src/test/java/com/loopers/application/batch/WeeklyRankingJobTest.java, apps/commerce-api/src/test/java/com/loopers/application/batch/MonthlyRankingJobTest.java
150๊ฐœ ๋ฉ”ํŠธ๋ฆญ ์‚ฝ์ž… ๋ฐ TOP 100 ๊ฒ€์ฆ, 100 ๋ฏธ๋งŒ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ, ์›”/์ฃผ ๊ฐ„ ๊ฒฉ๋ฆฌ, ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ๋ฅผ ํ™•์ธํ•˜๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

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

์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ ์ž‘์—… ํ๋ฆ„

sequenceDiagram
    participant JobLauncher
    participant BatchJob as Batch Job
    participant Reader
    participant Processor as Processor<br/>(Score Calc)
    participant Collector as Memory<br/>Collector
    participant Listener as Step<br/>Listener
    participant DB as Database
    
    JobLauncher->>BatchJob: Launch with targetDate
    
    rect rgb(200, 220, 255)
        note over Reader,Collector: Chunk Processing Loop
        Reader->>DB: Read ProductMetrics (paginated)
        Reader-->>Processor: ProductMetricsDto
        Processor->>Processor: Calculate weighted score<br/>(view, like, order)
        Processor-->>Collector: RankedProductDto
        Collector->>Collector: Collect in memory
    end
    
    rect rgb(220, 255, 220)
        note over Listener,DB: After Step Execution
        Listener->>Listener: Parse targetDate
        Listener->>Listener: Compute week/month range
        Listener->>DB: Delete old rankings<br/>for period
        Listener->>Collector: Get all collected items
        Listener->>Listener: Sort by score (DESC)
        Listener->>Listener: Select TOP 100
        Listener->>Listener: Assign rank positions
        Listener->>DB: Persist MonthlyRanking<br/>or WeeklyRanking
        Listener->>Listener: Log completion
    end
    
    BatchJob-->>JobLauncher: Job Completed
Loading

API ๋žญํ‚น ์กฐํšŒ ํ๋ฆ„ (์ฃผ๊ฐ„/์›”๊ฐ„)

sequenceDiagram
    participant Client
    participant Controller as RankingV1<br/>Controller
    participant Facade as Ranking<br/>Facade
    participant Service as Ranking<br/>Service
    participant WeekRepo as Weekly/Monthly<br/>Repository
    participant ProductRepo as Product<br/>Repository
    participant BrandRepo as Brand<br/>Repository
    
    Client->>Controller: GET /rankings?period=WEEKLY&page=1
    Controller->>Facade: getRankings(date, WEEKLY, page, size)
    
    alt period == WEEKLY or MONTHLY
        Facade->>Service: getWeeklyRankings<br/>or getMonthlyRankings<br/>(targetDate, page, size)
        Service->>WeekRepo: findByWeekStartDate<br/>WithPagination
        WeekRepo->>WeekRepo: Query with LIMIT/OFFSET
        WeekRepo-->>Service: List<WeeklyRanking>
        
        rect rgb(255, 240, 200)
            note over ProductRepo,BrandRepo: Batch Loading (้ฟๅ… N+1)
            Service->>ProductRepo: loadProductsByIds<br/>(collected productIds)
            ProductRepo-->>Service: Map<Long, Product>
            Service->>BrandRepo: loadBrandsByIds<br/>(collected brandIds)
            BrandRepo-->>Service: Map<Long, Brand>
        end
        
        Service->>Service: Build RankingItemInfo list<br/>(rank, product, brand, score)
        Service-->>Facade: RankingPageInfo
    else period == DAILY
        Facade->>Facade: getDailyRankings<br/>(from ProductRankingCache)
    end
    
    Facade-->>Controller: RankingPageInfo
    Controller-->>Client: ApiResponse<RankingPageResponse>
Loading

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

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

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

  • [volume-9] Product Ranking with Redis ย #220: ์ฃผ๊ฐ„/์›”๊ฐ„ ํ™•์žฅ, RankingFacade ๋ฐ RankingV1Controller ์ˆ˜์ •, PeriodType ์ถ”๊ฐ€ ๋ฐ ์ƒˆ๋กœ์šด ์„œ๋น„์Šค ์—ฐ๊ฒฐ๋กœ ์ด PR์˜ ๊ฐ™์€ ๋žญํ‚น ๊ด€๋ จ ํด๋ž˜์Šค๋ฅผ ํ™•์žฅยท๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
  • [volume-9] Product Ranking with Redisย #216: ์ƒํ’ˆ ๋žญํ‚น ๊ธฐ๋Šฅ ๊ตฌํ˜„, ๋„๋ฉ”์ธ ํด๋ž˜์Šค(RankingService, RankingFacade, ์ปจํŠธ๋กค๋Ÿฌ/DTO) ์ถ”๊ฐ€ยท์ˆ˜์ •์œผ๋กœ ์ฝ”๋“œ ์ˆ˜์ค€์—์„œ ์ง์ ‘ ๊ด€๋ จ๋ฉ๋‹ˆ๋‹ค.
  • [volume-9] Product Ranking with Redisย #213: ๋žญํ‚น ์„œ๋ธŒ์‹œ์Šคํ…œ ์ˆ˜์ •, RankingService/Ranking ๊ด€๋ จ API(์ปจํŠธ๋กค๋Ÿฌ, DTO, ์ €์žฅ์†Œ)๋ฅผ ๊ฐ™์€ ๋„๋ฉ”์ธ ํŒจํ‚ค์ง€์—์„œ ๋„์ž…ํ•˜์—ฌ ๊ด€๋ จ๋ฉ๋‹ˆ๋‹ค.

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

enhancement


๐Ÿฐ ๋ฐฐ์น˜๋กœ ์—ด์‹ฌํžˆ ์ •๋ ฌํ•˜๊ณ ,
์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น์„ ๊ณ„์‚ฐํ•˜๊ณ ,
TOP 100์€ ์˜๊ด‘์˜ ์ž๋ฆฌ,
์ ์ˆ˜๋กœ ์ ํ”„ํ•˜๋Š” ์ƒํ’ˆ๋“ค,
์ด์ œ ๋ชจ๋“  ์‹œ๊ฐ„์„ ํ’ˆ์•ˆ์—! ๐ŸŒŸ

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 39.47% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check โ“ Inconclusive ์ œ๋ชฉ 'Round10'์€ ๋งค์šฐ ๋ชจํ˜ธํ•˜๊ณ  PR์˜ ์‹ค์ œ ๋ณ€๊ฒฝ ๋‚ด์šฉ์„ ์ „ํ˜€ ์„ค๋ช…ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. PR์˜ ์ฃผ์š” ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๋ช…ํ™•ํžˆ ๋ฐ˜์˜ํ•˜๋Š” ๊ตฌ์ฒด์ ์ธ ์ œ๋ชฉ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์„ธ์š”. ์˜ˆ: 'feat: ์ฃผ๊ฐ„/์›”๊ฐ„ ์ƒํ’ˆ ๋žญํ‚น ๋ฐฐ์น˜ ์‹œ์Šคํ…œ ๊ตฌํ˜„'
โœ… Passed checks (1 passed)
Check name Status Explanation
Description check โœ… Passed PR ์„ค๋ช…์ด ์š”์•ฝ, ๋ฆฌ๋ทฐ ํฌ์ธํŠธ, ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ํฌํ•จํ•˜์—ฌ ํ…œํ”Œ๋ฆฟ ๊ตฌ์กฐ๋ฅผ ๋Œ€์ฒด๋กœ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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: 7

โ™ป๏ธ Duplicate comments (1)
apps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.java (1)

143-160: WeeklyRankingJobConfig์—์„œ ์–ธ๊ธ‰ํ•œ ๋™์ผํ•œ ๋™์‹œ ์‹คํ–‰ ์œ„ํ—˜์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค

deleteByMonthYear() ํ›„ saveAll() ์‚ฌ์ด์— ๊ฒฝํ•ฉ ์กฐ๊ฑด์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. month_year์™€ rank_position์— ๋Œ€ํ•œ ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

๐Ÿงน Nitpick comments (21)
docker/mysql/init/03-ranking-materialized-views.sql (1)

1-28: DDL ๊ตฌ์กฐ๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ธ๋ฑ์Šค ์ „๋žต์ด ๊ธฐ๊ฐ„๋ณ„ ์กฐํšŒ(idx_week_product, idx_month_product)์™€ ์ƒํ’ˆ๋ณ„ ์ด๋ ฅ ์กฐํšŒ(idx_product_week, idx_product_month) ๋ชจ๋‘๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

๋ช‡ ๊ฐ€์ง€ ๊ณ ๋ ค ์‚ฌํ•ญ:

  1. ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ: ๋™์ผ ๊ธฐ๊ฐ„ ๋‚ด ๋™์ผ product_id์˜ ์ค‘๋ณต ์‚ฝ์ž…์„ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด UNIQUE ์ œ์•ฝ ์กฐ๊ฑด ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”:

    UNIQUE KEY uk_week_product (week_start_date, product_id),
    UNIQUE KEY uk_month_product (month_year, product_id),
  2. ์ ์ˆ˜ ์ •๋ฐ€๋„: DOUBLE์€ ๋ถ€๋™์†Œ์ˆ˜์  ์˜ค์ฐจ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ •๋ฐ€ํ•œ ์ ์ˆ˜ ๋น„๊ต๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด DECIMAL(10,4) ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

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

33-34: ๋‚ ์งœ ํ˜•์‹ ๊ฒ€์ฆ ์ถ”๊ฐ€ ๊ถŒ์žฅ

date ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ yyyyMMdd ํ˜•์‹์ด์–ด์•ผ ํ•˜์ง€๋งŒ ํ˜•์‹ ๊ฒ€์ฆ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ํ˜•์‹์˜ ๋‚ ์งœ๊ฐ€ ์ž…๋ ฅ๋˜๋ฉด RankingFacade๋‚˜ ํ•˜์œ„ ๋ ˆ์ด์–ด์—์„œ ํŒŒ์‹ฑ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋‚ ์งœ ํ˜•์‹ ๊ฒ€์ฆ ์ถ”๊ฐ€
+import jakarta.validation.constraints.Pattern;
+
 @GetMapping("/rankings")
 public ApiResponse<RankingPageResponse> getRankings(
-        @RequestParam(value = "date", required = false) String date,
+        @RequestParam(value = "date", required = false) 
+        @Pattern(regexp = "^\\d{8}$", message = "๋‚ ์งœ๋Š” yyyyMMdd ํ˜•์‹์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค") 
+        String date,
         @RequestParam(value = "period", defaultValue = "DAILY") PeriodType period,
apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRanking.java (1)

30-34: MonthlyRanking๊ณผ์˜ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ์ฃผ์ฐจ ์‹๋ณ„์ž ํ•„๋“œ ์ถ”๊ฐ€ ๊ณ ๋ ค

MonthlyRanking์—๋Š” monthYear ํ•„๋“œ(์˜ˆ: "2024-01")๊ฐ€ ์žˆ์–ด ์›”๋ณ„ ์กฐํšŒ๊ฐ€ ํŽธ๋ฆฌํ•œ๋ฐ, WeeklyRanking์—๋Š” ์ด์— ๋Œ€์‘ํ•˜๋Š” ์ฃผ์ฐจ ์‹๋ณ„์ž ํ•„๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ฃผ ๋‹จ์œ„ ์กฐํšŒ ์‹œ ๋‚ ์งœ ๋ฒ”์œ„ ๋น„๊ต๊ฐ€ ํ•„์š”ํ•˜์—ฌ ์ฟผ๋ฆฌ๊ฐ€ ๋ณต์žกํ•ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ฃผ์ฐจ ์‹๋ณ„์ž ํ•„๋“œ ์ถ”๊ฐ€ ์˜ˆ์‹œ
 @Column(name = "week_end_date", nullable = false) 
 private LocalDate weekEndDate;
 
+@Column(name = "week_identifier", nullable = false)
+private String weekIdentifier; // ์˜ˆ: "2024-W01", "2024-W52"
+
 public WeeklyRanking(Integer rankPosition, Long productId, Double totalScore, 
-                    LocalDate weekStartDate, LocalDate weekEndDate) {
+                    LocalDate weekStartDate, LocalDate weekEndDate, String weekIdentifier) {
     this.rankPosition = rankPosition;
     this.productId = productId;
     this.totalScore = totalScore;
     this.weekStartDate = weekStartDate;
     this.weekEndDate = weekEndDate;
+    this.weekIdentifier = weekIdentifier;
 }
apps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRanking.java (1)

11-48: LGTM! ์—”ํ‹ฐํ‹ฐ ๊ตฌ์กฐ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

JPA ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. @NoArgsConstructor(access = AccessLevel.PROTECTED)๋กœ JPA ์š”๊ตฌ์‚ฌํ•ญ์„ ์ถฉ์กฑํ•˜๊ณ , WeeklyRanking๊ณผ ์ผ๊ด€๋œ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

์„ ํƒ์ ์œผ๋กœ, ์ƒ์„ฑ์ž ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•œ null ๊ฒ€์ฆ ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

๐Ÿ”Ž Null ๊ฒ€์ฆ ์ถ”๊ฐ€ ์˜ˆ์‹œ
 public MonthlyRanking(Integer rankPosition, Long productId, Double totalScore,
                       String monthYear, LocalDate monthStartDate, LocalDate monthEndDate) {
+    Objects.requireNonNull(rankPosition, "rankPosition must not be null");
+    Objects.requireNonNull(productId, "productId must not be null");
     this.rankPosition = rankPosition;
     this.productId = productId;
     this.totalScore = totalScore;
     this.monthYear = monthYear;
     this.monthStartDate = monthStartDate;
     this.monthEndDate = monthEndDate;
 }
apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodUtils.java (3)

8-8: ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค์— private ์ƒ์„ฑ์ž๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

์ธ์Šคํ„ด์Šคํ™”๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด private ์ƒ์„ฑ์ž๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž Private ์ƒ์„ฑ์ž ์ถ”๊ฐ€
 public class PeriodUtils {
+    
+    private PeriodUtils() {
+        // Utility class - prevent instantiation
+    }
     
     private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");

50-52: DateTimeFormatter๋ฅผ ํ˜ธ์ถœ๋งˆ๋‹ค ์ƒ์„ฑํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

parseDate ๋ฉ”์„œ๋“œ์—์„œ ๋งค๋ฒˆ ์ƒˆ๋กœ์šด DateTimeFormatter๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. MONTH_FORMATTER์ฒ˜๋Ÿผ ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•˜๋ฉด ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž Formatter ์ƒ์ˆ˜ํ™”
 public class PeriodUtils {
     
     private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
     
     // ... 
     
     public static LocalDate parseDate(String dateString) {
-        return LocalDate.parse(dateString, DateTimeFormatter.ofPattern("yyyyMMdd"));
+        return LocalDate.parse(dateString, DATE_FORMATTER);
     }

22-24: getWeekEndDate ๋ฉ”์„œ๋“œ ๊ฐ„์†Œํ™” ๊ฐ€๋Šฅ.

DayOfWeek.SUNDAY๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋ฉด ๋” ๊ฐ„๊ฒฐํ•˜๊ณ  ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฐ„์†Œํ™”๋œ ๊ตฌํ˜„
 public static LocalDate getWeekEndDate(LocalDate date) {
-    return date.with(DayOfWeek.MONDAY).plusDays(6);
+    return date.with(DayOfWeek.SUNDAY);
 }
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingJpaRepository.java (1)

15-17: ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ๊ฐ€ ์ผ๋ฐ˜์ ์ธ ํŒจํ„ด๊ณผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์—์„œ offset์ด limit ์•ž์— ์žˆ์Šต๋‹ˆ๋‹ค. SQL์˜ LIMIT ... OFFSET ์ˆœ์„œ์™€ ์ผ์น˜ํ•˜๋„๋ก limit, offset ์ˆœ์„œ๊ฐ€ ๋” ์ง๊ด€์ ์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ ๋ณ€๊ฒฝ
-    List<MonthlyRanking> findByMonthYearWithPagination(String monthYear, int offset, int limit);
+    List<MonthlyRanking> findByMonthYearWithPagination(String monthYear, int limit, int offset);
apps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.java (2)

44-47: ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€ ๋…ธ์ถœ ์ฃผ์˜

e.getMessage()๋ฅผ ์‘๋‹ต์— ์ง์ ‘ ํฌํ•จํ•˜๋ฉด ๋‚ด๋ถ€ ๊ตฌํ˜„ ์„ธ๋ถ€ ์‚ฌํ•ญ(์Šคํƒ ์ •๋ณด, DB ์˜ค๋ฅ˜ ๋“ฑ)์ด ์™ธ๋ถ€์— ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ผ๋ฐ˜์ ์ธ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋งŒ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์ƒ์„ธ ์ •๋ณด๋Š” ๋กœ๊ทธ์—๋งŒ ๊ธฐ๋กํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
         } catch (Exception e) {
             log.error("์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ Job ์‹คํ–‰ ์‹คํŒจ", e);
-            return ApiResponse.fail("BATCH_ERROR", "์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ Job ์‹คํ–‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: " + e.getMessage());
+            return ApiResponse.fail("BATCH_ERROR", "์ฃผ๊ฐ„ ๋žญํ‚น ์ง‘๊ณ„ Job ์‹คํ–‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");
         }

27-74: ์ค‘๋ณต ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง ๊ณ ๋ ค

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

๐Ÿ”Ž ๋ฆฌํŒฉํ† ๋ง ์˜ˆ์‹œ
private ApiResponse<Object> runRankingJob(Job job, String targetDate, String jobName) {
    try {
        String dateParam = targetDate != null ? targetDate : LocalDate.now().toString();
        
        JobParameters jobParameters = new JobParametersBuilder()
            .addString("targetDate", dateParam)
            .addLong("timestamp", System.currentTimeMillis())
            .toJobParameters();
        
        jobLauncher.run(job, jobParameters);
        
        log.info("{} Job ์‹คํ–‰ ์™„๋ฃŒ - targetDate: {}", jobName, dateParam);
        return ApiResponse.success(jobName + " Job์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
        
    } catch (Exception e) {
        log.error("{} Job ์‹คํ–‰ ์‹คํŒจ", jobName, e);
        return ApiResponse.fail("BATCH_ERROR", jobName + " Job ์‹คํ–‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");
    }
}
apps/commerce-api/src/test/java/com/loopers/application/batch/MonthlyRankingJobTest.java (1)

198-202: ์ฃผ์„๊ณผ ๊ตฌํ˜„์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Œ

์ฃผ์„์—๋Š” "์ ์ˆ˜๊ฐ€ ๋‹ค์–‘ํ•˜๋„๋ก ๋žœ๋คํ•˜๊ฒŒ ์ƒ์„ฑ"์ด๋ผ๊ณ  ๋˜์–ด ์žˆ์ง€๋งŒ, ์‹ค์ œ ๊ตฌํ˜„์€ ๊ฒฐ์ •์ (deterministic) ๋ฐฉ์‹์œผ๋กœ ์ ์ˆ˜๋ฅผ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ํ…Œ์ŠคํŠธ ์žฌํ˜„์„ฑ ์ธก๋ฉด์—์„œ ์˜ฌ๋ฐ”๋ฅธ ์ ‘๊ทผ์ด๋ฏ€๋กœ ์ฃผ์„์„ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
     /**
      * ์ƒ˜ํ”Œ ProductMetrics ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
      * - productId: 1๋ถ€ํ„ฐ count๊นŒ์ง€
-     * - ์ ์ˆ˜๊ฐ€ ๋‹ค์–‘ํ•˜๋„๋ก ๋žœ๋คํ•˜๊ฒŒ ์ƒ์„ฑ
+     * - productId ์—ญ์ˆœ์œผ๋กœ ์ ์ˆ˜ ๋ถ€์—ฌ (ํ…Œ์ŠคํŠธ ์žฌํ˜„์„ฑ์„ ์œ„ํ•ด ๊ฒฐ์ •์  ๊ฐ’ ์‚ฌ์šฉ)
      */
apps/commerce-api/src/test/java/com/loopers/application/batch/WeeklyRankingJobTest.java (3)

170-174: ์ฃผ์„๊ณผ ๊ตฌํ˜„์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Œ

MonthlyRankingJobTest์™€ ๋™์ผํ•˜๊ฒŒ, ์ฃผ์„์—๋Š” "๋žœ๋คํ•˜๊ฒŒ ์ƒ์„ฑ"์ด๋ผ๊ณ  ๋˜์–ด ์žˆ์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” ๊ฒฐ์ •์  ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
     /**
      * ์ƒ˜ํ”Œ ProductMetrics ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
      * - productId: 1๋ถ€ํ„ฐ count๊นŒ์ง€
-     * - ์ ์ˆ˜๊ฐ€ ๋‹ค์–‘ํ•˜๋„๋ก ๋žœ๋คํ•˜๊ฒŒ ์ƒ์„ฑ
+     * - productId ์—ญ์ˆœ์œผ๋กœ ์ ์ˆ˜ ๋ถ€์—ฌ (ํ…Œ์ŠคํŠธ ์žฌํ˜„์„ฑ์„ ์œ„ํ•ด ๊ฒฐ์ •์  ๊ฐ’ ์‚ฌ์šฉ)
      */

138-168: ๋‹ค๋ฅธ ์ฃผ(week) ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ํ…Œ์ŠคํŠธ ๋ˆ„๋ฝ

MonthlyRankingJobTest์—๋Š” shouldNotAffectOtherMonthsRanking ํ…Œ์ŠคํŠธ๊ฐ€ ์žˆ์ง€๋งŒ, ์ด ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์—๋Š” ์œ ์‚ฌํ•œ "๋‹ค๋ฅธ ์ฃผ์˜ ๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์˜ํ–ฅ๋ฐ›์ง€ ์•Š๋Š”๋‹ค" ํ…Œ์ŠคํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ์ž‘์—…์˜ ๊ฒฉ๋ฆฌ์„ฑ์„ ๋ณด์žฅํ•˜๋ ค๋ฉด ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

๋‹ค๋ฅธ ์ฃผ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•ด ๋“œ๋ฆด๊นŒ์š”?


175-188: ํ…Œ์ŠคํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ค‘๋ณต ์ฝ”๋“œ

insertSampleProductMetrics ๋ฉ”์„œ๋“œ๊ฐ€ MonthlyRankingJobTest์™€ ๋™์ผํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค๋กœ ์ถ”์ถœํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•ด์ง‘๋‹ˆ๋‹ค.

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

53-56: ์ „์ฒด ๋ฐ์ดํ„ฐ ์กฐํšŒ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ๋ฌธ์ œ

totalCount๋ฅผ ๊ตฌํ•˜๊ธฐ ์œ„ํ•ด ๋ชจ๋“  ๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋žญํ‚น ๋ฐ์ดํ„ฐ๊ฐ€ ์ตœ๋Œ€ 100๊ฐœ๋กœ ์ œํ•œ๋˜์–ด ์žˆ์–ด ํ˜„์žฌ๋Š” ์‹ฌ๊ฐํ•˜์ง€ ์•Š์ง€๋งŒ, ํ–ฅํ›„ ํ™•์žฅ์„ฑ์„ ์œ„ํ•ด ๋ณ„๋„์˜ count ์ฟผ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

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

Repository์— count ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€:

// WeeklyRankingRepository
long countByWeekStartDate(LocalDate weekStartDate);

Service์—์„œ ์‚ฌ์šฉ:

-        // 3. ์ „์ฒด ๊ฐœ์ˆ˜ ์กฐํšŒ
-        List<WeeklyRanking> allRankings = weeklyRankingRepository
-            .findByWeekStartDateOrderByRankPosition(weekStartDate);
-        long totalCount = allRankings.size();
+        // 3. ์ „์ฒด ๊ฐœ์ˆ˜ ์กฐํšŒ
+        long totalCount = weeklyRankingRepository.countByWeekStartDate(weekStartDate);

95-138: Weekly/Monthly ๋นŒ๋” ๋ฉ”์„œ๋“œ ์ค‘๋ณต

buildRankingItemsFromWeekly์™€ buildRankingItemsFromMonthly๊ฐ€ ๊ฑฐ์˜ ๋™์ผํ•œ ๋กœ์ง์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค๋‚˜ ์ œ๋„ค๋ฆญ์„ ํ™œ์šฉํ•˜์—ฌ ์ค‘๋ณต์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋ฆฌํŒฉํ† ๋ง ์˜ˆ์‹œ
private <T> List<RankingItemInfo> buildRankingItems(
    List<T> rankings,
    Function<T, Long> productIdExtractor,
    Function<T, Integer> rankPositionExtractor,
    Function<T, Double> scoreExtractor,
    String logPrefix
) {
    List<Long> productIds = rankings.stream()
        .map(productIdExtractor)
        .toList();
    
    List<Product> products = productRepository.findByIdIn(productIds);
    Map<Long, Product> productMap = products.stream()
        .collect(Collectors.toMap(Product::getId, Function.identity()));
    
    List<Long> brandIds = products.stream()
        .map(Product::getBrandId)
        .distinct()
        .toList();
    List<Brand> brands = brandRepository.findByIdIn(brandIds);
    Map<Long, Brand> brandMap = brands.stream()
        .collect(Collectors.toMap(Brand::getId, Function.identity()));
    
    return rankings.stream()
        .map(ranking -> {
            Product product = productMap.get(productIdExtractor.apply(ranking));
            if (product == null) {
                log.warn("[{}] Product not found - productId: {}", logPrefix, productIdExtractor.apply(ranking));
                return null;
            }
            Brand brand = brandMap.get(product.getBrandId());
            String brandName = brand != null ? brand.getName() : "Unknown";
            
            return new RankingItemInfo(
                rankPositionExtractor.apply(ranking),
                product.getId(),
                product.getName(),
                brandName,
                product.getPrice(),
                product.getLikeCount(),
                scoreExtractor.apply(ranking)
            );
        })
        .filter(Objects::nonNull)
        .toList();
}

136-136: ๋ฉ”์„œ๋“œ ์ฐธ์กฐ ์‚ฌ์šฉ ๊ถŒ์žฅ

filter(item -> item != null) ๋Œ€์‹  filter(Objects::nonNull)์„ ์‚ฌ์šฉํ•˜๋ฉด ๋” ๊ฐ„๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
+import java.util.Objects;
 ...
             })
-            .filter(item -> item != null)
+            .filter(Objects::nonNull)
             .toList();
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)

118-143: parseAndValidateDate์™€ validateAndNormalizeDate ๋ฉ”์„œ๋“œ์˜ ์ค‘๋ณต ๋กœ์ง

๋‘ ๋ฉ”์„œ๋“œ๊ฐ€ ์œ ์‚ฌํ•œ ๋‚ ์งœ ํŒŒ์‹ฑ/๊ฒ€์ฆ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. validateAndNormalizeDate๊ฐ€ ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์ œ๊ฑฐ๋ฅผ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”. ์‚ฌ์šฉ๋œ๋‹ค๋ฉด parseAndValidateDate๋ฅผ ํ™œ์šฉํ•˜๋„๋ก ๋ฆฌํŒฉํ† ๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

#!/bin/bash
# validateAndNormalizeDate ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ ์—ฌ๋ถ€ ํ™•์ธ
rg -n "validateAndNormalizeDate" --type java
apps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.java (1)

44-201: WeeklyRankingJobConfig์™€ ์ƒ๋‹นํ•œ ์ฝ”๋“œ ์ค‘๋ณต์ด ์žˆ์Šต๋‹ˆ๋‹ค

MonthlyRankingJobConfig์™€ WeeklyRankingJobConfig๋Š” reader ์„ค์ •, processor ์ƒ์„ฑ, listener ๋กœ์ง ๋“ฑ ๋Œ€๋ถ€๋ถ„์˜ ์ฝ”๋“œ๊ฐ€ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” ๋‘ ํด๋ž˜์Šค๋งŒ ์žˆ์–ด ๊ด€๋ฆฌ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ํ–ฅํ›„ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ์œ„ํ•ด ๊ณตํ†ต ๋กœ์ง ์ถ”์ถœ์„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ณตํ†ต ๋กœ์ง ์ถ”์ถœ ์˜ˆ์‹œ
// ๊ณตํ†ต ์ถ”์ƒ ํด๋ž˜์Šค ๋˜๋Š” ํ—ฌํผ ํด๋ž˜์Šค ์ถ”์ถœ ์˜ˆ์‹œ
public abstract class AbstractRankingJobConfig {
    
    protected JdbcPagingItemReader<ProductMetricsDto> createReader(
            DataSource dataSource, String readerName) {
        // ๊ณตํ†ต reader ์„ค์ • ๋กœ์ง
    }
    
    protected RowMapper<ProductMetricsDto> productMetricsRowMapper() {
        // ๊ณตํ†ต RowMapper
    }
    
    protected List<T> processAndRankItems(
            InMemoryRankingCollector collector, int topN) {
        // ๊ณตํ†ต ์ •๋ ฌ ๋ฐ TOP N ์„ ํƒ ๋กœ์ง
    }
}
apps/commerce-api/src/test/resources/db/init-ranking-tables.sql (2)

2-16: ๋™์ผ ์ฃผ๊ฐ„์— ์ค‘๋ณต ์ˆœ์œ„ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด ์ถ”๊ฐ€ ๊ถŒ์žฅ

ํ˜„์žฌ ์Šคํ‚ค๋งˆ์—์„œ๋Š” ๋™์ผํ•œ week_start_date์— ๊ฐ™์€ rank_position์ด ์—ฌ๋Ÿฌ ๋ฒˆ ์‚ฝ์ž…๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฐ์น˜ job์˜ ๋™์‹œ ์‹คํ–‰์ด๋‚˜ ์žฌ์‹คํ–‰ ์‹œ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด ์ถ”๊ฐ€ ์˜ˆ์‹œ
 CREATE TABLE IF NOT EXISTS mv_product_rank_weekly (
     id BIGINT AUTO_INCREMENT,
     rank_position INT NOT NULL,
     product_id BIGINT NOT NULL,
     total_score DOUBLE NOT NULL,
     week_start_date DATE NOT NULL COMMENT '์ฃผ๊ฐ„ ์‹œ์ž‘์ผ (์›”์š”์ผ)',
     week_end_date DATE NOT NULL COMMENT '์ฃผ๊ฐ„ ์ข…๋ฃŒ์ผ (์ผ์š”์ผ)',
     created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
     updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

     PRIMARY KEY (id),
+    UNIQUE KEY uk_week_rank (week_start_date, rank_position),
     INDEX idx_week_rank (week_start_date, rank_position),
     INDEX idx_week_product (week_start_date, product_id),
     INDEX idx_product_week (product_id, week_start_date)
 ) COMMENT '์ฃผ๊ฐ„ ์ƒํ’ˆ ๋žญํ‚น (TOP 100)';

19-34: ์›”๊ฐ„ ํ…Œ์ด๋ธ”์—๋„ ๋™์ผํ•œ ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด ์ถ”๊ฐ€ ๊ถŒ์žฅ

mv_product_rank_monthly ํ…Œ์ด๋ธ”์—๋„ (month_year, rank_position)์— ๋Œ€ํ•œ ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด์ด ์—†์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด ์ถ”๊ฐ€ ์˜ˆ์‹œ
 CREATE TABLE IF NOT EXISTS mv_product_rank_monthly (
     ...
     PRIMARY KEY (id),
+    UNIQUE KEY uk_month_rank (month_year, rank_position),
     INDEX idx_month_rank (month_year, rank_position),
     ...
 ) COMMENT '์›”๊ฐ„ ์ƒํ’ˆ ๋žญํ‚น (TOP 100)';
๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 53345ef and 0450d2a.

๐Ÿ“’ Files selected for processing (30)
  • apps/commerce-api/build.gradle.kts
  • apps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.java
  • apps/commerce-api/src/main/java/com/loopers/application/batch/WeeklyRankingJobConfig.java
  • apps/commerce-api/src/main/java/com/loopers/application/batch/dto/ProductMetricsDto.java
  • apps/commerce-api/src/main/java/com/loopers/application/batch/dto/RankedProductDto.java
  • apps/commerce-api/src/main/java/com/loopers/application/batch/processor/RankingScoreProcessor.java
  • apps/commerce-api/src/main/java/com/loopers/application/batch/writer/InMemoryRankingCollector.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodType.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodUtils.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRanking.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRankingRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRanking.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRankingRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
  • apps/commerce-api/src/main/resources/application.yml
  • apps/commerce-api/src/test/java/com/loopers/application/batch/MonthlyRankingJobTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/batch/WeeklyRankingJobTest.java
  • apps/commerce-api/src/test/resources/db/init-ranking-tables.sql
  • docker/mysql/init/03-ranking-materialized-views.sql
  • modules/batch/build.gradle.kts
  • modules/batch/src/main/java/com/loopers/config/batch/BatchConfig.java
  • modules/batch/src/main/resources/batch.yml
  • settings.gradle.kts
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (4)
๐Ÿ“š 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/application/batch/dto/RankedProductDto.java
  • apps/commerce-api/src/main/java/com/loopers/application/batch/dto/ProductMetricsDto.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:

  • modules/batch/src/main/resources/batch.yml
  • apps/commerce-api/src/main/resources/application.yml
  • apps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.java
  • apps/commerce-api/src/main/java/com/loopers/application/batch/WeeklyRankingJobConfig.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.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-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRankingRepository.java
๐Ÿ“š Learning: 2025-12-02T08:12:06.383Z
Learnt from: junoade
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 0
File: :0-0
Timestamp: 2025-12-02T08:12:06.383Z
Learning: ProductQueryService์—์„œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ์‹œ Redis ์บ์‹œ๋ฅผ ์ ์šฉํ–ˆ์œผ๋ฉฐ, ์บ์‹œ ํ‚ค๋Š” brandId, sortType, pageNumber, pageSize์˜ ์กฐํ•ฉ์œผ๋กœ ๊ตฌ์„ฑ๋˜๊ณ  TTL์€ 5๋ถ„์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ๋‹ค.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
๐Ÿงฌ Code graph analysis (9)
apps/commerce-api/src/main/java/com/loopers/application/batch/processor/RankingScoreProcessor.java (1)
apps/commerce-api/src/main/java/com/loopers/application/batch/writer/InMemoryRankingCollector.java (1)
  • Slf4j (17-42)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingRepositoryImpl.java (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingRepositoryImpl.java (1)
  • Repository (15-56)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingRepositoryImpl.java (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingRepositoryImpl.java (1)
  • Repository (14-55)
apps/commerce-api/src/test/java/com/loopers/application/batch/WeeklyRankingJobTest.java (1)
apps/commerce-api/src/test/java/com/loopers/application/batch/MonthlyRankingJobTest.java (1)
  • SpringBootTest (30-217)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java (1)
  • RankingInfo (7-40)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.java (1)
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt (1)
  • log (22-119)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRanking.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRanking.java (1)
  • Entity (11-48)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRanking.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRanking.java (1)
  • Entity (11-44)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodUtils.java (1)
  • PeriodUtils (8-75)
๐Ÿ”‡ Additional comments (23)
modules/batch/src/main/resources/batch.yml (1)

1-6: LGTM! Spring Batch ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

spring.batch.job.enabled=false๋กœ ์ž๋™ ์‹คํ–‰์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ณ  ์ˆ˜๋™ ์‹คํ–‰์„ ์ง€์›ํ•˜๋Š” ๊ฒƒ์€ ์˜ฌ๋ฐ”๋ฅธ ์ ‘๊ทผ์ž…๋‹ˆ๋‹ค.

initialize-schema: always ์„ค์ •์€ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” ์ ํ•ฉํ•˜์ง€๋งŒ, ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์‹œ์—๋Š” never ๋˜๋Š” embedded๋กœ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ํ”„๋กœํŒŒ์ผ๋ณ„๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”. (๊ธฐ์กด ํ•™์Šต์— ๋”ฐ๋ฅด๋ฉด EDA๋กœ ์ „ํ™˜ ์˜ˆ์ •์ด๋ฏ€๋กœ ํ˜„์žฌ๋Š” ๋ฌธ์ œ์—†์Šต๋‹ˆ๋‹ค.)

settings.gradle.kts (1)

10-10: LGTM!

์ƒˆ๋กœ์šด batch ๋ชจ๋“ˆ์ด ์ ์ ˆํ•˜๊ฒŒ Gradle ๋นŒ๋“œ์— ํฌํ•จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/resources/application.yml (1)

25-25: LGTM!

batch.yml ์„ค์ • ํŒŒ์ผ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ import๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ธํ”„๋ผ ๋ชจ๋“ˆ ์„ค์ • ๋‹ค์Œ, ์ง€์› ๋ชจ๋“ˆ ์„ค์ • ์ „์— ๋ฐฐ์น˜ํ•˜์—ฌ ์ผ๊ด€์„ฑ ์žˆ๋Š” ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

modules/batch/src/main/java/com/loopers/config/batch/BatchConfig.java (1)

1-16: LGTM! Spring Boot 3.x ๋ฐฐ์น˜ ์„ค์ • ๋ฐฉ์‹์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

@EnableBatchProcessing ๋Œ€์‹  auto-configuration์„ ์‚ฌ์šฉํ•˜๋Š” Spring Boot 3.x ์ ‘๊ทผ ๋ฐฉ์‹์— ๋Œ€ํ•œ ์„ค๋ช…์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๋นˆ ํด๋ž˜์Šค์ด์ง€๋งŒ, ํ–ฅํ›„ ์ปค์Šคํ…€ ๋ฐฐ์น˜ ๋นˆ(์˜ˆ: JobRepository, PlatformTransactionManager ์ปค์Šคํ„ฐ๋งˆ์ด์ง•)์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ํ™•์žฅ ์ง€์ ์œผ๋กœ ์œ ์ง€ํ•ด๋„ ์ข‹์Šต๋‹ˆ๋‹ค.

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

1-7: LGTM!

๊ธฐ๊ฐ„ ์œ ํ˜•์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜ํ•œ ์‹ฌํ”Œํ•œ enum์ž…๋‹ˆ๋‹ค. DAILY๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์–ด ํ–ฅํ›„ ์ผ๊ฐ„ ๋žญํ‚น ๊ธฐ๋Šฅ ํ™•์žฅ์—๋„ ๋Œ€๋น„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

6-6: LGTM!

batch ๋ชจ๋“ˆ ์˜์กด์„ฑ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด WeeklyRankingJobConfig, MonthlyRankingJobConfig ๋“ฑ ๋ฐฐ์น˜ ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ๋“ค์ด Spring Batch๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

modules/batch/build.gradle.kts (1)

1-14: LGTM!

๋ฐฐ์น˜ ๋ชจ๋“ˆ ๋นŒ๋“œ ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. java-library ํ”Œ๋Ÿฌ๊ทธ์ธ ์‚ฌ์šฉ๊ณผ api ์˜์กด์„ฑ ๋…ธ์ถœ ๋ฐฉ์‹์ด ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

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

11-34: ์ธ๋ฑ์Šค๋Š” ์ด๋ฏธ DDL ํŒŒ์ผ์—์„œ ์ •์˜๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ Java ์—”ํ‹ฐํ‹ฐ์— ์ถ”๊ฐ€ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/test/resources/db/init-ranking-tables.sql์— mv_product_rank_weekly ํ…Œ์ด๋ธ”์ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • INDEX idx_week_rank (week_start_date, rank_position) โœ“
  • INDEX idx_week_product (week_start_date, product_id) โœ“
  • INDEX idx_product_week (product_id, week_start_date) โœ“

์ด ํ”„๋กœ์ ํŠธ๋Š” DDL ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ตํ•ด ์ธ๋ฑ์Šค๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ, Java ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์— @Index ์ฃผ์„์„ ์ถ”๊ฐ€ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์ƒํƒœ๊ฐ€ ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

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

34-34: Spring์˜ enum ํŒŒ๋ผ๋ฏธํ„ฐ ์ž๋™ ๊ฒ€์ฆ ํ™•์ธ๋จ

Spring Framework๋Š” @RequestParam์œผ๋กœ ์„ ์–ธ๋œ enum ํƒ€์ž… ํŒŒ๋ผ๋ฏธํ„ฐ์— ์ž˜๋ชป๋œ ๊ฐ’์ด ์ž…๋ ฅ๋  ๋•Œ ์ž๋™์œผ๋กœ HTTP 400 Bad Request๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. StringToEnumConverterFactory๊ฐ€ ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š” Enum.valueOf()๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฐ’์— ๋Œ€ํ•ด ConversionFailedException์ด ๋ฐœ์ƒํ•˜๋ฉฐ ์ด๋Š” ์ž๋™์œผ๋กœ 400 ์‘๋‹ต์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. PeriodType enum์— ๋Œ€ํ•ด์„œ๋„ ์ด ํ‘œ์ค€ ๋™์ž‘์ด ์ ์šฉ๋˜๋ฏ€๋กœ ์ถ”๊ฐ€ ๊ฒ€์ฆ ๋กœ์ง์€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

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

6-15: LGTM! ๊น”๋”ํ•œ ๋„๋ฉ”์ธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค.

Spring Data ์˜์กด์„ฑ ์—†์ด ๋„๋ฉ”์ธ ๊ณ„์ธต์—์„œ ํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ๋งŒ ์ •์˜ํ•˜์—ฌ ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ž˜ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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

5-14: LGTM! WeeklyRankingRepository์™€ ์ผ๊ด€๋œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

monthYear๋ฅผ String ํƒ€์ž…์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด "yyyy-MM" ํฌ๋งท ์ €์žฅ ๋ฐฉ์‹๊ณผ ์ž˜ ๋งž์Šต๋‹ˆ๋‹ค.

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

27-43: LGTM! ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง์ด ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Math.log1p๋ฅผ ์‚ฌ์šฉํ•œ ๋กœ๊ทธ ์ •๊ทœํ™”๋กœ ๊ณ ๊ฐ€ ์ƒํ’ˆ ํŽธ์ค‘์„ ๋ฐฉ์ง€ํ•˜๋Š” ์„ค๊ณ„๊ฐ€ ์ข‹์Šต๋‹ˆ๋‹ค. ๊ฐ€์ค‘์น˜๋ฅผ ์ƒ์„ฑ์ž ์ฃผ์ž…์œผ๋กœ ๋ฐ›์•„ ์œ ์—ฐ์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.

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

20-27: Collections.synchronizedList ์‚ฌ์šฉ์€ ํ˜„์žฌ ๊ตฌ์กฐ์—์„œ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ Step ์„ค์ •์—์„œ taskExecutor๊ฐ€ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์ง€ ์•Š์œผ๋ฏ€๋กœ, ์ด Step์€ ๋‹จ์ผ ์Šค๋ ˆ๋“œ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฉฐ, Collections.synchronizedList๋Š” ๋ถˆํ•„์š”ํ•œ ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

๋‹จ์ˆœํ•œ ArrayList๋กœ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜, ํ–ฅํ›„ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด์งˆ ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•˜์—ฌ ๋ช…ํ™•ํ•œ ์˜๋„๋ฅผ ๋ฐ˜์˜ํ•œ ์ปฌ๋ ‰์…˜(์˜ˆ: ConcurrentHashMap ๊ธฐ๋ฐ˜ ๋˜๋Š” ๋ช…์‹œ์  ๋™๊ธฐํ™”)์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

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

14-55: LGTM!

๊ตฌํ˜„์ด WeeklyRankingRepositoryImpl๊ณผ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. QueryDSL์„ ์‚ฌ์šฉํ•œ ์กฐํšŒ ๋กœ์ง๊ณผ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/application/batch/MonthlyRankingJobTest.java (1)

30-33: ํ…Œ์ŠคํŠธ ๊ตฌ์„ฑ ์ ์ ˆํ•จ

@SpringBootTest์™€ @ActiveProfiles("test") ์กฐํ•ฉ์œผ๋กœ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์ด ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. @Sql์„ ํ†ตํ•œ ํ…Œ์ด๋ธ” ์ดˆ๊ธฐํ™”์™€ @BeforeEach์—์„œ์˜ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ๋กœ ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ๊ฐ€ ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค.

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

15-56: LGTM!

MonthlyRankingRepositoryImpl๊ณผ ์ผ๊ด€๋œ ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. QueryDSL์„ ํ™œ์šฉํ•œ ์กฐํšŒ์™€ ํŽ˜์ด์ง€๋„ค์ด์…˜, ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

24-33: N+1 ๋ฌธ์ œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ฐฐ์น˜ ๋กœ๋”ฉ ๊ตฌํ˜„ ์ ์ ˆํ•จ

์ƒํ’ˆ๊ณผ ๋ธŒ๋žœ๋“œ ์ •๋ณด๋ฅผ ํ•œ ๋ฒˆ์— ์กฐํšŒํ•˜์—ฌ N+1 ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ์„ค๊ณ„๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

16-18: JPQL LIMIT/OFFSET ๊ตฌ๋ฌธ์€ ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—์„œ ์ „์ ์œผ๋กœ ์ง€์›๋ฉ๋‹ˆ๋‹ค

Spring Boot 3.4.4๋Š” Hibernate 6.6.2.Final์„ ์‚ฌ์šฉํ•˜๋ฉฐ, Hibernate 6.0๋ถ€ํ„ฐ JPQL์—์„œ ๋„ค์ดํ‹ฐ๋ธŒํ•˜๊ฒŒ LIMIT๊ณผ OFFSET ํ‚ค์›Œ๋“œ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ•ด๋‹น ์ฟผ๋ฆฌ๋Š” ์œ ํšจํ•˜๋ฉฐ, ์ด ๋ฉ”์„œ๋“œ๋Š” RankingService.java์—์„œ ์‹ค์ œ๋กœ ์‚ฌ์šฉ ์ค‘์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/batch/WeeklyRankingJobConfig.java (3)

58-65: ๊ฐ€์ค‘์น˜ ํ•ฉ์ด 1.0์ด ์•„๋‹™๋‹ˆ๋‹ค - ์˜๋„๋œ ์„ค๊ณ„์ธ์ง€ ํ™•์ธ ํ•„์š”

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


75-86: RankingScoreProcessor ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ์‹œ @Value ํ•„๋“œ ์ฃผ์ž… ์‹œ์  ํ™•์ธ ํ•„์š”

weeklyRankingStep() ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ new RankingScoreProcessor(viewWeight, likeWeight, orderWeight)๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ, @Value ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ฃผ์ž…๋˜๋Š” ํ•„๋“œ๋“ค์ด ์ด๋ฏธ ์ดˆ๊ธฐํ™”๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Spring์˜ ๋นˆ ๋ผ์ดํ”„์‚ฌ์ดํด์ƒ @Bean ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ์‹œ์ ์—๋Š” ํ•„๋“œ ์ฃผ์ž…์ด ์™„๋ฃŒ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ์ •์ƒ ๋™์ž‘ํ•˜์ง€๋งŒ, weeklyRankingStep()์ด ๋‹ค๋ฅธ ๋นˆ์—์„œ ์ง์ ‘ ํ˜ธ์ถœ๋˜๋Š” ๊ฒฝ์šฐ ๋ฌธ์ œ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


91-114: Reader ์„ค์ •์€ ์ ์ ˆํ•˜์ง€๋งŒ, ๋นˆ ์Šค์ฝ”ํ”„ ๊ณ ๋ ค ํ•„์š”

JdbcPagingItemReader๋Š” statefulํ•˜๋ฏ€๋กœ ๊ธฐ๋ณธ singleton ์Šค์ฝ”ํ”„์—์„œ ๋™์‹œ ์‹คํ–‰ ์‹œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ private ๋ฉ”์„œ๋“œ๋กœ ๋งค๋ฒˆ ์ƒˆ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๋ฏ€๋กœ ๊ดœ์ฐฎ์ง€๋งŒ, ํ–ฅํ›„ @Bean์œผ๋กœ ๋ณ€๊ฒฝ ์‹œ @StepScope๋ฅผ ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

42-56: ๊ธฐ๊ฐ„ ํƒ€์ž…๋ณ„ ๋ผ์šฐํŒ… ๋กœ์ง์ด ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

switch ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•œ period type ๋ผ์šฐํŒ…์ด ๋ช…ํ™•ํ•˜๊ณ , ๊ฐ ๊ธฐ๊ฐ„ ํƒ€์ž…์— ๋งž๋Š” ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/test/resources/db/init-ranking-tables.sql (1)

37-46: ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

product_metrics ํ…Œ์ด๋ธ”์€ ๋ฐฐ์น˜ job์˜ ์†Œ์Šค ๋ฐ์ดํ„ฐ๋กœ์„œ ์ ํ•ฉํ•œ ์Šคํ‚ค๋งˆ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. product_id๋ฅผ PK๋กœ ์‚ฌ์šฉํ•˜๊ณ  ๋ฉ”ํŠธ๋ฆญ ์ปฌ๋Ÿผ๋“ค์˜ ๊ธฐ๋ณธ๊ฐ’์ด 0์œผ๋กœ ์„ค์ •๋œ ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

Comment on lines +143 to +160
public ExitStatus afterStep(StepExecution stepExecution) {
String targetDateParam = stepExecution.getJobParameters()
.getString("targetDate");

if (targetDateParam == null) {
log.error("[WeeklyRanking] targetDate ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค");
return ExitStatus.FAILED;
}

LocalDate targetDate = LocalDate.parse(targetDateParam);
LocalDate weekStartDate = PeriodUtils.getWeekStartDate(targetDate);
LocalDate weekEndDate = PeriodUtils.getWeekEndDate(targetDate);

log.info("[WeeklyRanking] Step ์™„๋ฃŒ - ์ฃผ๊ฐ„: {} ~ {}", weekStartDate, weekEndDate);

// 1. ๊ธฐ์กด ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์‚ญ์ œ
weeklyRankingRepository.deleteByWeekStartDate(weekStartDate);
log.info("[WeeklyRanking] ๊ธฐ์กด ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์™„๋ฃŒ: {}", weekStartDate);
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

๋™์‹œ ์‹คํ–‰ ์‹œ ๋ฐ์ดํ„ฐ ์†์‹ค ์œ„ํ—˜ - ์‚ญ์ œ ํ›„ ์ €์žฅ ์‚ฌ์ด์˜ ๊ฒฝํ•ฉ ์กฐ๊ฑด

deleteByWeekStartDate() ํ˜ธ์ถœ ํ›„ saveAll() ํ˜ธ์ถœ ์ „์— ๋‹ค๋ฅธ job ์ธ์Šคํ„ด์Šค๊ฐ€ ๊ฐ™์€ ์ฃผ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์†์‹ค๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. RunIdIncrementer๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๋™์ผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์žฌ์‹คํ–‰์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๋™์‹œ ์‹คํ–‰ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ฐฉ์•ˆ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”:

  • DB ๋ ˆ๋ฒจ์˜ ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด ์ถ”๊ฐ€ (week_start_date, rank_position)
  • ๋˜๋Š” JobParametersIncrementer ๋Œ€์‹  ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€ ๋กœ์ง ์ถ”๊ฐ€

Comment on lines +171 to +172
// 3. ์ •๋ ฌ (์ ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ)
Collections.sort(collectedItems);
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

find . -type f -name "*.java" | xargs grep -l "InMemoryRankingCollector"

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

Length of output: 373


๐Ÿ Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/application/batch/writer/InMemoryRankingCollector.java

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

Length of output: 1472


๐Ÿ Script executed:

head -n 180 ./apps/commerce-api/src/main/java/com/loopers/application/batch/WeeklyRankingJobConfig.java | tail -n 20

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

Length of output: 820


Collections.sort()๋Š” InMemoryRankingCollector์˜ ๋‚ด๋ถ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ง์ ‘ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค

InMemoryRankingCollector์˜ @Getter ์–ด๋…ธํ…Œ์ด์…˜์€ ๋‚ด๋ถ€ collectedItems ๋ฆฌ์ŠคํŠธ์— ๋Œ€ํ•œ ์ง์ ‘ ์ฐธ์กฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Collections.sort()๊ฐ€ ์ด ์ฐธ์กฐ๋ฅผ ์ˆ˜์ •ํ•˜๋ฏ€๋กœ collector์˜ ๋‚ด๋ถ€ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. getCollectedItems()์—์„œ ๋ฐฉ์–ด์  ๋ณต์‚ฌ๋ณธ์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜, ํ˜ธ์ถœ ์‹œ์ ์—์„œ new ArrayList<>(collector.getCollectedItems())๋กœ ๋ณต์‚ฌ๋ณธ์„ ์ƒ์„ฑํ•˜์„ธ์š”.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/batch/WeeklyRankingJobConfig.java
around lines 171-172, Collections.sort(collectedItems) mutates the internal list
returned by InMemoryRankingCollector#getCollectedItems because the getter
exposes the internal reference; fix by avoiding in-place modification โ€” either
change InMemoryRankingCollector#getCollectedItems to return a defensive copy
(e.g., new ArrayList<>(collectedItems) or Collections.unmodifiableList(copy))
or, at this call site, sort a copy: create a new
ArrayList<>(collector.getCollectedItems()) and call Collections.sort on that
copy so the collector's internal state is not mutated.

Comment on lines +19 to +21
@Modifying
@Transactional
void deleteByMonthYear(String monthYear);
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

rg -n "deleteByMonthYear|deleteByWeekStartDate" --type=java -B5 -A5

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

Length of output: 10779


๐Ÿ Script executed:

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

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

Length of output: 2044


๐Ÿ Script executed:

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

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

Length of output: 2812


๐Ÿ Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.java | sed -n '1,85p'

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

Length of output: 4033


๐Ÿ Script executed:

rg -n "\.deleteByMonthYear\(" --type=java -B2 -A2

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

Length of output: 767


๐Ÿ Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingJpaRepository.java

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

Length of output: 1124


๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์—์„œ @Transactional ์ œ๊ฑฐ ๊ถŒ์žฅ

@Transactional์ด ์ธํ„ฐํŽ˜์ด์Šค์™€ ๊ตฌํ˜„์ฒด ์–‘์ชฝ์— ์ค‘๋ณต์œผ๋กœ ์„ ์–ธ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. StepExecutionListener.afterStep()์—์„œ ํ˜ธ์ถœ๋˜๋Š” ๋ฉ”์„œ๋“œ์ด๋ฏ€๋กœ, ํŠธ๋žœ์žญ์…˜์€ ์„œ๋น„์Šค ๊ณ„์ธต์ด๋‚˜ ๋ฐฐ์น˜ ์„ค์ • ๋ ˆ๋ฒจ์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๋” ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์˜ @Transactional์€ ์ œ๊ฑฐํ•˜๊ณ , ํ•„์š”์‹œ ํ˜ธ์ถœ ๊ณ„์ธต์—์„œ ๊ด€๋ฆฌํ•˜์„ธ์š”.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingJpaRepository.java
around lines 19-21, remove the @Transactional annotation from the repository
method declaration (keep @Modifying) so the repository does not declare its own
transaction; let the service layer or the batch StepExecutionListener manage the
transaction. If there is an implementation or another interface duplicating
@Transactional, remove the duplicate there as well and ensure callers (service
or batch config) wrap the deleteByMonthYear(String monthYear) call in an
appropriate @Transactional boundary.

Comment on lines +14 to +18
@RestController
@RequestMapping("/api/v1/admin/batch")
@RequiredArgsConstructor
@Slf4j
public class BatchJobController {
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Search for the BatchJobController file
find . -type f -name "BatchJobController.java" | head -5

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

Length of output: 178


๐Ÿ Script executed:

#!/bin/bash
# Search for Spring Security configuration files
find . -type f -name "*Security*.java" -o -name "*Config*.java" | grep -i security | head -20

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

Length of output: 261


๐Ÿ Script executed:

#!/bin/bash
# Look for WebSecurityConfigurerAdapter or SecurityFilterChain patterns
rg -l "SecurityFilterChain|WebSecurityConfigurerAdapter|@EnableWebSecurity" --type=java | head -10

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

Length of output: 158


๐Ÿ Script executed:

#!/bin/bash
# Search for admin path security configurations
rg -n -C5 "admin|ADMIN" --type=java -g '*Security*' -g '*Config*' | head -50

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

Length of output: 70


๐Ÿ Script executed:

#!/bin/bash
# Read SecurityConfig.java to see admin path security configuration
cat -n ./apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.java

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

Length of output: 1348


๐Ÿ Script executed:

#!/bin/bash
# Also read the full BatchJobController.java to check for any security annotations
cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.java

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

Length of output: 3329


/admin/** ์—”๋“œํฌ์ธํŠธ์— ์ธ์ฆ ์„ค์ • ํ•„์š”

ํ˜„์žฌ SecurityConfig์—์„œ anyRequest().permitAll()๋กœ ๋ชจ๋“  ์š”์ฒญ์„ ํ—ˆ๊ฐ€ํ•˜๊ณ  ์žˆ์–ด, /api/v1/admin/batch ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ธ์ฆ ์—†์ด ๊ณต๊ฐœ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ์ด๋ฏ€๋กœ ์ ์ ˆํ•œ ์ธ์ฆ/์ธ๊ฐ€ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

SecurityConfig์˜ authorizeHttpRequests ๋ฉ”์„œ๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์—ฌ /api/v1/admin/** ๊ฒฝ๋กœ์— authenticated() ๋˜๋Š” ํŠน์ • ๊ถŒํ•œ ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.java
around lines 14 to 18, the controller is mapped to /api/v1/admin/batch and is
currently exposed because SecurityConfig uses anyRequest().permitAll(); update
SecurityConfig.authorizeHttpRequests() to require authentication for the admin
path (e.g., add an antMatcher or requestMatchers for "/api/v1/admin/**" with
.authenticated() or .hasRole("ADMIN") before the anyRequest rule), ensure order
of matchers places the admin rule above the permissive catch-all, and keep
CSRF/session settings consistent with API auth.

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