-
Notifications
You must be signed in to change notification settings - Fork 35
[volume-10] Collect, Stack, Zip #231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Round 10에서 작업한 주간/월간 랭킹 배치 Job을 upstream 배치 템플릿 구조에 통합합니다. 변경 사항: - WeeklyRankingJobConfig, MonthlyRankingJobConfig 추가 - ProductMetricsAggregateReader 추가 (집계 쿼리) - RankingScoreProcessor 추가 (점수 계산 로직) - WeeklyRankingWriter, MonthlyRankingWriter 추가 - 도메인 엔티티 추가: WeeklyProductRank, MonthlyProductRank - 리포지토리 추가: WeeklyRankRepository, MonthlyRankRepository - JpaConfig 수정: com.loopers.domain 패키지 스캔 추가 - CommerceBatchApplicationTest 수정: Job 자동 실행 비활성화 Upstream의 Listener 구조(JobListener, StepMonitorListener, ChunkListener)를 유지하면서 Round 10 랭킹 Job을 추가했습니다.
변경사항 분석Walkthrough새로운 Changes
Sequence Diagram(s)sequenceDiagram
participant Scheduler as Batch Scheduler
participant Job as Ranking Job
participant Reader as ProductMetricsAggregateReader
participant DB as Database
participant Processor as RankingScoreProcessor
participant Writer as Ranking Writer
participant RankDB as Ranking DB
Scheduler->>Job: Launch Weekly/Monthly Job
Job->>Reader: Initialize Reader (period, topN)
Reader->>DB: Execute aggregation query<br/>(metrics by period)
DB-->>Reader: ProductRankingAggregation list
Reader->>Reader: Sort & assign rankPosition
loop Chunk Processing (size=100)
Reader->>Processor: Pass aggregated metrics
Processor->>Processor: Calculate weighted score<br/>(views × 0.1 + likes × 0.2<br/>+ orders × 0.6 × log10(sales))
Processor->>Processor: Build Weekly/MonthlyProductRank
Processor-->>Writer: Ranking entity
Writer->>RankDB: Delete existing<br/>for period
Writer->>RankDB: SaveAll new rankings
Writer-->>Job: Chunk completed
end
Job->>Job: Job Listener logs<br/>start/end time & duration
Job-->>Scheduler: Job completed (COMPLETED/FAILED)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 14
🧹 Nitpick comments (23)
.docs/design/04-erd.md (1)
705-705: 섹션 번호 누락: Section 8 누락됨Table of Contents 관점에서 섹션 7(order_items)과 섹션 9(coupons) 사이에 섹션 8이 누락되어 있습니다. 문서의 논리적 흐름과 가독성을 위해 섹션 번호를 재정렬하거나, 섹션 8 제목을 추가하는 것이 좋습니다.
Also applies to: 749-749, 791-791, 830-830
apps/commerce-batch/src/main/resources/application.yml (1)
34-50: 비어있는 프로필 섹션을 정리하는 것을 고려하세요.dev/qa/prd 프로필 섹션이 비어있어 현재는 아무런 설정도 제공하지 않습니다. 향후 환경별 설정 추가를 위한 플레이스홀더로 의도된 것이라면 주석으로 설명을 추가하거나, 당장 필요하지 않다면 제거하여 설정 파일을 간소화할 수 있습니다.
🔎 불필요한 프로필 섹션 제거 제안
---- -spring: - config: - activate: - on-profile: dev - ---- -spring: - config: - activate: - on-profile: qa - ---- -spring: - config: - activate: - on-profile: prd - springdoc: api-docs: enabled: falseapps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
39-39: TODO 주석 구현을 도와드릴 수 있습니다.Slack 또는 다른 모니터링 채널로 알림을 전송하는 기능이 필요하신가요? 에러 발생 시 즉각적인 알림은 배치 작업 모니터링에 중요합니다.
이 기능을 구현하는 코드를 생성하거나 새 이슈를 열어드릴까요?
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
7-11: 테스트 커버리지 확장을 고려해보세요.현재 테스트는 배치 자동 실행이 비활성화된 상태에서 애플리케이션 컨텍스트가 로드되는지만 확인합니다.
DemoJobE2ETest처럼 실제 배치 작업 실행 및 결과 검증을 추가하면 더 견고한 테스트가 될 수 있습니다.apps/commerce-api/src/main/java/com/loopers/domain/rank/MonthlyProductRank.java (2)
33-34: 백틱(backtick)이 필요한지 확인하세요.
year_month컬럼명에 백틱이 사용되었지만,year_month는 MySQL 예약어가 아닙니다.WeeklyProductRank의year_week필드는 백틱 없이 정의되어 있어 일관성이 없습니다.특별한 이유가 없다면 백틱을 제거하는 것이 좋습니다.
🔎 백틱 제거 제안
- @Column(name = "`year_month`", nullable = false, length = 7) + @Column(name = "year_month", nullable = false, length = 7) private String yearMonth;
24-59: 빌더 패턴과 생명주기 콜백 추가를 고려하세요.배치 작업에서 이 엔티티를 생성할 때 빌더 패턴이 유용할 수 있습니다. 또한
@PreUpdate콜백을 추가하여updatedAt필드를 자동으로 갱신하는 것이 좋습니다.
WeeklyProductRank와 일관성을 유지하기 위해 동일한 패턴을 적용하세요.🔎 빌더와 생명주기 콜백 추가 제안
+import jakarta.persistence.PreUpdate; +import lombok.Builder; + @Entity @Table(name = "mv_product_rank_monthly") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class MonthlyProductRank { // ... 필드 선언 ... + + @Builder + public MonthlyProductRank( + Long productId, + String yearMonth, + Integer rankPosition, + Double totalScore, + Integer likeCount, + Integer viewCount, + Integer orderCount, + BigDecimal salesAmount + ) { + this.productId = productId; + this.yearMonth = yearMonth; + this.rankPosition = rankPosition; + this.totalScore = totalScore; + this.likeCount = likeCount; + this.viewCount = viewCount; + this.orderCount = orderCount; + this.salesAmount = salesAmount; + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } }apps/commerce-batch/src/main/java/com/loopers/batch/writer/WeeklyRankingWriter.java (1)
35-50: 청크 내 다른 yearWeek 값이 있을 경우 데이터 불일치 가능성현재 구현은 첫 번째 아이템의
yearWeek만 사용하여 삭제를 수행합니다. 만약 청크 내에 다른yearWeek값을 가진 아이템이 있다면, 해당 주차의 기존 데이터는 삭제되지 않아 데이터 중복이 발생할 수 있습니다.현재 설정(CHUNK_SIZE=100, TOP_N=100)에서는 모든 데이터가 단일 청크로 처리되어 문제가 없지만, 향후 설정 변경 시 문제가 될 수 있습니다.
🔎 방어적 검증 추가 제안
@Override @Transactional public void write(Chunk<? extends WeeklyProductRank> chunk) { if (chunk.isEmpty()) { log.warn("Empty chunk received, skipping write operation"); return; } String yearWeek = chunk.getItems().get(0).getYearWeek(); + + // 청크 내 모든 아이템이 동일한 yearWeek인지 검증 + boolean allSameYearWeek = chunk.getItems().stream() + .allMatch(item -> yearWeek.equals(item.getYearWeek())); + if (!allSameYearWeek) { + throw new IllegalStateException("Chunk contains items with different yearWeek values"); + } + log.info("Writing weekly rankings: yearWeek={}, count={}", yearWeek, chunk.size());apps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyRankRepository.java (1)
31-33: @Modifying에 clearAutomatically 속성 누락벌크 삭제 쿼리 실행 후 영속성 컨텍스트에 삭제된 엔티티가 남아있을 수 있습니다.
saveAll호출 시 문제가 발생하지 않도록clearAutomatically = true를 추가하는 것을 권장합니다.🔎 수정 제안
- @Modifying + @Modifying(clearAutomatically = true) @Query("DELETE FROM WeeklyProductRank w WHERE w.yearWeek = :yearWeek") void deleteByYearWeek(@Param("yearWeek") String yearWeek);apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (2)
35-38: 비어있는 @beforeeach 메서드 제거 권장
beforeEach()메서드가 비어있습니다. 현재 사용되지 않으므로 제거하거나, 향후 사용 계획이 있다면 TODO 주석을 추가하세요.
40-54: 테스트 메서드명이 실제 테스트 시나리오와 불일치
shouldNotSaveCategories_whenApiError는 API 에러 상황을 암시하지만, 실제로는requestDate파라미터 누락 시나리오를 테스트합니다. 테스트 의도를 명확히 반영하도록 메서드명을 변경하세요.🔎 메서드명 수정 제안
- @DisplayName("jobParameter 중 requestDate 인자가 주어지지 않았을 때, demoJob 배치는 실패한다.") - @Test - void shouldNotSaveCategories_whenApiError() throws Exception { + @DisplayName("jobParameter 중 requestDate 인자가 주어지지 않았을 때, demoJob 배치는 실패한다.") + @Test + void shouldFailJob_whenRequestDateParameterIsMissing() throws Exception {apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
24-27: ExecutionContext에서 값 조회 시 키 존재 여부 확인 필요
getLong("startTime")은 키가 존재하지 않으면 예외를 발생시킵니다.beforeJob이 실패하거나 호출되지 않은 경우를 방어적으로 처리하는 것이 좋습니다.🔎 방어적 코드 제안
@AfterJob void afterJob(JobExecution jobExecution) { - var startTime = jobExecution.getExecutionContext().getLong("startTime"); + var executionContext = jobExecution.getExecutionContext(); + if (!executionContext.containsKey("startTime")) { + log.warn("Start time not found in execution context"); + return; + } + var startTime = executionContext.getLong("startTime"); var endTime = System.currentTimeMillis();apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
24-29: System.out.println 대신 로거 사용 권장 및 예외 타입 개선데모 목적의 코드이지만, 일관성을 위해
System.out.println대신 Slf4j 로거를 사용하는 것이 좋습니다. 또한RuntimeException보다 구체적인 예외 타입을 사용하세요.🔎 수정 제안
+import lombok.extern.slf4j.Slf4j; + +@Slf4j @StepScope @ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME) @RequiredArgsConstructor @Component public class DemoTasklet implements Tasklet { @Value("#{jobParameters['requestDate']}") private String requestDate; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { if (requestDate == null) { - throw new RuntimeException("requestDate is null"); + throw new IllegalArgumentException("Required job parameter 'requestDate' is missing"); } - System.out.println("Demo Tasklet 실행 (실행 일자 : " + requestDate + ")"); + log.info("Demo Tasklet 실행 (실행 일자 : {})", requestDate); Thread.sleep(1000); - System.out.println("Demo Tasklet 작업 완료"); + log.info("Demo Tasklet 작업 완료"); return RepeatStatus.FINISHED; } }apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java (1)
40-47: 메서드명과 상수명 불일치
categorySyncStep()메서드명이 실제 용도(데모 태스크)와 맞지 않으며,STEP_DEMO_SIMPLE_TASK_NAME상수와도 일치하지 않습니다. 명확성을 위해 메서드명을demoSimpleTaskStep()등으로 변경하는 것을 권장합니다.🔎 수정 제안
@JobScope @Bean(STEP_DEMO_SIMPLE_TASK_NAME) - public Step categorySyncStep() { + public Step demoSimpleTaskStep() { return new StepBuilder(STEP_DEMO_SIMPLE_TASK_NAME, jobRepository) .tasklet(demoTasklet, new ResourcelessTransactionManager()) .listener(stepMonitorListener) .build(); }그리고
demoJob()메서드에서도 참조를 업데이트하세요:@Bean(JOB_NAME) public Job demoJob() { return new JobBuilder(JOB_NAME, jobRepository) .incrementer(new RunIdIncrementer()) - .start(categorySyncStep()) + .start(demoSimpleTaskStep()) .listener(jobListener) .build(); }apps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyProductRank.java (1)
59-60:totalScore필드 타입에 대한 고려사항랭킹 점수에
Double타입을 사용하고 있습니다. 현재 랭킹 비교 용도로는 충분하지만, 동점 처리 시 부동소수점 비교의 정밀도 문제가 발생할 수 있습니다. 필요 시BigDecimal로 변경을 고려해 주세요.apps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyRankRepository.java (1)
31-33:@Modifying어노테이션에clearAutomatically = true추가 권장삭제 쿼리 실행 후 동일 트랜잭션 내에서 새 엔티티를 저장할 때, 영속성 컨텍스트에 오래된 엔티티가 남아있을 수 있습니다. Writer에서
deleteByYearMonth호출 후saveAll을 수행하므로, 캐시 정리를 명시하는 것이 안전합니다.🔎 수정 제안
- @Modifying + @Modifying(clearAutomatically = true) @Query("DELETE FROM MonthlyProductRank m WHERE m.yearMonth = :yearMonth") void deleteByYearMonth(@Param("yearMonth") String yearMonth);apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricsAggregateReader.java (1)
86-90: 점수 계산 로직 중복 주의SQL 쿼리의 점수 계산 공식이
RankingScoreProcessor의calculateScore메서드와 동일한 가중치를 사용하고 있습니다. 한쪽만 수정될 경우 정렬 순서와 저장된 점수 간 불일치가 발생할 수 있습니다. 가중치 상수를 공통 위치에서 관리하거나, 이 중복에 대한 주석을 추가해 주세요.apps/commerce-batch/src/main/java/com/loopers/batch/config/MonthlyRankingJobConfig.java (1)
83-98: Step 내에서 @StepScope 빈 메서드 직접 호출 문제
monthlyRankingStep에서monthlyMetricsReader(yearMonth),monthlyRankingProcessor(yearMonth),monthlyRankingWriter()를 직접 호출하고 있습니다. 이 메서드들은@Bean @StepScope로 선언되어 있지만, 직접 호출하면 Spring 프록시를 우회하여 SpEL(#{jobParameters['yearMonth']})이 해석되지 않습니다.현재 구조에서는
@JobScopestep에서 이미yearMonth를 주입받아 직접 전달하므로 동작에는 문제가 없지만, 이는@StepScope빈의 의도된 사용 방식이 아닙니다.🔎 권장 수정안: 빈 주입 방식으로 변경
@Bean @JobScope public Step monthlyRankingStep( JobRepository jobRepository, PlatformTransactionManager transactionManager, - @Value("#{jobParameters['yearMonth']}") String yearMonth + @Value("#{jobParameters['yearMonth']}") String yearMonth, + ItemReader<ProductRankingAggregation> monthlyMetricsReader, + ItemProcessor<ProductRankingAggregation, MonthlyProductRank> monthlyRankingProcessor, + ItemWriter<MonthlyProductRank> monthlyRankingWriter ) { log.info("Initializing monthly ranking step: yearMonth={}", yearMonth); return new StepBuilder("monthlyRankingStep", jobRepository) .<ProductRankingAggregation, MonthlyProductRank>chunk(CHUNK_SIZE, transactionManager) - .reader(monthlyMetricsReader(yearMonth)) - .processor(monthlyRankingProcessor(yearMonth)) - .writer(monthlyRankingWriter()) + .reader(monthlyMetricsReader) + .processor(monthlyRankingProcessor) + .writer(monthlyRankingWriter) .build(); }참고:
WeeklyRankingJobConfig에서도 동일한 패턴이 사용되고 있어 일관성은 유지되고 있습니다.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (3)
61-64: 인라인 패키지 참조 대신 import 사용 권장
java.time.LocalDate와java.time.format.DateTimeFormatter가 메서드 내에서 전체 패키지 경로로 참조되고 있습니다. 파일 상단에 import를 추가하면 가독성이 향상됩니다.🔎 권장 수정안
파일 상단에 import 추가:
import java.time.LocalDate; import java.time.format.DateTimeFormatter;메서드 수정:
public List<RankingProductInfo> getTodayRanking(int page, int size) { - String today = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")); + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); return getDailyRanking(today, page, size); }
74-78: 페이지네이션 파라미터 유효성 검증 누락
page가 1-based로 문서화되어 있지만,page < 1또는size <= 0인 경우PageRequest.of(page - 1, size)에서IllegalArgumentException이 발생합니다. 방어적 검증을 추가하는 것이 좋습니다.🔎 권장 수정안
public List<RankingProductInfo> getWeeklyRanking(String yearWeek, int page, int size) { + if (page < 1) { + throw new IllegalArgumentException("Page must be 1 or greater"); + } + if (size < 1) { + throw new IllegalArgumentException("Size must be 1 or greater"); + } PageRequest pageRequest = PageRequest.of(page - 1, size);
getMonthlyRanking과getDailyRanking에도 동일한 검증을 적용하세요.
84-92: 주간/월간 랭킹 매핑 로직 중복
getWeeklyRanking과getMonthlyRanking에서 엔티티를RankingItem으로 변환하는 로직이 거의 동일합니다. 현재 수준의 중복은 수용 가능하지만, 향후 유지보수를 위해 공통 인터페이스(예:RankEntry)를 도입하거나 매퍼를 사용하는 것을 고려할 수 있습니다.Also applies to: 113-121
apps/commerce-batch/src/main/java/com/loopers/batch/processor/RankingScoreProcessor.java (2)
31-31:Object반환 타입으로 인한 타입 안전성 저하
ItemProcessor<ProductRankingAggregation, Object>로 선언되어 타입 안전성이 떨어집니다. 호출하는 쪽에서 캐스팅이 필요합니다.대안으로 제네릭을 활용한 팩토리 패턴이나 별도의
WeeklyRankingProcessor,MonthlyRankingProcessor를 고려할 수 있습니다. 다만 현재 구현도 기능적으로는 문제없이 동작합니다.
33-35: 점수 계산 공식이 Reader SQL과 Processor Java에 중복 정의됨
ProductMetricsAggregateReader의 SQL 쿼리와RankingScoreProcessor.calculateScore()에서 동일한 가중치(0.1, 0.2, 0.6)와 공식이 사용됩니다. 가중치 변경 시 두 곳을 모두 수정해야 하므로 불일치 위험이 있습니다.🔎 권장 개선안
가중치를 공유 상수 클래스로 추출하거나, Reader에서 계산된 점수를
ProductRankingAggregation에 포함시켜 Processor에서 재사용하는 것을 고려하세요:// 공유 상수 클래스 예시 public final class RankingWeights { public static final double VIEW = 0.1; public static final double LIKE = 0.2; public static final double ORDER = 0.6; }Also applies to: 92-96
apps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java (1)
74-75:@PrePersist콜백 사용 권장
createdAt이 생성자에서 설정되고 있습니다. JPA 라이프사이클 관리를 위해@PrePersist콜백을 사용하는 것이 더 관용적입니다.🔎 권장 수정안
+ @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + @PreUpdate protected void onUpdate() { this.updatedAt = LocalDateTime.now(); }생성자에서 timestamp 초기화 제거:
@Builder public WeeklyProductRank(...) { ... - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); }참고:
MonthlyProductRank에서도 동일한 패턴이 사용되고 있으므로, 변경 시 두 엔티티 모두 수정하세요.Also applies to: 99-100
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (35)
.docs/design/03-class-diagram.md.docs/design/04-erd.mdREADME.mdapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.javaapps/commerce-api/src/main/java/com/loopers/domain/rank/MonthlyProductRank.javaapps/commerce-api/src/main/java/com/loopers/domain/rank/WeeklyProductRank.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/rank/MonthlyRankJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/rank/WeeklyRankJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingApi.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingResponse.javaapps/commerce-batch/build.gradle.ktsapps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.javaapps/commerce-batch/src/main/java/com/loopers/batch/config/BatchConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/config/MonthlyRankingJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/config/WeeklyRankingJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/processor/RankingScoreProcessor.javaapps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricsAggregateReader.javaapps/commerce-batch/src/main/java/com/loopers/batch/writer/MonthlyRankingWriter.javaapps/commerce-batch/src/main/java/com/loopers/batch/writer/WeeklyRankingWriter.javaapps/commerce-batch/src/main/java/com/loopers/domain/dto/ProductRankingAggregation.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyProductRank.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyRankRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyProductRank.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyRankRepository.javaapps/commerce-batch/src/main/resources/application.ymlapps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.javaapps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.javadocker/init-db.sqlmodules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.javasettings.gradle.kts
🧰 Additional context used
🧠 Learnings (8)
📚 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/rank/WeeklyRankJpaRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyRankRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyRankRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/rank/MonthlyRankJpaRepository.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-batch/src/main/java/com/loopers/domain/dto/ProductRankingAggregation.java.docs/design/04-erd.mdapps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyProductRank.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java
📚 Learning: 2025-11-21T03:38:07.494Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 99
File: apps/commerce-api/src/main/resources/application.yml:26-30
Timestamp: 2025-11-21T03:38:07.494Z
Learning: The batch job implementation for likeCount synchronization in apps/commerce-api is temporary and intended for development environment only. It will be replaced with Event-Driven Architecture (EDA) before production deployment, so production-level configuration concerns (like profile-based initialize-schema settings) are not required.
Applied to files:
apps/commerce-batch/src/main/resources/application.ymlapps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java
📚 Learning: 2025-11-15T08:01:46.868Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 29
File: docs/week2/03.1-state-diagram.md:80-105
Timestamp: 2025-11-15T08:01:46.868Z
Learning: MySQL/PostgreSQL에서 `SELECT ... WHERE id IN (...) FOR UPDATE`는 애플리케이션이 제공한 ID 순서를 무시하고 인덱스 스캔 순서(일반적으로 PK 오름차순)로 비관적 락을 획득한다. 따라서 교착상태 방지를 위해 애플리케이션 레벨에서 ID를 정렬하는 것은 효과가 없다. 실제 교착상태 위험은 서로 다른 리소스 타입(예: Product와 PointAccount)을 서로 다른 순서로 락킹할 때 발생하므로, 리소스 타입 간 일관된 락킹 순서를 유지하는 것이 중요하다.
Applied to files:
.docs/design/04-erd.md
📚 Learning: 2026-01-01T14:12:00.842Z
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:00.842Z
Learning: In Spring Batch 6.x, ChunkListener API changed from using ChunkContext to Chunk<T> parameters. The correct method signatures are: beforeChunk(Chunk<T> chunk), afterChunk(Chunk<T> chunk), and onChunkError(Exception exception, Chunk<T> chunk). The import path is org.springframework.batch.core.listener.ChunkListener.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
📚 Learning: 2026-01-01T14:12:00.557Z
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:00.557Z
Learning: Spring Batch 6.x에서 JobExecution은 org.springframework.batch.core.job 패키지에, JobExecutionListener는 org.springframework.batch.core.listener 패키지에 위치합니다. Spring Batch 5.x와 달리 하위 패키지를 사용합니다.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
📚 Learning: 2025-12-22T16:33:50.678Z
Learnt from: jikimee64
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 71
File: apps/commerce-streamer/src/main/kotlin/com/loopers/application/ranking/RankingFacade.kt:167-173
Timestamp: 2025-12-22T16:33:50.678Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository, using system default timezone (ZonedDateTime.now() without explicit ZoneId) in date key calculations is an intentional design choice. The deployment ensures all instances share the same timezone configuration.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.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:
modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java
🧬 Code graph analysis (15)
apps/commerce-batch/src/main/java/com/loopers/batch/writer/MonthlyRankingWriter.java (5)
apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricsAggregateReader.java (1)
Slf4j(31-180)apps/commerce-batch/src/main/java/com/loopers/batch/config/MonthlyRankingJobConfig.java (1)
Slf4j(47-139)apps/commerce-batch/src/main/java/com/loopers/batch/config/WeeklyRankingJobConfig.java (1)
Slf4j(47-139)apps/commerce-batch/src/main/java/com/loopers/batch/processor/RankingScoreProcessor.java (1)
Slf4j(30-98)apps/commerce-batch/src/main/java/com/loopers/batch/writer/WeeklyRankingWriter.java (1)
Slf4j(18-51)
apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricsAggregateReader.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/config/MonthlyRankingJobConfig.java (1)
Slf4j(47-139)apps/commerce-batch/src/main/java/com/loopers/batch/config/WeeklyRankingJobConfig.java (1)
Slf4j(47-139)apps/commerce-batch/src/main/java/com/loopers/batch/processor/RankingScoreProcessor.java (1)
Slf4j(30-98)
apps/commerce-api/src/main/java/com/loopers/domain/rank/MonthlyProductRank.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java (1)
Entity(20-59)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java (1)
ConditionalOnProperty(19-48)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
Slf4j(13-52)apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
Slf4j(9-21)
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (1)
modules/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt (1)
main(22-24)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.java (1)
Service(19-100)
apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
SpringBootTest(7-12)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
Slf4j(13-52)apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
Slf4j(13-44)
apps/commerce-batch/src/main/java/com/loopers/batch/writer/WeeklyRankingWriter.java (4)
apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricsAggregateReader.java (1)
Slf4j(31-180)apps/commerce-batch/src/main/java/com/loopers/batch/config/MonthlyRankingJobConfig.java (1)
Slf4j(47-139)apps/commerce-batch/src/main/java/com/loopers/batch/config/WeeklyRankingJobConfig.java (1)
Slf4j(47-139)apps/commerce-batch/src/main/java/com/loopers/batch/writer/MonthlyRankingWriter.java (1)
Slf4j(18-51)
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
SpringBootTest(21-74)
apps/commerce-batch/src/main/java/com/loopers/batch/config/BatchConfig.java (1)
modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java (1)
Configuration(8-13)
apps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java (2)
apps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyProductRank.java (1)
Entity(27-107)apps/commerce-batch/src/main/java/com/loopers/domain/dto/ProductRankingAggregation.java (1)
Getter(13-46)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
Slf4j(13-44)apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
Slf4j(9-21)
apps/commerce-batch/src/main/java/com/loopers/batch/config/MonthlyRankingJobConfig.java (5)
apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricsAggregateReader.java (1)
Slf4j(31-180)apps/commerce-batch/src/main/java/com/loopers/batch/config/WeeklyRankingJobConfig.java (1)
Slf4j(47-139)apps/commerce-batch/src/main/java/com/loopers/batch/processor/RankingScoreProcessor.java (1)
Slf4j(30-98)apps/commerce-batch/src/main/java/com/loopers/batch/writer/MonthlyRankingWriter.java (1)
Slf4j(18-51)apps/commerce-batch/src/main/java/com/loopers/batch/writer/WeeklyRankingWriter.java (1)
Slf4j(18-51)
🔇 Additional comments (34)
.docs/design/04-erd.md (4)
18-26: ER 다이어그램 및 Materialized View 설계 적절새로운 관계 정의(주문/결제/좋아요 이벤트 → event_outbox, 이벤트 인박스 → 카탈로그/주문 이벤트)가 이벤트 기반 아키텍처 패턴과 일치합니다. 주간/월간 랭킹 Materialized View의 UNIQUE 제약조건(uk_product_week, uk_product_month)과 인덱싱 전략이 쿼리 성능을 고려한 설계로 보입니다.
Also applies to: 143-169, 587-648
833-850: 동시성 제어 전략 업데이트 적절기존 version 필드(@Version) 기반 낙관적 락에 더해, 비관적 락 획득 순서(UserCoupon → Product → Point)를 명시하여 데드락 방지 전략을 명확히 했습니다. 제공된 learnings의 "리소스 타입 간 일관된 락킹 순서 유지" 원칙과 일치합니다.
132-141: PRIMARY KEY가 유일성 제약조건을 이미 보장하므로 추가 UNIQUE 제약조건 불필요
product_metrics테이블의product_id는 PRIMARY KEY로 정의되어 있으며, 이는 데이터베이스에서 자동으로 유일성(UNIQUE) 제약조건을 보장합니다. 따라서 각 상품당 정확히 하나의 메트릭 레코드는 이미 보장되고 있으므로, 별도의 UNIQUE 제약조건 추가는 불필요합니다.
171-183: ERD 문서의 payments 테이블 명세가 실제 엔티티 정의와 불일치ERD (673-705)의 payments 테이블 정의가 Payment.java의 실제 구현과 맞지 않습니다:
order_id: ERD는 varchar(20)으로 표시하지만 엔티티는 length 50amount: ERD는 decimal(19,2)로 표시하지만 엔티티는 decimal(19,0)transaction_key: ERD는 varchar(100)으로 표시하지만 엔티티는 length 50Payment의 orderId는 Order.id와의 직접 외래키 관계가 없도록 의도된 설계입니다(이벤트 기반 아키텍처의 느슨한 결합). 데이터 무결성 문제가 아니므로 설계 선택을 문서에 명시하고 ERD의 필드 정의를 실제 엔티티와 일치시켜 주세요.
Likely an incorrect or invalid review comment.
apps/commerce-batch/src/main/resources/application.yml (4)
1-13: 배치 애플리케이션 설정이 적절합니다.
web-application-type: none설정은 Spring Batch 애플리케이션에 적합하며, 필요한 설정 파일(JPA, Redis, 로깅, 모니터링)을 올바르게 import하고 있습니다.
20-32: 헬스체크 및 local/test 프로필 설정이 적절합니다.배치 애플리케이션에서는 기본 헬스 인디케이터가 불필요하므로 비활성화가 적절하며, local/test 환경에서
initialize-schema: always설정은 개발/테스트 시 스키마 자동 생성을 위한 표준 패턴입니다.
52-54: Springdoc 비활성화가 적절합니다.배치 애플리케이션에서는 REST API 문서가 필요하지 않으므로 API docs를 비활성화하는 것이 적절합니다.
14-18: 배치 모듈의 임시 특성을 고려한 스키마 설정 확인현재 Spring Batch 메타데이터 테이블이 docker/init-db.sql에 포함되지 않아 있으며, 마이그레이션 도구(Flyway/Liquibase)도 구성되지 않은 상태입니다. 기본 설정의
initialize-schema: never는 dev/qa/prd 환경에서 배치 메타데이터 테이블 자동 생성을 방지합니다.다만 이 배치 구현은 임시 특성의 개발 환경 전용 솔루션이며 추후 Event-Driven Architecture로 대체될 예정이므로, 현재 구성이 의도된 범위에서는 적절합니다. 향후 프로덕션 수준의 배치 작업 추가 시에는 다음을 검토해야 합니다:
- Flyway/Liquibase 등 마이그레이션 도구를 통한 Spring Batch 메타데이터 테이블 관리
- dev/qa/prd 환경에서 배치 실행이 필요한 경우 스키마 초기화 전략 수립
또한
job.name: ${job.name:NONE}설정은 배치 작업 실행 시 외부에서 job.name 파라미터를 전달해야 하므로, 배포 문서에 이 요구사항을 명시하세요.settings.gradle.kts (1)
3-14: LGTM!새로운
commerce-batch모듈이 멀티모듈 빌드에 올바르게 추가되었습니다.apps/commerce-batch/src/main/java/com/loopers/batch/config/BatchConfig.java (1)
11-13: 현재는 비어있지만 향후 확장을 위한 준비 완료이 설정 클래스는 현재 비어있지만 Spring Boot 3.x의 자동 설정을 활용하므로 문제없습니다. 향후 공통 배치 빈(예: JobRepository 커스터마이징, TaskExecutor 설정 등)이 필요할 때 이 클래스에 추가하면 됩니다.
apps/commerce-batch/build.gradle.kts (1)
1-21: LGTM!배치 모듈에 필요한 의존성이 적절히 구성되었습니다. Spring Batch, JPA, Querydsl, 그리고 필요한 모듈 의존성들이 모두 포함되어 있습니다.
apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/MonthlyRankJpaRepository.java (1)
20-20: 쿼리 메서드 네이밍과 엔티티 필드가 올바르게 일치
MonthlyProductRank엔티티에yearMonth(String)와rankPosition(Integer) 필드가 정확히 존재하며,findByYearMonthOrderByRankPositionAsc메서드는 Spring Data JPA의 쿼리 메서드 네이밍 규칙을 올바르게 따르고 있습니다. 필드명과 데이터베이스 컬럼명도 정확하게 매핑되어 있습니다.apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
14-15: 검토 의견 불필요 - 현재 코드가 올바름프로젝트는 Spring Boot 3.4.4를 사용하며, 이는 Spring Batch 5.2.x를 관리합니다. Spring Batch 5.x에서는
ChunkContext가 올바른 API이므로 현재 구현이 정확합니다. Spring Batch 6.x로의 마이그레이션이 계획될 때만Chunk<T>파라미터로의 변경이 필요합니다.Likely an incorrect or invalid review comment.
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (2)
13-17: 타임존 설정이 올바르게 구현되었습니다.Asia/Seoul 타임존을 설정하는 것은 프로젝트의 의도된 설계입니다. 모든 인스턴스가 동일한 타임존 설정을 공유하여 날짜 키 계산의 일관성을 보장합니다.
Based on learnings, the timezone setting is an intentional design choice across the project.
19-22: 다른 Java 애플리케이션과의 일관성 확인 필요
CommerceBatchApplication은SpringApplication.exit(SpringApplication.run(...))패턴을 사용하고 있지만,CommerceStreamerApplication과CommerceApiApplication은 단순히SpringApplication.run(...)을 사용합니다.Batch 애플리케이션의 경우 스케줄러에 종료 코드를 반환해야 한다면 이 패턴이 필요할 수 있지만, 프로젝트 전반의 설계 의도와 일관성을 확인해주세요.
.docs/design/03-class-diagram.md (2)
165-261: 도메인 모델 확장이 잘 문서화되었습니다.새로운 엔티티들(Payment, EventOutbox, WeeklyProductRank, MonthlyProductRank, ProductMetrics)과 그들의 관계가 명확하게 정의되어 있습니다. Materialized View 패턴과 Transactional Outbox 패턴의 적용이 적절하게 표현되었습니다.
954-1411: 배치 아키텍처 문서화가 우수합니다.주간/월간 랭킹 배치 작업의 설계가 상세하게 문서화되어 있습니다:
- Materialized View 패턴의 목적과 이점
- Chunk-Oriented Processing의 책임 분리
- Reader/Processor/Writer의 역할 구분
README.md (1)
30-30: 문서 업데이트가 올바릅니다.새로운
commerce-batch모듈이 프로젝트 구조에 정확하게 반영되었습니다.modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java (1)
11-11:com.loopers.domain패키지 추가는 필요하지 않습니다.
WeeklyRankJpaRepository와MonthlyRankJpaRepository는 올바르게com.loopers.infrastructure.rank패키지에 위치하고 있습니다. 도메인 패키지는 리포지토리 인터페이스(BrandRepository, OrderRepository 등)만 포함하며, JPA 구현체는 인프라스트럭처 계층에만 존재합니다. 따라서 현재의 아키텍처는 클린 아키텍처 원칙을 잘 따르고 있습니다.
@EnableJpaRepositories에서com.loopers.domain을 제거해도 무방합니다. 도메인 패키지에는 Spring Data JPA 스캔 대상인 JpaRepository 구현체가 없으므로, 이를 포함하는 것은 실질적인 영향이 없습니다.Likely an incorrect or invalid review comment.
docker/init-db.sql (2)
64-80: LGTM! 주간 랭킹 테이블 설계가 적절합니다.Materialized View 패턴에 적합한 테이블 구조입니다:
uk_product_week유니크 제약으로 중복 방지idx_year_week_rank와idx_year_week_score인덱스로 조회 성능 최적화- ISO 주차 형식(
YYYY-Wnn)을 위한 VARCHAR(10) 길이 적절
83-99: LGTM! 월간 랭킹 테이블 설계가 적절합니다.주간 랭킹 테이블과 일관된 구조로 설계되었으며, 월간 집계에 필요한 모든 요소가 포함되어 있습니다.
apps/commerce-batch/src/main/java/com/loopers/domain/dto/ProductRankingAggregation.java (1)
13-46: LGTM! DTO 구조가 명확하고 적절합니다.배치 프로세스에서 집계된 랭킹 데이터를 전달하기에 적합한 구조입니다:
- 필요한 모든 메트릭 필드 포함
- Lombok을 통한 간결한 구현
- 명확한 Javadoc 문서화
apps/commerce-batch/src/main/java/com/loopers/batch/writer/MonthlyRankingWriter.java (1)
35-50: LGTM - WeeklyRankingWriter와 일관된 구현
WeeklyRankingWriter와 동일한 패턴으로 구현되어 있습니다. 삭제-삽입 전략이 트랜잭션으로 올바르게 래핑되어 있습니다.참고:
WeeklyRankingWriter에서 언급한 청크 내 다른 기간 값에 대한 검증 로직을 추가한다면 이 클래스에도 동일하게 적용하세요.apps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyProductRank.java (2)
19-41: 엔티티 구조가 잘 설계되었습니다.Materialized View 패턴에 적합한 인덱스 설계와 복합 유니크 제약조건이 잘 구성되어 있습니다.
year_month컬럼에 대한 백틱 처리도 예약어 충돌을 방지하기 위한 좋은 접근입니다.
80-106: 생성자에서 타임스탬프 초기화 - 적절합니다.빌더 패턴을 통한 생성자에서
createdAt과updatedAt을 초기화하고,@PreUpdate로 수정 시간을 갱신하는 방식이 잘 구현되어 있습니다.apps/commerce-batch/src/main/java/com/loopers/batch/config/WeeklyRankingJobConfig.java (3)
65-98: Job 및 Step 구성이 잘 구현되어 있습니다.Chunk 지향 처리 패턴이 적절하게 구성되어 있으며,
@JobScope와@StepScope를 올바르게 사용하여 Job 파라미터 주입을 처리하고 있습니다.MonthlyRankingJobConfig와 일관된 구조를 유지하고 있습니다.
52-53: Chunk 크기 설정에 대한 코멘트
CHUNK_SIZE = 100과TOP_N = 100이 동일하여 모든 랭킹 데이터가 단일 청크로 처리됩니다. 이는 Writer의 삭제-삽입 작업이 원자적으로 수행되어 데이터 일관성에 유리합니다. 다만,TOP_N이 증가할 경우 청크 크기 조정과 함께 Writer의 삭제 로직 재검토가 필요합니다.
54-56: EntityManager 직접 주입은 안전한 패턴입니다Spring은 @requiredargsconstructor를 통해 EntityManager를 주입할 때 자동으로 프록시 관리되는 EntityManager를 제공합니다. 이 프록시는 스레드 로컬 기반 세션 팩토리에 위임하므로 각 스레드에서 격리된 EntityManager 세션을 얻게 되어 본질적으로 스레드 안전합니다. 이는 Spring Data JPA의 권장 패턴이며, MonthlyRankingJobConfig와 동일한 방식으로 사용되고 있습니다.
Likely an incorrect or invalid review comment.
apps/commerce-batch/src/main/java/com/loopers/batch/config/MonthlyRankingJobConfig.java (1)
120-127: 프로세서의 명시적 캐스팅 필요
RankingScoreProcessor.process()가Object를 반환하므로MonthlyProductRank로 캐스팅이 필요합니다. 현재 구현은 동작하지만, 타입 안전성을 위해 제네릭 프로세서 또는 팩토리 패턴을 고려할 수 있습니다.현재 접근 방식은
WeeklyRankingJobConfig와 일관성이 있으며 동작에 문제가 없습니다.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (2)
138-157: 누락된 상품 처리 시 랭킹 일관성 문제상품 정보가 없는 경우 해당 항목을 건너뛰면 반환된 결과에서 랭킹 순위가 불연속적으로 나타날 수 있습니다 (예: 1, 2, 4, 5). 이는 API 소비자에게 혼란을 줄 수 있습니다.
상품이 삭제된 경우의 처리 방식을 확인해 주세요:
- 현재 방식 유지 (건너뛰기)
- 기본값으로 플레이스홀더 반환
- 삭제된 상품도 기본 정보와 함께 표시
비즈니스 요구사항에 따라 적절한 방식을 선택하세요.
165-176: LGTM!
RankingProductInfoDTO가 적절하게 정의되었습니다. Lombok의@Getter와@Builder를 사용하여 불변 객체 패턴을 따르고 있습니다.apps/commerce-batch/src/main/java/com/loopers/batch/processor/RankingScoreProcessor.java (1)
51-81: LGTM!
process메서드가periodType에 따라 적절한 엔티티(WeeklyProductRank또는MonthlyProductRank)를 생성합니다. 로그 출력과 필드 매핑이 명확합니다.apps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java (2)
27-38: LGTM!테이블 정의와 인덱스 설계가 적절합니다:
- 유니크 제약조건
(product_id, year_week)이 데이터 무결성을 보장합니다(year_week, rank_position)인덱스가 주간 랭킹 조회 쿼리를 최적화합니다(year_week, total_score DESC)인덱스가 점수 기반 정렬 쿼리를 지원합니다
80-101: LGTM!Builder 패턴이 올바르게 구현되었으며, 모든 필드가 적절하게 초기화됩니다.
MonthlyProductRank와 일관된 구조를 유지하고 있습니다.
|
|
||
| ## 13. Spring Batch 아키텍처 (Round 10 추가) | ||
|
|
||
| ### 9.1 Batch Job 구성 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
섹션 번호가 일치하지 않습니다.
Line 1322에서 "## 13. Spring Batch 아키텍처"로 선언되었지만, Line 1324에서는 "### 9.1 Batch Job 구성"으로 시작합니다. 하위 섹션 번호를 13.1로 수정해야 합니다.
🔎 제안하는 수정
-### 9.1 Batch Job 구성
+### 13.1 Batch Job 구성📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ### 9.1 Batch Job 구성 | |
| ### 13.1 Batch Job 구성 |
🤖 Prompt for AI Agents
.docs/design/03-class-diagram.md around line 1324: the subsection heading "###
9.1 Batch Job 구성" mismatches the parent "## 13. Spring Batch 아키텍처" declared at
line 1322; update the subsection numbering to "### 13.1 Batch Job 구성" (and scan
nearby headings for any other inconsistent 9.* references to rename them to 13.*
to keep numbering consistent).
| ## 11. product_metrics (상품 메트릭 집계) | ||
|
|
||
| **설명**: 실시간 상품 이벤트 집계를 저장하는 테이블 (Round 9 추가) | ||
|
|
||
| | 컬럼명 | 타입 | 제약조건 | 설명 | | ||
| |---|---|---|---| | ||
| | product_id | bigint | PK | 상품 ID | | ||
| | like_count | int | NOT NULL, DEFAULT 0 | 좋아요 수 | | ||
| | view_count | int | NOT NULL, DEFAULT 0 | 조회 수 | | ||
| | order_count | int | NOT NULL, DEFAULT 0 | 주문 수 | | ||
| | sales_amount | decimal(15,2) | NOT NULL, DEFAULT 0.00 | 판매 금액 | | ||
| | version | int | NOT NULL, DEFAULT 0 | 낙관적 락 버전 | | ||
| | created_at | timestamp | NOT NULL | 생성 시간 | | ||
| | updated_at | timestamp | NOT NULL | 수정 시간 | | ||
|
|
||
| **비즈니스 규칙**: | ||
| - Kafka 이벤트를 통해 실시간으로 집계 | ||
| - `version` 필드를 통한 낙관적 락으로 동시성 제어 | ||
| - 일별 데이터 집계 | ||
|
|
||
| **인덱스**: | ||
| ```sql | ||
| INDEX idx_like_count (like_count DESC) | ||
| INDEX idx_view_count (view_count DESC) | ||
| INDEX idx_order_count (order_count DESC) | ||
| INDEX idx_updated_at (updated_at) | ||
| ``` | ||
|
|
||
| --- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Search for ProductMetricsAggregateReader and related query implementations
rg -i 'ProductMetricsAggregateReader|ProductMetricsReader|product_metrics.*read' --type java -A 15Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 12624
🏁 Script executed:
# Search for usage of like_count, view_count, order_count in queries
rg 'like_count|view_count|order_count' --type java -B 3 -A 3 | head -60Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 6687
🏁 Script executed:
# Search for scoring weight definitions and ranking logic
rg -i 'weight|score.*like|score.*view|score.*order|ranking' --type java | head -30Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 4174
🏁 Script executed:
# Check for sort order definitions related to metrics
rg 'DESC|ORDER BY.*like_count|ORDER BY.*view_count|ORDER BY.*order_count' --type java -B 2 -A 2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 8385
🏁 Script executed:
# Search for other queries that use product_metrics table directly
rg 'FROM product_metrics|product_metrics.*WHERE' --type java -B 2 -A 8Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1515
🏁 Script executed:
# Check the full ProductMetricsAggregateReader implementation
sed -n '30,90p' apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricsAggregateReader.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2093
🏁 Script executed:
# Check RankingScoreProcessor for scoring logic details
rg -A 30 'class RankingScoreProcessor' --type javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3914
🏁 Script executed:
# Search for all queries on product_metrics to understand full usage pattern
rg 'product_metrics' --type java -B 3 -A 5Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 5423
🏁 Script executed:
# Check if there are any queries that would use individual metric DESC indexes
rg 'ORDER BY.*like_count|ORDER BY.*view_count|ORDER BY.*order_count' --type javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
product_metrics 인덱스 설계 재검토 필요
점수 계산 공식과 가중치(view: 0.1, like: 0.2, order: 0.6)는 일관성 있게 적용되고 있으나, 문서화된 DESC 인덱스(like_count, view_count, order_count DESC)는 실제 Top-N 쿼리에서 활용되지 않습니다.
실제 쿼리 실행 패턴:
WHERE created_at >= :startDate AND created_at < :endDate(인덱스 없음)GROUP BY product_idORDER BY total_score DESC(계산된 필드, 인덱스 불가)
현재 개별 메트릭 DESC 인덱스는 이 쿼리에서 사용되지 않으며, created_at 범위 필터링과 계산된 total_score 정렬로 인한 filesort 오버헤드가 발생합니다. 실제 쿼리 성능을 고려하여 created_at 인덱싱 또는 복합 인덱스 전략을 재평가하시기 바랍니다.
| @Entity | ||
| @Table(name = "mv_product_rank_monthly") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class MonthlyProductRank { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@table 어노테이션에 제약 조건과 인덱스가 누락되었습니다.
엔티티가 mv_product_rank_monthly 테이블에 매핑되지만, SQL 스키마(docker/init-db.sql)에 정의된 유니크 제약 조건(uk_product_month)과 인덱스(idx_year_month_rank, idx_year_month_score)가 @Table 어노테이션에 선언되지 않았습니다.
JPA가 스키마를 생성하거나 검증할 때 이러한 제약 조건과 인덱스를 인식하지 못할 수 있습니다.
🔎 제약 조건과 인덱스 추가 제안
@Entity
-@Table(name = "mv_product_rank_monthly")
+@Table(
+ name = "mv_product_rank_monthly",
+ uniqueConstraints = {
+ @UniqueConstraint(name = "uk_product_month", columnNames = {"product_id", "year_month"})
+ },
+ indexes = {
+ @Index(name = "idx_year_month_rank", columnList = "year_month, rank_position"),
+ @Index(name = "idx_year_month_score", columnList = "year_month, total_score DESC")
+ }
+)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MonthlyProductRank {필요한 임포트 추가:
import jakarta.persistence.Index;
import jakarta.persistence.UniqueConstraint;Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/domain/rank/MonthlyProductRank.java
(lines 20-24), the @Table annotation is missing the unique constraint and
indexes defined in docker/init-db.sql; update the @Table to include a
UniqueConstraint with name "uk_product_month" and the same columnNames used in
the SQL schema, and add Index entries for "idx_year_month_rank" and
"idx_year_month_score" with the exact columnNames from the SQL; also add the
imports jakarta.persistence.Index and jakarta.persistence.UniqueConstraint so
the annotation compiles.
| package com.loopers.domain.rank; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.Table; | ||
| import java.math.BigDecimal; | ||
| import java.time.LocalDateTime; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| /** | ||
| * Weekly product ranking entity for materialized view. | ||
| * | ||
| * <p>This table stores pre-aggregated weekly ranking data for performance optimization. | ||
| */ | ||
| @Entity | ||
| @Table(name = "mv_product_rank_weekly") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class WeeklyProductRank { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(name = "product_id", nullable = false) | ||
| private Long productId; | ||
|
|
||
| @Column(name = "year_week", nullable = false, length = 10) | ||
| private String yearWeek; | ||
|
|
||
| @Column(name = "rank_position", nullable = false) | ||
| private Integer rankPosition; | ||
|
|
||
| @Column(name = "total_score", nullable = false) | ||
| private Double totalScore; | ||
|
|
||
| @Column(name = "like_count", nullable = false) | ||
| private Integer likeCount; | ||
|
|
||
| @Column(name = "view_count", nullable = false) | ||
| private Integer viewCount; | ||
|
|
||
| @Column(name = "order_count", nullable = false) | ||
| private Integer orderCount; | ||
|
|
||
| @Column(name = "sales_amount", nullable = false, precision = 15, scale = 2) | ||
| private BigDecimal salesAmount; | ||
|
|
||
| @Column(name = "created_at", nullable = false, updatable = false) | ||
| private LocalDateTime createdAt; | ||
|
|
||
| @Column(name = "updated_at", nullable = false) | ||
| private LocalDateTime updatedAt; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
fd -e java "WeeklyProductRank.java"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 247
🏁 Script executed:
find . -type f -name "*.java" | xargs grep -l "class WeeklyProductRank"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 251
🏁 Script executed:
# Check if there are multiple WeeklyProductRank files and their locations
git ls-files | grep -i "weeklyproduct"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 247
🏁 Script executed:
cat -n apps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyProductRank.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3892
🏁 Script executed:
# Check entity scanning configuration in both modules
find apps -name "application*.yml" -o -name "application*.yaml" | xargs grep -A 5 -B 5 "entity\|EntityScan" 2>/dev/nullRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 72
🏁 Script executed:
# Check for @EntityScan or @ComponentScan annotations
rg "@EntityScan|@ComponentScan" apps/commerce-api apps/commerce-batchRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
🏁 Script executed:
# Check if these are separate modules/artifacts
cat apps/commerce-api/pom.xml | head -30Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 145
🏁 Script executed:
cat apps/commerce-batch/pom.xml | head -30Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 147
🏁 Script executed:
# Check Spring Boot entity scanning configuration
rg "spring.jpa.hibernate\.|spring.jpa.show-sql\|EntityScan" apps/commerce-api apps/commerce-batch -A 2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
🏁 Script executed:
# Find build configuration files
find . -maxdepth 2 -name "pom.xml" -o -name "build.gradle" -o -name "settings.gradle" | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
🏁 Script executed:
# Check the directory structure
ls -la apps/Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 391
🏁 Script executed:
# Look for parent pom or root gradle file
cat pom.xml 2>/dev/null || cat build.gradle 2>/dev/null || echo "No root build file found"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 112
🏁 Script executed:
# Check what's in root directory
ls -la | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1237
🏁 Script executed:
# Look for any build files anywhere
find . -maxdepth 3 -type f \( -name "pom.xml" -o -name "build.gradle" -o -name "settings.gradle" \) 2>/dev/nullRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
🏁 Script executed:
# Check if each app is a standalone Spring Boot application
cat apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java 2>/dev/null || echo "Not found"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 900
🏁 Script executed:
cat apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java 2>/dev/null || echo "Not found"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 806
commerce-api 모듈의 WeeklyProductRank 정의가 불완전합니다
commerce-batch 모듈의 WeeklyProductRank와 구조적으로 일치하지 않습니다. 배치 모듈은 @Builder, @PreUpdate, 데이터베이스 제약조건(@UniqueConstraint), 인덱스 정의를 포함하지만, API 모듈 버전은 이들이 없습니다.
같은 테이블 mv_product_rank_weekly을 매핑하면서도 정의가 다르면, API가 배치의 제약조건을 무시하고 데이터를 작성할 수 있어 데이터 무결성 문제가 발생할 수 있습니다. 또한 두 엔티티가 시간에 따라 더욱 벌어질 수 있는 유지보수 위험이 있습니다.
배치 모듈 버전과 동일하게 @Builder 생성자, @PreUpdate 메서드, 그리고 테이블 제약조건 및 인덱스 정의를 추가하여 일관성을 맞추기 바랍니다.
🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java
around lines 1-59, the entity is missing the same builder constructor,
@PreUpdate lifecycle method, and table-level unique constraint/index annotations
present in the commerce-batch version, causing potential data-integrity
divergence; update this class to match the batch module by adding a lombok
@Builder-enabled constructor (or @Builder on the class) that sets all fields,
implement a @PreUpdate method to refresh updatedAt (and any other lifecycle
logic present in batch), and add the same @Table(uniqueConstraints = ...,
indexes = ...) definitions used in commerce-batch; ensure field types, column
attributes, and annotations (nullable, precision/scale, lengths) exactly match
the batch entity so both modules map the same mv_product_rank_weekly schema.
| @Entity | ||
| @Table(name = "mv_product_rank_weekly") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class WeeklyProductRank { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MonthlyProductRank와의 구조적 불일치
이 엔티티에는 MonthlyProductRank에 정의된 인덱스(@Index), 유니크 제약조건(@UniqueConstraint), @Builder, 그리고 @PreUpdate 콜백이 누락되어 있습니다. 동일한 패턴을 유지하여 일관성을 확보해 주세요.
🔎 수정 제안
@Entity
-@Table(name = "mv_product_rank_weekly")
+@Table(
+ name = "mv_product_rank_weekly",
+ uniqueConstraints = @UniqueConstraint(
+ name = "uk_product_week",
+ columnNames = {"product_id", "year_week"}
+ ),
+ indexes = {
+ @Index(name = "idx_year_week_rank", columnList = "year_week, rank_position"),
+ @Index(name = "idx_year_week_score", columnList = "year_week, total_score DESC")
+ }
+)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class WeeklyProductRank {또한 @Builder 생성자와 @PreUpdate 콜백 추가가 필요합니다.
🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java
around lines 20-24, this entity is missing the same @Table indexes and
@UniqueConstraint, the Lombok @Builder constructor, and the @PreUpdate lifecycle
callback that MonthlyProductRank defines; to fix, mirror the MonthlyProductRank
class by adding the same @Table(name = "mv_product_rank_weekly", indexes =
{...}, uniqueConstraints = {...}) with identical index and unique constraint
definitions, add a @Builder-annotated constructor that initializes all required
fields (matching MonthlyProductRank’s builder signature), and implement the same
@PreUpdate method body used in MonthlyProductRank (e.g., to update timestamps or
derived fields) so the weekly entity follows the identical structure and
behavior.
| log.info( | ||
| "청크 종료: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " + | ||
| "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}" | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Java 문자열 보간 구문 오류 수정 필요
17-18번 라인의 로그 메시지가 잘못된 문자열 보간 구문을 사용하고 있습니다. ${...} 구문은 Java에서 작동하지 않으며, 리터럴 문자열로 출력됩니다. 실제 값을 출력하려면 String.format() 또는 문자열 연결을 사용해야 합니다.
🔎 제안하는 수정 방법
@AfterChunk
void afterChunk(ChunkContext chunkContext) {
- log.info(
- "청크 종료: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " +
- "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}"
- );
+ var stepExecution = chunkContext.getStepContext().getStepExecution();
+ log.info(
+ "청크 종료: readCount: {}, writeCount: {}",
+ stepExecution.getReadCount(),
+ stepExecution.getWriteCount()
+ );
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| log.info( | |
| "청크 종료: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " + | |
| "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}" | |
| ); | |
| @AfterChunk | |
| void afterChunk(ChunkContext chunkContext) { | |
| var stepExecution = chunkContext.getStepContext().getStepExecution(); | |
| log.info( | |
| "청크 종료: readCount: {}, writeCount: {}", | |
| stepExecution.getReadCount(), | |
| stepExecution.getWriteCount() | |
| ); | |
| } |
| @BeforeJob | ||
| void beforeJob(JobExecution jobExecution) { | ||
| log.info("Job '${jobExecution.jobInstance.jobName}' 시작"); | ||
| jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Java에서 Kotlin 문자열 보간 문법 사용 - 버그
Line 20의 "${jobExecution.jobInstance.jobName}"은 Kotlin 문자열 보간 문법입니다. Java에서는 이 문법이 지원되지 않아 리터럴 문자열 그대로 출력됩니다.
또한 ChunkListener.java (Line 15-16)에서도 동일한 문제가 있습니다.
🔎 수정 제안
@BeforeJob
void beforeJob(JobExecution jobExecution) {
- log.info("Job '${jobExecution.jobInstance.jobName}' 시작");
+ log.info("Job '{}' 시작", jobExecution.getJobInstance().getJobName());
jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis());
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @BeforeJob | |
| void beforeJob(JobExecution jobExecution) { | |
| log.info("Job '${jobExecution.jobInstance.jobName}' 시작"); | |
| jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis()); | |
| } | |
| @BeforeJob | |
| void beforeJob(JobExecution jobExecution) { | |
| log.info("Job '{}' 시작", jobExecution.getJobInstance().getJobName()); | |
| jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis()); | |
| } |
🤖 Prompt for AI Agents
In apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
around lines 18-22 (and also in ChunkListener.java around lines 15-16), the code
uses Kotlin-style string interpolation "${...}" which Java treats as a literal;
change the log calls to use a proper Java logging pattern (use logger
placeholders like log.info("Job '{}' 시작",
jobExecution.getJobInstance().getJobName()) or concatenate/format the string and
call getJobInstance().getJobName() via Java getters) and ensure the execution
context call remains unchanged; update both JobListener and ChunkListener log
statements accordingly.
| log.info( | ||
| """ | ||
| [에러 발생] | ||
| jobName: {} | ||
| exceptions: | ||
| {} | ||
| """.trim(), jobName, exceptions | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로그 레벨과 메시지 내용이 일치하지 않습니다.
에러 상황을 로깅하고 있지만 log.info()를 사용하고 있습니다. 에러 발생 시에는 log.error()를 사용하는 것이 적절합니다.
🔎 제안하는 수정
- log.info(
+ log.error(
"""
[에러 발생]
jobName: {}
exceptions:
{}
""".trim(), jobName, exceptions
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| log.info( | |
| """ | |
| [에러 발생] | |
| jobName: {} | |
| exceptions: | |
| {} | |
| """.trim(), jobName, exceptions | |
| ); | |
| log.error( | |
| """ | |
| [에러 발생] | |
| jobName: {} | |
| exceptions: | |
| {} | |
| """.trim(), jobName, exceptions | |
| ); |
🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
around lines 31 to 38, the code logs an error message using log.info(); change
this to use log.error() so the message severity matches an error condition, and
pass the exceptions object to the logger so the stack trace is recorded (i.e.,
replace log.info(...) with log.error(...) keeping the same message/template and
parameters).
| return new ProductRankingAggregation( | ||
| ((Number) row[0]).longValue(), | ||
| ((Number) row[1]).intValue(), | ||
| ((Number) row[2]).intValue(), | ||
| ((Number) row[3]).intValue(), | ||
| (BigDecimal) row[4], | ||
| i + 1 | ||
| ); | ||
| }) | ||
| .collect(Collectors.toList()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BigDecimal 캐스팅 안정성 개선 필요
row[4]를 BigDecimal로 직접 캐스팅하고 있지만, 데이터베이스 드라이버에 따라 Double이나 다른 숫자 타입이 반환될 수 있습니다. 다른 필드처럼 안전한 변환 패턴을 사용하세요.
🔎 수정 제안
return new ProductRankingAggregation(
((Number) row[0]).longValue(),
((Number) row[1]).intValue(),
((Number) row[2]).intValue(),
((Number) row[3]).intValue(),
- (BigDecimal) row[4],
+ row[4] instanceof BigDecimal ? (BigDecimal) row[4] : BigDecimal.valueOf(((Number) row[4]).doubleValue()),
i + 1
);🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricsAggregateReader.java
around lines 110 to 119, the code directly casts row[4] to BigDecimal which can
fail if the JDBC driver returns Double/Long/etc.; replace the direct cast with a
safe numeric conversion: check for null, if row[4] is an instance of BigDecimal
use it, else if it's a Number create a BigDecimal from its longValue/doubleValue
(e.g., BigDecimal.valueOf(((Number) row[4]).doubleValue())), and otherwise
handle/throw a clear IllegalArgumentException so the mapping is robust across
driver types.
| private DateRange calculateWeeklyDateRange(String yearWeek) { | ||
| int year = Integer.parseInt(yearWeek.substring(0, 4)); | ||
| int week = Integer.parseInt(yearWeek.substring(6)); | ||
|
|
||
| LocalDate firstDayOfYear = LocalDate.of(year, 1, 1); | ||
| LocalDate firstMonday = firstDayOfYear.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)); | ||
|
|
||
| if (firstMonday.isAfter(firstDayOfYear)) { | ||
| firstMonday = firstMonday.minusWeeks(1); | ||
| } | ||
|
|
||
| LocalDate startOfWeek = firstMonday.plusWeeks(week - 1); | ||
| LocalDate endOfWeek = startOfWeek.plusWeeks(1); | ||
|
|
||
| return new DateRange(startOfWeek, endOfWeek); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ISO 주차 계산 로직 검증 필요
현재 주차 계산 방식이 표준 ISO-8601 주차 정의와 다를 수 있습니다. Java의 WeekFields.ISO를 사용하면 더 정확하고 엣지 케이스(연초/연말 주차 경계)를 올바르게 처리할 수 있습니다.
🔎 수정 제안
+import java.time.temporal.WeekFields;
+import java.util.Locale;
private DateRange calculateWeeklyDateRange(String yearWeek) {
int year = Integer.parseInt(yearWeek.substring(0, 4));
int week = Integer.parseInt(yearWeek.substring(6));
- LocalDate firstDayOfYear = LocalDate.of(year, 1, 1);
- LocalDate firstMonday = firstDayOfYear.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
-
- if (firstMonday.isAfter(firstDayOfYear)) {
- firstMonday = firstMonday.minusWeeks(1);
- }
-
- LocalDate startOfWeek = firstMonday.plusWeeks(week - 1);
- LocalDate endOfWeek = startOfWeek.plusWeeks(1);
+ WeekFields weekFields = WeekFields.ISO;
+ LocalDate startOfWeek = LocalDate.of(year, 1, 1)
+ .with(weekFields.weekOfWeekBasedYear(), week)
+ .with(weekFields.dayOfWeek(), 1);
+ LocalDate endOfWeek = startOfWeek.plusWeeks(1);
return new DateRange(startOfWeek, endOfWeek);
}
📌 Summary
구현 내용
Spring Batch를 활용한 주간/월간 상품 랭킹 집계 시스템을 구현했습니다.
commerce-batch모듈 분리로 배포 독립성주요 변경사항
commerce-batch모듈 신규 생성WeeklyRankingJob,MonthlyRankingJob)mv_product_rank_weekly,mv_product_rank_monthly)GET /api/rankings/weekly,GET /api/rankings/monthly)💬 Review Points
1. 제가 생각한 Reader/Processor/Writer 역할이 괜찮을까요?
제가 생각한 Reader, Processor, Writer
2.
배치 청크 기반 처리에 대한 질문입니다.현재는 단순히 Top 100만 처리하도록 설계했습니다.보통 실무에서 Chunk Size를 어떻게 잡으시는지 궁금합니다.
3. 멘토링 때
배치 성능 최적화를 언제 하면 좋을지에 대한 질문의 연장입니다.멘토링때 배치 성능 개선 시점은 보통
배치 실행이 느릴때진행 된다고 들었습니다.그렇다면 실제 배치 성능 개선을 실무에서 어떻게 하는지 궁금합니다!
✅ Checklist
🧱 Spring Batch
🧩 Ranking API
Summary by CodeRabbit
릴리스 노트
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.