-
Notifications
You must be signed in to change notification settings - Fork 34
Round10 #241
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: sylee6529
Are you sure you want to change the base?
Round10 #241
Conversation
Spring Boot 3.x Auto-configuration ์ฌ์ฉ์ ์ํด @EnableBatchProcessing ์ ๊ฑฐ Batch ๋ชจ๋์ ๋ณ๋ ๋ชจ๋๋ก ๋ถ๋ฆฌํ์ฌ ์ฌ์ฌ์ฉ์ฑ ํฅ์
์ฃผ๊ฐ/์๊ฐ ๋ญํน์ ์ํ Entity ๋ฐ Repository ์ธํฐํ์ด์ค ์ถ๊ฐ ๊ธฐ๊ฐ ํ์ (DAILY/WEEKLY/MONTHLY) ๋ฐ ๋ ์ง ๊ณ์ฐ ์ ํธ๋ฆฌํฐ ๊ตฌํ
QueryDSL ๊ธฐ๋ฐ Repository ๊ตฌํ์ผ๋ก N+1 ๋ฌธ์ ํด๊ฒฐ ์ฃผ๊ฐ/์๊ฐ TOP 100 ๋ญํน ์ ์ฅ์ ์ํ MV ํ ์ด๋ธ DDL ์ถ๊ฐ ํธ๋์ญ์ ์ฒ๋ฆฌ ๋ฐ QueryDSL delete ์ฟผ๋ฆฌ ์ต์ ํ
ProductMetricsDto: ๋ฐฐ์น Reader๊ฐ ์ฝ์ด์ฌ DTO RankedProductDto: ์ ์ ๊ณ์ฐ ํ ์ ๋ ฌ์ฉ DTO RankingScoreProcessor: ๊ฐ์ค์น ๊ธฐ๋ฐ ์ ์ ๊ณ์ฐ (view 0.1, like 0.2, order 0.6) InMemoryRankingCollector: Step ์๋ฃ ํ ์ ๋ ฌ์ ์ํ ๋ฉ๋ชจ๋ฆฌ ์์ง๊ธฐ
Chunk-Oriented Processing (CHUNK_SIZE=100) ์ ์ฉ JdbcPagingItemReader๋ก product_metrics ํ์ด์ง ์กฐํ StepExecutionListener์์ ์ ๋ ฌ ํ TOP 100 ์ ํ ๋ฐ ์ ์ฅ Reader ์ด๊ธฐํ๋ฅผ ์ํ afterPropertiesSet() ํธ์ถ ์ถ๊ฐ
RankingService: ์ฃผ๊ฐ/์๊ฐ ๋ญํน ์กฐํ ๋ฐ N+1 ํด๊ฒฐ RankingFacade: PeriodType์ ๋ฐ๋ผ DAILY/WEEKLY/MONTHLY ๋ถ๊ธฐ ์ฒ๋ฆฌ ๋ฐฐ์น ์กฐํ๋ก ์ํ ๋ฐ ๋ธ๋๋ ์ ๋ณด ํจ์จ์ ์กฐํ
BatchJobController: ์ฃผ๊ฐ/์๊ฐ ๋ญํน ์ง๊ณ Job ์๋ ์คํ API RankingV1Controller: period ํ๋ผ๋ฏธํฐ๋ก ์ผ๊ฐ/์ฃผ๊ฐ/์๊ฐ ์ ํ ์ง์ batch.yml ์ค์ import ์ถ๊ฐ
WeeklyRankingJobTest: ์ฃผ๊ฐ ๋ญํน ๋ฐฐ์น 3๊ฐ ์๋๋ฆฌ์ค ํ ์คํธ MonthlyRankingJobTest: ์๊ฐ ๋ญํน ๋ฐฐ์น 4๊ฐ ์๋๋ฆฌ์ค ํ ์คํธ TestContainers ๊ธฐ๋ฐ ์ค์ MySQL ํ๊ฒฝ ํ ์คํธ init-ranking-tables.sql: ํ ์คํธ์ฉ DDL ์คํฌ๋ฆฝํธ
|
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 ๊ฐ์์ด PR์ Spring Batch๋ฅผ ํ์ฉํ ์ฃผ๊ฐ ๋ฐ ์๊ฐ ์ํ ๋ญํน ๋ฐฐ์น ์์ ์ ๋์ ํฉ๋๋ค. ์๋ก์ด ๋ฐฐ์น ๋ชจ๋, ๋ญํน ์ํฐํฐ, ์ ์ฅ์, ํ๋ก์ธ์, ๊ทธ๋ฆฌ๊ณ RankingFacade๋ฅผ ํตํ ๋ค์ค ๊ธฐ๊ฐ ์กฐํ ๊ธฐ๋ฅ(์ผ์ผ, ์ฃผ๊ฐ, ์๊ฐ)์ ์ถ๊ฐํฉ๋๋ค. ๋ณ๊ฒฝ ์ฌํญ
์ํ์ค ๋ค์ด์ด๊ทธ๋จ์ฃผ๊ฐ/์๊ฐ ๋ญํน ๋ฐฐ์น ์์ ํ๋ฆsequenceDiagram
participant JobLauncher
participant BatchJob as Batch Job
participant Reader
participant Processor as Processor<br/>(Score Calc)
participant Collector as Memory<br/>Collector
participant Listener as Step<br/>Listener
participant DB as Database
JobLauncher->>BatchJob: Launch with targetDate
rect rgb(200, 220, 255)
note over Reader,Collector: Chunk Processing Loop
Reader->>DB: Read ProductMetrics (paginated)
Reader-->>Processor: ProductMetricsDto
Processor->>Processor: Calculate weighted score<br/>(view, like, order)
Processor-->>Collector: RankedProductDto
Collector->>Collector: Collect in memory
end
rect rgb(220, 255, 220)
note over Listener,DB: After Step Execution
Listener->>Listener: Parse targetDate
Listener->>Listener: Compute week/month range
Listener->>DB: Delete old rankings<br/>for period
Listener->>Collector: Get all collected items
Listener->>Listener: Sort by score (DESC)
Listener->>Listener: Select TOP 100
Listener->>Listener: Assign rank positions
Listener->>DB: Persist MonthlyRanking<br/>or WeeklyRanking
Listener->>Listener: Log completion
end
BatchJob-->>JobLauncher: Job Completed
API ๋ญํน ์กฐํ ํ๋ฆ (์ฃผ๊ฐ/์๊ฐ)sequenceDiagram
participant Client
participant Controller as RankingV1<br/>Controller
participant Facade as Ranking<br/>Facade
participant Service as Ranking<br/>Service
participant WeekRepo as Weekly/Monthly<br/>Repository
participant ProductRepo as Product<br/>Repository
participant BrandRepo as Brand<br/>Repository
Client->>Controller: GET /rankings?period=WEEKLY&page=1
Controller->>Facade: getRankings(date, WEEKLY, page, size)
alt period == WEEKLY or MONTHLY
Facade->>Service: getWeeklyRankings<br/>or getMonthlyRankings<br/>(targetDate, page, size)
Service->>WeekRepo: findByWeekStartDate<br/>WithPagination
WeekRepo->>WeekRepo: Query with LIMIT/OFFSET
WeekRepo-->>Service: List<WeeklyRanking>
rect rgb(255, 240, 200)
note over ProductRepo,BrandRepo: Batch Loading (้ฟๅ
N+1)
Service->>ProductRepo: loadProductsByIds<br/>(collected productIds)
ProductRepo-->>Service: Map<Long, Product>
Service->>BrandRepo: loadBrandsByIds<br/>(collected brandIds)
BrandRepo-->>Service: Map<Long, Brand>
end
Service->>Service: Build RankingItemInfo list<br/>(rank, product, brand, score)
Service-->>Facade: RankingPageInfo
else period == DAILY
Facade->>Facade: getDailyRankings<br/>(from ProductRankingCache)
end
Facade-->>Controller: RankingPageInfo
Controller-->>Client: ApiResponse<RankingPageResponse>
์์ ์ฝ๋ ๋ฆฌ๋ทฐ ๋ ธ๋ ฅ๐ฏ 4 (๋ณต์กํจ) | โฑ๏ธ ~60 ๋ถ ๊ด๋ จ ๊ฐ๋ฅ์ฑ ์๋ PR
์ ์๋ ๋ ์ด๋ธ
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: 7
โป๏ธ Duplicate comments (1)
apps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.java (1)
143-160:WeeklyRankingJobConfig์์ ์ธ๊ธํ ๋์ผํ ๋์ ์คํ ์ํ์ด ์กด์ฌํฉ๋๋ค
deleteByMonthYear()ํsaveAll()์ฌ์ด์ ๊ฒฝํฉ ์กฐ๊ฑด์ด ๋ฐ์ํ ์ ์์ต๋๋ค.month_year์rank_position์ ๋ํ ์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํด ์ฃผ์ธ์.
๐งน Nitpick comments (21)
docker/mysql/init/03-ranking-materialized-views.sql (1)
1-28: DDL ๊ตฌ์กฐ๊ฐ ์ ์ค๊ณ๋์์ต๋๋ค.์ธ๋ฑ์ค ์ ๋ต์ด ๊ธฐ๊ฐ๋ณ ์กฐํ(
idx_week_product,idx_month_product)์ ์ํ๋ณ ์ด๋ ฅ ์กฐํ(idx_product_week,idx_product_month) ๋ชจ๋๋ฅผ ํจ์จ์ ์ผ๋ก ์ง์ํฉ๋๋ค.๋ช ๊ฐ์ง ๊ณ ๋ ค ์ฌํญ:
๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ: ๋์ผ ๊ธฐ๊ฐ ๋ด ๋์ผ
product_id์ ์ค๋ณต ์ฝ์ ์ ๋ฐฉ์งํ๋ ค๋ฉดUNIQUE์ ์ฝ ์กฐ๊ฑด ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํ์ธ์:UNIQUE KEY uk_week_product (week_start_date, product_id), UNIQUE KEY uk_month_product (month_year, product_id),์ ์ ์ ๋ฐ๋:
DOUBLE์ ๋ถ๋์์์ ์ค์ฐจ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์ ๋ฐํ ์ ์ ๋น๊ต๊ฐ ํ์ํ๋ค๋ฉดDECIMAL(10,4)์ฌ์ฉ์ ๊ณ ๋ คํ์ธ์.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
33-34: ๋ ์ง ํ์ ๊ฒ์ฆ ์ถ๊ฐ ๊ถ์ฅ
dateํ๋ผ๋ฏธํฐ๊ฐyyyyMMddํ์์ด์ด์ผ ํ์ง๋ง ํ์ ๊ฒ์ฆ์ด ์์ต๋๋ค. ์๋ชป๋ ํ์์ ๋ ์ง๊ฐ ์ ๋ ฅ๋๋ฉดRankingFacade๋ ํ์ ๋ ์ด์ด์์ ํ์ฑ ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.๐ ๋ ์ง ํ์ ๊ฒ์ฆ ์ถ๊ฐ
+import jakarta.validation.constraints.Pattern; + @GetMapping("/rankings") public ApiResponse<RankingPageResponse> getRankings( - @RequestParam(value = "date", required = false) String date, + @RequestParam(value = "date", required = false) + @Pattern(regexp = "^\\d{8}$", message = "๋ ์ง๋ yyyyMMdd ํ์์ด์ด์ผ ํฉ๋๋ค") + String date, @RequestParam(value = "period", defaultValue = "DAILY") PeriodType period,apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRanking.java (1)
30-34: MonthlyRanking๊ณผ์ ์ผ๊ด์ฑ์ ์ํด ์ฃผ์ฐจ ์๋ณ์ ํ๋ ์ถ๊ฐ ๊ณ ๋ ค
MonthlyRanking์๋monthYearํ๋(์: "2024-01")๊ฐ ์์ด ์๋ณ ์กฐํ๊ฐ ํธ๋ฆฌํ๋ฐ,WeeklyRanking์๋ ์ด์ ๋์ํ๋ ์ฃผ์ฐจ ์๋ณ์ ํ๋๊ฐ ์์ต๋๋ค. ์ฃผ ๋จ์ ์กฐํ ์ ๋ ์ง ๋ฒ์ ๋น๊ต๊ฐ ํ์ํ์ฌ ์ฟผ๋ฆฌ๊ฐ ๋ณต์กํด์ง ์ ์์ต๋๋ค.๐ ์ฃผ์ฐจ ์๋ณ์ ํ๋ ์ถ๊ฐ ์์
@Column(name = "week_end_date", nullable = false) private LocalDate weekEndDate; +@Column(name = "week_identifier", nullable = false) +private String weekIdentifier; // ์: "2024-W01", "2024-W52" + public WeeklyRanking(Integer rankPosition, Long productId, Double totalScore, - LocalDate weekStartDate, LocalDate weekEndDate) { + LocalDate weekStartDate, LocalDate weekEndDate, String weekIdentifier) { this.rankPosition = rankPosition; this.productId = productId; this.totalScore = totalScore; this.weekStartDate = weekStartDate; this.weekEndDate = weekEndDate; + this.weekIdentifier = weekIdentifier; }apps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRanking.java (1)
11-48: LGTM! ์ํฐํฐ ๊ตฌ์กฐ๊ฐ ์ ์ ํฉ๋๋ค.JPA ์ํฐํฐ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ค๊ณ๋์์ต๋๋ค.
@NoArgsConstructor(access = AccessLevel.PROTECTED)๋ก JPA ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ๊ณ ,WeeklyRanking๊ณผ ์ผ๊ด๋ ๊ตฌ์กฐ๋ฅผ ์ ์งํฉ๋๋ค.์ ํ์ ์ผ๋ก, ์์ฑ์ ํ๋ผ๋ฏธํฐ์ ๋ํ null ๊ฒ์ฆ ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํด๋ณผ ์ ์์ต๋๋ค:
๐ Null ๊ฒ์ฆ ์ถ๊ฐ ์์
public MonthlyRanking(Integer rankPosition, Long productId, Double totalScore, String monthYear, LocalDate monthStartDate, LocalDate monthEndDate) { + Objects.requireNonNull(rankPosition, "rankPosition must not be null"); + Objects.requireNonNull(productId, "productId must not be null"); this.rankPosition = rankPosition; this.productId = productId; this.totalScore = totalScore; this.monthYear = monthYear; this.monthStartDate = monthStartDate; this.monthEndDate = monthEndDate; }apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodUtils.java (3)
8-8: ์ ํธ๋ฆฌํฐ ํด๋์ค์ private ์์ฑ์๊ฐ ์์ต๋๋ค.์ธ์คํด์คํ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด private ์์ฑ์๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๐ Private ์์ฑ์ ์ถ๊ฐ
public class PeriodUtils { + + private PeriodUtils() { + // Utility class - prevent instantiation + } private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
50-52: DateTimeFormatter๋ฅผ ํธ์ถ๋ง๋ค ์์ฑํ๊ณ ์์ต๋๋ค.
parseDate๋ฉ์๋์์ ๋งค๋ฒ ์๋ก์ดDateTimeFormatter๋ฅผ ์์ฑํฉ๋๋ค.MONTH_FORMATTER์ฒ๋ผ ์์๋ก ์ถ์ถํ๋ฉด ํจ์จ์ ์ ๋๋ค.๐ Formatter ์์ํ
public class PeriodUtils { private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); // ... public static LocalDate parseDate(String dateString) { - return LocalDate.parse(dateString, DateTimeFormatter.ofPattern("yyyyMMdd")); + return LocalDate.parse(dateString, DATE_FORMATTER); }
22-24:getWeekEndDate๋ฉ์๋ ๊ฐ์ํ ๊ฐ๋ฅ.
DayOfWeek.SUNDAY๋ฅผ ์ง์ ์ฌ์ฉํ๋ฉด ๋ ๊ฐ๊ฒฐํ๊ณ ๋ช ํํฉ๋๋ค.๐ ๊ฐ์ํ๋ ๊ตฌํ
public static LocalDate getWeekEndDate(LocalDate date) { - return date.with(DayOfWeek.MONDAY).plusDays(6); + return date.with(DayOfWeek.SUNDAY); }apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingJpaRepository.java (1)
15-17: ํ๋ผ๋ฏธํฐ ์์๊ฐ ์ผ๋ฐ์ ์ธ ํจํด๊ณผ ๋ค๋ฆ ๋๋ค.๋ฉ์๋ ์๊ทธ๋์ฒ์์
offset์ดlimit์์ ์์ต๋๋ค. SQL์LIMIT ... OFFSET์์์ ์ผ์นํ๋๋กlimit, offset์์๊ฐ ๋ ์ง๊ด์ ์ ๋๋ค.๐ ํ๋ผ๋ฏธํฐ ์์ ๋ณ๊ฒฝ
- List<MonthlyRanking> findByMonthYearWithPagination(String monthYear, int offset, int limit); + List<MonthlyRanking> findByMonthYearWithPagination(String monthYear, int limit, int offset);apps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.java (2)
44-47: ์์ธ ๋ฉ์์ง ๋ ธ์ถ ์ฃผ์
e.getMessage()๋ฅผ ์๋ต์ ์ง์ ํฌํจํ๋ฉด ๋ด๋ถ ๊ตฌํ ์ธ๋ถ ์ฌํญ(์คํ ์ ๋ณด, DB ์ค๋ฅ ๋ฑ)์ด ์ธ๋ถ์ ๋ ธ์ถ๋ ์ ์์ต๋๋ค. ํ๋ก๋์ ํ๊ฒฝ์์๋ ์ผ๋ฐ์ ์ธ ์ค๋ฅ ๋ฉ์์ง๋ง ๋ฐํํ๊ณ ์์ธ ์ ๋ณด๋ ๋ก๊ทธ์๋ง ๊ธฐ๋กํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์๋ ์์
} catch (Exception e) { log.error("์ฃผ๊ฐ ๋ญํน ์ง๊ณ Job ์คํ ์คํจ", e); - return ApiResponse.fail("BATCH_ERROR", "์ฃผ๊ฐ ๋ญํน ์ง๊ณ Job ์คํ์ ์คํจํ์ต๋๋ค: " + e.getMessage()); + return ApiResponse.fail("BATCH_ERROR", "์ฃผ๊ฐ ๋ญํน ์ง๊ณ Job ์คํ์ ์คํจํ์ต๋๋ค."); }
27-74: ์ค๋ณต ์ฝ๋ ๋ฆฌํฉํ ๋ง ๊ณ ๋ ค
runWeeklyRankingJob๊ณผrunMonthlyRankingJob๋ฉ์๋๊ฐ ๊ฑฐ์ ๋์ผํ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค. ๊ณตํต ๋ก์ง์ private ํฌํผ ๋ฉ์๋๋ก ์ถ์ถํ๋ฉด ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ฉ๋๋ค.๐ ๋ฆฌํฉํ ๋ง ์์
private ApiResponse<Object> runRankingJob(Job job, String targetDate, String jobName) { try { String dateParam = targetDate != null ? targetDate : LocalDate.now().toString(); JobParameters jobParameters = new JobParametersBuilder() .addString("targetDate", dateParam) .addLong("timestamp", System.currentTimeMillis()) .toJobParameters(); jobLauncher.run(job, jobParameters); log.info("{} Job ์คํ ์๋ฃ - targetDate: {}", jobName, dateParam); return ApiResponse.success(jobName + " Job์ด ์ฑ๊ณต์ ์ผ๋ก ์คํ๋์์ต๋๋ค."); } catch (Exception e) { log.error("{} Job ์คํ ์คํจ", jobName, e); return ApiResponse.fail("BATCH_ERROR", jobName + " Job ์คํ์ ์คํจํ์ต๋๋ค."); } }apps/commerce-api/src/test/java/com/loopers/application/batch/MonthlyRankingJobTest.java (1)
198-202: ์ฃผ์๊ณผ ๊ตฌํ์ด ์ผ์นํ์ง ์์์ฃผ์์๋ "์ ์๊ฐ ๋ค์ํ๋๋ก ๋๋คํ๊ฒ ์์ฑ"์ด๋ผ๊ณ ๋์ด ์์ง๋ง, ์ค์ ๊ตฌํ์ ๊ฒฐ์ ์ (deterministic) ๋ฐฉ์์ผ๋ก ์ ์๋ฅผ ๋ถ์ฌํฉ๋๋ค. ์ด๋ ํ ์คํธ ์ฌํ์ฑ ์ธก๋ฉด์์ ์ฌ๋ฐ๋ฅธ ์ ๊ทผ์ด๋ฏ๋ก ์ฃผ์์ ์์ ํ๋ ๊ฒ์ด ์ข๊ฒ ์ต๋๋ค.
๐ ์ ์๋ ์์
/** * ์ํ ProductMetrics ๋ฐ์ดํฐ ์์ฑ * - productId: 1๋ถํฐ count๊น์ง - * - ์ ์๊ฐ ๋ค์ํ๋๋ก ๋๋คํ๊ฒ ์์ฑ + * - productId ์ญ์์ผ๋ก ์ ์ ๋ถ์ฌ (ํ ์คํธ ์ฌํ์ฑ์ ์ํด ๊ฒฐ์ ์ ๊ฐ ์ฌ์ฉ) */apps/commerce-api/src/test/java/com/loopers/application/batch/WeeklyRankingJobTest.java (3)
170-174: ์ฃผ์๊ณผ ๊ตฌํ์ด ์ผ์นํ์ง ์์
MonthlyRankingJobTest์ ๋์ผํ๊ฒ, ์ฃผ์์๋ "๋๋คํ๊ฒ ์์ฑ"์ด๋ผ๊ณ ๋์ด ์์ง๋ง ์ค์ ๋ก๋ ๊ฒฐ์ ์ ๋ฐฉ์์ ๋๋ค.๐ ์ ์๋ ์์
/** * ์ํ ProductMetrics ๋ฐ์ดํฐ ์์ฑ * - productId: 1๋ถํฐ count๊น์ง - * - ์ ์๊ฐ ๋ค์ํ๋๋ก ๋๋คํ๊ฒ ์์ฑ + * - productId ์ญ์์ผ๋ก ์ ์ ๋ถ์ฌ (ํ ์คํธ ์ฌํ์ฑ์ ์ํด ๊ฒฐ์ ์ ๊ฐ ์ฌ์ฉ) */
138-168: ๋ค๋ฅธ ์ฃผ(week) ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ ํ ์คํธ ๋๋ฝ
MonthlyRankingJobTest์๋shouldNotAffectOtherMonthsRankingํ ์คํธ๊ฐ ์์ง๋ง, ์ด ํ ์คํธ ํด๋์ค์๋ ์ ์ฌํ "๋ค๋ฅธ ์ฃผ์ ๋ญํน ๋ฐ์ดํฐ๊ฐ ์ํฅ๋ฐ์ง ์๋๋ค" ํ ์คํธ๊ฐ ์์ต๋๋ค. ๋ฐฐ์น ์์ ์ ๊ฒฉ๋ฆฌ์ฑ์ ๋ณด์ฅํ๋ ค๋ฉด ์ถ๊ฐํ๋ ๊ฒ์ด ์ข๊ฒ ์ต๋๋ค.๋ค๋ฅธ ์ฃผ ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด ๋๋ฆด๊น์?
175-188: ํ ์คํธ ์ ํธ๋ฆฌํฐ ์ค๋ณต ์ฝ๋
insertSampleProductMetrics๋ฉ์๋๊ฐMonthlyRankingJobTest์ ๋์ผํ๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค. ํ ์คํธ ์ ํธ๋ฆฌํฐ ํด๋์ค๋ก ์ถ์ถํ๋ฉด ์ ์ง๋ณด์๊ฐ ์ฉ์ดํด์ง๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (3)
53-56: ์ ์ฒด ๋ฐ์ดํฐ ์กฐํ๋ก ์ธํ ์ฑ๋ฅ ๋ฌธ์
totalCount๋ฅผ ๊ตฌํ๊ธฐ ์ํด ๋ชจ๋ ๋ญํน ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๊ณ ์์ต๋๋ค. ๋ญํน ๋ฐ์ดํฐ๊ฐ ์ต๋ 100๊ฐ๋ก ์ ํ๋์ด ์์ด ํ์ฌ๋ ์ฌ๊ฐํ์ง ์์ง๋ง, ํฅํ ํ์ฅ์ฑ์ ์ํด ๋ณ๋์ count ์ฟผ๋ฆฌ๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์๋ ์์
Repository์ count ๋ฉ์๋ ์ถ๊ฐ:
// WeeklyRankingRepository long countByWeekStartDate(LocalDate weekStartDate);Service์์ ์ฌ์ฉ:
- // 3. ์ ์ฒด ๊ฐ์ ์กฐํ - List<WeeklyRanking> allRankings = weeklyRankingRepository - .findByWeekStartDateOrderByRankPosition(weekStartDate); - long totalCount = allRankings.size(); + // 3. ์ ์ฒด ๊ฐ์ ์กฐํ + long totalCount = weeklyRankingRepository.countByWeekStartDate(weekStartDate);
95-138: Weekly/Monthly ๋น๋ ๋ฉ์๋ ์ค๋ณต
buildRankingItemsFromWeekly์buildRankingItemsFromMonthly๊ฐ ๊ฑฐ์ ๋์ผํ ๋ก์ง์ ๊ฐ์ง๊ณ ์์ต๋๋ค. ๊ณตํต ์ธํฐํ์ด์ค๋ ์ ๋ค๋ฆญ์ ํ์ฉํ์ฌ ์ค๋ณต์ ์ค์ผ ์ ์์ต๋๋ค.๐ ๋ฆฌํฉํ ๋ง ์์
private <T> List<RankingItemInfo> buildRankingItems( List<T> rankings, Function<T, Long> productIdExtractor, Function<T, Integer> rankPositionExtractor, Function<T, Double> scoreExtractor, String logPrefix ) { List<Long> productIds = rankings.stream() .map(productIdExtractor) .toList(); List<Product> products = productRepository.findByIdIn(productIds); Map<Long, Product> productMap = products.stream() .collect(Collectors.toMap(Product::getId, Function.identity())); List<Long> brandIds = products.stream() .map(Product::getBrandId) .distinct() .toList(); List<Brand> brands = brandRepository.findByIdIn(brandIds); Map<Long, Brand> brandMap = brands.stream() .collect(Collectors.toMap(Brand::getId, Function.identity())); return rankings.stream() .map(ranking -> { Product product = productMap.get(productIdExtractor.apply(ranking)); if (product == null) { log.warn("[{}] Product not found - productId: {}", logPrefix, productIdExtractor.apply(ranking)); return null; } Brand brand = brandMap.get(product.getBrandId()); String brandName = brand != null ? brand.getName() : "Unknown"; return new RankingItemInfo( rankPositionExtractor.apply(ranking), product.getId(), product.getName(), brandName, product.getPrice(), product.getLikeCount(), scoreExtractor.apply(ranking) ); }) .filter(Objects::nonNull) .toList(); }
136-136: ๋ฉ์๋ ์ฐธ์กฐ ์ฌ์ฉ ๊ถ์ฅ
filter(item -> item != null)๋์filter(Objects::nonNull)์ ์ฌ์ฉํ๋ฉด ๋ ๊ฐ๊ฒฐํฉ๋๋ค.๐ ์ ์๋ ์์
+import java.util.Objects; ... }) - .filter(item -> item != null) + .filter(Objects::nonNull) .toList();apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
118-143:parseAndValidateDate์validateAndNormalizeDate๋ฉ์๋์ ์ค๋ณต ๋ก์ง๋ ๋ฉ์๋๊ฐ ์ ์ฌํ ๋ ์ง ํ์ฑ/๊ฒ์ฆ ๋ก์ง์ ์ํํฉ๋๋ค.
validateAndNormalizeDate๊ฐ ๋ ์ด์ ์ฌ์ฉ๋์ง ์๋๋ค๋ฉด ์ ๊ฑฐ๋ฅผ ๊ณ ๋ คํด ์ฃผ์ธ์. ์ฌ์ฉ๋๋ค๋ฉดparseAndValidateDate๋ฅผ ํ์ฉํ๋๋ก ๋ฆฌํฉํ ๋งํ ์ ์์ต๋๋ค.#!/bin/bash # validateAndNormalizeDate ๋ฉ์๋ ์ฌ์ฉ ์ฌ๋ถ ํ์ธ rg -n "validateAndNormalizeDate" --type javaapps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.java (1)
44-201:WeeklyRankingJobConfig์ ์๋นํ ์ฝ๋ ์ค๋ณต์ด ์์ต๋๋ค
MonthlyRankingJobConfig์WeeklyRankingJobConfig๋ reader ์ค์ , processor ์์ฑ, listener ๋ก์ง ๋ฑ ๋๋ถ๋ถ์ ์ฝ๋๊ฐ ๋์ผํฉ๋๋ค. ํ์ฌ๋ ๋ ํด๋์ค๋ง ์์ด ๊ด๋ฆฌ ๊ฐ๋ฅํ์ง๋ง, ํฅํ ์ ์ง๋ณด์์ฑ์ ์ํด ๊ณตํต ๋ก์ง ์ถ์ถ์ ๊ณ ๋ คํด ๋ณผ ์ ์์ต๋๋ค.๐ ๊ณตํต ๋ก์ง ์ถ์ถ ์์
// ๊ณตํต ์ถ์ ํด๋์ค ๋๋ ํฌํผ ํด๋์ค ์ถ์ถ ์์ public abstract class AbstractRankingJobConfig { protected JdbcPagingItemReader<ProductMetricsDto> createReader( DataSource dataSource, String readerName) { // ๊ณตํต reader ์ค์ ๋ก์ง } protected RowMapper<ProductMetricsDto> productMetricsRowMapper() { // ๊ณตํต RowMapper } protected List<T> processAndRankItems( InMemoryRankingCollector collector, int topN) { // ๊ณตํต ์ ๋ ฌ ๋ฐ TOP N ์ ํ ๋ก์ง } }apps/commerce-api/src/test/resources/db/init-ranking-tables.sql (2)
2-16: ๋์ผ ์ฃผ๊ฐ์ ์ค๋ณต ์์ ๋ฐฉ์ง๋ฅผ ์ํ ์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด ์ถ๊ฐ ๊ถ์ฅํ์ฌ ์คํค๋ง์์๋ ๋์ผํ
week_start_date์ ๊ฐ์rank_position์ด ์ฌ๋ฌ ๋ฒ ์ฝ์ ๋ ์ ์์ต๋๋ค. ๋ฐฐ์น job์ ๋์ ์คํ์ด๋ ์ฌ์คํ ์ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.๐ ์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด ์ถ๊ฐ ์์
CREATE TABLE IF NOT EXISTS mv_product_rank_weekly ( id BIGINT AUTO_INCREMENT, rank_position INT NOT NULL, product_id BIGINT NOT NULL, total_score DOUBLE NOT NULL, week_start_date DATE NOT NULL COMMENT '์ฃผ๊ฐ ์์์ผ (์์์ผ)', week_end_date DATE NOT NULL COMMENT '์ฃผ๊ฐ ์ข ๋ฃ์ผ (์ผ์์ผ)', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), + UNIQUE KEY uk_week_rank (week_start_date, rank_position), INDEX idx_week_rank (week_start_date, rank_position), INDEX idx_week_product (week_start_date, product_id), INDEX idx_product_week (product_id, week_start_date) ) COMMENT '์ฃผ๊ฐ ์ํ ๋ญํน (TOP 100)';
19-34: ์๊ฐ ํ ์ด๋ธ์๋ ๋์ผํ ์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด ์ถ๊ฐ ๊ถ์ฅ
mv_product_rank_monthlyํ ์ด๋ธ์๋(month_year, rank_position)์ ๋ํ ์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด์ด ์์ต๋๋ค.๐ ์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด ์ถ๊ฐ ์์
CREATE TABLE IF NOT EXISTS mv_product_rank_monthly ( ... PRIMARY KEY (id), + UNIQUE KEY uk_month_rank (month_year, rank_position), INDEX idx_month_rank (month_year, rank_position), ... ) COMMENT '์๊ฐ ์ํ ๋ญํน (TOP 100)';
๐ Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (30)
apps/commerce-api/build.gradle.ktsapps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.javaapps/commerce-api/src/main/java/com/loopers/application/batch/WeeklyRankingJobConfig.javaapps/commerce-api/src/main/java/com/loopers/application/batch/dto/ProductMetricsDto.javaapps/commerce-api/src/main/java/com/loopers/application/batch/dto/RankedProductDto.javaapps/commerce-api/src/main/java/com/loopers/application/batch/processor/RankingScoreProcessor.javaapps/commerce-api/src/main/java/com/loopers/application/batch/writer/InMemoryRankingCollector.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodType.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodUtils.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRanking.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRankingRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRanking.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRankingRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.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/application/batch/MonthlyRankingJobTest.javaapps/commerce-api/src/test/java/com/loopers/application/batch/WeeklyRankingJobTest.javaapps/commerce-api/src/test/resources/db/init-ranking-tables.sqldocker/mysql/init/03-ranking-materialized-views.sqlmodules/batch/build.gradle.ktsmodules/batch/src/main/java/com/loopers/config/batch/BatchConfig.javamodules/batch/src/main/resources/batch.ymlsettings.gradle.kts
๐งฐ Additional context used
๐ง Learnings (4)
๐ 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/application/batch/dto/RankedProductDto.javaapps/commerce-api/src/main/java/com/loopers/application/batch/dto/ProductMetricsDto.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:
modules/batch/src/main/resources/batch.ymlapps/commerce-api/src/main/resources/application.ymlapps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.javaapps/commerce-api/src/main/java/com/loopers/application/batch/WeeklyRankingJobConfig.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.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-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRankingRepository.java
๐ Learning: 2025-12-02T08:12:06.383Z
Learnt from: junoade
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 0
File: :0-0
Timestamp: 2025-12-02T08:12:06.383Z
Learning: ProductQueryService์์ ์ํ ๋ชฉ๋ก ์กฐํ ์ Redis ์บ์๋ฅผ ์ ์ฉํ์ผ๋ฉฐ, ์บ์ ํค๋ brandId, sortType, pageNumber, pageSize์ ์กฐํฉ์ผ๋ก ๊ตฌ์ฑ๋๊ณ TTL์ 5๋ถ์ผ๋ก ์ค์ ๋์ด ์๋ค.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
๐งฌ Code graph analysis (9)
apps/commerce-api/src/main/java/com/loopers/application/batch/processor/RankingScoreProcessor.java (1)
apps/commerce-api/src/main/java/com/loopers/application/batch/writer/InMemoryRankingCollector.java (1)
Slf4j(17-42)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingRepositoryImpl.java (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingRepositoryImpl.java (1)
Repository(15-56)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingRepositoryImpl.java (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingRepositoryImpl.java (1)
Repository(14-55)
apps/commerce-api/src/test/java/com/loopers/application/batch/WeeklyRankingJobTest.java (1)
apps/commerce-api/src/test/java/com/loopers/application/batch/MonthlyRankingJobTest.java (1)
SpringBootTest(30-217)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java (1)
RankingInfo(7-40)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.java (1)
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt (1)
log(22-119)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRanking.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRanking.java (1)
Entity(11-48)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRanking.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRanking.java (1)
Entity(11-44)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodUtils.java (1)
PeriodUtils(8-75)
๐ Additional comments (23)
modules/batch/src/main/resources/batch.yml (1)
1-6: LGTM! Spring Batch ์ค์ ์ด ์ ์ ํฉ๋๋ค.
spring.batch.job.enabled=false๋ก ์๋ ์คํ์ ๋นํ์ฑํํ๊ณ ์๋ ์คํ์ ์ง์ํ๋ ๊ฒ์ ์ฌ๋ฐ๋ฅธ ์ ๊ทผ์ ๋๋ค.
initialize-schema: always์ค์ ์ ๊ฐ๋ฐ ํ๊ฒฝ์์๋ ์ ํฉํ์ง๋ง, ํ๋ก๋์ ๋ฐฐํฌ ์์๋never๋๋embedded๋ก ๋ณ๊ฒฝํ๊ฑฐ๋ ํ๋กํ์ผ๋ณ๋ก ๋ถ๋ฆฌํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์. (๊ธฐ์กด ํ์ต์ ๋ฐ๋ฅด๋ฉด EDA๋ก ์ ํ ์์ ์ด๋ฏ๋ก ํ์ฌ๋ ๋ฌธ์ ์์ต๋๋ค.)settings.gradle.kts (1)
10-10: LGTM!์๋ก์ด batch ๋ชจ๋์ด ์ ์ ํ๊ฒ Gradle ๋น๋์ ํฌํจ๋์์ต๋๋ค.
apps/commerce-api/src/main/resources/application.yml (1)
25-25: LGTM!batch.yml ์ค์ ํ์ผ์ด ์ฌ๋ฐ๋ฅด๊ฒ import๋์์ต๋๋ค. ์ธํ๋ผ ๋ชจ๋ ์ค์ ๋ค์, ์ง์ ๋ชจ๋ ์ค์ ์ ์ ๋ฐฐ์นํ์ฌ ์ผ๊ด์ฑ ์๋ ๊ตฌ์กฐ๋ฅผ ์ ์งํ๊ณ ์์ต๋๋ค.
modules/batch/src/main/java/com/loopers/config/batch/BatchConfig.java (1)
1-16: LGTM! Spring Boot 3.x ๋ฐฐ์น ์ค์ ๋ฐฉ์์ด ์ฌ๋ฐ๋ฅด๊ฒ ๋ฌธ์ํ๋์ด ์์ต๋๋ค.
@EnableBatchProcessing๋์ auto-configuration์ ์ฌ์ฉํ๋ Spring Boot 3.x ์ ๊ทผ ๋ฐฉ์์ ๋ํ ์ค๋ช ์ด ๋ช ํํฉ๋๋ค.ํ์ฌ ๋น ํด๋์ค์ด์ง๋ง, ํฅํ ์ปค์คํ ๋ฐฐ์น ๋น(์:
JobRepository,PlatformTransactionManager์ปค์คํฐ๋ง์ด์ง)์ ์ถ๊ฐํ ์ ์๋ ํ์ฅ ์ง์ ์ผ๋ก ์ ์งํด๋ ์ข์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/ranking/PeriodType.java (1)
1-7: LGTM!๊ธฐ๊ฐ ์ ํ์ ๋ช ํํ๊ฒ ์ ์ํ ์ฌํํ enum์ ๋๋ค.
DAILY๊ฐ ํฌํจ๋์ด ์์ด ํฅํ ์ผ๊ฐ ๋ญํน ๊ธฐ๋ฅ ํ์ฅ์๋ ๋๋น๋์ด ์์ต๋๋ค.apps/commerce-api/build.gradle.kts (1)
6-6: LGTM!batch ๋ชจ๋ ์์กด์ฑ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ถ๊ฐ๋์์ต๋๋ค. ์ด๋ฅผ ํตํด
WeeklyRankingJobConfig,MonthlyRankingJobConfig๋ฑ ๋ฐฐ์น ๊ด๋ จ ์ปดํฌ๋ํธ๋ค์ด Spring Batch๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ๋ฉ๋๋ค.modules/batch/build.gradle.kts (1)
1-14: LGTM!๋ฐฐ์น ๋ชจ๋ ๋น๋ ์ค์ ์ด ์ ์ ํฉ๋๋ค.
java-libraryํ๋ฌ๊ทธ์ธ ์ฌ์ฉ๊ณผapi์์กด์ฑ ๋ ธ์ถ ๋ฐฉ์์ด ์ฌ๋ฐ๋ฆ ๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRanking.java (1)
11-34: ์ธ๋ฑ์ค๋ ์ด๋ฏธ DDL ํ์ผ์์ ์ ์๋์ด ์์ผ๋ฏ๋ก Java ์ํฐํฐ์ ์ถ๊ฐํ ํ์๊ฐ ์์ต๋๋ค.
apps/commerce-api/src/test/resources/db/init-ranking-tables.sql์mv_product_rank_weeklyํ ์ด๋ธ์ด ๋ค์๊ณผ ๊ฐ์ด ์ ์๋์ด ์์ต๋๋ค:
INDEX idx_week_rank (week_start_date, rank_position)โINDEX idx_week_product (week_start_date, product_id)โINDEX idx_product_week (product_id, week_start_date)โ์ด ํ๋ก์ ํธ๋ DDL ๋ง์ด๊ทธ๋ ์ด์ ์คํฌ๋ฆฝํธ๋ฅผ ํตํด ์ธ๋ฑ์ค๋ฅผ ๊ด๋ฆฌํ๊ณ ์์ผ๋ฏ๋ก, Java ์ํฐํฐ ํด๋์ค์
@Index์ฃผ์์ ์ถ๊ฐํ ํ์๊ฐ ์์ต๋๋ค. ํ์ฌ ์ํ๊ฐ ์ฌ๋ฐ๋ฆ ๋๋ค.Likely an incorrect or invalid review comment.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
34-34: Spring์ enum ํ๋ผ๋ฏธํฐ ์๋ ๊ฒ์ฆ ํ์ธ๋จSpring Framework๋
@RequestParam์ผ๋ก ์ ์ธ๋ enum ํ์ ํ๋ผ๋ฏธํฐ์ ์๋ชป๋ ๊ฐ์ด ์ ๋ ฅ๋ ๋ ์๋์ผ๋ก HTTP 400 Bad Request๋ฅผ ๋ฐํํฉ๋๋ค.StringToEnumConverterFactory๊ฐ ๋์๋ฌธ์๋ฅผ ๊ตฌ๋ถํ๋Enum.valueOf()๋ฅผ ์ฌ์ฉํ๋ฏ๋ก, ์ ํจํ์ง ์์ ๊ฐ์ ๋ํดConversionFailedException์ด ๋ฐ์ํ๋ฉฐ ์ด๋ ์๋์ผ๋ก 400 ์๋ต์ผ๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค.PeriodTypeenum์ ๋ํด์๋ ์ด ํ์ค ๋์์ด ์ ์ฉ๋๋ฏ๋ก ์ถ๊ฐ ๊ฒ์ฆ ๋ก์ง์ ํ์ํ์ง ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/ranking/weekly/WeeklyRankingRepository.java (1)
6-15: LGTM! ๊น๋ํ ๋๋ฉ์ธ ๋ ํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค์ ๋๋ค.Spring Data ์์กด์ฑ ์์ด ๋๋ฉ์ธ ๊ณ์ธต์์ ํ์ํ ๋ฉ์๋๋ง ์ ์ํ์ฌ ํด๋ฆฐ ์ํคํ ์ฒ๋ฅผ ์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/domain/ranking/monthly/MonthlyRankingRepository.java (1)
5-14: LGTM! WeeklyRankingRepository์ ์ผ๊ด๋ ๊ตฌ์กฐ์ ๋๋ค.
monthYear๋ฅผStringํ์ ์ผ๋ก ์ฌ์ฉํ๋ ๊ฒ์ด "yyyy-MM" ํฌ๋งท ์ ์ฅ ๋ฐฉ์๊ณผ ์ ๋ง์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/batch/processor/RankingScoreProcessor.java (1)
27-43: LGTM! ์ ์ ๊ณ์ฐ ๋ก์ง์ด ์ ๊ตฌํ๋์์ต๋๋ค.
Math.log1p๋ฅผ ์ฌ์ฉํ ๋ก๊ทธ ์ ๊ทํ๋ก ๊ณ ๊ฐ ์ํ ํธ์ค์ ๋ฐฉ์งํ๋ ์ค๊ณ๊ฐ ์ข์ต๋๋ค. ๊ฐ์ค์น๋ฅผ ์์ฑ์ ์ฃผ์ ์ผ๋ก ๋ฐ์ ์ ์ฐ์ฑ์ ํ๋ณดํ์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/batch/writer/InMemoryRankingCollector.java (1)
20-27:Collections.synchronizedList์ฌ์ฉ์ ํ์ฌ ๊ตฌ์กฐ์์ ๋ถํ์ํฉ๋๋ค.ํ์ฌ Step ์ค์ ์์
taskExecutor๊ฐ ๊ตฌ์ฑ๋์ด ์์ง ์์ผ๋ฏ๋ก, ์ด Step์ ๋จ์ผ ์ค๋ ๋๋ก ์คํ๋ฉ๋๋ค. ๋ฐ๋ผ์ ๋์์ฑ ๋ฌธ์ ๋ ๋ฐ์ํ์ง ์์ผ๋ฉฐ,Collections.synchronizedList๋ ๋ถํ์ํ ์ค๋ฒํค๋๋ฅผ ์ถ๊ฐํฉ๋๋ค.๋จ์ํ
ArrayList๋ก ๋ณ๊ฒฝํ๊ฑฐ๋, ํฅํ ๋ฉํฐ์ค๋ ๋ ์ฒ๋ฆฌ๊ฐ ํ์ํด์ง ๊ฒฝ์ฐ๋ฅผ ๋๋นํ์ฌ ๋ช ํํ ์๋๋ฅผ ๋ฐ์ํ ์ปฌ๋ ์ (์:ConcurrentHashMap๊ธฐ๋ฐ ๋๋ ๋ช ์์ ๋๊ธฐํ)์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingRepositoryImpl.java (1)
14-55: LGTM!๊ตฌํ์ด
WeeklyRankingRepositoryImpl๊ณผ ์ผ๊ด์ฑ ์๊ฒ ์์ฑ๋์์ต๋๋ค. QueryDSL์ ์ฌ์ฉํ ์กฐํ ๋ก์ง๊ณผ ํธ๋์ญ์ ์ฒ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.apps/commerce-api/src/test/java/com/loopers/application/batch/MonthlyRankingJobTest.java (1)
30-33: ํ ์คํธ ๊ตฌ์ฑ ์ ์ ํจ
@SpringBootTest์@ActiveProfiles("test")์กฐํฉ์ผ๋ก ํตํฉ ํ ์คํธ ํ๊ฒฝ์ด ์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.@Sql์ ํตํ ํ ์ด๋ธ ์ด๊ธฐํ์@BeforeEach์์์ ๋ฐ์ดํฐ ์ ๋ฆฌ๋ก ํ ์คํธ ๊ฒฉ๋ฆฌ๊ฐ ๋ณด์ฅ๋ฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingRepositoryImpl.java (1)
15-56: LGTM!
MonthlyRankingRepositoryImpl๊ณผ ์ผ๊ด๋ ํจํด์ผ๋ก ๊ตฌํ๋์ด ์์ต๋๋ค. QueryDSL์ ํ์ฉํ ์กฐํ์ ํ์ด์ง๋ค์ด์ , ํธ๋์ญ์ ์ฒ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
24-33: N+1 ๋ฌธ์ ๋ฐฉ์ง๋ฅผ ์ํ ๋ฐฐ์น ๋ก๋ฉ ๊ตฌํ ์ ์ ํจ์ํ๊ณผ ๋ธ๋๋ ์ ๋ณด๋ฅผ ํ ๋ฒ์ ์กฐํํ์ฌ N+1 ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๋ ์ค๊ณ๊ฐ ์ ๋์ด ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/weekly/WeeklyRankingJpaRepository.java (1)
16-18: JPQL LIMIT/OFFSET ๊ตฌ๋ฌธ์ ํ์ฌ ํ๋ก์ ํธ์์ ์ ์ ์ผ๋ก ์ง์๋ฉ๋๋คSpring Boot 3.4.4๋ Hibernate 6.6.2.Final์ ์ฌ์ฉํ๋ฉฐ, Hibernate 6.0๋ถํฐ JPQL์์ ๋ค์ดํฐ๋ธํ๊ฒ LIMIT๊ณผ OFFSET ํค์๋๋ฅผ ์ง์ํฉ๋๋ค. ๋ฐ๋ผ์ ํด๋น ์ฟผ๋ฆฌ๋ ์ ํจํ๋ฉฐ, ์ด ๋ฉ์๋๋ RankingService.java์์ ์ค์ ๋ก ์ฌ์ฉ ์ค์ ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/batch/WeeklyRankingJobConfig.java (3)
58-65: ๊ฐ์ค์น ํฉ์ด 1.0์ด ์๋๋๋ค - ์๋๋ ์ค๊ณ์ธ์ง ํ์ธ ํ์ํ์ฌ ์ค์ ๋ ๊ฐ์ค์น์ ํฉ์ 0.1 + 0.2 + 0.6 = 0.9์ ๋๋ค. ์ด๊ฒ์ด ์๋๋ ์ค๊ณ์ธ์ง ํ์ธํด ์ฃผ์ธ์. ๋ง์ฝ ์ ๊ทํ๋ ์ ์๊ฐ ํ์ํ๋ค๋ฉด ๊ฐ์ค์น ํฉ์ 1.0์ผ๋ก ๋ง์ถ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ ๋๋ค.
75-86:RankingScoreProcessor์ธ์คํด์ค ์์ฑ ์@Valueํ๋ ์ฃผ์ ์์ ํ์ธ ํ์
weeklyRankingStep()๋ฉ์๋ ๋ด์์new RankingScoreProcessor(viewWeight, likeWeight, orderWeight)๋ฅผ ํธ์ถํ ๋,@Value์ด๋ ธํ ์ด์ ์ผ๋ก ์ฃผ์ ๋๋ ํ๋๋ค์ด ์ด๋ฏธ ์ด๊ธฐํ๋์ด ์์ด์ผ ํฉ๋๋ค. Spring์ ๋น ๋ผ์ดํ์ฌ์ดํด์@Bean๋ฉ์๋ ํธ์ถ ์์ ์๋ ํ๋ ์ฃผ์ ์ด ์๋ฃ๋์ด ์์ผ๋ฏ๋ก ์ ์ ๋์ํ์ง๋ง,weeklyRankingStep()์ด ๋ค๋ฅธ ๋น์์ ์ง์ ํธ์ถ๋๋ ๊ฒฝ์ฐ ๋ฌธ์ ๊ฐ ๋ ์ ์์ต๋๋ค.
91-114: Reader ์ค์ ์ ์ ์ ํ์ง๋ง, ๋น ์ค์ฝํ ๊ณ ๋ ค ํ์
JdbcPagingItemReader๋ statefulํ๋ฏ๋ก ๊ธฐ๋ณธ singleton ์ค์ฝํ์์ ๋์ ์คํ ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ํ์ฌprivate๋ฉ์๋๋ก ๋งค๋ฒ ์ ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ฏ๋ก ๊ด์ฐฎ์ง๋ง, ํฅํ@Bean์ผ๋ก ๋ณ๊ฒฝ ์@StepScope๋ฅผ ์ ์ฉํด์ผ ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
42-56: ๊ธฐ๊ฐ ํ์ ๋ณ ๋ผ์ฐํ ๋ก์ง์ด ๊น๋ํ๊ฒ ๊ตฌํ๋์์ต๋๋ค.
switchํํ์์ ์ฌ์ฉํ period type ๋ผ์ฐํ ์ด ๋ช ํํ๊ณ , ๊ฐ ๊ธฐ๊ฐ ํ์ ์ ๋ง๋ ์๋น์ค ๋ฉ์๋๋ฅผ ํธ์ถํ๋ ๊ตฌ์กฐ๊ฐ ์ ์ค๊ณ๋์์ต๋๋ค.apps/commerce-api/src/test/resources/db/init-ranking-tables.sql (1)
37-46: ํ ์ด๋ธ ๊ตฌ์กฐ๊ฐ ์ ์ ํฉ๋๋ค.
product_metricsํ ์ด๋ธ์ ๋ฐฐ์น job์ ์์ค ๋ฐ์ดํฐ๋ก์ ์ ํฉํ ์คํค๋ง๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค.product_id๋ฅผ PK๋ก ์ฌ์ฉํ๊ณ ๋ฉํธ๋ฆญ ์ปฌ๋ผ๋ค์ ๊ธฐ๋ณธ๊ฐ์ด 0์ผ๋ก ์ค์ ๋ ๊ฒ์ด ์ข์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/batch/dto/ProductMetricsDto.java
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/application/batch/dto/RankedProductDto.java
Show resolved
Hide resolved
...ommerce-api/src/main/java/com/loopers/application/batch/processor/RankingScoreProcessor.java
Outdated
Show resolved
Hide resolved
| public ExitStatus afterStep(StepExecution stepExecution) { | ||
| String targetDateParam = stepExecution.getJobParameters() | ||
| .getString("targetDate"); | ||
|
|
||
| if (targetDateParam == null) { | ||
| log.error("[WeeklyRanking] targetDate ํ๋ผ๋ฏธํฐ๊ฐ ์์ต๋๋ค"); | ||
| return ExitStatus.FAILED; | ||
| } | ||
|
|
||
| LocalDate targetDate = LocalDate.parse(targetDateParam); | ||
| LocalDate weekStartDate = PeriodUtils.getWeekStartDate(targetDate); | ||
| LocalDate weekEndDate = PeriodUtils.getWeekEndDate(targetDate); | ||
|
|
||
| log.info("[WeeklyRanking] Step ์๋ฃ - ์ฃผ๊ฐ: {} ~ {}", weekStartDate, weekEndDate); | ||
|
|
||
| // 1. ๊ธฐ์กด ์ฃผ๊ฐ ๋ญํน ๋ฐ์ดํฐ ์ญ์ | ||
| weeklyRankingRepository.deleteByWeekStartDate(weekStartDate); | ||
| log.info("[WeeklyRanking] ๊ธฐ์กด ์ฃผ๊ฐ ๋ญํน ๋ฐ์ดํฐ ์ญ์ ์๋ฃ: {}", weekStartDate); |
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.
๋์ ์คํ ์ ๋ฐ์ดํฐ ์์ค ์ํ - ์ญ์ ํ ์ ์ฅ ์ฌ์ด์ ๊ฒฝํฉ ์กฐ๊ฑด
deleteByWeekStartDate() ํธ์ถ ํ saveAll() ํธ์ถ ์ ์ ๋ค๋ฅธ job ์ธ์คํด์ค๊ฐ ๊ฐ์ ์ฃผ๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๋ฉด ๋ฐ์ดํฐ๊ฐ ์์ค๋ ์ ์์ต๋๋ค. RunIdIncrementer๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ๋์ผ ํ๋ผ๋ฏธํฐ๋ก ์ฌ์คํ์ด ๊ฐ๋ฅํฉ๋๋ค.
๋์ ์คํ ๋ฐฉ์ง๋ฅผ ์ํ ๋ฐฉ์์ ๊ณ ๋ คํด ์ฃผ์ธ์:
- DB ๋ ๋ฒจ์ ์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด ์ถ๊ฐ (
week_start_date,rank_position) - ๋๋
JobParametersIncrementer๋์ ์ค๋ณต ์คํ ๋ฐฉ์ง ๋ก์ง ์ถ๊ฐ
| // 3. ์ ๋ ฌ (์ ์ ๋ด๋ฆผ์ฐจ์) | ||
| Collections.sort(collectedItems); |
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 . -type f -name "*.java" | xargs grep -l "InMemoryRankingCollector"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 373
๐ Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/application/batch/writer/InMemoryRankingCollector.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1472
๐ Script executed:
head -n 180 ./apps/commerce-api/src/main/java/com/loopers/application/batch/WeeklyRankingJobConfig.java | tail -n 20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 820
Collections.sort()๋ InMemoryRankingCollector์ ๋ด๋ถ ๋ฆฌ์คํธ๋ฅผ ์ง์ ์์ ํฉ๋๋ค
InMemoryRankingCollector์ @Getter ์ด๋
ธํ
์ด์
์ ๋ด๋ถ collectedItems ๋ฆฌ์คํธ์ ๋ํ ์ง์ ์ฐธ์กฐ๋ฅผ ๋ฐํํฉ๋๋ค. Collections.sort()๊ฐ ์ด ์ฐธ์กฐ๋ฅผ ์์ ํ๋ฏ๋ก collector์ ๋ด๋ถ ์ํ๊ฐ ๋ณ๊ฒฝ๋ฉ๋๋ค. getCollectedItems()์์ ๋ฐฉ์ด์ ๋ณต์ฌ๋ณธ์ ๋ฐํํ๋๋ก ์์ ํ๊ฑฐ๋, ํธ์ถ ์์ ์์ new ArrayList<>(collector.getCollectedItems())๋ก ๋ณต์ฌ๋ณธ์ ์์ฑํ์ธ์.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/batch/WeeklyRankingJobConfig.java
around lines 171-172, Collections.sort(collectedItems) mutates the internal list
returned by InMemoryRankingCollector#getCollectedItems because the getter
exposes the internal reference; fix by avoiding in-place modification โ either
change InMemoryRankingCollector#getCollectedItems to return a defensive copy
(e.g., new ArrayList<>(collectedItems) or Collections.unmodifiableList(copy))
or, at this call site, sort a copy: create a new
ArrayList<>(collector.getCollectedItems()) and call Collections.sort on that
copy so the collector's internal state is not mutated.
| @Modifying | ||
| @Transactional | ||
| void deleteByMonthYear(String monthYear); |
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:
rg -n "deleteByMonthYear|deleteByWeekStartDate" --type=java -B5 -A5Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 10779
๐ Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.java | sed -n '140,180p'Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2044
๐ Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.java | sed -n '80,140p'Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2812
๐ Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/application/batch/MonthlyRankingJobConfig.java | sed -n '1,85p'Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 4033
๐ Script executed:
rg -n "\.deleteByMonthYear\(" --type=java -B2 -A2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 767
๐ Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingJpaRepository.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1124
๋ฆฌํฌ์งํ ๋ฆฌ ๋ฉ์๋์์ @Transactional ์ ๊ฑฐ ๊ถ์ฅ
@Transactional์ด ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด ์์ชฝ์ ์ค๋ณต์ผ๋ก ์ ์ธ๋์ด ์์ต๋๋ค. StepExecutionListener.afterStep()์์ ํธ์ถ๋๋ ๋ฉ์๋์ด๋ฏ๋ก, ํธ๋์ญ์
์ ์๋น์ค ๊ณ์ธต์ด๋ ๋ฐฐ์น ์ค์ ๋ ๋ฒจ์์ ๊ด๋ฆฌํ๋ ๊ฒ์ด ๋ ์ ์ ํฉ๋๋ค. ๋ฆฌํฌ์งํ ๋ฆฌ ๋ฉ์๋์ @Transactional์ ์ ๊ฑฐํ๊ณ , ํ์์ ํธ์ถ ๊ณ์ธต์์ ๊ด๋ฆฌํ์ธ์.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/monthly/MonthlyRankingJpaRepository.java
around lines 19-21, remove the @Transactional annotation from the repository
method declaration (keep @Modifying) so the repository does not declare its own
transaction; let the service layer or the batch StepExecutionListener manage the
transaction. If there is an implementation or another interface duplicating
@Transactional, remove the duplicate there as well and ensure callers (service
or batch config) wrap the deleteByMonthYear(String monthYear) call in an
appropriate @Transactional boundary.
| @RestController | ||
| @RequestMapping("/api/v1/admin/batch") | ||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| public class BatchJobController { |
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 the BatchJobController file
find . -type f -name "BatchJobController.java" | head -5Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 178
๐ Script executed:
#!/bin/bash
# Search for Spring Security configuration files
find . -type f -name "*Security*.java" -o -name "*Config*.java" | grep -i security | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 261
๐ Script executed:
#!/bin/bash
# Look for WebSecurityConfigurerAdapter or SecurityFilterChain patterns
rg -l "SecurityFilterChain|WebSecurityConfigurerAdapter|@EnableWebSecurity" --type=java | head -10Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 158
๐ Script executed:
#!/bin/bash
# Search for admin path security configurations
rg -n -C5 "admin|ADMIN" --type=java -g '*Security*' -g '*Config*' | head -50Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
๐ Script executed:
#!/bin/bash
# Read SecurityConfig.java to see admin path security configuration
cat -n ./apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1348
๐ Script executed:
#!/bin/bash
# Also read the full BatchJobController.java to check for any security annotations
cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3329
/admin/** ์๋ํฌ์ธํธ์ ์ธ์ฆ ์ค์ ํ์
ํ์ฌ SecurityConfig์์ anyRequest().permitAll()๋ก ๋ชจ๋ ์์ฒญ์ ํ๊ฐํ๊ณ ์์ด, /api/v1/admin/batch ์๋ํฌ์ธํธ๊ฐ ์ธ์ฆ ์์ด ๊ณต๊ฐ๋์ด ์์ต๋๋ค. ๊ด๋ฆฌ์ ๊ธฐ๋ฅ์ด๋ฏ๋ก ์ ์ ํ ์ธ์ฆ/์ธ๊ฐ ์ค์ ์ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
SecurityConfig์ authorizeHttpRequests ๋ฉ์๋๋ฅผ ์์ ํ์ฌ /api/v1/admin/** ๊ฒฝ๋ก์ authenticated() ๋๋ ํน์ ๊ถํ ๊ฒ์ฆ์ ์ถ๊ฐํ์ธ์.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/interfaces/api/batch/BatchJobController.java
around lines 14 to 18, the controller is mapped to /api/v1/admin/batch and is
currently exposed because SecurityConfig uses anyRequest().permitAll(); update
SecurityConfig.authorizeHttpRequests() to require authentication for the admin
path (e.g., add an antMatcher or requestMatchers for "/api/v1/admin/**" with
.authenticated() or .hasRole("ADMIN") before the anyRequest rule), ensure order
of matchers places the admin rule above the permissive catch-all, and keep
CSRF/session settings consistent with API auth.
๐ Summary
์ฃผ๊ฐ/์๊ฐ ์ํ ๋ญํน ์ง๊ณ ์์คํ ์ ๊ตฌํํ์ต๋๋ค.
์ฃผ์ ๊ตฌํ ๋ด์ฉ:
๋ฐฐ์น ์ฒ๋ฆฌ ํ๋ก์ฐ:
๐ฌ Review Points
1.QueryDSL ๋ฒํฌ DELETE vs JPA deleteBy ์ ํ
ํ์ฌ ๊ตฌํ (QueryDSL ๋ฒํฌ DELETE):
JPA deleteBy ๋ฐฉ์:
์ง๋ฌธ:
@Query+@Modifying(clearAutomatically = true)๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ ์์ ํ๊ฐ์?2.StepExecutionListener์์ DELETE + SAVE ํธ๋์ญ์ ๋ถ๋ฆฌ ๋ฌธ์
ํ์ฌ ๊ตฌํ:
StepExecutionListener์์ ํธ์ถ:
์ง๋ฌธ:
@Transactional replaceWeeklyRanking()๋ฉ์๋๋ฅผ ๋ง๋๋ ๊ฒ์ด ๋์๊น์?3. ๋ญํน๋ฐ์ดํฐ๋ฅผ ์์ ๋, ๋ชจ๋ ๋ฐ์ดํฐ์ ๋ํ ๋ญํน์ ์์ง๋ ์์ ๊ฒ ๊ฐ์๋ฐ, Top-100์ด๋ฉด 100๊ฐ๋ง ์ ์ฅํ๋ ์์ผ๋ก ๊ตฌํํ์๋์?
โ Checklist
๐งฑ Spring Batch
๐งฉ Ranking API
Summary by CodeRabbit
์ถ์ ๋ ธํธ
์๋ก์ด ๊ธฐ๋ฅ
์ธํ๋ผ
โ๏ธ Tip: You can customize this high-level summary in your review settings.