-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-10] Collect, Stack, Zip #233
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: minor7295
Are you sure you want to change the base?
Conversation
* feat: batch ์ฒ๋ฆฌ ๋ชจ๋ ๋ถ๋ฆฌ * feat: batch ๋ชจ๋์ ProductMetrics ๋๋ฉ์ธ ์ถ๊ฐ * feat: ProudctMetrics์ Repository ์ถ๊ฐ * test: Product Metrics ๋ฐฐ์น ์์ ์ ๋ํ ํ ์คํธ ์ฝ๋ ์ถ๊ฐ * feat: ProductMetrics ๋ฐฐ์น ์์ ๊ตฌํ * test: Product Rank์ ๋ํ ํ ์คํธ ์ฝ๋ ์ถ๊ฐ * feat: Product Rank ๋๋ฉ์ธ ๊ตฌํ * feat: Product Rank Repository ์ถ๊ฐ * test: Product Rank ๋ฐฐ์น์ ๋ํ ํ ์คํธ ์ฝ๋ ์ถ๊ฐ * feat: Product Rank ๋ฐฐ์น ์์ ์ถ๊ฐ * feat: ์ผ๊ฐ, ์ฃผ๊ฐ, ์๊ฐ ๋ญํน์ ์ ๊ณตํ๋ api ์ถ๊ฐ * refractor: ๋ญํน ์ง๊ณ ๋ก์ง์ ์ฌ๋ฌ step์ผ๋ก ๋ถ๋ฆฌํจ * chore: db ์ด๊ธฐํ ๋ก์ง์์ ๋ฐ์ํ๋ ์ค๋ฅ ์์ * test: ๋ญํน ์ง๊ณ์ ๊ฐ step์ ๋ํ ํ ์คํธ ์ฝ๋ ์ถ๊ฐ
Walkthrough์๋ก์ด ๋ฐฐ์น ์ฒ๋ฆฌ ๋ชจ๋(commerce-batch)์ ์ถ๊ฐํ๊ณ ์ํ ์์๋ฅผ ์ผ์ผ(Redis), ์ฃผ๊ฐ/์๊ฐ(๊ตฌ์ฒดํ๋ ๋ทฐ)์ผ๋ก ์ฒ๋ฆฌํ๋๋ก ํ์ฅํ์ต๋๋ค. RankingService์ ๋ค์ค ๊ธฐ๊ฐ ์ฟผ๋ฆฌ ์ง์, ProductMetrics ๋ฐ ProductRank ์ํฐํฐ, ๋ฐฐ์น ์์ ๋ฐ ๊ด๋ จ ์ ์ฅ์๋ฅผ ๋์ ํ์ต๋๋ค. Changes
Sequence DiagramssequenceDiagram
participant Client
participant RankingV1Controller
participant RankingService
participant Redis as Redis<br/>(DAILY)
participant MV as Materialized View<br/>(WEEKLY/MONTHLY)
participant ProductDB as Product<br/>Repository
Client->>RankingV1Controller: getRankings(date, period, page, size)
RankingV1Controller->>RankingV1Controller: parsePeriodType(period)
RankingV1Controller->>RankingService: getRankings(date, PeriodType, page, size)
alt period == DAILY
RankingService->>Redis: Redis ZSET ์กฐํ
Redis-->>RankingService: ์์ ๋ฐ์ดํฐ
else period == WEEKLY or MONTHLY
RankingService->>MV: periodStartDate, PeriodType๋ก ์กฐํ
MV-->>RankingService: TOP 100 ์ํ ์์
end
RankingService->>ProductDB: ์ํ/๋ธ๋๋ ์ ๋ณด ์กฐํ
ProductDB-->>RankingService: ์ํ ๋ฐ์ดํฐ
RankingService->>RankingService: ์์ ํญ๋ชฉ ๊ตฌ์ฑ
RankingService-->>RankingV1Controller: RankingsResponse (ํ์ด์ง๋ค์ด์
)
RankingV1Controller-->>Client: ApiResponse<RankingsResponse>
sequenceDiagram
participant Scheduler as Job Scheduler
participant BatchApp as Batch<br/>Application
participant MetricsJob as ProductMetrics<br/>Aggregation Job
participant RankJob as ProductRank<br/>Aggregation Job
participant MetricsDB as ProductMetrics<br/>Table
participant ScoreTable as ProductRankScore<br/>(Temp)
participant RankMV as ProductRank MV<br/>(mv_product_rank)
Scheduler->>BatchApp: productMetricsAggregationJob(targetDate)
BatchApp->>MetricsJob: Step 1: ๋ฉํธ๋ฆญ ์ฝ๊ธฐ/์ฒ๋ฆฌ/์ฐ๊ธฐ
MetricsJob->>MetricsDB: updatedAt ๋ฒ์๋ก ์กฐํ
MetricsDB-->>MetricsJob: ProductMetrics ์ฒญํฌ
MetricsJob->>MetricsJob: pass-through (์ฒ๋ฆฌ)
MetricsJob->>MetricsJob: ๋ก๊น
Scheduler->>BatchApp: productRankAggregationJob(periodType, targetDate)
BatchApp->>RankJob: Step 1: ๋ฉํธ๋ฆญ -> ์ ์ ์ง๊ณ
RankJob->>MetricsDB: ๋ฒ์๋ณ ๋ฉํธ๋ฆญ ์ฝ๊ธฐ
MetricsDB-->>RankJob: ProductMetrics
RankJob->>RankJob: ์ ํ๋ณ ๊ทธ๋ฃนํ<br/>๊ฐ์ค์น ์ ์ ๊ณ์ฐ<br/>(์ข์์ 0.3, ํ๋งค 0.5, ์กฐํ 0.2)
RankJob->>ScoreTable: ProductRankScore ์ ์ฅ (UPSERT)
ScoreTable-->>RankJob: ์ ์ฅ ์๋ฃ
BatchApp->>RankJob: Step 2: ์ ์ -> ์์ ๋ณํ
RankJob->>ScoreTable: ์ ์ ๋ด๋ฆผ์ฐจ์ ์กฐํ
ScoreTable-->>RankJob: ProductRankScore (๋ชจ๋ ํญ๋ชฉ)
RankJob->>RankJob: TOP 100 ์ ํ<br/>1-100 ์์ ํ ๋น<br/>(ThreadLocal ์นด์ดํฐ)
RankJob->>RankMV: ProductRank ์ ์ฅ<br/>(๊ธฐ๊ฐ๋ณ UPSERT)
RankMV-->>RankJob: ์ ์ฅ ์๋ฃ
RankJob->>ScoreTable: ์์ ํ
์ด๋ธ ์ ๋ฆฌ
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~60 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touchesโ Failed checks (1 warning, 1 inconclusive)
โ Passed checks (1 passed)
โจ 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 |
|
@coderabbitai review |
โ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 13
๐งน Nitpick comments (24)
modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java (1)
43-57: ๋ชจ๋ ์์ธ๋ฅผ ๋ฌด์ํ๋ ๊ฒ์ ๋๋ฒ๊น ์ ์ด๋ ต๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.ํ ์ด๋ธ ์กด์ฌ ์ฌ๋ถ๋ฅผ ๋จผ์ ํ์ธํ๋ ๋ก์ง์ ์ข์ง๋ง, ๋ชจ๋ ์์ธ๋ฅผ ๋ก๊น ์์ด ์ผํค๋ ๊ฒ์ ์ค์ ๋ฌธ์ (DB ์ฐ๊ฒฐ ์ค๋ฅ, ๊ถํ ๋ฌธ์ ๋ฑ)๋ฅผ ์จ๊ธธ ์ ์์ต๋๋ค. ํ ์คํธ ์คํจ ์์ธ ํ์ ์ด ์ด๋ ค์์ง ์ ์์ผ๋ ์ต์ํ์ ๋๋ฒ๊ทธ ๋ก๊น ์ ๊ณ ๋ คํด์ฃผ์ธ์.
๋ํ, ํ ์ด๋ธ ์กด์ฌ ์ฌ๋ถ๋ฅผ ํ์ธํ ํ์๋ try-catch๊ฐ ํ์ํ ์ด์ ๊ฐ ๋ถ๋ถ๋ช ํฉ๋๋ค. ์กด์ฌ ํ์ธ์ด ์ฑ๊ณตํ๋ฉด TRUNCATE๋ ์ฑ๊ณตํ ๊ฐ๋ฅ์ฑ์ด ๋์ผ๋ฏ๋ก, ์์ธ ๋ฐ์ ์ ์์ธ์ ํ์ ํ ์ ์๋๋ก ๋ก๊น ์ด ๋์ฑ ์ค์ํฉ๋๋ค.
๐ ์ต์ ๋ก๊น ์ถ๊ฐ ์ ์
+import org.slf4j.Logger; +import org.slf4j.LoggerFactory;@Component public class DatabaseCleanUp implements InitializingBean { + private static final Logger log = LoggerFactory.getLogger(DatabaseCleanUp.class); @PersistenceContext private EntityManager entityManager;} catch (Exception e) { - // ํ ์ด๋ธ์ด ์๊ฑฐ๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด ๋ฌด์ํ๊ณ ๊ณ์ ์งํ - // ๋ก๊ทธ๋ ๋จ๊ธฐ์ง ์์ (ํ ์คํธ ํ๊ฒฝ์์ ์ ์์ ์ธ ์ํฉ์ผ ์ ์์) + // ํ ์ด๋ธ์ด ์๊ฑฐ๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด ๋ฌด์ํ๊ณ ๊ณ์ ์งํ + log.debug("ํ ์ด๋ธ {} ์ ๋ฆฌ ์ค ์์ธ ๋ฐ์ (๋ฌด์๋จ): {}", table, e.getMessage()); }apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (2)
64-71: ํ ์คํธ ๊ฐ๋ฅ์ฑ์ ์ํด ์๊ฐ ์ฃผ์ ๊ณ ๋ ค์์ฑ์์์
LocalDateTime.now()๋ฅผ ์ง์ ํธ์ถํ๋ฉด ํ ์คํธ ์์ฑ์ด ์ด๋ ต๊ณ ์์คํ ์๊ฐ์ ๊ฒฐํฉ๋ฉ๋๋ค.ํ ์คํธ ๊ฐ๋ฅ์ฑ๊ณผ ๋๋ฉ์ธ ์์์ฑ์ ๊ฐ์ ํ๋ ค๋ฉด ๋ค์์ ๊ณ ๋ คํ์ธ์:
- ์ต์ 1:
updatedAt์ ์์ฑ์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๊ธฐ- ์ต์ 2:
Clock๊ฐ์ฒด๋ฅผ ์ฃผ์ ๋ฐ์ ์ฌ์ฉ๐ ์๊ฐ ์ฃผ์ ์์
-public ProductMetrics(Long productId) { +public ProductMetrics(Long productId, LocalDateTime now) { this.productId = productId; this.likeCount = 0L; this.salesCount = 0L; this.viewCount = 0L; this.version = 0L; - this.updatedAt = LocalDateTime.now(); + this.updatedAt = now; }
76-113: ๋ฉ์๋ ๋ด ์๊ฐ ์์ฑ ์ผ๊ด์ฑ๋ชจ๋ ๋ณ๊ฒฝ ๋ฉ์๋์์
LocalDateTime.now()๋ฅผ ์ง์ ํธ์ถํ๋ ํจํด์ด ๋ฐ๋ณต๋ฉ๋๋ค. ์์ฑ์์์ ์ธ๊ธํ ๊ฒ๊ณผ ๋์ผํ ํ ์คํธ ๊ฐ๋ฅ์ฑ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.๊ธ์ ์ ์ธ ์ :
decrementLikeCount()์ ์์ ๋ฐฉ์ง ๊ฐ๋ ๋ก์ง์ด ์ ์ ํฉ๋๋คincrementSalesCount()์ ์๋ ๊ฒ์ฆ์ด ์ฌ๋ฐ๋ฆ ๋๋คapps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessorTest.java (1)
38-50: ์ค๋ณต ํ ์คํธ ์ผ์ด์ค
processesNonNullItemํ ์คํธ๋processesItem_andReturnsSameItemํ ์คํธ์ ์ค๋ณต๋ฉ๋๋ค. ์ฒซ ๋ฒ์งธ ํ ์คํธ๊ฐ ์ด๋ฏธ ๋์ผํ ๊ฒ์ฆ(๋์ผ ๊ฐ์ฒด ๋ฐํ, non-null)์ ๋ ํฌ๊ด์ ์ผ๋ก ์ํํ๊ณ ์์ต๋๋ค.ํ ์คํธ ์ค์ํธ๋ฅผ ๊ฐ๊ฒฐํ๊ฒ ์ ์งํ๊ธฐ ์ํด ์ด ์ค๋ณต ํ ์คํธ๋ฅผ ์ ๊ฑฐํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReaderTest.java (2)
13-15: ์ฌ์ฉ๋์ง ์๋ import ๋ฌธ์ด ์์ต๋๋ค.
LocalDateTime๊ณผLocalTime์ด import๋์์ง๋ง ์ค์ ๋ก ์ฌ์ฉ๋์ง ์์ต๋๋ค.๐ ์์ ์ ์
import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime;
94-114: ํ ์คํธ ๊ฒ์ฆ์ด ๋ถ์์ ํฉ๋๋ค.
parsesDateCorrectly_andSetsDateTimeRangeํ ์คํธ์์expectedStart์expectedEnd๋ณ์๋ฅผ ์ ์ธํ์ง๋ง ์ค์ ๊ฒ์ฆ์ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ์ฃผ์์์ "๊ฐ์ ์ ์ผ๋ก ๊ฒ์ฆ"์ด๋ผ๊ณ ์ธ๊ธํ๊ณ ์์ผ๋, Reader ๋ด๋ถ ์ํ๋ฅผ ์ง์ ๊ฒ์ฆํ๊ฑฐ๋ Repository ํธ์ถ ์ ์ ๋ฌ๋ ์ธ์๋ฅผ ๊ฒ์ฆํ๋ ๊ฒ์ด ๋ ๋ช ํํฉ๋๋ค.ํ์ฌ ๊ตฌํ์์๋ Repository ๋ฉ์๋ ํธ์ถ ์ ArgumentCaptor๋ฅผ ์ฌ์ฉํ๊ฑฐ๋, Reader์ ๋ด๋ถ ์ํ์ ์ ๊ทผํ ์ ์๋ค๋ฉด ํด๋น ๊ฐ์ ๊ฒ์ฆํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค. ๋ง์ฝ ํ์ฌ ๊ตฌ์กฐ์์ ๊ฒ์ฆ์ด ์ด๋ ต๋ค๋ฉด, ์ฌ์ฉ๋์ง ์๋ ๋ณ์๋ ์ ๊ฑฐํ๊ณ ํ ์คํธ ์ด๋ฆ๊ณผ ์ฃผ์์ ์ค์ ๊ฒ์ฆ ๋ด์ฉ์ ๋ง๊ฒ ์์ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
apps/commerce-batch/src/test/java/com/loopers/domain/rank/ProductRankTest.java (1)
54-82: ์๊ฐ ๊ธฐ๋ฐ ํ ์คํธ์์Thread.sleep์ฌ์ฉ
Thread.sleep(1)์ ์ฌ์ฉํ์ฌ ํ์์คํฌํ ์ฐจ์ด๋ฅผ ๋ณด์ฅํ๊ณ ์์ต๋๋ค. ํ์ฌ ์ ๊ทผ ๋ฐฉ์์ ๋์ํ์ง๋ง, ํ ์คํธ๊ฐ ๊ฐํ์ ์ผ๋ก ์คํจํ ๊ฐ๋ฅ์ฑ์ด ์์ต๋๋ค.๋ ๊ฒฐ์ ์ ์ธ ํ ์คํธ๋ฅผ ์ํด
java.time.Clock์ ์ฃผ์ ํ์ฌ ์๊ฐ์ ์ ์ดํ๋ ๋ฐฉ์์ ๊ณ ๋ คํด๋ณผ ์ ์์ต๋๋ค. ๋ค๋ง, ํ์ฌ 1ms sleep์ ์ค์ฉ์ ์ผ๋ก ์ถฉ๋ถํ ์์ ์ ์ ๋๋ค.apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessor.java (1)
3-15: ์ฌ์ฉ๋์ง ์๋ import ๋ฌธ์ด ์์ต๋๋ค.
ProductMetrics,ItemProcessor,Comparator,List,Map,Collectors,IntStream๋ฑ์ import๊ฐ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ๋ฆฌํฉํ ๋ง ํ ๋จ์ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค.๐ ์ ๋ฆฌ ์ ์
package com.loopers.infrastructure.batch.rank; -import com.loopers.domain.metrics.ProductMetrics; import com.loopers.domain.rank.ProductRank; import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.item.ItemProcessor; import org.springframework.stereotype.Component; import java.time.LocalDate; import java.time.temporal.TemporalAdjusters; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.IntStream;apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReaderTest.java (1)
100-150: ๋ ์ง ๋ฒ์ ๊ณ์ฐ ๋ก์ง ๊ฒ์ฆ์ด ๋๋ฝ๋์์ต๋๋ค.
weeklyReaderCalculatesCorrectWeekRange_forAnyDayInWeek์monthlyReaderCalculatesCorrectMonthRange_forAnyDayInMonthํ ์คํธ๋ "๋ชจ๋ ๊ฐ์ ์ฃผ/์์ ์์์ผ๋ถํฐ ์์ํด์ผ ํจ"์ด๋ผ๊ณ ์ฃผ์์ผ๋ก ๋ช ์ํ์ง๋ง, ์ด๋ฅผ ๊ฒ์ฆํ๋ assertion์ด ์์ต๋๋ค.๋ ์ง ๋ฒ์ ๊ณ์ฐ ๋ก์ง์ ๋ณ๋ ํฌํผ ๋ฉ์๋๋ก ์ถ์ถํ๋ฉด ๋จ์ ํ ์คํธ๊ฐ ์ฉ์ดํด์ง๋๋ค:
// ProductRankAggregationReader์ ์ถ๊ฐ public LocalDate calculateWeekStart(LocalDate targetDate) { return targetDate.with(DayOfWeek.MONDAY); } public LocalDate calculateMonthStart(LocalDate targetDate) { return targetDate.with(TemporalAdjusters.firstDayOfMonth()); }๊ทธ๋ฐ ๋ค์ ์ด ๋ฉ์๋๋ค์ ์ง์ ํ ์คํธํ ์ ์์ต๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReader.java (1)
69-72: ๋ถ๋ณ Map ์ฌ์ฉ์ ๊ณ ๋ คํด์ฃผ์ธ์.๋จ์ผ ํญ๋ชฉ๋ง ํฌํจํ๋ ์ ๋ ฌ ๊ธฐ์ค์
HashMap์ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. Java 9+์Map.of()๋ฅผ ์ฌ์ฉํ๋ฉด ๋ ๊ฐ๊ฒฐํ๊ณ ๋ถ๋ณ์ฑ์ ๋ณด์ฅํฉ๋๋ค.๐ ์์ ์ ์
- // ์ ๋ ฌ ๊ธฐ์ค ์ค์ (product_id ๊ธฐ์ค ์ค๋ฆ์ฐจ์) - Map<String, Sort.Direction> sorts = new HashMap<>(); - sorts.put("productId", Sort.Direction.ASC); + // ์ ๋ ฌ ๊ธฐ์ค ์ค์ (product_id ๊ธฐ์ค ์ค๋ฆ์ฐจ์) + Map<String, Sort.Direction> sorts = Map.of("productId", Sort.Direction.ASC);์ด ๊ฒฝ์ฐ
java.util.HashMapimport๋ ์ ๊ฑฐํ ์ ์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScore.java (2)
34-41: ์ธ๋ฑ์ค ์ ์ ๋ฐฉ์์ ๋ํ ์ฐธ๊ณ ์ฌํญ์ ๋๋ค.
@Index์unique = true์@Column์unique = true๊ฐ ๋ชจ๋ ์ ์ฉ๋์ด ์์ต๋๋ค (Line 38๊ณผ Line 54). ๋ ์ค์ ๋ชจ๋ ์ ๋ํฌ ์ ์ฝ์กฐ๊ฑด์ ์์ฑํ๋ฏ๋ก ์ค๋ณต ์ ์์ผ ์ ์์ต๋๋ค.์ผ๊ด์ฑ์ ์ํด ๋ ์ค ํ๋๋ง ์ ์งํ๋ ๊ฒ์ ๊ณ ๋ คํด์ฃผ์ธ์. ์ผ๋ฐ์ ์ผ๋ก
@Column(unique = true)๋ ๋จ์ผ ์ปฌ๋ผ ์ ๋ํฌ ์ ์ฝ์,@Index(unique = true)๋ ๋ณตํฉ ์ธ๋ฑ์ค์ ์ ๋ํฌ ์ ์ฝ์ ์ฌ์ฉ๋ฉ๋๋ค.
95-101:setMetrics()๋ฉ์๋๊ฐ public์ผ๋ก ๋ ธ์ถ๋์ด ์์ต๋๋ค.Javadoc์์ "Repository์์๋ง ์ฌ์ฉํ๋ ๋ด๋ถ ๋ฉ์๋"๋ผ๊ณ ๋ช ์ํ์ง๋ง ์ ๊ทผ ์ ์ด์๊ฐ
public์ ๋๋ค. ์๋์น ์์ ์ธ๋ถ ์ ๊ทผ์ ๋ฐฉ์งํ๋ ค๋ฉด package-private ๋๋protected๋ก ๋ณ๊ฒฝํ๋ ๊ฒ์ ๊ณ ๋ คํด์ฃผ์ธ์.๐ ์์ ์ ์
- public void setMetrics(Long likeCount, Long salesCount, Long viewCount, Double score) { + void setMetrics(Long likeCount, Long salesCount, Long viewCount, Double score) {apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationWriter.java (1)
76-79: ๋งค Chunk๋ง๋ค ์ ์ฒด ๋ฐ์ดํฐ ์ญ์ +์ฝ์ ์ ๋นํจ์จ์ ํ์ฌ ๊ตฌํ์ ๋งค Chunk๋ง๋ค delete + insert๋ฅผ ์ํํฉ๋๋ค. Step ์๋ฃ ์์ ์ ํ ๋ฒ๋ง ์ ์ฅํ๋ ๋ฐฉ์์ด ๋ ํจ์จ์ ์ ๋๋ค.
StepExecutionListener.afterStep()์์ ์ต์ข ์ ์ฅ์ ์ํํ๋ ๊ฒ์ ๊ณ ๋ คํด ๋ณด์ธ์.apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.java (1)
146-168: Java record๋ก ๊ฐ์ํ ๊ฐ๋ฅ
AggregatedMetrics๋ด๋ถ ํด๋์ค๋ฅผ Java record๋ก ๋์ฒดํ๋ฉด ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋๋ฅผ ์ค์ผ ์ ์์ต๋๋ค.๐ record ์ฌ์ฉ ์ ์
- private static class AggregatedMetrics { - private final Long likeCount; - private final Long salesCount; - private final Long viewCount; - - public AggregatedMetrics(Long likeCount, Long salesCount, Long viewCount) { - this.likeCount = likeCount; - this.salesCount = salesCount; - this.viewCount = viewCount; - } - - public Long getLikeCount() { - return likeCount; - } - - public Long getSalesCount() { - return salesCount; - } - - public Long getViewCount() { - return viewCount; - } - } + private record AggregatedMetrics(Long likeCount, Long salesCount, Long viewCount) {}apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankScoreRepositoryImpl.java (1)
35-42: ๊ด๋ฆฌ ์ํ ์ํฐํฐ์merge()ํธ์ถ ๋ถํ์
existingScore๋findByProductId์์ ์กฐํ๋ ๊ด๋ฆฌ(managed) ์ํ ์ํฐํฐ์ ๋๋ค.@Transactional์ปจํ ์คํธ ๋ด์์setMetrics()๋ก ๊ฐ์ ๋ณ๊ฒฝํ๋ฉด JPA dirty checking์ ์ํด ์๋์ผ๋ก flush๋ฉ๋๋ค.merge()ํธ์ถ์ ๋ถํ์ํฉ๋๋ค.๐ ์์ ์ ์
ProductRankScore existingScore = existing.get(); existingScore.setMetrics( score.getLikeCount(), score.getSalesCount(), score.getViewCount(), score.getScore() ); - entityManager.merge(existingScore); log.debug("ProductRankScore ์ ๋ฐ์ดํธ: productId={}", score.getProductId());Based on learnings, ์ด ์ฝ๋๋ฒ ์ด์ค์์๋ ํธ๋์ญ์ ์ปจํ ์คํธ ๋ด์์ JPA dirty checking์ ํตํ ์๋ ์์ํ๋ฅผ ์ ํธํฉ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.java (1)
102-109: JPA ๋ผ์ดํ์ฌ์ดํด ์ฝ๋ฐฑ์ผ๋ก ํ์์คํฌํ ๊ด๋ฆฌ ๊ถ์ฅ
createdAt๊ณผupdatedAt์ ์๋์ผ๋ก ์ค์ ํ๋ ๋์@PrePersist์@PreUpdate๋ฅผ ์ฌ์ฉํ๋ฉด ์ผ๊ด์ฑ ์๋ ํ์์คํฌํ ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.๐ JPA ์ฝ๋ฐฑ ์ฌ์ฉ ์ ์
+ @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } + public ProductRank( PeriodType periodType, LocalDate periodStartDate, Long productId, Integer rank, Long likeCount, Long salesCount, Long viewCount ) { this.periodType = periodType; this.periodStartDate = periodStartDate; this.productId = productId; this.rank = rank; this.likeCount = likeCount; this.salesCount = salesCount; this.viewCount = viewCount; - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); } public void updateRank(Integer rank, Long likeCount, Long salesCount, Long viewCount) { this.rank = rank; this.likeCount = likeCount; this.salesCount = salesCount; this.viewCount = viewCount; - this.updatedAt = LocalDateTime.now(); }Also applies to: 138-139, 155-155
apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.java (2)
36-38: ๋๋ ๋ฐ์ดํฐ ์ ์ฅ ์ ์ฑ๋ฅ ๊ฐ์ ๊ณ ๋ ค๊ฐ๋ณ
persist()ํธ์ถ ๋์ ์ผ์ ๊ฐ๊ฒฉ์ผ๋กflush()์clear()๋ฅผ ์ํํ๋ฉด ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ด ํฅ์๋ฉ๋๋ค. TOP 100์ผ๋ก ์ ํ๋์ด ํ์ฌ๋ ๋ฌธ์ ์์ง๋ง, ํฅํ ํ์ฅ ์ ๊ณ ๋ คํด ์ฃผ์ธ์.๐ ๋ฐฐ์น ํ๋ฌ์ ํจํด ์ ์
public void saveRanks(ProductRank.PeriodType periodType, LocalDate periodStartDate, List<ProductRank> ranks) { // ๊ธฐ์กด ๋ฐ์ดํฐ ์ญ์ deleteByPeriod(periodType, periodStartDate); // ์ ๋ฐ์ดํฐ ์ ์ฅ - for (ProductRank rank : ranks) { - entityManager.persist(rank); - } + for (int i = 0; i < ranks.size(); i++) { + entityManager.persist(ranks.get(i)); + if (i > 0 && i % 50 == 0) { + entityManager.flush(); + entityManager.clear(); + } + } + entityManager.flush(); log.info("ProductRank ์ ์ฅ ์๋ฃ: periodType={}, periodStartDate={}, count={}", periodType, periodStartDate, ranks.size()); }
68-77:getResultList()๋ฅผ ์ฌ์ฉํ ๊ฐ๊ฒฐํ ๊ตฌํ ๊ณ ๋ ค
getSingleResult()์ ์์ธ ์ฒ๋ฆฌ ๋์getResultList()๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๋๊ฐ ๋ ๊ฐ๊ฒฐํด์ง๋๋ค.๐ ๊ฐ๊ฒฐํ ๊ตฌํ ์ ์
- try { - ProductRank rank = entityManager.createQuery(jpql, ProductRank.class) - .setParameter("periodType", periodType) - .setParameter("periodStartDate", periodStartDate) - .setParameter("productId", productId) - .getSingleResult(); - return Optional.of(rank); - } catch (jakarta.persistence.NoResultException e) { - return Optional.empty(); - } + return entityManager.createQuery(jpql, ProductRank.class) + .setParameter("periodType", periodType) + .setParameter("periodStartDate", periodStartDate) + .setParameter("productId", productId) + .getResultList() + .stream() + .findFirst();apps/commerce-batch/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.java (1)
46-52:Thread.sleep()๋์ ๋ ์์ ์ ์ธ ์๊ฐ ๊ฒ์ฆ ๋ฐฉ๋ฒ ๊ณ ๋ ค
Thread.sleep(1)์ ๊ฐํ์ ์ผ๋ก ํ ์คํธ ์คํจ๋ฅผ ์ ๋ฐํ ์ ์์ต๋๋ค. ์์คํ ๋ถํ์ ๋ฐ๋ผ 1ms ๋ด์ ๋ ์์ ์ด ๋์ผํ ์๊ฐ์ ์๋ฃ๋ ์ ์์ต๋๋ค.
Clock์ฃผ์ ํจํด์ด๋isAfterOrEqualTo()๊ฐ์ ์ํ๋ ๊ฒ์ฆ์ ๊ณ ๋ คํด ์ฃผ์ธ์.๐ ๋์ ์ ์
- Thread.sleep(1); // ์๊ฐ ์ฐจ์ด๋ฅผ ๋ณด์ฅํ๊ธฐ ์ํ ์์ ์ง์ฐ metrics.incrementLikeCount(); // assert assertThat(metrics.getLikeCount()).isEqualTo(initialLikeCount + 1); assertThat(metrics.getVersion()).isEqualTo(initialVersion + 1); - assertThat(metrics.getUpdatedAt()).isAfter(initialUpdatedAt); + assertThat(metrics.getUpdatedAt()).isAfterOrEqualTo(initialUpdatedAt);apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReader.java (1)
104-106:Map.of()์ฌ์ฉ์ผ๋ก ๊ฐ๊ฒฐํ ๊ฐ๋ฅ๋จ์ผ ์ํธ๋ฆฌ์ ๊ฒฝ์ฐ
Map.of()๋ฅผ ์ฌ์ฉํ๋ฉด ๋ ๊ฐ๊ฒฐํ๊ณ ๋ถ๋ณ์ฑ์ ๋ณด์ฅํฉ๋๋ค.๐ ์ ์
- // ์ ๋ ฌ ๊ธฐ์ค ์ค์ (product_id ๊ธฐ์ค ์ค๋ฆ์ฐจ์) - Map<String, Sort.Direction> sorts = new HashMap<>(); - sorts.put("productId", Sort.Direction.ASC); + // ์ ๋ ฌ ๊ธฐ์ค ์ค์ (product_id ๊ธฐ์ค ์ค๋ฆ์ฐจ์) + Map<String, Sort.Direction> sorts = Map.of("productId", Sort.Direction.ASC);apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java (1)
187-188: Reader Bean์์ Processor ์ํ ์ค์ ์ ๊ฒฐํฉ๋๊ฐ ๋์Reader bean ์์ฑ ์
productRankAggregationProcessor.setPeriod()๋ฅผ ํธ์ถํ๋ ๊ฒ์ ์์์ ์์กด์ฑ์ ๋ง๋ญ๋๋ค. Step ๋ฆฌ์ค๋๋ ๋ณ๋์ ์ด๊ธฐํ ๋น์ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ ๋ช ์์ ์ ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (3)
49-49: ์์ ํ ํด๋์ค๋ช (FQCN) ๋์ import ๋ฌธ ์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.
com.loopers.domain.rank.ProductRankRepository์com.loopers.domain.rank.ProductRankํด๋์ค๊ฐ ์ฌ๋ฌ ๊ณณ(๋ผ์ธ 375-378, 380, 396, 399-400, 422)์์ ์์ ํ ํด๋์ค๋ช ์ผ๋ก ์ฌ์ฉ๋๊ณ ์์ต๋๋ค. ๊ฐ๋ ์ฑ์ ์ํด ์๋จ์ import ๋ฌธ์ ์ถ๊ฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์ํ๋ ์์ ์ฌํญ
ํ์ผ ์๋จ import ์์ญ์ ์ถ๊ฐ:
import com.loopers.domain.rank.ProductRank; import com.loopers.domain.rank.ProductRankRepository;๊ทธ ํ ํด๋น ํด๋์ค๋ช ์ ๊ฐ๋ตํ๊ฒ ์์ :
- private final com.loopers.domain.rank.ProductRankRepository productRankRepository; + private final ProductRankRepository productRankRepository;
480-487: PeriodType enum ์ค๋ณต ์ ์ ๊ฒํ .
RankingService.PeriodType๊ณผProductRank.PeriodType๋ ๊ฐ์ enum์ด ์กด์ฌํ๋ฉฐ, ๋ผ์ธ 375-378์์ ์๋์ผ๋ก ๋ณํํ๊ณ ์์ต๋๋ค. ์ด๋ฌํ ๊ตฌ์กฐ๋ ํฅํ ์๋ก์ด ๊ธฐ๊ฐ ํ์ ์ถ๊ฐ ์ ๋๊ธฐํ ๋๋ฝ ์ํ์ด ์์ต๋๋ค.๋ค์ ๋ฐฉ์์ ๊ณ ๋ คํด ๋ณผ ์ ์์ต๋๋ค:
- ๋๋ฉ์ธ enum ์ฌ์ฌ์ฉ:
ProductRank.PeriodType์ API ๊ณ์ธต์์๋ ์ฌ์ฉ- ๊ณตํต enum ์ถ์ถ: ๋ณ๋ ๊ณตํต ๋ชจ๋์ enum ์ ์ ํ ์์ชฝ์์ ์ฐธ์กฐ
ํ์ฌ ๊ธฐ๋ฅ ๋์์๋ ๋ฌธ์ ์์ผ๋ฏ๋ก ํฅํ ๋ฆฌํฉํ ๋ง์ผ๋ก ๊ณ ๋ คํ์ ๋ ๋ฉ๋๋ค.
380-396: ํ์ด์ง๋ค์ด์ ์ DB ์ฟผ๋ฆฌ๋ก ์์ํ๋ ๊ฒ์ ๊ณ ๋ คํ ์ ์์ต๋๋ค.ํ์ฌ 100๊ฑด ์ ์ฒด๋ฅผ ์กฐํํ ํ ๋ฉ๋ชจ๋ฆฌ์์
subList๋ก ํ์ด์งํ๊ณ ์์ต๋๋ค. TOP 100 ์ ํ์ด ์์ด ์ฑ๋ฅ์ ํฐ ๋ฌธ์ ๋ ์์ง๋ง,ProductRankRepository.findByPeriod์ offset/limit ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํ๋ฉด ๋ถํ์ํ ๋ฐ์ดํฐ ์ ์ก์ ์ค์ผ ์ ์์ต๋๋ค.ํ์ฌ ๊ตฌํ๋ ๋์์ ๋ฌธ์ ์์ผ๋ฏ๋ก ํ์์ ์ต์ ํ๋ก ๊ฒํ ํ์๋ฉด ๋ฉ๋๋ค.
๐ Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (42)
apps/commerce-api/build.gradle.ktsapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.javaapps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.javaapps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRankRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.javaapps/commerce-api/src/main/resources/application.ymlapps/commerce-batch/build.gradle.ktsapps/commerce-batch/src/main/java/com/loopers/BatchApplication.javaapps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.javaapps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScore.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScoreRepository.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessor.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReader.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriter.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessor.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReader.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessor.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationReader.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationWriter.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankScoreRepositoryImpl.javaapps/commerce-batch/src/main/resources/application.ymlapps/commerce-batch/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.javaapps/commerce-batch/src/test/java/com/loopers/domain/rank/ProductRankTest.javaapps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessorTest.javaapps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReaderTest.javaapps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriterTest.javaapps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessorTest.javaapps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReaderTest.javaapps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessorTest.javaapps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriterTest.javamodules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.javasettings.gradle.kts
๐ค Files with no reviewable changes (2)
- apps/commerce-api/src/main/resources/application.yml
- apps/commerce-api/build.gradle.kts
๐งฐ Additional context used
๐ง Learnings (8)
๐ Common learnings
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๋ถ์ผ๋ก ์ค์ ๋์ด ์๋ค.
๐ 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/domain/rank/ProductRank.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScore.javaapps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.javaapps/commerce-batch/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.javaapps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java
๐ Learning: 2025-11-12T13:04:50.782Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 72
File: core/domain/src/main/java/com/loopers/core/domain/productlike/vo/ProductLikeId.java:5-7
Timestamp: 2025-11-12T13:04:50.782Z
Learning: In the com.loopers codebase, domain entity ID value objects (e.g., ProductLikeId, OrderItemId, ProductId, PaymentId, OrderId, BrandId) are system-generated identifiers and do not require pattern validation (regex, length checks). They are implemented as simple records with a String value and an empty() factory method returning null for unsaved entities. This differs from UserIdentifier, which is a user-supplied login ID that requires format validation. Domain IDs should not be flagged for missing validation logic in the create() method.
<!-- [add_learning]
UserIdentifier์ ๊ฐ์ ์ฌ์ฉ์ ์
๋ ฅ ID์ ProductLikeId, OrderItemId ๋ฑ์ ๋๋ฉ์ธ ID๋ ๊ฒ์ฆ ํจํด์ด ๋ค๋ฆ
๋๋ค. UserIdentifier๋ ์ฌ์ฉ์๊ฐ ์
๋ ฅํ๋ ๋ก๊ทธ์ธ ID๋ก์ ์ ๊ท์, ๊ธธ์ด ๋ฑ์ ํ์ ๊ฒ์ฆ์ด ํ์ํ์ง๋ง, ๋๋ฉ์ธ ID๋ ์์คํ
์์ ์์ฑํ๋ ์๋ณ์(UUID, DB ์์ฑ ID)์ด๋ฏ๋ก ํจํด ๊ฒ์ฆ์ด ๋ถํ์ํฉ๋๋ค. ๋๋ฉ์ธ ID VO๋ ๋จ์ํ record์ empty() ํฉํ ๋ฆฌ ๋ฉ์๋๋ง์ผ๋ก ์ถฉ๋ถํฉ๋๋ค.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScore.javaapps/commerce-batch/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.java
๐ Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.javaapps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScoreRepository.java
๐ Learning: 2025-12-19T09:30:12.459Z
Learnt from: HongChangMo
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 198
File: apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventListener.java:0-0
Timestamp: 2025-12-19T09:30:12.459Z
Learning: In the loopers-spring-java-template repository's commerce-api module, when entities are managed within a transactional context (e.g., Transactional methods), prefer relying on JPA dirty checking for automatic persistence rather than explicit save() calls. Both Payment and Order entities in PaymentEventListener use this pattern, with state changes automatically flushed on transaction commit.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankScoreRepositoryImpl.javaapps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java
๐ Learning: 2025-11-21T03:38:07.494Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 99
File: apps/commerce-api/src/main/resources/application.yml:26-30
Timestamp: 2025-11-21T03:38:07.494Z
Learning: The batch job implementation for likeCount synchronization in apps/commerce-api is temporary and intended for development environment only. It will be replaced with Event-Driven Architecture (EDA) before production deployment, so production-level configuration concerns (like profile-based initialize-schema settings) are not required.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.javaapps/commerce-batch/src/main/resources/application.yml
๐ 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/domain/metrics/ProductMetrics.java
๐ Learning: 2025-12-19T03:23:51.674Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 191
File: modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java:24-34
Timestamp: 2025-12-19T03:23:51.674Z
Learning: Maintain the use of static initialization blocks in test fixtures for testcontainers configurations (e.g., MySQL, Redis, Kafka) to ensure consistency and predictable initialization across the loopers-spring-java-template project. Apply this pattern to all test-fixture Java files under src/testFixtures/java to preserve uniform lifecycle behavior, unless there's a compelling, well-justified reason to diverge.
Applied to files:
modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java
๐งฌ Code graph analysis (10)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriter.java (2)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessor.java (1)
Slf4j(25-44)apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
Component(30-72)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessor.java (3)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessor.java (1)
Slf4j(30-86)apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationWriter.java (1)
Slf4j(33-81)apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java (1)
Slf4j(65-256)
apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.java (1)
apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.java (1)
Entity(35-165)
apps/commerce-batch/src/main/java/com/loopers/BatchApplication.java (1)
apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt (1)
main(22-24)
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReaderTest.java (1)
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReaderTest.java (1)
ExtendWith(21-151)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt (1)
result(7-19)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java (5)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessor.java (1)
Slf4j(27-73)apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReader.java (1)
Slf4j(36-122)apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessor.java (1)
Slf4j(30-86)apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationWriter.java (1)
Slf4j(33-81)apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.java (1)
Slf4j(36-169)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReader.java (1)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
Component(30-72)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.java (1)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankScoreRepositoryImpl.java (1)
Slf4j(20-99)
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessorTest.java (4)
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReaderTest.java (1)
ExtendWith(23-133)apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReaderTest.java (1)
ExtendWith(21-151)apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriterTest.java (1)
ExtendWith(27-250)apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt (1)
result(7-19)
๐ Additional comments (40)
modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java (1)
45-49: ํ ์ด๋ธ ์กด์ฌ ํ์ธ ์ฟผ๋ฆฌ๋ ์ ์ ํฉ๋๋ค.
information_schema.tables์กฐํ ์ ํ๋ผ๋ฏธํฐํ๋ ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ SQL ์ธ์ ์ ์ ๋ฐฉ์งํ๊ณ ์์ผ๋ฉฐ,DATABASE()ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ํ์ฌ ์คํค๋ง ๋ด์์๋ง ๊ฒ์ํ๋ ๊ฒ์ด ์ฌ๋ฐ๋ฆ ๋๋ค. ๋ฐฐ์น ์์ ์ผ๋ก ์ธํ ์ถ๊ฐ ํ ์ด๋ธ(mv_product_rank๋ฑ)์ด ์ผ๋ถ ํ ์คํธ ์ปจํ ์คํธ์์๋ง ์กด์ฌํ ์ ์๋ ์ํฉ์์ ์ด ๋ฐฉ์ด์ ์ ๊ทผ์ ํฉ๋ฆฌ์ ์ ๋๋ค.settings.gradle.kts (1)
5-5: ๋ชจ๋ ๋ฑ๋ก์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ํ๋์์ต๋๋ค.์๋ก์ด
commerce-batch๋ชจ๋์ด ํ๋ก์ ํธ ๊ตฌ์กฐ์ ์ ํํ๊ฒ ์ถ๊ฐ๋์์ต๋๋ค.apps/commerce-batch/src/main/resources/application.yml (2)
1-13: ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ๊ธฐ๋ณธ ์ค์ ์ด ์ ์ ํฉ๋๋ค.์น ์๋ฒ ๋นํ์ฑํ(
web-application-type: none)์ ํ์ํ ์ค์ ํ์ผ ์ํฌํธ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
14-18: ํ๋ก๋์ ํ๊ฒฝ์์ ์คํค๋ง ์๋ ์ด๊ธฐํ ์ค์ ์ ๊ฒํ ํ์ธ์.
initialize-schema: always์ค์ ์ด ๋ชจ๋ ํ๊ฒฝ์ ์ ์ฉ๋ฉ๋๋ค. ํ๋ก๋์ ํ๊ฒฝ์์๋ ์ด ์ค์ ์ด ์๊ธฐ์น ์์ ์คํค๋ง ์ฌ์์ฑ์ ์ ๋ฐํ๊ฑฐ๋ ๊ธฐ์กด ๋ฐฐ์น ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์์์ํฌ ์ ์์ต๋๋ค.ํ๋กํ๋ณ๋ก ์ค์ ์ ๋ถ๋ฆฌํ์ฌ local/test ํ๊ฒฝ์์๋ง
always๋ฅผ ์ฌ์ฉํ๊ณ , dev/qa/prd ํ๊ฒฝ์์๋never๋ฅผ ์ฌ์ฉํ๋๋ก ์์ ํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ํ๋กํ๋ณ ์ค์ ๋ถ๋ฆฌ ์์
- batch: - jdbc: - initialize-schema: always # Spring Batch ๋ฉํ๋ฐ์ดํฐ ํ ์ด๋ธ ์๋ ์์ฑ - job: - enabled: false # ๋ช ๋ น์ค์์ ์๋ ์คํํ๋ฏ๋ก ์๋ ์คํ ๋นํ์ฑํ + batch: + job: + enabled: false # ๋ช ๋ น์ค์์ ์๋ ์คํํ๋ฏ๋ก ์๋ ์คํ ๋นํ์ฑํ --- spring: config: activate: on-profile: local, test + batch: + jdbc: + initialize-schema: always --- spring: config: activate: on-profile: dev + batch: + jdbc: + initialize-schema: never --- spring: config: activate: on-profile: qa + batch: + jdbc: + initialize-schema: never --- spring: config: activate: on-profile: prd + batch: + jdbc: + initialize-schema: neverโ Skipped due to learnings
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.apps/commerce-batch/build.gradle.kts (1)
1-21: ์์กด์ฑ ๊ตฌ์ฑ์ด ์ ์ ํฉ๋๋ค.๋ฐฐ์น ๋ชจ๋์ ํ์ํ Spring Batch, QueryDSL, ํ ์คํธ ํฝ์ค์ฒ ๋ฑ์ ์์กด์ฑ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ๋์ด ์์ต๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java (1)
33-67: ๋๋ฉ์ธ ์ ์ฅ์ ๋ฉ์๋๊ฐ ์ ์ ํ๊ฒ ์ค๊ณ๋์์ต๋๋ค.
save,findByProductId,findByUpdatedAtBetween๋ฉ์๋๊ฐ ๋ฐฐ์น ์ฒ๋ฆฌ ์๊ตฌ์ฌํญ์ ์ ๋ฐ์ํ๊ณ ์์ผ๋ฉฐ, Spring Data์Page/Pageable์ฌ์ฉ๋ ์ฝ๋๋ฒ ์ด์ค์ ์ํคํ ์ฒ ๊ฒฐ์ ์ ๋ถํฉํฉ๋๋ค.Based on learnings, Spring Data์ Page/Pageable ํ์ ์ฌ์ฉ์ ์ด ์ฝ๋๋ฒ ์ด์ค์์ ํ์ฉ๋ ์ํคํ ์ฒ ๊ฒฐ์ ์ ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRankRepository.java (1)
13-38: ์ธํฐํ์ด์ค๋ ์ฌ๋ฐ๋ฅด๊ฒ ์ค๊ณ๋์์ต๋๋ค - ๋ชจ๋๋ณ ์ฑ ์ ๋ถ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.๊ฒ์ฆ ๊ฒฐ๊ณผ, ์ด ์ธํฐํ์ด์ค(
apps/commerce-api)๋ ์๋์ ์ผ๋ก ์กฐํ ๋ฉ์๋๋ง ํฌํจํ๋๋ก ์ค๊ณ๋์์ต๋๋ค. ์ ์ฅ ๋ฐ ์ญ์ ๋ฉ์๋(saveRanks,deleteByPeriod)๋ ๋ฐฐ์น ๋ชจ๋(apps/commerce-batch)์ ๋ณ๋ ์ธํฐํ์ด์ค์๋ง ์ ์๋์ด ์์ผ๋ฉฐ, ์ค์ ๋ก ๋ฐฐ์น ์์ ์์๋ง ์ฌ์ฉ๋ฉ๋๋ค. ์ด๋ DIP ์๋ฐ์ด ์๋๋ผ ๋ชจ๋๋ณ ์ฑ ์ ๋ถ๋ฆฌ์ ์ฌ๋ฐ๋ฅธ ์์ ๋๋ค:
- API ๋ชจ๋: ์ฝ๊ธฐ ์ ์ฉ (findByPeriod, findByPeriodAndProductId)
- ๋ฐฐ์น ๋ชจ๋: ์ฝ๊ธฐ + ์ฐ๊ธฐ (๋ชจ๋ 4๊ฐ ๋ฉ์๋ ํฌํจ)
AI ์์ฝ์ด ๋ ๋ชจ๋์ ์ธํฐํ์ด์ค๋ฅผ ํผ๋ํ์ผ๋, ์ฝ๋ ์ค๊ณ ์์ฒด๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (1)
125-132: ๋ฒ์ ๊ธฐ๋ฐ ์ ๋ฐ์ดํธ ์ฒดํฌ ๋ก์ง ํ์ธ์ด๋ฒคํธ ๋ฒ์ ๋น๊ต ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค. null ์ฒ๋ฆฌ๋ฅผ ํตํ ํ์ ํธํ์ฑ๋ ์ข์ต๋๋ค.
์ฐธ๊ณ :
versionํ๋๊ฐ JPA์@Version(๋๊ด์ ๋ฝ)๊ณผ ํผ๋๋ ์ ์์ต๋๋ค. ์ด ํ๋๋ ์ด๋ฒคํธ ๋ฒ์ ๊ด๋ฆฌ์ฉ์ด๋ฏ๋ก ๋ช ํํ ๊ตฌ๋ถ๋ฉ๋๋ค.apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessorTest.java (1)
14-86: Pass-through ํ๋ก์ธ์ ํ ์คํธ ์ปค๋ฒ๋ฆฌ์งPass-through ๋ก์ง์ ์ํ ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๊ฐ ์ถฉ๋ถํฉ๋๋ค. PR ๋ชฉํ์ ๋ช ์๋ ๋๋ก ์ ์ฒด Job ํตํฉ ํ ์คํธ๊ฐ ์๋ ํต์ฌ ๋ก์ง์ ๋จ์ ํ ์คํธ์ ์ง์คํ๋ ์ ๋ต๊ณผ ์ผ์นํฉ๋๋ค.
ํ ์คํธ๊ฐ ๋ช ํํ๊ณ ํ๋ก์ธ์์ ์์ ๋์์ ๊ฒ์ฆํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (2)
56-64: ๊ธฐ๊ฐ ํ๋ผ๋ฏธํฐ ๊ธฐ๋ณธ๊ฐ ์ฒ๋ฆฌ
@RequestParam์defaultValue = "DAILY"์parsePeriodType์ DAILY ๊ธฐ๋ณธ๊ฐ์ด ์ค๋ณต๋์ง๋ง ๋ฐฉ์ด์ ์ด๊ณ ์์ ํฉ๋๋ค.ํ์ฌ ๊ตฌํ์ ์๋ชป๋ period ๊ฐ์ ์กฐ์ฉํ DAILY๋ก ํด๋ฐฑํฉ๋๋ค. ์ด๋ ์ฌ์ฉ์ ์นํ์ ์ด์ง๋ง, API ์ฌ์ฉ์๊ฐ ์คํ๋ฅผ ๋ฐ๊ฒฌํ๊ธฐ ์ด๋ ค์ธ ์ ์์ต๋๋ค. ํ๋ก์ ํธ์ ์๋ฌ ์ฒ๋ฆฌ ๋ฐฉ์นจ์ ๋ฐ๋ผ ๋ค์์ ๊ณ ๋ คํ์ธ์:
- ํ์ฌ ๋ฐฉ์ ์ ์ง: ๊ด๋ํ ์ฒ๋ฆฌ๋ก ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์
- ๋๋ 400 ์๋ฌ ๋ฐํ: ๋ช ์์ ํผ๋๋ฐฑ์ผ๋ก API ๊ณ์ฝ ๊ฐํ
ํ์ฌ API ์๋ฌ ์ฒ๋ฆฌ ๋ฐฉ์นจ(๊ด๋ํ ๊ธฐ๋ณธ๊ฐ vs ๋ช ์์ ์๋ฌ)์ ํ์ธํ์ธ์.
112-123: ๊ธฐ๊ฐ ํ์ ํ์ฑ ๋ก์ง ๊ตฌํ
parsePeriodType๋ฉ์๋๊ฐ ์ ๊ตฌํ๋์์ต๋๋ค:
- null/blank ์ฒ๋ฆฌ
- ๋์๋ฌธ์ ๊ตฌ๋ถ ์๋ ํ์ฑ (
toUpperCase())- ์์ ํ ์์ธ ์ฒ๋ฆฌ
์ฝ๋๊ฐ ๋ช ํํ๊ณ ๊ฒฌ๊ณ ํฉ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/BatchApplication.java (1)
25-32: ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์ง์ ์ ๊ตฌํ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์์ต๋๋ค:
@SpringBootApplication์scanBasePackages๊ฐ ์ ์ ํ ์ค์ ๋จ@EnableJpaRepositories์@EntityScan์ด ์ธํ๋ผ ๋ฐ ๋๋ฉ์ธ ํจํค์ง๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ง์ SpringApplication.exitํจํด์ด ๋ฐฐ์น ์์ ์๋ฃ ํ ์ ์ ํ ์ข ๋ฃ ์ฝ๋๋ฅผ ๋ฐํํ๋๋ก ๋ณด์ฅJavadoc์ ์คํ ์์๋ ๋ช ํํ๊ณ ์ ์ฉํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.java (2)
28-38: ๊ธฐ๊ฐ๋ณ ๋ญํน ์กฐํ ์ฟผ๋ฆฌ ๊ตฌํJPQL ์ฟผ๋ฆฌ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค:
- ์ ์ ํ ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ
ORDER BY pr.rank ASC๋ก ์์ ์ ๋ ฌsetMaxResults๋ก ๊ฒฐ๊ณผ ์ ํPR ๋ชฉํ์ ๋ช ์๋ ๋ณตํฉ ์ธ๋ฑ์ค(period_type, period_start_date, rank)๊ฐ ์์ผ๋ฉด ์ด ์ฟผ๋ฆฌ์ ์ฑ๋ฅ์ด ์ต์ ํ๋ฉ๋๋ค.
40-61: ํน์ ์ํ ๋ญํน ์กฐํ ๊ตฌํ๊ฐ๋ณ ์ํ ๋ญํน ์กฐํ๊ฐ ์ ๊ตฌํ๋์์ต๋๋ค:
NoResultException์์ธ๋ฅผ ์ ์ ํ ์ฒ๋ฆฌOptionalํจํด์ ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉ- ๋ช ํํ ์ฟผ๋ฆฌ ๋ก์ง
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessorTest.java (1)
1-120: LGTM! ํ ์คํธ ๊ตฌ์ฑ์ด ์ ๋์ด ์์ต๋๋ค.
ProductRankAggregationProcessor์ ๊ธฐ๊ฐ ์ค์ ๋ก์ง์ ๋ํ ํ ์คํธ๊ฐ ์ฒด๊ณ์ ์ผ๋ก ์์ฑ๋์์ต๋๋ค. ์ฃผ๊ฐ/์๊ฐ ๊ธฐ๊ฐ ๊ณ์ฐ, ๋ค์ํ ๋ ์ง ์ ๋ ฅ์ ๋ํ ๊ฒฝ๊ณ ์ผ์ด์ค, ๊ทธ๋ฆฌ๊ณ ์ฌ๋ฌ ๋ฒ ์ค์ ์ ์ํ ์ ๋ฐ์ดํธ๊ฐ ์ ๊ฒ์ฆ๋๊ณ ์์ต๋๋ค.apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriterTest.java (1)
100-116: LGTM! ํ ์คํธ ํฌํผ ๋ฉ์๋๊ฐ ์ ๊ตฌํ๋์์ต๋๋ค.
createProductMetricsListํฌํผ ๋ฉ์๋๊ฐ ํ ์คํธ ๋ฐ์ดํฐ ์์ฑ์ ํจ๊ณผ์ ์ผ๋ก ์ง์ํฉ๋๋ค.apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemProcessor.java (1)
25-44: LGTM! ํ์ฅ ํฌ์ธํธ๋ก์์ ์ญํ ์ด ๋ช ํํฉ๋๋ค.ํ์ฌ pass-through ๊ตฌํ์ด์ง๋ง, Javadoc์์ ํฅํ ์ง๊ณ/๋ณํ/ํํฐ๋ง ๋ก์ง ์ถ๊ฐ๋ฅผ ์ํ ํ์ฅ ํฌ์ธํธ์์ ์ ์ค๋ช ํ๊ณ ์์ต๋๋ค.
@Slf4j์ด๋ ธํ ์ด์ ์ด ์ ์ธ๋์ด ์์ง๋ง ํ์ฌ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ํฅํ ๋ก์ง ์ถ๊ฐ ์ ์ฌ์ฉ๋ ๊ฒ์ผ๋ก ๋ณด์ด๋ฏ๋ก ์ ์งํด๋ ๋ฌด๋ฐฉํฉ๋๋ค.apps/commerce-batch/src/test/java/com/loopers/domain/rank/ProductRankTest.java (1)
182-233: LGTM! PeriodType enum๊ณผ ๋ญํน ๋ฒ์ ํ ์คํธ๊ฐ ์ ์์ฑ๋์์ต๋๋ค.
PeriodTypeenum ๊ฒ์ฆ๊ณผ TOP 100/1์ ๋ญํน ๊ฒฝ๊ณ ํ ์คํธ๊ฐ ์ ์ ํ๊ฒ ๊ตฌํ๋์์ต๋๋ค.apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessorTest.java (2)
92-129: ThreadLocal ์ ๋ฆฌ ๋์ ํ ์คํธ์ ๋ํ ์ฐธ๊ณ ์ฌํญ์ด ํ ์คํธ๋
ThreadLocal์ ๋ฆฌ ๋์์ ๊ฒ์ฆํ๊ธฐ ์ํด ๋ด๋ถ ๊ตฌํ์ ์์กดํ๊ณ ์์ต๋๋ค. ์ฃผ์์์ ์ค๋ช ํ๋ฏ์ด 101๋ฒ์งธ ์ฒ๋ฆฌ๊ฐ ์ค์ ๋ฐฐ์น ๋์๊ณผ ๋ค๋ฅด์ง๋ง,ThreadLocal์ ๋ฆฌ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ํ๋๋์ง ํ์ธํ๋ ๋ชฉ์ ์ ๋๋ค.์ค์ ๋ฐฐ์น ์คํ ์์๋ 100๊ฐ ์ดํ ํญ๋ชฉ์ด ์ฒ๋ฆฌ๋์ง ์์ผ๋ฏ๋ก, ์ด ํ ์คํธ๊ฐ ๊ตฌํ ๋ณ๊ฒฝ ์ ๊นจ์ง ์ ์์์ ์ธ์งํ์๊ธฐ ๋ฐ๋๋๋ค. ์ฃผ์์ด ์ด๋ฏธ ์ด ์ ์ ์ ์ค๋ช ํ๊ณ ์์ต๋๋ค.
255-261: LGTM! ํ ์คํธ ํฌํผ ๋ฉ์๋๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.
createProductRankScoreํฌํผ๊ฐ ์ ์ ๊ณ์ฐ ๊ณต์(๊ฐ์ค์น 0.3, 0.5, 0.2)์ProductRankScoreAggregationWriterTest์ ๊ณต์๊ณผ ์ผ์นํ๊ฒ ๊ตฌํํ๊ณ ์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriter.java (1)
41-56: ํ์ฌ ์ํ๋ ์๋์ ์ธ ์ค๊ณ์ ๋๋ค.
ProductMetricsItemWriter๊ฐ ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ์ง ์๋ ๊ฒ์ ํ์ธ๋์์ต๋๋ค. ๊ทธ๋ฌ๋ProductMetricsJobConfig์ Javadoc์์ "Writer: ์ง๊ณ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ (ํ์ฌ๋ ๋ก๊น , ํฅํ MV ์ ์ฅ)"์ด๋ผ๊ณ ๋ช ์๋ ๋๋ก ์ด๋ ์๋์ ์ธ ์ค๊ณ์ ๋๋ค.๋ฐ์ดํฐ ์ ์ค ์ฐ๋ ค๋ ์์ต๋๋ค.
ProductRankScoreAggregationWriter๋ ๋ณ๊ฐ์ Step์์ProductMetrics๋ฅผ ์ฝ์ดProductRankScore๋ฅผ ๊ณ์ฐํ์ฌproductRankScoreRepository.saveAll()๋ก ์ค์ DB์ ์ ์ฅํ๋ฏ๋ก, ์ญํ ๊ตฌ๋ถ์ด ๋ช ํํฉ๋๋ค:
ProductMetricsItemWriter: ProductMetrics ๋ก๊น (ํ์ฌ ์ํ), ํฅํ Materialized View ์ ์ฅ ์์ ProductRankScoreAggregationWriter: ProductMetrics๋ฅผ ProductRankScore๋ก ๋ณํํ์ฌ ์ค์ DB ์ ์ฅ (์ด๋ฏธ ๊ตฌํ)apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationProcessor.java (1)
27-53: ๋์ Job ์คํ ์ ์ค๋ ๋ ์์ ์ฑ ๊ฒํ ๊ฐ ํ์ํฉ๋๋ค.์ด ํด๋์ค๋ ์ฑ๊ธํค
@Component์ด์ง๋ง ๊ฐ๋ณ ์ธ์คํด์ค ํ๋(periodType,periodStartDate)๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค. ๋์ผํ Job์ด ๋ค๋ฅธ ํ๋ผ๋ฏธํฐ๋ก ๋์์ ์คํ๋ ๊ฒฝ์ฐ ๊ฒฝ์ ์ํ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.ํ์ฌ ๊ตฌ์กฐ์์๋
productRankReader๊ฐ@StepScope์ด์ง๋ง ์ด Processor๋ ์ฑ๊ธํค์ด๋ฏ๋ก, ๋์ ์คํ ์ ํ Job์setPeriod()ํธ์ถ์ด ๋ค๋ฅธ Job์ ๊ธฐ๊ฐ ์ ๋ณด๋ฅผ ๋ฎ์ด์ธ ์ ์์ต๋๋ค.๋์ ๋ฐฐ์น Job ์คํ์ด ์์๋์ง ์๋๋ค๋ฉด ํ์ฌ ๊ตฌํ์ผ๋ก ์ถฉ๋ถํ์ง๋ง, ๋์ ์คํ์ด ํ์ํ๋ค๋ฉด
@StepScope๋ก ๋ณ๊ฒฝํ๊ฑฐ๋ThreadLocal์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ณ ๋ คํด์ฃผ์ธ์.apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.java (1)
35-118: LGTM!์ํฐํฐ ๊ตฌ์กฐ๊ฐ ์ ์ ํฉ๋๋ค. API ๋ชจ๋์์๋ ์กฐํ ์ ์ฉ์ผ๋ก ์ฌ์ฉ๋๋ฏ๋ก ์์ฑ์ ์์ด
@NoArgsConstructor(access = AccessLevel.PROTECTED)๋ง ์๋ ๊ฒ์ด ์๋๋ ์ค๊ณ์ ๋๋ค. ์ธ๋ฑ์ค ์ ๋ต๋ ๊ธฐ๊ฐ๋ณ ๋ญํน ์กฐํ์ ํน์ ์ํ ์กฐํ์ ์ต์ ํ๋์ด ์์ต๋๋ค.apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriterTest.java (3)
36-93: LGTM!์ง๊ณ ๋ก์ง ํ ์คํธ๊ฐ ์ ์์ฑ๋์์ต๋๋ค. ๊ฐ์
product_id๋ฅผ ๊ฐ์ง ๋ฉํธ๋ฆญ๋ค์ ์ง๊ณ, ๋ค๋ฅธproduct_id์ฒ๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ๊ฒฐ๊ณผ ๊ฒ์ฆ์ด ๋ช ํํฉ๋๋ค.
95-159: LGTM!์ ์ ๊ฐ์ค์น ๊ณ์ฐ ํ ์คํธ์ ๊ธฐ์กด ๋ฐ์ดํฐ ๋์ ํ ์คํธ๊ฐ ์ ์์ฑ๋์์ต๋๋ค. ๊ฐ์ค์น ๊ณต์(
like * 0.3 + sales * 0.5 + view * 0.2)์ด ๋ช ํํ๊ฒ ๊ฒ์ฆ๋ฉ๋๋ค.
161-249: LGTM!๋น Chunk ์ฒ๋ฆฌ, ๋ค์ค product_id ์ฒ๋ฆฌ, ์ ๋ฐ์ดํฐ ์์ฑ ํ ์คํธ๊ฐ edge case๋ฅผ ์ ์ปค๋ฒํฉ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemReader.java (1)
97-109: ๋ ์ง ํ์ฑ ์คํจ ์ ์ค๋ ๋ ์ง๋ก ๋์ฒดํ๋ ๋์์ ์ฌ๊ณ ํด์ฃผ์ธ์.์๋ชป๋ ๋ ์ง ํ๋ผ๋ฏธํฐ๊ฐ ์ ๋ฌ๋๋ฉด ๊ฒฝ๊ณ ๋ง ๋ก๊น ํ๊ณ ์ค๋ ๋ ์ง๋ก ์งํํฉ๋๋ค. ์ค์ผ์ค๋ง๋ ๋ฐฐ์น ์์ ์์ ์ด๋ ์๋ชป๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ก ์ด์ด์ง ์ ์์ต๋๋ค.
์๋์ ์ธ ์ค๊ณ๋ผ๋ฉด ํ์ฌ ์ํ๋ก ์ ์งํด๋ ๋์ง๋ง, ์๊ฒฉํ ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ์ด ํ์ํ ๊ฒฝ์ฐ ์์ธ๋ฅผ ๋์ง๋ ๊ฒ์ ๊ณ ๋ คํด์ฃผ์ธ์:
private LocalDate parseDate(String dateStr) { if (dateStr == null || dateStr.isEmpty()) { throw new IllegalArgumentException("๋ ์ง ํ๋ผ๋ฏธํฐ๊ฐ ํ์์ ๋๋ค."); } try { return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyyMMdd")); } catch (DateTimeParseException e) { throw new IllegalArgumentException("์๋ชป๋ ๋ ์ง ํ์: " + dateStr, e); } }apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScore.java (1)
124-138: LGTM!์์ฑ์๊ฐ ๋ชจ๋ ํ์ ํ๋๋ฅผ ์ด๊ธฐํํ๊ณ ํ์์คํฌํ๋ฅผ ์ค์ ํฉ๋๋ค. ๋ฐฐ์น ์ฒ๋ฆฌ์ ์์ ํ ์ด๋ธ์ฉ ์ํฐํฐ๋ก ์ ์ ํ ๊ตฌ์กฐ์ ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankRepository.java (1)
7-58: LGTM!Repository ์ธํฐํ์ด์ค๊ฐ ์ ์ค๊ณ๋์์ต๋๋ค. ๊ธฐ๊ฐ๋ณ ๋ญํน ์กฐํ, ํน์ ์ํ ์กฐํ, ์ ์ฅ/์ญ์ ๋ฉ์๋๊ฐ Materialized View ํจํด์ ์ ํฉํ๊ฒ ์ ์๋์ด ์์ต๋๋ค. Javadoc๋ ๋ช ํํฉ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRankScoreRepository.java (1)
12-67: LGTM!์ธํฐํ์ด์ค ์ค๊ณ๊ฐ ๋ช ํํ๊ณ , UPSERT ๋์ ๋ฐ ์ฉ๋๊ฐ Javadoc์ ์ ๋ฌธ์ํ๋์ด ์์ต๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/infrastructure/rank/ProductRankScoreRepositoryImpl.java (1)
59-71:NoResultException์ฒ๋ฆฌ ๋ฐฉ์ ์ ์ ์์ธ ๊ธฐ๋ฐ Optional ์ฒ๋ฆฌ๊ฐ ์ ํํ๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.java (1)
35-42: ์ธ๋ฑ์ค ์ค๊ณ ์ ์ ๊ธฐ๊ฐ๋ณ ๋ญํน ์กฐํ ๋ฐ ํน์ ์ํ ๋ญํน ์กฐํ ์ฟผ๋ฆฌ ํจํด์ ๋ง๋ ๋ณตํฉ ์ธ๋ฑ์ค๊ฐ ์ ์ค๊ณ๋์ด ์์ต๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
30-72: LGTM!JPA ๋ ํฌ์งํ ๋ฆฌ์ ๋ํ ์์ ํจํด์ด ๊น๋ํ๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค.
getJpaRepository()๋ Spring Batch์RepositoryItemReaderAPI ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ๊ธฐ ์ํ ์ ์ ํ ์ ๊ทผ ๋ฐฉ์์ ๋๋ค.apps/commerce-batch/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.java (1)
162-215: shouldUpdate ๋ก์ง ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง ์ฐ์์ด๋ฒคํธ ๋ฒ์ ๊ณผ ๋ฉํธ๋ฆญ ๋ฒ์ ๋น๊ต ๋ก์ง์ ๋ํ ํ ์คํธ๊ฐ ์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค. null ํ์ ํธํ์ฑ, ์ด๊ธฐ ๋ฒ์ ์ฒ๋ฆฌ ๋ฑ ๋ค์ํ ์ฃ์ง ์ผ์ด์ค๋ฅผ ๋ค๋ฃจ๊ณ ์์ต๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/metrics/ProductMetricsJobConfig.java (2)
74-109: Job ๋ฐ Step ๊ตฌ์ฑ์ด ์ ์ ํฉ๋๋คChunk ํฌ๊ธฐ 100, StepScope Reader ํ์ฉ, ๋ช ํํ ๋ฌธ์ํ ๋ฑ Spring Batch ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.
120-126: ๋ฌธ์ ์์ - null ํ๋ผ๋ฏธํฐ ์ฒ๋ฆฌ๊ฐ ์ด๋ฏธ ๊ตฌํ๋์ด ์์ต๋๋ค
ProductMetricsItemReader.createReader()๋ฉ์๋ ๋ด๋ถ์parseDate()๋ฉ์๋(๋ผ์ธ 97-109)์์ ์ด๋ฏธ null ๋ฐ ๋น ๋ฌธ์์ด์ ๋ํ ์ฒ๋ฆฌ๊ฐ ๊ตฌํ๋์ด ์์ต๋๋ค. null์ธ ๊ฒฝ์ฐ ์ค๋ ๋ ์ง๋ฅผ ๋ฐํํ๋ฉฐ, ํ์ฑ ์คํจ ์์๋ ๋์ผํ๊ฒ ์ฒ๋ฆฌ๋ฉ๋๋ค. ์ด๋ProductRankJobConfig์์ ์ฌ์ฉ๋๋ ํจํด๊ณผ ๋์ผํฉ๋๋ค.apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReader.java (1)
52-65: ์ฃผ๊ฐ ๋ ์ง ๋ฒ์ ๊ณ์ฐ ์ ํ์์์ผ๋ถํฐ ๋ค์ ์ฃผ ์์์ผ 00:00:00๊น์ง์ ๋ฒ์๊ฐ ์ ํํ๊ฒ ๊ณ์ฐ๋์ด ์์ต๋๋ค. exclusive end date ํจํด์ ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉํ๊ณ ์์ต๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java (1)
93-102: 2-Step Job ๊ตฌ์กฐ ์ ์ Step 1์์ ์ ์ ์ง๊ณ ํ Step 2์์ ๋ญํน ๊ณ์ฐํ๋ ๊ตฌ์กฐ๊ฐ ๋ช ํํฉ๋๋ค.
start().next()์ฒด์ธ์ผ๋ก ์์ฐจ ์คํ์ด ๋ณด์ฅ๋ฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (2)
79-87: LGTM!๊ธฐ๊ฐ ํ์ ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ ์ฒ๋ฆฌ๊ฐ ๋ช ํํ๊ณ , ๊ธฐ์กด Redis ๊ธฐ๋ฐ ์ผ๊ฐ ๋ญํน๊ณผ ์๋ก์ด Materialized View ๊ธฐ๋ฐ ์ฃผ๊ฐ/์๊ฐ ๋ญํน์ ๊น๋ํ๊ฒ ๋ถ๋ฆฌํ์ต๋๋ค.
474-478: ๋ฐฐ์น ์์ ๊ณผ์ ์ ์ ๊ณ์ฐ ๋ก์ง์ ์ด๋ฏธ ์ผ๊ด์ฑ์ ์ ์งํ๊ณ ์์ต๋๋ค.API์
calculateScore๋ฉ์๋๋ ๋ฐฐ์น ์์ ์ProductRankScoreAggregationWriter์์ ์ฌ์ฉํ๋ ๊ฒ๊ณผ ๋์ผํ ๊ฐ์ค์น(์ข์์ 0.3, ํ๋งค๋ 0.5, ์กฐํ์ 0.2)๋ฅผ ์ ์ฉํ๊ณ ์์ต๋๋ค. ์ ์ผํ ์ฐจ์ด๋ API์์ null ๊ฐ์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ผํญ ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ธ๋ฐ, ์ด๋ Materialized View ์กฐํ ๊ฒฐ๊ณผ์ null ๊ฐ์ด ์ฌ ์ ์๊ธฐ ๋๋ฌธ์ ์ ์ ํฉ๋๋ค.Likely an incorrect or invalid review comment.
| // ๋ญํน ํญ๋ชฉ ์์ฑ | ||
| List<RankingItem> rankingItems = new ArrayList<>(); | ||
| for (com.loopers.domain.rank.ProductRank rank : pagedRanks) { | ||
| Long productId = rank.getProductId(); | ||
| Product product = productMap.get(productId); | ||
|
|
||
| if (product == null) { | ||
| log.warn("๋ญํน์ ํฌํจ๋ ์ํ์ ์ฐพ์ ์ ์์ต๋๋ค: productId={}", productId); | ||
| continue; | ||
| } | ||
|
|
||
| Brand brand = brandMap.get(product.getBrandId()); | ||
| if (brand == null) { | ||
| log.warn("์ํ์ ๋ธ๋๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค: productId={}, brandId={}", | ||
| productId, product.getBrandId()); | ||
| continue; | ||
| } | ||
|
|
||
| ProductDetail productDetail = ProductDetail.from( | ||
| product, | ||
| brand.getName(), | ||
| rank.getLikeCount() | ||
| ); | ||
|
|
||
| // ์ข ํฉ ์ ์ ๊ณ์ฐ (Materialized View์๋ ์ ์ฅ๋์ง ์์ผ๋ฏ๋ก ๊ณ์ฐ) | ||
| double score = calculateScore(rank.getLikeCount(), rank.getSalesCount(), rank.getViewCount()); | ||
|
|
||
| rankingItems.add(new RankingItem( | ||
| rank.getRank().longValue(), | ||
| score, | ||
| productDetail | ||
| )); | ||
| } |
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.
์ํ/๋ธ๋๋ ๋๋ฝ ์ ์์ ๊ฐ๊ฒฉ(gap) ๋ฐ์ ๊ฐ๋ฅ์ฑ ํ์ธ ํ์.
์ํ์ด๋ ๋ธ๋๋๊ฐ ์กฐํ๋์ง ์์ ๊ฒฝ์ฐ continue๋ก ๊ฑด๋๋ฐ์ง๋ง, ์๋ต์์๋ DB์ ์ ์ฅ๋ rank ๊ฐ์ ๊ทธ๋๋ก ์ฌ์ฉํฉ๋๋ค. ์ด๋ก ์ธํด API ์๋ต์์ ์์๊ฐ 1, 2, 5, 6๊ณผ ๊ฐ์ด ๋ถ์ฐ์์ ์ผ๋ก ๋ํ๋ ์ ์์ต๋๋ค.
์๋๋ ๋์์ด๋ผ๋ฉด ๋ฌด์ํด๋ ๋์ง๋ง, ์ฐ์์ ์ธ ์์๋ฅผ ๊ธฐ๋ํ๋ ํด๋ผ์ด์ธํธ๊ฐ ์๋ค๋ฉด ์ด์๊ฐ ๋ ์ ์์ต๋๋ค. ๋ ๊ฐ์ง ์ ๊ทผ ๋ฐฉ์์ ๊ณ ๋ คํด ๋ณผ ์ ์์ต๋๋ค:
- ํ์ฌ ๋ฐฉ์ ์ ์ง: ์๋ ์์ ๋ณด์กด (๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ์ฐ์ )
- ์์ ์ฌ๊ณ์ฐ: ๋๋ฝ๋ ํญ๋ชฉ ์ ์ธ ํ ์ฐ์ ์์ ๋ถ์ฌ (UX ์ฐ์ )
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java
around lines 420 to 452, the loop skips missing products/brands but keeps the
original DB rank value, causing gaps in returned ranks; update the construction
so that after filtering out missing items you assign continuous ranks to
rankingItems (e.g., maintain a local counter starting at 1 and increment per
added item) instead of using rank.getRank(), while preserving score calculation
and product details; optionally make this behavior conditional via a flag/config
if you need to support preserving original DB ranks in some cases.
| * <b>Materialized View ์ค๊ณ:</b> | ||
| * <ul> | ||
| * <li>์ฃผ๊ฐ ๋ญํน: `mv_product_rank_weekly` (period_type = WEEKLY)</li> | ||
| * <li>์๊ฐ ๋ญํน: `mv_product_rank_monthly` (period_type = MONTHLY)</li> | ||
| * <li>TOP 100๋ง ์ ์ฅํ์ฌ ์กฐํ ์ฑ๋ฅ ์ต์ ํ</li> | ||
| * </ul> | ||
| * </p> |
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.
Javadoc๊ณผ ์ค์ ํ ์ด๋ธ ๊ตฌ์กฐ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.
Javadoc์์๋ mv_product_rank_weekly, mv_product_rank_monthly ๋ ๊ฐ์ ํ
์ด๋ธ์ ์ธ๊ธํ๊ณ ์์ง๋ง, ์ค์ ๊ตฌํ์ period_type ์ปฌ๋ผ์ ๊ฐ์ง ๋จ์ผ ํ
์ด๋ธ mv_product_rank๋ฅผ ์ฌ์ฉํฉ๋๋ค.
๐ ์์ ์ ์
* <b>Materialized View ์ค๊ณ:</b>
* <ul>
- * <li>์ฃผ๊ฐ ๋ญํน: `mv_product_rank_weekly` (period_type = WEEKLY)</li>
- * <li>์๊ฐ ๋ญํน: `mv_product_rank_monthly` (period_type = MONTHLY)</li>
+ * <li>์ฃผ๊ฐ ๋ญํน: period_type = WEEKLY</li>
+ * <li>์๊ฐ ๋ญํน: period_type = MONTHLY</li>
* <li>TOP 100๋ง ์ ์ฅํ์ฌ ์กฐํ ์ฑ๋ฅ ์ต์ ํ</li>
* </ul>๐ค Prompt for AI Agents
apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.java lines
17-23: the Javadoc incorrectly describes two separate materialized views
(`mv_product_rank_weekly`, `mv_product_rank_monthly`) while the implementation
uses a single table `mv_product_rank` with a `period_type` column; update the
Javadoc to describe the single materialized view `mv_product_rank`, explain that
`period_type` distinguishes WEEKLY vs MONTHLY, and retain the note about storing
TOP 100 for performance (or alternatively, if you intended separate tables,
change the implementation to match the two-view description).
| @Slf4j | ||
| @Repository | ||
| public class ProductRankRepositoryImpl implements ProductRankRepository { |
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.
ํธ๋์ญ์ ๊ด๋ฆฌ ๋๋ฝ
Repository ๊ตฌํ์ฒด์ @Transactional ์ด๋
ธํ
์ด์
์ด ์์ต๋๋ค. EntityManager๋ฅผ ์ฌ์ฉํ๋ JPA ์์
์๋ ํธ๋์ญ์
์ปจํ
์คํธ๊ฐ ํ์ํฉ๋๋ค.
ํ์ฌ ์ฝ๋๋ ์ฝ๊ธฐ ์ ์ฉ ๋ฉ์๋๋ง ํ์๋์ด ์์ง๋ง, AI ์์ฝ์๋ saveRanks์ deleteByPeriod ๊ฐ์ ์ฐ๊ธฐ ์์
์ด ์ธ๊ธ๋์ด ์์ต๋๋ค. ์ด๋ฌํ ์์
์๋ ํธ๋์ญ์
์ด ํ์์ ์
๋๋ค.
๐ ํธ๋์ญ์ ์ด๋ ธํ ์ด์ ์ถ๊ฐ
ํด๋์ค ๋ ๋ฒจ์ ์ฝ๊ธฐ ์ ์ฉ ํธ๋์ญ์ ์ ์ถ๊ฐํ๊ณ , ์ฐ๊ธฐ ๋ฉ์๋์๋ ๊ฐ๋ณ์ ์ผ๋ก ์ ์ฉ:
@Slf4j
@Repository
+@Transactional(readOnly = true)
public class ProductRankRepositoryImpl implements ProductRankRepository {์ฐ๊ธฐ ๋ฉ์๋(saveRanks, deleteByPeriod ๋ฑ)์๋:
@Transactional
public void saveRanks(...) {
// implementation
}๐ 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.
| @Slf4j | |
| @Repository | |
| public class ProductRankRepositoryImpl implements ProductRankRepository { | |
| @Slf4j | |
| @Repository | |
| @Transactional(readOnly = true) | |
| public class ProductRankRepositoryImpl implements ProductRankRepository { |
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.java
around lines 20-22, the repository implementation lacks transaction management:
add class-level @Transactional(readOnly = true) to ensure a transactional
context for read operations and then annotate mutating methods (e.g., saveRanks,
deleteByPeriod) individually with @Transactional (without readOnly) so write
operations run in a proper transaction; also import
org.springframework.transaction.annotation.Transactional and place the
annotations on the class and the specific write methods.
| @SuppressWarnings("rawtypes") | ||
| org.springframework.data.repository.PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository(); | ||
| } |
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
ํ์ ์์ ์ฑ์ ๊ฐ์ ํ์ธ์.
getJpaRepository() ๋ฉ์๋๊ฐ raw type์ ๋ฐํํ๊ณ @SuppressWarnings("rawtypes")๋ก ๊ฒฝ๊ณ ๋ฅผ ์ต์ ํ๊ณ ์์ต๋๋ค. Spring Batch์ RepositoryItemReader๊ฐ PagingAndSortingRepository๋ฅผ ์๊ตฌํ๋๋ผ๋, ํ์
์์ ์ฑ์ ์ ์งํ ์ ์๋ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.
๐ ์ ๋ค๋ฆญ ํ์ ์ ๋ช ์ํ ๊ฐ์ ์
/**
* Spring Batch์ RepositoryItemReader์์ ์ฌ์ฉํ๊ธฐ ์ํ JPA Repository๋ฅผ ๋ฐํํฉ๋๋ค.
* <p>
* RepositoryItemReader๋ PagingAndSortingRepository๋ฅผ ์ง์ ์๊ตฌํ๋ฏ๋ก,
* ๊ธฐ์ ์ ์ ์ฝ์ผ๋ก ์ธํด JPA Repository์ ๋ํ ์ ๊ทผ์ ์ ๊ณตํฉ๋๋ค.
* </p>
* <p>
* <b>์ฃผ์:</b> ์ด ๋ฉ์๋๋ Spring Batch์ ๊ธฐ์ ์ ์๊ตฌ์ฌํญ์ผ๋ก ์ธํด ์ ๊ณต๋ฉ๋๋ค.
* ์ผ๋ฐ์ ์ธ ๋น์ฆ๋์ค ๋ก์ง์์๋ ์ด ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ,
* ์์ ๋๋ฉ์ธ ๋ฉ์๋๋ค์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
* </p>
*
* @return PagingAndSortingRepository๋ฅผ ๊ตฌํํ JPA Repository
*/
- @SuppressWarnings("rawtypes")
- org.springframework.data.repository.PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository();
+ org.springframework.data.repository.PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository();๊ตฌํ์ฒด(ProductMetricsRepositoryImpl)์์๋ ์ ๋ค๋ฆญ ํ์
์ ๋ช
์ํ์ฌ ๋ฐํํ๋ฉด ๊ฒฝ๊ณ ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค.
๐ 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.
| @SuppressWarnings("rawtypes") | |
| org.springframework.data.repository.PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository(); | |
| } | |
| /** | |
| * Spring Batch์ RepositoryItemReader์์ ์ฌ์ฉํ๊ธฐ ์ํ JPA Repository๋ฅผ ๋ฐํํฉ๋๋ค. | |
| * <p> | |
| * RepositoryItemReader๋ PagingAndSortingRepository๋ฅผ ์ง์ ์๊ตฌํ๋ฏ๋ก, | |
| * ๊ธฐ์ ์ ์ ์ฝ์ผ๋ก ์ธํด JPA Repository์ ๋ํ ์ ๊ทผ์ ์ ๊ณตํฉ๋๋ค. | |
| * </p> | |
| * <p> | |
| * <b>์ฃผ์:</b> ์ด ๋ฉ์๋๋ Spring Batch์ ๊ธฐ์ ์ ์๊ตฌ์ฌํญ์ผ๋ก ์ธํด ์ ๊ณต๋ฉ๋๋ค. | |
| * ์ผ๋ฐ์ ์ธ ๋น์ฆ๋์ค ๋ก์ง์์๋ ์ด ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ง ์๊ณ , | |
| * ์์ ๋๋ฉ์ธ ๋ฉ์๋๋ค์ ์ฌ์ฉํด์ผ ํฉ๋๋ค. | |
| * </p> | |
| * | |
| * @return PagingAndSortingRepository๋ฅผ ๊ตฌํํ JPA Repository | |
| */ | |
| org.springframework.data.repository.PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository(); | |
| } |
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
around lines 83-85, the method currently suppresses raw-type warnings; change
the signature to return a parameterized repository type instead of a raw type
and remove the @SuppressWarnings: declare the method as
PagingAndSortingRepository<ProductMetrics, Long> getJpaRepository(); then update
the implementing class ProductMetricsRepositoryImpl to match that exact generic
return type and adjust imports so the code compiles without raw-type warnings.
| private final ThreadLocal<Integer> currentRank = ThreadLocal.withInitial(() -> 0); | ||
| private static final int TOP_RANK_LIMIT = 100; |
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.
ThreadLocal ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐ ๋ญํน ์ค๋ฅ ๊ฐ๋ฅ์ฑ
currentRank์ remove()๊ฐ rank == 100์ผ ๋๋ง ํธ์ถ๋ฉ๋๋ค. ๋ค์ ์ํฉ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค:
- ์ํ์ด 100๊ฐ ๋ฏธ๋ง์ธ ๊ฒฝ์ฐ ThreadLocal์ด ์ ๋ฆฌ๋์ง ์์
- Job ์คํจ ์ ๋ค์ ์คํ์์ ์ด์ ๊ฐ์ด ๋จ์์์
- ์ค๋ ๋ ์ฌ์ฌ์ฉ ์ ์๋ชป๋ ๋ญํน ๋ฒํธ ๋ถ์ฌ
๐ StepExecutionListener๋ก ์์ ํ ์ด๊ธฐํ/์ ๋ฆฌ ์ ์
@Slf4j
@Component
@RequiredArgsConstructor
-public class ProductRankCalculationProcessor implements ItemProcessor<ProductRankScore, ProductRank> {
+public class ProductRankCalculationProcessor implements ItemProcessor<ProductRankScore, ProductRank>, StepExecutionListener {
private final ProductRankAggregationProcessor productRankAggregationProcessor;
private final ThreadLocal<Integer> currentRank = ThreadLocal.withInitial(() -> 0);
private static final int TOP_RANK_LIMIT = 100;
+ @Override
+ public void beforeStep(StepExecution stepExecution) {
+ currentRank.set(0);
+ }
+
+ @Override
+ public ExitStatus afterStep(StepExecution stepExecution) {
+ currentRank.remove();
+ return stepExecution.getExitStatus();
+ }
+
@Override
public ProductRank process(ProductRankScore score) throws Exception {
int rank = currentRank.get() + 1;
currentRank.set(rank);
if (rank > TOP_RANK_LIMIT) {
return null;
}
// ...
- if (rank == TOP_RANK_LIMIT) {
- currentRank.remove();
- }
return productRank;
}Also applies to: 79-82
| LocalDate date = parseDate(targetDate); | ||
| ProductRank.PeriodType period = ProductRank.PeriodType.valueOf(periodType.toUpperCase()); |
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.
periodType null ์ฒดํฌ ํ์
periodType์ด Job ํ๋ผ๋ฏธํฐ์์ ์ฃผ์
๋์ง ์์ผ๋ฉด null์ด ๋ ์ ์์ผ๋ฉฐ, toUpperCase() ํธ์ถ ์ NullPointerException์ด ๋ฐ์ํฉ๋๋ค.
๐ null ๊ฒ์ฆ ์ถ๊ฐ ์ ์
public ItemReader<ProductMetrics> productRankReader(
@Value("#{jobParameters['periodType']}") String periodType,
@Value("#{jobParameters['targetDate']}") String targetDate
) {
+ if (periodType == null || periodType.isEmpty()) {
+ throw new IllegalArgumentException("periodType ํ๋ผ๋ฏธํฐ๋ ํ์์
๋๋ค. (WEEKLY ๋๋ MONTHLY)");
+ }
+
LocalDate date = parseDate(targetDate);
ProductRank.PeriodType period = ProductRank.PeriodType.valueOf(periodType.toUpperCase());๐ 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.
| LocalDate date = parseDate(targetDate); | |
| ProductRank.PeriodType period = ProductRank.PeriodType.valueOf(periodType.toUpperCase()); | |
| public ItemReader<ProductMetrics> productRankReader( | |
| @Value("#{jobParameters['periodType']}") String periodType, | |
| @Value("#{jobParameters['targetDate']}") String targetDate | |
| ) { | |
| if (periodType == null || periodType.isEmpty()) { | |
| throw new IllegalArgumentException("periodType ํ๋ผ๋ฏธํฐ๋ ํ์์ ๋๋ค. (WEEKLY ๋๋ MONTHLY)"); | |
| } | |
| LocalDate date = parseDate(targetDate); | |
| ProductRank.PeriodType period = ProductRank.PeriodType.valueOf(periodType.toUpperCase()); |
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java
around lines 184-185, the code calls periodType.toUpperCase() without checking
for null which can cause a NullPointerException; add a null/blank check before
calling toUpperCase() (e.g., if periodType is null or blank, either set a
sensible default PeriodType or throw an IllegalArgumentException with a clear
message), then use periodType.trim().toUpperCase() to create the
ProductRank.PeriodType via valueOf; ensure the error path logs or surfaces a
descriptive message indicating the missing/invalid job parameter.
| List<ProductRankScore> scores = chunkAggregatedMap.entrySet().stream() | ||
| .map(entry -> { | ||
| Long productId = entry.getKey(); | ||
| AggregatedMetrics chunkAggregated = entry.getValue(); | ||
|
|
||
| // ๊ธฐ์กด ๋ฐ์ดํฐ ์กฐํ | ||
| java.util.Optional<ProductRankScore> existing = productRankScoreRepository.findByProductId(productId); | ||
|
|
||
| // ๊ธฐ์กด ๋ฐ์ดํฐ์ ๋์ | ||
| Long totalLikeCount = chunkAggregated.getLikeCount(); | ||
| Long totalSalesCount = chunkAggregated.getSalesCount(); | ||
| Long totalViewCount = chunkAggregated.getViewCount(); | ||
|
|
||
| if (existing.isPresent()) { | ||
| ProductRankScore existingScore = existing.get(); | ||
| totalLikeCount += existingScore.getLikeCount(); | ||
| totalSalesCount += existingScore.getSalesCount(); | ||
| totalViewCount += existingScore.getViewCount(); | ||
| } |
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.
N+1 ์ฟผ๋ฆฌ ํจํด์ผ๋ก ์ธํ ์ฑ๋ฅ ์ ํ ์ฐ๋ ค
Chunk ๋ด ๊ฐ productId๋ง๋ค findByProductId๋ฅผ ๊ฐ๋ณ ํธ์ถํ์ฌ N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค. Chunk ํฌ๊ธฐ๊ฐ ํฌ๊ฑฐ๋ ๊ณ ์ ์ํ์ด ๋ง์ ๊ฒฝ์ฐ ์ฑ๋ฅ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค.
๐ ์ผ๊ด ์กฐํ ๋ฐฉ์์ผ๋ก ๊ฐ์ ์ ์
+ // Chunk ๋ด ๋ชจ๋ productId๋ฅผ ํ ๋ฒ์ ์กฐํ
+ Set<Long> productIds = chunkAggregatedMap.keySet();
+ Map<Long, ProductRankScore> existingScores = productRankScoreRepository
+ .findAllByProductIdIn(productIds)
+ .stream()
+ .collect(Collectors.toMap(ProductRankScore::getProductId, Function.identity()));
+
List<ProductRankScore> scores = chunkAggregatedMap.entrySet().stream()
.map(entry -> {
Long productId = entry.getKey();
AggregatedMetrics chunkAggregated = entry.getValue();
- // ๊ธฐ์กด ๋ฐ์ดํฐ ์กฐํ
- java.util.Optional<ProductRankScore> existing = productRankScoreRepository.findByProductId(productId);
+ // ๊ธฐ์กด ๋ฐ์ดํฐ ์กฐํ (์ผ๊ด ์กฐํ ๊ฒฐ๊ณผ์์)
+ ProductRankScore existing = existingScores.get(productId);
// ๊ธฐ์กด ๋ฐ์ดํฐ์ ๋์
Long totalLikeCount = chunkAggregated.getLikeCount();
Long totalSalesCount = chunkAggregated.getSalesCount();
Long totalViewCount = chunkAggregated.getViewCount();
- if (existing.isPresent()) {
- ProductRankScore existingScore = existing.get();
- totalLikeCount += existingScore.getLikeCount();
+ if (existing != null) {
+ totalLikeCount += existing.getLikeCount();ProductRankScoreRepository์ findAllByProductIdIn(Set<Long> productIds) ๋ฉ์๋ ์ถ๊ฐ๊ฐ ํ์ํฉ๋๋ค.
Committable suggestion skipped: line range outside the PR's diff.
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.java
around lines 84-102, the code calls productRankScoreRepository.findByProductId
for each productId causing an N+1 query problem; instead add a repository method
findAllByProductIdIn(Set<Long> productIds) (or equivalent) and before mapping
collect the set of productIds from chunkAggregatedMap, call the new batch method
once, convert the returned list to a Map<Long, ProductRankScore>, and then use
that map to look up existing scores while computing
totalLikeCount/totalSalesCount/totalViewCount so all existing records are
fetched in a single query.
| * @param startDateTime ์กฐํ ์์ ์๊ฐ (ํด๋น ๋ ์ง์ 00:00:00) | ||
| * @param endDateTime ์กฐํ ์ข ๋ฃ ์๊ฐ (ํด๋น ๋ ์ง์ 23:59:59.999999999) | ||
| * @param pageable ํ์ด์ง ์ ๋ณด | ||
| * @return ์กฐํ๋ ๋ฉํธ๋ฆญ ํ์ด์ง | ||
| */ | ||
| @Query("SELECT pm FROM ProductMetrics pm " + | ||
| "WHERE pm.updatedAt >= :startDateTime AND pm.updatedAt < :endDateTime " + | ||
| "ORDER BY pm.productId") | ||
| Page<ProductMetrics> findByUpdatedAtBetween( | ||
| @Param("startDateTime") LocalDateTime startDateTime, | ||
| @Param("endDateTime") LocalDateTime endDateTime, | ||
| Pageable pageable | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Javadoc๊ณผ ์ฟผ๋ฆฌ ์กฐ๊ฑด ๋ถ์ผ์น
Javadoc์์ endDateTime์ "23:59:59.999999999"๋ก ์ค๋ช
ํ์ง๋ง, ์ฟผ๋ฆฌ๋ < :endDateTime์ ์ฌ์ฉํฉ๋๋ค. ์ค์ ํธ์ถ ์ ๋ค์ ๋ 00:00:00์ ์ ๋ฌํด์ผ ์ ํํฉ๋๋ค. Javadoc์ ์์ ํ๊ฑฐ๋ ํธ์ถ๋ถ์ ์ผ๊ด์ฑ์ ํ์ธํด ์ฃผ์ธ์.
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java
around lines 44 to 56, the Javadoc claims endDateTime represents
"23:59:59.999999999" but the JPQL uses a strict less-than (< :endDateTime),
causing an off-by-one-day behavior; fix by making them consistent: either update
the Javadoc to state endDateTime is exclusive (e.g., the caller should pass
nextDay at 00:00:00) or change the query to use a non-strict comparison (<=
:endDateTime) if you intend endDateTime to be inclusive, and ensure any callers
are adjusted accordingly.
| @DisplayName("Chunk์ ๋ชจ๋ ํญ๋ชฉ์ ์ฒ๋ฆฌํ๋ค") | ||
| @Test | ||
| void writesChunk_processesAllItems() throws Exception { | ||
| // arrange | ||
| int itemCount = 10; | ||
| List<ProductMetrics> items = createProductMetricsList(itemCount); | ||
| Chunk<ProductMetrics> chunk = new Chunk<>(items); | ||
|
|
||
| // act | ||
| writer.write(chunk); | ||
|
|
||
| // assert | ||
| // ํ์ฌ๋ ๋ก๊น ๋ง ์ํํ๋ฏ๋ก ์์ธ๊ฐ ๋ฐ์ํ์ง ์์ผ๋ฉด ์ฑ๊ณต | ||
| // ํฅํ Materialized View ์ ์ฅ ๋ก์ง ์ถ๊ฐ ์ ์ถ๊ฐ ๊ฒ์ฆ ํ์ | ||
| assertThatCode(() -> writer.write(chunk)) | ||
| .doesNotThrowAnyException(); | ||
| } |
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.
์ค๋ณต๋ write ํธ์ถ์ด ์์ต๋๋ค.
writesChunk_processesAllItems ํ
์คํธ์์ writer.write(chunk)๊ฐ ๋ ๋ฒ ํธ์ถ๋ฉ๋๋ค (Line 91๊ณผ Line 96). ์ฒซ ๋ฒ์งธ ํธ์ถ ํ ๋ ๋ฒ์งธ ํธ์ถ์ ์ถ๊ฐ์ ์ธ ๊ฒ์ฆ ๊ฐ์น๊ฐ ์์ผ๋ฉฐ, ํ
์คํธ ์๋๊ฐ ๋ถ๋ช
ํํด์ง๋๋ค.
๐ ์์ ์ ์
@DisplayName("Chunk์ ๋ชจ๋ ํญ๋ชฉ์ ์ฒ๋ฆฌํ๋ค")
@Test
void writesChunk_processesAllItems() throws Exception {
// arrange
int itemCount = 10;
List<ProductMetrics> items = createProductMetricsList(itemCount);
Chunk<ProductMetrics> chunk = new Chunk<>(items);
- // act
- writer.write(chunk);
-
- // assert
+ // act & assert
// ํ์ฌ๋ ๋ก๊น
๋ง ์ํํ๋ฏ๋ก ์์ธ๊ฐ ๋ฐ์ํ์ง ์์ผ๋ฉด ์ฑ๊ณต
// ํฅํ Materialized View ์ ์ฅ ๋ก์ง ์ถ๊ฐ ์ ์ถ๊ฐ ๊ฒ์ฆ ํ์
assertThatCode(() -> writer.write(chunk))
.doesNotThrowAnyException();
}๐ 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.
| @DisplayName("Chunk์ ๋ชจ๋ ํญ๋ชฉ์ ์ฒ๋ฆฌํ๋ค") | |
| @Test | |
| void writesChunk_processesAllItems() throws Exception { | |
| // arrange | |
| int itemCount = 10; | |
| List<ProductMetrics> items = createProductMetricsList(itemCount); | |
| Chunk<ProductMetrics> chunk = new Chunk<>(items); | |
| // act | |
| writer.write(chunk); | |
| // assert | |
| // ํ์ฌ๋ ๋ก๊น ๋ง ์ํํ๋ฏ๋ก ์์ธ๊ฐ ๋ฐ์ํ์ง ์์ผ๋ฉด ์ฑ๊ณต | |
| // ํฅํ Materialized View ์ ์ฅ ๋ก์ง ์ถ๊ฐ ์ ์ถ๊ฐ ๊ฒ์ฆ ํ์ | |
| assertThatCode(() -> writer.write(chunk)) | |
| .doesNotThrowAnyException(); | |
| } | |
| @DisplayName("Chunk์ ๋ชจ๋ ํญ๋ชฉ์ ์ฒ๋ฆฌํ๋ค") | |
| @Test | |
| void writesChunk_processesAllItems() throws Exception { | |
| // arrange | |
| int itemCount = 10; | |
| List<ProductMetrics> items = createProductMetricsList(itemCount); | |
| Chunk<ProductMetrics> chunk = new Chunk<>(items); | |
| // act & assert | |
| // ํ์ฌ๋ ๋ก๊น ๋ง ์ํํ๋ฏ๋ก ์์ธ๊ฐ ๋ฐ์ํ์ง ์์ผ๋ฉด ์ฑ๊ณต | |
| // ํฅํ Materialized View ์ ์ฅ ๋ก์ง ์ถ๊ฐ ์ ์ถ๊ฐ ๊ฒ์ฆ ํ์ | |
| assertThatCode(() -> writer.write(chunk)) | |
| .doesNotThrowAnyException(); | |
| } |
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriterTest.java
around lines 82 to 98, the test calls writer.write(chunk) twice (once before the
assertion and once inside assertThatCode), creating a redundant invocation;
remove the standalone writer.write(chunk) before the assertion so the test
invokes write only within the assertThatCode (or alternatively call write once
and assert around that single call) to keep the test intent clear and eliminate
the duplicate call.
| @DisplayName("์ฃผ๊ฐ Reader๋ ํด๋น ์ฃผ์ ์์์ผ๋ถํฐ ๋ค์ ์ฃผ ์์์ผ๊น์ง์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ค") | ||
| @Test | ||
| void weeklyReaderQueriesFromMondayToNextMonday() { | ||
| // arrange | ||
| LocalDate targetDate = LocalDate.of(2024, 12, 15); // ์ผ์์ผ | ||
| when(productMetricsRepository.getJpaRepository()).thenReturn(jpaRepository); | ||
|
|
||
| ProductRankAggregationReader reader = new ProductRankAggregationReader(productMetricsRepository); | ||
|
|
||
| // act | ||
| RepositoryItemReader<ProductMetrics> itemReader = reader.createWeeklyReader(targetDate); | ||
|
|
||
| // assert | ||
| assertThat(itemReader).isNotNull(); | ||
| // ์ฃผ๊ฐ ์์์ผ์ ํด๋น ์ฃผ์ ์์์ผ์ด์ด์ผ ํจ | ||
| // 2024-12-15(์ผ) -> 2024-12-09(์)์ด ์์์ผ | ||
| } |
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.
ํ ์คํธ assertion์ด ๋ถ์์ ํฉ๋๋ค.
ํ ์คํธ ์ด๋ฆ๊ณผ ์ฃผ์์์๋ "ํด๋น ์ฃผ์ ์์์ผ๋ถํฐ ๋ค์ ์ฃผ ์์์ผ๊น์ง์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ค"๊ณ ๋ช ์ํ์ง๋ง, ์ค์ ๋ก๋ reader๊ฐ null์ด ์๋์ง๋ง ๊ฒ์ฆํฉ๋๋ค. ๋ ์ง ๋ฒ์๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ๋์๋์ง ํ์ธํ๋ assertion์ด ์์ต๋๋ค.
RepositoryItemReader์ ๋ด๋ถ ์ํ๋ฅผ ๊ฒ์ฆํ๊ธฐ ์ด๋ ต๋ค๋ฉด, ์ค์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ ํตํฉ ํ
์คํธ๋ฅผ ์ถ๊ฐํ๊ฑฐ๋, Reader ์์ฑ ๋ก์ง์์ ๋ ์ง ๋ฒ์ ๊ณ์ฐ์ ๋ณ๋ ๋ฉ์๋๋ก ๋ถ๋ฆฌํ์ฌ ๋จ์ ํ
์คํธ๋ฅผ ์์ฑํ๋ ๊ฒ์ ๊ณ ๋ คํด์ฃผ์ธ์.
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReaderTest.java
around lines 47-63, the test only asserts the reader is non-null but does not
verify the computed date range; update the test to assert the reader queries
from the expected Monday to next Monday. Either: 1) assert readerโs internal
properties that hold start/end dates (or the query parameters) to equal
2024-12-09 (start) and 2024-12-16 (end), or 2) refactor
ProductRankAggregationReader to extract the weekly date-range calculation into a
package-visible method (e.g., calculateWeeklyRange(LocalDate)) and add a unit
test that calls that method with 2024-12-15 and asserts the returned start is
2024-12-09 and end is 2024-12-16. Ensure mocks/stubs expose whatever
fields/methods are needed so assertions can be made.
๐ Summary
Spring Batch๋ฅผ ํ์ฉํ์ฌ
product_metricsํ ์ด๋ธ ๊ธฐ๋ฐ์ผ๋ก ์ฃผ๊ฐ/์๊ฐ ๋ญํน ์์คํ ์ ๊ตฌํํ์ต๋๋ค. ๋๋ ๋ฐ์ดํฐ ์ง๊ณ์ ์ ํ์ฑ๊ณผ ์์ ์ฑ์ ์ํด 2-Step ๊ตฌ์กฐ๋ก ์ง๊ณ์ ๋ญํน์ ๋ถ๋ฆฌํ๊ณ , Materialized View์ TOP 100 ๋ญํน์ ์ ์ฅํ์ฌ ์กฐํ ์ฑ๋ฅ์ ์ต์ ํํ์ต๋๋ค.์ฃผ์ ๊ตฌํ ๋ด์ฉ:
product_metricsํ ์ด๋ธ์ ์ฝ์ด Chunk-Oriented Processing์ผ๋ก ๋๋ ๋ฐ์ดํฐ ์ง๊ณmv_product_rank)์period_type์ผ๋ก ์ฃผ๊ฐ/์๊ฐ ๊ตฌ๋ถํ์ฌ TOP 100 ์ ์ฅperiodํ๋ผ๋ฏธํฐ ์ถ๊ฐํ์ฌ ์ผ๊ฐ(Redis), ์ฃผ๊ฐ/์๊ฐ(Materialized View) ๋ญํน ์ ๊ณต๊ตฌํ๋ ๊ธฐ๋ฅ:
GET /api/v1/rankings?date=yyyyMMdd&period=WEEKLY&size=20&page=1: ์ฃผ๊ฐ/์๊ฐ ๋ญํน ์กฐํperiodType=WEEKLY targetDate=20241215๐ฌ Review Points
1. 2-Step ๊ตฌ์กฐ๋ก ์ง๊ณ์ ๋ญํน ๋ถ๋ฆฌ: ์ ์ฒด ๋ฐ์ดํฐ ๊ธฐ๋ฐ ์ ํํ TOP 100 ์ ์
๋ฐฐ๊ฒฝ ๋ฐ ์ค๊ณ ์๋:
๋๋ ๋ฐ์ดํฐ๋ฅผ Chunk ๋จ์๋ก ์ฒ๋ฆฌํ ๋, ๊ฐ Chunk๋ง๋ค TOP 100์ ๊ณ์ฐํ๋ฉด ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ ์ ํํ TOP 100์ ์ ์ ํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ์ฒซ ๋ฒ์งธ Chunk์์ ์ ์๊ฐ ๋์ ์ํ 100๊ฐ๋ฅผ ์ ์ ํ์ง๋ง, ์ดํ Chunk์์ ๋ ๋์ ์ ์๋ฅผ ๊ฐ์ง ์ํ์ด ๋ํ๋ ์ ์์ด ๊ฒฐ๊ณผ๊ฐ ๋ถ์ ํํด์ง๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด Step์ ์คํจ ๊ฒฉ๋ฆฌ์ ์ฌ์์ ๋จ์๋ก ์ฌ์ฉํ์ฌ ์ง๊ณ ๊ณ์ฐ๊ณผ ๋ญํน ์ ์ฌ๋ฅผ ๋ถ๋ฆฌํ์ต๋๋ค. ์ด๋ ๊ฒ ๋ถ๋ฆฌํ๋ฉด:
๊ตฌ์กฐ:
๊ด๋ จ ์ฝ๋:
๊ณ ๋ฏผํ ์ ๋ฐ ์์ฌ๊ฒฐ์ :
Step ๋ถ๋ฆฌ vs StepListener ์ฌ์ฉ
์์ ํ ์ด๋ธ ๋์
tmp_product_rank_score)์ ๋์ ํ์ต๋๋ค.์ฃผ๊ฐ/์๊ฐ ์ฒ๋ฆฌ ๋ฐฉ์
periodType)๋ก ๋ถ๊ธฐํ์ฌ ๋ณ๋ ์คํํ๋ ๋ฐฉ์์ ์ ํํ์ต๋๋ค.Chunk ๋จ์ ์ฒ๋ฆฌ์ ์ ์ฒด ๋ฐ์ดํฐ ์ง๊ณ
product_id๊ฐ ์ฌ๋ฌ Chunk์ ๊ฑธ์ณ ์์ ๊ฒฝ์ฐ ์์ ํ ์ด๋ธ(tmp_product_rank_score)์ UPSERT ๋ฐฉ์์ผ๋ก ๋์ ํ์ต๋๋ค.entityManager.merge()๋ก ์ ์ฅMaterialized View ์ ์ฅ ๋ฐฉ์: delete+insert
saveRanks()๋ฉ์๋์์deleteByPeriod()ํธ์ถ ํentityManager.persist()๋ก ์ ์ฅ2. Materialized View ์ค๊ณ: ํ๋์ ํ ์ด๋ธ์ period_type์ผ๋ก ๊ตฌ๋ถ
๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ ์ํฉ:
์๊ตฌ์ฌํญ์์๋
mv_product_rank_weekly์mv_product_rank_monthly๋ฅผ ๋ณ๋ ํ ์ด๋ธ๋ก ์ค๊ณํ๋ผ๊ณ ํ์ต๋๋ค. ํ์ง๋ง ์ค์ ๊ตฌํ์์๋ ํ๋์ ํ ์ด๋ธ(mv_product_rank)์period_type์ปฌ๋ผ์ผ๋ก ์ฃผ๊ฐ/์๊ฐ์ ๊ตฌ๋ถํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ์ต๋๋ค.ํด๊ฒฐ ๋ฐฉ์:
๋ ผ๋ฆฌ์ ์ผ๋ก๋ ๋ณ๋ ํ ์ด๋ธ์ฒ๋ผ ๋์ํ์ง๋ง, ๋ฌผ๋ฆฌ์ ์ผ๋ก๋ ํ๋์ ํ ์ด๋ธ์
period_type์ผ๋ก ๊ตฌ๋ถํ๋ ๋ฐฉ์์ ์ ํํ์ต๋๋ค:mv_product_rankํ ์ด๋ธ์period_type(WEEKLY/MONTHLY) ์ปฌ๋ผ์ผ๋ก ๊ตฌ๋ถ(period_type, period_start_date, rank)๋ณตํฉ ์ธ๋ฑ์ค๋ก ๊ธฐ๊ฐ๋ณ ๋ญํน ์กฐํ ์ต์ ํperiod_type๊ณผperiod_start_date๋ก ํํฐ๋งํ์ฌ ์กฐํ์ด ๋ฐฉ์์ ์ฅ์ :
๊ด๋ จ ์ฝ๋:
๊ณ ๋ฏผํ ์ :
period_type์ผ๋ก ๊ตฌ๋ถํ๋ ๋ฐฉ์์ด ๋ ์ ์ฐํ๊ณ ๊ด๋ฆฌํ๊ธฐ ์ฝ๋ค๊ณ ํ๋จํ์ต๋๋ค. ๋ ผ๋ฆฌ์ ์ผ๋ก๋ ๋ณ๋ ํ ์ด๋ธ์ฒ๋ผ ๋์ํ๋ฏ๋ก ์๊ตฌ์ฌํญ์ ์๋๋ ์ถฉ์กฑํ๋ค๊ณ ๋ด ๋๋ค.3. ๋ฐฐ์น ๋ชจ๋ ๋ถ๋ฆฌ: API์ ๋ฐฐ์น๋ฅผ ๋ ๋ฆฝ์ ์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก ๋ถ๋ฆฌ
๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ ์ํฉ:
API ์์ฒญ ์ฒ๋ฆฌ์ ๋ฐฐ์น ์ง๊ณ๋ ์คํ ์ฃผ๊ธฐ, ํธ๋์ญ์ ์ฑ๊ฒฉ, ์ฅ์ ๋์ ๋ฐฉ์์ด ๋ค๋ฆ ๋๋ค. API๋ ์ค์๊ฐ ์์ฒญ ์ฒ๋ฆฌ์ ์ต์ ํ๋์ด ์๊ณ , ๋ฐฐ์น๋ ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ์ ์ต์ ํ๋์ด ์์ต๋๋ค. ํ๋์ ๋ชจ๋์ ๋ ๊ฐ์ง๋ฅผ ๋ชจ๋ ํฌํจํ๋ฉด ์ค์ , Job/Step ๊ตฌ์ฑ, ํ ์คํธ ์ ๋ต์ด ์์ฌ ๊ด๋ฆฌ ๋ณต์ก๋๊ฐ ์ฆ๊ฐํฉ๋๋ค.
๋ถ๋ฆฌ์ ํต์ฌ ์ด์ :
์คํ ์ฃผ๊ธฐ์ ์ฐจ์ด
ํธ๋์ญ์ ์ฑ๊ฒฉ์ ์ฐจ์ด
์ฅ์ ๋์ ๋ฐฉ์์ ์ฐจ์ด
๋ ๋ฆฝ์ ์คํ, ์ฌ์คํ, ๊ด์ธก
ํด๊ฒฐ ๋ฐฉ์:
commerce-batch๋ชจ๋์ ๋ณ๋๋ก ๋ถ๋ฆฌํ์ฌ ๋ ๋ฆฝ์ ์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก ๊ตฌ์ฑํ์ต๋๋ค:BatchApplication์ ํตํด ๋ฐฐ์น๋ง ๋ ๋ฆฝ์ ์ผ๋ก ์คํ ๊ฐ๋ฅapplication.yml์์ ๋ฐฐ์น ์ ์ฉ ์ค์ ๊ด๋ฆฌ (์น ์๋ฒ ๋นํ์ฑํ, Job ์๋ ์คํ ๋นํ์ฑํ)com.loopers.domainํจํค์ง์ ๋๋ฉ์ธ์ ๊ณต์ ํ๋, Repository ๊ตฌํ์ ๋ชจ๋๋ณ๋ก ๋ถ๋ฆฌ๊ตฌ์กฐ:
๊ด๋ จ ์ฝ๋:
๋ถ๋ฆฌ์ ํจ๊ณผ:
๊ณ ๋ฏผํ ์ :
4. ๋ฐฐ์น ํ ์คํธ ์ ๋ต: ๋น์ฆ๋์ค ๋ก์ง ์ค์ฌ์ ๋จ์ ํ ์คํธ
๋ฐฐ๊ฒฝ ๋ฐ ์ค๊ณ ์๋:
๋ฉํ ๋ง ์ธ์ ์์ ๋ฐฐ์น ์ ์ฒด๋ฅผ execํด์ ์ ์คํ๋๋์ง๋ฅผ ํ์ธํ๋ ๊ฒ๋ณด๋ค ๊ทธ ์์ ์๋ processor๊ฐ์ ์๋ฏธ์๋ ๋น์ฆ๋์ค ๋ก์ง์ ๋ํ ํ ์คํธ๋ก ์ฒ๋ฆฌํ๋ ๊ฒ ๋ซ๋ค๋ ์กฐ์ธ์ ๋ฐ์์ต๋๋ค. ๋ฐ๋ผ์ ๋ฐฐ์น ์ ์ฒด ์คํ ํ ์คํธ ๋์ , ๋น์ฆ๋์ค ๋ก์ง์ด ์๋ ์ปดํฌ๋ํธ์ ๋ํ ๋จ์ ํ ์คํธ์ ์ด์ ์ ๋์์ต๋๋ค:
ํ ์คํธ ์์:
๊ณ ๋ฏผํ ์ :
โ Checklist
Spring Batch
Spring Batch Job์ ์์ฑํ๊ณ , ํ๋ผ๋ฏธํฐ ๊ธฐ๋ฐ์ผ๋ก ๋์์ํฌ ์ ์๋ค
periodType(WEEKLY/MONTHLY),targetDate(yyyyMMdd)apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.javaChunk Oriented Processing (Reader/Processor/Writer) ๊ธฐ๋ฐ์ ๋ฐฐ์น ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํ๋ค
apps/commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java์ง๊ณ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํ Materialized View์ ๊ตฌ์กฐ๋ฅผ ์ค๊ณํ๊ณ ์ฌ๋ฐ๋ฅด๊ฒ ์ ์ฌํ๋ค
mv_product_rank(period_type์ผ๋ก ์ฃผ๊ฐ/์๊ฐ ๊ตฌ๋ถ)delete + insert(TOP 100๋ง ์ ์ฅ)apps/commerce-batch/src/main/java/com/loopers/domain/rank/ProductRank.javaRanking API
GET /api/v1/rankings?date=yyyyMMdd&period=WEEKLY&size=20&page=1apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java๐ References
Summary by CodeRabbit
๋ฆด๋ฆฌ์ค ๋ ธํธ
์๋ก์ด ๊ธฐ๋ฅ
๊ฐ์
โ๏ธ Tip: You can customize this high-level summary in your review settings.