[volume-10] Collect, Stack, Zip #232
[volume-10] Collect, Stack, Zip #232yeonsu00 wants to merge 13 commits intoLoopers-dev-lab:yeonsu00from
Conversation
Feature/week10 ranking batch
[volume-10] Collect, Stack, Zip
Walkthrough이 PR은 상품 랭킹 기능을 일일(DAILY)에서 주간(WEEKLY), 월간(MONTHLY)으로 확장합니다. 새로운 commerce-batch 애플리케이션을 추가하고, 배치 작업으로 주간/월간 랭킹을 계산 및 업데이트합니다. API, 도메인 모델, 저장소, 서비스 계층이 다중 시간 범위를 지원하도록 개선됩니다. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant API as RankingV1Controller
participant Facade as RankingFacade
participant Service as RankingService
participant Repo as MvProductRank*Repository
participant DB as Database
Client->>API: GET /rankings?date=2025-01-15&type=WEEKLY&page=1&size=10
API->>API: 파라미터 파싱 (type→RankingType.WEEKLY)
API->>API: GetRankingCommand 생성
API->>Facade: getRanking(command)
alt RankingType.WEEKLY
Facade->>Service: getWeeklyRanking(date, page, size)
Service->>Repo: findByPeriodOrderByRankingAsc(startDate, endDate, page, size)
Repo->>DB: 쿼리 실행
DB-->>Repo: List<MvProductRankWeekly>
Repo-->>Service: 결과 반환
else RankingType.MONTHLY
Facade->>Service: getMonthlyRanking(date, page, size)
Service->>Repo: findByPeriodOrderByRankingAsc(startDate, endDate, page, size)
Repo->>DB: 쿼리 실행
DB-->>Repo: List<MvProductRankMonthly>
Repo-->>Service: 결과 반환
else Unsupported Type
Facade->>API: CoreException (BAD_REQUEST)
end
Service-->>Facade: List<Ranking>
Facade->>Facade: 상품/브랜드 정보 조회 및 구성
Facade-->>API: RankingInfo
API->>API: RankingListResponse로 변환
API-->>Client: ApiResponse<RankingListResponse>
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
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: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
20-20: 잠재적 NullPointerException
item.rank()가null을 반환할 경우intValue()호출 시 NPE가 발생합니다.RankingInfo.Item의rank필드가 항상 non-null임이 보장되는지 확인하거나, 방어적으로 처리하세요.🔎 방어적 처리 제안
- item.rank().intValue() + item.rank() != null ? item.rank().intValue() : 0apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java (2)
46-57: 동시성 문제:incrementLikeCount와 동일한 패턴 적용 필요두 스레드가 동시에 같은
productId에 대해 호출하면, 둘 다findByProductIdAndMetricsDate에서 빈 결과를 받아 새 엔티티를 저장하려 시도할 수 있습니다. 이 경우 unique constraint 위반으로DataIntegrityViolationException이 발생합니다.
incrementLikeCount처럼 원자적 업데이트 후 예외 처리 패턴을 적용하는 것이 좋습니다.🔎 권장 수정안
@Transactional public void incrementViewCount(Long productId) { LocalDate today = LocalDate.now(); - ProductMetrics metrics = productMetricsRepository.findByProductIdAndMetricsDate(productId, today) - .orElseGet(() -> { - ProductMetrics newMetrics = ProductMetrics.create(productId, today); - productMetricsRepository.saveProductMetrics(newMetrics); - return newMetrics; - }); - metrics.incrementViewCount(); - productMetricsRepository.saveProductMetrics(metrics); + int updatedRows = productMetricsRepository.incrementViewCount(productId, today); + + if (updatedRows == 0) { + try { + ProductMetrics newMetrics = ProductMetrics.create(productId, today); + newMetrics.incrementViewCount(); + productMetricsRepository.saveProductMetrics(newMetrics); + } catch (DataIntegrityViolationException e) { + productMetricsRepository.incrementViewCount(productId, today); + } + } }
59-70: 동시성 문제:incrementViewCount와 동일한 문제
incrementViewCount와 마찬가지로 동시 호출 시 unique constraint 위반이 발생할 수 있습니다. 동일한 원자적 업데이트 패턴을 적용해야 합니다.
🧹 Nitpick comments (25)
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (1)
14-18: 타임존 설정 방식에 대한 고려사항
TimeZone.setDefault()는 JVM 전역에 영향을 미칩니다. 현재 commerce-batch가 독립 애플리케이션으로 실행되므로 문제없지만, 향후 다른 모듈과 동일 JVM에서 실행될 경우 부작용이 발생할 수 있습니다.대안으로
application.yml에서spring.jackson.time-zone설정이나, 필요한 곳에서 명시적으로ZoneId.of("Asia/Seoul")을 사용하는 방식도 고려해 보세요.apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingCalculationTasklet.java (1)
35-43: 명시적인 타입 처리 권장현재
else분기가WEEKLY가 아닌 모든 경우를MONTHLY로 처리합니다.RankingType열거형이 확장될 경우 의도치 않은 동작이 발생할 수 있습니다.
RankingWriter와 동일한 패턴이지만, 향후 유지보수를 위해switch문이나 명시적 조건 처리를 고려해 보세요.🔎 switch 문을 사용한 제안
- if (rankingType == RankingType.WEEKLY) { - period = RankingPeriod.ofWeek(targetDate); - weeklyRankingService.calculateAndUpdateRanking(period); - log.info("주간 랭킹 계산 완료: targetDate={}, period={} ~ {}", targetDate, period.startDate(), period.endDate()); - } else { - period = RankingPeriod.ofMonth(targetDate); - monthlyRankingService.calculateAndUpdateRanking(period); - log.info("월간 랭킹 계산 완료: targetDate={}, period={} ~ {}", targetDate, period.startDate(), period.endDate()); - } + switch (rankingType) { + case WEEKLY -> { + period = RankingPeriod.ofWeek(targetDate); + weeklyRankingService.calculateAndUpdateRanking(period); + log.info("주간 랭킹 계산 완료: targetDate={}, period={} ~ {}", targetDate, period.startDate(), period.endDate()); + } + case MONTHLY -> { + period = RankingPeriod.ofMonth(targetDate); + monthlyRankingService.calculateAndUpdateRanking(period); + log.info("월간 랭킹 계산 완료: targetDate={}, period={} ~ {}", targetDate, period.startDate(), period.endDate()); + } + default -> throw new IllegalArgumentException("지원하지 않는 랭킹 타입: " + rankingType); + }apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.java (1)
6-8: 페이지네이션 방식을 Spring Data의Pageable로 통일하는 것을 권장합니다.현재
int page, int size파라미터로 수동 페이지네이션을 구현하고 있습니다. 학습 컨텍스트에 따르면 이 코드베이스에서는 도메인 저장소에서 Spring Data의Pageable타입 사용이 허용되며, 이는 일관성과 유지보수성을 향상시킵니다.Based on learnings, domain repositories in this codebase are allowed to use Spring Data's Pageable types.
🔎 Pageable을 사용하도록 리팩토링 제안
+import org.springframework.data.domain.Pageable; + public interface MvProductRankWeeklyRepository { - List<MvProductRankWeekly> findByPeriodOrderByRankingAsc(LocalDate periodStartDate, LocalDate periodEndDate, int page, int size); + List<MvProductRankWeekly> findByPeriodOrderByRankingAsc(LocalDate periodStartDate, LocalDate periodEndDate, Pageable pageable); }apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.java (1)
6-8: 페이지네이션 방식을 Spring Data의Pageable로 통일하는 것을 권장합니다.
MvProductRankWeeklyRepository와 마찬가지로, 수동 페이지네이션(int page, int size) 대신 Spring Data의Pageable을 사용하여 일관성을 유지하세요.Based on learnings, domain repositories in this codebase are allowed to use Spring Data's Pageable types.
🔎 Pageable을 사용하도록 리팩토링 제안
+import org.springframework.data.domain.Pageable; + public interface MvProductRankMonthlyRepository { - List<MvProductRankMonthly> findByPeriodOrderByRankingAsc(LocalDate periodStartDate, LocalDate periodEndDate); + List<MvProductRankMonthly> findByPeriodOrderByRankingAsc(LocalDate periodStartDate, LocalDate periodEndDate, Pageable pageable); }apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java (1)
15-17: 읽기 전용 트랜잭션 추가를 권장합니다.데이터베이스 읽기 작업에
@Transactional(readOnly = true)어노테이션을 추가하면 커넥션 풀 최적화 및 JDBC 드라이버에 대한 읽기 전용 힌트를 제공하여 성능을 개선할 수 있습니다.🔎 제안하는 수정 사항
+import org.springframework.transaction.annotation.Transactional; + @Service @RequiredArgsConstructor public class ProductMetricsService { private final ProductMetricsRepository productMetricsRepository; + @Transactional(readOnly = true) public List<ProductMetrics> findByMetricsDate(LocalDate metricsDate) { return productMetricsRepository.findByMetricsDate(metricsDate); }apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingPeriod.java (1)
12-23: 입력 파라미터 null 검증을 고려해보세요.팩토리 메서드들이 null
date를 받으면 NullPointerException이 발생합니다. 방어적 프로그래밍을 위해 null 검증 추가를 권장합니다.🔎 제안하는 수정안
+import java.util.Objects; + public record RankingPeriod( LocalDate startDate, LocalDate endDate ) { public static RankingPeriod ofWeek(LocalDate date) { + Objects.requireNonNull(date, "date must not be null"); LocalDate weekStart = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); LocalDate weekEnd = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); return new RankingPeriod(weekStart, weekEnd); } public static RankingPeriod ofMonth(LocalDate date) { + Objects.requireNonNull(date, "date must not be null"); YearMonth yearMonth = YearMonth.from(date); LocalDate monthStart = yearMonth.atDay(1); LocalDate monthEnd = yearMonth.atEndOfMonth(); return new RankingPeriod(monthStart, monthEnd); } }apps/commerce-batch/src/main/java/com/loopers/application/ranking/ProductMetricsItemReader.java (1)
49-52: 대용량 데이터 메모리 로딩 고려
findByMetricsDate()가 해당 날짜의 모든ProductMetrics를 메모리에 로드합니다. 데이터가 많아지면 OOM 위험이 있습니다. 현재 데이터 규모가 작다면 문제없지만, 향후JdbcPagingItemReader또는 커서 기반 리더로 전환을 고려해 주세요.apps/commerce-api/src/test/java/com/loopers/domain/ranking/RankingServiceIntegrationTest.java (1)
403-405: 헬퍼 메서드 이름 일관성 개선 고려
getDailyRankingKey로 이름이 변경되었으나, 다른 테스트 파일들(RankingCacheServiceIntegrationTest,RankingFacadeIntegrationTest,RankingV1ApiE2ETest)에서는 여전히getRankingKey를 사용하고 있습니다. 테스트 코드 전체의 일관성을 위해 다른 파일들도 함께 업데이트하는 것을 고려해 주세요.apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
19-23: 입력 파라미터 유효성 검증 추가 권장컨트롤러 레이어에서
page와size파라미터에 대한 명시적 검증이 없습니다. 현재 Spring Data의PageRequest.of()가 암묵적으로page < 0또는size < 1일 때IllegalArgumentException을 발생시키지만, 컨트롤러에서@Min(1)과@Positive어노테이션을 추가하면 유효성 검증 계약을 명확히 하고 더 명확한 오류 메시지를 제공할 수 있습니다.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
28-39: if-else 체인 대신 switch 표현식 사용 권장현재 if-else 체인은 동작하지만, 새로운 랭킹 타입이 추가될 때마다 이 메서드를 수정해야 합니다. Java 14+ switch 표현식을 사용하면 가독성과 유지보수성이 향상됩니다.
🔎 Switch 표현식을 사용한 리팩터링 제안
public RankingInfo getRanking(RankingCommand.GetRankingCommand command) { - List<Ranking> rankings; - - if (command.type() == RankingCommand.RankingType.DAILY) { - rankings = rankingService.getDailyRanking(command.date(), command.page(), command.size()); - } else if (command.type() == RankingCommand.RankingType.WEEKLY) { - rankings = rankingService.getWeeklyRanking(command.date(), command.page(), command.size()); - } else if (command.type() == RankingCommand.RankingType.MONTHLY) { - rankings = rankingService.getMonthlyRanking(command.date(), command.page(), command.size()); - } else { - throw new CoreException(ErrorType.BAD_REQUEST, "지원하지 않는 랭킹 타입입니다: " + command.type()); - } + List<Ranking> rankings = switch (command.type()) { + case DAILY -> rankingService.getDailyRanking(command.date(), command.page(), command.size()); + case WEEKLY -> rankingService.getWeeklyRanking(command.date(), command.page(), command.size()); + case MONTHLY -> rankingService.getMonthlyRanking(command.date(), command.page(), command.size()); + };참고: enum이 모든 케이스를 커버하므로 default 절이 불필요합니다. 새로운 RankingType이 추가되면 컴파일 타임에 에러가 발생하여 안전합니다.
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.java (1)
7-15: 인터페이스 구현은 올바르나, 엔티티 중복 문제 검토 권장리포지토리 인터페이스 자체는 잘 설계되었습니다. 하지만 PR 목적에서 언급하신 것처럼, commerce-batch와 commerce-api가 동일한 MV 테이블에 대해 각각 별도의 엔티티 클래스를 정의하는 것은 코드 중복 및 유지보수 문제를 야기할 수 있습니다.
권장사항:
- 공유 MV 엔티티(
MvProductRankWeekly,MvProductRankMonthly)를modules/jpa에 정의하고 두 애플리케이션에서 공통으로 사용하는 것을 고려하세요.- 이렇게 하면 스키마 변경 시 한 곳만 수정하면 되고, 타입 안정성도 보장됩니다.
- 각 애플리케이션의 도메인 리포지토리는 현재처럼 독립적으로 유지하되, 인프라 계층에서 공유 엔티티를 참조하도록 구성할 수 있습니다.
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java (1)
16-18: @Modifying 쿼리에 clearAutomatically 플래그 추가 권장
@Modifying어노테이션을 사용한 벌크 업데이트 쿼리는 영속성 컨텍스트를 우회하므로, 캐시된 엔티티가 오래된 상태로 남을 수 있습니다. 이는 동일 트랜잭션 내에서 해당 엔티티를 다시 조회할 때 예상치 못한 동작을 유발할 수 있습니다.🔎 clearAutomatically 플래그 추가 제안
- @Modifying + @Modifying(clearAutomatically = true) @Query("UPDATE ProductMetrics m SET m.likeCount = m.likeCount + 1 WHERE m.productId = :productId AND m.metricsDate = :metricsDate") int incrementLikeCount(@Param("productId") Long productId, @Param("metricsDate") LocalDate metricsDate);
clearAutomatically = true를 설정하면 쿼리 실행 후 영속성 컨텍스트가 자동으로 클리어되어 데이터 일관성이 보장됩니다.apps/commerce-batch/src/main/java/com/loopers/interfaces/scheduler/RankingBatchScheduler.java (1)
31-73: LocalDateTime.now() 중복 호출 제거 권장각 메서드에서
LocalDateTime.now()가 여러 번 호출됩니다(라인 34, 42, 47 및 56, 64, 69). 메서드 시작 시 한 번만 호출하여 재사용하면 코드가 더 명확해지고, 로그의 시간 일관성도 보장됩니다.🔎 리팩터링 제안
@Scheduled(cron = "0 0 3 * * *") public void runWeeklyRankingJob() { try { - LocalDate yesterday = LocalDate.now().minusDays(1); + LocalDateTime executionTime = LocalDateTime.now(); + LocalDate yesterday = executionTime.toLocalDate().minusDays(1); String targetDate = yesterday.format(DateTimeFormatter.ofPattern("yyyyMMdd")); - log.info("주간 랭킹 배치 작업 시작: targetDate={}, time={}", targetDate, LocalDateTime.now()); + log.info("주간 랭킹 배치 작업 시작: targetDate={}, time={}", targetDate, executionTime); JobParameters jobParameters = new JobParametersBuilder() .addString("rankingType", "WEEKLY") .addString("targetDate", targetDate) - .addString("executionTime", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) + .addString("executionTime", executionTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) .toJobParameters(); jobLauncher.run(rankingJob, jobParameters); - log.info("주간 랭킹 배치 작업 완료: targetDate={}, time={}", targetDate, LocalDateTime.now()); + log.info("주간 랭킹 배치 작업 완료: targetDate={}, time={}", targetDate, executionTime); } catch (Exception e) { log.error("주간 랭킹 배치 작업 실패", e); } }월간 랭킹 작업에도 동일한 패턴을 적용하세요.
apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingV1ApiE2ETest.java (1)
75-214: 일일 랭킹 테스트는 올바르게 업데이트되었습니다. 주간/월간 랭킹 테스트 추가를 고려해주세요.DTO 타입 변경과
type=DAILY파라미터 추가가 올바르게 반영되었습니다. 다만, 이 PR에서 주간(WEEKLY) 및 월간(MONTHLY) 랭킹 API가 추가되었으므로 해당 케이스에 대한 E2E 테스트도 추가하면 좋겠습니다.apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
39-61: 주간/월간 랭킹 조회 로직이 중복됩니다.
getWeeklyRanking과getMonthlyRanking메서드는 리포지토리와 period 생성 방식만 다르고 나머지 로직이 동일합니다. 현재 상태에서도 동작에는 문제없지만, 추후 유지보수성을 위해 공통 로직을 추출하는 것을 고려해볼 수 있습니다.🔎 리팩토링 예시
private <T> List<Ranking> getRankingFromRepository( RankingPeriod period, int page, int size, TriFunction<LocalDate, LocalDate, Integer, Integer, List<T>> finder, Function<T, Long> productIdExtractor, Function<T, Integer> rankingExtractor, Function<T, Double> scoreExtractor ) { List<T> ranks = finder.apply(period.startDate(), period.endDate(), page, size); if (ranks.isEmpty()) { return new ArrayList<>(); } return ranks.stream() .filter(rank -> rankingExtractor.apply(rank) != null) .map(rank -> new Ranking( productIdExtractor.apply(rank), rankingExtractor.apply(rank).longValue(), scoreExtractor.apply(rank) )) .toList(); }Also applies to: 63-85
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.java (1)
7-15:commerce-batch와commerce-api모듈에 동일한 인터페이스가 중복 정의되어 있습니다.두 모듈 모두에
MvProductRankWeeklyRepository가 존재하지만 메서드 시그니처가 다릅니다:
commerce-batch:findByPeriodOrderByRankingAsc(LocalDate periodStartDate, LocalDate periodEndDate)- 페이지네이션 없음commerce-api:findByPeriodOrderByRankingAsc(LocalDate periodStartDate, LocalDate periodEndDate, int page, int size)- 페이지네이션 포함같은 이름의 인터페이스가 다른 시그니처로 정의되어 있어 향후 유지보수가 어려워질 수 있습니다. PR 설명에 언급된 공통 모듈(
modules/jpa등)로 이 인터페이스와 엔티티를 분리하거나, 모듈별 요구사항에 맞게 인터페이스를 명확히 분리하는 것을 권장합니다.apps/commerce-batch/src/main/java/com/loopers/domain/ranking/WeeklyRankingService.java (3)
62-68: 배치 저장을 사용하여 성능을 개선할 수 있습니다.현재 각 엔티티를 개별적으로 저장하고 있어 최대 100번의 save 호출이 발생합니다.
saveAll()을 사용하면 배치로 처리되어 데이터베이스 왕복 횟수가 줄어듭니다.🔎 제안하는 수정
// TOP 100 상품의 랭킹만 업데이트 -int updatedCount = 0; for (int i = 0; i < top100.size(); i++) { MvProductRankWeekly rank = top100.get(i); rank.updateRanking(i + 1); - mvProductRankWeeklyRepository.save(rank); - updatedCount++; } +mvProductRankWeeklyRepository.saveAll(top100); +int updatedCount = top100.size();Note:
saveAll메서드를MvProductRankWeeklyRepository인터페이스에 추가해야 합니다.
75-80: 배치 삭제를 사용하여 성능을 개선할 수 있습니다.개별 삭제 대신 삭제할 엔티티를 수집한 후 한 번에 삭제하면 성능이 향상됩니다.
🔎 제안하는 수정
-int deletedCount = 0; +List<MvProductRankWeekly> toDelete = new ArrayList<>(); for (MvProductRankWeekly existing : allRanks) { if (!top100ProductIds.contains(existing.getProductId())) { - mvProductRankWeeklyRepository.delete(existing); - deletedCount++; + toDelete.add(existing); } } +mvProductRankWeeklyRepository.deleteAll(toDelete); +int deletedCount = toDelete.size();Note:
deleteAll메서드를MvProductRankWeeklyRepository인터페이스에 추가해야 합니다.
14-86: MonthlyRankingService와 코드 중복을 고려해보세요.
WeeklyRankingService와MonthlyRankingService가 거의 동일한 로직을 가지고 있습니다. 제네릭 타입이나 전략 패턴을 사용하여 공통 로직을 추상화할 수 있습니다. 다만 현재 구조도 명확하고 이해하기 쉬우므로, 향후 리팩토링 시 고려하시면 됩니다.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (1)
19-20: type 파라미터의 타입 안정성을 고려해보세요.현재
type파라미터가String으로 정의되어 있어 런타임에 검증이 필요합니다. 가능하다면 Swagger 문서화를 위해@Schema(allowableValues = {"DAILY", "WEEKLY", "MONTHLY"})를 추가하거나, 컨트롤러에서 enum으로 받아 타입 안정성을 높이는 것을 고려해보세요.apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
12-37: MvProductRankWeeklyRepositoryImpl과 코드 중복을 고려해보세요.두 repository 구현체가 거의 동일한 구조를 가지고 있습니다. 제네릭 타입을 사용한 추상 클래스로 공통 로직을 추출할 수 있지만, 현재 구조도 충분히 명확하므로 선택 사항입니다.
apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingJobConfig.java (1)
63-76: 동시 실행 시 상태 충돌 가능:@StepScope적용 권장
ProductMetricsItemReader와RankingWriter는 내부에 상태(stepExecution,productMetricsList,currentIndex,period,rankingType)를 가지고 있습니다. 현재 싱글톤으로 생성되므로, 주간/월간 배치가 동시에 실행되면 상태가 덮어씌워질 수 있습니다.🔎 권장 수정안
+import org.springframework.batch.core.configuration.annotation.StepScope; @Bean +@StepScope public ItemReader<ProductMetrics> rankingReader() { return new ProductMetricsItemReader(productMetricsService); } @Bean +@StepScope public ItemProcessor<ProductMetrics, ProductRankingAggregate> rankingProcessor() { return new RankingProcessor(rankingCalculator); } @Bean +@StepScope public ItemWriter<ProductRankingAggregate> rankingWriter() { return new RankingWriter(weeklyRankingService, monthlyRankingService); }apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MonthlyRankingService.java (2)
61-80: 성능 최적화 고려: 배치 저장/삭제루프 내 개별
save()/delete()호출은 배치 작업에서 병목이 될 수 있습니다.saveAll()및deleteAll()로 변경하면 성능이 향상됩니다.🔎 권장 수정안
- int updatedCount = 0; - for (int i = 0; i < top100.size(); i++) { - MvProductRankMonthly rank = top100.get(i); - rank.updateRanking(i + 1); - mvProductRankMonthlyRepository.save(rank); - updatedCount++; - } + for (int i = 0; i < top100.size(); i++) { + top100.get(i).updateRanking(i + 1); + } + mvProductRankMonthlyRepository.saveAll(top100); // TOP 100에서 밀려난 기존 데이터 삭제 Set<Long> top100ProductIds = top100.stream() .map(MvProductRankMonthly::getProductId) .collect(Collectors.toSet()); - int deletedCount = 0; - for (MvProductRankMonthly existing : allRanks) { - if (!top100ProductIds.contains(existing.getProductId())) { - mvProductRankMonthlyRepository.delete(existing); - deletedCount++; - } - } + List<MvProductRankMonthly> toDelete = allRanks.stream() + .filter(r -> !top100ProductIds.contains(r.getProductId())) + .toList(); + mvProductRankMonthlyRepository.deleteAll(toDelete); - log.info("월간 랭킹 계산 완료: period={} ~ {}, updated={}, deleted={}", - period.startDate(), period.endDate(), updatedCount, deletedCount); + log.info("월간 랭킹 계산 완료: period={} ~ {}, updated={}, deleted={}", + period.startDate(), period.endDate(), top100.size(), toDelete.size());
11-85: 코드 중복:WeeklyRankingService와 동일한 로직
WeeklyRankingService와MonthlyRankingService가 거의 동일한 구조입니다. 제네릭 추상 클래스나 템플릿 메서드 패턴을 사용하면 중복을 줄일 수 있습니다.다만, 현재 규모에서는 과한 추상화일 수 있으므로 향후 확장 시 고려하셔도 됩니다.
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
1-95: 엔티티 구조가 MvProductRankWeekly와 일관되게 잘 구현되었습니다.
MvProductRankWeekly와 동일한 구조를 따르고 있으며, JPA 매핑, 팩토리 메서드, 업데이트 메서드가 적절하게 구현되어 있습니다.PR 목표에서 언급된 것처럼, commerce-batch와 commerce-api에서 동일한 테이블에 대해 별도의 엔티티 클래스를 정의하고 있는 점에 대해서는 공유 모듈(modules/jpa)로 통합하는 것이 유지보수성 측면에서 더 좋을 수 있습니다. 다만 이는 별도의 리팩토링으로 진행해도 무방합니다.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (59)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingCommand.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.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-api/src/main/resources/application.ymlapps/commerce-api/src/test/java/com/loopers/application/ranking/RankingFacadeIntegrationTest.javaapps/commerce-api/src/test/java/com/loopers/domain/ranking/RankingServiceIntegrationTest.javaapps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingV1ApiE2ETest.javaapps/commerce-batch/build.gradle.ktsapps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.javaapps/commerce-batch/src/main/java/com/loopers/application/ranking/JobParameterUtils.javaapps/commerce-batch/src/main/java/com/loopers/application/ranking/ProductMetricsItemReader.javaapps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingCalculationTasklet.javaapps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingProcessor.javaapps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingWriter.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/BatchConfig.javaapps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.javaapps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsService.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/MonthlyRankingService.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingAggregate.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingCalculator.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingPeriod.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingType.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingWeight.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/WeeklyRankingService.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.javaapps/commerce-batch/src/main/java/com/loopers/interfaces/scheduler/RankingBatchScheduler.javaapps/commerce-batch/src/main/resources/application.ymlapps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTests.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/MetricsConsumerIdempotencyTest.javamodules/kafka/src/main/java/com/loopers/config/kafka/KafkaConfig.javasettings.gradle.kts
🧰 Additional context used
🧠 Learnings (5)
📚 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-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.javaapps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.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-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.javaapps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/MetricsConsumerIdempotencyTest.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java
📚 Learning: 2025-12-19T21:30:16.024Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/outbox/OutboxEventListener.kt:0-0
Timestamp: 2025-12-19T21:30:16.024Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template Kafka event pipeline, Like events (LikeCreatedEventV1, LikeCanceledEventV1) intentionally use aggregateType="Like" with aggregateId=productId. The aggregateId serves as a partitioning/grouping key (not a unique Like entity identifier), ensuring all like events for the same product go to the same partition for ordering guarantees and aligning with ProductStatisticService's product-based aggregation logic. Using individual like_id would scatter events across partitions and break the statistics aggregation pattern.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingAggregate.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.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/CommerceBatchApplication.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/BatchConfig.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-batch/src/test/java/com/loopers/CommerceBatchApplicationTests.java
🧬 Code graph analysis (17)
apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingCalculationTasklet.java (5)
apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingProcessor.java (1)
RequiredArgsConstructor(9-21)apps/commerce-batch/src/main/java/com/loopers/application/ranking/JobParameterUtils.java (1)
Slf4j(10-46)apps/commerce-batch/src/main/java/com/loopers/application/ranking/ProductMetricsItemReader.java (1)
Slf4j(17-65)apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingJobConfig.java (1)
Slf4j(23-77)apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingWriter.java (1)
Slf4j(17-65)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java (1)
Entity(13-76)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (1)
Repository(12-37)
apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingWriter.java (4)
apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingProcessor.java (1)
RequiredArgsConstructor(9-21)apps/commerce-batch/src/main/java/com/loopers/application/ranking/JobParameterUtils.java (1)
Slf4j(10-46)apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingCalculationTasklet.java (1)
Slf4j(17-47)apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingJobConfig.java (1)
Slf4j(23-77)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java (1)
Entity(13-95)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java (1)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
Entity(13-95)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
RequiredArgsConstructor(12-86)apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
Repository(13-24)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
Entity(13-76)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/WeeklyRankingService.java (1)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MonthlyRankingService.java (1)
Slf4j(11-85)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingCommand.java (1)
RankingCommand(5-21)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MonthlyRankingService.java (1)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/WeeklyRankingService.java (1)
Slf4j(11-86)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingCommand.java (1)
RankingCommand(5-21)apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
RankingV1Dto(7-38)
apps/commerce-batch/src/main/java/com/loopers/application/ranking/ProductMetricsItemReader.java (5)
apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingProcessor.java (1)
RequiredArgsConstructor(9-21)apps/commerce-batch/src/main/java/com/loopers/application/ranking/JobParameterUtils.java (1)
Slf4j(10-46)apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingCalculationTasklet.java (1)
Slf4j(17-47)apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingJobConfig.java (1)
Slf4j(23-77)apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingWriter.java (1)
Slf4j(17-65)
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/MetricsConsumerIdempotencyTest.java (1)
k6_product_list.js (1)
productId(356-356)
apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingJobConfig.java (6)
apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingProcessor.java (1)
RequiredArgsConstructor(9-21)apps/commerce-batch/src/main/java/com/loopers/application/ranking/JobParameterUtils.java (1)
Slf4j(10-46)apps/commerce-batch/src/main/java/com/loopers/application/ranking/ProductMetricsItemReader.java (1)
Slf4j(17-65)apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingCalculationTasklet.java (1)
Slf4j(17-47)apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingWriter.java (1)
Slf4j(17-65)apps/commerce-batch/src/main/java/com/loopers/batch/job/BatchConfig.java (1)
Configuration(9-19)
apps/commerce-api/src/test/java/com/loopers/domain/ranking/RankingServiceIntegrationTest.java (3)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingV1ApiE2ETest.java (1)
DisplayName(75-214)apps/commerce-api/src/test/java/com/loopers/application/ranking/RankingFacadeIntegrationTest.java (1)
DisplayName(61-196)apps/commerce-api/src/test/java/com/loopers/infrastructure/cache/RankingCacheServiceIntegrationTest.java (2)
DisplayName(43-115)DisplayName(117-160)
apps/commerce-api/src/test/java/com/loopers/application/ranking/RankingFacadeIntegrationTest.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingCommand.java (1)
RankingCommand(5-21)
🔇 Additional comments (51)
apps/commerce-api/src/main/resources/application.yml (1)
24-24: Kafka 설정이 올바르게 추가되었습니다.kafka.yml 파일이
modules/kafka/src/main/resources/위치에 존재하며, 필요한 모든 Kafka 연결 정보와 직렬화 설정(StringSerializer, JsonSerializer, ByteArrayDeserializer)이 포함되어 있습니다. commerce-api의 build.gradle.kts에서 kafka 모듈을 의존성으로 선언하고 있으며, KafkaConfig에서 String 타입의 KafkaTemplate 빈과 기타 필요한 Kafka 빈들이 올바르게 정의되어 있습니다. application.yml의 import 순서(jpa.yml → redis.yml → kafka.yml → logging.yml → monitoring.yml)도 적절합니다.modules/kafka/src/main/java/com/loopers/config/kafka/KafkaConfig.java (1)
49-58: String 타입 Kafka 빈의 직렬화(Serializer) 설정을 확인해 주세요.
stringProducerFactory와stringKafkaTemplate(49-58줄)이 추가되었는데, 기존Object, Object타입 빈과 동일한kafkaProperties.buildProducerProperties()설정을 재사용하고 있습니다.
KafkaProperties에서 명시적으로StringSerializer가 설정되어 있지 않으므로,buildProducerProperties()에서 제공하는 기본 설정이 String 타입에 적합한지 확인이 필요합니다. 특히 Object/JSON 직렬화 방식으로 설정된 경우 String 타입 빈이 예상대로 동작하지 않을 수 있습니다.추가로, String 타입의 producer 빈만 존재하고 consumer 빈(stringConsumerFactory, stringConsumerTemplate)이 없다면, 메시지를 정상적으로 수신하기 위해 별도의 consumer 설정도 필요합니다.
settings.gradle.kts (1)
7-7: LGTM!새로운 commerce-batch 모듈이 정상적으로 추가되었습니다.
apps/commerce-batch/build.gradle.kts (3)
11-11: LGTM!배치 처리를 위한 필수 의존성입니다.
22-23: LGTM!테스트 픽스처 의존성이 적절하게 구성되었습니다.
17-17: 이 구문은 올바릅니다.
com.querydsl:querydsl-apt::jakarta의 이중 콜론(::) 구문은 유효한 Gradle 의존성 구문입니다. 이는 Spring Boot의 의존성 관리(dependency-management)나 BOM을 사용할 때 버전을 생략하는 정상적인 패턴이며, 프로젝트 전역에서 일관되게 적용되고 있습니다. 수정이 필요 없습니다.apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTests.java (1)
6-11: LGTM!애플리케이션 컨텍스트 로딩을 검증하는 표준 스모크 테스트입니다. 배치 설정 오류를 조기에 감지하는 데 유용합니다.
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.java (1)
8-24: LGTM!
RankingPeriod레코드가 깔끔하게 구현되었습니다.ofWeek은 ISO 표준(월요일 시작)을 따르며,ofMonth는YearMonth를 적절히 활용합니다.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
9-26: LGTM - API DTO 리팩토링
DailyRankingListResponse에서RankingListResponse로의 이름 변경과 확장된 필드 추가가 다중 랭킹 타입(DAILY/WEEKLY/MONTHLY) 지원에 적절히 반영되었습니다.apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (2)
14-29: LGTM!
metricsDate필드 추가와(product_id, metrics_date)복합 유니크 제약조건이 일별 메트릭스 집계 요구사항에 적합합니다.@Version을 통한 낙관적 락킹도 기존 패턴과 일관됩니다.
55-63: LGTM!
create()팩토리 메서드가metricsDate파라미터를 포함하도록 적절히 업데이트되었습니다.apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingType.java (1)
3-6: LGTM!
RankingType열거형이 배치 작업의 주간/월간 랭킹 유형을 명확히 구분합니다. DAILY 랭킹은 Redis에서 처리되므로 이 열거형에서 제외된 것이 적절합니다.apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java (1)
6-8: LGTM!간결하고 명확한 저장소 인터페이스입니다.
apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
11-21: LGTM!저장소 패턴을 올바르게 구현했으며, JPA 저장소로의 위임이 깔끔합니다.
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingCommand.java (1)
7-19: LGTM!
RankingTypeenum과 통합된GetRankingCommand구조가 깔끔하게 설계되었습니다. DAILY, WEEKLY, MONTHLY 타입을 명확하게 구분하고 있습니다.apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/MetricsConsumerIdempotencyTest.java (1)
78-80: 날짜 기반 쿼리로의 전환이 올바르게 적용되었습니다.
findByProductIdAndMetricsDate를 사용하여 날짜별 메트릭 조회로 정확하게 업데이트되었습니다.LocalDate.now()사용은 동일 날짜 내 멱등성 테스트에 적합하며, 테스트 로직이 정확합니다.Also applies to: 90-91, 100-101, 141-143, 152-153, 200-202, 213-215, 260-262, 270-272
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingWeight.java (1)
6-17: 가중치 설정이 합리적이지만 비즈니스 요구사항 검증을 권장합니다.랭킹 점수 계산을 위한 가중치가 명확하게 정의되었습니다:
- 조회수: 10%
- 좋아요: 20%
- 주문 생성: 70%
가중치 합이 1.0으로 정규화되어 있어 점수 계산에 적합합니다. 주문 생성에 가장 높은 가중치를 부여한 것은 전자상거래 맥락에서 타당해 보입니다.
비즈니스 이해관계자와 이 가중치 비율이 제품 랭킹 목표와 일치하는지 확인하시기 바랍니다.
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingPeriod.java (2)
12-16: 주간 기간 계산 로직이 정확합니다.ISO-8601 표준(월요일 시작, 일요일 종료)에 따라 올바르게 구현되었습니다. TemporalAdjusters 활용도 적절합니다.
18-23: 월간 기간 계산 로직이 정확합니다.YearMonth를 활용하여 월의 첫날과 마지막 날을 올바르게 계산합니다.
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java (2)
11-11: 날짜별 메트릭 조회 메서드가 적절하게 추가되었습니다.제품 ID와 메트릭 날짜로 조회하는 메서드가 명확하게 정의되었습니다.
13-13: 메서드 시그니처 변경이 일관되게 구현되었습니다.
incrementLikeCount의LocalDate metricsDate파라미터 추가가 모든 계층에서 올바르게 적용되었습니다. ProductMetricsRepositoryImpl과 ProductMetricsJpaRepository의 구현이 인터페이스 시그니처와 일치하며, ProductMetricsService의 모든 호출부(21번 줄, 29번 줄)에서 LocalDate 파라미터를 정확하게 전달하고 있습니다.apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java (1)
12-16: 복합 키 기반 엔티티 조회 메서드가 적절합니다.제품 ID와 기간으로 주간 랭킹을 조회하는 메서드 시그니처가 올바릅니다.
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingCalculator.java (1)
8-24: 점수 계산 로직이 정확하고 안전합니다.null 체크와 가중치 적용이 올바르게 구현되었습니다. 각 메트릭에 대한 방어적 검증이 적절합니다.
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java (1)
12-16: Pageable을 활용한 페이징 쿼리가 올바르게 구현되었습니다.기간별 랭킹 조회 메서드에 Pageable이 포함되어 성능과 확장성을 고려한 설계입니다. (단, Weekly 저장소에는 이 파라미터가 누락되어 있으니 Weekly 저장소 리뷰 코멘트를 참고하세요)
apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingProcessor.java (1)
9-21: LGTM!Spring Batch
ItemProcessor구현이 깔끔합니다.ProductMetrics를 받아ProductRankingAggregate로 변환하는 로직이 명확하고,RankingCalculator를 통한 점수 계산 위임이 적절합니다.apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java (1)
11-14: LGTM!날짜 기반 조회 쿼리가 올바르게 구현되었습니다.
ORDER BY productId로 일관된 순서를 보장하여 배치 처리 시 재시작 가능성을 지원합니다.apps/commerce-api/src/test/java/com/loopers/domain/ranking/RankingServiceIntegrationTest.java (2)
169-284: 주간 랭킹 테스트 커버리지가 양호합니다.기본 조회, 페이징, 빈 리스트, null 랭킹 필터링 케이스를 포함하여 필요한 시나리오가 잘 테스트되어 있습니다.
286-401: 월간 랭킹 테스트 커버리지가 양호합니다.주간 랭킹 테스트와 동일한 구조로 일관성 있게 작성되었습니다.
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
26-34: LGTM!날짜 기반 메트릭 조회 및 업데이트 메서드가 적절하게 구현되었습니다.
incrementLikeCount가int를 반환하여 영향받은 행 수를 확인할 수 있어 좋습니다.apps/commerce-batch/src/main/resources/application.yml (1)
13-25: 배치 애플리케이션에web-application-type: servlet설정 확인배치 전용 애플리케이션에서
servlet타입을 사용하고 있습니다. 모니터링 엔드포인트나 Actuator가 필요한 경우 적절하지만, 순수 배치 실행만 필요하다면none으로 설정하여 리소스를 절약할 수 있습니다.apps/commerce-batch/src/main/java/com/loopers/application/ranking/ProductMetricsItemReader.java (1)
28-31:@BeforeStep과open()호출 순서 주의
ItemStream.open()이@BeforeStep보다 먼저 호출될 수 있습니다. 이 경우 Line 49에서stepExecution이null일 수 있습니다.JobParameterUtils.getTargetDate()가 null을 처리하여 전날 날짜를 반환하지만, 이것이 의도된 동작인지 확인이 필요합니다.🔎 안전한 처리를 위한 대안
StepExecutionListener를 구현하여beforeStep에서 데이터 로딩을 수행하거나,open()에서StepSynchronizationManager를 통해StepExecution을 가져오는 방법을 고려해 주세요.@Override public void open(ExecutionContext executionContext) throws ItemStreamException { // StepExecution이 null인 경우 대비 StepExecution stepExec = this.stepExecution; if (stepExec == null) { stepExec = StepSynchronizationManager.getContext().getStepExecution(); } LocalDate targetDate = JobParameterUtils.getTargetDate(stepExec); // ... }Also applies to: 41-53
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java (1)
14-14: 날짜 기반 조회 메서드 추가 확인
findByProductIdAndMetricsDate메서드가 올바르게 추가되었습니다. Spring Data JPA 네이밍 컨벤션을 준수하고 있으며, 날짜별 메트릭 조회를 지원합니다.apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java (1)
10-17: 현재 인터페이스 설계는 올바름
MvProductRankWeeklyJpaRepository(commerce-api 모듈)는 실제 구현체(MvProductRankWeeklyRepositoryImpl)에서findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc메서드만 사용하고 있으므로 현재 코드는 정확합니다.
findByProductIdAndPeriodStartDateAndPeriodEndDate메서드는 commerce-batch 모듈의 별도MvProductRankWeeklyJpaRepository에만 존재하며, 두 모듈은 서로 다른 요구사항을 가지고 있습니다. 따라서 AI 요약과의 불일치는 실제 문제가 아니며, 현재 코드는 모듈의 사용 패턴과 일치합니다.Likely an incorrect or invalid review comment.
apps/commerce-batch/src/main/java/com/loopers/interfaces/scheduler/RankingBatchScheduler.java (2)
53-73: 월간 랭킹 배치 작업이 매일 실행되는 스케줄 검증 필요현재 cron 표현식
"0 30 3 * * *"는 매일 03:30에 실행됩니다. 월간(MONTHLY) 랭킹이 정말 매일 실행되어야 하는지 확인이 필요합니다.주간(WEEKLY) 배치도 동일한 패턴으로 매일 실행되고 있으므로, 이는 일관된 설계 선택으로 보입니다:
- 주간 배치: 매일 03:00 실행
- 월간 배치: 매일 03:30 실행
고려사항:
- 만약 rolling window 방식(7일/30일 윈도우를 매일 갱신)이 의도라면 현재 구현이 맞습니다.
- 하지만 일반적으로 월간 랭킹은 월 1회만 실행되는 것이 일반적입니다.
- 매일 실행 시 상당한 계산 비용과 데이터베이스 부하가 발생할 수 있습니다.
비즈니스 요구사항과 구현 의도(rolling window vs 월별/주별 정산)를 명확히 확인하시기 바랍니다.
31-51: 현재 스케줄링 설계는 의도적인 롤링 윈도우 패턴이므로 수정 불필요코드를 검토한 결과, 주간 랭킹 배치가 매일 실행되는 것은 의도적인 설계입니다:
- ProductMetricsItemReader: 어제의 일일 메트릭을 조회
- RankingPeriod.ofWeek(): 해당 날짜가 속한 주(월요일~일요일)를 자동 계산
- WeeklyRankingService.upsertMetrics(): 해당 주의 기간에 메트릭 업데이트
- calculateAndUpdateRanking(): 그 주의 상위 100개 상품 재계산
매일 새로운 일일 메트릭이 해당 주에 반영되어 최신 데이터를 기반으로 주간 랭킹이 유지되는 롤링 윈도우 패턴입니다. 월간 랭킹도 동일한 방식으로 작동합니다. 이는 정상적이고 의도된 동작이므로 현재 구현이 올바릅니다.
apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingWriter.java (1)
40-63: LGTM - 구현이 올바르게 동작합니다.개별 아이템을 순회하며
rankingType에 따라 적절한 서비스를 호출하는 로직이 명확합니다. 청크 사이즈(100)에 대해 개별 upsert 호출이 발생하지만, 현재 요구사항에서는 수용 가능한 수준입니다.apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingAggregate.java (1)
5-30: LGTM - 깔끔한 aggregate 구현입니다.
addMetrics에서 null 처리가 적절하게 되어 있고,RankingCalculator에 점수 계산을 위임하여 단일 책임 원칙을 잘 따르고 있습니다.apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (1)
19-23: 인터페이스와 구현 시그니처가 일치합니다.
MvProductRankWeeklyRepository인터페이스(apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.java, Line 7)에서 메서드는 다음과 같이 정의되어 있습니다:List<MvProductRankWeekly> findByPeriodOrderByRankingAsc(LocalDate periodStartDate, LocalDate periodEndDate, int page, int size);이는 구현체의 시그니처와 정확히 일치합니다. 컴파일 에러는 발생하지 않습니다.
apps/commerce-batch/src/main/java/com/loopers/application/ranking/JobParameterUtils.java (1)
16-32: LGTM!
getTargetDate메서드는 null 체크, 파싱 예외 처리, 적절한 기본값 반환 등 방어적으로 잘 구현되어 있습니다.apps/commerce-batch/src/main/java/com/loopers/domain/ranking/WeeklyRankingService.java (1)
20-42: LGTM!upsert 로직이 명확하고 정확하게 구현되어 있습니다.
ifPresentOrElse를 사용한 구조도 읽기 좋습니다.apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
13-76: commerce-api와 commerce-batch 간 엔티티 중복을 검토하세요.PR 설명에 언급된 대로, 동일한 MV 테이블을 가리키는 엔티티가 commerce-api와 commerce-batch에 각각 정의되어 있습니다. 이는 코드 중복과 불일치 위험을 초래할 수 있습니다. modules/jpa와 같은 공통 모듈로 이동하는 것을 고려해보세요.
이 문제에 대한 팀의 결정 사항을 확인하고, 공유 엔티티로 통합할 계획이 있는지 검토하는 것이 좋습니다.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (1)
16-25: LGTM!API 시그니처가 일간/주간/월간 랭킹을 지원하도록 명확하게 확장되었습니다. 파라미터 순서도 직관적입니다.
apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
12-37: LGTM!Repository 구현이 명확하고 JPA repository로 적절히 위임하고 있습니다.
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java (1)
18-32: 좋아요 증가 시 경쟁 조건(race condition) 처리가 잘 되어 있습니다.원자적 업데이트를 먼저 시도하고, 행이 없으면 새로 생성하며, 동시에 다른 스레드가 생성했을 경우
DataIntegrityViolationException을 catch하여 다시 업데이트하는 패턴이 적절합니다.apps/commerce-batch/src/main/java/com/loopers/application/ranking/RankingJobConfig.java (1)
38-61: Job 구성 LGTM두 단계 구성이 적절합니다:
rankingStep: 청크 처리로 ProductMetrics를 읽어 점수 계산 후 MV 테이블에 upsertrankingCalculationStep: TOP 100 추출 및 랭킹 업데이트apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (1)
14-64: 엔티티 구조 LGTM배치 모듈에서 읽기 전용으로 사용되는 엔티티로 적절하게 구성되어 있습니다.
@Version필드는 낙관적 락킹을 위해 포함되어 있으며, unique constraint가 올바르게 설정되어 있습니다.PR objectives에서 언급된 것처럼, commerce-batch와 commerce-streamer가 별도의 엔티티 클래스를 사용하는 현재 구조는 작동하지만, 모듈간 일관성을 위해 공유 모듈(modules/jpa)로 통합하는 것도 고려해 볼 수 있습니다.
apps/commerce-api/src/test/java/com/loopers/application/ranking/RankingFacadeIntegrationTest.java (1)
85-90: 새 커맨드 구조 적용 LGTM
GetRankingCommand와RankingType.DAILY사용이 올바르게 업데이트되었습니다.WEEKLY/MONTHLY 타입에 대한 통합 테스트도 추가하면 API 변경에 대한 검증이 더 완벽해질 것입니다.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
32-42: 랭킹 타입 검증 및 API 통합 LGTM잘못된 랭킹 타입에 대한 예외 처리가 적절하며, 새로운 통합 API 구조가 잘 구현되었습니다.
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MonthlyRankingService.java (1)
24-26: 메서드가MvProductRankMonthly엔티티에 정의되어 있습니다.updateMetrics()(라인 85-90)과updateRanking()(라인 92-94)이 모두 구현되어 있으므로 코드는 정상적으로 컴파일되고 실행됩니다.Likely an incorrect or invalid review comment.
apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (2)
12-37: Repository 구현이 적절합니다.JpaRepository로의 위임 패턴이 잘 적용되어 있고, 도메인 레이어와 인프라스트럭처 레이어가 적절히 분리되어 있습니다. 이 코드베이스에서 Spring Data의
Page/Pageable타입 사용이 허용된 패턴임을 확인했습니다.
23-26: 이 리뷰 지적은 부정확합니다. 페이징 지원 누락이 아니라 의도적 설계입니다.
commerce-batch모듈의MvProductRankWeeklyRepository인터페이스는 페이징 파라미터 없이 정의되어 있으며(LocalDate periodStartDate, LocalDate periodEndDate), 구현체가 이를 정확하게 따르고 있습니다.배치 처리용 모듈이므로 페이징이 필요 없으며, 별도 모듈인
commerce-api에 페이징을 지원하는 별도의 인터페이스와 구현체가 있습니다. 각 모듈의 역할에 맞게 설계되어 있으므로 수정이 필요하지 않습니다.Likely an incorrect or invalid review comment.
| @Entity | ||
| @Table( | ||
| name = "mv_product_rank_monthly", | ||
| uniqueConstraints = { | ||
| @UniqueConstraint(columnNames = {"product_id", "period_start_date", "period_end_date"}) | ||
| } | ||
| ) | ||
| @Getter | ||
| public class MvProductRankMonthly extends BaseEntity { | ||
|
|
||
| @Column(name = "product_id", nullable = false) | ||
| private Long productId; | ||
|
|
||
| @Column(nullable = true) | ||
| private Integer ranking; | ||
|
|
||
| @Column(nullable = false) | ||
| private Double score; | ||
|
|
||
| @Column(name = "period_start_date", nullable = false) | ||
| private LocalDate periodStartDate; | ||
|
|
||
| @Column(name = "period_end_date", nullable = false) | ||
| private LocalDate periodEndDate; | ||
|
|
||
| @Column(name = "like_count", nullable = false) | ||
| private Long likeCount; | ||
|
|
||
| @Column(name = "view_count", nullable = false) | ||
| private Long viewCount; | ||
|
|
||
| @Column(name = "sales_count", nullable = false) | ||
| private Long salesCount; | ||
|
|
||
| @Builder | ||
| private MvProductRankMonthly(Long productId, Integer ranking, Double score, LocalDate periodStartDate, | ||
| LocalDate periodEndDate, Long likeCount, Long viewCount, Long salesCount) { | ||
| this.productId = productId; | ||
| this.ranking = ranking; | ||
| this.score = score; | ||
| this.periodStartDate = periodStartDate; | ||
| this.periodEndDate = periodEndDate; | ||
| this.likeCount = likeCount; | ||
| this.viewCount = viewCount; | ||
| this.salesCount = salesCount; | ||
| } | ||
|
|
||
| public MvProductRankMonthly() { | ||
| } | ||
|
|
||
| public static MvProductRankMonthly create(Long productId, Integer ranking, Double score, LocalDate periodStartDate, | ||
| LocalDate periodEndDate, Long likeCount, Long viewCount, Long salesCount) { | ||
| return MvProductRankMonthly.builder() | ||
| .productId(productId) | ||
| .ranking(ranking) | ||
| .score(score) | ||
| .periodStartDate(periodStartDate) | ||
| .periodEndDate(periodEndDate) | ||
| .likeCount(likeCount) | ||
| .viewCount(viewCount) | ||
| .salesCount(salesCount) | ||
| .build(); | ||
| } | ||
| } |
There was a problem hiding this comment.
엔티티에 필요한 업데이트 메서드가 누락되어 있습니다.
MonthlyRankingService에서 existing.updateMetrics(score, likeCount, viewCount, salesCount)와 rank.updateRanking(i + 1)를 호출하는데, 이 엔티티에는 해당 메서드들이 없습니다. 이 메서드들을 추가해야 합니다.
🔎 제안하는 수정
public MvProductRankMonthly() {
}
+ public void updateMetrics(Double score, Long likeCount, Long viewCount, Long salesCount) {
+ this.score = score;
+ this.likeCount = likeCount;
+ this.viewCount = viewCount;
+ this.salesCount = salesCount;
+ }
+
+ public void updateRanking(Integer ranking) {
+ this.ranking = ranking;
+ }
+
public static MvProductRankMonthly create(Long productId, Integer ranking, Double score, LocalDate periodStartDate,🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java
around lines 13 to 76, the entity is missing the updateMetrics(...) and
updateRanking(...) methods used by MonthlyRankingService; add a public method
updateMetrics(Double score, Long likeCount, Long viewCount, Long salesCount)
that updates the score, likeCount, viewCount and salesCount fields (and
optionally validates/null-checks inputs) and a public method
updateRanking(Integer ranking) that sets the ranking field; keep methods simple
mutators (void) and place them alongside the existing constructors/builders so
the service calls compile.
| @Builder | ||
| private MvProductRankWeekly(Long productId, Integer ranking, Double score, LocalDate periodStartDate, LocalDate periodEndDate, | ||
| Long likeCount, Long viewCount, Long salesCount) { | ||
| this.productId = productId; | ||
| this.ranking = ranking; | ||
| this.score = score; | ||
| this.periodStartDate = periodStartDate; | ||
| this.periodEndDate = periodEndDate; | ||
| this.likeCount = likeCount; | ||
| this.viewCount = viewCount; | ||
| this.salesCount = salesCount; | ||
| } |
There was a problem hiding this comment.
누락된 메서드: updateMetrics 및 updateRanking
WeeklyRankingService에서 existing.updateMetrics(...) 및 rank.updateRanking(...)을 호출하지만, 이 엔티티에 해당 메서드가 정의되어 있지 않습니다. 컴파일 에러가 발생합니다.
🔎 권장 수정안
public MvProductRankWeekly() {
}
+ public void updateMetrics(Double score, Long likeCount, Long viewCount, Long salesCount) {
+ this.score = score;
+ this.likeCount = likeCount;
+ this.viewCount = viewCount;
+ this.salesCount = salesCount;
+ }
+
+ public void updateRanking(Integer ranking) {
+ this.ranking = ranking;
+ }
+
public static MvProductRankWeekly create(Long productId, Integer ranking, Double score, LocalDate periodStartDate,Committable suggestion skipped: line range outside the PR's diff.
| public interface MvProductRankMonthlyJpaRepository extends JpaRepository<MvProductRankMonthly, Long> { | ||
|
|
||
| List<MvProductRankMonthly> findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc( | ||
| LocalDate periodStartDate, | ||
| LocalDate periodEndDate, | ||
| Pageable pageable | ||
| ); | ||
|
|
||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the Weekly repository interface
fd -t f "MvProductRankWeeklyJpaRepository" --type=fileRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 299
🏁 Script executed:
# Check the Monthly repository file mentioned in the review
cat "apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 660
🏁 Script executed:
# Search for all methods in the Weekly repository
rg "findByProductId" --type=java -A 5 | head -50Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 7308
🏁 Script executed:
# Check for usage of Monthly repository's methods
rg "MvProductRankMonthlyJpaRepository" --type=java -A 3Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3870
🏁 Script executed:
cat "apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1087
findByProductIdAndPeriodStartDateAndPeriodEndDate 메서드 추가 필요
이 Monthly 저장소에서 findByProductIdAndPeriodStartDateAndPeriodEndDate 메서드가 누락되어 있습니다. Weekly 저장소에는 있으며, 동일한 엔티티에 대한 batch 모듈의 MvProductRankMonthlyJpaRepository에도 이 메서드가 존재합니다.
모듈 간 인터페이스 일관성을 유지하기 위해 다음 메서드를 추가하세요:
Optional<MvProductRankMonthly> findByProductIdAndPeriodStartDateAndPeriodEndDate(
Long productId,
LocalDate periodStartDate,
LocalDate periodEndDate
);🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java
around lines 10 to 18, the repository is missing the method
findByProductIdAndPeriodStartDateAndPeriodEndDate required for consistency with
the weekly repository and the batch module; add the following method signature
to the interface: Optional<MvProductRankMonthly>
findByProductIdAndPeriodStartDateAndPeriodEndDate(Long productId, LocalDate
periodStartDate, LocalDate periodEndDate); ensuring imports for Optional and
LocalDate are available and the method follows the existing repository style.
| implementation(project(":modules:jpa")) | ||
| implementation(project(":modules:redis")) | ||
| implementation(project(":modules:kafka")) | ||
| implementation(project(":supports:jackson")) | ||
| implementation(project(":supports:logging")) | ||
| implementation(project(":supports:monitoring")) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for Redis and Kafka usage in commerce-batch
echo "=== Checking for Redis usage ==="
rg -n --type=java --type=kt -C3 'RedisTemplate|StringRedisTemplate|@EnableRedis|RedisConfig|redis' apps/commerce-batch/ 2>/dev/null || echo "No Redis usage found"
echo -e "\n=== Checking for Kafka usage ==="
rg -n --type=java --type=kt -C3 'KafkaTemplate|@KafkaListener|@EnableKafka|KafkaConfig|kafka' apps/commerce-batch/ 2>/dev/null || echo "No Kafka usage found"
echo -e "\n=== Checking directory structure ==="
find apps/commerce-batch -type f \( -name "*.java" -o -name "*.kt" \) | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1972
🏁 Script executed:
#!/bin/bash
# Check the actual build.gradle.kts for commerce-batch to understand all dependencies
echo "=== Full build.gradle.kts content for commerce-batch ==="
cat apps/commerce-batch/build.gradle.ktsRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 999
🏁 Script executed:
#!/bin/bash
# List all source files in commerce-batch
echo "=== Source files in commerce-batch ==="
find apps/commerce-batch -type f \( -name "*.java" -o -name "*.kt" \) -exec wc -l {} + | sort -nRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3035
commerce-batch에서 Redis와 Kafka 의존성을 제거하세요.
코드베이스 검색 결과, commerce-batch의 모든 소스 파일(1,224줄)에서 Redis나 Kafka 관련 클래스(RedisTemplate, KafkaTemplate, @KafkaListener 등)의 사용이 확인되지 않습니다. 배치 작업은 product_metrics 테이블에서 데이터를 읽고 MV(Materialized View) 테이블에 순위 정보를 쓰는 순수 배치 처리만 수행합니다.
불필요한 의존성을 제거하면 애플리케이션 시작 시간과 메모리 사용량을 개선할 수 있습니다:
dependencies {
implementation(project(":modules:jpa"))
// implementation(project(":modules:redis")) // 사용되지 않음
// implementation(project(":modules:kafka")) // 사용되지 않음
implementation(project(":supports:jackson"))
implementation(project(":supports:logging"))
implementation(project(":supports:monitoring"))
...
}🤖 Prompt for AI Agents
In apps/commerce-batch/build.gradle.kts around lines 3 to 8, the commerce-batch
module declares unused Redis and Kafka project dependencies; remove or comment
out the implementation(project(":modules:redis")) and
implementation(project(":modules:kafka")) lines to eliminate unnecessary startup
and memory overhead, then re-sync/refresh Gradle to ensure the build works
without those dependencies and run the test/build to confirm nothing else
requires them.
| implementation("org.springframework.boot:spring-boot-starter-batch") | ||
|
|
||
| // web | ||
| implementation("org.springframework.boot:spring-boot-starter-web") |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for web-related functionality in commerce-batch
echo "=== Checking for Controller/RestController annotations ==="
rg -n --type=java --type=kt -C3 '@Controller|@RestController|@RequestMapping' apps/commerce-batch/ || echo "No controllers found"
echo -e "\n=== Checking for WebMvc configuration ==="
rg -n --type=java --type=kt -C3 '@EnableWebMvc|WebMvcConfigurer' apps/commerce-batch/ || echo "No WebMvc configuration found"
echo -e "\n=== Checking for servlet/web-related imports ==="
rg -n --type=java --type=kt 'org\.springframework\.web|javax\.servlet|jakarta\.servlet' apps/commerce-batch/ || echo "No servlet/web imports found"
echo -e "\n=== Listing all files in commerce-batch source ==="
find apps/commerce-batch -name "*.kt" -o -name "*.java" | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2207
🏁 Script executed:
#!/bin/bash
# Check for actuator or monitoring setup
echo "=== Checking for actuator imports/configuration ==="
rg -n 'actuator|health|metrics' apps/commerce-batch/build.gradle.kts
echo -e "\n=== Checking for server configuration ==="
rg -n 'server\.port|server\.servlet' apps/commerce-batch/ || echo "No server configuration found"
echo -e "\n=== Viewing build.gradle.kts to see all dependencies ==="
cat apps/commerce-batch/build.gradle.ktsRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1125
spring-boot-starter-web 의존성을 제거하세요.
이 배치 애플리케이션에서 웹 기능이 전혀 사용되지 않습니다. 컨트롤러, 웹 설정, 서블릿 관련 코드가 없으며, 서버 포트 설정도 없습니다. 웹 의존성을 제거하면 불필요한 오버헤드(Tomcat, 웹 스택, 스레드 풀)를 줄일 수 있습니다.
spring-boot-starter-batch만으로 배치 처리에 필요한 모든 기능이 충분합니다.
🤖 Prompt for AI Agents
In apps/commerce-batch/build.gradle.kts around line 14, remove the unnecessary
web starter dependency
implementation("org.springframework.boot:spring-boot-starter-web") because this
batch app does not use any web functionality; delete that line and keep only
spring-boot-starter-batch (or other required batch/runtime dependencies), then
refresh/resolve Gradle dependencies and verify the build and batch tests pass.
| @Bean | ||
| public TaskExecutor taskExecutor() { | ||
| SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); | ||
| taskExecutor.setConcurrencyLimit(10); | ||
| return taskExecutor; | ||
| } |
There was a problem hiding this comment.
프로덕션 환경에서 SimpleAsyncTaskExecutor 사용을 재고하세요.
SimpleAsyncTaskExecutor는 각 태스크마다 새로운 스레드를 생성하므로 스레드 풀링이 없어 리소스 관리가 비효율적입니다. 프로덕션 환경에서는 ThreadPoolTaskExecutor를 사용하는 것이 권장됩니다.
🔎 ThreadPoolTaskExecutor로 변경하는 제안
-import org.springframework.core.task.SimpleAsyncTaskExecutor;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.core.task.TaskExecutor;
@Configuration
@EnableBatchProcessing
public class BatchConfig {
@Bean
public TaskExecutor taskExecutor() {
- SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
- taskExecutor.setConcurrencyLimit(10);
+ ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
+ taskExecutor.setCorePoolSize(5);
+ taskExecutor.setMaxPoolSize(10);
+ taskExecutor.setQueueCapacity(25);
+ taskExecutor.setThreadNamePrefix("batch-");
+ taskExecutor.initialize();
return taskExecutor;
}
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/commerce-batch/src/main/java/com/loopers/batch/job/BatchConfig.java
around lines 13-18, the bean currently creates a SimpleAsyncTaskExecutor which
spawns a new thread per task and is unsuitable for production; replace it with a
ThreadPoolTaskExecutor, configure sensible pool settings (corePoolSize,
maxPoolSize), set a queueCapacity, threadNamePrefix, call initialize(), and
return it as the TaskExecutor bean so tasks are pooled and resource usage is
controlled.
| @Entity | ||
| @Table( | ||
| name = "mv_product_rank_weekly", | ||
| uniqueConstraints = { | ||
| @UniqueConstraint(columnNames = {"product_id", "period_start_date", "period_end_date"}) | ||
| } | ||
| ) | ||
| @Getter | ||
| public class MvProductRankWeekly extends BaseEntity { | ||
|
|
||
| @Column(name = "product_id", nullable = false) | ||
| private Long productId; | ||
|
|
||
| @Column(nullable = true) | ||
| private Integer ranking; | ||
|
|
||
| @Column(nullable = false) | ||
| private Double score; | ||
|
|
||
| @Column(name = "period_start_date", nullable = false) | ||
| private LocalDate periodStartDate; | ||
|
|
||
| @Column(name = "period_end_date", nullable = false) | ||
| private LocalDate periodEndDate; | ||
|
|
||
| @Column(name = "like_count", nullable = false) | ||
| private Long likeCount; | ||
|
|
||
| @Column(name = "view_count", nullable = false) | ||
| private Long viewCount; | ||
|
|
||
| @Column(name = "sales_count", nullable = false) | ||
| private Long salesCount; | ||
|
|
||
| @Builder | ||
| private MvProductRankWeekly(Long productId, Integer ranking, Double score, LocalDate periodStartDate, LocalDate periodEndDate, | ||
| Long likeCount, Long viewCount, Long salesCount) { | ||
| this.productId = productId; | ||
| this.ranking = ranking; | ||
| this.score = score; | ||
| this.periodStartDate = periodStartDate; | ||
| this.periodEndDate = periodEndDate; | ||
| this.likeCount = likeCount; | ||
| this.viewCount = viewCount; | ||
| this.salesCount = salesCount; | ||
| } | ||
|
|
||
| public MvProductRankWeekly() { | ||
| } | ||
|
|
||
| public static MvProductRankWeekly create(Long productId, Integer ranking, Double score, LocalDate periodStartDate, | ||
| LocalDate periodEndDate, Long likeCount, Long viewCount, Long salesCount) { | ||
| return MvProductRankWeekly.builder() | ||
| .productId(productId) | ||
| .ranking(ranking) | ||
| .score(score) | ||
| .periodStartDate(periodStartDate) | ||
| .periodEndDate(periodEndDate) | ||
| .likeCount(likeCount) | ||
| .viewCount(viewCount) | ||
| .salesCount(salesCount) | ||
| .build(); | ||
| } | ||
|
|
||
| public void update(Integer ranking, Double score, Long likeCount, Long viewCount, Long salesCount) { | ||
| this.ranking = ranking; | ||
| this.score = score; | ||
| this.likeCount = likeCount; | ||
| this.viewCount = viewCount; | ||
| this.salesCount = salesCount; | ||
| } | ||
|
|
||
| public void updateMetrics(Double score, Long likeCount, Long viewCount, Long salesCount) { | ||
| this.score = score; | ||
| this.likeCount = likeCount; | ||
| this.viewCount = viewCount; | ||
| this.salesCount = salesCount; | ||
| } | ||
|
|
||
| public void updateRanking(Integer ranking) { | ||
| this.ranking = ranking; | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
MvProductRankMonthly와의 중복 코드를 제거하고 공유 모듈 활용을 고려하세요.
MvProductRankWeekly와 MvProductRankMonthly 엔티티가 거의 동일한 구조와 메서드를 가지고 있습니다 (테이블명과 클래스명만 다름). PR 목표에서 언급하신 것처럼, commerce-batch와 commerce-api가 동일한 테이블을 참조하는 별도의 엔티티 클래스를 정의하고 있습니다.
다음 접근 방법을 권장합니다:
- 공유 엔티티 모듈 생성:
modules/jpa에MvProductRankWeekly와MvProductRankMonthly를 공통 엔티티로 정의하여 commerce-batch와 commerce-api가 모두 참조하도록 합니다. - 추상 베이스 클래스 활용: 공통 필드와 메서드를 추출한 추상 클래스
MvProductRankBase를 만들어 주간/월간 엔티티가 상속받도록 하는 방식도 고려할 수 있습니다.
이렇게 하면 중복을 제거하고 유지보수성을 향상시킬 수 있습니다.
이 리팩토링을 위한 공유 엔티티 구조를 생성하거나, 새로운 이슈를 열어 작업을 추적하는 것을 도와드릴까요?
🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java
lines 13-95, this entity is nearly identical to MvProductRankMonthly; extract
the common fields and behavior into a shared module (e.g. modules/jpa) as a
MvProductRankBase (use @MappedSuperclass or a shared @Entity base) containing
productId, ranking, score, periodStartDate, periodEndDate, likeCount, viewCount,
salesCount and the common builder/update methods, then make two thin entity
classes MvProductRankWeekly and MvProductRankMonthly that extend the base and
only declare the @Table(name=...) unique constraints; update commerce-batch and
commerce-api to depend on the new shared module and remove the duplicate classes
from each service (or open a tracking issue if you prefer staged migration).
| List<MvProductRankMonthly> findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc( | ||
| LocalDate periodStartDate, | ||
| LocalDate periodEndDate | ||
| ); |
There was a problem hiding this comment.
Pageable 파라미터가 누락되어 컴파일 에러가 발생합니다.
MvProductRankMonthlyRepositoryImpl에서 이 메서드를 Pageable 파라미터와 함께 호출하지만, 현재 메서드 시그니처에는 해당 파라미터가 없습니다. 이로 인해 컴파일 에러가 발생하거나 잘못된 메서드가 호출될 수 있습니다.
🔎 수정 제안
+import org.springframework.data.domain.Pageable;
public interface MvProductRankMonthlyJpaRepository extends JpaRepository<MvProductRankMonthly, Long> {
Optional<MvProductRankMonthly> findByProductIdAndPeriodStartDateAndPeriodEndDate(
Long productId,
LocalDate periodStartDate,
LocalDate periodEndDate
);
List<MvProductRankMonthly> findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc(
LocalDate periodStartDate,
- LocalDate periodEndDate
+ LocalDate periodEndDate,
+ Pageable pageable
);
}📝 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.
| List<MvProductRankMonthly> findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc( | |
| LocalDate periodStartDate, | |
| LocalDate periodEndDate | |
| ); | |
| import org.springframework.data.domain.Pageable; | |
| public interface MvProductRankMonthlyJpaRepository extends JpaRepository<MvProductRankMonthly, Long> { | |
| Optional<MvProductRankMonthly> findByProductIdAndPeriodStartDateAndPeriodEndDate( | |
| Long productId, | |
| LocalDate periodStartDate, | |
| LocalDate periodEndDate | |
| ); | |
| List<MvProductRankMonthly> findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc( | |
| LocalDate periodStartDate, | |
| LocalDate periodEndDate, | |
| Pageable pageable | |
| ); | |
| } |
🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java
around lines 18-21, the repository method is missing a Pageable parameter
causing compile errors when MvProductRankMonthlyRepositoryImpl calls it with a
Pageable; update the method signature to accept a Pageable as the last parameter
(e.g. add "Pageable pageable") and import
org.springframework.data.domain.Pageable so the implementation can pass
pagination through.
| List<MvProductRankWeekly> findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc( | ||
| LocalDate periodStartDate, | ||
| LocalDate periodEndDate | ||
| ); |
There was a problem hiding this comment.
[Critical] Pageable 파라미터 누락으로 인한 성능 문제가 예상됩니다.
findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc 메서드에 Pageable 파라미터가 없어 모든 레코드를 한 번에 로드하게 됩니다. 이는 다음 문제를 야기합니다:
- 성능 이슈: 랭킹 데이터가 증가하면 메모리 부족 및 응답 지연 발생
- Monthly 저장소와의 불일치:
MvProductRankMonthlyJpaRepository는 동일한 메서드에 Pageable 파라미터를 포함하고 있음 - AI 요약과의 불일치: 요약에는 Pageable이 있다고 명시되어 있으나 실제 코드에는 누락됨
🔎 제안하는 수정안
public interface MvProductRankWeeklyJpaRepository extends JpaRepository<MvProductRankWeekly, Long> {
Optional<MvProductRankWeekly> findByProductIdAndPeriodStartDateAndPeriodEndDate(
Long productId,
LocalDate periodStartDate,
LocalDate periodEndDate
);
List<MvProductRankWeekly> findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc(
LocalDate periodStartDate,
LocalDate periodEndDate,
+ Pageable pageable
);
}📝 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.
| List<MvProductRankWeekly> findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc( | |
| LocalDate periodStartDate, | |
| LocalDate periodEndDate | |
| ); | |
| Page<MvProductRankWeekly> findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc( | |
| LocalDate periodStartDate, | |
| LocalDate periodEndDate, | |
| Pageable pageable | |
| ); |
🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java
around lines 18-21, the repository method lacks a Pageable parameter causing
full-table loads; add a Pageable parameter to the method signature (and import
org.springframework.data.domain.Pageable), then update all call sites to pass a
Pageable; if you need pagination metadata instead of just a slice, change the
return type to Page<MvProductRankWeekly> and update callers accordingly to
handle Page results.
| threads: | ||
| max: 200 # ?? ?? ??? ? (default : 200) | ||
| min-spare: 10 # ?? ?? ??? ? (default : 10) | ||
| connection-timeout: 1m # ?? ???? (ms) (default : 60000ms = 1m) | ||
| max-connections: 8192 # ?? ?? ?? ? (default : 8192) | ||
| accept-count: 100 # ?? ? ?? (default : 100) | ||
| keep-alive-timeout: 60s # 60s |
There was a problem hiding this comment.
주석 인코딩 깨짐
주석이 ??로 표시되어 인코딩 문제가 있어 보입니다. 파일 인코딩이 UTF-8인지 확인하고 주석을 복구해 주세요.
🔎 예상되는 수정
server:
shutdown: graceful
tomcat:
threads:
- max: 200 # ?? ?? ??? ? (default : 200)
- min-spare: 10 # ?? ?? ??? ? (default : 10)
- connection-timeout: 1m # ?? ???? (ms) (default : 60000ms = 1m)
- max-connections: 8192 # ?? ?? ?? ? (default : 8192)
- accept-count: 100 # ?? ? ?? (default : 100)
- keep-alive-timeout: 60s # 60s
+ max: 200 # 최대 스레드 수 (default: 200)
+ min-spare: 10 # 최소 여유 스레드 수 (default: 10)
+ connection-timeout: 1m # 연결 타임아웃 (default: 60000ms = 1m)
+ max-connections: 8192 # 최대 연결 수 (default: 8192)
+ accept-count: 100 # 대기 큐 크기 (default: 100)
+ keep-alive-timeout: 60s # Keep-Alive 타임아웃📝 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.
| threads: | |
| max: 200 # ?? ?? ??? ? (default : 200) | |
| min-spare: 10 # ?? ?? ??? ? (default : 10) | |
| connection-timeout: 1m # ?? ???? (ms) (default : 60000ms = 1m) | |
| max-connections: 8192 # ?? ?? ?? ? (default : 8192) | |
| accept-count: 100 # ?? ? ?? (default : 100) | |
| keep-alive-timeout: 60s # 60s | |
| threads: | |
| max: 200 # 최대 스레드 수 (default: 200) | |
| min-spare: 10 # 최소 여유 스레드 수 (default: 10) | |
| connection-timeout: 1m # 연결 타임아웃 (default: 60000ms = 1m) | |
| max-connections: 8192 # 최대 연결 수 (default: 8192) | |
| accept-count: 100 # 대기 큐 크기 (default: 100) | |
| keep-alive-timeout: 60s # Keep-Alive 타임아웃 |
🤖 Prompt for AI Agents
In apps/commerce-batch/src/main/resources/application.yml around lines 4-10 the
inline comments show garbled characters ("??"), indicating the file was saved
with the wrong encoding; re-save the file as UTF-8 and restore the original
Korean comments (or replace with clear English) by retrieving the correct text
from git history (git log/git show for this file or the last good commit) or
from the author, then commit the UTF-8-encoded file; verify by opening the file
in your editor with UTF-8 and confirming the comments read correctly, and run a
quick build to ensure no tooling breaks.
📌 Summary
mv_product_rank_weekly,mv_product_rank_monthly테이블에 주간/월간 TOP 100 랭킹 저장GET/api/v1/rankings?date=yyyyMMdd&type=DAILY|WEEKLY|MONTHLY&page=1&size=20💬 Review Points
1. 배치 애플리케이션과 API 애플리케이션 간의 데이터베이스 공유 방식
배치 애플리케이션은 commerce-batch로 따로 만들었습니다.
commerce-batch에서 MV 테이블에 랭킹을 계산해서 저장하고, API 애플리케이션(commerce-api)은 MV 테이블에서 조회합니다.
이렇게 commerce-batch와 commerce-api에서 같은 데이터베이스를 공유하고 있는데 이걸 어떻게 처리해야 할지 궁금합니다.
현재는 각 애플리케이션에 (같은 테이블을 가리키지만) 별도 엔티티 클래스를 구현했습니다.
modules/jpa 모듈의 공통 설정으로 하나의 클래스만 구현해야 할까요?
✅ Checklist
🧱 Spring Batch
🧩 Ranking API
Summary by CodeRabbit
Release Notes
New Features
Infrastructure
✏️ Tip: You can customize this high-level summary in your review settings.