-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-10] Collect, Stack, Zip #246
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: JVHE
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์ด PR์ Spring Batch๋ฅผ ํ์ฉํ์ฌ ์ผ์ผ ์ ํ ๋ฉํธ๋ฆญ์ ๊ธฐ๋ฐ์ผ๋ก ์ฃผ๊ฐ ๋ฐ ์๊ฐ ์ํ ์์ ์ง๊ณ๋ฅผ ์๋ํํ๋ ๋ฐฐ์น ์ฒ๋ฆฌ ์์คํ ์ ๋์ ํฉ๋๋ค. ๋ฉํธ๋ฆญ ๋ฐ์ดํฐ ์์ง, ๋ฐฐ์น ์ก ๊ตฌ์ฑ, ์ค์ผ์ค๋ง ๋ฐ REST API ์๋ํฌ์ธํธ๋ฅผ ํฌํจํฉ๋๋ค. Changes
Sequence Diagram(s)sequenceDiagram
participant Scheduler as RankingAggregationScheduler
participant Service as RankingAggregationService
participant JobLauncher as JobLauncher
participant Job as rankingJob
participant Step as rankingChunkStep
participant Repos as Repositories<br/>(Weekly/Monthly)
Note over Scheduler: ๋งค์ผ ์์ ์คํ
Scheduler->>Service: executeWeeklyAndMonthlyRanking(targetDate)
rect rgb(100, 150, 200)
Note over Service: Weekly ์์ ์ง๊ณ
Service->>JobLauncher: launch(Job, JobParameters)
JobLauncher->>Job: execute()
Job->>Step: execute()
Step->>Repos: ์ ์ฅ
Step-->>Job: StepExecution
end
rect rgb(150, 100, 200)
Note over Service: Monthly ์์ ์ง๊ณ
Service->>JobLauncher: launch(Job, JobParameters)
JobLauncher->>Job: execute()
Job->>Step: execute()
Step->>Repos: ์ ์ฅ
Step-->>Job: StepExecution
end
Service->>Service: combined(weeklyResult, monthlyResult)
Service-->>Scheduler: RankingAggregationInfo
sequenceDiagram
participant Reader as ProductMetricsDailyReader
participant Processor as RankingProcessor
participant StepListener as RankingStepExecutionListener
participant JobListener as RankingJobExecutionListener
participant Repos as Repositories
Note over Reader,JobListener: ๋ฐฐ์น ์คํ
์คํ ํ๋ฆ
StepListener->>Reader: beforeStep()
Reader->>Reader: setDateRange()
loop ํ์ด์ง ๋จ์ ์ฒ๋ฆฌ
Reader->>Reader: doPageRead()
Reader-->>Processor: List<ProductMetricsDailyAggregated>
Processor->>Processor: process(item)
Note over Processor: ์์ 100๊ฐ ์ฐ์ ์์<br/>ํ ์ ์ง
end
Processor-->>StepListener: afterChunk()
StepListener->>StepListener: getTop100Queue()
StepListener->>StepListener: createWeekly/MonthlyRankings()
StepListener->>StepListener: jsonConverter.toJson()
StepListener->>StepListener: ExecutionContext ์ ์ฅ
JobListener->>JobListener: afterJob()
JobListener->>StepListener: ExecutionContext ์กฐํ
JobListener->>Repos: deleteByRankingDate()
JobListener->>Repos: saveAll(rankings)
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~75 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touchesโ Failed checks (2 warnings)
โ Passed checks (1 passed)
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: 13
๐งน Nitpick comments (19)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/OrderEventListener.java (1)
205-205: ํ์์กด ์ฒ๋ฆฌ ๋ฐ ํ ์คํธ ๊ฐ๋ฅ์ฑ ๊ฐ์ ๊ถ์ฅ
LocalDate.now()๋ฅผ ์ฌ์ฉํ๋ฉด ์์คํ ์ ๊ธฐ๋ณธ ํ์์กด์ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค. ๋ถ์ฐ ํ๊ฒฝ์ด๋ ์ฌ๋ฌ ๋ฆฌ์ ์ ๋ฐฐํฌ๋๋ ๊ฒฝ์ฐ ์ผ๊ด๋์ง ์์ ๋ ์ง ๊ธฐ๋ก์ด ๋ฐ์ํ ์ ์์ต๋๋ค.ํ ์คํธ ๊ฐ๋ฅ์ฑ๊ณผ ํ์์กด ์ผ๊ด์ฑ์ ์ํด
Clock์ธ์คํด์ค๋ฅผ ์ฃผ์ ๋ฐ์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ Clock ์ฃผ์ ์ ํตํ ๊ฐ์ ๋ฐฉ์
ํด๋์ค์ Clock ํ๋ ์ถ๊ฐ:
@Slf4j @Component @RequiredArgsConstructor public class OrderEventListener { private final IdempotencyService idempotencyService; private final ProductMetricsService productMetricsService; private final ProductMetricsDailyService productMetricsDailyService; private final OrderRepository orderRepository; private final DlqService dlqService; private final ObjectMapper objectMapper; + private final Clock clock;์ฌ์ฉ ๋ถ๋ถ ์์ :
try { productMetricsService.incrementSoldCount(productId, Long.valueOf(quantity)); - productMetricsDailyService.incrementSoldCount(productId, LocalDate.now(), Long.valueOf(quantity)); + productMetricsDailyService.incrementSoldCount(productId, LocalDate.now(clock), Long.valueOf(quantity)); log.debug("Incremented sold count for product: productId={}, quantity={}", productId, quantity);์ค์ ํด๋์ค์์ Clock ๋น ๋ฑ๋ก:
@Bean public Clock clock() { return Clock.system(ZoneId.of("Asia/Seoul")); // ๋๋ UTC }apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepConfig.java (1)
28-28: ์ฒญํฌ ํฌ๊ธฐ 1000์ ๋ํ ์ฑ๋ฅ ๊ณ ๋ ค์ฒญํฌ ํฌ๊ธฐ๊ฐ 1000์ผ๋ก ์ค์ ๋์ด ์์ต๋๋ค. ์ผ๋ฐ์ ์ผ๋ก ์ ์ ํ ํฌ๊ธฐ์ด์ง๋ง, ์ค์ ๋ฐ์ดํฐ ๋ณผ๋ฅจ๊ณผ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ๋ฐ๋ผ ์กฐ์ ์ด ํ์ํ ์ ์์ต๋๋ค.
RankingProcessor๊ฐ thread-local PriorityQueue๋ก TOP 100๋ง ์ ์งํ๋ค๊ณ ํ๋๋ผ๋, ์ฒญํฌ ๋จ์ ํธ๋์ญ์ ์ปค๋ฐ ์ ๋ถํ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ํ ์คํธ ํ ํ์์ ์ฒญํฌ ํฌ๊ธฐ๋ฅผ ์กฐ์ ํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค (์: 500 ๋๋ 2000). ๋ค์ ์์๋ฅผ ๊ณ ๋ คํ์ธ์:
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ (์ฒญํฌ ํฌ๊ธฐ ร ์์ดํ ํฌ๊ธฐ)
- ํธ๋์ญ์ ์ปค๋ฐ ๋น๋
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปค๋ฅ์ ํ์์์
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyAggregated.java (1)
18-20: ์ ์ ๊ณ์ฐ ๊ฐ์ค์น์ ๋ํ ๋ฌธ์ํ ๊ถ์ฅ์ ์ ๊ณ์ฐ ๊ณต์
์กฐํ์ * 0.1 + ์ข์์ * 0.2 + ํ๋งค๋ * 0.7์ ๊ฐ์ค์น ๋น์จ์ด ์ฝ๋์๋ง ์กด์ฌํ์ฌ ๋น์ฆ๋์ค ๋ก์ง์ ์ดํดํ๊ธฐ ์ด๋ ต์ต๋๋ค.ํฅํ ๊ฐ์ค์น ์กฐ์ ์ด๋ ๊ฒ์ฆ์ ์ํด ๋ค์์ ๊ถ์ฅํฉ๋๋ค:
- ๊ฐ์ค์น๋ฅผ ์์๋ก ๋ถ๋ฆฌ
- ๊ฐ์ค์น ์ ์ ๊ทผ๊ฑฐ๋ฅผ JavaDoc์ ๋ฌธ์ํ
๐ ๋ฌธ์ํ ๊ฐ์ ์ ์
+ /** + * ๋ญํน ์ ์ ๊ณ์ฐ ๊ฐ์ค์น + * - ์กฐํ์: 10% - ๊ด์ฌ๋ ์งํ + * - ์ข์์: 20% - ์ ํธ๋ ์งํ + * - ํ๋งค๋: 70% - ์ค์ ๊ตฌ๋งค ์ ํ ์งํ (๊ฐ์ฅ ์ค์) + */ + private static final double WEIGHT_VIEW = 0.1; + private static final double WEIGHT_LIKE = 0.2; + private static final double WEIGHT_SOLD = 0.7; + + /** + * ์ ํ์ ๋ญํน ์ ์๋ฅผ ๊ณ์ฐํฉ๋๋ค. + * ํ๋งค๋์ ๊ฐ์ฅ ์ค์ํ ์งํ๋ก ํ์ฌ ๊ฐ์ค ํ๊ท ์ ๊ณ์ฐํฉ๋๋ค. + * + * @return ๊ฐ์ค์น๊ฐ ์ ์ฉ๋ ๋ญํน ์ ์ + */ public double calculateScore() { long viewCount = totalViewCount != null ? totalViewCount : 0L; long likeCount = totalLikeCount != null ? totalLikeCount : 0L; long soldCount = totalSoldCount != null ? totalSoldCount : 0L; - return viewCount * 0.1 + likeCount * 0.2 + soldCount * 0.7; + return viewCount * WEIGHT_VIEW + likeCount * WEIGHT_LIKE + soldCount * WEIGHT_SOLD; }apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationScheduler.java (1)
42-45: ์์ธ ์ฒ๋ฆฌ ์ ๋ต ๊ฐ์ ๊ฒํ๋ชจ๋ ์์ธ๋ฅผ catchํ์ฌ ๋ก๊ทธ๋ง ๋จ๊ธฐ๊ณ ์๋๋ฐ, ์ด๋ ๋ฐฐ์น ์คํจ๋ฅผ ๋ชจ๋ํฐ๋งํ๊ธฐ ์ด๋ ต๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.
๋ค์์ ๊ณ ๋ คํด๋ณด์ธ์:
- ์๋ฆผ(Slack, ์ด๋ฉ์ผ ๋ฑ)์ ํตํ ์คํจ ํต์ง
- ๋ฉํธ๋ฆญ ์์ง์ ํตํ ์คํจ์จ ๋ชจ๋ํฐ๋ง
- ํน์ ์์ธ(์ผ์์ ์คํจ vs ์๊ตฌ์ ์ค๋ฅ)์ ๋ํ ์ฐจ๋ณํ๋ ์ฒ๋ฆฌ
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java (1)
11-15: ์ํฐํฐ์ @Setter ์ฌ์ฉ ์ฌ๊ณJPA ์ํฐํฐ์
@Setter๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฌด๋ถ๋ณํ ์ํ ๋ณ๊ฒฝ์ด ๊ฐ๋ฅํด์ ธ ๋น์ฆ๋์ค ๋ก์ง์ ์ฐํํ ์ ์์ต๋๋ค.๋ค์์ ๊ถ์ฅํฉ๋๋ค:
- ํ์ํ ํ๋์๋ง ๊ฐ๋ณ setter ์ ๊ณต
- ๋๋ ๋น์ฆ๋์ค ์๋ฏธ๊ฐ ์๋ ๋ฉ์๋๋ก ์ํ ๋ณ๊ฒฝ (์:
updateRanking(int newRanking))- ๋ถ๋ณ์ฑ์ด ๋ณด์ฅ๋์ด์ผ ํ๋ ํ๋๋ setter ์ ๊ฑฐ
๋ฐฐ์น ์ฒ๋ฆฌ์์ ๋๋์ ๋ฐ์ดํฐ๋ฅผ ์ฐ๊ธฐ ์ํด setter๊ฐ ํ์ํ ๊ฒฝ์ฐ๋ผ๋, ์ต์ํ ๋ฌธ์ํํ์ฌ ์๋๋ฅผ ๋ช ํํ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java (1)
25-29: ThreadLocal ์ฌ์ฉ ์ synchronized ๋ธ๋ก ๋ถํ์
ThreadLocal์ ์ค๋ ๋๋ณ๋ก ๊ฒฉ๋ฆฌ๋ ์ ์ฅ์๋ฅผ ์ ๊ณตํ๋ฏ๋ก, ๋์ผ ์ค๋ ๋ ๋ด์์synchronized๋ธ๋ก์ ๋ถํ์ํ ์ค๋ฒํค๋๋ฅผ ์ถ๊ฐํฉ๋๋ค. ๋ง์ฝ ๋ฉํฐ์ค๋ ๋ ์ฒญํฌ ์ฒ๋ฆฌ๋ฅผ ๊ณ ๋ คํ๋ค๋ฉด,ThreadLocal๋์ ๋ค๋ฅธ ๋๊ธฐํ ์ ๋ต์ด ํ์ํฉ๋๋ค.๐ ๋จ์ผ ์ค๋ ๋ ์ฒ๋ฆฌ ์ ๊ฐ์ํ ์ ์
- synchronized (queue) { - if (queue.size() < 100) { - queue.offer(item); - log.info("[RankingProcessor] process: ํ์ ์์ดํ ์ถ๊ฐ - productId={}, score={}, queueSize={}", - item.getProductId(), item.calculateScore(), queue.size()); - } else { - ProductMetricsDailyAggregated min = queue.peek(); - if (min != null && item.calculateScore() > min.calculateScore()) { - ProductMetricsDailyAggregated removed = queue.poll(); - queue.offer(item); - log.info("[RankingProcessor] process: ํ์์ ์์ดํ ๊ต์ฒด - ์ ๊ฑฐ๋ productId={} (score={}), ์ถ๊ฐ๋ productId={} (score={}), queueSize={}", - removed.getProductId(), removed.calculateScore(), item.getProductId(), item.calculateScore(), queue.size()); - } else { - log.debug("[RankingProcessor] process: ์์ดํ ๊ฑฐ๋ถ๋จ - productId={}, score={}, minScore={}", - item.getProductId(), item.calculateScore(), min != null ? min.calculateScore() : "null"); - } + if (queue.size() < 100) { + queue.offer(item); + log.info("[RankingProcessor] process: ํ์ ์์ดํ ์ถ๊ฐ - productId={}, score={}, queueSize={}", + item.getProductId(), item.calculateScore(), queue.size()); + } else { + ProductMetricsDailyAggregated min = queue.peek(); + if (min != null && item.calculateScore() > min.calculateScore()) { + ProductMetricsDailyAggregated removed = queue.poll(); + queue.offer(item); + log.info("[RankingProcessor] process: ํ์์ ์์ดํ ๊ต์ฒด - ์ ๊ฑฐ๋ productId={} (score={}), ์ถ๊ฐ๋ productId={} (score={}), queueSize={}", + removed.getProductId(), removed.calculateScore(), item.getProductId(), item.calculateScore(), queue.size()); + } else { + log.debug("[RankingProcessor] process: ์์ดํ ๊ฑฐ๋ถ๋จ - productId={}, score={}, minScore={}", + item.getProductId(), item.calculateScore(), min != null ? min.calculateScore() : "null"); } }Also applies to: 62-79
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java (1)
35-42: ํ๋ ํ์ ์ผ๊ด์ฑ ๊ณ ๋ ค
likeCount๋Integer์ด๊ณ ,viewCount์soldCount๋Long์ ๋๋ค. ๋ฐ์ดํฐ ๋ฒ์์ ๋ฐ๋ฅธ ์๋์ ์ ํ์ผ ์ ์์ง๋ง, ํฅํ ์ ์ง๋ณด์๋ฅผ ์ํด ์ผ๊ด๋ ํ์ ์ฌ์ฉ์ ๊ณ ๋ คํด ๋ณผ ์ ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJsonConverter.java (1)
18-20: ํ์ ์์ ์ฑ ๊ฐ์ ํ์๋ฉ์๋ ํ๋ผ๋ฏธํฐ๋ก raw type
List๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ์ ๋ค๋ฆญ ํ์ ์ ๋ช ์ํ์ฌ ์ปดํ์ผ ํ์ ํ์ ์ฒดํฌ๋ฅผ ํ์ฑํํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์ํ๋ ๊ฐ์ ์
- public String toJson(List items) throws JsonProcessingException { + public String toJson(List<?> items) throws JsonProcessingException { return objectMapper.writeValueAsString(items); }๋๋ ์ ๋ค๋ฆญ ๋ฉ์๋๋ก ๋ ์๊ฒฉํ๊ฒ ์ ํํ ์ ์์ต๋๋ค:
- public String toJson(List items) throws JsonProcessingException { + public <T> String toJson(List<T> items) throws JsonProcessingException { return objectMapper.writeValueAsString(items); }apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1ApiSpec.java (1)
16-20: ์ค๋ณต๋ ์ด๋ ธํ ์ด์ ์ ๋ฆฌ ๊ถ์ฅ
targetDateํ๋ผ๋ฏธํฐ์@Parameter์@Schema์ด๋ ธํ ์ด์ ์ด ๋์ผํ ๋ด์ฉ์ผ๋ก ์ค๋ณต ์ ์ธ๋์ด ์์ต๋๋ค. ์ผ๋ฐ์ ์ผ๋ก@Parameter๋ง์ผ๋ก ์ถฉ๋ถํ๋ฉฐ,@Schema๋ ๋ณต์กํ ํ์ ์ ์๊ฐ ํ์ํ ๊ฒฝ์ฐ์ ์ถ๊ฐํฉ๋๋ค.์ด ํจํด์ด ๋ค๋ฅธ ๋ ๋ฉ์๋์๋ ๋ฐ๋ณต๋๊ณ ์์ต๋๋ค (lines 27-31, 38-42).
๐ ์ ์ํ๋ ๊ฐ์ ์
ApiResponse<RankingBatchV1Dto.BatchExecutionResponse> executeWeeklyRanking( @Parameter(description = "์ง๊ณ ๋์ ๋ ์ง (yyyy-MM-dd ๋๋ ISO_ZONED_DATE_TIME ํ์, ๋ฏธ์ง์ ์ ํ์ฌ ์๊ฐ)", example = "2024-12-19") - @Schema(description = "์ง๊ณ ๋์ ๋ ์ง (yyyy-MM-dd ๋๋ ISO_ZONED_DATE_TIME ํ์, ๋ฏธ์ง์ ์ ํ์ฌ ์๊ฐ)", example = "2024-12-19") String targetDate );apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyService.java (1)
39-49: ์ ๊ท ์ํฐํฐ ์์ฑ ์ ๋ถํ์ํ ์ค๋ณต ์ ์ฅ ๋ฐ์์ ๊ท
ProductMetricsDaily์ํฐํฐ๋ฅผ ์์ฑํ๋ ๊ฒฝ์ฐ, ๋ค์๊ณผ ๊ฐ์ด ๋ ๋ฒ์ ์ ์ฅ์ด ๋ฐ์ํฉ๋๋ค:
orElseGet๋ด๋ถ์์ ๋น ์ํฐํฐ ์์ฑ ํ ์ ์ฅ (line 44)- ์ฆ๊ฐ ํ ๋ค์ ์ ์ฅ (line 47)
์ด๋ ์ฑ๋ฅ์ ๋นํจ์จ์ ์ ๋๋ค. ์ ๊ท ์ํฐํฐ์ ๊ฒฝ์ฐ ์ด๊ธฐ๊ฐ์ 1๋ก ์ค์ ํ์ฌ ํ ๋ฒ๋ง ์ ์ฅํ๋ ๊ฒ์ด ๋ ํจ์จ์ ์ ๋๋ค.
๋์ผํ ํจํด์ด
incrementViewCount(lines 68-77) ๋ฐincrementSoldCount(lines 83-93) ๋ฉ์๋์๋ ๋ฐ๋ณต๋๊ณ ์์ต๋๋ค.๐ ์ ์ํ๋ ๊ฐ์ ์
@Transactional public void incrementLikeCount(Long productId, LocalDate date) { - ProductMetricsDaily daily = repository.findByProductIdAndDateForUpdate(productId, date) - .orElseGet(() -> { - ProductMetricsDaily newDaily = ProductMetricsDaily.create(productId, date); - return repository.save(newDaily); - }); - daily.incrementLikeCount(); - repository.save(daily); + ProductMetricsDaily daily = repository.findByProductIdAndDateForUpdate(productId, date) + .orElseGet(() -> ProductMetricsDaily.createWithLikeCount(productId, date, 1L)); + + if (daily.getId() == null) { + // ์ ๊ท ์์ฑ๋ ์ํฐํฐ + repository.save(daily); + } else { + // ๊ธฐ์กด ์ํฐํฐ ์ฆ๊ฐ + daily.incrementLikeCount(); + repository.save(daily); + } log.debug("Incremented daily like count: productId={}, date={}", productId, date); }๋๋ ๋ ๊ฐ๋จํ๊ฒ,
ProductMetricsDaily์ ์ด๊ธฐ๊ฐ์ ๋ฐ๋ ํฉํ ๋ฆฌ ๋ฉ์๋๋ฅผ ์ถ๊ฐํ ์ ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/ProductMetricsDailyReaderFactory.java (2)
44-46: ๋ ์ง ํ์ฑ ์ ์์ธ ์ฒ๋ฆฌ ๋๋ฝ
LocalDate.parse()๋ ์๋ชป๋ ํ์์ ๋ฌธ์์ด์ ๋ํดDateTimeParseException์ ๋ฐ์์ํต๋๋ค. ์๋ชป๋ Job ํ๋ผ๋ฏธํฐ๋ก ์ธํด ๋ฐฐ์น๊ฐ ์คํจํ ์ ์์ผ๋ฏ๋ก ์์ธ ์ฒ๋ฆฌ ๋๋ ์ ํจ์ฑ ๊ฒ์ฆ์ ์ถ๊ฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์ํ๋ ์์ ์ฌํญ
// weekly: Job ํ๋ผ๋ฏธํฐ์์ ๋ ์ง๊ฐ ์ ๊ณต๋ ๊ฒฝ์ฐ ์ฌ์ฉ, ์์ผ๋ฉด ์ต๊ทผ 7์ผ if (startDateStr != null && endDateStr != null) { - startDate = LocalDate.parse(startDateStr); - endDate = LocalDate.parse(endDateStr); + try { + startDate = LocalDate.parse(startDateStr); + endDate = LocalDate.parse(endDateStr); + } catch (DateTimeParseException e) { + log.warn("[ProductMetricsDailyReaderFactory] Invalid date format, using default 7 days: startDate={}, endDate={}", startDateStr, endDateStr); + startDate = endDate.minusDays(7); + } log.info("[ProductMetricsDailyReaderFactory] Using provided date range for weekly: startDate={}, endDate={}", startDate, endDate);
99-105: CopyOnWriteArrayList ์ฌ์ฉ์ ๋ถํ์ํ ์ค๋ฒํค๋ ๋ฐ์
CopyOnWriteArrayList๋ ๋์์ฑ ์ฒ๋ฆฌ์๋ ์ ์ฉํ์ง๋ง, ๋ฐฐ์น Reader๋ ๋จ์ผ ์ค๋ ๋์์ ์์ฐจ์ ์ผ๋ก ์คํ๋๋ฏ๋ก ์ผ๋ฐArrayList๋ก ์ถฉ๋ถํฉ๋๋ค. ๋งค ํ์ด์ง๋ง๋ค ๋ฐฐ์ด ๋ณต์ฌ๊ฐ ๋ฐ์ํ์ฌ ์ฑ๋ฅ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค.๐ ์ ์ํ๋ ์์ ์ฌํญ
-import java.util.concurrent.CopyOnWriteArrayList; +import java.util.ArrayList;// ๊ฒฐ๊ณผ๋ฅผ results์ ์ ์ฅ (๋ถ๋ชจ ํด๋์ค์ ํ๋) if (results == null) { - results = new CopyOnWriteArrayList<>(); + results = new ArrayList<>(); } else { results.clear(); }apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java (2)
67-70: ์๋ชป๋ periodType ์ฒ๋ฆฌ ํ ๋ช ์์ ๋ฐํ ์ถ๊ฐ ๊ถ์ฅ
addFailureExceptionํธ์ถ ํ ๋ฉ์๋๊ฐ ๊ณ์ ์งํ๋ฉ๋๋ค. ํ์ฌ ์ฝ๋ ํ๋ฆ์ ๋ฌธ์ ๋ ์์ง๋ง, ๋ช ์์ ์ผ๋กreturn์ ์ถ๊ฐํ๋ฉด ์๋๊ฐ ๋ ๋ช ํํด์ง๋๋ค.๐ ์ ์ํ๋ ์์ ์ฌํญ
} else { log.error("[RankingJobExecutionListener] afterJob: ์๋ชป๋ periodType={}, 'weekly' ๋๋ 'monthly'์ฌ์ผ ํฉ๋๋ค", periodType); jobExecution.addFailureException(new IllegalArgumentException("Invalid periodType: " + periodType)); + return; }
78-139: ์ฃผ๊ฐ/์๊ฐ ์ฒ๋ฆฌ ๋ฉ์๋์ ์ค๋ณต ์ฝ๋
processWeeklyRanking๊ณผprocessMonthlyRanking๋ฉ์๋๊ฐ ๊ฑฐ์ ๋์ผํ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค. ํ์ฌ ์ํ๋ก๋ ๋์์๋ ๋ฌธ์ ๊ฐ ์์ง๋ง, ํฅํ ์ ์ง๋ณด์ ์ ์์ชฝ ๋ชจ๋ ์์ ํด์ผ ํ๋ ๋ถ๋ด์ด ์์ต๋๋ค. ์ ๋ค๋ฆญ์ ํ์ฉํ ๊ณตํต ๋ฉ์๋ ์ถ์ถ์ ๊ณ ๋ คํด๋ณผ ์ ์์ต๋๋ค.Also applies to: 141-202
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java (2)
142-178: N+1 ์ฟผ๋ฆฌ ๋ฌธ์ : ๋ฃจํ ๋ด ๊ฐ๋ณ ์กฐํ
createWeeklyRankings๋ฉ์๋์์ ๊ฐ ์์ดํ ๋ง๋คproductService.getProductById์brandService.getBrandById๋ฅผ ํธ์ถํฉ๋๋ค. TOP 100 ์ฒ๋ฆฌ ์ ์ต๋ 200๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์ฑ๋ฅ ๊ฐ์ ์ ์ํด productId ๋ชฉ๋ก์ผ๋ก ์ผ๊ด ์กฐํ ํ Map์ผ๋ก ์บ์ฑํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ์ ์ํ๋ ๊ฐ์ ๋ฐฉํฅ
// ์์: ์ผ๊ด ์กฐํ ๋ฐฉ์ List<Long> productIds = top100.stream() .map(ProductMetricsDailyAggregated::getProductId) .toList(); Map<Long, Product> productMap = productService.getProductsByIds(productIds).stream() .collect(Collectors.toMap(Product::getId, Function.identity())); // ์ดํ ๋ฃจํ์์ productMap.get(aggregated.getProductId()) ์ฌ์ฉ
187-228:createMonthlyRankings์๋ ๋์ผํ ๊ฐ์ ์ฌํญ ์ ์ฉ ํ์
createWeeklyRankings์์ ์ธ๊ธ๋ N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ์ null rankingDate ์ฒ๋ฆฌ ์ด์๊ฐ ์ด ๋ฉ์๋์๋ ๋์ผํ๊ฒ ์ ์ฉ๋ฉ๋๋ค.apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java (1)
18-20: @Modifying์ clearAutomatically ์ต์ ๊ณ ๋ ค
@Modifying์ด๋ ธํ ์ด์ ์ผ๋ก ๋ฒํฌ ์ญ์ ์, ์์์ฑ ์ปจํ ์คํธ์ DB ๊ฐ ๋ถ์ผ์น๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ๋์ผ ํธ๋์ญ์ ๋ด์์ ์ญ์ ํ ๋ค์ ์กฐํํ๋ ๊ฒฝ์ฐ๋ฅผ ๋๋นํ์ฌclearAutomatically = true์ต์ ์ถ๊ฐ๋ฅผ ๊ถ์ฅํฉ๋๋ค.๐ ์ ์ํ๋ ์์ ์ฌํญ
- @Modifying + @Modifying(clearAutomatically = true) @Query("DELETE FROM MvProductRankWeekly r WHERE r.rankingDate = :rankingDate") void deleteByRankingDate(@Param("rankingDate") ZonedDateTime rankingDate);apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java (1)
48-57: Object[] โ ProductMetricsDailyAggregated ๋ณํ ๋ก์ง ์ค๋ณต
findAggregatedByDateBetween๊ณผfindAggregatedByDateBetweenPaged์์ ๋์ผํ ๋งคํ ๋ก์ง์ด ๋ฐ๋ณต๋ฉ๋๋ค. private ํฌํผ ๋ฉ์๋๋ก ์ถ์ถํ๋ฉด ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ฉ๋๋ค.๐ ์ ์ํ๋ ์์ ์ฌํญ
private ProductMetricsDailyAggregated mapToAggregated(Object[] row) { return new ProductMetricsDailyAggregated( ((Number) row[0]).longValue(), // productId ((Number) row[1]).longValue(), // totalLikeCount ((Number) row[2]).longValue(), // totalViewCount ((Number) row[3]).longValue() // totalSoldCount ); }Also applies to: 65-70
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1Controller.java (1)
26-54: ์๋ํฌ์ธํธ ๊ตฌํ์ด ์ผ๊ด๋๊ณ ๋ช ํํฉ๋๋ค.์ธ ์๋ํฌ์ธํธ ๋ชจ๋ ๋์ผํ ํจํด์ ๋ฐ๋ฅด๋ฉฐ, ์๋น์ค ๋ ์ด์ด์ ์ ์ ํ ์์ํ๊ณ ์์ต๋๋ค.
targetDateํ๋ผ๋ฏธํฐ๋ฅผ ์ ํ์ ์ผ๋ก ๋ฐ์ ์ ์ฐ์ฑ์ ์ ๊ณตํฉ๋๋ค.์ ํ์ ์ ์: ์ค๋ณต ์ฝ๋ ๋ฆฌํฉํ ๋ง
์ธ ๋ฉ์๋์ ๊ตฌ์กฐ๊ฐ ๊ฑฐ์ ๋์ผํ๋ฏ๋ก, ํ์์ ๋ด๋ถ ํฌํผ ๋ฉ์๋๋ก ๊ณตํต ๋ก์ง์ ์ถ์ถํ ์ ์์ต๋๋ค:
private ApiResponse<RankingBatchV1Dto.BatchExecutionResponse> executeRanking( String targetDate, Function<ZonedDateTime, RankingAggregationInfo> executor) { ZonedDateTime date = parseDateTime(targetDate); RankingAggregationInfo result = executor.apply(date); return ApiResponse.success(RankingBatchV1Dto.BatchExecutionResponse.from(result)); }๋จ, ํ์ฌ ๊ตฌ์กฐ๋ ์ถฉ๋ถํ ๋ช ํํ๋ฏ๋ก ์ ํ์ ์ผ๋ก ๊ณ ๋ คํ์๋ฉด ๋ฉ๋๋ค.
๐ Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (33)
apps/commerce-api/build.gradle.ktsapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationInfo.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationScheduler.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationService.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/batch/ProductMetricsDailyReaderFactory.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJob.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJobConfig.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJsonConverter.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepConfig.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/batch/BatchConfig.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1ApiSpec.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1Dto.javaapps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.javaapps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.javaapps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyAggregated.javaapps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyRepository.javaapps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyService.javaapps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.javaapps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.javaapps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.javaapps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.javaapps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyJpaRepository.javaapps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.javaapps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.javaapps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.javaapps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.javaapps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/CatalogEventListener.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/OrderEventListener.java
๐งฐ Additional context used
๐ง Learnings (10)
๐ 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-core/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.javaapps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyAggregated.javaapps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyService.javaapps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyJpaRepository.javaapps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java
๐ Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.
Applied to files:
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.javaapps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.javaapps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.javaapps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyJpaRepository.javaapps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyRepository.javaapps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.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-api/src/main/java/com/loopers/infrastructure/batch/BatchConfig.java
๐ Learning: 2025-12-19T03:23:54.756Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 191
File: modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java:24-34
Timestamp: 2025-12-19T03:23:54.756Z
Learning: In the loopers-spring-java-template project, test container configurations (MySQL, Redis, Kafka) use static initialization blocks for consistency across the template. This pattern should be maintained for uniformity, even if alternative lifecycle management approaches exist.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/infrastructure/batch/BatchConfig.java
๐ Learning: 2025-12-19T20:59:57.713Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: docs/week8/round8-detailed-design.md:151-178
Timestamp: 2025-12-19T20:59:57.713Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository's Kafka event pipeline, only 5 domain events are intentionally published to Kafka via CloudEventEnvelopeFactory: OrderPaidEventV1, LikeCreatedEventV1, LikeCanceledEventV1, ProductViewedEventV1, and StockDepletedEventV1. Other domain events (OrderCreatedEventV1, OrderCanceledEventV1, PaymentCreatedEventV1, PaymentPaidEventV1, PaymentFailedEventV1) are internal-only and intentionally not mapped in resolveMetadata(), which correctly returns null for them to exclude them from Outbox publication.
Applied to files:
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/CatalogEventListener.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/OrderEventListener.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-streamer/src/main/java/com/loopers/infrastructure/listener/CatalogEventListener.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-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/OrderEventListener.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-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.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-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java
๐ Learning: 2025-11-12T13:04:50.782Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 72
File: core/domain/src/main/java/com/loopers/core/domain/productlike/vo/ProductLikeId.java:5-7
Timestamp: 2025-11-12T13:04:50.782Z
Learning: In the com.loopers codebase, domain entity ID value objects (e.g., ProductLikeId, OrderItemId, ProductId, PaymentId, OrderId, BrandId) are system-generated identifiers and do not require pattern validation (regex, length checks). They are implemented as simple records with a String value and an empty() factory method returning null for unsaved entities. This differs from UserIdentifier, which is a user-supplied login ID that requires format validation. Domain IDs should not be flagged for missing validation logic in the create() method.
<!-- [add_learning]
UserIdentifier์ ๊ฐ์ ์ฌ์ฉ์ ์
๋ ฅ ID์ ProductLikeId, OrderItemId ๋ฑ์ ๋๋ฉ์ธ ID๋ ๊ฒ์ฆ ํจํด์ด ๋ค๋ฆ
๋๋ค. UserIdentifier๋ ์ฌ์ฉ์๊ฐ ์
๋ ฅํ๋ ๋ก๊ทธ์ธ ID๋ก์ ์ ๊ท์, ๊ธธ์ด ๋ฑ์ ํ์ ๊ฒ์ฆ์ด ํ์ํ์ง๋ง, ๋๋ฉ์ธ ID๋ ์์คํ
์์ ์์ฑํ๋ ์๋ณ์(UUID, DB ์์ฑ ID)์ด๋ฏ๋ก ํจํด ๊ฒ์ฆ์ด ๋ถํ์ํฉ๋๋ค. ๋๋ฉ์ธ ID VO๋ ๋จ์ํ record์ empty() ํฉํ ๋ฆฌ ๋ฉ์๋๋ง์ผ๋ก ์ถฉ๋ถํฉ๋๋ค.
Applied to files:
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java
๐งฌ Code graph analysis (9)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java (2)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
Entity(11-26)apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyAggregated.java (1)
Getter(10-21)
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/ProductMetricsDailyReaderFactory.java (1)
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java (1)
Component(18-84)
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJsonConverter.java (2)
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
Component(13-41)apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (1)
Component(13-41)
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyService.java (1)
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java (1)
Component(18-84)
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (1)
Component(13-41)
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJobConfig.java (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/batch/BatchConfig.java (1)
Configuration(18-52)
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (1)
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
Component(13-41)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java (1)
Entity(11-26)
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java (1)
Slf4j(19-120)apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJsonConverter.java (1)
Component(12-41)
๐ Additional comments (34)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/CatalogEventListener.java (2)
4-4: ์ผ๋ณ ๋ฉํธ๋ฆญ ์๋น์ค ์ถ๊ฐ๊ฐ ์ ์ ํฉ๋๋ค.ProductMetricsDailyService ๋์ ์ผ๋ก ์ผ๋ณ ์ง๊ณ ๊ธฐ๋ฅ์ ๋๊ณ์ ๋ถ๋ฆฌํ์ฌ ๊ด๋ฆฌํ ์ ์๊ฒ ๋์์ต๋๋ค. ์ฝ๋ ๊ตฌ์กฐ๊ฐ ๋ช ํํ๊ณ ์ฃผ์๋ ์ ์์ฑ๋์์ต๋๋ค.
Also applies to: 17-17, 28-29
145-167: ๋๊ณ์ ์ผ๋ณ ๋ฉํธ๋ฆญ ์ด์ค ์ ๋ฐ์ดํธ ํจํด์ ์์ ํ๊ฒ ๊ตฌํ๋จ๊ฐ ์ด๋ฒคํธ ํ์ ๋ง๋ค ๋๊ณ์ ์ผ๋ณ ๋ฉํธ๋ฆญ์ ์์ฐจ์ ์ผ๋ก ์ ๋ฐ์ดํธํ๋ ์ด์ค ์ ๋ฐ์ดํธ ํจํด์ด ์ ์ฉ๋์์ผ๋ฉฐ,
@Transactional๋ฒ์ ๋ด์์ ์คํ๋๋ฏ๋ก ์ผ๊ด์ฑ์ด ๋ณด์ฅ๋ฉ๋๋ค.
ProductUnliked์ด๋ฒคํธ์์decrementLikeCountํธ์ถ ์ ์์ ๊ฐ์ด ๋ ์ ์๋ค๋ ์ฐ๋ ค๋ ์ด๋ฏธ ์ฝ๋์์ ์ ์ ํ ์ฒ๋ฆฌ๋๊ณ ์์ต๋๋ค.ProductMetricsDaily๋ฐProductMetrics์ํฐํฐ์decrementLikeCount()๋ฉ์๋๋likeCount <= 0์กฐ๊ฑด์ ๊ฒ์ฆํ์ฌ ์์ ์ํ๋ฅผ ๋ฐฉ์งํ๋ฉฐ, ์๋น์ค ๊ณ์ธต์decrementLikeCount๋ ๋น๊ด์ ๋ฝ(findByProductIdAndDateForUpdate)์ ์ฌ์ฉํ๊ณ ๋ ์ฝ๋ ๋ฏธ์กด์ฌ ์ ์์ธ๋ฅผ ๋ฐ์์ํต๋๋ค. ์ด๋ ์ด๋ฒคํธ ์์ ๋ฌธ์ ๋ฅผ ์ฆ์ ๊ฐ์งํ๋ ๋ฐฉ์ด์ ์ค๊ณ๋ก, ์๋ชป๋ ์ํ ์ง์ ์ ์์ฒ ์ฐจ๋จํฉ๋๋ค.apps/commerce-api/build.gradle.kts (1)
22-23: Spring Batch ์์กด์ฑ ์ถ๊ฐ ์น์ธSpring Batch ๊ธฐ๋ฅ ๊ตฌํ์ ์ํ ํ์ค ์์กด์ฑ์ด ์ ์ ํ ์ถ๊ฐ๋์์ต๋๋ค. Spring Boot BOM์์ ๋ฒ์ ์ด ๊ด๋ฆฌ๋๋ฏ๋ก ํธํ์ฑ ๋ฌธ์ ๊ฐ ์์ ๊ฒ์ผ๋ก ํ๋จ๋ฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepConfig.java (1)
31-35: Writer ํจํด ํ์ธ ์๋ฃ์ค๊ณ๊ฐ ์๋๋ ๋๋ก ๊ตฌํ๋์ด ์์ต๋๋ค.
RankingStepExecutionListener.afterStep()์์ TOP 100์ ์ถ์ถํ๊ณ JSON์ผ๋ก ๋ณํํ ํExecutionContext์ ์ ์ฅํ๋ฉฐ,RankingJobExecutionListener.afterJob()์processWeeklyRanking()๋ฐprocessMonthlyRanking()๋ฉ์๋์์ ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฅ์ ์ํํฉ๋๋ค(weeklyRepository.saveAll(),monthlyRepository.saveAll()).apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetrics.java (1)
15-15: ์ ํ๋ณ ์กฐํ ์ฑ๋ฅ ๊ฐ์ ์ ์ํ ์ธ๋ฑ์ค
product_id์ ๋ํ ์ธ๋ฑ์ค ์ถ๊ฐ๋ProductMetricsRepository์findByProductId(),findByProductIdForUpdate()๋ฑ ๊ฐ๋ณ ์ ํ ๋ฉํธ๋ฆญ ์กฐํ ์ ์ฑ๋ฅ์ ํฅ์์ํต๋๋ค.Likely an incorrect or invalid review comment.
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJobConfig.java (1)
20-26: ์ ๊ตฌํ๋ Spring Batch Job ์ค์ ์ ๋๋ค.ํ์ค์ ์ธ Spring Batch Job ๊ตฌ์ฑ ํจํด์ ์ฌ๋ฐ๋ฅด๊ฒ ๋ฐ๋ฅด๊ณ ์์ผ๋ฉฐ, JobRepository, Step, Listener๊ฐ ์ ์ ํ๊ฒ ์ฐ๊ฒฐ๋์ด ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1Dto.java (1)
15-38: ์ ์ค๊ณ๋ DTO์ ๋๋ค.Java record๋ฅผ ์ฌ์ฉํ ๋ถ๋ณ DTO ๊ตฌ์กฐ์ ํฉํ ๋ฆฌ ๋ฉ์๋ ํจํด์ด ์ ์ ํ๊ฒ ์ ์ฉ๋์ด ์์ต๋๋ค. Swagger ์ด๋ ธํ ์ด์ ๋ ์ ์์ฑ๋์ด API ๋ฌธ์ํ์ ๋์์ด ๋ฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/infrastructure/batch/BatchConfig.java (1)
25-34: LGTM!
JobRepository์JobExplorer๋น ์ค์ ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค. MySQL ๋ฐ์ดํฐ ํ์ ๊ณผ ํ ์ด๋ธ ์ ๋์ฌ๊ฐ ์ผ๊ด๋๊ฒ ์ ์ฉ๋์์ต๋๋ค.Also applies to: 43-51
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
11-26: LGTM!
MvProductRankWeekly์ํฐํฐ์ ์ผ๊ด๋ ๊ตฌ์กฐ๋ฅผ ์ ์งํ๊ณ ์์ต๋๋ค. ๋ฐฐ์น ์ฒ๋ฆฌ์์ ํ๋ ์ค์ ์ด ํ์ํ ๊ฒฝ์ฐ๋ฅผ ์ํด@Setter์ฌ์ฉ์ด ์ ์ ํด ๋ณด์ ๋๋ค.apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankMonthlyRepository.java (1)
7-12: LGTM!๋๋ฉ์ธ ๋ ํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค๊ฐ ๊น๋ํ๊ฒ ์ ์๋์์ต๋๋ค.
MvProductRankWeeklyRepository์ ์ผ๊ด๋ ๋ฉ์๋ ์๊ทธ๋์ฒ๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationInfo.java (1)
10-67: LGTM!๋ถ๋ณ ๋ฐ์ดํฐ ํด๋์ค๋ก ์ ์ค๊ณ๋์์ต๋๋ค.
from()๋ฉ์๋์ null-safe ์ฒ๋ฆฌ์combined()๋ฉ์๋์ ์ํ ํฉ์ฐ ๋ก์ง์ด ๋ช ํํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java (1)
45-82: ์ง๊ณ ํจํด:process()๊ฐ ํญ์ null ๋ฐํ
process()๋ฉ์๋๊ฐ ํญ์null์ ๋ฐํํ์ฌ Writer๋ก ์์ดํ ์ด ์ ๋ฌ๋์ง ์์ต๋๋ค. ์ด๋ PriorityQueue์ TOP 100์ ๋์ ํ๊ณRankingStepExecutionListener์์ ๊ฒฐ๊ณผ๋ฅผ ์ถ์ถํ๋ ์๋๋ ํจํด์ผ๋ก ๋ณด์ ๋๋ค.์ด ํจํด์ด ์๋๋ ๊ฒ์ธ์ง ํ์ธํด ์ฃผ์ธ์. Writer๊ฐ ์กด์ฌํ๋ค๋ฉด ์๋ฌด ์์ ๋ ์ํํ์ง ์๊ฒ ๋ฉ๋๋ค.
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java (1)
11-65: LGTM!์ํฐํฐ ์ค๊ณ์ ๊ฒ์ฆ ๋ก์ง์ด ์ ์ ํฉ๋๋ค. ์ธ๋ฑ์ค์ ์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ๋์๊ณ ,
create()ํฉํ ๋ฆฌ ๋ฉ์๋์ ๊ฒ์ฆ ๋ก์ง์ด ๋ช ํํฉ๋๋ค.Also applies to: 71-93
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.java (1)
7-27: LGTM!์ฃผ๊ฐ ๋ญํน ๋ ํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค๊ฐ ์ ์ ์๋์์ต๋๋ค.
MvProductRankMonthlyRepository์ ์ผ๊ด๋ ๋ฉ์๋ ์๊ทธ๋์ฒ์ ๋ช ํํ ํ๊ธ ๋ฌธ์ํ๊ฐ ์ข์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJsonConverter.java (2)
22-30: ๊ตฌํ์ด ์ ์ ํฉ๋๋ค
TypeFactory๋ฅผ ์ฌ์ฉํ์ฌ ์ ๋ค๋ฆญ ํ์ ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ฒ๋ฆฌํ๊ณ ์์ต๋๋ค. Jackson์ ํ์ ์์ ํ ์ญ์ง๋ ฌํ ํจํด์ ์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.
32-40: ๊ตฌํ์ด ์ ์ ํฉ๋๋ค์ฃผ๊ฐ ๋ญํน ์ญ์ง๋ ฌํ ๋ฉ์๋์ ๋์ผํ ํจํด์ ์ฌ์ฉํ์ฌ ์ผ๊ด์ฑ์ด ์ ์ง๋๊ณ ์์ต๋๋ค.
apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java (1)
13-21: ๊ตฌํ์ด ์ ์ ํฉ๋๋คSpring Data JPA์ ์ฟผ๋ฆฌ ๋ฉ์๋ ๋ค์ด๋ฐ ๊ท์น์ ์ฌ๋ฐ๋ฅด๊ฒ ๋ฐ๋ฅด๊ณ ์์ผ๋ฉฐ, ์ฃผ๊ฐ ๋ญํน ๋ฆฌํฌ์งํ ๋ฆฌ(
MvProductRankWeeklyJpaRepository)์ ์ผ๊ด๋ ๊ตฌ์กฐ๋ฅผ ์ ์งํ๊ณ ์์ต๋๋ค.findTop100๋ฉ์๋๋ TOP 100 ๋ญํน ์กฐํ ์๊ตฌ์ฌํญ์ ์ ํฉํฉ๋๋ค.apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java (1)
13-41: ๊ตฌํ์ด ์ ์ ํ๋ฉฐ ์ผ๊ด์ฑ์ด ์ ์ ์ง๋๊ณ ์์ต๋๋ค์ฃผ๊ฐ ๋ญํน ๋ฆฌํฌ์งํ ๋ฆฌ ๊ตฌํ(
MvProductRankWeeklyRepositoryImpl)๊ณผ ๋์ผํ ํจํด๊ณผ ํธ๋์ญ์ ๊ฒฝ๊ณ ์ค์ ์ ์ฌ์ฉํ๊ณ ์์ด ์ฝ๋๋ฒ ์ด์ค์ ์ผ๊ด์ฑ์ด ์ฐ์ํฉ๋๋ค. ์ฝ๊ธฐ ์ ์ฉ ์์ ์๋readOnly=true๋ฅผ, ์ฐ๊ธฐ ์์ ์๋ ์ผ๋ฐ@Transactional์ ์ ์ ํ ์ฌ์ฉํ๊ณ ์์ต๋๋ค.apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyService.java (2)
27-34: ์ ์ฌ์ ๋์์ฑ ์ด์ ํ์ธ ํ์
getOrCreate๋ฉ์๋๊ฐ ๋ฝ ์์ด ์กฐํ(findByProductIdAndDate)ํ ํ ์์ฑํ๋ ํจํด์ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ๋์ผํ(productId, date)์กฐํฉ์ ๋ํด ์ฌ๋ฌ ์ค๋ ๋๊ฐ ๋์์ ์ด ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด ์ค๋ณต ์์ฑ์ ์๋ํ ์ ์์ต๋๋ค.๋ค๋ฅธ ๋ฉ์๋๋ค(
incrementLikeCount,incrementViewCount๋ฑ)์findByProductIdAndDateForUpdate๋ฅผ ์ฌ์ฉํ์ฌ ๋ฝ์ ํ๋ํ๋๋ฐ, ์ด ๋ฉ์๋๋ง ๋ฝ์ ์ฌ์ฉํ์ง ์๊ณ ์์ต๋๋ค.๋ฐ์ดํฐ๋ฒ ์ด์ค์
(productId, date)์ ๋ํ unique constraint๊ฐ ์๋ค๋ฉด ์ค๋ณต ์ฝ์ ์ ๋ฐฉ์ง๋์ง๋ง, ๋์ ์์ฑ ์๋ ์ ํ ์ชฝ์์ ์์ธ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.๋ค์์ ํ์ธํด ์ฃผ์ธ์:
ProductMetricsDailyํ ์ด๋ธ์(product_id, date)unique constraint๊ฐ ์ค์ ๋์ด ์๋์ง- ์ด ๋ฉ์๋๊ฐ ๋์ ํธ์ถ๋ ๊ฐ๋ฅ์ฑ์ด ์๋ ์๋๋ฆฌ์ค๊ฐ ์๋์ง
- ์๋์ ์ผ๋ก ๋ฝ์ ์ฌ์ฉํ์ง ์์ ๊ฒ์ธ์ง (์ฝ๊ธฐ ์์ฃผ ์๋๋ฆฌ์ค ๋ฑ)
ํ์ํ๋ค๋ฉด ๋ค๋ฅธ ๋ฉ์๋๋ค์ฒ๋ผ
findByProductIdAndDateForUpdate๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๊ณ ๋ คํด๋ณด์ธ์.
54-62: ์ ์ ํ ๊ฒ์ฆ ๋ก์ง์ ๋๋ค์กด์ฌํ์ง ์๋ ์ํฐํฐ์ ๋ํ ๊ฐ์ ์์ฒญ์
orElseThrow๋ก ์ฒ๋ฆฌํ์ฌ ์ ์ ํ ์์ธ๋ฅผ ๋ฐ์์ํค๊ณ ์์ต๋๋ค. ์ด๋ ์ฆ๊ฐ ๋ฉ์๋์ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌํด์ผ ํ๋ ๋น์ฆ๋์ค ๋ก์ง์ ์ฌ๋ฐ๋ฅด๊ฒ ๋ฐ์ํ๊ณ ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/ProductMetricsDailyReaderFactory.java (1)
25-62: LGTM!Factory ํจํด์ ์ฌ์ฉํ์ฌ Reader๋ฅผ ์์ฑํ๊ณ ,
@StepScope๋ฅผ ํตํด Job ํ๋ผ๋ฏธํฐ๋ฅผ late bindingํ๋ ๊ตฌ์กฐ๊ฐ ์ ์ ํฉ๋๋ค. periodType์ ๋ฐ๋ฅธ ๋ ์ง ๋ฒ์ ์๋ ๊ณ์ฐ ๋ก์ง๋ ๋ช ํํฉ๋๋ค.apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepositoryImpl.java (1)
13-41: LGTM!Repository ๊ตฌํ์ด ๊น๋ํ๊ณ , Monthly ๊ตฌํ์ฒด์ ์ผ๊ด๋ ํจํด์ ์ ์งํ๊ณ ์์ต๋๋ค. ๊ฐ ๋ฉ์๋์ ์ ์ ํ
@Transactional์ด๋ ธํ ์ด์ ์ด ์ ์ฉ๋์ด ์์ต๋๋ค.apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java (1)
13-16: LGTM!Spring Data JPA ๋ฉ์๋ ๋ค์ด๋ฐ ์ปจ๋ฒค์ ์ ์ ํ์ฉํ๊ณ ์์ผ๋ฉฐ, ์ฟผ๋ฆฌ ๋ฉ์๋ ์ ์๊ฐ ๋ช ํํฉ๋๋ค. Based on learnings, ๋๋ฉ์ธ ๋ฆฌํฌ์งํ ๋ฆฌ์์ Spring Data ํ์ ์ฌ์ฉ์ด ์ด ์ฝ๋๋ฒ ์ด์ค์์ ํ์ฉ๋ ์ค๊ณ ๊ฒฐ์ ์์ ํ์ธํ์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationService.java (3)
154-169: ์ฃผ๊ฐ ์คํจ ์ ์๊ฐ ์คํ ์คํต์ ๋ํ ์ ์ฑ ํ์ธํ์ฌ
executeWeeklyAndMonthlyRanking์์ ์ฃผ๊ฐ ๋ญํน ์คํ์ด ์คํจํ๋ฉด ์๊ฐ ๋ญํน์ ์คํ๋์ง ์์ต๋๋ค. ์ด๊ฒ์ด ์๋๋ ๋์์ธ์ง ํ์ธ์ด ํ์ํฉ๋๋ค. ๋ ์ง๊ณ๊ฐ ๋ ๋ฆฝ์ ์ด๋ผ๋ฉด ๊ฐ๊ฐ try-catch๋ก ๊ฐ์ธ์ ๋ชจ๋ ์คํ ํ ๊ฒฐ๊ณผ๋ฅผ ์ทจํฉํ๋ ๋ฐฉ์๋ ๊ณ ๋ คํ ์ ์์ต๋๋ค.
23-36: LGTM!์๋น์ค ๊ตฌ์กฐ๊ฐ ๋ช ํํ๊ณ ,
@Qualifier๋ฅผ ์ฌ์ฉํ์ฌ ํน์ Job Bean์ ์ฃผ์ ๋ฐ๋ ๋ฐฉ์์ด ์ ์ ํฉ๋๋ค. Spring Batch๊ฐ ์์ฒด ํธ๋์ญ์ ์ ๊ด๋ฆฌํ๋ฏ๋ก@Transactional์ ์ฌ์ฉํ์ง ์๋๋ค๋ ์ฃผ์๋ ์ ์ฉํฉ๋๋ค.
64-70: ZonedDateTime ๋ฌธ์์ด ํ์์ ISO-8601 ํธํ์ด๋ฉฐ ๋ณํ ๋ฌธ์ ์์
rankingDate.toString()์ ISO-8601 ํ์ฅ ํ์(์:2024-01-15T23:59:59+09:00[Asia/Seoul])์ ์์ฑํฉ๋๋ค. Spring Boot 3.4.4์ ํฌํจ๋ Spring Framework 6.x๋ ์ด ํ์์ ZonedDateTime์ผ๋ก ์๋ ๋ณํํ๋ฏ๋ก RankingJobExecutionListener์ RankingStepExecutionListener์ @value ๋ฐ์ธ๋ฉ์ด ์ ์ ์๋ํฉ๋๋ค. ์ฝ๋๋ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค.apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java (1)
18-40: LGTM!Repository ๊ตฌํ์ด ์ ๋ฐ์ ์ผ๋ก ๊น๋ํ๋ฉฐ, JPA Repository์ ์ ์ ํ๊ฒ ์์ํ๊ณ ์์ต๋๋ค. ํธ๋์ญ์ ์ค์ ๋ ๋ฉ์๋ ํน์ฑ์ ๋ง๊ฒ ์ ์ฉ๋์ด ์์ต๋๋ค. Based on learnings, ๋๋ฉ์ธ ๋ฆฌํฌ์งํ ๋ฆฌ์์ Spring Data์ Page/Pageable ํ์ ์ฌ์ฉ์ด ํ์ฉ๋ ์ค๊ณ ๊ฒฐ์ ์์ ํ์ธํ์ต๋๋ค.
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyRepository.java (3)
11-20: ๊ธฐ๋ณธ ์กฐํ ๋ฉ์๋ ์ค๊ณ๊ฐ ์ ์ ํฉ๋๋ค.
Optional๋ฐํ ํ์ ์ ์ฌ์ฉํ๊ณ ,ForUpdate์ ๋ฏธ์ฌ๋ก ๋ฝ ์๋๋ฅผ ๋ช ํํ ํํํ์ต๋๋ค. ๋ฉ์๋ ๋ช ๋ช ์ด ์ผ๊ด๋๊ณ ์๋๊ฐ ๋ช ํํฉ๋๋ค.
22-42: ๋ฐฐ์น ์ฒ๋ฆฌ๋ฅผ ์ํ ์ง๊ณ ๋ฉ์๋ ์ค๊ณ๊ฐ ์ฐ์ํฉ๋๋ค.์์ ๋ฐ์ดํฐ ์กฐํ(
findByDateBetween)์ ์ง๊ณ ๋ฐ์ดํฐ ์กฐํ(findAggregatedByDateBetween,findAggregatedByDateBetweenPaged)๋ฅผ ๋ช ํํ ๊ตฌ๋ถํ์ต๋๋ค. ํ์ด์ง ์ง์์ผ๋ก ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ฑ์ ํ๋ณดํ์ต๋๋ค.
44-53: ์ ์ฅ ๋ฉ์๋๊ฐ ํ์ค ํจํด์ ๋ฐ๋ฆ ๋๋ค.๋จ๊ฑด ์ ์ฅ๊ณผ ์ผ๊ด ์ ์ฅ์ ๋ชจ๋ ์ง์ํ๋ฉฐ,
Collectionํ์ ์ ์ฌ์ฉํด ํ์ ์์ ์ฑ์ ํ๋ณดํ์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1Controller.java (2)
16-24: ์ปจํธ๋กค๋ฌ ์ค์ ์ด ์ ์ ํฉ๋๋ค.Lombok๊ณผ Spring ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ ํ์ค ์ค์ ์ด๋ฉฐ, ๋ ์ง ํ์ฑ์ ์ํ ํฌ๋งคํฐ ์์๊ฐ ๋ช ํํ๊ฒ ์ ์๋์์ต๋๋ค.
61-80: ๋ ์ง ํ์ฑ ๋ก์ง์ ์์คํ ํ์์กด ์์กด์ฑ์ ํ์ธํ์ธ์.๋ ์ง ํ์ฑ ๋ก์ง์ ์ ๋ฐ์ ์ผ๋ก ์ ๊ตฌํ๋์ด ์์ผ๋, Line 73์์
ZoneId.systemDefault()๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ์ด๋ ์๋ฒ๊ฐ ์คํ๋๋ ํ๊ฒฝ์ ํ์์กด์ ๋ฐ๋ผ ๋ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.๋ถ์ฐ ํ๊ฒฝ์ด๋ ์๋ก ๋ค๋ฅธ ํ์์กด์์ ์คํ๋๋ ์๋ฒ๊ฐ ์๋ค๋ฉด ์ผ๊ด์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ๋ช ์์ ์ธ ํ์์กด(์:
ZoneId.of("Asia/Seoul")๋๋ZoneOffset.UTC)์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ณ ๋ คํด๋ณด์ธ์.๋ค์ ์ฌํญ์ ํ์ธํด์ฃผ์ธ์:
- ๋ฐฐ์น ์์ ์ด ์ฌ๋ฌ ์๋ฒ/์ปจํ ์ด๋์์ ์คํ๋ ๊ฐ๋ฅ์ฑ์ด ์๋์ง
- ํ ์คํธ ํ๊ฒฝ๊ณผ ํ๋ก๋์ ํ๊ฒฝ์ ํ์์กด์ด ๋์ผํ๊ฒ ์ค์ ๋์ด ์๋์ง
- ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ์ ํน์ ํ์์กด(์: KST)์ ์ฌ์ฉํด์ผ ํ๋์ง
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyJpaRepository.java (2)
18-27: ๋น๊ด์ ๋ฝ ์ ๋ต์ด ๋ฉํธ๋ฆญ ์ ๋ฐ์ดํธ์ ์ ํฉํฉ๋๋ค.
findByProductIdAndDateForUpdate๋ฉ์๋์PESSIMISTIC_WRITE๋ฝ์ ์ฌ์ฉํ์ฌ ๋์ ์ ๋ฐ์ดํธ ์ ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ๋ณด์ฅํฉ๋๋ค. ๋ช ์์ @Query๋ฅผ ํตํด ๋ฝ์ด ์ ํํ ์ ์ฉ๋๋๋ก ํ์ต๋๋ค.
29-45: JPQL ์ง๊ณ ์ฟผ๋ฆฌ๊ฐ ์ ์ ํ๊ฒ ๊ตฌํ๋์์ต๋๋ค.
productId๋ณ๋ก ๋ฉํธ๋ฆญ์ ์ง๊ณํ๋ ์ฟผ๋ฆฌ ๊ตฌ์กฐ๊ฐ ์ ํํ๋ฉฐ,SUMํจ์๋ฅผ ์ฌ์ฉํด ์ผ๋ณ ๋ฐ์ดํฐ๋ฅผ ํฉ์ฐํฉ๋๋ค. ๋ ์ง ๋ฒ์ ํํฐ๋ง์ด ์์ชฝ ๊ฒฝ๊ณ๋ฅผ ํฌํจ(>=,<=)ํ๋ฏ๋ก ๋น์ฆ๋์ค ๋ก์ง๊ณผ ์ผ์นํ๋์ง ํ์ธํ์ธ์.
| public class RankingAggregationJob { | ||
|
|
||
| private final PlatformTransactionManager transactionManager; | ||
| private final RankingService rankingService; | ||
| private final JobRepository jobRepository; | ||
| /** | ||
| * ์ฃผ๊ฐ, ์๊ฐ ๋ญํน ์ง๊ณ ์คํ | ||
| */ | ||
| public void execute() { | ||
| log.info("์ฃผ๊ฐ ๋ฐ ์๊ฐ ๋ญํน ์ง๊ณ ๋ฐฐ์น ์คํ ์์"); | ||
| // 1. ์ผ๊ฐ ์ง๊ณ ๋ฐ์ดํฐ ์ฝ๊ธฐ (tb_product_metrics) | ||
| // 2. ์ฃผ๊ฐ ๋ญํน ๊ณ์ฐ | ||
| // 3. ์๊ฐ ๋ญํน ๊ณ์ฐ | ||
| // 4. ๊ฒฐ๊ณผ๋ฅผ tb_mv_product_rank_weekly, tb_mv_product_rank_month | ||
|
|
||
| log.info("์ฃผ๊ฐ ๋ฐ ์๊ฐ ๋ญํน ์ง๊ณ ๋ฐฐ์น ์คํ ์๋ฃ"); | ||
|
|
||
| } | ||
| } |
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.
๋ฏธ์ฌ์ฉ/๋ฏธ์์ฑ ์ฝ๋ ์ ๊ฑฐ ํ์
์ด ํด๋์ค๋ ์ค์ ๊ตฌํ ๋ก์ง ์์ด ํ๋ ์ด์คํ๋ ์ฃผ์๋ง ํฌํจํ๊ณ ์์ผ๋ฉฐ, ์ฃผ์
๋ ์์กด์ฑ(transactionManager, rankingService, jobRepository)๋ ์ฌ์ฉ๋์ง ์์ต๋๋ค.
PR ์์ฝ๊ณผ ๋ค๋ฅธ ํ์ผ๋ค์ ๋ณด๋ฉด, ์ค์ ๋ฐฐ์น ๋ก์ง์ ๋ค์์ ๊ตฌํ๋์ด ์์ต๋๋ค:
RankingAggregationJobConfig: Job ์ ์RankingStepConfig: Step ์ ์ (Reader, Processor, Writer)RankingJobExecutionListener,RankingStepExecutionListener: ์คํ ๋ก์ง
๊ถ์ฅ์ฌํญ:
- ์ด ํด๋์ค๊ฐ ๋ ์ด์ ํ์ํ์ง ์๋ค๋ฉด ์ ๊ฑฐ
- ๋ฏธ๋์ ์ฌ์ฉํ ๊ณํ์ด๋ผ๋ฉด TODO ์ฃผ์์ ์ถ๊ฐํ๊ณ ์ค์ ์ฌ์ฉ ๊ณํ์ ๋ฌธ์ํ
- ์๋๋ฉด ์ค์ ๊ตฌํ์ ์๋ฃ
์ด ํด๋์ค๋ฅผ ์ ๊ฑฐํ๊ฑฐ๋ ์ค์ ๊ตฌํ์ ์๋ฃํ๋ ๊ฒ์ ๋์๋๋ฆด๊น์?
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJob.java
(lines 18-36), the class is a placeholder containing only comments and unused
injected fields; remove or complete it. Option A (preferred): delete this file
and remove any references to it so the project relies on
RankingAggregationJobConfig/RankingStepConfig and listeners for batch execution.
Option B (if keeping): replace placeholder with a minimal, documented TODO and
Javadoc stating planned behavior, mark it with an appropriate annotation if
needed (e.g., @Component) and either implement execute() to delegate to the real
JobLauncher/RankingService or wire and use the injected
transactionManager/rankingService/jobRepository; ensure no unused fields remain
and add unit tests or a TODO ticket reference.
| log.info("[RankingJobExecutionListener] processWeeklyRanking: ๊ธฐ์กด ์ฃผ๊ฐ ๋ญํน ๋ฐ์ดํฐ ์ญ์ ์ค - ๋ ์ง: {}", rankingDate); | ||
| weeklyRepository.deleteByRankingDate(rankingDate); | ||
| log.info("[RankingJobExecutionListener] processWeeklyRanking: ๊ธฐ์กด ์ฃผ๊ฐ ๋ญํน ๋ฐ์ดํฐ ์ญ์ ์๋ฃ - ๋ ์ง: {}", rankingDate); | ||
|
|
||
| log.info("[RankingJobExecutionListener] processWeeklyRanking: {}๊ฑด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฅ ์ค...", top100.size()); | ||
| weeklyRepository.saveAll(top100); | ||
| log.info("[RankingJobExecutionListener] processWeeklyRanking: ์ฃผ๊ฐ ๋ญํน {}๊ฑด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฅ ์๋ฃ", top100.size()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
find . -name "*.java" -type f | xargs rg -l "RankingJobExecutionListener" | head -5Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 192
๐ Script executed:
find . -name "*Repository.java" -type f | xargs rg -l "weeklyRepository|Weekly" | head -10Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 292
๐ Script executed:
rg -nP "@Transactional|class RankingJobExecutionListener" --type=java -A5 apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java 2>/dev/null | head -50Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 370
๐ Script executed:
cat -n ./apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 959
๐ Script executed:
sed -n '120,150p' ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1963
๐ Script executed:
rg -nP "processWeeklyRanking|@Transactional" --type=java ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2375
๐ Script executed:
cat -n ./apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1137
๐ Script executed:
sed -n '1,80p' ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 4004
๐ Script executed:
rg -nP "class RankingJobExecutionListener|@Transactional|afterJob|void afterJob" --type=java ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java -B2 -A2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1677
๐ Script executed:
rg -nP "@Transactional|PlatformTransactionManager|TransactionTemplate" --type=java ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/ -A2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1822
๐ Script executed:
rg -nP "class.*RankingJobExecutionListener|afterJob|processWeeklyRanking" --type=java ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2302
๐ Script executed:
rg -nP "JobExecutionListener|@Transactional" --type=java ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJob.java -B5 -A5Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
๐ Script executed:
sed -n '78,140p' ./apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java | cat -nRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 4195
processWeeklyRanking ๋ฉ์๋์ @Transactional ์ถ๊ฐ ํ์
deleteByRankingDate์ saveAll์ด ๋ณ๋์ ํธ๋์ญ์
์์ ์คํ๋ฉ๋๋ค. saveAll ์ค ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ๊ธฐ์กด ๋ฐ์ดํฐ๋ ์ด๋ฏธ ์ญ์ ๋ ์ํ๋ก ๋ฐ์ดํฐ ์์ค์ด ๋ฐ์ํฉ๋๋ค. ์ด ๋ฉ์๋์ @Transactional ์ ๋ํ
์ด์
์ ์ถ๊ฐํ์ฌ ๋ ์์
์ด ๋จ์ผ ํธ๋์ญ์
๋ด์์ ์์์ฑ ์๊ฒ ์คํ๋๋๋ก ํด์ฃผ์ธ์.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingJobExecutionListener.java
around lines 128 to 134, the deleteByRankingDate and saveAll calls run in
separate transactions which can leave data deleted if saveAll fails; annotate
the processWeeklyRanking method with @Transactional so both operations run in a
single transaction and will roll back together on failure, and add the import
org.springframework.transaction.annotation.Transactional (ensure the class is a
Spring-managed bean and transaction management is enabled).
| public static PriorityQueue<ProductMetricsDailyAggregated> getTop100Queue() { | ||
| PriorityQueue<ProductMetricsDailyAggregated> queue = top100Queue.get(); | ||
| if (queue == null) { | ||
| log.warn("[RankingProcessor] getTop100Queue: PriorityQueue๊ฐ null์ ๋๋ค!"); | ||
| } else { | ||
| log.info("[RankingProcessor] getTop100Queue: ํ ๋ฐํ - size={}, thread={}", | ||
| queue.size(), Thread.currentThread().getName()); | ||
| } | ||
| return queue; | ||
| } |
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.
static ๋ฉ์๋์์ ์ธ์คํด์ค ๋ก๊ฑฐ ์ ๊ทผ ๋ถ๊ฐ
getTop100Queue()๊ฐ static ๋ฉ์๋๋ก ์ ์ธ๋์ด ์์ง๋ง, @Slf4j๋ก ์์ฑ๋ log ํ๋๋ ์ธ์คํด์ค ํ๋์
๋๋ค. ์ด ์ฝ๋๋ ์ปดํ์ผ ์ค๋ฅ๋ฅผ ๋ฐ์์ํต๋๋ค.
๐ ์์ ์ ์
-public static PriorityQueue<ProductMetricsDailyAggregated> getTop100Queue() {
+public PriorityQueue<ProductMetricsDailyAggregated> getTop100Queue() {
PriorityQueue<ProductMetricsDailyAggregated> queue = top100Queue.get();
if (queue == null) {
log.warn("[RankingProcessor] getTop100Queue: PriorityQueue๊ฐ null์
๋๋ค!");
} else {
log.info("[RankingProcessor] getTop100Queue: ํ ๋ฐํ - size={}, thread={}",
queue.size(), Thread.currentThread().getName());
}
return queue;
}
-public static void clearTop100Queue() {
+public void clearTop100Queue() {
top100Queue.remove();
}๋๋ static ๋ฉ์๋๋ฅผ ์ ์งํด์ผ ํ๋ค๋ฉด ๋ณ๋์ static ๋ก๊ฑฐ๋ฅผ ์ ์ธํด์ผ ํฉ๋๋ค:
private static final Logger staticLog = LoggerFactory.getLogger(RankingProcessor.class);๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java
around lines 106 to 115: the static method getTop100Queue() uses the instance
logger `log` generated by @Slf4j which causes a compile error; either make the
method non-static or add/use a static logger. Fix by replacing instance logger
usage with a static logger (declare a private static final Logger STATIC_LOG =
LoggerFactory.getLogger(RankingProcessor.class)) and update the log calls inside
getTop100Queue() to use STATIC_LOG, or alternatively remove static from the
method if instance access is acceptable.
| PriorityQueue<ProductMetricsDailyAggregated> queue = RankingProcessor.getTop100Queue(); | ||
|
|
||
| if (queue == null) { | ||
| log.error("[RankingStepExecutionListener] afterStep: PriorityQueue๊ฐ null์ ๋๋ค!"); | ||
| return stepExecution.getExitStatus(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
#!/bin/bash
# First, find the RankingProcessor file to understand ThreadLocal usage
find apps/commerce-api -name "RankingProcessor.java" -type fRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 179
๐ Script executed:
#!/bin/bash
# Check the ThreadLocal and scope patterns in RankingProcessor
rg -nP 'ThreadLocal|getTop100Queue|@StepScope|@BeforeStep|@AfterStep' --type=java apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1644
๐ Script executed:
#!/bin/bash
# Examine the RankingStepExecutionListener file to understand the afterStep context
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java | head -100Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 6148
๐ Script executed:
#!/bin/bash
# Get the full RankingProcessor.java to understand ThreadLocal usage and initialization
wc -l apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 183
๐ Script executed:
#!/bin/bash
# Read the relevant parts of RankingProcessor.java focusing on ThreadLocal and @BeforeStep
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java | head -130Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 6480
๐ Script executed:
#!/bin/bash
# Check the getTop100Queue method implementation
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingProcessor.java | sed -n '106,115p'Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 641
๐ Script executed:
#!/bin/bash
# Find job configuration files
find apps/commerce-api -name "*JobConfig*" -o -name "*Job.java" | grep -i rankingRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 287
๐ Script executed:
#!/bin/bash
# Check if clearTop100Queue is called anywhere
rg 'clearTop100Queue|top100Queue\.remove' --type=java apps/commerce-api/Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 620
๐ Script executed:
#!/bin/bash
# Check RankingStepExecutionListener for where clearTop100Queue is called
rg -n 'clearTop100Queue' --type=java -B3 -A3 apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 389
๐ Script executed:
#!/bin/bash
# Check job configuration to understand step structure
wc -l apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJobConfig.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 193
๐ Script executed:
#!/bin/bash
# Read the job config to see step sequencing
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJobConfig.java | head -80Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1220
๐ Script executed:
#!/bin/bash
# Check the complete afterStep method to understand error handling
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java | sed -n '54,135p'Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 5891
๐ Script executed:
#!/bin/bash
# Verify Spring Batch behavior for @StepScope and thread context
# Look for how other listeners handle failures
rg 'return ExitStatus.FAILED' --type=java apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 375
๐ Script executed:
#!/bin/bash
# Check if there are any test files that verify thread behavior
find apps/commerce-api -name "*RankingProcessor*Test*" -o -name "*RankingListener*Test*"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
PriorityQueue๊ฐ null ๋๋ ๋น์ด์์ ๋ ์คํ ์ ์คํจ ์ํ๋ก ํ์ํด์ผ ํฉ๋๋ค
ํ์ฌ ์ฝ๋๋ queue๊ฐ null์ด๊ฑฐ๋ ๋น์ด์๋ ๊ฒฝ์ฐ ๋ก๊ทธ๋ง ๋จ๊ธฐ๊ณ stepExecution.getExitStatus()๋ฅผ ๋ฐํํ๋ฏ๋ก ์คํ
์ด ์ฑ๊ณต์ผ๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค. ์ด๋ ์ผ๊ด์ฑ ์๋ ์ค๋ฅ ์ฒ๋ฆฌ์
๋๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก ์คํจํ๋ ๊ฒฝ์ฐ๋ฅผ ์ฒ๋ฆฌํ๋ 122์ค, 128์ค๊ณผ ๊ฐ์ด null ๋๋ empty ์ฒดํฌ ์์๋ ExitStatus.FAILED๋ฅผ ๋ฐํํด์ผ ํฉ๋๋ค.
if (queue == null) {
log.error("[RankingStepExecutionListener] afterStep: PriorityQueue๊ฐ null์
๋๋ค!");
return ExitStatus.FAILED;
}
if (queue.isEmpty()) {
log.warn("[RankingStepExecutionListener] afterStep: PriorityQueue๊ฐ ๋น์ด์์ต๋๋ค!");
return ExitStatus.FAILED;
}๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java
around lines 63 to 68 (and ensure consistency at the similar checks on lines
~122 and ~128), the code currently logs when the PriorityQueue is null or empty
but returns stepExecution.getExitStatus(), causing the step to be treated as
successful; change these returns to ExitStatus.FAILED so the step fails when the
queue is null or empty, and make both the null-check and the isEmpty-check
return ExitStatus.FAILED (use log.error for null and log.warn for empty) to
mirror the intended failure handling consistently across the file.
| if (weekly.getRankingDate() == null) { | ||
| log.error("[RankingStepExecutionListener] afterStep: ์์ดํ [{}]์ rankingDate๊ฐ NULL์ ๋๋ค - productId={}", | ||
| i, weekly.getProductId()); | ||
| } | ||
|
|
||
| weeklyRankings.add(weekly); |
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.
rankingDate๊ฐ null์ธ ๊ฒฝ์ฐ ์ฒ๋ฆฌ ๋ก์ง ๊ฒํ ํ์
rankingDate๊ฐ null์ธ ๊ฒฝ์ฐ ์๋ฌ ๋ก๊ทธ๋ง ๋จ๊ธฐ๊ณ weeklyRankings์ ์ถ๊ฐ๋ฉ๋๋ค. ์ด ์ํ๋ก DB์ ์ ์ฅ๋๋ฉด ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. null์ธ ๊ฒฝ์ฐ ์ถ๊ฐํ์ง ์๊ฑฐ๋ ์์ธ๋ฅผ ๋ฐ์์ํค๋ ๊ฒ์ด ์ ์ ํฉ๋๋ค.
๐ ์ ์ํ๋ ์์ ์ฌํญ
if (weekly.getRankingDate() == null) {
log.error("[RankingStepExecutionListener] afterStep: ์์ดํ
[{}]์ rankingDate๊ฐ NULL์
๋๋ค - productId={}",
i, weekly.getProductId());
+ failureCount++;
+ continue;
}
weeklyRankings.add(weekly);๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingStepExecutionListener.java
around lines 161 to 166, the code currently logs an error when
weekly.getRankingDate() is null but still adds the entity to weeklyRankings;
change this so null-rankingDate entries are not added (or explicitly throw a
runtime exception) to prevent invalid DB writes: either surround the add with an
else branch so weeklyRankings.add(weekly) only occurs when rankingDate != null,
or after logging throw a descriptive IllegalStateException to stop processing;
ensure any callers/transactional behavior are adjusted accordingly (or update
tests) to reflect the chosen approach.
| public double calculateScore() { | ||
| return totalViewCount * 0.1 + totalLikeCount * 0.2 + totalSoldCount * 0.7; | ||
| } |
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.
Null ์์ ์ฑ ๊ฒ์ฆ ํ์ - NPE ์ํ
calculateScore() ๋ฉ์๋๊ฐ null ์ฒดํฌ ์์ด ์ฐ์ ์ฐ์ฐ์ ์ํํฉ๋๋ค. totalViewCount, totalLikeCount, totalSoldCount ์ค ํ๋๋ผ๋ null์ด๋ฉด NullPointerException์ด ๋ฐ์ํ์ฌ ๋ฐฐ์น ์ฒ๋ฆฌ๊ฐ ์คํจํฉ๋๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ง๊ณ ์ null ๊ฐ์ด ๋ฐํ๋ ๊ฐ๋ฅ์ฑ(์: ํน์ ๋ฉํธ๋ฆญ์ด ์๋ ์ํ)์ด ์๋ค๋ฉด ๋ฐฉ์ด ๋ก์ง์ด ํ์์ ๋๋ค.
๐ Null ์์ ์ฑ ๊ฐ์ ๋ฐฉ์
๋ฐฉ์ 1 (๊ถ์ฅ): Null์ 0์ผ๋ก ์ฒ๋ฆฌ
public double calculateScore() {
- return totalViewCount * 0.1 + totalLikeCount * 0.2 + totalSoldCount * 0.7;
+ long viewCount = totalViewCount != null ? totalViewCount : 0L;
+ long likeCount = totalLikeCount != null ? totalLikeCount : 0L;
+ long soldCount = totalSoldCount != null ? totalSoldCount : 0L;
+ return viewCount * 0.1 + likeCount * 0.2 + soldCount * 0.7;
}๋ฐฉ์ 2: ์์ฑ์์์ ๊ฒ์ฆ
@Getter
-@AllArgsConstructor
public class ProductMetricsDailyAggregated {
private Long productId;
private Long totalLikeCount;
private Long totalViewCount;
private Long totalSoldCount;
+
+ public ProductMetricsDailyAggregated(Long productId, Long totalLikeCount, Long totalViewCount, Long totalSoldCount) {
+ this.productId = productId;
+ this.totalLikeCount = totalLikeCount != null ? totalLikeCount : 0L;
+ this.totalViewCount = totalViewCount != null ? totalViewCount : 0L;
+ this.totalSoldCount = totalSoldCount != null ? totalSoldCount : 0L;
+ }๐ 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.
| public double calculateScore() { | |
| return totalViewCount * 0.1 + totalLikeCount * 0.2 + totalSoldCount * 0.7; | |
| } | |
| public double calculateScore() { | |
| long viewCount = totalViewCount != null ? totalViewCount : 0L; | |
| long likeCount = totalLikeCount != null ? totalLikeCount : 0L; | |
| long soldCount = totalSoldCount != null ? totalSoldCount : 0L; | |
| return viewCount * 0.1 + likeCount * 0.2 + soldCount * 0.7; | |
| } |
๐ค Prompt for AI Agents
In
apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDailyAggregated.java
around lines 18 to 20: calculateScore() performs arithmetic on totalViewCount,
totalLikeCount, and totalSoldCount without null checks which can cause NPEs;
update the method to defensively handle nulls (preferred: treat any null metric
as 0 before computing the weighted sum) or alternatively ensure these fields are
validated/non-null in the constructor/setters so the method can safely assume
non-null values.
| private Long productId; | ||
| private String productName; | ||
| private Long brandId; | ||
| private String brandName; | ||
| private Double score; | ||
| private Integer likeCount; | ||
| private Integer viewCount; | ||
| private Integer orderCount; | ||
| private Integer ranking; | ||
| private ZonedDateTime rankingDate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
#!/bin/bash
# Search for SQL migration and DDL files related to tb_mv_product_rank_weekly table
echo "=== Searching for SQL files with tb_mv_product_rank_weekly reference ==="
fd -e sql | head -20
echo -e "\n=== Searching for tb_mv_product_rank_weekly table definition ==="
fd -e sql -x grep -l "tb_mv_product_rank_weekly" 2>/dev/null
echo -e "\n=== Searching for migration/schema directories ==="
fd -type d -name "*migration*" -o -name "*sql*" -o -name "*schema*" | grep -E "(migration|sql|schema|db)" | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 466
๐ Script executed:
#!/bin/bash
# More comprehensive search for database-related files
echo "=== Searching for Flyway migration files ==="
fd -path "*/db/migration/*" -type f | head -30
echo -e "\n=== Searching for Liquibase changelog files ==="
fd -e xml -o -e yaml | xargs grep -l "product_rank_weekly" 2>/dev/null | head -20
echo -e "\n=== Searching in common database locations ==="
find . -type f \( -name "*.sql" -o -name "*.xml" \) 2>/dev/null | grep -iE "(db|migration|schema|changelog)" | head -30Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 532
๐ Script executed:
#!/bin/bash
# Search for the table definition and index information more broadly
echo "=== Looking for any reference to MvProductRankWeekly or product_rank_weekly ==="
rg -l "mv_product_rank_weekly|MvProductRankWeekly" --max-count 20 2>/dev/null
echo -e "\n=== Looking for CREATE TABLE statements ==="
rg "CREATE TABLE.*product_rank_weekly" -A 20 2>/dev/null | head -50Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1005
๐ Script executed:
#!/bin/bash
# Read the entity class to check for @Index annotations
echo "=== MvProductRankWeekly.java entity class ==="
cat -n apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java
echo -e "\n=== MvProductRankWeeklyRepository interface ==="
cat -n apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeeklyRepository.java
echo -e "\n=== MvProductRankWeeklyJpaRepository interface ==="
cat -n apps/commerce-core/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2987
๐ Script executed:
#!/bin/bash
# Search for schema definitions in resources
echo "=== Searching for schema files in resources ==="
find . -path "*/resources/*" -name "*schema*" -o -path "*/resources/*" -name "*.sql" | head -20
echo -e "\n=== Searching for application.properties or application.yml ==="
find . -name "application*.properties" -o -name "application*.yml" | xargs grep -l "jpa.hibernate.ddl" 2>/dev/null | head -10
echo -e "\n=== Checking for schema.sql in resources ==="
find . -name "schema.sql" -o -name "schema-*.sql" 2>/dev/nullRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 246
๐ Script executed:
#!/bin/bash
# Search for @Table and @Index annotations in the entity
echo "=== Checking for @Table and @Index annotations ==="
rg -A 5 "@Table|@Index" apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java 2>/dev/nullRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 311
๐ Script executed:
#!/bin/bash
# Check BaseEntity to understand the inheritance
echo "=== BaseEntity class ==="
rg -A 20 "class BaseEntity" --max-count 1 2>/dev/null
echo -e "\n=== Checking for any existing @Index usage elsewhere ==="
rg "@Index" apps/commerce-core/src/main/java/com/loopers/domain/ | head -10Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3606
@Index ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ค ์ ์
MvProductRankWeekly ์ํฐํฐ์ ์ธ๋ฑ์ค๊ฐ ์ ์๋์ด ์์ง ์์ต๋๋ค. ๋ค์ ์ธ๋ฑ์ค๋ฅผ @Table ์ด๋
ธํ
์ด์
์ indexes ํ๋ผ๋ฏธํฐ์ ์ถ๊ฐํ์ธ์:
@Table(name = "tb_mv_product_rank_weekly", indexes = {
@Index(name = "idx_mv_product_rank_weekly_ranking_date", columnList = "ranking_date"),
@Index(name = "idx_mv_product_rank_weekly_product_date", columnList = "product_id,ranking_date"),
@Index(name = "idx_mv_product_rank_weekly_date_ranking", columnList = "ranking_date,ranking")
})์ด๋ ๋ค์ ์ฟผ๋ฆฌ ๋ฉ์๋๋ค์ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํด ํ์ํฉ๋๋ค:
findByProductIdAndRankingDate(): product_id + ranking_date ๋ณตํฉ ์ธ๋ฑ์ค ํ์findTop100ByRankingDateOrderByRankingAsc(): ranking_date + ranking ๋ณตํฉ ์ธ๋ฑ์ค ํ์deleteByRankingDate(): ranking_date ์ธ๋ฑ์ค ํ์
๐ค Prompt for AI Agents
In
apps/commerce-core/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java
around lines 16 to 25, the entity lacks DB indexes; update the class-level JPA
@Table annotation to include the provided indexes:
idx_mv_product_rank_weekly_ranking_date on ranking_date,
idx_mv_product_rank_weekly_product_date on product_id,ranking_date, and
idx_mv_product_rank_weekly_date_ranking on ranking_date,ranking so the
repository methods (findByProductIdAndRankingDate,
findTop100ByRankingDateOrderByRankingAsc, deleteByRankingDate) are properly
optimized; add the indexes parameter to @Table (or create a new @Table if
missing) ensuring column names match the entity column mappings.
| /** | ||
| * ํน์ ๋ ์ง ๋ฒ์์ ๋ฉํธ๋ฆญ์ product_id๋ณ๋ก ์ง๊ณํ์ฌ ํ์ด์ง ์กฐํ (๋ฐฐ์น์ฉ) | ||
| * | ||
| * @param startDate ์์์ผ | ||
| * @param endDate ์ข ๋ฃ์ผ | ||
| * @param pageable ํ์ด์ง ์ ๋ณด | ||
| * @return ์ง๊ณ๋ ๋ฉํธ๋ฆญ ํ์ด์ง | ||
| */ | ||
| @Query( | ||
| value = | ||
| "SELECT " + | ||
| " pmd.product_id as productId, " + | ||
| " SUM(pmd.like_count) as totalLikeCount, " + | ||
| " SUM(pmd.view_count) as totalViewCount, " + | ||
| " SUM(pmd.sold_count) as totalSoldCount " + | ||
| "FROM tb_product_metrics_daily pmd " + | ||
| "WHERE pmd.date >= :startDate AND pmd.date <= :endDate " + | ||
| "GROUP BY pmd.product_id " + | ||
| "ORDER BY pmd.product_id", | ||
| countQuery = | ||
| "SELECT COUNT(DISTINCT pmd.product_id) " + | ||
| "FROM tb_product_metrics_daily pmd " + | ||
| "WHERE pmd.date >= :startDate AND pmd.date <= :endDate", | ||
| nativeQuery = true | ||
| ) | ||
| Page<Object[]> findAggregatedByDateBetweenPaged( | ||
| @Param("startDate") LocalDate startDate, | ||
| @Param("endDate") LocalDate endDate, | ||
| Pageable pageable | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
# Search for table definitions in SQL migration files
fd -e sql -e xml | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 547
๐ Script executed:
# Find the entity class for ProductMetricsDaily
fd -type f -name "*ProductMetricsDaily*"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 258
๐ Script executed:
# Search for @Table annotation related to product metrics
rg -n "@Table|@Column" --type java | grep -i "product.*metric\|metric.*daily" -A 3 -B 3Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1650
๐ Script executed:
# Find entity class definitions with field mappings
rg -n "class ProductMetricsDaily" --type java -A 50Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 37490
๐ Script executed:
# Search for any schema definitions or migration scripts
fd -e sql | xargs grep -l "tb_product_metrics_daily" 2>/dev/nullRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 72
๐ Script executed:
# Get the complete @Table annotation for ProductMetricsDaily
cat apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 739
๐ Script executed:
# Search for the table name in the @Table annotation
rg -n "@Table" apps/commerce-core/src/main/java/com/loopers/domain/metrics/product/ProductMetricsDaily.java -A 2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 159
๋ค์ดํฐ๋ธ ์ฟผ๋ฆฌ์ ํ ์ด๋ธ๋ช ๊ณผ ์ปฌ๋ผ๋ช ์ด ์คํค๋ง์ ์ผ์นํฉ๋๋ค.
ํ
์ด๋ธ๋ช
tb_product_metrics_daily์ ๋ชจ๋ ์ปฌ๋ผ๋ช
(product_id, like_count, view_count, sold_count)์ด ProductMetricsDaily ์ํฐํฐ ์ ์์ ์ ํํ ์ผ์นํ๋ฉฐ, countQuery๋ฅผ ๋ณ๋๋ก ์ ๊ณตํ ํ์ด์ง ์ต์ ํ๋ ์ ์ ํฉ๋๋ค.
๋ค๋ง, ORDER BY pmd.product_id๋ ์ผ๊ด๋ ํ์ด์ง์ ๋ณด์ฅํ์ง๋ง ๋ฐฐ์น ์ฒ๋ฆฌ์ ์ค์ ๋น์ฆ๋์ค ๋ชฉํ(์: ํ๋งค๋ ๋๋ ์กฐํ์ ๊ธฐ์ค ์์ ๊ณ์ฐ)์ ๋ง๋์ง ํ์ธํ์ธ์. ํ์์ ORDER BY SUM(pmd.sold_count) DESC ๊ฐ์ ์๋ฏธ ์๋ ์ ๋ ฌ๋ก ๋ณ๊ฒฝํ๋ฉด ๋ ํจ์จ์ ์ผ ์ ์์ต๋๋ค.
๐ค Prompt for AI Agents
In
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyJpaRepository.java
around lines 47 to 76, the native query currently orders results by
pmd.product_id which ensures pagination stability but may not reflect the
intended business ordering for the batch (e.g., ranking by sales or views);
change the ORDER BY clause to a business-meaningful aggregate such as ORDER BY
SUM(pmd.sold_count) DESC (or SUM(pmd.view_count) DESC / SUM(pmd.like_count) DESC
as appropriate), and if stability is needed add a secondary tie-breaker like
pmd.product_id; keep the countQuery as-is for paging.
| @Override | ||
| public Page<ProductMetricsDailyAggregated> findAggregatedByDateBetweenPaged(LocalDate startDate, LocalDate endDate, Pageable pageable) { | ||
| Page<Object[]> results = jpaRepository.findAggregatedByDateBetweenPaged( | ||
| startDate, endDate, pageable | ||
| ); | ||
|
|
||
| return results.map(row -> new ProductMetricsDailyAggregated( | ||
| ((Number) row[0]).longValue(), // productId | ||
| ((Number) row[1]).longValue(), // totalLikeCount | ||
| ((Number) row[2]).longValue(), // totalViewCount | ||
| ((Number) row[3]).longValue() // totalSoldCount | ||
| )); | ||
| } |
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.
@transactional(readOnly = true) ๋๋ฝ
findAggregatedByDateBetweenPaged ๋ฉ์๋์ @Transactional ์ด๋
ธํ
์ด์
์ด ๋๋ฝ๋์ด ์์ต๋๋ค. ๋ค๋ฅธ ์กฐํ ๋ฉ์๋๋ค๊ณผ ์ผ๊ด์ฑ์ ์ํด @Transactional(readOnly = true)๋ฅผ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
๐ ์ ์ํ๋ ์์ ์ฌํญ
@Override
+ @Transactional(readOnly = true)
public Page<ProductMetricsDailyAggregated> findAggregatedByDateBetweenPaged(LocalDate startDate, LocalDate endDate, Pageable pageable) {๐ค Prompt for AI Agents
In
apps/commerce-core/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsDailyRepositoryImpl.java
around lines 59 to 71, the method findAggregatedByDateBetweenPaged is missing
the @Transactional(readOnly = true) annotation; add @Transactional(readOnly =
true) immediately above the method signature to match other read-only queries,
and if the import is absent add import
org.springframework.transaction.annotation.Transactional; ensure no other
transactional attributes are changed.
| LocalDate today = LocalDate.now(); | ||
|
|
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.
์ด๋ฒคํธ ๋ฐ์ ์๊ฐ์ด ์๋ ์ฒ๋ฆฌ ์๊ฐ์ ์ฌ์ฉํ๋ ๋ฌธ์
ํ์ฌ ๊ตฌํ์ ๋ฐฐ์น ์ฒ๋ฆฌ ์์ ์ ๋ ์ง๋ฅผ ์ฌ์ฉํ์ฌ ๋ชจ๋ ์ด๋ฒคํธ๋ฅผ ๋์ผํ ๋ ์ง๋ก ๊ธฐ๋กํฉ๋๋ค. ์ด๋ ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๋ฅผ ์ผ๊ธฐํ ์ ์์ต๋๋ค:
- ์ง์ฐ๋ ์ด๋ฒคํธ ์ฒ๋ฆฌ: Kafka ์ด๋ฒคํธ๊ฐ ์ง์ฐ๋๊ฑฐ๋ ์ฌ์ฒ๋ฆฌ๋ ๊ฒฝ์ฐ, ์ค์ ์ด๋ฒคํธ ๋ฐ์์ผ์ด ์๋ ์ฒ๋ฆฌ์ผ์ ๋ฉํธ๋ฆญ์ด ๊ธฐ๋ก๋ฉ๋๋ค.
- ํ์์กด ๋ถ์ผ์น:
LocalDate.now()๋ ์์คํ ๊ธฐ๋ณธ ํ์์กด์ ์ฌ์ฉํ๋ฏ๋ก ๋ถ์ฐ ํ๊ฒฝ์์ ์ผ๊ด์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. - ์์ ๊ฒฝ๊ณ ๋ฌธ์ : ๋ฐฐ์น ์ฒ๋ฆฌ๊ฐ ์์ ์ ๋์ด๊ฐ๋ฉด ๋์ผ ๋ฐฐ์น ๋ด ์ด๋ฒคํธ๋ค์ด ์๋ก ๋ค๋ฅธ ๋ ์ง์ ๊ธฐ๋ก๋ ์ ์์ต๋๋ค.
๐ ์ด๋ฒคํธ ํ์์คํฌํ ๊ธฐ๋ฐ ์ฒ๋ฆฌ๋ก ๊ฐ์
- LocalDate today = LocalDate.now();
-
for (ConsumerRecord<Object, Object> record : records) {
try {
+ // ์ด๋ฒคํธ ์ค์ ๋ฐ์ ์๊ฐ ์ฌ์ฉ (Kafka record timestamp)
+ LocalDate eventDate = Instant.ofEpochMilli(record.timestamp())
+ .atZone(ZoneId.of("Asia/Seoul")) // ๋๋ ์ค์ ์์ ์ฃผ์
+ .toLocalDate();
+
String key = record.key() != null ? record.key().toString() : null;
Object value = record.value();
// JSON ๋ฌธ์์ด์ Map์ผ๋ก ํ์ฑ
Map<String, Object> message = parseMessage(value);
// ์ด๋ฒคํธ ์ ๋ณด ์ถ์ถ
String eventId = extractEventId(message);
String eventType = extractEventType(message);
String aggregateId = key != null ? key : extractAggregateId(message);
if (eventId == null || eventType == null || aggregateId == null) {
log.warn("Missing required fields in message: eventId={}, eventType={}, aggregateId={}",
eventId, eventType, aggregateId);
continue;
}
// ๋ฉฑ๋ฑ์ฑ ์ฒดํฌ
if (idempotencyService.isAlreadyHandled(eventId)) {
log.warn("Event already handled, skipping: eventId={}, eventType={}", eventId, eventType);
continue;
}
// ์ด๋ฒคํธ ํ์
๋ณ ์ฒ๋ฆฌ
- handleEvent(eventType, aggregateId, today);
+ handleEvent(eventType, aggregateId, eventDate);์ด ๋ฐฉ์์:
- ์ค์ ์ด๋ฒคํธ ๋ฐ์ ์๊ฐ์ ๊ธฐ์ค์ผ๋ก ์ผ๋ณ ๋ฉํธ๋ฆญ์ ์ ํํ๊ฒ ์ง๊ณํฉ๋๋ค.
- ํ์์กด์ ๋ช ์์ ์ผ๋ก ์ง์ ํ์ฌ ๋ถ์ฐ ํ๊ฒฝ์์๋ ์ผ๊ด์ฑ์ ๋ณด์ฅํฉ๋๋ค.
- ์ง์ฐ๋๊ฑฐ๋ ์ฌ์ฒ๋ฆฌ๋ ์ด๋ฒคํธ๋ ์ฌ๋ฐ๋ฅธ ๋ ์ง์ ๊ธฐ๋ก๋ฉ๋๋ค.
Committable suggestion skipped: line range outside the PR's diff.
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/listener/CatalogEventListener.java
around lines 62-63, replace the use of LocalDate.now() (processing time) with
the event's timestamp: extract the event occurrence instant (e.g. a timestamp
field from the Kafka payload or record metadata), convert it to a LocalDate
using a fixed ZoneId (preferably UTC or a configured ZoneId) via Instant ->
ZonedDateTime/LocalDate, and use that date for metric recording; if the event
timestamp is missing or unparsable, fall back to a safe default and log a
warning so you donโt silently misattribute dates.
๐ Summary
Spring Batch๋ฅผ ํ์ฉํ ์ฃผ๊ฐ/์๊ฐ ์ํ ๋ญํน ์ง๊ณ ๋ฐฐ์น ์์คํ ์ ๊ตฌํํ์ต๋๋ค.
์ฃผ์ ๊ตฌํ ๋ด์ฉ:
ProductMetricsDaily๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฃผ๊ฐ/์๊ฐ TOP 100 ๋ญํน ์ง๊ณ๊ฒฐ๊ณผ:
MvProductRankWeekly,MvProductRankMonthlyํ ์ด๋ธ์ ์ ์ฅ๐ฌ Review Points
(1) ๋ฆฌ๋ทฐ์ด๊ฐ ์ค์ ์ ์ผ๋ก ๋ด์คฌ์ผ๋ฉด ํ๋ ๋ถ๋ถ
1.1 ํตํฉ Job/Step ๊ตฌ์กฐ ์ค๊ณ
RankingAggregationJobConfig,RankingStepConfigperiodTypeํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ์ฉํ ํตํฉ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝํ์ต๋๋ค.1.2 PriorityQueue๋ฅผ ํตํ TOP 100 ํํฐ๋ง ๋ก์ง
RankingProcessor.process()1.3 ProductMetricsDaily ๊ตฌํ
์ผ๊ฐ ๋ฐ์ดํฐ์ ๋๊ณ๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํด daily ์ง๊ณ๋ฅผ ์ถ๊ฐํ์์ต๋๋ค.
์ด๋ฒคํธ ๋ฐ์์ ๊ธฐ์กด ๋ฉํธ๋ฆญ์ ๋๊ณ๋ก ์ฌ์ฉ๋ฉ๋๋ค.
์ง๊ธ์ด์ผ ๋ ์ง๋ก ๊ทธ๋ฃนํํ์ง๋ง, ๋ง์ ๋ณ์(๋ชฐ๋ฆฌ๋ ์๊ฐ ๋ฑ)๋ค์ ๊ด๋ฆฌํ๊ธฐ ์ํ ๋ฐฉ๋ฒ์ด ๊ถ๊ธํฉ๋๋ค.
(2) ๊ณ ๋ฏผํ๋ ์ค๊ณ ํฌ์ธํธ๋ ๋ก์ง
2.1 Reader์ ๋ ์ง ๋ฒ์ ์๋ ๊ณ์ฐ
ProductMetricsDailyReaderFactory์์periodType์ ๋ฐ๋ผ ๋ ์ง ๋ฒ์๋ฅผ ๊ฒฐ์ (3) ๋ฆฌ๋ทฐ์ด๊ฐ ํ์ธํด์คฌ์ผ๋ฉด ํ๋ ํ ์คํธ ์ผ์ด์ค๋ ์์ธ ์ํฉ
(4) ๊ธฐํ ๋ฆฌ๋ทฐ์ด๊ฐ ์ฐธ๊ณ ํด์ผ ํ ์ฌํญ
โ Checklist
๊ตฌํ ์๋ฃ
ํ ์คํธ
TODO - ํฅํ ๊ฐ์ ์ฌํญ
๐ References
๊ด๋ จ ๋ฌธ์
์ฃผ์ ๋ณ๊ฒฝ ํ์ผ
apps/commerce-api/src/main/java/com/loopers/application/ranking/batch/RankingAggregationJobConfig.java: ํตํฉ Job ์ ์RankingStepConfig.java: ํตํฉ Step ์ ์ProductMetricsDailyReaderFactory.java: Reader (๋ ์ง ๋ฒ์ ์๋ ๊ณ์ฐ)RankingProcessor.java: Processor (TOP 100 ํํฐ๋ง)RankingStepExecutionListener.java: Step ๋ฆฌ์ค๋ (TOP 100 ์ถ์ถ, JSON ๋ณํ)RankingJobExecutionListener.java: Job ๋ฆฌ์ค๋ (DB ์ ์ฅ)RankingJsonConverter.java: JSON ๋ณํ ์ ํธ๋ฆฌํฐapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingAggregationService.java: ๋ฐฐ์น ์คํ ์๋น์คRankingAggregationScheduler.java: ์ค์ผ์ค๋ฌapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingBatchV1Controller.java: ๋ฐฐ์น ์คํ APISummary by CodeRabbit
โ๏ธ Tip: You can customize this high-level summary in your review settings.