-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-10] Collect, Stack, Zip #238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: rnqhstmd
Are you sure you want to change the base?
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Walkthrough다중 주기(일별, 주별, 월별) 랭킹 조회 기능을 지원하기 위해 API, 애플리케이션 서비스, 도메인 엔티티, 저장소 및 배치 작업을 확장합니다. RankingPeriod 열거형, WeeklyRanking/MonthlyRanking JPA 엔티티, 기간별 쿼리 메서드, 주간/월간 랭킹 배치 작업 및 API 엔드포인트를 추가합니다. Changes
Sequence DiagramssequenceDiagram
actor Client
participant API as RankingV1<br/>Controller
participant Facade as RankingFacade
participant Service as RankingService
participant Redis as Redis
participant Repo as RankingRepository
participant DB as Database
Client->>API: GET /rankings?period=weekly
API->>Facade: getRankingPage(date, WEEKLY, page, size)
alt Weekly Period
Facade->>Service: getWeeklyRankingPage(date, page, size)
Service->>Repo: findWeeklyByDateOrderByRank(weekStart, weekEnd, limit, offset)
Repo->>DB: Query WeeklyRanking
DB-->>Repo: Rankings
Repo-->>Service: List<WeeklyRanking>
Service-->>Facade: List<RankingEntry>
else Daily Period
Facade->>Service: getDailyRankingPage(date, page, size)
Service->>Redis: ZREVRANGE ranking:all:yyyyMMdd
Redis-->>Service: Ranked entries
Service-->>Facade: List<RankingEntry>
end
Facade->>Facade: Build RankingPageInfo with period
Facade-->>API: RankingPageInfo
API-->>Client: RankingPageResponse with period field
sequenceDiagram
participant Job as Spring Batch<br/>Scheduler
participant Reader as WeeklyRanking<br/>Reader
participant Processor as WeeklyRanking<br/>Processor
participant Writer as WeeklyRanking<br/>Writer
participant Repo as Ranking<br/>Repository
participant DB as Database
Job->>Reader: Read metrics for week
Reader->>Repo: findAllByDateRange(startDate, endDate)
Repo->>DB: Query ProductMetrics
DB-->>Repo: Metrics summary
Repo-->>Reader: ProductMetricsSummary list
Reader->>Reader: Aggregate & calculate scores
loop For each RankedProduct
Reader->>Processor: RankedProduct
Processor->>Processor: Assign rank (1, 2, 3...)
Processor-->>Writer: WeeklyRanking
end
Writer->>Repo: deleteByWeekStartAndWeekEnd()
Repo->>DB: DELETE old rankings
Writer->>Repo: saveAll(rankings)
Repo->>DB: INSERT new rankings
DB-->>Repo: Success
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 2 inconclusive)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
🧹 Nitpick comments (37)
apps/commerce-batch/src/main/resources/application.yml (2)
20-23: 헬스 체크 비활성화가 관찰성에 영향을 줄 수 있습니다.배치 애플리케이션이라도 운영 모니터링을 위해 기본 헬스 인디케이터를 활성화하는 것을 고려해보세요. 작업 실행 상태, 데이터베이스 연결 등의 헬스 체크는 운영 환경에서 유용합니다.
34-50: 빈 프로파일 섹션을 정리하는 것을 고려하세요.dev, qa, prd 프로파일 섹션이 현재 비어있습니다. 향후 프로파일별 설정이 필요할 때 추가하는 것으로 보일러플레이트를 줄일 수 있습니다.
apps/commerce-batch/src/main/java/com/loopers/batch/domain/metrics/ProductMetricsId.java (1)
13-24: 복합 키 인스턴스 생성을 위한 생성자 추가를 권장합니다.현재 protected 기본 생성자만 있어
ProductMetricsId인스턴스를 생성하기 어렵습니다. 테스트 코드나 엔티티 수동 생성 시 불편할 수 있습니다.🔎 전체 인자 생성자 추가 제안
@Embeddable @Getter @EqualsAndHashCode @NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor public class ProductMetricsId implements Serializable {apps/commerce-api/src/main/java/com/loopers/infrastructure/converter/YearMonthAttributeConverter.java (1)
8-20: YearMonth 변환기가 올바르게 구현되었습니다.ISO-8601 형식(
yyyy-MM)을 사용한 표준 구현이며, null 처리도 적절합니다.autoApply = true설정으로 모든YearMonth필드에 자동 적용됩니다.선택적으로 저장 형식을 클래스 JavaDoc에 명시하면 유지보수에 도움이 될 수 있습니다.
apps/commerce-batch/src/main/java/com/loopers/dto/ProductMetricsSummary.java (1)
8-17: DTO 구현이 올바르게 작동합니다.JPQL 프로젝션용 DTO로 잘 설계되었습니다.
선택적으로 필드를
final로 선언하면 불변성을 보장하여 더 안전한 DTO가 됩니다.🔎 불변 DTO로 개선하는 제안
@Getter @AllArgsConstructor public class ProductMetricsSummary { - private Long productId; - private LocalDate date; - private Long likeCount; - private Long stockCount; - private Long viewCount; + private final Long productId; + private final LocalDate date; + private final Long likeCount; + private final Long stockCount; + private final Long viewCount; }apps/commerce-batch/src/main/java/com/loopers/batch/domain/ranking/WeeklyRanking.java (2)
31-37: 명시적인 @column 매핑 추가를 고려하세요.productId, score, weekStart, weekEnd 필드에 @column 어노테이션이 없습니다. Materialized view의 경우 스키마 변경 시 명확성을 위해 명시적인 컬럼 매핑을 권장합니다.
🔎 제안하는 수정
- private Long productId; + @Column(name = "product_id", nullable = false) + private Long productId; - private double score; + @Column(nullable = false) + private double score; - private LocalDate weekStart; + @Column(name = "week_start", nullable = false) + private LocalDate weekStart; - private LocalDate weekEnd; + @Column(name = "week_end", nullable = false) + private LocalDate weekEnd;
39-49: 팩토리 메서드에 유효성 검증 추가를 권장합니다.파라미터 검증(null 체크, weekStart가 weekEnd보다 이전인지, score가 음수가 아닌지 등)이 없어 잘못된 데이터가 생성될 수 있습니다.
🔎 제안하는 수정
public static WeeklyRanking create(int rank, Long productId, double score, LocalDate weekStart, LocalDate weekEnd) { + if (productId == null || weekStart == null || weekEnd == null) { + throw new IllegalArgumentException("productId, weekStart, weekEnd cannot be null"); + } + if (weekStart.isAfter(weekEnd)) { + throw new IllegalArgumentException("weekStart must be before or equal to weekEnd"); + } + if (score < 0) { + throw new IllegalArgumentException("score cannot be negative"); + } + WeeklyRanking weeklyRanking = new WeeklyRanking(); weeklyRanking.rank = rank; weeklyRanking.productId = productId; weeklyRanking.score = score; weeklyRanking.weekStart = weekStart; weeklyRanking.weekEnd = weekEnd; return weeklyRanking; }apps/commerce-batch/src/main/java/com/loopers/infrastructure/converter/YearMonthAttributeConverter.java (1)
16-19: 파싱 예외 처리를 추가하는 것을 권장합니다.
YearMonth.parse()가 DB에 잘못된 형식의 데이터가 있을 경우DateTimeParseException을 던질 수 있습니다. 더 명확한 에러 메시지와 함께 예외 처리를 추가하는 것이 좋습니다.🔎 제안하는 수정
@Override public YearMonth convertToEntityAttribute(String dbData) { - return dbData != null ? YearMonth.parse(dbData) : null; + if (dbData == null) { + return null; + } + try { + return YearMonth.parse(dbData); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("Invalid YearMonth format in database: " + dbData, e); + } }apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRanking.java (1)
19-28: 명시적인 컬럼 매핑 추가를 고려하세요.API 모듈은 읽기 전용이지만, 명시적인 @column 어노테이션을 추가하면 배치 모듈과의 스키마 일관성 확인이 용이합니다. 특히 엔티티 중복 문제를 해결하기 전까지는 더욱 중요합니다.
apps/commerce-batch/src/main/java/com/loopers/batch/domain/RankingWeightReader.java (2)
42-44: 파라미터 순서를 계산 순서와 일치시키세요.메서드 시그니처의 파라미터 순서
(totalLikes, totalSales, totalViews)가 계산식에서 사용되는 순서(totalViews, totalLikes, totalSales)와 다릅니다. 가독성과 유지보수를 위해 일관된 순서를 사용하세요.🔎 제안하는 수정
/** * 총점 계산 */ -public double calculateTotalScore(int totalLikes, int totalSales, int totalViews) { +public double calculateTotalScore(int totalViews, int totalLikes, int totalSales) { return (totalViews * getViewWeight()) + (totalLikes * getLikeWeight()) + (totalSales * getOrderWeight()); }호출하는 곳도 함께 수정해야 합니다.
46-56: 파싱 예외를 더 명확하게 처리하는 것을 고려하세요.
Double.parseDouble()에서 발생하는NumberFormatException과 Redis 조회 실패를 구분하면 디버깅이 더 쉬워집니다.🔎 제안하는 수정
private double getWeight(String field, double defaultValue) { try { Object value = redisTemplate.opsForHash().get(WEIGHT_KEY, field); if (value != null) { - return Double.parseDouble(value.toString()); + try { + return Double.parseDouble(value.toString()); + } catch (NumberFormatException e) { + log.warn("가중치 파싱 실패, 기본값 사용: field={}, value={}, default={}", field, value, defaultValue); + return defaultValue; + } } } catch (Exception e) { log.warn("가중치 조회 실패, 기본값 사용: field={}, default={}", field, defaultValue, e); } return defaultValue; }apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyRankingReader.java (1)
72-87: 집계 로직의 가독성을 개선할 수 있습니다.double 배열을 사용한 집계는 동작하지만, 명명된 필드를 가진 중간 DTO를 사용하면 코드 가독성이 향상됩니다. 예를 들어
AggregatedMetrics(likeCount, stockCount, viewCount)같은 레코드를 사용하면sums[0],sums[1],sums[2]보다 의미가 명확합니다.apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/WeeklyRankingProcessor.java (1)
29-29: 단일 스레드 환경에서 AtomicInteger는 불필요합니다.Spring Batch의
ItemProcessor는 기본적으로 단일 스레드에서 실행되므로,AtomicInteger대신 일반int변수로도 충분합니다. 다만 현재 구현도 동작하므로 우선순위가 낮습니다.apps/commerce-batch/src/main/java/com/loopers/batch/domain/ranking/MonthlyRanking.java (3)
32-32: productId 필드에 제약 조건을 명시하세요.productId는 unique constraint의 일부이며 비즈니스 로직상 필수 값으로 보입니다.
@Column(nullable = false)을 추가하여 데이터베이스 수준에서 제약을 강제하는 것을 권장합니다.🔎 제안하는 수정
+ @Column(nullable = false) private Long productId;
40-49: 팩토리 메서드에 파라미터 검증을 추가하는 것을 고려하세요.
create메서드가 null 파라미터나 유효하지 않은 값(예: 음수 rank)을 검증하지 않습니다. 데이터 무결성을 위해 검증 로직을 추가하는 것을 권장합니다.🔎 제안하는 수정
public static MonthlyRanking create(int rank, Long productId, double score, YearMonth yearMonth) { + if (productId == null) { + throw new IllegalArgumentException("productId는 필수입니다"); + } + if (yearMonth == null) { + throw new IllegalArgumentException("yearMonth는 필수입니다"); + } + if (rank < 0) { + throw new IllegalArgumentException("rank는 0 이상이어야 합니다"); + } + MonthlyRanking monthlyRanking = new MonthlyRanking(); monthlyRanking.rank = rank; monthlyRanking.productId = productId; monthlyRanking.score = score; monthlyRanking.monthPeriod = yearMonth; return monthlyRanking; }
10-23: JPA 엔티티에 equals/hashCode 구현을 고려하세요.JPA 엔티티의 모범 사례로, 특히 컬렉션에서 사용되거나 비교가 필요한 경우 비즈니스 키(productId, monthPeriod)를 기반으로 equals/hashCode를 구현하는 것이 좋습니다.
apps/commerce-batch/src/test/java/com/loopers/job/step/DemoJobE2ETest.java (3)
4-4: 사용하지 않는 코드를 제거하세요.
@RequiredArgsConstructor어노테이션은 생성자 주입할 필드가 없어 불필요합니다. 또한 빈beforeEach()메서드도 사용되지 않으므로 제거하는 것이 좋습니다.🔎 제안하는 수정
package com.loopers.job.step; import com.loopers.batch.job.demo.DemoJobConfig; -import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.BeforeEach;- @BeforeEach - void beforeEach() { - - } - @DisplayName("jobParameter 중 requestDate 인자가 주어지지 않았을 때, demoJob 배치는 실패한다.")Also applies to: 37-40
42-56: 테스트 메서드 이름이 실제 테스트 내용과 일치하지 않습니다.메서드 이름
shouldNotSaveCategories_whenApiError는 카테고리 저장 실패와 API 에러를 암시하지만, 실제로는requestDate파라미터가 누락되었을 때의 실패를 테스트합니다. 더 정확한 이름으로 변경하는 것을 권장합니다 (예:shouldFail_whenRequestDateParameterIsMissing).
28-29: 주석을 간결하게 정리할 수 있습니다.주석이 유용하지만 다소 장황합니다. 더 간결하게 표현할 수 있습니다.
🔎 제안하는 수정
- // IDE 정적 분석 상 [SpringBatchTest] 의 주입보다 [SpringBootTest] 의 주입이 우선되어, 해당 컴포넌트는 없으므로 오류처럼 보일 수 있음. - // [SpringBatchTest] 자체가 Scope 기반으로 주입하기 때문에 정상 동작함. + // Note: @SpringBatchTest가 scope 기반으로 주입하므로 IDE 경고는 무시 가능 @Autowired private JobLauncherTestUtils jobLauncherTestUtils;apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (2)
27-29: System.out.println 대신 로거를 사용하세요.
System.out.println은 프로덕션 환경에서 로그 레벨 제어, 로그 수집 등이 불가능합니다. SLF4J 로거를 사용하는 것을 권장합니다.🔎 제안하는 수정
package com.loopers.batch.job.demo.step; import com.loopers.batch.job.demo.DemoJobConfig; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.StepContribution;+@Slf4j @StepScope @ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME)@Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { if (requestDate == null) { throw new RuntimeException("requestDate is null"); } - System.out.println("Demo Tasklet 실행 (실행 일자 : " + requestDate + ")"); + log.info("Demo Tasklet 실행 (실행 일자 : {})", requestDate); Thread.sleep(1000); - System.out.println("Demo Tasklet 작업 완료"); + log.info("Demo Tasklet 작업 완료"); return RepeatStatus.FINISHED; }
24-26: 더 구체적인 예외 타입을 사용하고 로깅을 추가하세요.제네릭한
RuntimeException대신IllegalArgumentException또는 커스텀 예외를 사용하고, 예외 발생 전에 로그를 남기는 것을 권장합니다.🔎 제안하는 수정
if (requestDate == null) { - throw new RuntimeException("requestDate is null"); + log.error("필수 파라미터 requestDate가 누락되었습니다"); + throw new IllegalArgumentException("requestDate는 필수 파라미터입니다"); }apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
23-43: 에러 모니터링 로직이 잘 구현되어 있습니다.예외 메시지 수집 및 로깅 처리가 적절합니다. Line 39의 주석에서 언급된 Slack 등 외부 채널로의 알림 전송 기능을 구현할 수 있도록 도와드릴까요?
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRanking.java (1)
12-12: 테이블 인덱스 정의를 추가하는 것을 권장합니다.
monthPeriod와rank필드에 대한 쿼리 성능 향상을 위해 인덱스를 정의하면 좋겠습니다. 특히 기간별 랭킹 조회 시 성능에 큰 영향을 줄 수 있습니다.🔎 인덱스 추가 제안
-@Table(name = "mv_product_rank_monthly") +@Table(name = "mv_product_rank_monthly", + indexes = { + @Index(name = "idx_monthly_period_rank", columnList = "month_period, monthly_rank"), + @Index(name = "idx_monthly_product", columnList = "product_id, month_period") + } +)apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankingWriter.java (2)
48-49: 불필요한 캐스팅과 스트림 연산을 제거할 수 있습니다.
chunk.getItems()는 이미List<? extends WeeklyRanking>타입이므로, 스트림 변환과 캐스팅 없이 직접 전달할 수 있습니다.🔎 간소화 제안
- log.info("주간 랭킹 저장: count={}", chunk.size()); - weeklyRankingRepository.saveAll(chunk.getItems().stream().map(item -> (WeeklyRanking) item).toList()); + log.info("주간 랭킹 저장: count={}", chunk.size()); + weeklyRankingRepository.saveAll(chunk.getItems());
52-64: 기본값 대신 필수 파라미터 검증을 고려해주세요.현재
startDate와endDate가 없을 때 현재 주의 월요일/일요일로 기본값을 사용하고 있습니다. 배치 작업의 명확성을 위해, 필수 파라미터가 누락된 경우 예외를 발생시키는 것이 더 안전할 수 있습니다.🔎 파라미터 검증 추가 제안
private LocalDate parseStartDate() { if (startDateStr != null && !startDateStr.isBlank()) { return LocalDate.parse(startDateStr, DATE_FORMATTER); } - return LocalDate.now().with(DayOfWeek.MONDAY); + throw new IllegalArgumentException("startDate 파라미터가 필수입니다"); } private LocalDate parseEndDate() { if (endDateStr != null && !endDateStr.isBlank()) { return LocalDate.parse(endDateStr, DATE_FORMATTER); } - return LocalDate.now().with(DayOfWeek.SUNDAY); + throw new IllegalArgumentException("endDate 파라미터가 필수입니다"); }apps/commerce-batch/src/main/java/com/loopers/batch/domain/metrics/ProductMetrics.java (1)
10-35: 인스턴스 생성을 위한 빌더 패턴 추가를 권장합니다.편의 접근자 메서드들은 좋은 설계이지만, 엔티티 인스턴스 생성을 위한 빌더나 팩토리 메서드가 없어서 테스트나 데이터 생성 시 불편할 수 있습니다.
🔎 빌더 패턴 추가 제안
+import lombok.AllArgsConstructor; +import lombok.Builder; + @Entity @Getter +@Builder @Table(name = "product_metrics") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class ProductMetrics {apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingJpaRepository.java (1)
13-15: 트랜잭션 컨텍스트 요구사항을 명확히 하는 것을 권장합니다.
@Modifying쿼리는 트랜잭션 내에서 실행되어야 합니다. 현재 호출자(WeeklyRankingWriter)가@Transactional을 가지고 있어 동작하지만, 이 메서드가 다른 곳에서 호출될 가능성을 고려하여 JavaDoc으로 트랜잭션 필요성을 문서화하거나, 메서드에@Transactional을 직접 추가하는 것을 고려해주세요.🔎 트랜잭션 어노테이션 추가 제안
+import org.springframework.transaction.annotation.Transactional; + public interface WeeklyRankingJpaRepository extends JpaRepository<WeeklyRanking, Long> { + @Transactional @Modifying @Query("DELETE FROM WeeklyRanking w WHERE w.weekStart = :weekStart AND w.weekEnd = :weekEnd") void deleteByWeekStartAndWeekEnd(@Param("weekStart") LocalDate weekStart, @Param("weekEnd") LocalDate weekEnd); }apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyRankingReader.java (1)
75-90:double[]대신long[]사용 권장
likeCount,stockCount,viewCount는 정수 값을 합산하므로double[]대신long[]을 사용하면 불필요한 타입 변환을 피하고 정밀도 문제를 방지할 수 있습니다.🔎 리팩토링 제안
Map<Long, double[]> aggregatedMap = summaries.stream() + Map<Long, long[]> aggregatedMap = summaries.stream() .collect(Collectors.groupingBy( ProductMetricsSummary::getProductId, Collectors.collectingAndThen( Collectors.toList(), list -> { - double[] sums = new double[3]; + long[] sums = new long[3]; for (ProductMetricsSummary s : list) { sums[0] += s.getLikeCount(); sums[1] += s.getStockCount(); sums[2] += s.getViewCount(); } return sums; } ) ));apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankingWriter.java (1)
44-45: 불필요한 캐스팅 제거를 권장합니다.
chunk.getItems()는 이미List<? extends MonthlyRanking>을 반환하므로,MonthlyRanking으로의 명시적 캐스팅은 불필요합니다. Spring Data JPA의saveAll은Iterable<S extends T>를 허용합니다.🔎 제안된 수정
log.info("월간 랭킹 저장: count={}", chunk.size()); - monthlyRankingRepository.saveAll(chunk.getItems().stream().map(item -> (MonthlyRanking) item).toList()); + monthlyRankingRepository.saveAll(chunk.getItems());apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingJpaRepository.java (1)
14-23: 네이티브 쿼리의 파라미터 타입 불일치 확인이 필요합니다.
findByPeriodWithPagination메서드는monthPeriod를String으로 받지만, 같은 리포지토리의 다른 메서드들(findByProductIdAndMonthPeriod,countByMonthPeriod)은YearMonth를 직접 사용합니다. 호출자가 일관된 타입을 사용할 수 있도록 통일하는 것이 좋습니다.네이티브 쿼리에서
YearMonth를 직접 사용하려면YearMonthAttributeConverter가 적용되도록@Convert또는 파라미터 변환이 필요합니다.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
48-57: 잘못된 period 값에 대한 로깅 추가를 고려해보세요.현재 유효하지 않은
period값이 전달되면 조용히DAILY로 기본값 처리됩니다. 디버깅 및 모니터링을 위해 잘못된 입력값을 로깅하는 것이 좋습니다.🔎 제안된 수정
private RankingPeriod parsePeriod(String period) { if (period == null || period.isBlank()) { return RankingPeriod.DAILY; } try { return RankingPeriod.valueOf(period.toUpperCase()); } catch (IllegalArgumentException e) { + log.warn("Invalid ranking period '{}', defaulting to DAILY", period); return RankingPeriod.DAILY; } }참고: 클래스에
@Slf4j어노테이션 추가가 필요합니다.apps/commerce-batch/src/main/java/com/loopers/interfaces/batch/BatchController.java (2)
18-34: 주간 랭킹 Job과 월간 랭킹 Job 간의 응답 처리 불일치.
runWeeklyRankingJob은 실행 상태를 확인하지 않고 바로 "started"를 반환하지만,runMonthlyRankingJob은JobExecution상태를 확인합니다. 동일한 패턴으로 일관성 있게 처리하는 것이 좋습니다.🔎 제안된 수정
@PostMapping("/weekly-ranking") public ResponseEntity<String> runWeeklyRankingJob( @RequestParam String startDate, @RequestParam String endDate ) throws Exception { JobParameters params = new JobParametersBuilder() .addString("startDate", startDate) .addString("endDate", endDate) .addLong("timestamp", System.currentTimeMillis()) // 중복 실행 방지 .toJobParameters(); Job job = jobRegistry.getJob("weeklyRankingJob"); - jobLauncher.run(job, params); + JobExecution execution = jobLauncher.run(job, params); - return ResponseEntity.ok("WeeklyRankingJob started!"); + if (execution.getStatus() == BatchStatus.FAILED) { + return ResponseEntity.status(500).body("weeklyRankingJob failed!"); + } + + return ResponseEntity.ok("weeklyRankingJob started!"); }
19-22: 입력 파라미터 유효성 검증 부재.
startDate,endDate,yearMonth파라미터에 대한 형식 검증이 없습니다. 잘못된 형식의 입력이 배치 작업 내부에서 파싱 오류를 일으킬 수 있으며, 이 경우 사용자에게 불명확한 오류 메시지가 전달될 수 있습니다.🔎 제안 예시 (yearMonth 검증)
private void validateYearMonth(String yearMonth) { try { YearMonth.parse(yearMonth, DateTimeFormatter.ofPattern("yyyy-MM")); } catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid yearMonth format. Expected: yyyy-MM"); } }Also applies to: 37-38
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
134-140:@Transactional(readOnly = true)어노테이션 추가를 권장합니다.
getProductRank(Long productId, LocalDate date, RankingPeriod period)메서드는 DB 조회를 수행하지만@Transactional어노테이션이 없습니다. 다른 조회 메서드들(getRankingPage,getTopN)과의 일관성 및 읽기 전용 최적화를 위해 추가하는 것이 좋습니다.🔎 제안된 수정
+ @Transactional(readOnly = true) public Long getProductRank(Long productId, LocalDate date, RankingPeriod period) { return switch (period) { case WEEKLY -> rankingService.getWeeklyRank(productId, date); case MONTHLY -> rankingService.getMonthlyRank(productId, date); default -> rankingService.getRank(productId, date); }; }apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/MonthlyRankingProcessor.java (2)
28-39:parseYearMonth()호출을 캐싱하여 성능을 개선할 수 있습니다.
parseYearMonth()가 매process()호출마다 실행되지만,yearMonth값은 Job 파라미터로 고정되어 있어 한 번만 파싱하면 됩니다. 필드에 캐싱하여 불필요한 파싱을 방지할 수 있습니다.🔎 제안된 수정
+ private YearMonth cachedYearMonth; + @Override public MonthlyRanking process(RankedProduct item) { - YearMonth yearMonth = parseYearMonth(); + if (cachedYearMonth == null) { + cachedYearMonth = parseYearMonth(); + } int rank = rankCounter.incrementAndGet(); return MonthlyRanking.create( rank, item.productId(), item.score(), - yearMonth + cachedYearMonth ); }
25-25:AtomicInteger대신 일반int사용을 고려해보세요.Spring Batch의
ItemProcessor.process()는 단일 스레드에서 순차적으로 호출되므로,AtomicInteger의 원자성 보장이 필요하지 않습니다. 단순한int카운터로 충분합니다.🔎 제안된 수정
- private final AtomicInteger rankCounter = new AtomicInteger(0); + private int rankCounter = 0; @Override public MonthlyRanking process(RankedProduct item) { YearMonth yearMonth = parseYearMonth(); - int rank = rankCounter.incrementAndGet(); + int rank = ++rankCounter; return MonthlyRanking.create(apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.java (1)
14-16: 모듈 간 동일한 클래스명 사용에 대한 검토가 필요합니다.
commerce-api와commerce-batch모듈 모두에RankingRepositoryImpl이라는 동일한 이름의 클래스가 존재합니다. 이는 코드 탐색과 유지보수를 어렵게 만들 수 있습니다.PR 설명에서 언급하신 것처럼, 다음 옵션들을 고려해보세요:
- 배치 모듈의 클래스명을 더 구체적으로 변경 (예:
BatchRankingRepositoryImpl또는RankingWriteRepositoryImpl)- API는 DTO/Projection 사용, Batch가 엔티티를 소유하도록 통합
- 공유 도메인 모듈로 엔티티를 분리하여 중복 제거
읽기/쓰기 책임이 명확히 분리된다면 1번 옵션(클래스명 구체화)이 가장 간단한 개선책이 될 수 있습니다.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (52)
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/application/ranking/RankingPageInfo.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRanking.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRanking.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/converter/YearMonthAttributeConverter.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingJpaRepository.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-batch/build.gradle.ktsapps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.javaapps/commerce-batch/src/main/java/com/loopers/batch/domain/RankingWeightReader.javaapps/commerce-batch/src/main/java/com/loopers/batch/domain/metrics/ProductMetrics.javaapps/commerce-batch/src/main/java/com/loopers/batch/domain/metrics/ProductMetricsId.javaapps/commerce-batch/src/main/java/com/loopers/batch/domain/metrics/ProductMetricsRepository.javaapps/commerce-batch/src/main/java/com/loopers/batch/domain/ranking/MonthlyRanking.javaapps/commerce-batch/src/main/java/com/loopers/batch/domain/ranking/MonthlyRankingRepository.javaapps/commerce-batch/src/main/java/com/loopers/batch/domain/ranking/WeeklyRanking.javaapps/commerce-batch/src/main/java/com/loopers/batch/domain/ranking/WeeklyRankingRepository.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/MonthlyRankingProcessor.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/processor/WeeklyRankingProcessor.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyRankingReader.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyRankingReader.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/MonthlyRankingWriter.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankingWriter.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.javaapps/commerce-batch/src/main/java/com/loopers/dto/ProductMetricsSummary.javaapps/commerce-batch/src/main/java/com/loopers/dto/RankedProduct.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/converter/YearMonthAttributeConverter.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/MonthlyRankingJpaRepository.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingJpaRepository.javaapps/commerce-batch/src/main/java/com/loopers/interfaces/batch/BatchController.javaapps/commerce-batch/src/main/resources/application.ymlapps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTests.javaapps/commerce-batch/src/test/java/com/loopers/job/step/DemoJobE2ETest.javaapps/commerce-streamer/build.gradle.ktssettings.gradle.kts
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingJpaRepository.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingJpaRepository.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-batch/src/main/java/com/loopers/batch/listener/JobListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
📚 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-batch/src/main/java/com/loopers/batch/listener/JobListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java
📚 Learning: 2025-11-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product 엔티티 (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)는 유즈케이스별로 의도적으로 다른 락 전략을 사용한다: 좋아요 기능에는 비관적 락(findByIdForUpdate)을, 재고 차감에는 낙관적 락(Version + 재시도)을 사용한다.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/batch/domain/metrics/ProductMetrics.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/batch/domain/metrics/ProductMetrics.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.yml
🔇 Additional comments (48)
apps/commerce-batch/src/main/resources/application.yml (3)
1-18: 배치 애플리케이션 기본 설정이 적절합니다.
web-application-type: none으로 웹 서버를 비활성화하고,job.name: ${job.name:NONE}템플릿으로 기본적으로 모든 작업 실행을 방지하는 설정이 Spring Batch 모범 사례를 따르고 있습니다.
25-32: 로컬/테스트 환경의 스키마 자동 초기화 설정이 적절합니다.개발 및 테스트 환경에서
initialize-schema: always를 사용하여 배치 메타데이터 테이블을 자동 생성하는 것은 합리적인 접근입니다.
52-54: Springdoc 비활성화가 적절합니다.배치 애플리케이션에서 웹 엔드포인트가 없으므로 API 문서 생성을 비활성화하는 것이 맞습니다.
apps/commerce-batch/build.gradle.kts (4)
10-10: 배치 모듈에서 웹 스타터 의존성 확인 필요배치 모듈에
spring-boot-starter-web을 포함하는 것은 일반적이지 않습니다. PR 설명에서 "manual batch controller"와 "BatchController" 엔드포인트를 언급하고 있어 의도적인 것으로 보이나, 다음을 확인해 주세요:
- 수동 트리거용 REST API가 필요한 경우, 이 접근 방식이 적절합니다.
- 만약 순수 스케줄링 기반 배치만 필요하다면, 웹 의존성을 제거하고 경량화하는 것을 고려해 보세요.
12-14: 배치 의존성 구성 적절함Spring Batch 의존성이 올바르게 구성되어 있습니다. 테스트 의존성도 적절하게
testImplementation스코프로 설정되었습니다.
21-23: 테스트 픽스처 구성 적절함JPA 및 Redis 모듈의 테스트 픽스처를 활용하여 배치 컴포넌트의 통합 테스트를 지원하는 구성이 적절합니다.
17-19: QueryDSL 의존성 버전 관리 검증 필요
querydsl-apt::jakarta구문의 더블 콜론 표기법(classifier)은 버전이 BOM, 플랫폼, 또는 명시적 버전 선언에서 관리될 때 사용됩니다. 루트 빌드 파일에서 QueryDSL 버전 관리 여부와 Spring Boot BOM이 이를 포함하는지 확인해 주세요. 버전 관리가 없으면 빌드가 실패할 수 있습니다.apps/commerce-streamer/build.gradle.kts (1)
14-14: OpenAPI 문서화 의존성 추가 승인.랭킹 API 확장에 맞춰 SpringDoc OpenAPI UI 의존성을 추가한 것은 적절합니다.
webmvc-ui아티팩트는 Spring MVC 기반 애플리케이션에 적합한 선택입니다.SpringDoc OpenAPI 자체에는 직접적인 공개 CVE가 없으나, 프로젝트 속성
springDocOpenApiVersion이 정의되어 있는지 확인이 필요합니다. 또한 사용 중인 특정 버전의 전이적 의존성(transitive dependencies)에 대한 보안 검증도 권장됩니다.settings.gradle.kts (1)
6-6: 새 배치 모듈 등록이 올바르게 추가되었습니다.기존 모듈 네이밍 패턴과 일관성 있게
commerce-batch모듈이 추가되었습니다.apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingPeriod.java (1)
3-7: 기간 타입 열거형이 명확하게 정의되었습니다.일별, 주별, 월별 랭킹 조회를 구분하기 위한 열거형이 깔끔하게 구현되었습니다.
apps/commerce-batch/src/main/java/com/loopers/batch/domain/ranking/WeeklyRanking.java (1)
11-20: 테이블 설계가 잘 되어 있습니다.인덱스와 유니크 제약조건이 랭킹 조회 및 중복 방지에 적합하게 구성되어 있습니다.
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (2)
15-19: 전역 타임존 설정이 적절하게 구성되었습니다.배치 애플리케이션에서 일관된 날짜/시간 처리를 위해 전역 타임존을 설정하는 것은 적절합니다. 다만,
TimeZone.setDefault()는 JVM 전체에 영향을 미치므로 문서화가 중요합니다.
21-24: 배치 애플리케이션의 종료 패턴이 올바릅니다.
SpringApplication.exit()로 컨텍스트를 정리하고 종료 코드를 얻은 후System.exit()로 JVM을 종료하는 패턴은 스케줄러와의 통합에 적합합니다.apps/commerce-batch/src/main/java/com/loopers/batch/domain/RankingWeightReader.java (2)
8-25: 문서화와 상수 정의가 명확합니다.commerce-streamer와의 호환성 언급과 기본 가중치 값이 잘 문서화되어 있습니다.
27-37: weight 메서드의 호출 패턴 확인이 필요합니다.이 메서드들이 배치 처리 루프 내에서 반복 호출되는지 확인하세요. 호출 빈도가 높은 경우, 배치 작업 시작 시 한 번만 조회하여 캐싱하는 방안을 검토하세요. 특히
getWeight()메서드 구현과 실제 사용처를 분석하여 최적화 필요성을 판단하세요.apps/commerce-batch/src/main/java/com/loopers/infrastructure/converter/YearMonthAttributeConverter.java (1)
8-8: YearMonth 필드에 autoApply 적용 시 형식 일관성 확인 필요
@Converter(autoApply = true)는 프로젝트 내 모든 YearMonth 필드에 자동으로 "yyyy-MM" 형식 변환을 적용합니다. JPA 모범 사례에 따르면, autoApply는 변환이 해당 Java 타입의 모든 사용처에서 동일하게 적용되어야 할 때만 사용하는 것이 권장됩니다. 서로 다른 날짜 형식이 필요한 YearMonth 필드가 있거나, 특정 필드에만 다른 변환을 적용해야 하는 경우 충돌이 발생할 수 있습니다.다음을 확인하세요:
- 프로젝트 내 모든 YearMonth 필드가 "yyyy-MM" 형식을 사용하는지
- 다른 형식의 YearMonth 변환이 필요한 필드가 있는지
- 필드별
@Convert명시가 필요한 경우가 있는지일관된 형식이 확실하면 autoApply=true는 적절하지만, 예외가 있다면 명시적인
@Convert어노테이션 사용을 고려하세요.apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTests.java (1)
6-11: LGTM!애플리케이션 컨텍스트 로드 테스트가 정상적으로 구성되어 있습니다. 향후 실제 배치 작업(주간/월간 랭킹 작업)에 대한 통합 테스트를 추가하면 더욱 견고한 테스트 커버리지를 확보할 수 있습니다.
apps/commerce-batch/src/main/java/com/loopers/batch/domain/metrics/ProductMetricsRepository.java (1)
8-9: LGTM!리포지토리 인터페이스가 명확하고 간결하게 정의되어 있습니다. 날짜 범위 기반 메트릭 조회를 위한 메서드 시그니처가 적절하며, 프로젝션 DTO(
ProductMetricsSummary)를 반환하여 필요한 데이터만 조회하는 것이 좋습니다.apps/commerce-batch/src/main/java/com/loopers/dto/RankedProduct.java (1)
3-6:productId와score필드의 null 안전성 및 유효성 검증 확인 필요합니다.
productId가Long(nullable) 타입이므로 null 값이 허용되지만, 배치 프로세서(MonthlyRankingProcessor,WeeklyRankingProcessor)에서 이를 사용할 때 NPE가 발생할 수 있습니다. 프로세서 구현에서 명시적인 null 검증이 있는지 확인하고,score필드의 유효 범위(음수 허용 여부 등) 제약 조건을 명확히 해야 합니다.apps/commerce-batch/src/main/java/com/loopers/batch/domain/ranking/MonthlyRankingRepository.java (1)
6-10: 리포지토리 인터페이스 설계가 명확합니다.
YearMonth타입을 사용하여 월별 기간을 표현하는 것이 타입 안전성 측면에서 우수하며,deleteByMonthPeriod→saveAll패턴은 기존 랭킹을 교체하는 전략으로 적절합니다.다만 배치 writer(
MonthlyRankingWriter)에서 이 두 메서드가 단일 트랜잭션 내에서 실행되는지 확인이 필요합니다. 삭제와 저장 사이에 트랜잭션이 분리되면 일관성 문제가 발생할 수 있으므로,MonthlyRankingWriter의 트랜잭션 경계를 검토해주세요.apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
11-21: 구현이 깔끔합니다.표준 위임 패턴을 사용하여 도메인 저장소를 구현했습니다. 코드가 명확하고 적절합니다.
apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingJpaRepository.java (1)
11-16: AI 요약과 코드 간 불일치가 있습니다.AI 요약에는
findByPeriodWithPagination,findByProductIdAndMonthPeriod,countByMonthPeriod메서드가 언급되어 있지만, 제공된 코드에는 존재하지 않습니다. 이는 코드가 불완전하거나 요약이 부정확함을 나타냅니다.apps/commerce-batch/src/main/java/com/loopers/batch/domain/ranking/WeeklyRankingRepository.java (1)
6-11: 인터페이스가 명확합니다.도메인 저장소 인터페이스가 적절하게 정의되어 있으며, 메서드 시그니처가 명확합니다.
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/MonthlyRankingReader.java (2)
26-26: 수동 확인이 필요합니다.
ProductMetricsJpaRepository타입이 사용되고 있지만 import 문이 없는지 확인이 필요합니다. 또한 다음 사항도 함께 검토하세요:
- ProductMetricsJpaRepository의 정확한 패키지 위치 확인
- 63번 줄에서 호출하는 메서드 이름 (
findAllByProductIdAndDateBetween)이 리포지토리에 실제로 존재하는지 확인- 필요한 import 문이 누락되었는지 확인
63-63: 메서드 이름 불일치를 확인하세요.
findAllByProductIdAndDateBetween메서드를 호출하고 있지만,ProductMetricsRepositoryImpl에 정의된 실제 메서드 시그니처를 확인해야 합니다. 메서드 이름이 일치하지 않으면 컴파일 오류를 유발할 수 있습니다.apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java (1)
43-44: 트랜잭션 매니저 선택을 검토하세요.
ResourcelessTransactionManager는 트랜잭션 지원이 없는 매니저입니다. 현재DemoTasklet은 단순 작업만 수행하므로 적절하지만, 향후 데이터베이스 작업이나 트랜잭션이 필요한 로직을 추가할 경우 적절한PlatformTransactionManager로 변경해야 합니다.apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
18-21: LGTM!Step 시작 로깅 로직이 명확하고 적절합니다.
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/writer/WeeklyRankingWriter.java (1)
24-34: 일회성 삭제 플래그 패턴이 적절합니다.
@StepScope가 Step 실행마다 새 인스턴스를 생성하므로,deletedExisting플래그를 사용한 일회성 삭제 로직이 올바르게 동작합니다.apps/commerce-batch/src/main/java/com/loopers/batch/domain/metrics/ProductMetrics.java (1)
10-27: 복합 키 패턴과 델타 필드 구조가 적절합니다.
@EmbeddedId를 사용한 복합 키 설계와 델타 필드들의 구조가 메트릭 집계에 적합합니다.apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java (1)
22-59: LGTM! Spring Batch Job 설정이 적절합니다.
ConditionalOnProperty로 선택적 활성화RunIdIncrementer로 Job 인스턴스 관리CHUNK_SIZE = 100이 Reader의TOP_N = 100과 일치하여 단일 청크로 처리- Job/Step 리스너가 적절히 연결됨
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingRepository.java (1)
8-23: LGTM! 도메인 레포지토리 인터페이스가 깔끔하게 설계되었습니다.
- 주간/월간 랭킹에 대한 메서드가 명확하게 분리됨
- 주간은
LocalDate쌍, 월간은YearMonth를 사용하여 각 기간의 특성을 잘 반영함- Spring Data 의존성 없이 도메인 계층의 순수성 유지
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
67-82: LGTM! 하위 호환성을 고려한 좋은 설계입니다.
TopNResponse.of메서드의 오버로드를 통해 기존 코드와의 하위 호환성을 유지하면서 새로운period파라미터를 추가했습니다.RankingPeriod.DAILY를 기본값으로 사용한 것은 적절합니다.apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java (1)
13-31: LGTM! 쿼리 프로젝션이 잘 구현되었습니다.JPQL DTO 프로젝션 패턴이 적절하게 사용되었습니다. 한 가지 확인 필요:
ProductMetrics엔티티의likesDelta,salesDelta,viewsDelta필드 타입을 확인하여, 이미Long타입이라면CAST(... AS long)구문을 제거할 수 있습니다.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (1)
29-31:period파라미터의 유효성 검증 구현 확인 필요
period파라미터가String타입으로 정의되어 있어daily,weekly,monthly외의 잘못된 값이 전달될 수 있습니다. Controller 레벨에서RankingPeriod.valueOf()변환 또는 다른 검증 메커니즘이 적절하게 구현되어 있는지 확인이 필요합니다. 가능하면enum타입으로 파라미터를 정의하는 것을 고려해 주세요.apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingJpaRepository.java (1)
14-24: Native 쿼리의 데이터베이스 의존성 고려
LIMIT :limit OFFSET :offset구문은 MySQL/PostgreSQL에 특화된 문법으로, 다른 데이터베이스로의 마이그레이션 시 호환성 문제가 발생할 수 있습니다. 현재 프로젝트에서 특정 데이터베이스만 지원하는 경우 문제가 없지만, 향후 이식성이 필요하다면 Spring Data의Pageable인터페이스 사용을 고려해 주세요.또한
weekStart와weekEnd컬럼에 대한 복합 인덱스 설정을 확인하고, 필요시 추가하여 페이지네이션 성능을 최적화해 주세요.apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyRankingReader.java (1)
110-124: 주간 랭킹 기본값 로직 재검토 권장현재 구현에서
parseStartDate()와parseEndDate()는 당일 기준 현재 주의 월요일부터 일요일까지를 반환합니다. 배치 작업이 주중(예: 수요일)에 실행되면 아직 도래하지 않은 날짜의 데이터까지 포함하게 되어, 일반적인 주간 랭킹 집계 관행과 맞지 않을 수 있습니다. 배치 실행 일정을 확인하고, 필요시 지난 주 데이터를 집계하도록 로직을 수정하는 것을 검토해 주세요.apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java (1)
22-59: 잘 구성된 Spring Batch Job 설정입니다.
ConditionalOnProperty를 사용하여 job 단위로 빈 활성화를 제어하고, chunk 기반 처리와 리스너 구성이 적절합니다.RunIdIncrementer를 통해 동일 파라미터로도 재실행이 가능합니다.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
35-48: 기간별 랭킹 조회 로직이 깔끔하게 구현되었습니다.
switch표현식을 활용하여RankingPeriod에 따른 분기 처리가 명확하고 확장 가능한 구조입니다.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingPageInfo.java (1)
4-36: LGTM! 하위 호환성을 유지하는 깔끔한 구현입니다.
RankingPeriod를 추가하면서도 기존 API 호출을 위한 기본값(DAILY) 제공 팩토리 메서드를 유지하여 하위 호환성을 잘 보장했습니다.apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.java (2)
19-32: LGTM! 주간 랭킹 조회 메서드가 명확하게 구현되었습니다.페이지네이션, 개별 상품 조회, 카운트 조회 메서드가 적절하게 위임되고 있습니다.
34-47:month_period컬럼의 실제 저장 형식 확인 필요Line 36에서
period.toString()을 사용하고 있습니다.YearMonth.toString()은 표준적으로 "yyyy-MM" 형식을 반환하지만,MonthlyRanking엔티티의month_period컬럼 정의와findByPeriodWithPagination()메서드의 실제 쿼리 구현을 확인하여 다음을 검증하세요:
month_period컬럼이 실제로 "yyyy-MM" 형식으로 저장되는지- JPA 쿼리 메서드가
YearMonth형식 변환을 올바르게 처리하는지apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingCommand.java (2)
30-39: LGTM! 기간 파싱 로직이 안전하게 구현되었습니다.null, 빈 문자열, 잘못된 값에 대해 모두
DAILY로 기본값 처리하며, 대소문자 구분 없이 파싱하는 것이 사용자 친화적입니다.
16-24: 페이지네이션 파라미터 검증이 제거되었습니다. 다른 레이어에서 검증하는지 확인하세요.
RankingCommand.of()메서드에서page와size파라미터에 대한 제약이 없습니다. 제거된MAX_PAGE_SIZE,DEFAULT_PAGE_SIZE등의 검증 로직이 컨트롤러 레이어나 다른 곳에서 수행되는지 확인하세요:
page >= 0size > 0 && size <= MAX_SIZE음수값이나 과도하게 큰 값이 전달될 수 있으므로 API 계층에서 유효성 검사를 보장해야 합니다.
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (4)
20-24: LGTM! 상수 추출로 가독성과 유지보수성이 향상되었습니다.Redis 키 접두사와 날짜 포맷터를 상수로 추출하여 일관성을 보장합니다.
26-87: LGTM! 일일 랭킹 메서드가 새로운 상수를 사용하도록 리팩토링되었습니다.기존 로직은 유지하면서 키 생성 방식만 일관되게 개선했습니다.
130-165: LGTM! 월간 랭킹 조회 로직이 주간 랭킹과 일관된 패턴으로 구현되었습니다.
YearMonth를 사용한 월간 기간 처리가 명확하고, 페이지네이션, 개별 조회, 카운트 메서드가 모두 적절하게 구현되었습니다.
89-128: 주간 랭킹 조회 로직의 주간 경계 일관성 확인 필요라인 90-91에서
DayOfWeek.MONDAY와DayOfWeek.SUNDAY를 사용하여 주간 범위를 설정하는 것은 올바른 구현입니다. 다만 배치 작업에서 주간 랭킹을 계산할 때 동일한 주간 경계(월요일 시작, 일요일 종료)를 사용하는지 확인하세요. 배치와 API에서 주간 범위 정의가 다르면 데이터 불일치가 발생할 수 있습니다.apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.java (1)
21-40: 배치 쓰기 작업의 트랜잭션 범위 확인이 필요합니다.이 repository의 메서드들이 delete-then-insert 패턴으로 멱등성을 보장하려면, 배치 Job 또는 Step 레벨에서 명시적인
@Transactional경계가 설정되어야 합니다. 부분 실패 시 데이터 일관성을 보장하기 위해 다음을 확인하세요:
- 배치 작업을 호출하는 Job/Step 클래스에
@Transactional어노테이션이 있는지- transaction manager가 적절히 설정되어 있는지
- 삭제와 저장 작업이 하나의 트랜잭션으로 처리되는지
| @Entity | ||
| @Getter | ||
| @Table(name = "mv_product_rank_monthly") | ||
| @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) | ||
| public class MonthlyRanking { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(name = "monthly_rank") | ||
| private int rank; | ||
|
|
||
| private Long productId; | ||
|
|
||
| private double score; | ||
|
|
||
| @Convert(converter = YearMonthAttributeConverter.class) | ||
| @Column(name = "month_period") | ||
| private YearMonth monthPeriod; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
인스턴스 생성을 위한 빌더 또는 팩토리 메서드 추가를 고려해주세요.
현재 protected 기본 생성자만 제공되어 있어, 배치 작업에서 MonthlyRanking 인스턴스를 생성하기 어렵습니다. 공개 생성자, 빌더 패턴, 또는 정적 팩토리 메서드를 추가하는 것을 권장합니다.
🔎 빌더 패턴 추가 제안
+import lombok.Builder;
+
@Entity
@Getter
+@Builder
@Table(name = "mv_product_rank_monthly")
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
+@AllArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class MonthlyRanking {🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRanking.java
around lines 10 to 30, the entity only exposes a protected no-arg constructor
which makes creating instances from batch code difficult; add a public
constructor or a static factory method and/or a Lombok @Builder to allow easy,
explicit instance creation with required fields (id optional) — implement a
public constructor or static of(...) that accepts rank, productId, score,
monthPeriod (and optionally id), or annotate the class with @Builder and add a
public all-args constructor so callers can construct MonthlyRanking instances in
batch jobs.
| @Entity | ||
| @Getter | ||
| @Table(name = "mv_product_rank_weekly") | ||
| @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
엔티티 정의가 배치 모듈과 다릅니다.
이 엔티티는 배치 모듈의 WeeklyRanking과 동일한 테이블을 매핑하지만 인덱스와 제약조건 정의가 없습니다. API 모듈은 읽기 전용이므로 이러한 정의가 필요하지 않지만, 엔티티 중복 자체가 문제입니다 (배치 모듈 파일 리뷰 참조).
| @Entity | ||
| @Getter | ||
| @Table( | ||
| name = "mv_product_rank_weekly", | ||
| indexes = { | ||
| @Index(name = "idx_week", columnList = "week_start, week_end"), | ||
| @Index(name = "idx_score", columnList = "score") | ||
| }, | ||
| uniqueConstraints = { | ||
| @UniqueConstraint(name = "uq_weekly_rank", columnNames = {"product_id", "week_start", "week_end"}) | ||
| } | ||
| ) | ||
| @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) | ||
| public class WeeklyRanking { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(name = "weekly_rank") | ||
| private int rank; | ||
|
|
||
| private Long productId; | ||
|
|
||
| private double score; | ||
|
|
||
| private LocalDate weekStart; | ||
|
|
||
| private LocalDate weekEnd; | ||
|
|
||
| public static WeeklyRanking create(int rank, Long productId, double score, LocalDate weekStart, LocalDate weekEnd) { | ||
| WeeklyRanking weeklyRanking = new WeeklyRanking(); | ||
|
|
||
| weeklyRanking.rank = rank; | ||
| weeklyRanking.productId = productId; | ||
| weeklyRanking.score = score; | ||
| weeklyRanking.weekStart = weekStart; | ||
| weeklyRanking.weekEnd = weekEnd; | ||
|
|
||
| return weeklyRanking; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
엔티티 중복을 해소하는 것을 권장합니다.
WeeklyRanking 엔티티가 batch 모듈과 API 모듈에 각각 존재합니다. 이로 인해:
- 스키마 변경 시 두 곳을 동기화해야 하는 유지보수 부담 증가
- 두 엔티티가 diverge할 위험 존재 (batch는 factory와 제약조건 보유, API는 bare-bones)
PR 목표에서 언급하신 대로, 다음 옵션을 고려하세요:
- Batch 모듈이 엔티티를 소유하고 API는 DTO/projection 사용
- 공통 모듈에 엔티티를 두고 양쪽에서 의존
batch 작업이 write를 담당하므로 옵션 1(Batch가 엔티티 소유)이 더 적합해 보입니다.
🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/domain/ranking/WeeklyRanking.java
around lines 9-50, there are duplicate WeeklyRanking entities across the batch
and API modules which risks divergence and extra maintenance; resolve this by
making the batch module the canonical owner of the entity and refactor the API
module to stop using its own entity: remove the WeeklyRanking class from the API
module, replace API references with DTOs or JPA projections that map only needed
fields, update API service/repository code to fetch projections or convert
batch-owned entities into DTOs, and adjust module dependencies so the API
depends on batch DTO/projection interfaces (or expose a read-only module that
contains only DTOs/projections) without importing the batch persistence
internals; ensure tests and schema migrations remain aligned with the
batch-owned entity and update any import/package references accordingly.
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java
Show resolved
Hide resolved
...erce-batch/src/main/java/com/loopers/batch/job/ranking/processor/WeeklyRankingProcessor.java
Show resolved
Hide resolved
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/reader/WeeklyRankingReader.java
Outdated
Show resolved
Hide resolved
| @AfterChunk | ||
| void afterChunk(ChunkContext chunkContext) { | ||
| log.info( | ||
| "청크 종료: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " + | ||
| "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}" | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
두 가지 critical 이슈를 수정해야 합니다.
-
잘못된 문자열 포맷팅 (Line 17-18): Java에서는
${}구문이 문자열 보간(interpolation)을 지원하지 않습니다. 이 코드는 컴파일되지만 로그에 리터럴"${chunkContext.stepContext...}"가 그대로 출력됩니다. -
Spring Batch 6.x API 호환성 문제: Spring Batch 6.x에서는
ChunkListener의 메서드 시그니처가ChunkContext에서Chunk<T>로 변경되었습니다. 현재 코드는 이전 API를 사용하고 있어 Spring Batch 6.x 환경에서 리스너가 호출되지 않을 수 있습니다.
Based on learnings, Spring Batch 6.x requires Chunk parameters instead of ChunkContext.
🔎 수정 제안
package com.loopers.batch.listener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.annotation.AfterChunk;
-import org.springframework.batch.core.scope.context.ChunkContext;
+import org.springframework.batch.item.Chunk;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class ChunkListener {
@AfterChunk
- void afterChunk(ChunkContext chunkContext) {
- log.info(
- "청크 종료: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " +
- "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}"
- );
+ <T> void afterChunk(Chunk<T> chunk) {
+ int itemCount = chunk.getItems().size();
+ log.info("청크 종료: processedItemCount: {}", itemCount);
}
}참고: Spring Batch 6.x에서는 chunk 객체 자체에서 read/write count를 직접 얻을 수 없으므로, chunk의 아이템 개수를 로깅합니다. Step execution 정보가 필요한 경우 StepExecution을 @BeforeChunk나 @AfterChunk 메서드에 추가 파라미터로 받을 수 있습니다.
📝 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.
| @AfterChunk | |
| void afterChunk(ChunkContext chunkContext) { | |
| log.info( | |
| "청크 종료: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " + | |
| "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}" | |
| ); | |
| } | |
| package com.loopers.batch.listener; | |
| import lombok.RequiredArgsConstructor; | |
| import lombok.extern.slf4j.Slf4j; | |
| import org.springframework.batch.core.annotation.AfterChunk; | |
| import org.springframework.batch.item.Chunk; | |
| import org.springframework.stereotype.Component; | |
| @Slf4j | |
| @RequiredArgsConstructor | |
| @Component | |
| public class ChunkListener { | |
| @AfterChunk | |
| <T> void afterChunk(Chunk<T> chunk) { | |
| int itemCount = chunk.getItems().size(); | |
| log.info("청크 종료: processedItemCount: {}", itemCount); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java
around lines 14-20, the listener uses Java string interpolation (${...}) and the
old Spring Batch ChunkContext API; update the @AfterChunk method signature to
the Spring Batch 6.x form (e.g., void
afterChunk(org.springframework.batch.core.scope.chunk.Chunk<?> chunk) and
optionally add StepExecution as a second parameter if you need step counters),
replace the interpolated string with a proper logger placeholder or
concatenation (e.g., log.info("청크 종료: itemsCount: {}", chunk.getItems().size())
or include stepExecution.getReadCount()/getWriteCount() when StepExecution is
provided), and ensure imports/reference types are adjusted to the new API.
| @BeforeJob | ||
| void beforeJob(JobExecution jobExecution) { | ||
| log.info("Job '${jobExecution.jobInstance.jobName}' 시작"); | ||
| jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문자열 보간 구문 오류를 수정하세요.
Line 21의 '${jobExecution.jobInstance.jobName}' 구문은 Java에서 유효하지 않습니다. 문자열 연결이나 String.format()을 사용해야 합니다.
🔎 제안하는 수정
@BeforeJob
void beforeJob(JobExecution jobExecution) {
- log.info("Job '${jobExecution.jobInstance.jobName}' 시작");
+ log.info("Job '{}' 시작", jobExecution.getJobInstance().getJobName());
jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis());
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @BeforeJob | |
| void beforeJob(JobExecution jobExecution) { | |
| log.info("Job '${jobExecution.jobInstance.jobName}' 시작"); | |
| jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis()); | |
| } | |
| @BeforeJob | |
| void beforeJob(JobExecution jobExecution) { | |
| log.info("Job '{}' 시작", jobExecution.getJobInstance().getJobName()); | |
| jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis()); | |
| } |
🤖 Prompt for AI Agents
In apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
around lines 19 to 23, the log message uses a Groovy-style interpolation
('${jobExecution.jobInstance.jobName}') which is invalid in Java; replace it
with a valid Java expression such as using String concatenation or
String.format, and call the proper getters
(jobExecution.getJobInstance().getJobName()), or use the logger's parameterized
form (e.g., log.info("Job {} 시작", jobExecution.getJobInstance().getJobName()));
ensure the final logged string includes the job name correctly.
| @AfterJob | ||
| void afterJob(JobExecution jobExecution) { | ||
| var startTime = jobExecution.getExecutionContext().getLong("startTime"); | ||
| var endTime = System.currentTimeMillis(); | ||
|
|
||
| var startDateTime = Instant.ofEpochMilli(startTime) | ||
| .atZone(ZoneId.systemDefault()) | ||
| .toLocalDateTime(); | ||
| var endDateTime = Instant.ofEpochMilli(endTime) | ||
| .atZone(ZoneId.systemDefault()) | ||
| .toLocalDateTime(); | ||
|
|
||
| var totalTime = endTime - startTime; | ||
| var duration = Duration.ofMillis(totalTime); | ||
| var hours = duration.toHours(); | ||
| var minutes = duration.toMinutes() % 60; | ||
| var seconds = duration.getSeconds() % 60; | ||
|
|
||
| var message = String.format( | ||
| """ | ||
| *Start Time:* %s | ||
| *End Time:* %s | ||
| *Total Time:* %d시간 %d분 %d초 | ||
| """, startDateTime, endDateTime, hours, minutes, seconds | ||
| ).trim(); | ||
|
|
||
| log.info(message); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ExecutionContext에서 값을 안전하게 가져오세요.
Line 27에서 getLong("startTime")을 호출하는데, beforeJob이 실패하거나 호출되지 않은 경우 키가 존재하지 않아 예외가 발생할 수 있습니다. 방어적 프로그래밍을 위해 키 존재 여부를 확인하거나 기본값을 사용하세요.
🔎 제안하는 수정
@AfterJob
void afterJob(JobExecution jobExecution) {
- var startTime = jobExecution.getExecutionContext().getLong("startTime");
+ var executionContext = jobExecution.getExecutionContext();
+ if (!executionContext.containsKey("startTime")) {
+ log.warn("startTime이 ExecutionContext에 없습니다. Job 시작 시간을 기록할 수 없습니다.");
+ return;
+ }
+
+ var startTime = executionContext.getLong("startTime");
var endTime = System.currentTimeMillis();
var startDateTime = Instant.ofEpochMilli(startTime)📝 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.
| @AfterJob | |
| void afterJob(JobExecution jobExecution) { | |
| var startTime = jobExecution.getExecutionContext().getLong("startTime"); | |
| var endTime = System.currentTimeMillis(); | |
| var startDateTime = Instant.ofEpochMilli(startTime) | |
| .atZone(ZoneId.systemDefault()) | |
| .toLocalDateTime(); | |
| var endDateTime = Instant.ofEpochMilli(endTime) | |
| .atZone(ZoneId.systemDefault()) | |
| .toLocalDateTime(); | |
| var totalTime = endTime - startTime; | |
| var duration = Duration.ofMillis(totalTime); | |
| var hours = duration.toHours(); | |
| var minutes = duration.toMinutes() % 60; | |
| var seconds = duration.getSeconds() % 60; | |
| var message = String.format( | |
| """ | |
| *Start Time:* %s | |
| *End Time:* %s | |
| *Total Time:* %d시간 %d분 %d초 | |
| """, startDateTime, endDateTime, hours, minutes, seconds | |
| ).trim(); | |
| log.info(message); | |
| } | |
| @AfterJob | |
| void afterJob(JobExecution jobExecution) { | |
| var executionContext = jobExecution.getExecutionContext(); | |
| if (!executionContext.containsKey("startTime")) { | |
| log.warn("startTime이 ExecutionContext에 없습니다. Job 시작 시간을 기록할 수 없습니다."); | |
| return; | |
| } | |
| var startTime = executionContext.getLong("startTime"); | |
| var endTime = System.currentTimeMillis(); | |
| var startDateTime = Instant.ofEpochMilli(startTime) | |
| .atZone(ZoneId.systemDefault()) | |
| .toLocalDateTime(); | |
| var endDateTime = Instant.ofEpochMilli(endTime) | |
| .atZone(ZoneId.systemDefault()) | |
| .toLocalDateTime(); | |
| var totalTime = endTime - startTime; | |
| var duration = Duration.ofMillis(totalTime); | |
| var hours = duration.toHours(); | |
| var minutes = duration.toMinutes() % 60; | |
| var seconds = duration.getSeconds() % 60; | |
| var message = String.format( | |
| """ | |
| *Start Time:* %s | |
| *End Time:* %s | |
| *Total Time:* %d시간 %d분 %d초 | |
| """, startDateTime, endDateTime, hours, minutes, seconds | |
| ).trim(); | |
| log.info(message); | |
| } |
🤖 Prompt for AI Agents
In apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
around lines 25 to 52, the code calls
jobExecution.getExecutionContext().getLong("startTime") directly which can throw
if the key is missing (e.g., beforeJob didn't run); update to defensively read
the ExecutionContext: check containsKey("startTime") first and if present read
the long, otherwise fall back to a safe default (for example
jobExecution.getStartTime().toEpochMilli() or System.currentTimeMillis()), and
use that value for the subsequent time/duration calculations so the method never
throws when the key is absent.
📌 Summary
💬 Review Points
✅ Checklist
🧱 Spring Batch
🧩 Ranking API
Summary by CodeRabbit
릴리스 노트
✏️ Tip: You can customize this high-level summary in your review settings.