-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-10] Collect, Stack, Zip #245
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: sky980221
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์ ๊ธฐ๊ฐ ์ ํ ํ๋ผ๋ฏธํฐ๋ฅผ ๋์ ํฉ๋๋ค. ์๋ก์ด ๋ฐฐ์น ๋ชจ๋๊ณผ JPA ์ํฐํฐ๋ฅผ ์ถ๊ฐํ๊ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์ ์ ํฌํจํฉ๋๋ค. Changes
Sequence Diagram(s)sequenceDiagram
actor Scheduler as ProductRankingJobScheduler
participant JobLauncher as JobLauncher
participant Reader as RankingScoreReader
participant Processor as RankingScoreProcessor
participant Writer as RankingWriter
participant DB as Database
Scheduler->>JobLauncher: runWeeklyRanking()/runMonthlyRanking()
JobLauncher->>Reader: read()
Reader->>DB: SELECT SUM(like/view/order/sales) GROUP BY product_id
DB-->>Reader: ProductRankingAggregation list
Reader-->>Processor: aggregate(ProductRankingAggregation)
loop Process each aggregation
Processor->>Processor: calculateScore() + assign rank
Processor-->>Writer: WeeklyProductRank/MonthlyProductRank
end
Writer->>DB: DELETE existing period data
Writer->>DB: INSERT new rankings
DB-->>Writer: Success
Writer-->>JobLauncher: Completed
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~60 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touchesโ Failed checks (1 warning, 1 inconclusive)
โ 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: 16
โป๏ธ Duplicate comments (1)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (1)
72-80: ์ค๋ณต ์ฝ๋ ์ ๊ฑฐ ํ์์ด
readEvent๋ฉ์๋๋CatalogEventConsumer์ ๋์ผ ๋ฉ์๋์ ์์ ํ ์ค๋ณต๋ฉ๋๋ค.CatalogEventConsumer์ ๋ฆฌ๋ทฐ ์ฝ๋ฉํธ๋ฅผ ์ฐธ๊ณ ํ์ฌ ๊ณตํต ์ ํธ๋ฆฌํฐ๋ก ์ถ์ถํด์ฃผ์ธ์.
๐งน Nitpick comments (30)
settings.gradle.kts (1)
3-14: LGTM!์๋ก์ด
commerce-batch๋ชจ๋์ด ์ ์์ ์ผ๋ก ์ถ๊ฐ๋์์ต๋๋ค. ๋ค๋ง, ๋ชจ๋ ์์๊ฐappsโmodulesโsupportsํจํด๊ณผ ์ฝ๊ฐ ๋ค๋ฅด๊ฒ:modules:pg-simulator๋ค์ ๋ฐฐ์น๋์ด ์์ต๋๋ค. ๊ธฐ๋ฅ์๋ ์ํฅ์ด ์์ง๋ง, ๊ฐ๋ ์ฑ์ ์ํด ๋ค๋ฅธapps๋ชจ๋๋ค๊ณผ ํจ๊ป ๊ทธ๋ฃนํํ๋ ๊ฒ์ ๊ณ ๋ คํด ๋ณด์ธ์.apps/commerce-api/src/test/java/com/loopers/domain/like/LikeIntegrationTest.java (1)
123-136:awaitTrue๊ฐ ํ์์์ ์ ์คํจํ์ง ์์ผ๋ฉด ํ ์คํธ ์ ๋ขฐ๋๊ฐ ์ ํ๋ ์ ์์ต๋๋ค.ํ์ฌ ๊ตฌํ์ 3์ด ๋ด์ ์กฐ๊ฑด์ด ์ถฉ์กฑ๋์ง ์์๋ ์กฐ์ฉํ ๋ฐํ๋ฉ๋๋ค. ์ดํ
assertThat์์ ์คํจ๊ฐ ๊ฐ์ง๋๊ธด ํ์ง๋ง, ์ด ๊ฒฝ์ฐawaitTrue๊ฐ ๋จ์ ์ง์ฐ ์ญํ ๋ง ํ๊ฒ ๋ฉ๋๋ค. ํ์์์ ์ ๋ช ํํ ์คํจ๋ฅผ ๋์ง๋ ๊ฒ์ด ํ ์คํธ ์๋๋ฅผ ๋ ์ ํํํฉ๋๋ค.๐ ํ์์์ ์ AssertionError๋ฅผ ๋์ง๋๋ก ๊ฐ์
private static void awaitTrue(BooleanSupplier condition) { long deadline = System.currentTimeMillis() + 3000; // wait up to 3s while (System.currentTimeMillis() < deadline) { if (condition.getAsBoolean()) { return; } try { Thread.sleep(20); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - return; + throw new AssertionError("Interrupted while waiting for condition"); } } + throw new AssertionError("Condition not met within 3 seconds"); }๋๋ Awaitility ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ์ ๊ณ ๋ คํด ๋ณด์ธ์:
await().atMost(3, SECONDS).until(condition::getAsBoolean);apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
14-17:@RequiredArgsConstructor๊ฐ ๋ถํ์ํฉ๋๋ค.์ด ํด๋์ค์๋
finalํ๋๊ฐ ์์ด์@RequiredArgsConstructor๊ฐ ๋น ์์ฑ์๋ง ์์ฑํฉ๋๋ค. ์ ๊ฑฐํด๋ ๊ธฐ๋ฅ์ ์ํฅ์ด ์์ต๋๋ค.๐ ๋ถํ์ํ ์ด๋ ธํ ์ด์ ์ ๊ฑฐ
@Slf4j -@RequiredArgsConstructor @Component public class JobListener {apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventHandler.java (1)
38-39:save()ํธ์ถ์ ์๋ตํ ์ ์์ต๋๋ค.
@Transactional์ปจํ ์คํธ ๋ด์์ ์ํฐํฐ ์ํ ๋ณ๊ฒฝ์ JPA dirty checking์ ์ํด ์๋์ผ๋ก flush๋ฉ๋๋ค. ๊ธฐ์กด learnings์ ๋ฐ๋ฅด๋ฉด, ์ด ํ๋ก์ ํธ์์๋ ๋ช ์์ save()ํธ์ถ๋ณด๋ค dirty checking์ ํ์ฉํ๋ ํจํด์ ์ ํธํฉ๋๋ค.๐ ์ ์๋ ๋ณ๊ฒฝ
order.markAsConfirmed(); - orderRepository.save(order);Line 64์
orderRepository.save(order)๋ ๋์ผํ๊ฒ ์ ์ฉ๋ฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/like/LikeEventHandler.java (1)
30-31:save()ํธ์ถ ์๋ต ๊ฐ๋ฅ
PaymentEventHandler์ ๋์ผํ๊ฒ,@Transactional๋ด์์ dirty checking์ด ์ ์ฉ๋๋ฏ๋ก ๋ช ์์ save()ํธ์ถ์ ์ ํ ์ฌํญ์ ๋๋ค. Learnings์ ๋ฐ๋ฅธ ํ๋ก์ ํธ ํจํด๊ณผ ์ผ๊ด์ฑ์ ์ํด ์๋ต์ ๊ณ ๋ คํด ๋ณผ ์ ์์ต๋๋ค.๐ ์ ์๋ ๋ณ๊ฒฝ
.ifPresent(product -> { product.increaseLikeCount(); - productRepository.save(product); });Line 50์
decreaseLikeCount()ํsave()ํธ์ถ๋ ๋์ผํ๊ฒ ์ ์ฉ๋ฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (1)
17-19:periodType์ enum ํ์ ์ผ๋ก ๋ณ๊ฒฝ ๊ถ์ฅํ์ฌ
Stringํ์ ์ผ๋ก ๊ตฌํ๋์ด ์์ผ๋ฉฐ, ์๋ชป๋ ๊ฐ์ด ์ ๋ ฅ๋๋ฉด ์กฐ์ฉํ "DAILY" ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค. enum ํ์ ์ผ๋ก ๋ณ๊ฒฝํ๋ฉด Spring์์ ์๋์ผ๋ก ๋ฌธ์์ด์ ๋ณํํ๊ณ , ์๋ชป๋ ๊ฐ์ ๋ํด400 Bad Request๋ฅผ ๋ฐํํ์ฌ ๋ ๋ช ํํ ์๋ฌ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.enum ํ์ ์ฌ์ฉ ์ ์
public enum PeriodType { DAILY, WEEKLY, MONTHLY }-@Parameter(description = "Period Type (DAILY, WEEKLY, MONTHLY)", example = "DAILY") -@RequestParam(required = false) String periodType, +@Parameter(description = "Period Type", example = "DAILY") +@RequestParam(required = false) PeriodType periodType,์ปจํธ๋กค๋ฌ์ switch ๋ฌธ๋ String ๋น๊ต ๋์ enum ์์ ๋น๊ต๋ก ๋ณ๊ฒฝํ๋ฉด ํ์ ์์ ์ฑ์ด ํฅ์๋ฉ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
23-43: ์๋ฌ ์ฒ๋ฆฌ ๋ก์ง์ด ์ ์ ํ๋ฉฐ, ํฅํ ๊ฐ์ ๊ณํ์ด ๋ช ์๋์ด ์์ต๋๋ค.์์ธ ๋ฉ์์ง ์์ง ๋ฐ ๋ก๊น ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค. Line 39์ Slack ์ ์ก ๊ณํ์ ๋ํด Issue๋ก ํธ๋ํนํ๊ฑฐ๋ TODO ์ฃผ์์ ์ถ๊ฐํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
Line 39์ ๋ชจ๋ํฐ๋ง ์๋ฆผ ๊ธฐ๋ฅ ๊ตฌํ์ ์ํ ๋ณ๋ Issue๋ฅผ ์์ฑํ์๊ฒ ์ต๋๊น?
apps/commerce-api/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java (1)
37-38:totalScore์BigDecimal์ฌ์ฉ์ ๊ณ ๋ คํด ๋ณด์ธ์.
salesAmount๋BigDecimal์ ์ฌ์ฉํ๊ณ ์๋๋ฐ,totalScore๊ฐ ๊ธ์ก ๊ธฐ๋ฐ ๊ณ์ฐ์ ํฌํจํ๋ค๋ฉดDouble์ ๋ถ๋์์์ ์ ๋ฐ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์ ์ ๊ณ์ฐ ๋ก์ง์ ๋ฐ๋ผBigDecimal์ฌ์ฉ์ ๊ฒํ ํด ๋ณด์ธ์.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
45-59:periodType์ enum ์ฌ์ฉ๊ณผ ๋ช ์์ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ๊ณ ๋ คํด ๋ณด์ธ์.ํ์ฌ ์๋ชป๋
periodType๊ฐ์ด ๋ค์ด์ค๋ฉด ์กฐ์ฉํDAILY๋ก ํด๋ฐฑ๋ฉ๋๋ค. ์ด๋ ํด๋ผ์ด์ธํธ๊ฐ ์คํ๋ฅผ ๋ด๋ ์๋ฌ๋ฅผ ์ธ์งํ์ง ๋ชปํ๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.๋ํ, switch ์ผ์ด์ค์ ๋ฌธ๋ฒ์ด ์ผ๊ด๋์ง ์์ต๋๋ค (
DAILY๋ ์ค๊ดํธ ์์ด,WEEKLY/MONTHLY๋ ์ค๊ดํธ ์ฌ์ฉ).๐ enum ์ฌ์ฉ ๋ฐ ์ผ๊ด๋ switch ๋ฌธ๋ฒ ์ ์
- String period = (periodType == null || periodType.isBlank()) - ? "DAILY" - : periodType.trim().toUpperCase(); - - List<RankingProductInfo> rankings; - switch (period) { - case "DAILY" -> rankings = rankingFacade.getDailyRanking(targetDate, page, size); - case "WEEKLY" -> { - rankings = rankingFacade.getWeeklyRanking(targetDate, page, size); - } - case "MONTHLY" -> { - rankings = rankingFacade.getMonthlyRanking(targetDate, page, size); - } - default -> rankings = rankingFacade.getDailyRanking(targetDate, page, size); - } + String period = (periodType == null || periodType.isBlank()) + ? "DAILY" + : periodType.trim().toUpperCase(); + + List<RankingProductInfo> rankings = switch (period) { + case "DAILY" -> rankingFacade.getDailyRanking(targetDate, page, size); + case "WEEKLY" -> rankingFacade.getWeeklyRanking(targetDate, page, size); + case "MONTHLY" -> rankingFacade.getMonthlyRanking(targetDate, page, size); + default -> rankingFacade.getDailyRanking(targetDate, page, size); + };apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankingAggregation.java (1)
8-25: Java record ์ฌ์ฉ์ ๊ณ ๋ คํด ๋ณด์ธ์.๋ถ๋ณ ๋ฐ์ดํฐ ์ ์ก ๊ฐ์ฒด๋ก ์ฌ์ฉ๋๋ค๋ฉด
record๊ฐ ๋ ๊ฐ๊ฒฐํ๊ณ ์๋๋ฅผ ๋ช ํํ ํํํฉ๋๋ค. ๋ํ ํ๋ ์ฌ์ด์ ๋ถํ์ํ ๋น ์ค์ ์ ๊ฑฐํ๋ฉด ๊ฐ๋ ์ฑ์ด ํฅ์๋ฉ๋๋ค.๐ record ์ฌ์ฉ ์ ์
package com.loopers.domain.rank; import java.math.BigDecimal; public record ProductRankingAggregation( Long productId, Integer likeCount, Integer viewCount, Integer orderCount, BigDecimal salesAmount, Integer rankPosition ) {}apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/WeeklyRankingWriter.java (1)
11-11: ์ฌ์ฉํ์ง ์๋ import๋ฅผ ์ ๊ฑฐํ์ธ์.
java.util.List๊ฐ import๋์์ง๋ง ์ฌ์ฉ๋์ง ์์ต๋๋ค.๐ ์์ ์ ์
-import java.util.List;apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/ProductRankingJobScheduler.java (3)
4-4: ์ฌ์ฉํ์ง ์๋ import๋ฅผ ์ ๊ฑฐํ์ธ์.
java.util.Map์ด import๋์์ง๋ง ์ฌ์ฉ๋์ง ์์ต๋๋ค.๐ ์์ ์ ์
-import java.util.Map;
42-44: ์์ธ ๋ฐ์ ์ ๋ชจ๋ํฐ๋ง ๋ฐ ์๋ฆผ ์ ๋ต์ ํ์ธํ์ธ์.ํ์ฌ ์์ธ๊ฐ ๋ก๊น ๋ง ๋๊ณ ๋ค์ throw๋์ง ์์, ๋ชจ๋ํฐ๋ง ์์คํ ์์ Job ์คํจ๋ฅผ ๊ฐ์งํ์ง ๋ชปํ ์ ์์ต๋๋ค. ๋ค์์ ๊ณ ๋ คํด ๋ณด์ธ์:
- ์์ธ๋ฅผ ๋ค์ throwํ๊ฑฐ๋
- ๋ฉํธ๋ฆญ/์๋ฆผ ์์คํ ์ ์คํจ๋ฅผ ๋ณด๊ณ ํ๊ฑฐ๋
- ์ต์ํ Job์
ExitStatus๋ฅผ ํ์ธํ์ฌ ์คํจ ์ฌ๋ถ๋ฅผ ๋ก๊น
24-31:LocalDate.now()์ ํ์์กด์ ๋ช ์ํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.
LocalDate.now()๋ ์์คํ ๊ธฐ๋ณธ ํ์์กด์ ์ฌ์ฉํฉ๋๋ค. ์๋ฒ ํ๊ฒฝ์ ๋ฐ๋ผ ๋ค๋ฅธ ๋ ์ง๊ฐ ๊ณ์ฐ๋ ์ ์์ผ๋ฏ๋ก, ๋ช ์์ ์ธ ํ์์กด ์ง์ ์ ๊ถ์ฅํฉ๋๋ค.๐ ํ์์กด ๋ช ์ ์์
+import java.time.ZoneId; + +// ํด๋์ค ๋ด๋ถ์ ์์ ์ ์ +private static final ZoneId ZONE_ID = ZoneId.of("Asia/Seoul"); + @Scheduled(cron = "0 10 1 * * *") public void runWeeklyRanking() { - runWithAnchor(weeklyRankingJob, LocalDate.now().minusDays(1)); + runWithAnchor(weeklyRankingJob, LocalDate.now(ZONE_ID).minusDays(1)); } @Scheduled(cron = "0 20 1 * * *") public void runMonthlyRanking() { - runWithAnchor(monthlyRankingJob, LocalDate.now().minusDays(1)); + runWithAnchor(monthlyRankingJob, LocalDate.now(ZONE_ID).minusDays(1)); }apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (3)
4-4: ์ฌ์ฉ๋์ง ์๋@RequiredArgsConstructor์ด๋ ธํ ์ด์ ์ด ํ ์คํธ ํด๋์ค์๋
finalํ๋๊ฐ ์์ด@RequiredArgsConstructor๊ฐ ๋ถํ์ํฉ๋๋ค. ์ ๊ฑฐ๋ฅผ ๊ถ์ฅํฉ๋๋ค.
37-40: ๋น@BeforeEach๋ฉ์๋ ์ ๊ฑฐ ๊ถ์ฅ๋ด์ฉ์ด ์๋
beforeEach()๋ฉ์๋์ ๋๋ค. ํฅํ ์ฌ์ฉ ๊ณํ์ด ์๋ค๋ฉด ์ ๊ฑฐํ๋ ๊ฒ์ด ๊น๋ํฉ๋๋ค.
42-56: ํ ์คํธ ๋ฉ์๋๋ช ์ด ์ค์ ํ ์คํธ ๋ด์ฉ๊ณผ ๋ถ์ผ์น
shouldNotSaveCategories_whenApiError๋ API ์ค๋ฅ ์ํฉ์ ํ ์คํธํ๋ ๊ฒ์ฒ๋ผ ๋ณด์ด์ง๋ง, ์ค์ ๋ก๋requestDateํ๋ผ๋ฏธํฐ ๋๋ฝ ์ ์คํจ๋ฅผ ๊ฒ์ฆํฉ๋๋ค. ํ ์คํธ ์๋๋ฅผ ๋ช ํํ ๋ฐ์ํ๋๋ก ๋ฉ์๋๋ช ์์ ์ ๊ถ์ฅํฉ๋๋ค.๐ ๋ฉ์๋๋ช ์์ ์ ์
- void shouldNotSaveCategories_whenApiError() throws Exception { + void shouldFailJob_whenRequestDateIsMissing() throws Exception {apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (2)
16-17: ์ฌ์ฉ๋์ง ์๋@RequiredArgsConstructor์ด๋ ธํ ์ด์
finalํ๋๊ฐ ์์ด Lombok@RequiredArgsConstructor๊ฐ ์์ฑํ๋ ์์ฑ์๊ฐ ์์ต๋๋ค. ์ ๊ฑฐํ๊ฑฐ๋,requestDateํ๋๋ฅผ ์์ฑ์ ์ฃผ์ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ์ธ์.
27-29:System.out.println๋์ SLF4J ๋ก๊ฑฐ ์ฌ์ฉ ๊ถ์ฅ๋ฐ๋ชจ ์ฝ๋๋ผ ํ๋๋ผ๋
System.out.println๋ณด๋ค๋ SLF4J ๋ก๊ฑฐ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ผ๊ด๋ ๋ก๊น ํจํด์ ์ ์งํ๊ณ , ๋ก๊ทธ ๋ ๋ฒจ ์ ์ด ๋ฐ ์ด์ ํ๊ฒฝ์์์ ๊ฐ์์ฑ ํ๋ณด์ ์ ๋ฆฌํฉ๋๋ค.๐ ๋ก๊ฑฐ ์ฌ์ฉ ์์
+import lombok.extern.slf4j.Slf4j; + +@Slf4j @StepScope @ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME) -@RequiredArgsConstructor @Component public class DemoTasklet implements Tasklet { @Value("#{jobParameters['requestDate']}") private String requestDate; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { if (requestDate == null) { throw new RuntimeException("requestDate is null"); } - System.out.println("Demo Tasklet ์คํ (์คํ ์ผ์ : " + requestDate + ")"); + log.info("Demo Tasklet ์คํ (์คํ ์ผ์ : {})", requestDate); Thread.sleep(1000); - System.out.println("Demo Tasklet ์์ ์๋ฃ"); + log.info("Demo Tasklet ์์ ์๋ฃ"); return RepeatStatus.FINISHED; } }apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java (1)
40-47: Step ๋ฉ์๋๋ช ๊ณผ ์ค์ ์ฉ๋ ๋ถ์ผ์น๋ฉ์๋๋ช
categorySyncStep()์ด ์ค์ ๋ฐ๋ชจ ์์ ๊ณผ ๋ง์ง ์์ต๋๋ค.STEP_DEMO_SIMPLE_TASK_NAME๊ณผ ์ผ๊ด๋๊ฒdemoSimpleTaskStep()๋ฑ์ผ๋ก ๋ณ๊ฒฝํ๋ฉด ๊ฐ๋ ์ฑ์ด ํฅ์๋ฉ๋๋ค.๐ ๋ฉ์๋๋ช ์์ ์ ์
@JobScope @Bean(STEP_DEMO_SIMPLE_TASK_NAME) - public Step categorySyncStep() { + public Step demoSimpleTaskStep() { return new StepBuilder(STEP_DEMO_SIMPLE_TASK_NAME, jobRepository) .tasklet(demoTasklet, new ResourcelessTransactionManager()) .listener(stepMonitorListener) .build(); }
demoJob()๋ฉ์๋ ๋ด์์ ํธ์ถ๋ถ๋ ํจ๊ป ์์ ํ์ธ์:return new JobBuilder(JOB_NAME, jobRepository) .incrementer(new RunIdIncrementer()) - .start(categorySyncStep()) + .start(demoSimpleTaskStep()) .listener(jobListener) .build();apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/MonthlyRankingWriter.java (1)
1-2: ํจํค์ง ์ ์ธ ํ ๋น ์ค ๋๋ฝ์ฝ๋ ์คํ์ผ ์ผ๊ด์ฑ์ ์ํด ํจํค์ง ์ ์ธ๊ณผ import ๋ฌธ ์ฌ์ด์ ๋น ์ค์ ์ถ๊ฐํ์ธ์.
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/processor/RankingScoreProcessor.java (3)
12-13:Object๋ฐํ ํ์ ์ ํ์ ์์ ์ฑ์ ์ ํ์ํด
ItemProcessor<ProductRankingAggregation, Object>๋ฐํ ํ์ ์ดObject์ด๋ฏ๋ก ํธ์ถ๋ถ์์ ์บ์คํ ์ด ํ์ํฉ๋๋ค. ์ ๋ค๋ฆญ์ ํ์ฉํ๊ฑฐ๋WeeklyRankingProcessor,MonthlyRankingProcessor๋ก ๋ถ๋ฆฌํ๋ฉด ํ์ ์์ ์ฑ๊ณผ ๊ฐ๋ ์ฑ์ด ํฅ์๋ฉ๋๋ค.ํ์ฌ ๊ตฌ์กฐ๊ฐ ์๋๋ ๊ฒ์ด๋ผ๋ฉด, ์ต์ํ ๋ฉ์๋์ ๋ฐํ ํ์ ์ ๋ํ ๋ฌธ์ํ(
@return)๋ฅผ ์ถ๊ฐํ์ธ์.
24-26:LocalDate.parse()์์ธ ์ฒ๋ฆฌ ๋ถ์ฌ
anchorDate๊ฐ ์๋ชป๋ ํ์์ผ ๊ฒฝ์ฐDateTimeParseException์ด ๋ฐ์ํฉ๋๋ค. Job Parameter ๊ฒ์ฆ ๋จ๊ณ์์ ํ์์ด ๋ณด์ฅ๋๋ค๋ฉด ๊ด์ฐฎ์ง๋ง, ๊ทธ๋ ์ง ์๋ค๋ฉด ๋ช ์์ ๊ฒ์ฆ ๋๋ ์์ธ ์ฒ๋ฆฌ๋ฅผ ์ถ๊ฐํ์ธ์.๐ ๋ฐฉ์ด์ ์ฝ๋ ์์
@Override public Object process(ProductRankingAggregation item) { double score = calculateScore(item); - LocalDate periodStart = LocalDate.parse(anchorDate); + LocalDate periodStart; + try { + periodStart = LocalDate.parse(anchorDate); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("Invalid anchorDate format: " + anchorDate, e); + }
57-66: ์ ์ ๊ณ์ฐ ๊ฐ์ค์น ํ๋์ฝ๋ฉVIEW(0.1), LIKE(0.2), ORDER(0.6) ๊ฐ์ค์น๊ฐ ์ฝ๋์ ์ง์ ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. ํฅํ ๊ฐ์ค์น ์กฐ์ ์ด ํ์ํ ๊ฒฝ์ฐ ์ค์ ํ์ผ์ด๋ ์์๋ก ๋ถ๋ฆฌํ๋ฉด ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ฉ๋๋ค. ํ์ฌ ๋ฐ๋ชจ/MVP ๋จ๊ณ๋ผ๋ฉด ์ด๋๋ก ์งํํด๋ ๋ฌด๋ฐฉํฉ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/MonthlyRankingStepConfig.java (1)
50-51: FQN ๋์ import ์ฌ์ฉ ๊ถ์ฅ
@org.springframework.beans.factory.annotation.Value๊ฐ ์ธ ๊ณณ์์ FQN์ผ๋ก ์ฌ์ฉ๋๊ณ ์์ต๋๋ค. ๊ฐ๋ ์ฑ์ ์ํด import ๋ฌธ์ผ๋ก ์ ๋ฆฌํ์ธ์.๐ import ์ถ๊ฐ ๋ฐ ์์
+import org.springframework.beans.factory.annotation.Value; ... public ItemReader<ProductRankingAggregation> monthlyRankingReader( - @org.springframework.beans.factory.annotation.Value("#{jobParameters['anchorDate']}") + @Value("#{jobParameters['anchorDate']}") String anchorDate ) {Also applies to: 63-64, 75-76
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/reader/RankingScoreReader.java (1)
42-91: ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ๋ก๋ํ๋ ๋ฐฉ์์ ๋์ฉ๋ ๋ฐ์ดํฐ์์ ๋ฌธ์ ๊ฐ ๋ ์ ์์ต๋๋ค.
fetch()๋ฉ์๋๊ฐ ๋ชจ๋ ์ง๊ณ ๊ฒฐ๊ณผ๋ฅผ ํ ๋ฒ์ ๋ฉ๋ชจ๋ฆฌ๋ก ๋ก๋ํฉ๋๋ค. ์ํ ์๊ฐ ๋ง์์ง๋ฉด OOM(Out of Memory) ์ํ์ด ์์ต๋๋ค. Spring Batch์ cursor/paging ๊ธฐ๋ฐ reader ์ฌ์ฉ์ ๊ณ ๋ คํด ๋ณด์ธ์.๋ํ, Line 72์
calcScore()๋ก์ง์ดRankingScoreProcessor.calculateScore()์ ๋์ผํ๊ฒ ์ค๋ณต๋์ด ์์ต๋๋ค. ์ ์ ๊ณ์ฐ ๋ก์ง์ ํ ๊ณณ์์ ๊ด๋ฆฌํ๋ฉด ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ฉ๋๋ค.๐ ์ ์ ๊ณ์ฐ ๋ก์ง ํตํฉ ์ ์
์ ์ ๊ณ์ฐ ์ ํธ๋ฆฌํฐ ํด๋์ค๋ฅผ ๋ง๋ค์ด ์ค๋ณต์ ์ ๊ฑฐํ ์ ์์ต๋๋ค:
public final class RankingScoreCalculator { private static final double VIEW_WEIGHT = 0.1d; private static final double LIKE_WEIGHT = 0.2d; private static final double ORDER_WEIGHT = 0.6d; public static double calculate(int viewCount, int likeCount, int orderCount, BigDecimal salesAmount) { BigDecimal amount = salesAmount == null ? BigDecimal.ZERO : salesAmount; double orderBase = amount.signum() > 0 ? amount.doubleValue() : (double) orderCount; return (VIEW_WEIGHT * viewCount) + (LIKE_WEIGHT * likeCount) + (ORDER_WEIGHT * orderBase); } }apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/WeeklyRankingStepConfig.java (2)
6-6: ์ฌ์ฉํ์ง ์๋ import๊ฐ ์์ต๋๋ค.
MonthlyProductRank๊ฐ import๋์ด ์์ง๋ง ์ด ํด๋์ค์์ ์ฌ์ฉ๋์ง ์์ต๋๋ค.WeeklyProductRank๋ง ์ฌ์ฉ๋ฉ๋๋ค.๐ ์ ์๋ ์์
-import com.loopers.domain.rank.MonthlyProductRank;
65-72: ํ์ ์บ์คํ ๋ฐฉ์๋ณด๋ค๋ ์ ๋ค๋ฆญ ๊ธฐ๋ฐ ๊ตฌ์กฐ ๊ฒํ ๋ฅผ ๊ถ์ฅํฉ๋๋ค.
RankingScoreProcessor.process()๊ฐObject๋ฅผ ๋ฐํํ๋ฏ๋ก ํธ์ถ ์ธก์์(WeeklyProductRank)์บ์คํ ์ด ํ์ํฉ๋๋ค. ํ์ฌ ์ฝ๋์์๋ "WEEKLY"๋ฅผ ํ๋์ฝ๋ฉํ์ฌ ์์ ํ์ง๋ง, ๋ ๋์ ๋ฐฉ์์ผ๋ก๋RankingScoreProcessor๋ฅผ ์ ๋ค๋ฆญ์ผ๋ก ์ค๊ณํ๋ฉด ์บ์คํ ์์ด ํ์ ์์ ์ฑ์ ํ๋ณดํ ์ ์์ต๋๋ค. ์ฐธ๊ณ ๋ก ์ ํจํ์ง ์์periodType์ด ์ ๋ฌ๋๋ฉดClassCastException๋์IllegalArgumentException์ด ๋ฐ์ํ๋๋ก ์ด๋ฏธ ๊ฒ์ฆ ๋ก์ง์ด ๊ตฌํ๋์ด ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (2)
62-73: Daily ๋ญํน์์ AtomicInteger ์ฌ์ฉ์ด ๋ถํ์ํฉ๋๋ค.๋จ์ผ ์ค๋ ๋์์ ์์ฐจ์ ์ผ๋ก ์ฒ๋ฆฌ๋๋ฏ๋ก
AtomicInteger๋์ ์ผ๋ฐint๋ณ์๋ก ์ถฉ๋ถํฉ๋๋ค.AtomicInteger๋ ๋์์ฑ์ด ํ์ํ ๊ฒฝ์ฐ์ ์ฌ์ฉ๋ฉ๋๋ค.๐ ์ ์๋ ์์
int baseRank = (int) start + 1; - AtomicInteger rankCounter = new AtomicInteger(baseRank); + int rank = baseRank; for (ZSetOperations.TypedTuple<String> t : tuples) { String member = t.getValue(); if (member == null || member.isBlank()) continue; Long productId = Long.valueOf(member); Product product = productMap.get(productId); if (product == null) continue; - int rank = rankCounter.getAndIncrement(); Double score = t.getScore(); - result.add(toInfo(productId, product, rank, score)); + result.add(toInfo(productId, product, rank++, score)); }
21-24:AtomicIntegerimport๊ฐ ์ ๊ฑฐ ๊ฐ๋ฅํฉ๋๋ค.์ ๋ฆฌํฉํ ๋ง์ ์ ์ฉํ๋ฉด
AtomicIntegerimport๋ ๋ถํ์ํด์ง๋๋ค.
๐ Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (47)
apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.javaapps/commerce-api/src/main/java/com/loopers/application/like/LikeEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingProductInfo.javaapps/commerce-api/src/main/java/com/loopers/domain/rank/MonthlyProductRank.javaapps/commerce-api/src/main/java/com/loopers/domain/rank/WeeklyProductRank.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/rank/MonthlyRankJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/rank/WeeklyRankJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.javaapps/commerce-api/src/main/resources/application.ymlapps/commerce-api/src/test/java/com/loopers/domain/like/LikeIntegrationTest.javaapps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.javaapps/commerce-batch/build.gradle.ktsapps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.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/productRankingJob/MonthlyRankingJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/ProductRankingJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/ProductRankingJobScheduler.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/WeeklyRankingJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/MonthlyRankingStepConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/WeeklyRankingStepConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/processor/RankingScoreProcessor.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/reader/RankingScoreReader.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/MonthlyRankingWriter.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/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/domain/rank/MonthlyProductRank.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyRankRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankingAggregation.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyProductRank.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyRankRepository.javaapps/commerce-batch/src/main/resources/application.ymlapps/commerce-batch/src/main/resources/db/migration/V20260102__ranking_mv_ddl.sqlapps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.javaapps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.javaapps/commerce-streamer/src/main/resources/application.ymlmodules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.javamodules/jpa/src/main/resources/jpa.ymlsettings.gradle.ktssupports/monitoring/src/main/resources/monitoring.yml
๐ค Files with no reviewable changes (1)
- apps/commerce-api/src/main/resources/application.yml
๐งฐ Additional context used
๐ง Learnings (12)
๐ 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-api/src/main/java/com/loopers/domain/rank/WeeklyProductRank.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyProductRank.javaapps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyProductRank.java
๐ Learning: 2025-12-19T09:30:12.459Z
Learnt from: HongChangMo
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 198
File: apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventListener.java:0-0
Timestamp: 2025-12-19T09:30:12.459Z
Learning: In the loopers-spring-java-template repository's commerce-api module, when entities are managed within a transactional context (e.g., Transactional methods), prefer relying on JPA dirty checking for automatic persistence rather than explicit save() calls. Both Payment and Order entities in PaymentEventListener use this pattern, with state changes automatically flushed on transaction commit.
Applied to files:
modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.javaapps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/like/LikeEventHandler.javaapps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.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/java/com/loopers/batch/job/productRankingJob/ProductRankingJobConfig.javaapps/commerce-api/src/main/java/com/loopers/application/like/LikeEventHandler.javaapps/commerce-batch/src/main/resources/application.ymlapps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.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/interfaces/consumer/OrderEventConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.javaapps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java
๐ Learning: 2025-12-18T13:24:51.650Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:51.650Z
Learning: Adopt centralized error handling for Kafka consumers by using the EventInboxAspect to intercept methods annotated with InboxEvent. Ensure that service methods annotated with InboxEvent save failed EventInbox entries and log errors, avoiding duplicating error handling logic in individual consumers. Apply this pattern broadly to similar consumer/service layers within the commerce-streamer module.
Applied to files:
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.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/interfaces/consumer/OrderEventConsumer.javaapps/commerce-api/src/main/java/com/loopers/application/like/LikeEventHandler.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankingAggregation.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.javaapps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.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-batch/src/main/java/com/loopers/domain/rank/WeeklyRankRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/rank/WeeklyRankJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/rank/MonthlyRankJpaRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyRankRepository.java
๐ Learning: 2025-12-07T12:45:45.556Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 52
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/payment/Payment.kt:160-181
Timestamp: 2025-12-07T12:45:45.556Z
Learning: In the Payment.confirmPayment method in com.loopers.domain.payment.Payment, the when-branch order is intentionally designed with business logic checks before timeout: (1) check SUCCESS/FAILED status first, (2) check for null match (business failure), (3) check timeout only as last resort for PENDING transactions. If matched transaction is PENDING and not timed out, no branch matches and payment stays IN_PROGRESS for scheduler retry. This design prioritizes business outcome determination over time-based fallback.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventHandler.java
๐ Learning: 2025-11-30T15:44:28.336Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 48
File: apps/commerce-api/src/main/kotlin/com/loopers/application/order/OrderFacade.kt:80-83
Timestamp: 2025-11-30T15:44:28.336Z
Learning: User ghojeong prefers explicit state management over transaction rollback for handling payment failures in order creation flow. When payment fails after order creation, the order should be explicitly marked as cancelled and persisted, rather than relying on transaction rollback.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventHandler.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/ChunkListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
๐ Learning: 2025-12-07T15:47:18.277Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 52
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/payment/PgFeignConfig.kt:0-0
Timestamp: 2025-12-07T15:47:18.277Z
Learning: In Spring Cloud OpenFeign, configuration classes specified in FeignClient(configuration = ...) do NOT need the Configuration annotation. Spring Cloud OpenFeign creates a separate ApplicationContext for each named client and directly instantiates Bean methods from the configuration class. Adding Configuration would cause the class to be registered globally via ComponentScan, making the configuration apply to ALL Feign clients instead of just the specific client, which is usually undesirable.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
๐งฌ Code graph analysis (9)
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/MonthlyRankingWriter.java (1)
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/WeeklyRankingWriter.java (1)
RequiredArgsConstructor(14-42)
apps/commerce-api/src/main/java/com/loopers/domain/rank/MonthlyProductRank.java (1)
apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankingAggregation.java (1)
Getter(8-25)
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/WeeklyRankingWriter.java (1)
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/MonthlyRankingWriter.java (1)
RequiredArgsConstructor(12-40)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
Slf4j(9-21)apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
Slf4j(13-44)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java (1)
ConditionalOnProperty(19-48)
apps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java (1)
Entity(17-57)apps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyProductRank.java (1)
Entity(25-106)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
Slf4j(9-21)apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
Slf4j(14-53)modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt (1)
log(22-119)
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
SpringBootTest(23-76)
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/WeeklyRankingStepConfig.java (4)
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/processor/RankingScoreProcessor.java (1)
RankingScoreProcessor(12-70)apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/reader/RankingScoreReader.java (1)
RankingScoreReader(15-138)apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/WeeklyRankingWriter.java (1)
RequiredArgsConstructor(14-42)apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/MonthlyRankingStepConfig.java (1)
Configuration(23-80)
๐ Additional comments (34)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (1)
30-31: ๋ฐฐ์น ๋ฆฌ์ค๋ ์ค์ ํ์ธ ํ์
KafkaConfig.BATCH_LISTENER์์๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ ์๋์ด ์๋์ง ํ์ธํด์ฃผ์ธ์.CatalogEventConsumer์ ๋์ผ ์ค์ ๊ณผ ํจ๊ป ๊ฒ์ฆ์ด ํ์ํฉ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1)
30-31:KafkaConfig.BATCH_LISTENER์์๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ ์๋์ด ์์ต๋๋ค.
KafkaConfigํด๋์ค์์BATCH_LISTENER์์๊ฐpublic static final String BATCH_LISTENER = "BATCH_LISTENER_DEFAULT";๋ก ์ฌ๋ฐ๋ฅด๊ฒ ์ ์๋์ด ์์ผ๋ฉฐ, ํ์ฌ ์ฝ๋์์์ ์ฌ์ฉ๋ ์ ํํฉ๋๋ค.modules/jpa/src/main/resources/jpa.yml (1)
40-40: migration ํ์ผ์ด ์ฌ์ฉ๋์ง ์๋ orphaned ์ํ์ ๋๋ค.ํ๋ก์ ํธ์๋ Flyway๋ Liquibase ๊ฐ์ ๋ง์ด๊ทธ๋ ์ด์ ๋๊ตฌ๊ฐ ๊ตฌ์ฑ๋์ด ์์ง ์์ต๋๋ค.
generate-ddl: false๋ ๋จ์ํ Hibernate ์๋ DDL ์์ฑ์ ๋นํ์ฑํํ๋ ์ค์ ์ผ ๋ฟ์ ๋๋ค.๋ฐ๋ผ์ local ํ๊ฒฝ์์
ddl-auto: update๋ก ์ค์ ํ๋ ๊ฒ์ ์ ์ ํฉ๋๋ค. ๋ฐ์ดํฐ ์์ค ์์ด ๋ก์ปฌ ๊ฐ๋ฐ ์ค ์คํค๋ง ๋ณ๊ฒฝ์ ๋ฐ์ํ ์ ์์ผ๋ฉฐ, ์๋ก์ด JPA ์ํฐํฐ(WeeklyProductRank, MonthlyProductRank)๋ ์ฌ๋ฐ๋ฅด๊ฒ ์ ์๋์ด ์์ต๋๋ค.๋ค๋ง ๋ค์์ ํ์ธํด ์ฃผ์ธ์:
V20260102__ranking_mv_ddl.sql ๋ง์ด๊ทธ๋ ์ด์ ํ์ผ์ ์ฉ๋๊ฐ ๋ช ํํ์ง ์์ต๋๋ค. ์ด๋ค ๋๊ตฌ๋ก๋ ์ฒ๋ฆฌ๋์ง ์๋ orphaned ์ํ์ ๋๋ค. ์ด ํ์ผ์ ๋ชฉ์ ์ ๋ฌธ์ํํ๊ฑฐ๋, ํฅํ ๋ง์ด๊ทธ๋ ์ด์ ๋๊ตฌ(Flyway/Liquibase) ๋์ ์ ์ฐ๊ณ ๊ณํ์ ๊ฒํ ํ์๊ธฐ ๋ฐ๋๋๋ค.
์ํฐํฐ๋ค์ด
@Immutable๋ก ํ์๋์ด ์์ผ๋ฏ๋ก materialized view ํจํด์ ์ฌ์ฉ์ด ์๋๋ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค. ์ด ์ค์ ์ ์ ์ ํฉ๋๋ค.Likely an incorrect or invalid review comment.
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
6-10: LGTM!Spring ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ ๋ก๋ฉ์ ๊ฒ์ฆํ๋ ํ์ค ์ค๋ชจํฌ ํ ์คํธ์ ๋๋ค.
DemoJobE2ETest์ ๊ฐ์ ๋ค๋ฅธ ๋ฐฐ์น ํ ์คํธ๋ค๊ณผ ์ผ๊ด๋ ํจํด์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.apps/commerce-streamer/src/main/resources/application.yml (1)
44-48: LGTM!
local/testํ๋กํ์์ ํฌํธ ์ค์ ์ด ์ ์ ํฉ๋๋ค.server.port: 8082๋ก ๋ค๋ฅธ ๋ชจ๋๊ณผ์ ํฌํธ ์ถฉ๋์ ๋ฐฉ์งํ๊ณ ,management.server.port: 0์ผ๋ก ํ ์คํธ ํ๊ฒฝ์์ ์์์ ํฌํธ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ธ๋ฉ ๋ฌธ์ ๋ฅผ ์๋ฐฉํฉ๋๋ค.apps/commerce-api/src/test/java/com/loopers/domain/like/LikeIntegrationTest.java (1)
76-78: ๋น๋๊ธฐ ์ด๋ฒคํธ ์ฒ๋ฆฌ๋ฅผ ์ํ ํด๋ง ๋์ ์ด ์ ์ ํฉ๋๋ค.
@EnableAsyncํ์ฑํ๋ก ์ธํด ์ด๋ฒคํธ ์ฒ๋ฆฌ๊ฐ ๋น๋๊ธฐ๋ก ๋์ํ๋ฏ๋ก, ์กฐ๊ฑด ํด๋ง ๋ฐฉ์์ด ํ์ํฉ๋๋ค. ํ ์คํธ ์๋๊ฐ ๋ช ํํ๊ฒ ๋๋ฌ๋ฉ๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
25-52: LGTM!๋ฐฐ์น ์์ ์คํ ์๊ฐ ๊ณ์ฐ ๋ฐ ๋ก๊น ๋ก์ง์ด ์ ๊ตฌํ๋์ด ์์ต๋๋ค.
DurationAPI๋ฅผ ํ์ฉํ ์๊ฐ ํฌ๋งทํ ๊ณผ text block์ ์ฌ์ฉํ ๊ฐ๋ ์ฑ ๋์ ๋ก๊ทธ ๋ฉ์์ง๊ฐ ์ ์ ํฉ๋๋ค.apps/commerce-batch/build.gradle.kts (1)
1-21: LGTM!๋ฐฐ์น ๋ชจ๋์ ํ์ํ ์์กด์ฑ์ด ์ ์ ํ๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค. Spring Batch, QueryDSL (Jakarta ๋ถ๋ฅ์), ๊ทธ๋ฆฌ๊ณ ํ ์คํธ ํฝ์ค์ฒ ์ค์ ์ด Spring Boot 3.x ๊ธฐ๋ฐ ํ๋ก์ ํธ์ ๋ง๊ฒ ๊ตฌ์ฑ๋์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventHandler.java (1)
29-29: REQUIRES_NEW ํธ๋์ญ์ ์ ํ ์ค์ ์ด ์ ์ ํฉ๋๋ค.
@TransactionalEventListener(phase = AFTER_COMMIT)๊ณผ@Async๋ฅผ ํจ๊ป ์ฌ์ฉํ ๋,REQUIRES_NEW๋ฅผ ํตํด ์๋ก์ด ํธ๋์ญ์ ์ปจํ ์คํธ์์ ๋น๋๊ธฐ ์ฒ๋ฆฌ๊ฐ ์ด๋ฃจ์ด์ง๋๋ก ํ ๊ฒ์ ์ฌ๋ฐ๋ฅธ ์ ๊ทผ์ ๋๋ค. ์๋ณธ ํธ๋์ญ์ ์ด ์ด๋ฏธ ์ปค๋ฐ๋ ํ์ ์คํ๋๋ฏ๋ก, ์๋ก์ด ํธ๋์ญ์ ์ด ํ์ํฉ๋๋ค.Also applies to: 54-54
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (2)
16-20: ํ์์กด ์ค์ ๋ฐฉ์์ ๋์ํ์ง๋ง, ๋์์ ๊ณ ๋ คํด ๋ณผ ์ ์์ต๋๋ค.
TimeZone.setDefault()๋ JVM ์ ์ญ์ ์ํฅ์ ๋ฏธ์นฉ๋๋ค. Spring Boot์์๋spring.jackson.time-zone์ด๋application.yml์ ์ค์ ์ ํตํด ๋ ์ ์ธ์ ์ผ๋ก ๊ด๋ฆฌํ ์๋ ์์ต๋๋ค. ํ์ฌ ๋ฐฉ์๋ ๋์์๋ ๋ฌธ์ ์์ต๋๋ค.
22-25: ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์ข ๋ฃ ํจํด์ด ์ ์ ํฉ๋๋ค.
SpringApplication.exit()์ ์ฌ์ฉํ์ฌ ์ข ๋ฃ ์ฝ๋๋ฅผ ๋ฐํํ๋ ํจํด์ ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ํ์ค ๊ดํ์ ๋๋ค. ์ด๋ฅผ ํตํด ์ค์ผ์ค๋ฌ๋ ์ค์ผ์คํธ๋ ์ด์ ๋๊ตฌ๊ฐ ์์ ์ฑ๊ณต/์คํจ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ํ๋จํ ์ ์์ต๋๋ค.apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java (2)
33-34: ์ด๋ฒคํธ ๊ธฐ๋ฐ ํ ์คํธ๋ก์ ์ ํ์ด ์ ์ ํฉ๋๋ค.
ApplicationEventPublisher๋ชจํน์ ํตํด ์ด๋ฒคํธ ๋ฐํ์ ๊ฒ์ฆํ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝ๋ ๊ฒ์ event-driven ์ํคํ ์ฒ ์ ํ์ ๋ถํฉํฉ๋๋ค. ์ง์ ์ ์ธproductRepository.save()๊ฒ์ฆ ๋์ ์ด๋ฒคํธ ๋ฐํ ๊ฒ์ฆ์ผ๋ก ๊ด์ฌ์ฌ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๋ถ๋ฆฌ๋์์ต๋๋ค.
137-138: ์ด๋ฒคํธ ๋ฐํ ์์ ๊ฒ์ฆ์ด ๋ช ํํฉ๋๋ค.
LikeCreatedEvent์LikeDeletedEvent๊ฐ ๊ฐ๊ฐ ํ ๋ฒ์ฉ ๋ฐํ๋๋์ง ๊ฒ์ฆํ๊ณ ์์ต๋๋ค. ์์๊น์ง ๊ฒ์ฆ์ด ํ์ํ๋ค๋ฉดInOrder๋ฅผ ์ฌ์ฉํ ์ ์์ง๋ง, ํ์ฌ ํ ์คํธ ์๋์๋ ์ถฉ๋ถํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/like/LikeEventHandler.java (1)
25-26: REQUIRES_NEW ํธ๋์ญ์ ์ ํ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ ์ฉ๋์์ต๋๋ค.
@Async์@TransactionalEventListener(phase = AFTER_COMMIT)์กฐํฉ์์REQUIRES_NEW๋ฅผ ์ฌ์ฉํ์ฌ ์ ํธ๋์ญ์ ์์ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ฒ์ ์ ์ ํฉ๋๋ค.Also applies to: 44-45
apps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyProductRank.java (1)
75-94: ์ํฐํฐ ์ค๊ณ๊ฐ ์ ์ ํฉ๋๋ค.
@Builderํจํด, protected ๊ธฐ๋ณธ ์์ฑ์, ๊ทธ๋ฆฌ๊ณ@PrePersist/@PreUpdate๋ผ์ดํ์ฌ์ดํด ํ ์ ํตํ ํ์์คํฌํ ๊ด๋ฆฌ๊ฐ JPA ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.(product_id, period_start)์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด์ผ๋ก ์ค๋ณต ๋ฐฉ์ง๋ ์ ๋์ด ์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
14-15: ํ์ฌ ์ฝ๋๋ Spring Batch 5.x์ ์ ํฉํ๋ฏ๋ก ๋ณ๊ฒฝ ๋ถํ์ํ๋ก์ ํธ์์ ์ฌ์ฉ ์ค์ธ Spring Boot ๋ฒ์ ์ 3.4.4์ด๋ฉฐ, ์ด๋ Spring Batch 5.1.x๋ฅผ ํฌํจํฉ๋๋ค. ํ์ฌ ์ฝ๋์
ChunkContextํ๋ผ๋ฏธํฐ๋ Spring Batch 5.x์ ์ฌ๋ฐ๋ฅธ API์ด๋ฏ๋ก ์์ ์ด ํ์ํ์ง ์์ต๋๋ค. Spring Batch 6.x์ API ๋ณ๊ฒฝ(Chunk ์ฌ์ฉ)์ Spring Boot 4.x ์ด์์ ์ฌ์ฉํ ๋๋ง ์ ์ฉ๋ฉ๋๋ค.Likely an incorrect or invalid review comment.
apps/commerce-batch/src/main/java/com/loopers/domain/rank/MonthlyRankRepository.java (1)
12-22: LGTM!๋ ํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค๊ฐ Spring Data JPA ํจํด์ ์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.
@Modifying์ด๋ ธํ ์ด์ ์clearAutomatically์flushAutomatically์ค์ ์ด ์ ์ ํ๋ฉฐ, ์ญ์ ๋ ํ ์๋ฅผ ๋ฐํํ๋ ๊ฒ๋ ๊ฒ์ฆ์ ์ ์ฉํฉ๋๋ค. ํ์ต ๊ธฐ๋ก์ ๋ฐ๋ฅด๋ฉด, ๋๋ฉ์ธ ๋ ํฌ์งํ ๋ฆฌ์์Pageableํ์ ์ฌ์ฉ์ ํ์ฉ๋ ์ํคํ ์ฒ ๊ฒฐ์ ์ ๋๋ค.apps/commerce-batch/src/main/resources/db/migration/V20260102__ranking_mv_ddl.sql (1)
1-50: ์คํค๋ง ์ค๊ณ๊ฐ ์ ๋์ด ์์ต๋๋ค.์ธ๋ฑ์ค ์ ๋ต์ด ๋ญํน ์กฐํ ํจํด์ ์ ํฉํฉ๋๋ค:
(period_start, rank_position): ๊ธฐ๊ฐ๋ณ ์์ N๊ฐ ์กฐํ์ ์ต์ ํ(period_start, total_score): ์ ์ ๊ธฐ๋ฐ ์ ๋ ฌ ์ฟผ๋ฆฌ ์ง์- ์ ๋ํฌ ์ ์ฝ์กฐ๊ฑด์ผ๋ก ์ค๋ณต ๋ฐ์ดํฐ ๋ฐฉ์ง
product_id์ ๋ํ ์ธ๋ํค ์ ์ฝ์กฐ๊ฑด์ด ์๋๋ฐ, ์ด๋ materialized view ํจํด์์ ์ฑ๋ฅ๊ณผ ๋์ปคํ๋ง์ ์ํด ์๋์ ์ผ๋ก ์๋ต๋ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค. ๋ง์ฝ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ด ์ค์ํ๋ค๋ฉด ์ถํ FK ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํด ๋ณผ ์ ์์ต๋๋ค.supports/monitoring/src/main/resources/monitoring.yml (1)
33-34: ๊ธฐ๋ณธ ํฌํธ ๊ฐ 0 ์ฌ์ฉ ๊ฒ์ฆ ํ์ํ๊ฒฝ ๋ณ์๋ก ํฌํธ๋ฅผ ์ค์ ํ ์ ์๊ฒ ๋ณ๊ฒฝํ ๊ฒ์ ์ข์ง๋ง, ๊ธฐ๋ณธ๊ฐ
0(๋๋ค ํฌํธ)์ด ์ด์ ํ๊ฒฝ์์ ์๋ํ ๊ฒ์ธ์ง ํ์ธ์ด ํ์ํฉ๋๋ค.๋๋ค ํฌํธ ์ฌ์ฉ ์ ๊ณ ๋ ค์ฌํญ:
- ๋ก๋๋ฐธ๋ฐ์ ํฌ์ค์ฒดํฌ ์ค์
- Prometheus ์คํฌ๋ํ ํ๊ฒ ์ค์
- Kubernetes ์๋น์ค ๋์ค์ปค๋ฒ๋ฆฌ
๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง ์ฌ์ฉํ๊ฑฐ๋ ์๋น์ค ๋์ค์ปค๋ฒ๋ฆฌ๋ฅผ ํ์ฉํ๋ ๊ฒฝ์ฐ๋ผ๋ฉด ๋ฌธ์ ์์ต๋๋ค. ๊ทธ๋ ์ง ์๋ค๋ฉด ์์ธก ๊ฐ๋ฅํ ๊ธฐ๋ณธ๊ฐ์ ์ค์ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค:
port: ${MANAGEMENT_PORT:8081}apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingProductInfo.java (1)
5-13: LGTM!๋ ์ฝ๋ ๊ตฌ์กฐ๊ฐ ๋ญํน ์ ๋ณด๋ฅผ ๋ด๊ธฐ์ ์ ํฉํฉ๋๋ค.
rank๊ฐ primitiveint๋ก ์ ์๋์ด ์์ด null์ด ๋ถ๊ฐ๋ฅํ์ง๋ง, ๋ญํน ์ปจํ ์คํธ์์๋ ํญ์ ๊ฐ์ด ์กด์ฌํด์ผ ํ๋ฏ๋ก ์ ์ ํ ์ ํ์ ๋๋ค.apps/commerce-batch/src/main/resources/application.yml (2)
1-18: ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ๊ธฐ๋ณธ ์ค์ ์ด ์ ์ ํฉ๋๋ค.
web-application-type: none์ค์ ๊ณผjob.name: ${job.name:NONE}๊ธฐ๋ณธ๊ฐ์ ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํ๋ฉฐ, ์๋ํ์ง ์์ Job ์คํ์ ๋ฐฉ์งํฉ๋๋ค.
25-32: ๋ก์ปฌ/ํ ์คํธ ํ๊ฒฝ์ ์คํค๋ง ์๋ ์ด๊ธฐํ ์ค์ ์ด ์ ์ ํฉ๋๋ค.
initialize-schema: always์ค์ ์ ๊ฐ๋ฐ ๋ฐ ํ ์คํธ ํ๊ฒฝ์์ ๋ฐฐ์น ๋ฉํ๋ฐ์ดํฐ ํ ์ด๋ธ์ ์๋์ผ๋ก ์์ฑํ์ฌ ํธ์์ฑ์ ๋์ ๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
18-21: Step ์์ ๋ก๊น ์ด ์ ์ ํฉ๋๋ค.๊ฐ๋จํ๊ณ ๋ช ํํ ๋ก๊น ๊ตฌํ์ ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/WeeklyRankJpaRepository.java (1)
10-13: LGTM!Spring Data JPA ๋ช ๋ช ๊ท์น์ ์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.
Pageable์ ์ฌ์ฉํ๋ฉด์List๋ฅผ ๋ฐํํ๋ ๊ฒ์ ํ์ด์ง๋ค์ด์ ๋ ๊ฒฐ๊ณผ๋ง ํ์ํ๊ณ ์ ์ฒด ๊ฐ์๋ ํ์ ์์ ๋ ์ ์ ํ ์ ํ์ ๋๋ค.apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/MonthlyRankJpaRepository.java (1)
10-13: LGTM!
WeeklyRankJpaRepository์ ์ผ๊ด๋ ๊ตฌ์กฐ๋ก ์ ๊ตฌํ๋์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java (1)
52-56:createdAt/updatedAtํ์์คํฌํ ์๋ ์์ฑ ํ์ธ์ด ํ์ํฉ๋๋ค.
@CreationTimestamp๋@UpdateTimestamp์ด๋ ธํ ์ด์ ์ด ์์ต๋๋ค. Materialized view์ด๊ณ ๋ฐฐ์น์์ ์ง์ ๊ฐ์ ์ค์ ํ๋ค๋ฉด ๋ฌธ์ ์์ง๋ง, ๊ทธ๋ ์ง ์๋ค๋ฉด ์ด ํ๋๋ค์ด ํญ์ null์ด ๋ ์ ์์ต๋๋ค. ๋ฐฐ์น Writer์์ ์ด ๊ฐ๋ค์ ์ด๋ป๊ฒ ์ค์ ํ๋์ง ํ์ธํด ์ฃผ์ธ์.apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/WeeklyRankingWriter.java (1)
21-27: Job ์คํจ ์ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ํ์ธ์ด ํ์ํฉ๋๋ค.
open()์์ ์ ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๊ณwrite()์์ ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ํจํด์ ๋๋ค. Job์ด ์ญ์ ํ ์ฐ๊ธฐ ์๋ฃ ์ ์ ์คํจํ๋ฉด ํด๋น ๊ธฐ๊ฐ์ ๋ฐ์ดํฐ๊ฐ ์์ค๋ ์ ์์ต๋๋ค.
MonthlyRankingWriter๋ ๋์ผํ ํจํด์ ์ฌ์ฉํ๋ฏ๋ก ์๋๋ ์ค๊ณ์ผ ์ ์์ง๋ง, ํ๋ก๋์ ํ๊ฒฝ์์๋ ๋ค์์ ๊ณ ๋ คํด ๋ณด์ธ์:
- Job ์คํจ ์ ์ฌ์คํ์ผ๋ก ๋ณต๊ตฌ ๊ฐ๋ฅํ์ง ํ์ธ
- ๋๋ ์์ ํ ์ด๋ธ์ ์ด ํ swapํ๋ ๋ฐฉ์ ๊ฒํ
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/MonthlyRankingWriter.java (1)
19-25:open()๋จ๊ณ์์์ ์ญ์ ์์ - ์ฅ์ ์ ๋ฐ์ดํฐ ์ ํฉ์ฑ ํ์ธ ํ์
open()์์ ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๊ณwrite()์์ ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ํจํด์ ๋๋ค. ๋ฐฐ์น ์์ ์ด ์ค๊ฐ์ ์คํจํ๋ฉด ํด๋นperiodStart์ ๋ฐ์ดํฐ๊ฐ ์ ์ค๋ ์ ์์ต๋๋ค.์ด ํจํด์ด ์๋๋ ๊ฒ์ด๋ผ๋ฉด ๊ด์ฐฎ์ง๋ง, ์ด์ ํ๊ฒฝ์์๋ ์ฌ์ฒ๋ฆฌ ์ ๋ต(์: ์คํจ ์ ์ฌ์์, ๋ชจ๋ํฐ๋ง ์๋ฆผ)์ด ๊ฐ์ถฐ์ ธ ์๋์ง ํ์ธํ์ธ์.
WeeklyRankingWriter์ ๋์ผํ ๊ตฌ์กฐ๋ก ์ผ๊ด์ฑ ์๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyProductRank.java (2)
25-39: ์ํฐํฐ ๊ตฌ์กฐ ์ ์ ํจ - ์ฝ๊ธฐ/์ฐ๊ธฐ ๋ถ๋ฆฌ ํจํด ํ์ธ
commerce-batch๋ชจ๋์WeeklyProductRank๋ ์ฐ๊ธฐ์ฉ์ผ๋ก,commerce-api๋ชจ๋์ ๋์ผ ํ ์ด๋ธ ์ํฐํฐ๋@Immutable๋ก ์ฝ๊ธฐ ์ ์ฉ์ ๋๋ค. ๋ชจ๋ ๊ฐ ์ฑ ์ ๋ถ๋ฆฌ๊ฐ ์ ๋์ด ์์ต๋๋ค.์ ๋ํฌ ์ ์ฝ์กฐ๊ฑด๊ณผ ์ธ๋ฑ์ค ์ ์๋
MonthlyProductRank์ ์ผ๊ด๋๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
96-105:LocalDateTime.now()์ฌ์ฉ ์ ์๊ฐ๋(Timezone) ๊ณ ๋ ค
@PrePersist์@PreUpdate์์LocalDateTime.now()๋ฅผ ์ฌ์ฉํฉ๋๋ค. ์๋ฒ ์๊ฐ๋์ ๋ฐ๋ผ ๊ฐ์ด ๋ฌ๋ผ์ง ์ ์์ผ๋ฏ๋ก, ์ด์ ํ๊ฒฝ์์ ์๊ฐ๋ ์ผ๊ด์ฑ์ด ๋ณด์ฅ๋๋์ง ํ์ธํ์ธ์. UTC ๊ธฐ์ค์ด ํ์ํ๋ค๋ฉดLocalDateTime.now(ZoneOffset.UTC)์ฌ์ฉ์ ๊ณ ๋ คํ ์ ์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/MonthlyRankingStepConfig.java (1)
28-28: ์ฌ์ฉ๋์ง ์๋TOP_N์์
TOP_N = 100์ด ์ ์๋์ด ์์ง๋ง ์ฝ๋์์ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ์๋๋ ๊ธฐ๋ฅ์ด ๋๋ฝ๋ ๊ฒ์ธ์ง, ์๋๋ฉด ๋ถํ์ํ ์์์ธ์ง ํ์ธํ์ธ์.apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/reader/RankingScoreReader.java (1)
43-45: ์๋์ฐ ๊ณ์ฐ ๋ก์ง์ ๋ํ ํ์ธ์ด ํ์ํฉ๋๋ค.
window = 6(WEEKLY) ๋๋29(MONTHLY)๋ก ์ค์ ํ์ฌstartDate = endDate.minusDays(window)๋ฅผ ๊ณ์ฐํฉ๋๋ค. ์ด๋ anchor date ํฌํจ ์ ๊ฐ๊ฐ 7์ผ, 30์ผ์ด ๋ฉ๋๋ค.SQL์
BETWEEN :start AND :end๋ ์ ๋์ ํฌํจํ๋ฏ๋ก:
- WEEKLY: anchorDate - 6์ผ ~ anchorDate = 7์ผ๊ฐ
- MONTHLY: anchorDate - 29์ผ ~ anchorDate = 30์ผ๊ฐ
์ด๊ฒ์ด ์๋ํ ๋์์ธ์ง ํ์ธํด ์ฃผ์ธ์. ์ผ๋ฐ์ ์ผ๋ก "์ฃผ๊ฐ"์ 7์ผ, "์๊ฐ"์ 30์ผ์ ์๋ฏธํ๋ฏ๋ก ๋ง์ ๋ณด์ด์ง๋ง, ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ๊ณผ ์ผ์นํ๋์ง ๊ฒ์ฆ์ด ํ์ํฉ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/WeeklyRankingStepConfig.java (1)
36-51: LGTM - Step ๊ตฌ์ฑ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๋์ด ์์ต๋๋ค.
@JobScope์@StepScope์ด๋ ธํ ์ด์ ์ด ์ ์ ํ๊ฒ ์ฌ์ฉ๋์๊ณ , chunk ๊ธฐ๋ฐ ์ฒ๋ฆฌ ํ๋ฆ์ดMonthlyRankingStepConfig์ ์ผ๊ด๋ ํจํด์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค. null ํ๋ผ๋ฏธํฐ ํจํด์ Spring Batch์์ SpEL์ ํตํ job parameter ์ฃผ์ ์ ์ํ ํ์ค ๋ฐฉ์์ ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
123-154: LGTM - ์ ๋ค๋ฆญ ํฌํผ ๋ฉ์๋๊ฐ ์ ์ค๊ณ๋์์ต๋๋ค.
buildRanking๋ฉ์๋๊ฐ ํจ์ํ ์ธํฐํ์ด์ค๋ฅผ ํ์ฉํ์ฌ weekly/monthly ๋ญํน ๋ก์ง์ ์ค๋ณต์ ํจ๊ณผ์ ์ผ๋ก ์ ๊ฑฐํ์ต๋๋ค. ์ํ ID ์ผ๊ด ์กฐํ๋ก N+1 ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ ์ ๋ ์ข์ต๋๋ค.
| @Transactional(readOnly = true) | ||
| public List<RankingProductInfo> getWeeklyRanking(String weekStartYyyymmdd, int page, int size) { | ||
| int p = Math.max(1, page); | ||
| int s = Math.max(1, size); | ||
| LocalDate periodStart = LocalDate.parse(weekStartYyyymmdd, DATE_FORMATTER); | ||
|
|
||
| var rows = weeklyRankJpaRepository.findByPeriodStartOrderByRankPositionAsc( | ||
| periodStart, PageRequest.of(p - 1, s) | ||
| ); | ||
| return buildRanking( | ||
| rows, | ||
| r -> r.getProductId(), | ||
| r -> r.getRankPosition() != null ? r.getRankPosition() : 0, | ||
| r -> r.getTotalScore() | ||
| ); | ||
| } |
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.
๋ ์ง ํ์ฑ ์ ์์ธ ์ฒ๋ฆฌ๊ฐ ํ์ํฉ๋๋ค.
LocalDate.parse(weekStartYyyymmdd, DATE_FORMATTER)๋ ์๋ชป๋ ํ์์ ์
๋ ฅ์ ๋ํด DateTimeParseException์ ๋ฐ์์ํต๋๋ค. API ๋ ์ด์ด์์ ์ ์ ํ ์์ธ ์ฒ๋ฆฌ ๋๋ ์
๋ ฅ ๊ฒ์ฆ์ด ํ์ํฉ๋๋ค.
๐ ์์ธ ์ฒ๋ฆฌ ์ถ๊ฐ ์ ์
Facade ๋ ์ด์ด์์ ์ฒ๋ฆฌํ๊ฑฐ๋, Controller ๋ ์ด์ด์์ @DateTimeFormat ๋ฑ์ ํ์ฉํ ์ ์์ต๋๋ค:
public List<RankingProductInfo> getWeeklyRanking(String weekStartYyyymmdd, int page, int size) {
int p = Math.max(1, page);
int s = Math.max(1, size);
- LocalDate periodStart = LocalDate.parse(weekStartYyyymmdd, DATE_FORMATTER);
+ LocalDate periodStart;
+ try {
+ periodStart = LocalDate.parse(weekStartYyyymmdd, DATE_FORMATTER);
+ } catch (DateTimeParseException e) {
+ throw new IllegalArgumentException("Invalid date format. Expected yyyyMMdd", e);
+ }
...
}๐ 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.
| @Transactional(readOnly = true) | |
| public List<RankingProductInfo> getWeeklyRanking(String weekStartYyyymmdd, int page, int size) { | |
| int p = Math.max(1, page); | |
| int s = Math.max(1, size); | |
| LocalDate periodStart = LocalDate.parse(weekStartYyyymmdd, DATE_FORMATTER); | |
| var rows = weeklyRankJpaRepository.findByPeriodStartOrderByRankPositionAsc( | |
| periodStart, PageRequest.of(p - 1, s) | |
| ); | |
| return buildRanking( | |
| rows, | |
| r -> r.getProductId(), | |
| r -> r.getRankPosition() != null ? r.getRankPosition() : 0, | |
| r -> r.getTotalScore() | |
| ); | |
| } | |
| @Transactional(readOnly = true) | |
| public List<RankingProductInfo> getWeeklyRanking(String weekStartYyyymmdd, int page, int size) { | |
| int p = Math.max(1, page); | |
| int s = Math.max(1, size); | |
| LocalDate periodStart; | |
| try { | |
| periodStart = LocalDate.parse(weekStartYyyymmdd, DATE_FORMATTER); | |
| } catch (DateTimeParseException e) { | |
| throw new IllegalArgumentException("Invalid date format. Expected yyyyMMdd", e); | |
| } | |
| var rows = weeklyRankJpaRepository.findByPeriodStartOrderByRankPositionAsc( | |
| periodStart, PageRequest.of(p - 1, s) | |
| ); | |
| return buildRanking( | |
| rows, | |
| r -> r.getProductId(), | |
| r -> r.getRankPosition() != null ? r.getRankPosition() : 0, | |
| r -> r.getTotalScore() | |
| ); | |
| } |
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
around lines 77-92, LocalDate.parse(...) can throw DateTimeParseException for
invalid input; wrap the parse call in a try-catch that catches
DateTimeParseException, validate the input format if desired, and rethrow a
clear runtime exception (e.g., IllegalArgumentException or a custom
BadRequest/InvalidRequestException) with a descriptive message that includes the
offending weekStartYyyymmdd value so the controller can map it to a 400
response.
| @EnableAsync | ||
| public class CommerceApiApplication { |
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 custom async executor configuration
rg -n --type=java -C3 'AsyncConfigurer|TaskExecutor|@Bean.*Executor' --max-count=50Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 6652
๐ Script executed:
#!/bin/bash
# Search for @Async method usage in the codebase
rg -n --type=java '@Async' --max-count=50Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1692
๐ Script executed:
#!/bin/bash
# Search for configurations or config classes
fd -e java | xargs rg -l 'Configuration|Config' | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1816
๐ Script executed:
#!/bin/bash
# Check if executor bean is defined anywhere
rg -n --type=java 'getAsyncExecutor|setAsyncExecutor|@Bean.*executor' -i --max-count=50Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
๐ Script executed:
#!/bin/bash
# Search for eventTaskExecutor bean definition
rg -n --type=java 'eventTaskExecutor' --max-count=50Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 985
๐ Script executed:
#!/bin/bash
# Search for configuration classes that might define the executor
rg -n --type=java 'AsyncConfigurer|@EnableAsync|TaskExecutor' -B5 -A10 --max-count=100Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 12161
๐ Script executed:
#!/bin/bash
# Look in config directories for AsyncConfiguration
fd -e java -path '*/config/*' | xargs rg -l 'AsyncConfigurer\|TaskExecutor' 2>/dev/nullRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 258
๐ Script executed:
#!/bin/bash
# Search the entire apps/commerce-api for eventTaskExecutor definition
find apps/commerce-api -name "*.java" -type f | xargs grep -l "eventTaskExecutor" | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 364
๐ Script executed:
#!/bin/bash
# Search for @Bean definitions with executor
rg -n --type=java '@Bean' -A5 | grep -i 'executor\|task' | head -30Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 524
๐ Script executed:
#!/bin/bash
# List Java files in commerce-api config directories
find apps/commerce-api -type d -name config -o -name config -type d 2>/dev/null | xargs find -name "*.java" 2>/dev/nullRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 17299
๐ Script executed:
#!/bin/bash
# Search for AsyncConfigurer implementation
rg -n --type=java 'implements AsyncConfigurer' -B3 -A20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
๐ Script executed:
#!/bin/bash
# Search entire repository for eventTaskExecutor definition
rg -n 'eventTaskExecutor' --type=javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 985
๐ Script executed:
#!/bin/bash
# Search for @Bean with getAsyncExecutor or custom executor methods
rg -n '@Bean' -A3 --type=java | grep -B3 -i 'executor\|async' | head -50Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
๐ Script executed:
#!/bin/bash
# Check if there's a configuration class defining TaskExecutor bean
fd -name "*.java" -type f | xargs rg -l 'TaskExecutor|ThreadPoolTaskExecutor' | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 612
๐ Script executed:
#!/bin/bash
# Search properties and yml files for executor configuration
find apps/commerce-api -name "*.yml" -o -name "*.yaml" -o -name "*.properties" | xargs grep -i 'executor\|async' 2>/dev/nullRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 72
๐ Script executed:
#!/bin/bash
# Broader search for any configuration with @Bean that might define eventTaskExecutor
rg -n 'eventTaskExecutor|@Bean.*Executor|ThreadPoolTaskExecutor' --type=java --max-count=20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 985
๐ Script executed:
#!/antml:bin/bash
# Check if there are any config files in the entire project for async
find . -name "*Async*Config*.java" -o -name "*Executor*Config*.java" 2>/dev/null | head -10Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
eventTaskExecutor ๋น์ด ์ ์๋์ง ์์์ต๋๋ค. ๋ฐํ์ ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค.
@Async("eventTaskExecutor")๋ฅผ ์ฌ์ฉํ๋ 7๊ฐ ๋ฉ์๋(ProductEventHandler, PaymentEventHandler, DataPlatformEventHandler)์์ ์ฐธ์กฐํ๋ eventTaskExecutor ๋น์ด ์ ์๋์ด ์์ง ์์ต๋๋ค. ์ด๋ก ์ธํด ๋ฐํ์์ NoSuchBeanDefinitionException์ด ๋ฐ์ํฉ๋๋ค.
๋ค์ ์ค ํ๋๋ฅผ ์ํํ์ธ์:
eventTaskExecutor๋น์ ์ ์ํ๋ ์ค์ ํด๋์ค๋ฅผ ์์ฑํ์ฌThreadPoolTaskExecutor๋ก ๊ตฌ์ฑ- ๋๋
@Async์ฃผ์์์ ๋ช ์์ executor ์ด๋ฆ์ ์ ๊ฑฐํ๊ณ ๊ธฐ๋ณธ executor ์ค์ ์ถ๊ฐ
๐ค Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
around lines 17-18, several methods use @Async("eventTaskExecutor") but no bean
named eventTaskExecutor is defined, causing NoSuchBeanDefinitionException at
runtime; fix by either (A) adding a configuration class that defines a
ThreadPoolTaskExecutor bean named "eventTaskExecutor" (configure corePoolSize,
maxPoolSize, queueCapacity, setThreadNamePrefix, call initialize(), and return
the executor) or (B) remove the explicit executor name from @Async annotations
and add a default async executor bean (e.g., a ThreadPoolTaskExecutor bean named
"taskExecutor") or enable the default by providing a @Bean of type
AsyncConfigurer/TaskExecutor so @Async uses it.
apps/commerce-api/src/main/java/com/loopers/domain/rank/MonthlyProductRankView.java
Show resolved
Hide resolved
| @Bean | ||
| public Job monthlyRankingJob( | ||
| JobRepository jobRepository, | ||
| @Qualifier("monthlyRankingStep") Step monthlyRankingStep | ||
| ) { | ||
| return new JobBuilder("monthlyRankingJob", jobRepository) | ||
| .start(monthlyRankingStep) | ||
| .build(); | ||
| } |
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.
์ค๋ณต๋ Bean ์ ์๋ก ์ธํ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์คํจ.
monthlyRankingJob Bean์ด ์ด ํด๋์ค์ ProductRankingJobConfig.java (Lines 27-31)์ ๋ชจ๋ ์ ์๋์ด ์์ต๋๋ค. Spring ์ปจํ
์คํธ ์ด๊ธฐํ ์ Bean ์ด๋ฆ ์ถฉ๋์ด ๋ฐ์ํฉ๋๋ค.
WeeklyRankingJobConfig์ ํจ๊ป ์ด ํด๋์ค๋ฅผ ์ ๊ฑฐํ๊ณ , ProductRankingJobConfig์์ ํตํฉ ๊ด๋ฆฌํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/MonthlyRankingJobConfig.java
around lines 14โ22, there is a duplicate monthlyRankingJob Bean also defined in
ProductRankingJobConfig (lines 27โ31) causing a Spring bean name collision;
remove the duplicate definition by deleting the monthlyRankingJob @Bean method
(or remove the entire MonthlyRankingJobConfig class if it contains no other
unique beans), and ensure monthlyRankingStep is wired/qualified from
ProductRankingJobConfig instead so the single monthlyRankingJob Bean remains
defined only in ProductRankingJobConfig; also clean up any now-unused imports
and references.
...rce-batch/src/main/java/com/loopers/batch/job/productRankingJob/ProductRankingJobConfig.java
Show resolved
Hide resolved
| @Modifying(clearAutomatically = true, flushAutomatically = true) | ||
| @Query("DELETE FROM WeeklyProductRank w WHERE w.periodStart = :periodStart") | ||
| int deleteByPeriodStart(@Param("periodStart") LocalDate periodStart); |
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
# deleteByPeriodStart ํธ์ถ๋ถ ๊ฒ์
rg -n "deleteByPeriodStart" --type=java -B5 -A2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 4091
๐ Script executed:
#!/bin/bash
# WeeklyRankingWriter์ MonthlyRankingWriter ํ์ผ ์ ์ฒด ์กฐํ
echo "=== WeeklyRankingWriter.java ==="
cat -n apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/WeeklyRankingWriter.java
echo -e "\n=== MonthlyRankingWriter.java ==="
cat -n apps/commerce-batch/src/main/java/com/loopers/batch/job/productRankingJob/step/writer/MonthlyRankingWriter.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2975
ํธ์ถ ์ง์ ์ @Transactional ๋ช
์์ ์ ์ฉ ํ์
deleteByPeriodStart ๋ฉ์๋๊ฐ ํธ์ถ๋๋ WeeklyRankingWriter์ MonthlyRankingWriter ํด๋์ค์ @Transactional ์ด๋
ธํ
์ด์
์ด ์์ต๋๋ค. @Modifying์ผ๋ก ์ ์ธ๋ ๋ฒํฌ ์ญ์ ์์
์ ๋ฐ๋์ ํธ๋์ญ์
์ปจํ
์คํธ ๋ด์์ ์คํ๋์ด์ผ ์์์ฑ ์ปจํ
์คํธ๊ฐ ์ ๋๋ก ๊ด๋ฆฌ๋ฉ๋๋ค. ๋ writer ํด๋์ค์ open() ๋ฉ์๋ ๋๋ ํด๋์ค ๋ ๋ฒจ์ @Transactional์ ์ถ๊ฐํ์ธ์.
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/domain/rank/WeeklyRankRepository.java
around lines 19-21 the method deleteByPeriodStart is annotated with @Modifying
and therefore must be invoked within a transactional context; update the caller
classes WeeklyRankingWriter and MonthlyRankingWriter by adding @Transactional
(either at the class level or on their open() methods) so the bulk delete runs
inside a transaction, import
org.springframework.transaction.annotation.Transactional, ensure the transaction
is not readOnly, and confirm the open() methods remain public so Spring can
proxy and apply the transaction.
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java
Outdated
Show resolved
Hide resolved
| @SuppressWarnings("unchecked") | ||
| private Map<String, Object> readEvent(String raw) throws Exception { | ||
| if (raw == null) return null; | ||
| String s = raw.trim(); | ||
| if (s.startsWith("\"") && s.endsWith("\"")) { | ||
| s = objectMapper.readValue(s, String.class); | ||
| } | ||
| return objectMapper.readValue(s, Map.class); | ||
| } |
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
์ค๋ณต ์ฝ๋ ์ ๊ฑฐ๋ฅผ ์ํ ๊ณตํต ์ ํธ๋ฆฌํฐ ์ถ์ถ ํ์
readEvent ๋ฉ์๋๊ฐ OrderEventConsumer์๋ ๋์ผํ๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค. DRY ์์น์ ๋ฐ๋ผ ๊ณตํต ์ ํธ๋ฆฌํฐ ํด๋์ค๋ก ์ถ์ถํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
๐ ๋ฆฌํฉํ ๋ง ์ ์
๊ณตํต ์ ํธ๋ฆฌํฐ ํด๋์ค๋ฅผ ์์ฑํ์ธ์ (์: EventParsingUtils.java):
package com.loopers.infrastructure.kafka;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
public class EventParsingUtils {
@SuppressWarnings("unchecked")
public static Map<String, Object> parseEvent(ObjectMapper objectMapper, String raw) throws Exception {
if (raw == null) return null;
String s = raw.trim();
if (s.startsWith("\"") && s.endsWith("\"")) {
s = objectMapper.readValue(s, String.class);
}
return objectMapper.readValue(s, Map.class);
}
}๊ทธ ๋ค์ ๋ Consumer์์ ์ฌ์ฉ:
- @SuppressWarnings("unchecked")
- private Map<String, Object> readEvent(String raw) throws Exception {
- if (raw == null) return null;
- String s = raw.trim();
- if (s.startsWith("\"") && s.endsWith("\"")) {
- s = objectMapper.readValue(s, String.class);
- }
- return objectMapper.readValue(s, Map.class);
- }
+ private Map<String, Object> readEvent(String raw) throws Exception {
+ return EventParsingUtils.parseEvent(objectMapper, raw);
+ }Committable suggestion skipped: line range outside the PR's diff.
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java
Outdated
Show resolved
Hide resolved
| @EnableTransactionManagement | ||
| @EntityScan({"com.loopers"}) | ||
| @EnableJpaRepositories({"com.loopers.infrastructure"}) | ||
| @EnableJpaRepositories({"com.loopers"}) |
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
# Find all JpaRepository implementations and their locations
echo "=== ๋ชจ๋ JpaRepository ํ์ฅ ์ธํฐํ์ด์ค ==="
find . -name "*.java" -type f | xargs grep -l "extends.*JpaRepository" | head -20
echo ""
echo "=== ํจํค์ง๋ณ JpaRepository ์์น ๋ถ์ ==="
find . -name "*.java" -type f | xargs grep -B5 "extends.*JpaRepository" | grep -E "^./|package|interface|extends.*JpaRepository" | head -100Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 13167
๐ Script executed:
#!/bin/bash
# Count repositories by package location
echo "=== infrastructure ๋ด๋ถ์ ๋ ํฌ์งํ ๋ฆฌ ==="
find . -path "*infrastructure*" -name "*.java" -type f | xargs grep -l "extends.*JpaRepository" 2>/dev/null | wc -l
echo ""
echo "=== infrastructure ์ธ๋ถ์ ๋ ํฌ์งํ ๋ฆฌ ==="
find . -name "*.java" -type f ! -path "*infrastructure*" | xargs grep -l "extends.*JpaRepository" 2>/dev/null
echo ""
echo "=== domain ํจํค์ง ๋ด ๋ ํฌ์งํ ๋ฆฌ ==="
find . -path "*domain*" -name "*.java" -type f | xargs grep -l "extends.*JpaRepository" 2>/dev/nullRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 723
๐ Script executed:
#!/bin/bash
# Check current JpaConfig.java and related configurations
echo "=== JpaConfig.java ํ์ธ ==="
cat -n modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java 2>/dev/null || find . -name "JpaConfig.java" -type f
echo ""
echo "=== EntityScan ์ค์ ํ์ธ ==="
find . -name "*.java" -type f | xargs grep -n "EntityScan\|EnableJpaRepositories" 2>/dev/null | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1171
@EnableJpaRepositories ์ค์บ ๋ฒ์๊ฐ ๊ณผ๋ํ๊ฒ ๋์ด์ง
com.loopers.infrastructure์์ com.loopers๋ก ๋ณ๊ฒฝํ๋ฉด ๋ชจ๋ ๋ชจ๋์ JpaRepository๊ฐ ์ค์บ๋ฉ๋๋ค. ํ์ฌ ๋ค์ ๋๋ฉ์ธ ๋ ํฌ์งํ ๋ฆฌ๋ค์ด ์๋์น ์๊ฒ ํฌํจ๋ฉ๋๋ค:
commerce-batch:MonthlyRankRepository,WeeklyRankRepository(com.loopers.domain.rank)commerce-streamer:ProductMetricsRepository(com.loopers.domain.metrics)
์ด๋ก ์ธํด:
- ๋ชจ๋ ๊ฐ ์์กด์ฑ์ด ์ฆ๊ฐ
- ๋๋ฉ์ธ ๋ ํฌ์งํ ๋ฆฌ๊ฐ infrastructure ์ค์ ์ ์ํด ์๋ ๊ด๋ฆฌ๋จ
- ์ํคํ ์ฒ ๊ฒฝ๊ณ๊ฐ ๋ชจํธํด์ง
ํจํค์ง๋ฅผ ๋ช
์์ ์ผ๋ก ์ง์ ํ๊ฑฐ๋ ์ ๋ ํฌ์งํ ๋ฆฌ๋ฅผ infrastructure ํจํค์ง๋ก ์ด๋ํ์ธ์:
๋ช ์์ ํจํค์ง ์ง์ ์์
-@EnableJpaRepositories({"com.loopers"})
+@EnableJpaRepositories({
+ "com.loopers.infrastructure",
+ "com.loopers.domain.rank"
+})๐ค Prompt for AI Agents
In modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java around line
11, @EnableJpaRepositories is set to scan "com.loopers" which is too broad and
pulls in domain repositories from other modules; change the base package(s) to
only the infrastructure packages (e.g. "com.loopers.infrastructure" or an
explicit list of infrastructure packages) or relocate repository classes into an
infrastructure package so only intended repositories are scanned; update the
annotation to use basePackages with the correct package(s) and run integration
tests to verify no unintended repositories are picked up.
Spring Batch
๐งฉ Ranking API
@coderabbitai ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ํ์ธํด์ค |
|
Tip For best results, initiate chat on the files or code changes. ๐งฉ Analysis chain@looper88888, ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ํ์ธํด๋๋ฆฌ๊ฒ ์ต๋๋ค. ์ฝ๋๋ฅผ ์์ธํ ๊ฒํ ํ์ฌ ๊ฐ ํญ๋ชฉ์ ๊ตฌํ ์ํ๋ฅผ ํ์ธํ๊ฒ ์ต๋๋ค. ๐ Script executed: Length of output: 9341 @looper88888, ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ๋ชจ๋ ํ์ธํ์ต๋๋ค! ๊ฐ ํญ๋ชฉ์ด ์ ๊ตฌํ๋์ด ์์ต๋๋ค. ๐ โ Spring Batchโ Spring Batch Job์ ์์ฑํ๊ณ , ํ๋ผ๋ฏธํฐ ๊ธฐ๋ฐ์ผ๋ก ๋์์ํฌ ์ ์๋ค.
JobParameters params = new JobParametersBuilder()
.addString("anchorDate", anchor.toString())
.addLong("ts", System.currentTimeMillis())
.toJobParameters();โ Chunk Oriented Processing (Reader/Processor/Writer or Tasklet) ๊ธฐ๋ฐ์ ๋ฐฐ์น ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํ๋ค.
โ ์ง๊ณ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํ Materialized View์ ๊ตฌ์กฐ๋ฅผ ์ค๊ณํ๊ณ ์ฌ๋ฐ๋ฅด๊ฒ ์ ์ฌํ๋ค.
โ ๐งฉ Ranking APIโ API๊ฐ ์ผ๊ฐ, ์ฃผ๊ฐ, ์๊ฐ ๋ญํน์ ์ ๊ณตํ๋ฉฐ ์กฐํํด์ผ ํ๋ ํํ์ ๋ฐ๋ผ ์ ์ ํ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ญํน์ ์ ๊ณตํ๋ค.
๋ชจ๋ ์๊ตฌ์ฌํญ์ด ์ถฉ์กฑ๋์์ต๋๋ค! ํนํ ํ๋ผ๋ฏธํฐ ๊ธฐ๋ฐ Job ์คํ, Chunk/Tasklet ํผ์ฉ, MV ์ค๊ณ์ ๋กค๋ง ์๋์ฐ ์ง๊ณ๊ฐ ์ ๊ตฌํ๋์ด ์์ต๋๋ค. ๐ |
๐ Summary
์ฃผ/์๊ฐ ๋ญํน ๋ฐฐ์น ๊ตฌํ: Rolling 7/30์ผ ์๋์ฐ๋ก ์ง๊ณ ํ MV(mv_product_rank_weekly/monthly) ์ ์ฌํ์์ต๋๋ค.
๐ฌ Review Points
API์์ ํ๋ผ๋ฏธํฐ ์ถ๊ฐ ์์ด ์๋ฒ์์ ๋ ์ง ๋ฒ์๋ฅผ ๊ณ์ฐํด ํ ๋ฒ์ ๋ด๋ ค์ค์ผ ํ ์ง, ์๋๋ฉด period(Daily, Weekly, Monthly) ๊ฐ์ ์ถ๊ฐ ํ๋ผ๋ฏธํฐ๊น์ง๋ ๋ฃ์ด๋ ๋ ์ง ๊ณ ๋ฏผ์ด ๋์์ต๋๋ค.
๋ฅํ๊ฒ ์๊ฐํ๋ ํ์ด ๊ถ๊ธํฉ๋๋ค. ๋ฉํ ๋๋ค์ ์กฐ์ธ์ ํตํด โ๋๋ ์ด๋ฐ ๊ณ ๋ฏผํ๋๋ฐ ์ ์ด๋ฐ๊ฑฐ์์?โ ๊ฐ์ ์๊ฐ์ ๊ผฌ๋ฆฌ์ ๊ผฌ๋ฆฌ๋ฅผ ๋ฌด๋ ์ต๊ด์ ๋ค์ฌ์ผ ํ๋ค๋ ๊ฒ์ ๋ฐฐ์ ์ต๋๋ค. ๊ทผ๋ฐ ์ ๋ ์ด๋ ์๊ฐ๋ถํฐ ์ค์ค๋ก โ์ ์ฌ๊ธฐ๊น์ง๋ง ์๋ฉด ๋๊ฒ ๋คโ, โ๋์๊ฐ๋ฉด ๋์ง~โ ๋ผ๋ ์๋ชป๋ ํ๋จ์ ๋ด๋ฆฌ๋ ์ต๊ด์ด ์๊ธด ๊ฒ ๊ฐ์ต๋๋ค. ์ถ๊ฐ์ ์ผ๋ก ๋ฉํ ๋๊ป์๋ ๋ธ๋๋ฐ์ค๋ก ์จ๋ ๋๋ ์์ญ์ ๊ฒฝ๊ณ๋ฅผ ์ด๋ป๊ฒ ๋๋์๋์ง๋ ๊ถ๊ธํฉ๋๋ค.
โ Checklist
๐งฑ Spring Batch
๐งฉ Ranking API
Summary by CodeRabbit
๋ฆด๋ฆฌ์ค ๋ ธํธ
์๋ก์ด ๊ธฐ๋ฅ
๊ฐ์ ์ฌํญ
ํ ์คํธ
โ๏ธ Tip: You can customize this high-level summary in your review settings.