-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-10] Collect, Stack, Zip #244
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: Kimjipang
Are you sure you want to change the base?
Conversation
* spring batch ํ์ฉ - chunk ๊ธฐ๋ฐ ์ฒ๋ฆฌ
- @RequestParam์ผ๋ก ๋ ์ง ๋ฐ์ดํฐ(date) ์ถ๊ฐ
Walkthrough์ผ์ผ, ์ฃผ๊ฐ, ์๊ฐ ์ํ ๋ญํน์ ์ฒ๋ฆฌํ๋ ์๋ก์ด ์๋น์ค ๊ณ์ธต์ ๋์ ํ๊ณ , RankingFacade๋ฅผ ๋ฆฌํฉํ ๋งํ์ฌ ์๋น์ค ์์์ผ๋ก ๋ณ๊ฒฝํ์ต๋๋ค. API ์๋ํฌ์ธํธ๋ฅผ ์ผ๋ฐํํ๊ณ , ๋ฐฐ์น ์์ ์ผ๋ก ์ฃผ๊ฐ/์๊ฐ ๋ญํน ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ์ต๋๋ค. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller as RankingV1Controller
participant Facade as RankingFacade
participant Service as DailyRankingService
participant Redis as Redis ZSET
participant DTO as Response DTO
Client->>Controller: GET /rankings?date=20260101&period=DAILY&page=1&size=20
Controller->>Facade: getProductRanking(date, DAILY, 1, 20)
Facade->>Service: getDailyRanking(20260101, DAILY, 1, 20)
Service->>Redis: reverseRangeWithScores(ranking:all:20260101, 0, 19)
Redis-->>Service: List<ZSetOperations.TypedTuple>
Service->>Service: ์ ์๋ณ ์ ๋ ฌ ๋ฐ ์์ ๊ณ์ฐ
Service->>DTO: ProductRankingPageResponse ์์ฑ
DTO-->>Service: ํ์ด์ง๋ค์ด์
๋ฉํ๋ฐ์ดํฐ ํฌํจ
Service-->>Facade: ProductRankingPageResponse
Facade-->>Controller: ProductRankingPageResponse
Controller-->>Client: ApiResponse ๋ฐํ
sequenceDiagram
participant Batch as Batch Job
participant Config as BatchConfig
participant Reader as ProductMetricReader
participant Processor as ProductMetricProcessor
participant DB as Database
participant Writer as ProductMetricWeeklyWriter
participant Target as mv_product_rank_weekly
Batch->>Config: weeklyProductRankingJob ์คํ
Config->>Config: deleteWeeklyMvStep ์คํ
Config->>DB: DELETE FROM mv_product_rank_weekly
DB-->>Config: ์ญ์ ์๋ฃ
Config->>Config: aggregateWeeklyRankStep ์คํ
Config->>Reader: ์ฃผ๊ฐ ์งํ ์ฝ๊ธฐ (JdbcPagingItemReader)
Reader->>DB: SELECT ์ง๊ณ ์งํ
DB-->>Reader: ProductMetricsAgg ์ฒญํฌ
Reader-->>Processor: ์ฒญํฌ ์ ๋ฌ
Processor->>Processor: ๊ฐ์ค ์ ์ ๊ณ์ฐ<br/>(view: 0.1, like: 0.3, sales: 0.6)
Processor-->>Writer: ProductRankRow ๋ณํ
Writer->>Writer: MapSqlParameterSource ๋ฐฐ์น ๊ตฌ์ฑ
Writer->>Target: batchUpdate (INSERT)
Target-->>Writer: ์ฝ์
์๋ฃ
Writer-->>Batch: ๋ฐฐ์น ์ฐ๊ธฐ ์๋ฃ
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touchesโ Failed checks (1 warning, 1 inconclusive)
โ Passed checks (1 passed)
โจ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 11
โป๏ธ Duplicate comments (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (1)
21-89: MonthlyRankingService์ ์ฝ๋ ๊ตฌ์กฐ ๋์ผ์ด ๋ฉ์๋๋ MonthlyRankingService์
getMonthlyTop100๊ณผ ๊ตฌ์กฐ์ ์ผ๋ก ๋์ผํฉ๋๋ค. ์์ MonthlyRankingService ๋ฆฌ๋ทฐ(Lines 21-89)์์ ์ธ๊ธํ๋ฏ์ด, ๊ณตํต ๋ก์ง์ ์ถ์ํํ์ฌ ๋ ์๋น์ค ๊ฐ ์ค๋ณต์ ์ ๊ฑฐํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.Based on learnings, ์ด๋ฏธ MonthlyRankingService์์ ๋์ผํ ๋ฆฌํฉํ ๋ง ์ ์์ ํ์ผ๋ฏ๋ก ์ฌ๊ธฐ์๋ ๊ฐ๋จํ ์ฐธ์กฐ๋ง ํฉ๋๋ค.
๐งน Nitpick comments (14)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (1)
19-21: ์๋น์ค ๊ณ์ธต๊ณผ ์ค๋ณต๋ ๊ฒ์ฆ ๋ก์ง
safePage์safeSize๊ฒ์ฆ์ดWeeklyRankingService์ 9-10๋ฒ ๋ผ์ธ์์๋ ๋์ผํ๊ฒ ์ํ๋๊ณ ์์ต๋๋ค. ๋ฆฌํฌ์งํ ๋ฆฌ ๊ณ์ธต์์์ ๋ฐฉ์ด์ ๊ฒ์ฆ์ ์ ์ฉํ์ง๋ง, ์๋น์ค์์ ์ด๋ฏธ ์ ๊ทํ๋ ๊ฐ์ ๋ฐ๋๋ค๋ฉด ์ค๋ณต ์ฒ๋ฆฌ์ ๋๋ค.๐ ์ ์: ๋ฆฌํฌ์งํ ๋ฆฌ๋ ์ด๋ฏธ ๊ฒ์ฆ๋ ํ๋ผ๋ฏธํฐ๋ฅผ ์ ๋ขฐ
@Override public List<WeeklyRankingMv> getWeeklyTop100(String endDate, String startDate, int page, int size) { - int safePage = Math.max(page, 1); - int safeSize = Math.max(size, 1); - - long offset = (long) (safePage - 1) * safeSize; + long offset = (long) (page - 1) * size; return queryFactory .selectFrom(weeklyRankingMv) .where( weeklyRankingMv.startDate.eq(startDate) .and(weeklyRankingMv.endDate.eq(endDate))) .orderBy( weeklyRankingMv.score.desc(), weeklyRankingMv.productId.asc() ) .offset(offset) - .limit(safeSize) + .limit(size) .fetch(); }apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.java (1)
49-50: ์คํธ๋ฆผ ๋ด๋ถ์์ ๋ฐ๋ณต์ ์ธ ZonedDateTime.now() ํธ์ถ๊ฐ ์์ดํ ๋ง๋ค
ZonedDateTime.now()๋ฅผ ํธ์ถํ๋ฉด ๋์ฉ๋ ๋ฐฐ์น์์ ๋ถํ์ํ ์ค๋ฒํค๋๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์คํธ๋ฆผ ์ธ๋ถ์์ ํ ๋ฒ๋ง ํธ์ถํ๋ ๊ฒ์ด ๋ ํจ์จ์ ์ ๋๋ค.๐ ์ ์: ํ์์คํฌํ๋ฅผ ํ ๋ฒ๋ง ์์ฑ
@Override public void write(Chunk<? extends ProductRankRow> chunk) { + ZonedDateTime now = ZonedDateTime.now(); MapSqlParameterSource[] batch = chunk.getItems().stream() .map(item -> new MapSqlParameterSource() .addValue("startDate", startDate) .addValue("endDate", endDate) .addValue("productId", item.productId()) .addValue("viewCountSum", item.viewCountSum()) .addValue("likeCountSum", item.likeCountSum()) .addValue("salesVolumeSum", item.salesVolumeSum()) .addValue("score", item.score()) - .addValue("createdAt", ZonedDateTime.now()) - .addValue("updatedAt", ZonedDateTime.now())) + .addValue("createdAt", now) + .addValue("updatedAt", now)) .toArray(MapSqlParameterSource[]::new); jdbcTemplate.batchUpdate(SQL, batch); }apps/commerce-batch/src/main/java/com/loopers/batch/dto/ProductMetricsAgg.java (1)
3-8: productId ํ์ ์ผ๊ด์ฑ ๊ฒํ
productId๋Long๋ํผ ํ์ ์ ์ฌ์ฉํ์ง๋ง ๋ค๋ฅธ ํ๋๋ค์long๊ธฐ๋ณธํ์ ์ฌ์ฉํฉ๋๋ค.ProductMetricReader(line 36-40)์์rs.getLong("ref_product_id")๋ก ๋งคํ๋๋ฉฐ, SQL ๊ฒฐ๊ณผ๊ฐ NULL์ผ ๊ฒฝ์ฐ 0์ผ๋ก ๋ฐํ๋ ์ ์์ต๋๋ค. productId๊ฐ ์ค์ ๋ก NULL์ผ ์ ์๋ค๋ฉดLong์ด ์ ์ ํ์ง๋ง, ๊ทธ๋ ์ง ์๋ค๋ฉด ์ผ๊ด์ฑ์ ์ํดlong์ผ๋ก ํต์ผํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ์ ์: NULL์ด ๋ถ๊ฐ๋ฅํ๋ค๋ฉด ๊ธฐ๋ณธํ์ผ๋ก ํต์ผ
public record ProductMetricsAgg( - Long productId, + long productId, long sumLike, long sumView, long sumSales ) { }apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingRepository.java (1)
6-6: ๋ฉ์๋ ์ด๋ฆ "Top100"์ด page/size ํ๋ผ๋ฏธํฐ์ ๋ชจ์๋ฉ๋๋ค๋ฉ์๋ ์ด๋ฆ์ด
getWeeklyTop100์ด์ง๋งpage์sizeํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์ 100๊ฐ ์ด์ ๋๋ ์ดํ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ ์ ์์ต๋๋ค. ์ด๋ ์ฌ์ฉ์์๊ฒ ํผ๋์ ์ค ์ ์์ต๋๋ค.์๋ฅผ ๋ค์ด,
getWeeklyTop100(date, lastWeek, 0, 50)์ ํธ์ถํ๋ฉด 50๊ฐ๋ง ๋ฐํ๋๊ณ ,getWeeklyTop100(date, lastWeek, 1, 100)์ 101~200๋ฒ์งธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๊ฒ ๋ฉ๋๋ค.๐ ๋ช ํํ ๋ฉ์๋ ์ด๋ฆ์ผ๋ก ๋ณ๊ฒฝ ์ ์
- List<WeeklyRankingMv> getWeeklyTop100(String date, String lastWeek, int page, int size); + List<WeeklyRankingMv> getWeeklyRankings(LocalDate endDate, LocalDate startDate, int page, int size);apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
21-27:dateํ๋ผ๋ฏธํฐ์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฆ ๋ฐ ๊ธฐ๋ณธ๊ฐ ์ค์ ๊ณ ๋ ค
dateํ๋ผ๋ฏธํฐ๊ฐ ํ์๊ฐ์ผ๋ก ์ค์ ๋์ด ์์ผ๋ ํ์ ๊ฒ์ฆ์ด ์์ต๋๋ค.DailyRankingService,WeeklyRankingService,MonthlyRankingService์์BASIC_ISO_DATEํ์(์: "20260102")์ ๊ธฐ๋ํ๊ณ ์์ด, ์๋ชป๋ ํ์์ ๋ ์ง๊ฐ ์ ๋ฌ๋๋ฉดDateTimeParseException์ด ๋ฐ์ํ ์ ์์ต๋๋ค.๋ํ
size์pageํ๋ผ๋ฏธํฐ์ ๊ธฐ๋ณธ๊ฐ์ด ์์ด ํด๋ผ์ด์ธํธ๊ฐ ์๋ตํ๋ฉด ์์ฒญ์ด ์คํจํฉ๋๋ค.๐ ๊ธฐ๋ณธ๊ฐ ์ถ๊ฐ ๋ฐ ๋ ์ง ํ์ ๊ฒ์ฆ ์ ์
public ApiResponse<RankingV1Dto.ProductRankingPageResponse> getProductRanking( - @RequestParam String date, + @RequestParam(defaultValue = "#{T(java.time.LocalDate).now().format(T(java.time.format.DateTimeFormatter).BASIC_ISO_DATE)}") String date, @RequestParam(defaultValue = "DAILY") Period period, - @RequestParam int size, - @RequestParam int page + @RequestParam(defaultValue = "20") int size, + @RequestParam(defaultValue = "1") int page ) {apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java (1)
18-18:Periodํ๋ผ๋ฏธํฐ ์ฌ์ฉ๋์ง ์์
periodํ๋ผ๋ฏธํฐ๊ฐ ๋ฉ์๋์ ์ ๋ฌ๋์ง๋ง, ์๋ต์์๋ ํญ์Period.DAILY๋ก ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค (๋ผ์ธ 29, 37, 60). ํ๋ผ๋ฏธํฐ๋ฅผ ์ ๊ฑฐํ๊ฑฐ๋ ์ ๋ฌ๋ ๊ฐ์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ผ๊ด์ฑ ์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java (1)
49-50: ๋ฐฐ์น ๋ด ํ์์คํฌํ ์ผ๊ด์ฑ ๊ณ ๋ ค
ZonedDateTime.now()๊ฐ ๊ฐ ์์ดํ ๋งคํ ์ ํธ์ถ๋์ด ๋ฐฐ์น ๋ด ๋ ์ฝ๋ ๊ฐ ๋ฏธ์ธํ ์๊ฐ ์ฐจ์ด๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์ผ๊ด๋ ํ์์คํฌํ๊ฐ ํ์ํ๋ค๋ฉด ๋ฐฐ์น ์์ ์์ ์ ์๊ฐ์ ์บก์ฒํ์ฌ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ผ๊ด๋ ํ์์คํฌํ ์ฌ์ฉ ์์
@Override public void write(Chunk<? extends ProductRankRow> chunk) { + ZonedDateTime now = ZonedDateTime.now(); MapSqlParameterSource[] batch = chunk.getItems().stream() .map(item -> new MapSqlParameterSource() .addValue("startDate", startDate) .addValue("endDate", endDate) .addValue("productId", item.productId()) .addValue("viewCountSum", item.viewCountSum()) .addValue("likeCountSum", item.likeCountSum()) .addValue("salesVolumeSum", item.salesVolumeSum()) .addValue("score", item.score()) - .addValue("createdAt", ZonedDateTime.now()) - .addValue("updatedAt", ZonedDateTime.now())) + .addValue("createdAt", now) + .addValue("updatedAt", now)) .toArray(MapSqlParameterSource[]::new); jdbcTemplate.batchUpdate(SQL, batch); }apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.java (1)
20-24: Exception ๋์ ๊ตฌ์ฒด์ ์ธ ์์ธ ํ์ ์ฌ์ฉ ๊ถ์ฅ
throws Exception์ ๋๋ฌด ๊ด๋ฒ์ํฉ๋๋ค.provider.getObject()์์ ๋ฐ์ํ ์ ์๋ ๊ตฌ์ฒด์ ์ธ ์์ธ ํ์ ์ ๋ช ์ํ๋ฉด ํธ์ถ์๊ฐ ๋ ๋ช ํํ๊ฒ ์์ธ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.java (1)
18-19: ํ๋ผ๋ฏธํฐ ์์ ํผ๋ ๋ฐ ๋ฉ์๋๋ช ์คํด ์์ง๋ ๊ฐ์ง ๊ฐ์ ์ฌํญ์ด ์์ต๋๋ค:
ํ๋ผ๋ฏธํฐ ์์:
getMonthlyTop100(String endDate, String startDate, ...)์์๊ฐ ์ง๊ด์ ์ด์ง ์์ต๋๋ค. ์ผ๋ฐ์ ์ผ๋กstartDate๊ฐ ๋จผ์ ์ค๋ ๊ฒ์ด ์์ฐ์ค๋ฝ์ต๋๋ค.WeeklyRankingRepositoryImpl๋ ๋์ผํ ํจํด์ ์ฌ์ฉํ๊ณ ์์ด ์ผ๊ด์ฑ์ ์์ง๋ง, ํธ์ถ๋ถ์์ ์ค์ํ๊ธฐ ์ฝ์ต๋๋ค.๋ฉ์๋๋ช :
getMonthlyTop100์ด๋ผ๋ ์ด๋ฆ์ด์ง๋ง ์ค์ ๋ก๋sizeํ๋ผ๋ฏธํฐ์ ๋ฐ๋ผ ๊ฐ์๊ฐ ๊ฒฐ์ ๋ฉ๋๋ค.getMonthlyRankings๋๋getMonthlyRankingPage๊ฐ ๋ ์ ํํ ์ ์์ต๋๋ค.๐ ํ๋ผ๋ฏธํฐ ์์ ์์ ์ ์
@Override - public List<MonthlyRankingMv> getMonthlyTop100(String endDate, String startDate, int page, int size) { + public List<MonthlyRankingMv> getMonthlyTop100(String startDate, String endDate, int page, int size) {์ฐธ๊ณ : ์ด ๋ณ๊ฒฝ์ ์ธํฐํ์ด์ค
MonthlyRankingRepository์ ํธ์ถ๋ถMonthlyRankingService์๋ ์ ์ฉ๋์ด์ผ ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java (1)
21-89: WeeklyRankingService์์ ์ฝ๋ ์ค๋ณต ์ ๊ฑฐ ๊ถ์ฅ์ด ๋ฉ์๋๋ WeeklyRankingService์
getWeeklyTop100๊ณผ ๊ตฌ์กฐ๊ฐ ๊ฑฐ์ ๋์ผํฉ๋๋ค(ํ์ด์ง๋ค์ด์ ๋ก์ง, ๋น ์๋ต ์ฒ๋ฆฌ, ๋ญํน ๋งคํ ๋ฑ). ๊ณตํต ๋ก์ง์ ์ถ์ํํ์ฌ ์ค๋ณต์ ์ ๊ฑฐํ๋ฉด ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ฉ๋๋ค.๐ก ๋ฆฌํฉํ ๋ง ๋ฐฉํฅ ์ ์
๊ณตํต ํ์ด์ง๋ค์ด์ ๋ฐ ๋ญํน ๋งคํ ๋ก์ง์ ์ถ์ถํ ์ ์์ต๋๋ค:
// ๊ณตํต ์ ํธ๋ฆฌํฐ ๋๋ ์ถ์ ๋ฒ ์ด์ค ํด๋์ค์ ์ถ๊ฐ protected <T extends RankingMv> RankingV1Dto.ProductRankingPageResponse buildRankingResponse( String date, Period period, String startDate, String endDate, int page, int size, long totalElements, List<T> rows, Function<T, Long> productIdExtractor, Function<T, BigDecimal> scoreExtractor ) { // ๊ณตํต ๋ก์ง ๊ตฌํ }์ด๋ ๊ฒ ํ๋ฉด Weekly/Monthly ์๋น์ค์์ ์ค๋ณต๋ ํ์ด์ง๋ค์ด์ ๋ฐ DTO ๋งคํ ์ฝ๋๋ฅผ ์ ๊ฑฐํ ์ ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingMv.java (1)
19-23: ๋ ์ง ํ๋๋ฅผ String์ผ๋ก ์ ์ฅ
startDate์endDate๊ฐ String ํ์ ์ ๋๋ค. BASIC_ISO_DATE ํฌ๋งท(yyyyMMdd)์ ์ฌ์ฉํ๋ ๊ฒ์ผ๋ก ๋ณด์ด์ง๋ง, ํ์ ์์ ์ฑ๊ณผ DB ์์ค์ ๋ ์ง ์ฐ์ฐ์ ์ํด LocalDate ๋๋ DB์ DATE ํ์ ์ฌ์ฉ์ ๊ณ ๋ คํ ์ ์์ต๋๋ค.ํ์ฌ ๊ตฌ์กฐ์ ์ฅ์ : String ๊ธฐ๋ฐ ๋ ์ง๋ ์ ๋ ฌ๊ณผ ๋น๊ต๊ฐ ๊ฐ๋จํ๊ณ ํฌ๋งท ์ ์ด๊ฐ ๋ช ํํฉ๋๋ค.
๋์ ๊ณ ๋ ค์ฌํญ: ๋ง์ฝ ํฅํ ๋ ์ง ๋ฒ์ ์ฟผ๋ฆฌ๋ ๋ ์ง ์ฐ์ฐ์ด ๋ณต์กํด์ง๋ค๋ฉด, LocalDate๋ก ๋ณ๊ฒฝํ๋ ๊ฒ์ด ๋ ์์ ํ ์ ์์ต๋๋ค. ํ์ง๋ง ํ์ฌ ์ฌ์ฉ ํจํด(๋จ์ equality ๋น๊ต)์์๋ String๋ ์ถฉ๋ถํ ์ ํฉํฉ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/batch/config/BatchConfig.java (3)
57-74: ์ญ์ ์์ ์ ๋ํ ๋ก๊น ์ถ๊ฐ๋ฅผ ๊ถ์ฅํฉ๋๋ค.DELETE ์์ ์ ์ํฅ์ ๋ฐ์ ํ ์๋ฅผ ๋ก๊น ํ๋ฉด ์ด์ ์ ๋ฌธ์ ์ถ์ ์ ๋์์ด ๋ฉ๋๋ค.
jdbcTemplate.update()๋ ์ํฅ๋ฐ์ ํ ์๋ฅผ ๋ฐํํฉ๋๋ค.๐ ๋ก๊น ์ถ๊ฐ ์ ์
@Bean @StepScope public Tasklet deleteWeeklyMvTasklet( @Value("#{jobParameters['weeklyStartDate']}") String startDate, @Value("#{jobParameters['weeklyEndDate']}") String endDate ) { return (contribution, chunkContext) -> { String sql = """ DELETE FROM mv_product_rank_weekly WHERE start_date = :weeklyStartDate AND end_date = :weeklyEndDate """; - jdbcTemplate.update(sql, Map.of( + int deletedCount = jdbcTemplate.update(sql, Map.of( "weeklyStartDate", startDate, "weeklyEndDate", endDate )); + // log.info("Deleted {} rows from mv_product_rank_weekly for period {} ~ {}", deletedCount, startDate, endDate); return RepeatStatus.FINISHED; }; }
76-84: Fault tolerance ์ ์ฑ ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํด ์ฃผ์ธ์.ํ์ฌ chunk ์ฒ๋ฆฌ ์ค ๋จ์ผ ์์ดํ ์คํจ ์ ์ ์ฒด Step์ด ์คํจํฉ๋๋ค. ์ผ์์ ์ธ ์ค๋ฅ(๋คํธ์ํฌ, ๋ฐ๋๋ฝ ๋ฑ)์ ๋ํ retry ์ ์ฑ ์ด๋, ํน์ ์์ธ์ ๋ํ skip ์ ์ฑ ์ถ๊ฐ๋ฅผ ๊ถ์ฅํฉ๋๋ค.
๐ Fault tolerance ์ค์ ์์
@Bean public Step aggregateWeeklyRankStep(@Qualifier("weeklyProductMetricsAggReader") JdbcPagingItemReader<ProductMetricsAgg> weeklyProductMetricsAggReader) { return new StepBuilder("aggregateWeeklyRankStep", jobRepository) .<ProductMetricsAgg, ProductRankRow>chunk(1000, transactionManager) .reader(weeklyProductMetricsAggReader) .processor(productMetricProcessor) .writer(productMetricWeeklyWriter) + .faultTolerant() + .retryLimit(3) + .retry(Exception.class) // ํ์์ ๋ฐ๋ผ ํน์ ์์ธ๋ก ์ ํ .build(); }
96-148: ์ฝ๋ ์ค๋ณต ๊ฐ์ ์ ๊ณ ๋ คํด ์ฃผ์ธ์.Weekly์ Monthly ์ค์ ์ด ๊ฑฐ์ ๋์ผํ ๊ตฌ์กฐ๋ก ๋ฐ๋ณต๋ฉ๋๋ค. ํ์ฌ ์ํ๋ก๋ ๋์์ ๋ฌธ์ ๋ ์์ง๋ง, ํฅํ ์ ์ง๋ณด์ ์ helper ๋ฉ์๋๋ ํฉํ ๋ฆฌ ํจํด์ผ๋ก ๊ณตํต ๋ก์ง์ ์ถ์ถํ๋ฉด ๋ณ๊ฒฝ ์ฌํญ์ ํ ๊ณณ์์ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
๐ ๋ฆฌํฉํ ๋ง ์์ด๋์ด
// ์์: ๊ณตํต Step ์์ฑ ๋ฉ์๋ private Step createDeleteMvStep(String stepName, String tableName, String startDateParam, String endDateParam) { Tasklet tasklet = (contribution, chunkContext) -> { String sql = String.format("DELETE FROM %s WHERE start_date = :startDate AND end_date = :endDate", tableName); jdbcTemplate.update(sql, Map.of( "startDate", chunkContext.getStepContext().getJobParameters().get(startDateParam), "endDate", chunkContext.getStepContext().getJobParameters().get(endDateParam) )); return RepeatStatus.FINISHED; }; return new StepBuilder(stepName, jobRepository) .tasklet(tasklet, transactionManager) .build(); }๋จ, ํ์ฌ ๊ตฌ์กฐ๊ฐ ๋ช ํํ๊ณ ๋ ๋ฆฝ์ ์ธ ์งํ๊ฐ ๊ฐ๋ฅํ๋ฏ๋ก, ํ์ ์ ๋์ค์ ๋ฆฌํฉํ ๋งํด๋ ๋ฌด๋ฐฉํฉ๋๋ค.
๐ Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (28)
apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingV1Dto.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRankingMv.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRankingRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/Period.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingMv.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.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-batch/src/main/java/com/loopers/batch/config/BatchConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/dto/ProductMetricsAgg.javaapps/commerce-batch/src/main/java/com/loopers/batch/dto/ProductRankRow.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/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/batch/processor/ProductMetricProcessor.javaapps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.javaapps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.javaapps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java
๐ค Files with no reviewable changes (5)
- apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
- apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java
- apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
- apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java
- apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java
๐งฐ Additional context used
๐ง Learnings (3)
๐ 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/WeeklyRankingJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingRepository.java
๐ Learning: 2025-11-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product ์ํฐํฐ (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)๋ ์ ์ฆ์ผ์ด์ค๋ณ๋ก ์๋์ ์ผ๋ก ๋ค๋ฅธ ๋ฝ ์ ๋ต์ ์ฌ์ฉํ๋ค: ์ข์์ ๊ธฐ๋ฅ์๋ ๋น๊ด์ ๋ฝ(findByIdForUpdate)์, ์ฌ๊ณ ์ฐจ๊ฐ์๋ ๋๊ด์ ๋ฝ(Version + ์ฌ์๋)์ ์ฌ์ฉํ๋ค.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/batch/dto/ProductRankRow.java
๐ Learning: 2025-12-19T21:30:16.024Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/outbox/OutboxEventListener.kt:0-0
Timestamp: 2025-12-19T21:30:16.024Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template Kafka event pipeline, Like events (LikeCreatedEventV1, LikeCanceledEventV1) intentionally use aggregateType="Like" with aggregateId=productId. The aggregateId serves as a partitioning/grouping key (not a unique Like entity identifier), ensuring all like events for the same product go to the same partition for ordering guarantees and aligning with ProductStatisticService's product-based aggregation logic. Using individual like_id would scatter events across partitions and break the statistics aggregation pattern.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/batch/dto/ProductMetricsAgg.java
๐งฌ Code graph analysis (12)
apps/commerce-batch/src/main/java/com/loopers/batch/processor/ProductMetricProcessor.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.java (1)
Component(15-60)apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.java (1)
Component(15-55)apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java (1)
Component(15-56)
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/processor/ProductMetricProcessor.java (1)
Component(12-35)apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.java (1)
Component(15-60)apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.java (1)
Component(15-55)
apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (4)
apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java (1)
Component(13-62)apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
Component(9-31)apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.java (1)
Component(13-52)apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (1)
Component(13-52)
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/processor/ProductMetricProcessor.java (1)
Component(12-35)apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.java (1)
Component(15-60)apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java (1)
Component(15-56)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingMv.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRankingMv.java (1)
Entity(13-38)
apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java (5)
apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java (1)
Component(13-62)apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
Component(9-31)apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (1)
Component(14-91)apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.java (1)
Component(13-52)apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (1)
Component(13-52)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.java (4)
apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java (1)
Component(14-90)apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
Component(9-31)apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (1)
Component(14-91)apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (1)
Component(13-52)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (5)
apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java (1)
Component(13-62)apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java (1)
Component(14-90)apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (1)
Component(14-91)apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.java (1)
Component(13-52)apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (1)
Component(13-52)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingV1Dto.java (1)
RankingV1Dto(7-25)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
Component(9-31)apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (1)
Component(14-91)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRankingMv.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingMv.java (1)
Entity(13-40)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingV1Dto.java (1)
RankingV1Dto(7-25)
๐ Additional comments (18)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingRepositoryImpl.java (2)
28-29: LGTM!๋ ์ง ๋ฒ์ ํํฐ๋ง๊ณผ null ์์ ์ฒ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.
Also applies to: 45-46
19-19: ๋ฉ์๋ ํธ์ถ ์ ํ๋ผ๋ฏธํฐ ์์ ํ์ธ ํ์
WeeklyRankingRepositoryImpl๋ฉ์๋ ์๊ทธ๋์ฒ์ ํ๋ผ๋ฏธํฐ ์์(endDate,startDate)์WeeklyRankingService์์์ ํธ์ถ ํจํด์ ๊ฒ์ฆํ์ฌ ์๋ํ ์์๊ฐ ๋ง๋์ง, ๊ทธ๋ฆฌ๊ณstartDate/endDate๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๋งคํ๋๋์ง ํ์ธํด์ผ ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/ranking/Period.java (1)
1-7: LGTM!๊ฐ๋จํ๊ณ ๋ช ํํ enum ์ ์์ ๋๋ค. ๋๋ฉ์ธ ๊ณ์ธต์ ์ ์ ํ๊ฒ ์์นํ๋ฉฐ ์๋น์ค์ API ๊ณ์ธต์์ ์ ํ์ฉ๋๊ณ ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingJpaRepository.java (1)
6-7: LGTM!ํ์ค Spring Data JPA ๋ ํฌ์งํ ๋ฆฌ ํจํด์ ๋ฐ๋ฅด๊ณ ์์ผ๋ฉฐ, ์ธํ๋ผ ๊ณ์ธต์ ์ฌ๋ฐ๋ฅด๊ฒ ๋ฐฐ์น๋์์ต๋๋ค. ๊ธฐ๋ณธ CRUD ์์ ์ ์ํ ์ ์ ํ ๊ตฌํ์ ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/batch/dto/ProductRankRow.java (1)
5-11: productId ํ์ ์ ์ค์ ๋ก๋ ๋ ๋ถ๊ฐ๋ฅํ๋ฏ๋ก ๊ฒํ ๋ถํ์
ProductMetricReader์ rowMapper์์rs.getLong("ref_product_id")๋ฅผ ์ฌ์ฉํ๋ฏ๋ก, productId๋ ์ค์ ๋ก null ๊ฐ์ ๋ฐ์ง ์์ต๋๋ค. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปฌ๋ผ์ด NULL์ด๋ฉดrs.getLong()์ null์ด ์๋ 0์ ๋ฐํํฉ๋๋ค. ๋ฐ๋ผ์Longํ์ ์ ์ธ์ ํผ๋์ ์ค ์ ์์ง๋ง, NullPointerException์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฝ ์กฐ๊ฑด ์๋ฐ์ ์ค์ ์ํ์ ์์ต๋๋ค. Writers์MapSqlParameterSource๋ null ๊ฐ์ ์์ ํ๊ฒ ์ฒ๋ฆฌํฉ๋๋ค. ์ด๋ ํ์ ์ผ๊ด์ฑ ๊ฐ์ ์ฌํญ์ด์ง ๊ธฐ๋ฅ์ ๊ฒฐํจ์ด ์๋๋๋ค.Likely an incorrect or invalid review comment.
apps/commerce-batch/src/main/java/com/loopers/batch/processor/ProductMetricProcessor.java (1)
14-34: LGTM! ๊ฐ์ค์น ๊ธฐ๋ฐ ์ ์ ๊ณ์ฐ ๋ก์ง์ด ์ ๊ตฌํ๋์์ต๋๋ค.
BigDecimal์ ์ฌ์ฉํ์ฌ ๋ถ๋์์์ ์ ๋ฐ๋ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๊ณ , ๊ฐ์ค์น ํฉ๊ณ๊ฐ 1.0(0.1 + 0.3 + 0.6)์ผ๋ก ์ ํํ ๋ง์ต๋๋ค.HALF_UP๋ฐ์ฌ๋ฆผ ๋ชจ๋๋ ์ ์ ํฉ๋๋ค.ํฅํ ๊ฐ์ค์น ์กฐ์ ์ด ํ์ํ ๊ฒฝ์ฐ,
@Value์ด๋ ธํ ์ด์ ์ ํตํด properties์์ ์ฃผ์ ๋ฐ๋๋ก ํ์ฅํ ์ ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingV1Dto.java (1)
1-25: LGTM! DTO ๊ตฌ์กฐ ๊ฐ์ ์ด ์ ๋์์ต๋๋ค.ํจํค์ง๊ฐ
interfaces.api.ranking์์application.ranking์ผ๋ก ์ด๋๋์ด ๊ณ์ธต ์ํคํ ์ฒ์ ๋ ์ ํฉํด์ก์ต๋๋ค.ProductRankingPageResponse์period,startDate,endDateํ๋๊ฐ ์ถ๊ฐ๋์ด ๋ค์ํ ๋ญํน ๊ธฐ๊ฐ์ ์ง์ํ ์ ์๊ฒ ๋์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MonthlyRankingRepositoryImpl.java (1)
39-51: LGTM! countMonthly ๊ตฌํ์ด ์์ ํ๊ฒ ์์ฑ๋์์ต๋๋ค.
fetchOne()๊ฒฐ๊ณผ๊ฐnull์ผ ๊ฒฝ์ฐ๋ฅผ ์ ์ ํ ์ฒ๋ฆฌํ์ฌ0L์ ๋ฐํํฉ๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/reader/ProductMetricReader.java (1)
52-57: RowMapper ํ๋ ์์ ํ์ธ ์๋ฃ
ProductMetricsAgg์์ฑ์์ ํ๋ผ๋ฏธํฐ ์์(productId, sumLike, sumView, sumSales)๊ฐ rowMapper์์ ์ถ์ถํ๋ ์์(ref_product_id, sum_like, sum_view, sum_sales)์ ์ ํํ ์ผ์นํฉ๋๋ค. ๊ฐ์ ์์๊ฐ ์ฌ๋ฐ๋ฅด๋ฏ๋ก ์ถ๊ฐ ์์ ์ด ํ์ํ์ง ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java (1)
28-28: ์๊ฐ ๊ธฐ๊ฐ ๊ณ์ฐ ๋ฐฉ์ ๊ฒํ ํ์
minusDays(30)์ ์ฌ์ฉํ๋ฉด ํญ์ 30์ผ ๊ธฐ๊ฐ์ด ๋์ด ์ค์ ๋ฌ๋ ฅ ์(28~31์ผ)๊ณผ ์ฐจ์ด๊ฐ ๋ฐ์ํฉ๋๋ค. ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ์ด "์ง๋ 30์ผ"์ธ์ง "ํด๋น ์"์ธ์ง ํ์ธ์ด ํ์ํฉ๋๋ค. ๋ง์ฝ ๋ฌ๋ ฅ ์ ๊ธฐ์ค์ด๋ผ๋ฉดminusMonths(1)์ ๊ณ ๋ คํ์ธ์.์ฐธ๊ณ : WeeklyRankingService์์๋
minusDays(6)์ ์ฌ์ฉํ์ฌ ์ ํํ 7์ผ ์๋์ฐ๋ฅผ ๊ตฌํํ์ต๋๋ค(Line 28). Monthly๋ ๋์ผํ ํจํด(๊ณ ์ ์ผ์)์ ๋ฐ๋ฅด๋ ๊ฒ์ผ๋ก ๋ณด์ด์ง๋ง, ์๊ฐ ๋ญํน์ ๊ฒฝ์ฐ ๋ฌ๋ ฅ ์ ๊ธฐ์ค์ด ๋ ์ง๊ด์ ์ผ ์ ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (2)
16-29: ์๋น์ค ์์ ํจํด์ด ์ ๊ตฌํ๋จRankingFacade๊ฐ Period์ ๋ฐ๋ผ ์ ์ ํ ์๋น์ค๋ก ์์ํ๋ ๊ตฌ์กฐ๊ฐ ๋ช ํํฉ๋๋ค. Switch expression์ ์ฌ์ฉํ์ฌ ๊ฐ๋ ์ฑ๋ ์ข์ต๋๋ค.
Minor suggestion: Java์ sealed class๋ exhaustive switch๋ฅผ ํ์ฉํ๋ฉด default ์ผ์ด์ค๊ฐ ๋ถํ์ํด์ง ์ ์์ง๋ง, ํ์ฌ ๊ตฌ์กฐ๋ ์ถฉ๋ถํ ์์ ํฉ๋๋ค. Period enum์ ์๋ก์ด ๊ฐ์ด ์ถ๊ฐ๋๋ฉด ์ปดํ์ผ๋ฌ๊ฐ ๊ฒฝ๊ณ ํ์ง ์์ผ๋ฏ๋ก, ์๋ก์ด Period ์ถ๊ฐ ์ ์ด ๋ฉ์๋๋ฅผ ์ ๋ฐ์ดํธํ๋ ๊ฒ์ ์์ง ๋ง์ธ์.
27-27: Period enum ๊ฐ ๊ฒ์ฆ ๋ก์ง์ ์ ์ ํ๋ฉฐ default ์ผ์ด์ค๋ ์ด๋ก ์ ์คํ๋์ง ์์ต๋๋คPeriod enum์ด DAILY, WEEKLY, MONTHLY ์ธ ๊ฐ์ง ๊ฐ๋ง ์ ์๋์ด ์๊ณ , ๋ชจ๋ ์ผ์ด์ค๊ฐ switch ๋ฌธ์์ ์ฒ๋ฆฌ๋๊ณ ์์ต๋๋ค. ๋ํ RankingV1Controller์
@RequestParam(defaultValue = "DAILY")์ค์ ์ผ๋ก ์ธํด null ๊ฐ์ด ์ ์คํธ๋ฆผ์์ ์ฐจ๋จ๋๋ฉฐ, Spring์ enum ๋ฐ์ธ๋ฉ ๊ฒ์ฆ์ด ์ ํจํ์ง ์์ Period ๊ฐ์ ๊ฑฐ๋ถํฉ๋๋ค. ๋ฐ๋ผ์ default ์ผ์ด์ค๋ ์ด๋ก ์ ์คํ๋ ์ ์์ต๋๋ค. ๊ทธ๋ผ์๋ ๋ถ๊ตฌํ๊ณ ๋ฐฉ์ด์ ํ๋ก๊ทธ๋๋ฐ ์ฐจ์์์ default ์ผ์ด์ค์ ์กด์ฌ๋ ์ ์ ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java (1)
28-28: ์ฃผ๊ฐ ๊ธฐ๊ฐ ๊ณ์ฐ์ด ์ฌ๋ฐ๋ฆ
minusDays(6)์ ์ฌ์ฉํ์ฌ endDate ํฌํจ 7์ผ ์๋์ฐ๋ฅผ ์ ํํ ๊ตฌํํ์ต๋๋ค. ์: endDate๊ฐ 2024-01-07์ด๋ฉด startDate๋ 2024-01-01์ด ๋์ด 7์ผ ๊ธฐ๊ฐ์ด ๋ฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRankingMv.java (1)
13-38: WeeklyRankingMv์ ์ผ๊ด๋ ๊ตฌ์กฐ๋ก ๊ตฌํ๋จMonthlyRankingMv๋ WeeklyRankingMv์ ๋์ผํ ํจํด์ ๋ฐ๋ฅด๋ฉฐ, ํ ์ด๋ธ ์ด๋ฆ(
mv_product_rank_monthly)๋ง ๋ค๋ฆ ๋๋ค. ์ด๋ฌํ ์ผ๊ด์ฑ์ ์ ์ง๋ณด์์ ๋์์ด ๋ฉ๋๋ค.์ฐธ๊ณ : WeeklyRankingMv ๋ฆฌ๋ทฐ(Lines 13-40)์์ ์ธ๊ธํ ์ฌํญ๋ค(batch writer ๊ฒ์ฆ, String ๋ ์ง ํ๋)์ด ์ด ์ํฐํฐ์๋ ๋์ผํ๊ฒ ์ ์ฉ๋ฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingMv.java (1)
13-40: ๋ฐฐ์น ์ฐ๊ธฐ๋ JPA๊ฐ ์๋ ์ง์ SQL์ ์ฌ์ฉํ๋ฏ๋ก ์ํฐํฐ ๊ตฌ์กฐ ๋ณ๊ฒฝ ๋ถํ์๊ฒ์ฆ ๊ฒฐ๊ณผ,
ProductMetricWeeklyWriter์ProductMetricMonthlyWriter๋NamedParameterJdbcTemplate.batchUpdate()๋ฅผ ์ฌ์ฉํ์ฌProductRankRowDTO ๊ฐ์ฒด๋ฅผ ์ง์ SQL INSERT ๋ฌธ์ผ๋ก ๋ณํํฉ๋๋ค. ๋ฐฐ์น ์ฐ๊ธฐ ๊ณผ์ ์์WeeklyRankingMv์ํฐํฐ ๊ฐ์ฒด๊ฐ ์ธ์คํด์คํ๋์ง ์์ผ๋ฏ๋ก, setter๋ ๋น๋๊ฐ ํ์ํ์ง ์์ต๋๋ค.์ด ์ํฐํฐ๋ Materialized View๋ฅผ ๋ํ๋ด๋ฉฐ ์ฝ๊ธฐ ์ ์ฉ์ผ๋ก ์ค๊ณ๋ ๊ฒ์ด ์ฌ๋ฐ๋ฆ ๋๋ค. ํ์ฌ ๊ตฌ์กฐ(protected no-arg ์์ฑ์, @Getter๋ง ํฌํจ)๋ JPA ํ์ด๋๋ ์ด์ ๊ณผ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ๋งคํ์ ์ต์ ํ๋์ด ์์ต๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/batch/config/BatchConfig.java (3)
29-40: LGTM!Spring Batch ์ค์ ์ ์ํ ์์กด์ฑ ์ฃผ์ ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค. Lombok
@RequiredArgsConstructor๋ฅผ ํ์ฉํ ์์ฑ์ ์ฃผ์ ๋ฐฉ์์ด ์ ์ ํฉ๋๋ค.
41-48: LGTM!
@StepScope๋ฅผ ํ์ฉํ job parameter ์ฃผ์ ์ด ์ ์ ํฉ๋๋ค. Step ์คํ ์์ ์ ํ๋ผ๋ฏธํฐ๊ฐ ๋ฐ์ธ๋ฉ๋์ด ๋์ ๋ ์ง ๋ฒ์ ์กฐํ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
86-93: LGTM!
RunIdIncrementer๋ฅผ ์ฌ์ฉํ์ฌ ๋์ผ ํ๋ผ๋ฏธํฐ๋ก Job ์ฌ์คํ์ด ๊ฐ๋ฅํฉ๋๋ค. Delete โ Aggregate ์์์ Step ์คํ ํ๋ฆ์ด ์ ์ ํฉ๋๋ค.
| for (var t : tuples) { | ||
| String member = t.getValue(); | ||
| Double score = t.getScore(); | ||
| if (member == null || score == null) continue; | ||
|
|
||
| items.add(new RankingV1Dto.ProductRankingResponse( | ||
| rank++, | ||
| Long.parseLong(member), | ||
| score | ||
| )); | ||
| } |
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.
Long.parseLong(member)์์ ์์ธ ๋ฐ์ ๊ฐ๋ฅ์ฑ
Redis์์ ๊ฐ์ ธ์จ member ๊ฐ์ด ์ ํจํ ์ซ์๊ฐ ์๋ ๊ฒฝ์ฐ NumberFormatException์ด ๋ฐ์ํ ์ ์์ต๋๋ค. ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ด ๋ณด์ฅ๋๋ค๋ฉด ๋ฌธ์ ๊ฐ ์์ง๋ง, ๋ฐฉ์ด์ ์ฝ๋ฉ์ ์ํด ์์ธ ์ฒ๋ฆฌ๋ฅผ ๊ณ ๋ คํด ๋ณด์ธ์.
๐ ๋ฐฉ์ด์ ์์ธ ์ฒ๋ฆฌ ์ ์
for (var t : tuples) {
String member = t.getValue();
Double score = t.getScore();
if (member == null || score == null) continue;
+ Long productId;
+ try {
+ productId = Long.parseLong(member);
+ } catch (NumberFormatException e) {
+ continue; // ๋๋ ๋ก๊น
ํ ๊ฑด๋๋ฐ๊ธฐ
+ }
+
items.add(new RankingV1Dto.ProductRankingResponse(
rank++,
- Long.parseLong(member),
+ productId,
score
));
}๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.java
around lines 46 to 56, parsing the Redis member string with
Long.parseLong(member) can throw NumberFormatException for non-numeric or
malformed values; wrap the parse in a defensive check (e.g., trim and validate
with a numeric check or regex) or a try-catch around Long.parseLong, log a
warning including the offending member and skip adding that entry (do not
increment rank for skipped items), and ensure null/blank members are also
handled consistently so the loop continues safely without crashing.
|
|
||
| String startDate = end.minusDays(30).format(BASIC); | ||
|
|
||
| System.out.println("startDate = " + startDate); |
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.
ํ๋ก๋์ ๋ก๊น ๋ฐฉ์์ผ๋ก ๊ต์ฒด ํ์
System.out.println์ ํ๋ก๋์
ํ๊ฒฝ์ ์ ํฉํ์ง ์์ต๋๋ค. ์ ์ ํ ๋ก๊น
ํ๋ ์์ํฌ(SLF4J)๋ฅผ ์ฌ์ฉํ์ฌ ๋ก๊ทธ ๋ ๋ฒจ๊ณผ ์ถ๋ ฅ ์์น๋ฅผ ์ ์ดํ ์ ์๋๋ก ๋ณ๊ฒฝํ์ธ์.
๐ ์ ์ํ๋ ์์ ์ฌํญ
ํด๋์ค ์๋จ์ logger ์ ์ธ์ ์ถ๊ฐ:
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
@Component
@RequiredArgsConstructor
public class MonthlyRankingService {๊ทธ๋ฆฌ๊ณ System.out.println์ ๋ก๊ฑฐ๋ก ๊ต์ฒด:
- System.out.println("startDate = " + startDate);
+ log.debug("Monthly ranking calculation - startDate: {}, endDate: {}", startDate, endDate);Committable suggestion skipped: line range outside the PR's diff.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java
around line 30, replace the System.out.println call with SLF4J logging: add a
private static final Logger logger =
LoggerFactory.getLogger(MonthlyRankingService.class) declaration at the top of
the class (ensure org.slf4j.Logger and org.slf4j.LoggerFactory are imported),
then change the println to logger.debug or logger.info("startDate = {}",
startDate) as appropriate for the message severity so logs are managed by the
configured logging framework.
| ); | ||
| } | ||
|
|
||
| List<MonthlyRankingMv> rows = monthlyRankingRepository.getMonthlyTop100(endDate, startDate, safePage, safeSize); |
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 repository interfaces and service implementation
echo "=== ํ์ผ ์์น ํ์ธ ==="
find . -type f -name "*RankingRepository*" -o -name "MonthlyRankingService.java" | head -20
echo -e "\n=== MonthlyRankingService ํ์ผ ์ฐพ๊ธฐ ==="
fd MonthlyRankingService.java
echo -e "\n=== ๋ฆฌํฌ์งํ ๋ฆฌ ํ์ผ๋ค ์ฐพ๊ธฐ ==="
fd "RankingRepository" --type fRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1111
๐ Script executed:
#!/bin/bash
# MonthlyRankingService์ line 32์ 65 ํ์ธ
echo "=== MonthlyRankingService.java ๋ด์ฉ (๋ผ์ธ 25-75) ==="
cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java | sed -n '25,75p'Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2190
๐ Script executed:
#!/bin/bash
# ๋ฆฌํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค ๋ฉ์๋ ์๊ทธ๋์ฒ ํ์ธ
echo "=== MonthlyRankingRepository getMonthlyTop100 ๋ฉ์๋ ==="
rg "getMonthlyTop100" -A 2 -B 1
echo -e "\n=== MonthlyRankingRepository countMonthly ๋ฉ์๋ ==="
rg "countMonthly" -A 2 -B 1
echo -e "\n=== WeeklyRankingRepository getWeeklyTop100 ๋ฉ์๋ ==="
rg "getWeeklyTop100" -A 2 -B 1
echo -e "\n=== WeeklyRankingRepository countWeekly ๋ฉ์๋ ==="
rg "countWeekly" -A 2 -B 1Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 9011
๋ฆฌํฌ์งํ ๋ฆฌ ๋ฉ์๋ ๊ฐ ํ๋ผ๋ฏธํฐ ์์ ์ผ๊ด์ฑ ํต์ผ ํ์
Line 65์ getMonthlyTop100(endDate, startDate, ...)์ Line 32์ countMonthly(startDate, endDate) ํธ์ถ์์ ํ๋ผ๋ฏธํฐ ์์๊ฐ ๋ฐ๋์
๋๋ค. MonthlyRankingRepository ์ธํฐํ์ด์ค์์ ๋ ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ ์์๊ฐ ์ผ๊ด๋์ง ์์ผ๋ฏ๋ก (getMonthlyTop100์ endDate ๋จผ์ , countMonthly๋ startDate ๋จผ์ ), WeeklyRankingRepository์ ๋์ผํ ํจํด์ผ๋ก ํต์ผํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค. ๊ฐ์ ๋
ผ๋ฆฌ๋ฅผ ๋ค๋ฃจ๋ ๋ฉ์๋๋ค์์ ํ๋ผ๋ฏธํฐ ์์๋ฅผ ์ผ๊ด๋๊ฒ ์ ์งํ๋ฉด ํผ๋์ ๋ฐฉ์งํ ์ ์์ต๋๋ค.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.java
around line 65, the call getMonthlyTop100(endDate, startDate, ...) uses the
opposite parameter order to countMonthly(startDate, endDate) (called at line
32); make the parameter ordering consistent with WeeklyRankingRepository and
other methods by changing getMonthlyTop100's signature and all its usages to
accept (startDate, endDate, ...) instead of (endDate, startDate), then update
the repository implementation and any callers (including this line 65) to pass
startDate first and endDate second so both methods use the same
startDate,endDate ordering.
|
|
||
| String startDate = end.minusDays(6).format(BASIC); | ||
|
|
||
| System.out.println("startDate = " + startDate); |
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.
ํ๋ก๋์ ๋ก๊น ๋ฐฉ์์ผ๋ก ๊ต์ฒด ํ์
MonthlyRankingService์ ๋์ผํ ์ด์: System.out.println์ ํ๋ก๋์
ํ๊ฒฝ์ ์ ํฉํ์ง ์์ต๋๋ค. SLF4J ๋ก๊ฑฐ๋ฅผ ์ฌ์ฉํ์ธ์.
๐ ์ ์ํ๋ ์์ ์ฌํญ
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
@Component
@RequiredArgsConstructor
public class WeeklyRankingService {- System.out.println("startDate = " + startDate);
+ log.debug("Weekly ranking calculation - startDate: {}, endDate: {}", startDate, endDate);Committable suggestion skipped: line range outside the PR's diff.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.java
around line 30, replace the System.out.println call with SLF4J logging: add a
private static final org.slf4j.Logger logger =
org.slf4j.LoggerFactory.getLogger(WeeklyRankingService.class); import the SLF4J
classes, then change the print statement to logger.info("startDate = {}",
startDate); ensuring you use parameterized logging and remove the
System.out.println.
| List<MonthlyRankingMv> getMonthlyTop100(String endDate, String startDate, int safePage, int safeSize); | ||
| long countMonthly(String startDate, String endDate); |
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.
์น๋ช ์ : ํ๋ผ๋ฏธํฐ ์์ ๋ถ์ผ์น๋ก ์ธํ ์ ์ฌ์ ๋ฒ๊ทธ ์ํ
getMonthlyTop100์ endDate๋ฅผ startDate๋ณด๋ค ๋จผ์ ๋ฐ์ง๋ง, countMonthly๋ startDate๋ฅผ endDate๋ณด๋ค ๋จผ์ ๋ฐ์ต๋๋ค. ์ด๋ฌํ ํ๋ผ๋ฏธํฐ ์์์ ๋ถ์ผ์น๋ ๊ฐ๋ฐ์๊ฐ ํธ์ถ ์ ๋ ์ง๋ฅผ ์๋ชป๋ ์์๋ก ์ ๋ฌํ์ฌ ์๋ชป๋ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ ์ฌ๊ฐํ ๋ฒ๊ทธ๋ฅผ ์ ๋ฐํ ์ ์์ต๋๋ค.
๋ํ WeeklyRankingRepository.getWeeklyTop100๊ณผ๋ ํ๋ผ๋ฏธํฐ ์์๊ฐ ๋ค๋ฆ
๋๋ค.
๐ ์ผ๊ด๋ ํ๋ผ๋ฏธํฐ ์์๋ก ์์ ์ ์
- List<MonthlyRankingMv> getMonthlyTop100(String endDate, String startDate, int safePage, int safeSize);
+ List<MonthlyRankingMv> getMonthlyTop100(String startDate, String endDate, int safePage, int safeSize);
long countMonthly(String startDate, String endDate);๋ ๋์ ๊ฐ์ ์ (String ๋ ์ง ๋ฌธ์ ๋ ํจ๊ป ํด๊ฒฐ):
+import java.time.LocalDate;
+
public interface MonthlyRankingRepository {
- List<MonthlyRankingMv> getMonthlyTop100(String endDate, String startDate, int safePage, int safeSize);
- long countMonthly(String startDate, String endDate);
+ List<MonthlyRankingMv> getMonthlyRankings(LocalDate startDate, LocalDate endDate, int page, int size);
+ long countMonthly(LocalDate startDate, LocalDate endDate);
}๐ Committable suggestion
โผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| List<MonthlyRankingMv> getMonthlyTop100(String endDate, String startDate, int safePage, int safeSize); | |
| long countMonthly(String startDate, String endDate); | |
| List<MonthlyRankingMv> getMonthlyTop100(String startDate, String endDate, int safePage, int safeSize); | |
| long countMonthly(String startDate, String endDate); |
| import java.util.List; | ||
|
|
||
| public interface WeeklyRankingRepository { | ||
| List<WeeklyRankingMv> getWeeklyTop100(String date, String lastWeek, int page, int size); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String ํ์ ๋ ์ง ํ๋ผ๋ฏธํฐ๋ ํ์ ์์ ์ฑ ๋ฌธ์ ๋ฅผ ์ผ๊ธฐํฉ๋๋ค
๋ ์ง๋ฅผ String ํ์
์ผ๋ก ๋ฐ์ผ๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค:
- ๋ฐํ์ ํ์ฑ ์ค๋ฅ ๊ฐ๋ฅ์ฑ
- ๋ ์ง ํ์ ๋ถ์ผ์น (์: "2025-01-02" vs "20250102")
- ์ปดํ์ผ ํ์ ํ์ ๊ฒ์ฆ ๋ถ๊ฐ
- ๋ ์ง ์ฐ์ฐ ์ ๋งค๋ฒ ํ์ฑ ํ์
๋ํ, ํ๋ผ๋ฏธํฐ ๋ช
๋ช
์ด ์ผ๊ด๋์ง ์์ต๋๋ค: date์ lastWeek๋ ์๋ฏธ๊ฐ ๋ช
ํํ์ง ์์ผ๋ฉฐ, countWeekly์ startDate/endDate์๋ ๋ถ์ผ์นํฉ๋๋ค.
๐ LocalDate ์ฌ์ฉ์ผ๋ก ๊ฐ์ ์ ์
- List<WeeklyRankingMv> getWeeklyTop100(String date, String lastWeek, int page, int size);
- long countWeekly(String startDate, String endDate);
+ List<WeeklyRankingMv> getWeeklyTop100(LocalDate endDate, LocalDate startDate, int page, int size);
+ long countWeekly(LocalDate startDate, LocalDate endDate);ํ์ผ ์๋จ์ import ์ถ๊ฐ:
import java.util.List;
+import java.time.LocalDate;Committable suggestion skipped: line range outside the PR's diff.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/domain/ranking/WeeklyRankingRepository.java
around line 6, replace the two String date parameters with java.time.LocalDate
and standardize names to match other APIs (e.g., startDate and endDate) to
ensure type safety; update the method signature to use LocalDate startDate,
LocalDate endDate (keeping page and size), add the necessary import
(java.time.LocalDate) at the top, and then propagate this change to all
implementations, callers, and unit/integration tests to parse/convert input
strings to LocalDate at the boundary (e.g., controller) rather than inside the
repository.
| public interface WeeklyRankingJpaRepository extends JpaRepository<WeeklyRankingMv, Long> { | ||
|
|
||
| } |
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 usage of WeeklyRankingJpaRepository
rg -nP --type=java 'WeeklyRankingJpaRepository' .Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 281
๐ Script executed:
#!/bin/bash
# Search for WeeklyRankingRepositoryImpl and related classes
rg -nP --type=java 'WeeklyRankingRepository' .Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 883
WeeklyRankingJpaRepository ์ ๊ฑฐ ํ์
WeeklyRankingJpaRepository๋ ์ ์ธ๋์ด ์์ง๋ง ์ฝ๋๋ฒ ์ด์ค ์ด๋์์๋ ์ฃผ์
๋๊ฑฐ๋ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ์ค์ ์ฌ์ฉ๋๋ ๊ฒ์ WeeklyRankingRepository ๋๋ฉ์ธ ์ธํฐํ์ด์ค์ด๋ฉฐ, WeeklyRankingRepositoryImpl์ด ์ด๋ฅผ ๊ตฌํํ๊ณ ์์ต๋๋ค. ๋ฐ๋ผ์ WeeklyRankingJpaRepository๋ ์ ๊ฑฐํ๋ฉด ๋ฉ๋๋ค.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/WeeklyRankingJpaRepository.java
around lines 6 to 8, remove the unused JpaRepository interface declaration:
WeeklyRankingJpaRepository is never injected or referenced (the code uses
WeeklyRankingRepository and its implementation WeeklyRankingRepositoryImpl), so
delete this file and any imports referencing it; run a project-wide search to
confirm no remaining usages and update build/config if needed.
| public interface RankingV1ApiSpec { | ||
|
|
||
| ApiResponse<RankingV1Dto.ProductRankingPageResponse> getDailyProductRanking(int size, int page); | ||
| ApiResponse<RankingV1Dto.ProductRankingPageResponse> getProductRanking(String date, Period period, int size, int page); |
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 "RankingV1ApiSpec.java" | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 178
๐ Script executed:
find . -type f -path "*commerce-api*" -name "*.java" | grep -i ranking | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1807
๐ Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 526
๐ Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1518
๐ Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1492
๐ Script executed:
rg -A 10 "getProductRanking" ./apps/commerce-api/src/main/java/com/loopers/ --type javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3461
๐ Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/application/ranking/DailyRankingService.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2833
๐ Script executed:
rg "LocalDate|DateTimeFormatter|parse" ./apps/commerce-api/src/main/java/com/loopers/application/ranking/ -A 2 --type javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3076
๐ Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/application/ranking/WeeklyRankingService.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3570
๐ Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/application/ranking/MonthlyRankingService.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3575
Controller์ date ํ๋ผ๋ฏธํฐ์ @DateTimeFormat ์ ๋
ธํ
์ด์
์ถ๊ฐ ๋ฐ ํ์ ๋ฌธ์ํ ํ์
@RequestParam String date์ ํ์ ์ง์ ์ด ์์ด ๋ค์ ๋ฌธ์ ๋ฐ์:
- WEEKLY์ MONTHLY ๊ธฐ๊ฐ ์กฐํ ์
LocalDate.parse(date, BASIC_ISO_DATE)ํธ์ถ๋ก "yyyyMMdd" ํ์ ๊ฐ์ ๋์ง๋ง ๋ฌธ์ํ๋์ง ์์ - ์๋ชป๋ ํ์(์: "yyyy-MM-dd") ์
๋ ฅ ์
DateTimeParseException๋ฐ์ - OpenAPI/Swagger ์คํ์์ ๋ ์ง ํ์ ๋ช ์ ๋ถ๊ฐ๋ฅ
RankingV1Controller์ getProductRanking ๋ฉ์๋์์ @DateTimeFormat(pattern = "yyyyMMdd")๋ฅผ ์ถ๊ฐํ๊ณ , JavaDoc์ผ๋ก "yyyyMMdd ํ์์ ๋ ์ง" ๋ช
์ ํ์.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java
around line 9, the date request parameter lacks a declared format causing
LocalDate.parse(date, BASIC_ISO_DATE) to fail for inputs like "yyyy-MM-dd" and
leaving the OpenAPI spec without a date pattern; add @DateTimeFormat(pattern =
"yyyyMMdd") to the controller/interface method parameter for date and update the
method JavaDoc to state "date must be in yyyyMMdd format" so the runtime
parsing, exception behavior and generated OpenAPI documentation clearly reflect
the expected format.
| public class ProductMetricMonthlyWriter implements ItemWriter<ProductRankRow> { | ||
| private final NamedParameterJdbcTemplate jdbcTemplate; | ||
|
|
||
| @Value("#{jobParameters['monthlyStartDate']}") | ||
| private String startDate; | ||
|
|
||
| @Value("#{jobParameters['monthlyEndDate']}") | ||
| private String endDate; | ||
|
|
||
| private static final String SQL = """ | ||
| INSERT INTO mv_product_rank_monthly | ||
| (start_date, end_date, ref_product_id, | ||
| view_count_sum, like_count_sum, sales_volume_sum, | ||
| score, created_at, updated_at, deleted_at) | ||
| VALUES | ||
| (:startDate, :endDate, :productId, | ||
| :viewCountSum, :likeCountSum, :salesVolumeSum, | ||
| :score, :createdAt, :updatedAt, NULL) | ||
| """; | ||
|
|
||
| @Override | ||
| public void write(Chunk<? extends ProductRankRow> chunk) { | ||
| MapSqlParameterSource[] batch = chunk.getItems().stream() | ||
| .map(item -> new MapSqlParameterSource() | ||
| .addValue("startDate", startDate) | ||
| .addValue("endDate", endDate) | ||
| .addValue("productId", item.productId()) | ||
| .addValue("viewCountSum", item.viewCountSum()) | ||
| .addValue("likeCountSum", item.likeCountSum()) | ||
| .addValue("salesVolumeSum", item.salesVolumeSum()) | ||
| .addValue("score", item.score()) | ||
| .addValue("createdAt", ZonedDateTime.now()) | ||
| .addValue("updatedAt", ZonedDateTime.now())) | ||
| .toArray(MapSqlParameterSource[]::new); | ||
|
|
||
| jdbcTemplate.batchUpdate(SQL, batch); | ||
| } |
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
ProductMetricWeeklyWriter์ ๋์ ์ฝ๋ ์ค๋ณต
์ด ํด๋์ค๋ ProductMetricWeeklyWriter์ ๊ฑฐ์ ๋์ผํ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๊ณ ์์ผ๋ฉฐ, ํ
์ด๋ธ ์ด๋ฆ๊ณผ ํ๋ผ๋ฏธํฐ ํค๋ง ๋ค๋ฆ
๋๋ค. ์ถ์ ๋ฒ ์ด์ค ํด๋์ค๋ ํ
ํ๋ฆฟ ๋ฉ์๋ ํจํด์ ์ฌ์ฉํ์ฌ ์ค๋ณต์ ์ ๊ฑฐํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
๐ ์ ์: ์ถ์ ๋ฒ ์ด์ค ํด๋์ค ์ถ์ถ
// ์ ํ์ผ: AbstractProductMetricWriter.java
@RequiredArgsConstructor
public abstract class AbstractProductMetricWriter implements ItemWriter<ProductRankRow> {
protected final NamedParameterJdbcTemplate jdbcTemplate;
protected abstract String getStartDateParam();
protected abstract String getEndDateParam();
protected abstract String getTableName();
@Override
public void write(Chunk<? extends ProductRankRow> chunk) {
ZonedDateTime now = ZonedDateTime.now();
String sql = String.format("""
INSERT INTO %s
(start_date, end_date, ref_product_id,
view_count_sum, like_count_sum, sales_volume_sum,
score, created_at, updated_at, deleted_at)
VALUES
(:startDate, :endDate, :productId,
:viewCountSum, :likeCountSum, :salesVolumeSum,
:score, :createdAt, :updatedAt, NULL)
""", getTableName());
MapSqlParameterSource[] batch = chunk.getItems().stream()
.map(item -> new MapSqlParameterSource()
.addValue("startDate", getStartDateParam())
.addValue("endDate", getEndDateParam())
.addValue("productId", item.productId())
.addValue("viewCountSum", item.viewCountSum())
.addValue("likeCountSum", item.likeCountSum())
.addValue("salesVolumeSum", item.salesVolumeSum())
.addValue("score", item.score())
.addValue("createdAt", now)
.addValue("updatedAt", now))
.toArray(MapSqlParameterSource[]::new);
jdbcTemplate.batchUpdate(sql, batch);
}
}
// ProductMetricMonthlyWriter๋ ๊ฐ์ํ
@Component
@StepScope
public class ProductMetricMonthlyWriter extends AbstractProductMetricWriter {
@Value("#{jobParameters['monthlyStartDate']}")
private String startDate;
@Value("#{jobParameters['monthlyEndDate']}")
private String endDate;
public ProductMetricMonthlyWriter(NamedParameterJdbcTemplate jdbcTemplate) {
super(jdbcTemplate);
}
@Override
protected String getStartDateParam() { return startDate; }
@Override
protected String getEndDateParam() { return endDate; }
@Override
protected String getTableName() { return "mv_product_rank_monthly"; }
}๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricMonthlyWriter.java
(lines 18-54) and its Weekly counterpart, there is high duplication; extract a
new AbstractProductMetricWriter base class that holds the
NamedParameterJdbcTemplate, the generic write(Chunk) implementation (builds SQL
using an abstract getTableName(), constructs the batch params using
ZonedDateTime.now() once, and calls jdbcTemplate.batchUpdate), and declares
abstract getters getStartDateParam(), getEndDateParam(), and getTableName();
make jdbcTemplate protected final and move common SQL/template logic into the
base class; update ProductMetricMonthlyWriter to be a thin subclass (keep
@Component and @StepScope, keep @Value fields for
monthlyStartDate/monthlyEndDate, call super(jdbcTemplate) in constructor, and
implement the three abstract getters returning the injected fields and
"mv_product_rank_monthly"); apply the same refactor to ProductMetricWeeklyWriter
(return weekly params and table) so both classes only provide parameter values
and table name.
| private static final String SQL = """ | ||
| INSERT INTO mv_product_rank_weekly | ||
| (start_date, end_date, ref_product_id, | ||
| view_count_sum, like_count_sum, sales_volume_sum, | ||
| score, created_at, updated_at, deleted_at) | ||
| VALUES | ||
| (:startDate, :endDate, :productId, | ||
| :viewCountSum, :likeCountSum, :salesVolumeSum, | ||
| :score, :createdAt, :updatedAt, NULL) | ||
| """; |
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.
๋ฐฐ์น ์ฌ์คํ ์ ์ค๋ณต ๋ฐ์ดํฐ ์ฝ์ ์ํ
INSERT ๋ฌธ์ ์ค๋ณต ์ฒ๋ฆฌ ๋ก์ง(ON CONFLICT ๋๋ ON DUPLICATE KEY UPDATE)์ด ์์ด, ๋์ผํ ๊ธฐ๊ฐ์ ๋ํด ๋ฐฐ์น๊ฐ ์ฌ์คํ๋๋ฉด ์ค๋ณต ๋ ์ฝ๋๊ฐ ์ฝ์
๋ฉ๋๋ค. ๋ฐฐ์น ์์
์ ๋ฉฑ๋ฑ์ฑ(idempotency)์ ๋ณด์ฅํ๊ธฐ ์ํด upsert ํจํด์ ๊ณ ๋ คํด ์ฃผ์ธ์.
๐ PostgreSQL์ ๊ฒฝ์ฐ ON CONFLICT ์ฌ์ฉ ์์
private static final String SQL = """
INSERT INTO mv_product_rank_weekly
(start_date, end_date, ref_product_id,
view_count_sum, like_count_sum, sales_volume_sum,
score, created_at, updated_at, deleted_at)
VALUES
(:startDate, :endDate, :productId,
:viewCountSum, :likeCountSum, :salesVolumeSum,
:score, :createdAt, :updatedAt, NULL)
+ ON CONFLICT (start_date, end_date, ref_product_id)
+ DO UPDATE SET
+ view_count_sum = EXCLUDED.view_count_sum,
+ like_count_sum = EXCLUDED.like_count_sum,
+ sales_volume_sum = EXCLUDED.sales_volume_sum,
+ score = EXCLUDED.score,
+ updated_at = EXCLUDED.updated_at
""";์ฐธ๊ณ : ProductMetricMonthlyWriter.java์๋ ๋์ผํ ์์ ์ด ํ์ํฉ๋๋ค.
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/writer/ProductMetricWeeklyWriter.java
around lines 27-36, the SQL INSERT lacks conflict handling so re-running the
batch can insert duplicate rows for the same period/product; update the
statement to an upsert by adding an ON CONFLICT clause keyed on the unique
constraint (probably start_date, end_date, ref_product_id) that performs DO
UPDATE setting the aggregate columns (view_count_sum, like_count_sum,
sales_volume_sum, score) and timestamps (updated_at) from the excluded values,
ensure created_at remains unchanged on update, and mirror the same change in
ProductMetricMonthlyWriter.java.
๐ Summary
Spring Batch
๋ญํน API
๐ฌ Review Points
Q1).
ํ์ฌ์์ 1๋ ์ 2๋ฒ ์ฝ๋ฌผ ๋ฐ์ดํฐ(.asc ํ์ฅ์ ํ์ผ)๋ฅผ ์ด๋ ์น์ฌ์ดํธ์์ ๋ค์ด๋ก๋ ๋ฐ์ ํ ์ด๋ธ์ ์ ์ฅํด์ฃผ๋ ์์ ์ spring batch๋ก ๊ตฌํํด๋ ์ํ์ ๋๋ค.
ํ์ฌ ์ ์ ์ ๋ฌด๊ฐ ํ๋ฉ๋ ์ํ์ด๊ธด ํ์ง๋ง ์ถํ์ ํด๋น ํ์ผ์ ์น์ฌ์ดํธ์์ ๋ค์ด๋ก๋ ๋ฐ์ ํ ์ด๋ธ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅ์ํค๊ณ S3์ ํ์ผ๋ค์ ์ ๋ก๋ ํด์ผ ํ๋๋ฐ์!
์ด๋ฅผ ์๋ํํ๋ค๋ฉด ์ด๋ป๊ฒ ํ ์ ์์๊น์? ์ฐพ์๋ณด๋ AWS Lambda ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด ์๊ธฐ๋ ํ๋๋ ์ด๋ค ๋ฐฉ๋ฒ์ด ์ข์๊น์?!
Q2).
์ฃผ๊ฐ/์๊ฐ ๋ญํน ์ง๊ณ ํ๋ก์ธ์ค๊ฐ ๋ค์๊ณผ ๊ฐ์๋ฐ ์๋ง์ ๋ฐฉ๋ฒ์ผ๊น์?
product_metricsํ ์ด๋ธ์ ์ง๊ณ๋ฅผ ํ ๋ ์ง๊ณ๋ ์ง(metric_date)๋ฅผ ๊ฐ์ด ์ ์ฅํ๊ณ ์์ต๋๋ค. ๊ทธ๋์ (product_id, metric_date) ๊ฐ ๊ณ ์ ํ ๊ฐ์ธ๋ฐ์.Reader
product_metricsํ ์ด๋ธ์์ productId ๋ณ๋ก 7์ผ ๊ฐ์ row๋ฅผ ์กฐํํ ๋ view_count, like_count, sales_volume๋ฅผ sum()ํด์ readProcessor
Writer
Q3).
์ผ๊ฐ/์ฃผ๊ฐ/์๊ฐ ๋ญํน ์ ๋ณด๋ฅผ ๋ฐํํด์ฃผ๋ API์ ๋ํ ์ง๋ฌธ์ ๋๋ค.
์๊ตฌ ์ฌํญ์ด ์๋ ํฌ์ธํธ๋ฅผ ํ๋๋ก ํต์ผํ๊ณ ๊ฐ๊ฐ์ ๊ตฌ๋ถํ ์ ์๋ ๋ฌด์์ธ๊ฐ๋ก ์ผ๊ฐ/์ฃผ๊ฐ/์๊ฐ ๋ญํน ๋ฐ์ดํฐ๋ฅผ ๋ฐํํด์ผ ํ๋ ๊ฒ์ผ๋ก ์ดํดํ๊ณ ๊ทธ๋ ๊ฒ ์งํ์ ํ์์ต๋๋ค.
์๋ํฌ์ธํธ๊ฐ ๋์ผํ๋ค๋ณด๋ ์๋ต ๋ณธ๋ฌธ์ ๋ง์ถฐ์ฃผ์ด์ผ ํ๊ณ ๊ฐ๊ฐ์ ๊ตฌ๋ถํ๊ณ ์ period๋ฅผ requestParam์ผ๋ก ๋ฃ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ Facade์์ period๊ฐ ์ด๋ค ๊ฐ์ธ์ง์ ๋ฐ๋ผ ๋ถ๊ธฐ ์ฒ๋ฆฌํด์ ๊ฐ๊ฐ์ ์๋น์ค ๋ก์ง์ ์ํํ๋๋ก ํ๋๋ฐ์.
Facade์์ ๊ฐ์ ๋ฐ๋ผ ๋ถ๊ธฐํ์ฌ ๋ค๋ฅธ ์๋น์ค ๋ก์ง์ ํธ์ถํ๋ ์ด ํํ๊ฐ ์ฌ๋ฐ๋ฅธ ํํ์ธ๊ฐ์?
Q4). ์ํคํ ์ฒ ๊ด๋ จ ์ง๋ฌธ์ ๋๋ค.
์๋ก์ด ํด๋์ค๋ฅผ ์ ์ํ ๋๋ง๋ค ์ด๊ฒ์ด ์ด๋ ๊ณณ์ ์์นํด์ผ ๋๋์ง๊ฐ ์ด๋ ค์ ์ต๋๋ค.
๊ธฐ์กด์ ์ ๊ณต๋์๋ ํ ํ๋ฆฟ์ ์กด์ฌํ๋ ์ ํ์ ํด๋์ค๋ค์ด๋ผ๋ฉด ๊ทธ๋๋ก ๋ฐ๋ผํ์ง๋ง ๊ทธ๋ ์ง ์์ ๊ฒฝ์ฐ(kafka publisher, listener, outbox ๋ฑ๋ฑ)์๋ ํ๋จ์ด ์ ์๋๋๋ผ๊ตฌ์ !
์ํคํ ์ฒ์ ๋ํ ์ดํด๋๋ฅผ ๋์ด๋ฉด ํด๊ฒฐ์ด ๋ ๊น ์ถ์ด์ ๋ฃจํผ์ค ์ดํ์ ์ํคํ ์ฒ์ ๋ํด์ ๊ณต๋ถํด๋ณด๋ ค๊ณ ํฉ๋๋ค. ์ด ๊ณต๋ถ๊ฐ ๋์์ด ๋ ๊ณต๋ถ์ธ์ง์ ๋์์ด ๋๋ค๋ฉด ์ด๋ป๊ฒ ๊ณต๋ถํ๋ฉด ์ข์์ง ๊ถ๊ธํฉ๋๋ค. (ex. ์์ ์ถ์ฒ)
Q5). ๋ฃจํผ์ค ์ดํ ๋ฐฉํฅ์ฑ์ ๋ํ ์ง๋ฌธ
๐งโ๐ป ์ฒซ ๋ฒ์งธ ์ง๋ฌธ
์์ฌ์์ด ๋ง์ด ๋จ์ง๋ง ๋์์ ๋ง์ด ์ฑ์ฅํ๋ค๊ณ ๋จ์ธํ ์ ์๋ ์๊ฐ์ด์์ต๋๋ค. ๊ถ๊ธํ ๊ฒ ์๊ธฐ๋ฉด ์ด๊ฒ ์ ๊ฒ ํ์ธ๋ฐ๊ณ ์ถ๊ณ ์ฐพ์๋ณด๊ณ ํ๋ ํธ์ด๋ผ ๊ณผ์ ํต๊ณผ๋ฅผ ๋ง์ด ๋ชปํ ๊ฒ ํนํ ์์ฝ์ต๋๋ค.
ํ์ gpt๋ก ์ด๋ก ์ ์ธ ํ์ต์ ๋ง์ด ํ๋ ํธ์ธ๋ฐ์.
์๋ฌด๋๋ gpt๊ฐ ๊ฑฐ์ง์ ๋งํ๋ ๊ฒฝ์ฐ๊ฐ ๋ถ๋ช ์๋ค๋ณด๋ ํด๋น ๋ฐฉ๋ฒ์ด ์ข์ ๊ฒ์ ์๋๊น ํ๋ ์๊ฐ๋ ๋ญ๋๋ค. ๋ฉํ ๋์ ์ด๋ป๊ฒ ๊ณต๋ถํ์๋์?
๐งโ๐ป ๋ ๋ฒ์งธ ์ง๋ฌธ
์ดํ์ ๋ฃจํผ์ค ๋ฐ์ ์๋ฃ๋ฅผ ์ฐธ๊ณ ํ๋ฉฐ ๋ค์ 1์ฃผ์ฐจ๋ถํฐ ๋ฐ์๋ณด๋ฉฐ ๋ชปํ๋ ๊ฒ๊ณผ ๊ถ๊ธํ๋ ๊ฒ์ ๋ํด ๊น์ด ํ์ตํด๋ณด๊ณ ์ ํฉ๋๋ค.
ํ๋ฉด์ ๋์์ด ๋ ๋งํ ์์ ์ด๋ ๋ฐฉ๋ฒ์ ๋ฌด์์ด ์์๊น์?
๐งโ๐ป ์ธ ๋ฒ์งธ ์ง๋ฌธ
๋ฉํ ๋์ด ์๊ฐํ๋ ์ ๋ ์ด๋ค ์ฌ๋์ธ๊ฐ์? (ํ๋, ์ค๋ ฅ ๋ฑ์ ์ฅ๋จ์ ์ผ๋ก ๊ฐ๊ฐ์์ด ์์งํ๊ฒ ๋ง์ ๋ถํ๋๋ฆฌ๊ฒ ์ต๋๋ค. ใ ใ .. ๋งค์ด๋ง๋ ๊ฐ๋ฅํฉ๋๋ค!)
์์ง์ ๋ชจ๋ฅด๊ฒ ๋ค ์ถ์ผ์๋ค๋ฉด ํธํ ๋ง์ ์ฃผ์ ๋ ๋ฉ๋๋ค.
๐งโ๐ป ๋ง๋ฌด๋ฆฌ..
10์ฃผ๊ฐ ์ ๋ง ๊ฐ์ฌ๋๋ฆฝ๋๋ค ใ ใ ํญ์ ์ ์พํ๊ฒ ๋ฉํ ๋ง ๋ฐ์์๋ค์.
2025๋ ๋ ๊ณ ์ ๋ง์ผ์ จ๊ตฌ ๋ค๊ฐ์จ ์ํด์๋ ๋์ฑ ๋ง์ ํ๋ณต์ ์ฐพ์๊ฐ์๊ธธ ์ง์ฌ์ผ๋ก ๋ฐ๋ผ๊ฒ ์ต๋๋ค !
โ Checklist
๐งฑ Spring Batch
๐งฉ Ranking API
๐ References
Summary by CodeRabbit
๋ฆด๋ฆฌ์ค ๋ ธํธ
์๋ก์ด ๊ธฐ๋ฅ
๊ฐ์ ์ฌํญ
์ ๊ฑฐ๋จ
โ๏ธ Tip: You can customize this high-level summary in your review settings.