-
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.
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java
Outdated
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/domain/rank/ProductRank.java
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/ProductRankRepositoryImpl.java
Show resolved
Hide resolved
apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
Outdated
Show resolved
Hide resolved
...tch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankCalculationProcessor.java
Outdated
Show resolved
Hide resolved
...commerce-batch/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankJobConfig.java
Show resolved
Hide resolved
...h/src/main/java/com/loopers/infrastructure/batch/rank/ProductRankScoreAggregationWriter.java
Show resolved
Hide resolved
...erce-batch/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java
Outdated
Show resolved
Hide resolved
...tch/src/test/java/com/loopers/infrastructure/batch/metrics/ProductMetricsItemWriterTest.java
Show resolved
Hide resolved
...ch/src/test/java/com/loopers/infrastructure/batch/rank/ProductRankAggregationReaderTest.java
Show resolved
Hide resolved
* ํธ๋์ ์ ์ด๋ ธํ ์ด์ ์ถ๊ฐ * ๋ญํน ๋์ ํญ๋ชฉ์ด 100๊ฐ ๋ฏธ๋ง์ผ ๋์ ๋ฐฐ์น ์์ธ ์ฒ๋ฆฌ * @StepScope๋ฅผ ์ ์ฉํ์ฌ Step ์คํ๋ง๋ค ์ ์ธ์คํด์ค๋ฅผ ์์ฑ * ๋ญํฌ ๊ณ์ฐ ํ ์ฑ๊ธํค ์ธ์คํด์ค ๋ด์ ํ๋ ์ด๊ธฐํํ์ฌ ๋ฐ์ดํฐ ์ค์ผ ๋ฐ ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฌธ์ ๋ฐฉ์ง * ๋ฐฐ์น ์คํ ํ๋ผ๋ฏธํฐ์์ ๋ฐ์ํ ์ ์๋ null pointer exeception ์์ * n+1 ์ฟผ๋ฆฌ ๊ฐ์
eaff8f0 to
37a09a9
Compare
๐ 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 ๋ฐฉ์์ผ๋ก ๋์ ํ์ต๋๋ค.findAllByProductIdIn)ํ์ฌ ๋์ ํฉ๋๋ค.productRankScoreRepository.saveAll()๋ก ์ ์ฅํ๋ฉฐ, Repository ๊ตฌํ์ฒด์์entityManager.merge()๋ฅผ ์ฌ์ฉํ์ฌ UPSERT ๋ฐฉ์์ผ๋ก ์ ์ฅํฉ๋๋ค.Materialized View ์ ์ฅ ๋ฐฉ์: delete+insert
saveRanks()๋ฉ์๋์์deleteByPeriod()ํธ์ถ ํentityManager.persist()๋ก ์ ์ฅํฉ๋๋ค.saveRanks()๊ฐ delete+insert๋ฅผ ์ํํ๋ฏ๋ก ์ค๋ณต ์ ์ฅ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.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.