[volume-10] Collect, Stack, Zip#236
[volume-10] Collect, Stack, Zip#236HongChangMo wants to merge 5 commits intoLoopers-dev-lab:HongChangMofrom
Conversation
- Chunk Oriented Processing 기반 주간 랭킹 배치 처리 구현 - 상품 집계 결과 MV 활용
- Chunk Oriented Processing 기반 월간 랭킹 배치 처리 구현 - 상품 집계 결과 MV 활용
- 오래된 일자별 데이터 정리 10일 기준 -> 30일 수정
- PeriodType별 일간, 주간, 월간 랭킹 제공 - 일간은 Redis ZSET 으로 제공 - 주간, 월간 MV를 활용한 랭킹 제공
워크스루주간 및 월간 상품 순위 조회 기능을 추가하기 위해 변경 사항
시퀀스 다이어그램sequenceDiagram
participant Client
participant RankingController as RankingV1<br/>Controller
participant RankingFacade as RankingFacade
participant PeriodService as PeriodRanking<br/>Service
participant WeeklyRepo as Weekly<br/>Repository
participant MonthlyRepo as Monthly<br/>Repository
participant Enrichment as Enrichment<br/>Logic
Client->>RankingController: GET /ranking<br/>(rankingType, periodType,<br/>date, limit)
RankingController->>RankingFacade: getTopRanking(type,<br/>periodType, date, limit)
alt periodType == WEEKLY
RankingFacade->>PeriodService: getTopWeeklyRanking(type,<br/>date, limit)
PeriodService->>WeeklyRepo: findByYearAndWeek<br/>OrderBy[Type]Desc(year,<br/>week, limit)
WeeklyRepo-->>PeriodService: List<ProductMetricsWeekly>
PeriodService->>PeriodService: convertWeeklyToRanking()<br/>calculateScore()
PeriodService-->>RankingFacade: List<Ranking>
else periodType == MONTHLY
RankingFacade->>PeriodService: getTopMonthlyRanking(type,<br/>date, limit)
PeriodService->>MonthlyRepo: findByYearAndMonth<br/>OrderBy[Type]Desc(year,<br/>month, limit)
MonthlyRepo-->>PeriodService: List<ProductMetricsMonthly>
PeriodService->>PeriodService: convertMonthlyToRanking()<br/>calculateScore()
PeriodService-->>RankingFacade: List<Ranking>
else periodType == DAILY (default)
RankingFacade->>RankingFacade: getTopRanking(type,<br/>date, limit)<br/>기존 로직
end
RankingFacade->>Enrichment: enrichWithProductInfo(rankings)
Enrichment-->>RankingFacade: List<RankingInfo>
RankingFacade-->>RankingController: List<RankingInfo>
RankingController-->>Client: JSON Response
예상 코드 리뷰 노력🎯 4 (복잡함) | ⏱️ ~60분 관련 가능성 있는 PR
제안된 레이블
시 (Poem)
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ 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.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (2)
37-37:getTotalRankingCount에 periodType 전달 누락
getRankingWithPaging에는periodType을 전달하지만,getTotalRankingCount에는 전달하지 않습니다. 주간/월간 랭킹의 경우 데이터 소스가 다르므로(일간: Redis, 주간/월간: MV), 총 개수 계산도periodType을 고려해야 합니다.이로 인해 페이지네이션의 총 페이지 수가 잘못 계산될 수 있습니다.
🔎 제안하는 수정 사항
- long totalCount = rankingFacade.getTotalRankingCount(request.type(), date); + long totalCount = rankingFacade.getTotalRankingCount(request.type(), request.periodType(), date);
63-63: 동일 이슈:getTopRanking에서도 periodType 누락
getTopRanking엔드포인트에서도getTotalRankingCount호출 시periodType을 전달해야 합니다.🔎 제안하는 수정 사항
- long totalCount = rankingFacade.getTotalRankingCount(request.type(), date); + long totalCount = rankingFacade.getTotalRankingCount(request.type(), request.periodType(), date);
♻️ Duplicate comments (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
38-65: 동일한 null 처리 검증이 필요합니다.
GetTopRankingRequest와 동일하게periodType필드의 null 처리를 확인하세요.
🧹 Nitpick comments (16)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java (1)
32-47: 팩토리 메서드 개선 제안
- 파라미터명
i를rank로 변경하여 가독성을 높이세요.- Line 40의
new Ranking사이 이중 공백을 수정하세요.🔎 제안하는 수정 사항
public static Ranking of( - int i, + int rank, Long productId, double score, Long totalLikeCount, Long totalViewCount, Long totalOrderCount ) { - return new Ranking( - i, + return new Ranking( + rank, productId, score, totalLikeCount, totalViewCount, totalOrderCount ); }apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java (1)
164-188: FQN 대신 import 문 사용 권장
org.springframework.data.domain.Page,org.springframework.data.domain.Pageable, 그리고 DTO 클래스들에 대해 완전한 패키지명(FQN)을 사용하고 있습니다. 가독성을 위해 파일 상단에 import 문을 추가하는 것이 좋습니다.🔎 제안하는 수정 사항
파일 상단에 import 추가:
import com.loopers.domain.metrics.dto.MonthlyAggregationDto; import com.loopers.domain.metrics.dto.WeeklyAggregationDto; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable;그 후 메서드 시그니처 간소화:
@Override - public org.springframework.data.domain.Page<com.loopers.domain.metrics.dto.WeeklyAggregationDto> findWeeklyAggregation( + public Page<WeeklyAggregationDto> findWeeklyAggregation( Integer year, Integer week, LocalDate startDate, LocalDate endDate, - org.springframework.data.domain.Pageable pageable + Pageable pageable ) {apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (1)
63-64: 복합 점수 계산 로직이 올바르게 구현되었습니다.가중치 계산 공식(좋아요 0.2 + 조회수 0.1 + 주문수 0.6)이 ProductMetricsWeekly와 일관되게 적용되었습니다.
🔎 방어적 null 처리 추가 고려
스키마에서
nullable = false로 설정되어 있어 안전하지만, 방어적 코딩을 위해 null 체크를 추가할 수 있습니다:public double calculateCompositeScore() { - return (totalLikeCount * 0.2) + (totalViewCount * 0.1) + (totalOrderCount * 0.6); + long likeCount = totalLikeCount != null ? totalLikeCount : 0L; + long viewCount = totalViewCount != null ? totalViewCount : 0L; + long orderCount = totalOrderCount != null ? totalOrderCount : 0L; + return (likeCount * 0.2) + (viewCount * 0.1) + (orderCount * 0.6); }apps/commerce-collector/src/main/java/com/loopers/batch/metrics/MonthlyMetricsWriter.java (1)
22-26: 타입 안전성을 개선할 수 있습니다.Line 26에서
List<? extends ProductMetricsMonthly>를List<ProductMetricsMonthly>로 unchecked cast하고 있습니다. Spring Batch 컨텍스트에서는 일반적으로 안전하지만, 컴파일러 경고가 발생하며 이론적으로는 타입 안전성 문제가 있을 수 있습니다.🔎 타입 안전한 대안
@Override public void write(Chunk<? extends ProductMetricsMonthly> chunk) { - List<? extends ProductMetricsMonthly> items = chunk.getItems(); - - // Bulk Insert/Update (UPSERT) - monthlyRepository.saveAll((List<ProductMetricsMonthly>) items); + List<ProductMetricsMonthly> items = new ArrayList<>(); + for (ProductMetricsMonthly item : chunk.getItems()) { + items.add(item); + } + + // Bulk Insert/Update (UPSERT) + monthlyRepository.saveAll(items); log.info("월간 집계 저장 완료: {} 건", items.size()); }또는 Java 10+ 스트림 사용:
@Override public void write(Chunk<? extends ProductMetricsMonthly> chunk) { - List<? extends ProductMetricsMonthly> items = chunk.getItems(); - - // Bulk Insert/Update (UPSERT) - monthlyRepository.saveAll((List<ProductMetricsMonthly>) items); + List<ProductMetricsMonthly> items = chunk.getItems().stream() + .map(ProductMetricsMonthly.class::cast) + .toList(); + + monthlyRepository.saveAll(items); log.info("월간 집계 저장 완료: {} 건", items.size()); }apps/commerce-collector/src/main/java/com/loopers/batch/metrics/MonthlyMetricsProcessor.java (1)
12-30: 엔티티 생성 패턴 검토 제안.현재
ProductMetricsMonthly.create()로 엔티티를 생성한 후 즉시updateMetrics()를 호출하는 패턴을 사용하고 있습니다. 이 패턴은 동작하지만, 두 단계로 나뉘어 있어 다소 비직관적일 수 있습니다.다음 중 하나를 고려해보세요:
create()메서드가 모든 매개변수(metrics 포함)를 받도록 확장- 빌더 패턴 사용
- 현재 패턴이 의도적인 설계라면 그대로 유지
현재 구현도 정상 동작하므로 팀의 설계 철학에 따라 선택하시면 됩니다.
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeeklyRepository.java (1)
8-10: 페이지네이션 지원 및 saveAll 반환 타입 개선 권장현재 구현에서 두 가지 개선 사항을 고려해보세요:
findAll()메서드는 페이지네이션 없이 모든 데이터를 메모리로 로드하므로 데이터가 많을 경우 성능 문제가 발생할 수 있습니다.saveAll()이 void를 반환하는데, 일반적인 Spring Data 패턴에서는 저장된 엔티티 리스트를 반환하여 생성된 ID 등을 확인할 수 있도록 합니다.🔎 개선된 메서드 시그니처 제안
public interface ProductMetricsWeeklyRepository { ProductMetricsWeekly save(ProductMetricsWeekly metrics); - void saveAll(List<ProductMetricsWeekly> metricsList); + List<ProductMetricsWeekly> saveAll(List<ProductMetricsWeekly> metricsList); int deleteByYearAndWeekBefore(Integer year, Integer week); - List<ProductMetricsWeekly> findAll(); + List<ProductMetricsWeekly> findAll(Pageable pageable); }apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsProcessor.java (1)
12-30: DTO 입력값에 대한 null 검증 추가 권장현재 프로세서는 DTO의 필드값에 대한 null 체크 없이 바로 사용하고 있습니다. Spring Batch에서 DTO 생성 시 null 값이 포함될 수 있으므로, 방어적 프로그래밍을 위해 입력값 검증을 추가하는 것을 고려해보세요.
🔎 null 검증 추가 제안
public class WeeklyMetricsProcessor implements ItemProcessor<WeeklyAggregationDto, ProductMetricsWeekly> { @Override public ProductMetricsWeekly process(WeeklyAggregationDto dto) { + // 필수 필드 검증 + if (dto.getProductId() == null || dto.getYear() == null || dto.getWeek() == null) { + throw new IllegalArgumentException("Required fields cannot be null"); + } + // DTO를 도메인 엔티티로 변환 ProductMetricsWeekly metrics = ProductMetricsWeekly.create( dto.getProductId(), dto.getYear(), dto.getWeek(), dto.getPeriodStartDate(), dto.getPeriodEndDate() ); // 집계 메트릭 업데이트 metrics.updateMetrics( - dto.getTotalLikeCount(), - dto.getTotalViewCount(), - dto.getTotalOrderCount() + dto.getTotalLikeCount() != null ? dto.getTotalLikeCount() : 0L, + dto.getTotalViewCount() != null ? dto.getTotalViewCount() : 0L, + dto.getTotalOrderCount() != null ? dto.getTotalOrderCount() : 0L ); return metrics; } }apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthlyRepository.java (1)
7-9: 페이지네이션 지원 및 saveAll 반환 타입 개선 권장ProductMetricsWeeklyRepository와 동일한 개선 사항을 고려해보세요:
findAll()메서드에 페이지네이션 지원 추가로 대용량 데이터 처리 시 메모리 문제 방지saveAll()의 반환 타입을List<ProductMetricsMonthly>로 변경하여 저장된 엔티티에 접근 가능하도록 개선🔎 개선된 메서드 시그니처 제안
public interface ProductMetricsMonthlyRepository { ProductMetricsMonthly save(ProductMetricsMonthly metrics); - void saveAll(List<ProductMetricsMonthly> metricsList); + List<ProductMetricsMonthly> saveAll(List<ProductMetricsMonthly> metricsList); int deleteByYearAndMonthBefore(Integer year, Integer month); - List<ProductMetricsMonthly> findAll(); + List<ProductMetricsMonthly> findAll(Pageable pageable); }apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (1)
91-100: 테스트 용이성을 위해 Clock 주입 고려Line 99에서
ZonedDateTime.now()를 직접 호출하면 단위 테스트 시 시간을 제어하기 어렵습니다. 프로덕션 환경에서는 문제없지만, 테스트 시 특정 시간을 검증해야 할 경우 어려움이 있을 수 있습니다.🔎 Clock 주입을 통한 개선 제안
필드에 Clock 추가:
+import java.time.Clock; + @Entity @Table(...) @Getter @NoArgsConstructor public class ProductMetricsMonthly extends BaseEntity { + + private Clock clock = Clock.systemDefaultZone(); + + // For testing + public void setClock(Clock clock) { + this.clock = clock; + }updateMetrics 메서드 수정:
public void updateMetrics( Long likeCount, Long viewCount, Long orderCount ) { this.totalLikeCount = likeCount; this.totalViewCount = viewCount; this.totalOrderCount = orderCount; - this.aggregatedAt = ZonedDateTime.now(); + this.aggregatedAt = ZonedDateTime.now(clock); }apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (1)
91-100: 테스트 용이성을 위해 Clock 주입 고려Line 99의
ZonedDateTime.now()직접 호출은 단위 테스트에서 시간 제어를 어렵게 만듭니다. ProductMetricsMonthly와 동일하게 Clock 주입을 고려해보세요.🔎 Clock 주입을 통한 개선 제안
+import java.time.Clock; + @Entity @Table(...) @Getter @NoArgsConstructor public class ProductMetricsWeekly extends BaseEntity { + + private Clock clock = Clock.systemDefaultZone(); + + // For testing + public void setClock(Clock clock) { + this.clock = clock; + } // ... existing fields ... public void updateMetrics( Long likeCount, Long viewCount, Long orderCount ) { this.totalLikeCount = likeCount; this.totalViewCount = viewCount; this.totalOrderCount = orderCount; - this.aggregatedAt = ZonedDateTime.now(); + this.aggregatedAt = ZonedDateTime.now(clock); } }apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodRankingService.java (2)
61-78: 인메모리 페이징은 대용량 데이터에서 비효율적입니다.현재 구현은
(page + 1) * size만큼의 데이터를 조회한 후 인메모리에서skip/limit을 적용합니다. 페이지 번호가 커질수록 불필요한 데이터를 많이 조회하게 됩니다.Repository에서 직접
OFFSET/LIMIT을 사용하는 페이징 쿼리 메서드를 추가하는 것을 고려해주세요.🔎 개선 제안
// Repository에 페이징 메서드 추가 List<ProductMetricsWeekly> findByYearAndWeekOrderByLikeCountDesc( int year, int week, int offset, int limit); // Service에서 직접 사용 public List<Ranking> getWeeklyRankingWithPaging(RankingType type, LocalDate date, int page, int size) { LocalDate targetDate = date != null ? date : LocalDate.now(); int year = targetDate.getYear(); int week = targetDate.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR); List<ProductMetricsWeekly> weeklyMetrics = switch (type) { case LIKE -> weeklyRepository.findByYearAndWeekOrderByLikeCountDescWithPaging( year, week, page * size, size); // ... other cases }; return convertWeeklyToRanking(weeklyMetrics, type, page * size); }
81-130: 중복 코드 추출을 고려해볼 수 있습니다.
convertWeeklyToRanking과convertMonthlyToRanking메서드가 거의 동일한 로직을 가지고 있습니다. 공통 인터페이스나 함수형 접근법으로 추출하면 유지보수성이 향상될 수 있습니다.🔎 리팩토링 예시
// 공통 변환 메서드 추출 private <T> List<Ranking> convertToRanking( List<T> metrics, RankingType type, Function<T, Long> productIdGetter, Function<T, Long> likeCountGetter, Function<T, Long> viewCountGetter, Function<T, Long> orderCountGetter ) { int rank = 1; List<Ranking> rankings = new ArrayList<>(); for (T metric : metrics) { double score = calculateScore(type, likeCountGetter.apply(metric), viewCountGetter.apply(metric), orderCountGetter.apply(metric)); rankings.add(Ranking.of( rank++, productIdGetter.apply(metric), score, likeCountGetter.apply(metric), viewCountGetter.apply(metric), orderCountGetter.apply(metric) )); } return rankings; }apps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobConfig.java (2)
73-91: ISO 주차 날짜 계산 시 연도 경계 엣지 케이스 주의ISO 8601 주차 기준으로, 1월 1일이 항상 해당 연도의 첫 주에 속하지 않을 수 있습니다. 예를 들어, 2026년 1월 1일은 ISO 기준 2025년의 53번째 주에 해당합니다.
현재 구현에서
LocalDate.of(year, 1, 1)을 시작점으로 사용하면, 특정 연도/주차 조합에서 예상치 못한 날짜가 계산될 수 있습니다.🔎 더 안정적인 ISO 주차 날짜 계산 방식 제안
@Bean @StepScope public RepositoryItemReader<WeeklyAggregationDto> weeklyMetricsReader( @Value("#{jobParameters['year']}") Integer year, @Value("#{jobParameters['week']}") Integer week ) { // 주간 시작일/종료일 계산 - LocalDate startDate = LocalDate.of(year, 1, 1) - .with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, week) - .with(DayOfWeek.MONDAY); + LocalDate startDate = LocalDate.ofYearDay(year, 1) + .with(IsoFields.WEEK_BASED_YEAR, year) + .with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, week) + .with(DayOfWeek.MONDAY); LocalDate endDate = startDate.plusDays(6);
93-103: Processor와 Writer의 불필요한 @StepScope 제거 고려
weeklyMetricsProcessor()와weeklyMetricsWriter()는 job parameter를 사용하지 않으므로@StepScope가 필요하지 않습니다.@StepScope는 매 Step 실행마다 새 인스턴스를 생성하여 약간의 오버헤드가 발생합니다.🔎 @StepScope 제거 제안
@Bean - @StepScope public ItemProcessor<WeeklyAggregationDto, ProductMetricsWeekly> weeklyMetricsProcessor() { return new WeeklyMetricsProcessor(); } @Bean - @StepScope public ItemWriter<ProductMetricsWeekly> weeklyMetricsWriter() { return new WeeklyMetricsWriter(weeklyRepository); }apps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsMonthlyJobConfig.java (1)
89-99: Processor와 Writer의 불필요한 @StepScope 제거 고려주간 설정과 동일하게,
monthlyMetricsProcessor()와monthlyMetricsWriter()는 job parameter를 사용하지 않으므로@StepScope가 불필요합니다.🔎 @StepScope 제거 제안
@Bean - @StepScope public ItemProcessor<MonthlyAggregationDto, ProductMetricsMonthly> monthlyMetricsProcessor() { return new MonthlyMetricsProcessor(); } @Bean - @StepScope public ItemWriter<ProductMetricsMonthly> monthlyMetricsWriter() { return new MonthlyMetricsWriter(monthlyRepository); }apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (1)
16-18: @component 대신 @repository 사용 고려이 클래스는 데이터 접근 계층을 담당하므로,
@Component대신@Repository를 사용하면 의미적으로 더 명확하고, Spring의 예외 변환(exception translation) 기능도 활용할 수 있습니다.🔎 @repository 사용 제안
-import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; /** * 주간 상품 집계 Repository 구현 (읽기 전용) * commerce-collector에서 생성한 집계 데이터 조회 */ -@Component +@Repository @RequiredArgsConstructor public class ProductMetricsWeeklyRepositoryImpl implements ProductMetricsWeeklyRepository {
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (40)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.javaapps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.javaapps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthlyRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.javaapps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeeklyRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodRankingService.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodType.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.javaapps/commerce-collector/build.gradle.ktsapps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingScheduler.javaapps/commerce-collector/src/main/java/com/loopers/batch/listener/ChunkListener.javaapps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.javaapps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.javaapps/commerce-collector/src/main/java/com/loopers/batch/metrics/MonthlyMetricsProcessor.javaapps/commerce-collector/src/main/java/com/loopers/batch/metrics/MonthlyMetricsWriter.javaapps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsProcessor.javaapps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsWriter.javaapps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsMonthlyJobConfig.javaapps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobConfig.javaapps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDailyRepository.javaapps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.javaapps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthlyRepository.javaapps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.javaapps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeeklyRepository.javaapps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/MonthlyAggregationDto.javaapps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/WeeklyAggregationDto.javaapps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyJpaRepository.javaapps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.javaapps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyJpaRepository.javaapps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.javaapps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyJpaRepository.javaapps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.javaapps/commerce-collector/src/test/java/com/loopers/batch/metrics/job/ProductMetricsMonthlyJobTest.javaapps/commerce-collector/src/test/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobTest.java
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2026-01-01T14:12:04.385Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 76
File: apps/commerce-batch/src/main/kotlin/com/loopers/batch/listener/ChunkListener.kt:4-5
Timestamp: 2026-01-01T14:12:04.385Z
Learning: In Spring Batch 6.x, ChunkListener API changed from using ChunkContext to Chunk<T> parameters. The correct method signatures are: beforeChunk(Chunk<T> chunk), afterChunk(Chunk<T> chunk), and onChunkError(Exception exception, Chunk<T> chunk). The import path is org.springframework.batch.core.listener.ChunkListener.
Applied to files:
apps/commerce-collector/src/main/java/com/loopers/batch/listener/ChunkListener.javaapps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.javaapps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
📚 Learning: 2026-01-01T14:12:07.943Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 76
File: apps/commerce-batch/src/main/kotlin/com/loopers/batch/listener/JobListener.kt:4-5
Timestamp: 2026-01-01T14:12:07.943Z
Learning: Spring Batch 6.x에서 JobExecution은 org.springframework.batch.core.job 패키지에, JobExecutionListener는 org.springframework.batch.core.listener 패키지에 위치합니다. Spring Batch 5.x와 달리 하위 패키지를 사용합니다.
Applied to files:
apps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.javaapps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
📚 Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.
Applied to files:
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDailyRepository.javaapps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyJpaRepository.javaapps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyJpaRepository.java
📚 Learning: 2025-12-19T09:30:12.459Z
Learnt from: HongChangMo
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 198
File: apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventListener.java:0-0
Timestamp: 2025-12-19T09:30:12.459Z
Learning: In the loopers-spring-java-template repository's commerce-api module, when entities are managed within a transactional context (e.g., Transactional methods), prefer relying on JPA dirty checking for automatic persistence rather than explicit save() calls. Both Payment and Order entities in PaymentEventListener use this pattern, with state changes automatically flushed on transaction commit.
Applied to files:
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java
📚 Learning: 2025-11-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product 엔티티 (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)는 유즈케이스별로 의도적으로 다른 락 전략을 사용한다: 좋아요 기능에는 비관적 락(findByIdForUpdate)을, 재고 차감에는 낙관적 락(Version + 재시도)을 사용한다.
Applied to files:
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.javaapps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.javaapps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.javaapps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java
🧬 Code graph analysis (10)
apps/commerce-collector/src/main/java/com/loopers/batch/listener/ChunkListener.java (2)
apps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.java (1)
Slf4j(14-53)apps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
Slf4j(14-45)
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (2)
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsWriter.java (1)
Slf4j(12-25)apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java (1)
Slf4j(20-189)
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/MonthlyMetricsWriter.java (1)
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java (1)
Slf4j(17-84)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodRankingService.java (3)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
Component(16-154)apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java (1)
Component(16-67)apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (1)
Component(16-67)
apps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (6)
apps/commerce-collector/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
Slf4j(9-21)apps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.java (1)
Slf4j(14-53)apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsWriter.java (1)
Slf4j(12-25)apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepositoryImpl.java (1)
Slf4j(20-189)apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (1)
Slf4j(17-84)apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt (1)
log(22-119)
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (1)
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/WeeklyAggregationDto.java (1)
Getter(13-26)
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java (1)
Getter(6-49)apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (1)
Entity(15-66)apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt (1)
name(14-87)
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java (1)
Getter(6-49)apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (1)
Entity(15-66)
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobConfig.java (1)
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsProcessor.java (1)
WeeklyMetricsProcessor(10-31)
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (1)
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/MonthlyAggregationDto.java (1)
Getter(13-26)
🔇 Additional comments (46)
apps/commerce-collector/build.gradle.kts (2)
36-36: Spring Batch 테스트 종속성이 올바르게 추가되었습니다.
spring-batch-test가testImplementation스코프로 적절하게 추가되어 배치 작업 테스트(JobLauncherTestUtils, StepScopeTestExecutionListener 등)를 지원할 수 있습니다.
14-14: Spring Batch 종속성이 적절히 추가되었습니다.
spring-boot-starter-batch와spring-batch-test종속성이 올바르게 추가되었습니다. 종속성은 적절히 스코프 처리되었으며(implementation vs testImplementation), Spring Boot 3.4.4의 의존성 관리를 통해 버전이 관리됩니다.2025~2026년 현재까지 Spring Batch 관련 보안 취약점(CVE)이 발견되지 않았으므로 보안상 문제가 없습니다.
apps/commerce-collector/src/main/java/com/loopers/application/ranking/RankingScheduler.java (1)
135-141: LGTM! 보관 기간 변경이 적절합니다.주간/월간 집계 배치 작업을 지원하기 위해 10일에서 30일로 보관 기간을 확장한 것은 합리적입니다. 월간 집계에는 최소 30일의 데이터가 필요합니다.
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyJpaRepository.java (2)
31-54: GROUP BY와 Page 조합 시 count 쿼리 검증 필요Spring Data JPA에서
GROUP BY가 포함된 쿼리에Page를 사용할 경우, 자동 생성되는 count 쿼리가 올바르게 동작하지 않을 수 있습니다.GROUP BY결과의 행 수가 아닌 전체 레코드 수를 반환할 수 있습니다.count 쿼리를 명시적으로 지정하는 것을 권장합니다:
🔎 제안하는 수정 사항
@Query(value = """ SELECT new com.loopers.domain.metrics.dto.WeeklyAggregationDto( p.productId, :year, :week, :startDate, :endDate, SUM(p.likeDelta), SUM(p.viewDelta), SUM(p.orderDelta), 0L ) FROM ProductMetricsDaily p WHERE p.metricDate BETWEEN :startDate AND :endDate GROUP BY p.productId ORDER BY p.productId - """) + """, + countQuery = """ + SELECT COUNT(DISTINCT p.productId) + FROM ProductMetricsDaily p + WHERE p.metricDate BETWEEN :startDate AND :endDate + """) Page<WeeklyAggregationDto> findWeeklyAggregation(
56-82: 월간 집계 쿼리도 동일하게 count 쿼리 명시 필요주간 집계와 동일한 이유로, 월간 집계 쿼리에도 명시적인 count 쿼리를 추가하세요.
🔎 제안하는 수정 사항
@Query(value = """ SELECT new com.loopers.domain.metrics.dto.MonthlyAggregationDto( p.productId, :year, :month, :startDate, :endDate, SUM(p.likeDelta), SUM(p.viewDelta), SUM(p.orderDelta), 0L ) FROM ProductMetricsDaily p WHERE p.metricDate BETWEEN :startDate AND :endDate GROUP BY p.productId ORDER BY p.productId - """) + """, + countQuery = """ + SELECT COUNT(DISTINCT p.productId) + FROM ProductMetricsDaily p + WHERE p.metricDate BETWEEN :startDate AND :endDate + """) Page<MonthlyAggregationDto> findMonthlyAggregation(apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsDailyRepository.java (1)
27-66: LGTM! 인터페이스 설계가 잘 되어 있습니다.주간/월간 집계 메서드에 대한 Javadoc이 명확하고, 페이징 지원이 Spring Batch Job과의 통합에 적합합니다. 기존 코드베이스의 Page/Pageable 사용 패턴과 일관성이 있습니다.
apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodType.java (1)
1-15: LGTM! 깔끔한 enum 설계입니다.
PeriodTypeenum이 명확하게 정의되어 있으며, 한국어 레이블이 사용자 친화적입니다. Lombok을 활용하여 보일러플레이트를 최소화한 점도 좋습니다.apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (2)
15-57: 읽기 전용 엔티티 설계가 적절합니다.Materialized View에 매핑되는 읽기 전용 엔티티로
@Immutable어노테이션이 적절하게 사용되었습니다. JPA를 통해 조회만 가능하며, 실제 데이터는 commerce-collector의 배치 작업에서 생성됩니다.
15-65: AI 요약과 실제 코드 간의 불일치가 있습니다.AI 요약에서 언급된 다음 메서드들이 실제 코드에는 존재하지 않습니다:
static factory create(productId, year, month, periodStartDate, periodEndDate)updateMetrics(likeCount, viewCount, orderCount)reset()읽기 전용 엔티티이므로 이러한 메서드가 없는 것이 올바른 설계이며, 실제 코드가 정확합니다.
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/WeeklyAggregationDto.java (1)
13-26: DTO 설계가 깔끔하고 적절합니다.Spring Batch
ItemReader에서 사용될 주간 집계 데이터 DTO가 올바르게 정의되었습니다. Lombok 어노테이션을 사용하여 보일러플레이트 코드를 효과적으로 제거했습니다.apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeeklyRepository.java (1)
6-36: 저장소 인터페이스가 잘 설계되었습니다.주간 메트릭스 조회를 위한 다양한 정렬 기준(좋아요, 조회수, 주문수, 복합 점수)을 제공하며, 메서드 명명이 명확하고 Javadoc이 적절하게 작성되었습니다.
apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthlyRepository.java (1)
6-36: 저장소 인터페이스가 잘 설계되었습니다.월간 메트릭스 조회를 위한 다양한 정렬 기준을 제공하며, ProductMetricsWeeklyRepository와 일관된 설계 패턴을 따르고 있습니다. 메서드 명명과 문서화가 적절합니다.
apps/commerce-collector/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
14-15: 코드는 현재 프로젝트의 Spring Batch 버전과 일치합니다.프로젝트는 Spring Boot 3.4.4를 사용하며, 이는 Spring Batch 5.2.2를 포함합니다. Spring Batch 5.2.2에서
ChunkListener의 올바른 메서드 시그니처는ChunkContext파라미터를 사용하는 것입니다:@AfterChunk void afterChunk(ChunkContext chunkContext)현재 코드는 Spring Batch 5.2.2 API 사양을 정확히 따르고 있으므로 문제가 없습니다.
Chunk<T>파라미터로의 변경은 Spring Batch 6.x 이상으로 업그레이드할 때만 필요합니다.apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyJpaRepository.java (1)
11-20: 삭제 쿼리 로직 확인 완료.년도와 주차를 기준으로 이전 데이터를 삭제하는 로직이 올바르게 구현되었습니다.
(m.year < :year) OR (m.year = :year AND m.week < :week)조건은 정확히 의도한 대로 동작합니다.apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (2)
15-57: 읽기 전용 엔티티 구조가 잘 설계되었습니다.
@Immutable어노테이션과 함께 읽기 전용 엔티티로 명확히 정의되어 있어, Materialized View의 특성을 코드 레벨에서 잘 표현하고 있습니다. 주간 집계 데이터를 위한 필드 구성도 적절합니다.
59-65: 점수 계산 로직의 일관성이 확보되었습니다.
calculateCompositeScore()메서드가 ProductMetricsMonthly(apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java Line 57-61)와 동일한 가중치(좋아요 0.2, 조회 0.1, 주문 0.6)를 사용하여 일관성을 유지하고 있습니다.apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyJpaRepository.java (1)
11-20: 삭제 쿼리 로직 확인 완료.년도와 월을 기준으로 이전 데이터를 삭제하는 로직이 올바르게 구현되었습니다.
(m.year < :year) OR (m.year = :year AND m.month < :month)조건은 정확히 의도한 대로 동작합니다.apps/commerce-collector/src/test/java/com/loopers/batch/metrics/job/ProductMetricsMonthlyJobTest.java (5)
26-61: 테스트 설정이 적절합니다.Spring Batch 테스트 설정과 생명주기 관리가 올바르게 구성되어 있습니다.
DatabaseCleanUp을 사용한 테이블 정리 방식도 적절합니다.
63-123: 월간 집계 성공 케이스가 잘 검증되었습니다.3개 상품에 대한 31일간 집계 데이터 생성 및 검증 로직이 정확합니다. Job/Step 상태와 집계 결과(310 likes, 3100 views, 155 orders)가 올바르게 검증되고 있습니다.
125-151: 데이터 부재 시나리오가 적절히 검증되었습니다.Daily 데이터가 없을 때 Job이 성공하지만 처리 건수가 0이고 Monthly 데이터가 생성되지 않는 것을 올바르게 검증하고 있습니다.
153-206: UPSERT 동작이 정확히 검증되었습니다.동일 월에 대해 재실행 시 새 레코드가 생성되지 않고 기존 레코드가 업데이트되는 것(10 → 30)을 올바르게 테스트하고 있습니다.
208-272: Step 실행과 월별 일수 처리가 잘 검증되었습니다.특정 Step만 실행하는 테스트와 2월(28일)과 12월(31일)의 일수 차이를 정확히 검증하는 테스트가 모두 적절합니다. 2025년이 평년임을 고려한 28일 검증도 정확합니다.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (2)
67-88: 상품 정보 필드가 올바르게 매핑되었습니다.
RankingItem에product필드가 추가되었고from()메서드에서info.product()를 사용하여 정확히 매핑하고 있습니다.
16-36: 이 검토 의견은 근거가 없습니다.periodType이 null일 때 NPE가 발생할 수 있다는 우려는 타당하지 않습니다.
RankingFacade클래스의getTopRanking()및getRankingWithPaging()메서드에서 다음과 같이 null 처리가 명시적으로 구현되어 있습니다:PeriodType period = periodType != null ? periodType : PeriodType.DAILY;따라서 null로 전달되더라도 기본값(DAILY)으로 자동 설정되므로 서비스 레이어에서 NPE 위험은 없습니다. DTO에
@NotNull추가나 기본값 설정은 불필요합니다.Likely an incorrect or invalid review comment.
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/WeeklyMetricsWriter.java (2)
18-19: unchecked cast는 Spring Batch 패턴에서 불가피합니다.Line 19의 unchecked cast는 Spring Batch의 제네릭 제약으로 인해 불가피하며, 런타임에 안전합니다.
12-25: @component 어노테이션이 필요하지 않습니다.
WeeklyMetricsWriter는ProductMetricsWeeklyJobConfig에서@Bean메서드로 명시적으로 인스턴스화되고 있습니다. 이는 Spring Batch의 표준 패턴으로, 자동 스캔이 아닌 설정 클래스에서 직접 빈을 생성합니다.@Component어노테이션 없이 현재 구현이 정상 작동하므로 추가 조치가 필요하지 않습니다.Likely an incorrect or invalid review comment.
apps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.java (1)
25-52: Job 완료 시 duration 계산 및 로깅이 적절합니다.실행 시간 계산과 한국어 시간 단위(시간/분/초) 포매팅이 정확하며, 로깅 로직에 문제가 없습니다.
apps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
19-22: Step 시작 로깅이 적절합니다.Step 시작 시 Step 이름을 올바른 SLF4J 구문으로 로깅하고 있습니다.
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java (1)
16-28: 테이블 설계가 잘 되어 있습니다Unique constraint와 인덱스 설정이 적절합니다:
uk_product_year_month는 중복 데이터 방지idx_year_month는 기간별 조회 최적화idx_product_id는 상품별 조회 최적화이는 배치 upsert 및 랭킹 조회 패턴에 잘 맞습니다.
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java (2)
16-28: 테이블 설계가 적절합니다Unique constraint와 인덱스 구성이 주간 랭킹 조회 패턴에 잘 맞습니다:
uk_product_year_week로 중복 방지idx_year_week로 기간별 조회 최적화idx_product_id로 상품별 조회 최적화
1-111: AI 요약의 모호함: calculateCompositeScore() 메서드 위치 명확화 필요검토 대상인
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/ProductMetricsWeekly.java파일에는 실제로calculateCompositeScore()메서드가 없습니다. 다만, 이는 코드 결함이 아니라 의도적인 아키텍처 설계입니다.코드베이스에는 동일한 이름의 ProductMetricsWeekly 엔티티가 두 모듈에 존재합니다:
- commerce-collector: 배치 집계용 쓰기 엔티티 (BaseEntity 상속) - 점수 계산 메서드 없음
- commerce-api: 읽기 전용 엔티티 (@immutable) - calculateCompositeScore() 메서드 구현됨 (가중치: like×0.2 + view×0.1 + order×0.6)
commerce-collector 모듈에서는 배치 처리 중 메트릭 집계만 담당하며, 점수 계산은 읽기 시점에 commerce-api의 엔티티 메서드나 PeriodRankingService 등에서 수행됩니다. 따라서 현재 코드는 정상이며, AI 요약이 컨텍스트 없이 메서드 추가를 언급한 것으로 보입니다.
apps/commerce-collector/src/main/java/com/loopers/domain/metrics/dto/MonthlyAggregationDto.java (1)
1-26: LGTM!DTO 구조가 월간 집계 용도에 적합하며, JPA 프로젝션 매핑을 위한
@NoArgsConstructor와 편의성을 위한@AllArgsConstructor가 올바르게 적용되어 있습니다.apps/commerce-collector/src/test/java/com/loopers/batch/metrics/job/ProductMetricsWeeklyJobTest.java (2)
125-232: LGTM!나머지 테스트 케이스들이 잘 구성되어 있습니다:
- 빈 데이터 시나리오 처리
- UPSERT 멱등성 검증 (10 + 20 = 30)
- Step 단위 실행 검증
테스트 커버리지가 배치 Job의 주요 동작을 포괄적으로 다루고 있습니다.
63-123: 테스트 케이스가 잘 구성되어 있습니다.주간 집계 배치 Job의 성공 시나리오를 적절히 검증하고 있으며, 7일간의 Daily 데이터가 Weekly로 올바르게 집계됩니다. ISO 주차 계산도 정확합니다: 2025-12-01은 월요일이며 ISO 주차 49의 첫 날입니다(49주 = 2025-12-01 월 ~ 2025-12-07 일).
apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodRankingService.java (1)
132-139: 복합 점수 가중치 합계가 1.0이 아닙니다.
ALL타입의 가중치 합계가 0.9입니다 (0.2 + 0.1 + 0.6). 의도된 것인지 확인이 필요합니다. 만약 정규화된 점수가 필요하다면 가중치 조정을 고려해주세요.apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (1)
25-28: LGTM!단일 저장 및 조회/삭제 메서드가 JPA Repository에 적절히 위임되어 있습니다.
Also applies to: 75-83
apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java (1)
1-67: LGTM!읽기 전용 Repository 구현이 JPA Repository에 적절히 위임되어 있으며, commerce-api 모듈의 ProductMetricsWeeklyRepositoryImpl과 일관된 패턴을 따르고 있습니다.
apps/commerce-collector/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java (1)
25-28: LGTM!단일 저장 및 조회/삭제 메서드가 JPA Repository에 적절히 위임되어 있습니다.
Also applies to: 75-83
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (2)
37-41: DAILY 타입에서date파라미터가 무시됩니다.DAILY 케이스에서 전달된
date파라미터 대신LocalDate.now()를 사용하고 있습니다. 이는 의도된 동작일 수 있지만, API 사용자가 특정 날짜의 일일 랭킹을 조회하려 할 때 혼란을 줄 수 있습니다.의도된 동작이라면 Javadoc에 이 동작을 명시하거나, 파라미터명을
weeklyOrMonthlyDate로 변경하는 것을 고려해주세요.Also applies to: 64-68
21-21: LGTM!
PeriodRankingService통합이 잘 구현되었습니다. switch 표현식을 사용한 기간별 라우팅 로직이 명확하고, null 처리를 통한 DAILY 기본값 적용이 적절합니다.Also applies to: 28-48
apps/commerce-collector/src/main/java/com/loopers/batch/metrics/job/ProductMetricsMonthlyJobConfig.java (1)
46-64: LGTM!월간 배치 Job 구성이 주간 구성과 일관된 패턴으로 잘 구현되었습니다. Job/Step/Chunk 리스너 연결과 chunk 크기(100)가 pageSize와 일치하여 효율적인 처리가 가능합니다.
apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyJpaRepository.java (2)
33-42: 복합 점수 가중치 합계 확인 필요현재 복합 점수 계산식의 가중치 합이 0.9입니다 (0.2 + 0.1 + 0.6 = 0.9). 이것이 의도된 설계인지, 아니면 가중치가 1.0이 되어야 하는지 확인이 필요합니다.
또한, 이 쿼리가 자주 호출된다면
(year, month)복합 인덱스가 성능에 도움이 될 수 있습니다.
12-53: LGTM!Repository 인터페이스가 Spring Data JPA 명명 규칙을 잘 따르고 있으며, 다양한 정렬 기준(좋아요, 조회수, 주문수, 복합 점수)에 대한 쿼리 메서드가 잘 구성되어 있습니다. Based on learnings, Pageable 사용은 이 코드베이스의 허용된 아키텍처 결정입니다.
apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java (1)
22-66: LGTM!JPA Repository로의 위임 패턴이 깔끔하게 구현되었습니다.
PageRequest.of(0, limit)를 사용한 페이지네이션 처리도 적절합니다.apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyJpaRepository.java (2)
33-42: 복합 점수 가중치 합계 - Monthly Repository와 동일한 확인 필요Monthly Repository와 동일하게 가중치 합이 0.9입니다. 두 repository 간 일관성은 유지되어 있으나, 의도된 설계인지 확인이 필요합니다.
12-53: LGTM!주간 Repository가 월간 Repository와 일관된 패턴으로 잘 구현되었습니다.
참고: AI 요약에서
deleteByYearAndWeekBefore메서드가 언급되었으나 실제 코드에는 존재하지 않습니다. 해당 메서드가 다른 파일에 있거나 요약이 부정확할 수 있습니다.
apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java
Outdated
Show resolved
Hide resolved
apps/commerce-collector/src/main/java/com/loopers/batch/listener/ChunkListener.java
Outdated
Show resolved
Hide resolved
apps/commerce-collector/src/main/java/com/loopers/batch/listener/JobListener.java
Show resolved
Hide resolved
apps/commerce-collector/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
Show resolved
Hide resolved
...tor/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyJpaRepository.java
Outdated
Show resolved
Hide resolved
...or/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsMonthlyRepositoryImpl.java
Show resolved
Hide resolved
...ctor/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyJpaRepository.java
Outdated
Show resolved
Hide resolved
...tor/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsWeeklyRepositoryImpl.java
Show resolved
Hide resolved
- 오타 수정 - NPE 방지 코드 적용
|
@coderabbitai 아래의 체크리스트를 만족하는지 확인해줘 🧱 Spring Batch
🧩 Ranking API
|
|
Tip For best results, initiate chat on the files or code changes. @HongChangMo 체크리스트 항목들을 확인한 결과, 모든 요구사항이 충족되었습니다! ✅ 🧱 Spring Batch
🧩 Ranking API
모든 요구사항이 잘 구현되어 있으며, 테스트 코드로도 검증되어 있습니다! 🎉 |
📌 Summary
💬 Review Points
커스텀 ItemReader가 아닌 RepositoryItemReader를 사용
처음 구현할때, 커스텀 ItemReader를 통해서 만약 좋아요 데이터가 100만 건이라면,
100만건을 모두 읽어와 처리를 한다면 데이터 양이 더 많아지면 OOM 이슈가 발생할 수 있을 것 같아,
Spring Batch 에서 지원하는 RepositoryItemReader를 사용해서 chunk size와 동일한
페이징 데이터를 처리하도록 구현하였습니다.
궁금한 점은 RepositoryItemReader 를 사용해서 처리하는 경우도 실무에서 많은지 궁금하고,
정답은 없다고 하셨지만, 구현한 방식이 올바른 방향성이 맞는지 궁금합니다.
MV를 활용한 주간 월간 랭킹 가중치 적용 질문
Redis의 ZSET으로 일간 랭킹을 구현할때에는 ZSET score 기반 top 랭킹을 뽑는 방법으로 구현했었는데, 이번 과제는 Spring Batch + MV 를 활용해 주간, 월간 집계 데이터를 수집하고, commerce-api에서 top 100 랭킹을 조회하는 기능을 구현해야 하는데,
제가 궁금한 점은 MV에 저장되는 데이터는 집계에 대한 내용뿐이라, top N 을 뽑아야하는 경우 별도의 score 를 어떻게 구현하는게 좋을지 궁금합니다.
저는 이번 과제에서 api를 조회하는 commerce-api 에 읽기 전용 엔티티를 만들고 MV에서 데이터를 db에서 조회할 때 score를 계산해서 조회하도록 처리하였습니다.
제가 생각한 다른 방법으로는 내부에 score를 계산하는 도메인 로직을 통해 종합 점수를 계산하도록 하는 방법도 있을 것 같아보이는데, commerce-collector 집계 처리하는 부분에서 미리 score를 계산하는 것이 더 좋은 방법인지, 아니면 제가 구현한 부분도 괜찮은 방법인지 궁금합니다.
스코어 계산을 repository에서 처리
도메인 로직에서 처리하는 경우
✅ Checklist
🧱 Spring Batch
🧩 Ranking API
Summary by CodeRabbit
릴리스 노트
새로운 기능
테스트
✏️ Tip: You can customize this high-level summary in your review settings.