-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-10] Collect, Stack, Zip #235
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: junoade
Are you sure you want to change the base?
Conversation
์ฃผ๊ฐ/์๊ฐ MV ๋ญํน ๋ฉํธ๋ฆญ ์ ๋ณด ์ง๊ณ๋ฅผ ์ํด
์๋์ ํ
์ด๋ธ๊ตฌ์กฐ๋ฅผ ๋ฐ๋ฅด๋๋ก ๋ณ๊ฒฝํ์์ต๋๋ค.
```mermaid
erDiagram
PRODUCT_METRICS {
BIGINT product_id PK
VARCHAR2(8) metrics_date PK
VARCHAR2(50) METRICS_TYPE PK
BIGINT count
TIMESTAMP updated_at
TIMESTAMP created_at
}
```
```mermaid
erDiagram {
MV_PRODUCT_RANK_WEEKLY {
varchar year_week PK
bigint product_id PK
bigint view_count
bigint like_count
bigint order_count
datetime created_at
datetime updated_at
}
MV_PRODUCT_RANK_MONTHLY {
varchar year_month PK
bigint product_id PK
bigint view_count
bigint like_count
bigint order_count
datetime created_at
datetime updated_at
}
}
```
- Instant / ZonedDateTime / LocalDateTime ํ์ต
์ฒญํฌ ๊ธฐ๋ฐ / jdbc / ๊ฐ์ฅ ๊ฐ๋จํ๊ฒ ```sh java -jar apps/commerce-batch/build/libs/commerce-batch-8472ca24.jar \ --spring.profiles.active=local \ --spring.batch.job.enabled=true \ --spring.batch.job.name=rankWeeklyMvJob \ yearWeek=2026-W01 ```
๋ค์๊ณผ ๊ฐ์ ์์ผ๋ก score๋ฅผ ๊ณ์ฐํ๋ค ```md score = 0.1*view + 0.2*like + 0.7*order ```
๋ค์๊ณผ ๊ฐ์ ์์ผ๋ก score๋ฅผ ๊ณ์ฐํ๋ค ```md score = 0.1*view + 0.2*like + 0.7*order ```
๋๋ฒ๊น ์ฉ์์ StepMonitorListener๋ฅผ ํ์ฉํ๋๋ก ๋ณ๊ฒฝ
streamer ๋ชจ๋์์ api ๋ชจ๋ ์์น๋ก ๋ณ๊ฒฝ streamer ๋ชจ๋์์ ์ง์ ์ ์ผ๋ก ์ฌ์ฉํ์ง ์๊ณ , metric ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ๋ฐ์ดํฐ๊ฐ ์ง๊ณ๋๋ฉฐ, api ์์ ์ฌ์ฉ๋๊ธฐ ๋๋ฌธ
[volume-10] Collect, Stack, Zip
Walkthrough์์ ์กฐํ ๊ธฐ๋ฅ์ ์ ๋ต ํจํด์ผ๋ก ๋ฆฌํฉํ ๋งํ๊ณ , ์๊ฐ/์ฃผ๊ฐ ์์ ์ง๊ณ๋ฅผ ์ํ ์๋ก์ด ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ (commerce-batch)์ ๋์ ํ๋ฉฐ, ์ํ ๋ฉํธ๋ฆญ ๋ชจ๋ธ์ ProductLikeMetrics์์ ๋ณตํฉ ํค ๊ธฐ๋ฐ์ ProductMetrics๋ก ํตํฉํฉ๋๋ค. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant RankingV1Controller
participant RankingQueryService
participant RankingFetchStrategyResolver
participant RankingFetchStrategy
participant Repository
Client->>RankingV1Controller: GET /ranking?period=WEEKLY&date=...&size=20
RankingV1Controller->>RankingQueryService: getDailyPopularProducts(WEEKLY, date, 20)
RankingQueryService->>RankingFetchStrategyResolver: resolve(WEEKLY, date, 20)
rect rgb(200, 220, 240)
Note over RankingFetchStrategyResolver: ํด์(Parse) ๋ฐ ์ ๊ทํ
RankingFetchStrategyResolver->>RankingFetchStrategyResolver: initLocalDate(date)<br/>normalizeSize(20)<br/>yearWeekKey(LocalDate)<br/>build RankingQuery
end
RankingFetchStrategyResolver->>RankingFetchStrategyResolver: ์ ๋ต ์ ํ (WEEKLY โ WeeklyRankingFetcher)
RankingFetchStrategyResolver-->>RankingQueryService: Resolved(RankingQuery, WeeklyFetcher)
rect rgb(220, 240, 220)
Note over RankingQueryService: ์กฐํ ์คํ
RankingQueryService->>RankingFetchStrategy: fetchRankingEntries(key, limit)
RankingFetchStrategy->>Repository: findTopByYearWeek(key, PageRequest(0, 20))
Repository-->>RankingFetchStrategy: List<RankingEntry>
RankingFetchStrategy-->>RankingQueryService: List<RankingEntry>
end
RankingQueryService-->>RankingV1Controller: List<RankingEntry>
RankingV1Controller-->>Client: HTTP 200 + JSON
sequenceDiagram
participant Batch as Spring Batch Scheduler
participant JobConfig
participant Reader as JDBC Reader
participant Processor
participant Writer as JDBC Writer
participant DB as Database
Batch->>JobConfig: ์๊ฐ ์์ ๋ฐฐ์น ์์
์์ (rankMonthlyMvJob)
JobConfig->>Reader: ์ฒญํฌ ํฌ๊ธฐ 1000์ผ๋ก ์์
rect rgb(255, 240, 200)
Note over Reader,DB: ์ฝ๊ธฐ ๋จ๊ณ
Reader->>DB: product_metrics ์ฟผ๋ฆฌ<br/>(startDate, endDate)<br/>์๋ณ ์ง๊ณ
DB-->>Reader: MonthlyAggRow ๋ฆฌ์คํธ
Reader-->>Processor: MonthlyAggRow ๋ฐฐ์น (์ต๋ 1000๊ฐ)
end
rect rgb(240, 220, 240)
Note over Processor: ์ฒ๋ฆฌ ๋จ๊ณ
Processor->>Processor: ๊ฐ ํ๋ง๋ค<br/>score = 0.1*view<br/> + 0.2*like<br/> + 0.7*order
Processor-->>Writer: MonthlyRankRow ๋ฐฐ์น
end
rect rgb(220, 240, 240)
Note over Writer,DB: ์ฐ๊ธฐ ๋จ๊ณ
Writer->>DB: INSERT ... ON DUPLICATE<br/>KEY UPDATE<br/>mv_product_rank_monthly
DB-->>Writer: ์
๋ฐ์ดํธ ์๋ฃ
Writer-->>JobConfig: ์ฒญํฌ ์๋ฃ
end
Note over JobConfig: ๋ชจ๋ ์ฒญํฌ ์ฒ๋ฆฌ ์๋ฃ ํ ์์
์ข
๋ฃ
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 |
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: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and canโt be posted inline due to platform limitations.
โ ๏ธ Outside diff range comments (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.java (2)
31-45: ๋ฉ์๋๋ช ์ด ๊ธฐ๋ฅ๊ณผ ์ผ์นํ์ง ์์ต๋๋ค.๋ฉ์๋๋ช ์ด
getDailyPopularProducts์ด์ง๋ง ์ด์ RankingPeriodํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์ ์ผ๊ฐ/์ฃผ๊ฐ/์๊ฐ ๋ชจ๋ ๊ธฐ๊ฐ์ ๋ญํน์ ์ฒ๋ฆฌํฉ๋๋ค. ๋ฉ์๋๋ช ์ด ์ค์ ๊ธฐ๋ฅ์ ๋ฐ์ํ์ง ๋ชปํ๊ณ ์์ด ํผ๋์ ์ผ๊ธฐํ ์ ์์ต๋๋ค.๋ฉ์๋๋ช ์
getPopularProducts๋๋getPopularProductsByPeriod๋ก ๋ณ๊ฒฝํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ๋ฉ์๋๋ช ๋ณ๊ฒฝ ์ ์
@Transactional(readOnly = true) - public RankingQueryResponse getDailyPopularProducts(RankingPeriod period, String date, int size) { + public RankingQueryResponse getPopularProducts(RankingPeriod period, String date, int size) { log.debug("Get {} popular products for {}", period, date);์ปจํธ๋กค๋ฌ์์๋ ํธ์ถ ๋ถ๋ถ์ ํจ๊ป ๋ณ๊ฒฝํด์ผ ํฉ๋๋ค.
53-69: null ๊ฐ์ด API ์๋ต์ ํฌํจ๋๋ ๋ฌธ์ - ์์ ํ์
findProductSummaryFrom๋ฉ์๋์์ ์ ํ์ ์ฐพ์ง ๋ชปํ ๊ฒฝ์ฐ null์ ๋ฆฌ์คํธ์ ์ถ๊ฐํ๋๋ฐ, ์ด ๋ฆฌ์คํธ๊ฐ ํํฐ๋ง ์์ดRankingQueryResponse์ ์ ๋ฌ๋๊ณ JSON์ผ๋ก ์ง๋ ฌํ๋ฉ๋๋ค. ํ์ฌ Jackson ์ค์ ์์ null ๊ฐ์ ์ ์ธํ๋ ์ต์ ์ด ์์ผ๋ฏ๋ก, API ์๋ต์productLikeSummary๋ฐฐ์ด์ null ์์๊ฐ ํฌํจ๋ ๊ฒ์ ๋๋ค.๋ค์ ์ค ํ๋๋ฅผ ์ํํ์ธ์:
- null ๊ฐ์ ํํฐ๋งํ์ฌ ๋ฆฌ์คํธ์์ ์ ๊ฑฐ
- ๋๋ ๋น summary ๊ฐ์ฒด๋ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ๋์ฒด
- ๋๋
@JsonInclude(JsonInclude.Include.NON_NULL)์ด๋ ธํ ์ด์ ์ถ๊ฐ (๋น๊ถ์ฅ)
๐ก Minor comments (4)
apps/commerce-batch/src/main/resources/db/fixtures/large-product-metric-data.sql-1-3 (1)
1-3: ์ฃผ์์ ๋ ์ง ๋ฒ์๊ฐ ์ค์ ๋ฐ์ดํฐ์ ๋ถ์ผ์นํฉ๋๋ค.์ฃผ์์๋
[2025-01-01, 2026-01-31]๋ฒ์๋ผ๊ณ ๋์ด ์์ง๋ง,n.n BETWEEN 0 AND 395๋ 396์ผ์ ์์ฑํ์ฌ ์ค์ ๋ก๋2025-01-01 + 395์ผ = 2026-02-01๊น์ง์ ๋ฐ์ดํฐ๊ฐ ์์ฑ๋ฉ๋๋ค.๐ ์ฃผ์ ์์ ์ ์
-- PRODUCT_METRICS bulk dummy data --- ๋ฒ์: [2025-01-01, 2026-01-31] +-- ๋ฒ์: [2025-01-01, 2026-02-01] (396์ผ) DELETE FROM product_metrics;apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.java-16-17 (1)
16-17: yearWeek ์ปฌ๋ผ ๊ธธ์ด๋ฅผ 8๋ก ์์ ํ์ธ์yearWeek ํ๋๋ ISO 8601 ์ฃผ๊ฐ ํ์์ธ
"YYYY-Www"์ผ๋ก๋ง ์ฌ์ฉ๋๋ฉฐ, ์ด๋ ์ ํํ 8์์ ๋๋ค. ํ์ฌ ์ง์ ๋length = 10์ ์ค์ ๋ฐ์ดํฐ ๊ธธ์ด๋ณด๋ค 2์ ํฌ๋ฏ๋ก, ๋ค์๊ณผ ๊ฐ์ด ์์ ํด์ผ ํฉ๋๋ค:- @Column(name = "year_week_key", length = 10, nullable = false) + @Column(name = "year_week_key", length = 8, nullable = false)apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessor.java-5-5 (1)
5-5: ์ฌ์ฉํ์ง ์๋ import๋ฅผ ์ ๊ฑฐํ์ธ์.
WeeklyAggRow๊ฐ import๋์์ง๋ง ์ด ํ์ผ์์ ์ฌ์ฉ๋์ง ์์ต๋๋ค.๐ ์์ ์ ์
import com.loopers.batch.job.rankMonthly.dto.MonthlyAggRow; import com.loopers.batch.job.rankMonthly.dto.MonthlyRankRow; -import com.loopers.batch.job.rankWeekly.step.dto.WeeklyAggRow; import lombok.RequiredArgsConstructor;apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyJobConfig.java-5-5 (1)
5-5: ์ฌ์ฉํ์ง ์๋ import๊ฐ ์์ต๋๋ค.
WeeklyAggRow๋ ์ฃผ๊ฐ ์ง๊ณ์ฉ DTO๋ก, ์๊ฐ ์ง๊ณ Job์์๋ ์ฌ์ฉ๋์ง ์์ต๋๋ค.RankWeeklyJobConfig์์ ๋ณต์ฌํ ๋ ๋จ์ ๋ถํ์ํ import์ ๋๋ค.๐ ์ ๊ฑฐ ์ ์
package com.loopers.batch.job.rankMonthly; import com.loopers.batch.job.rankMonthly.dto.MonthlyAggRow; import com.loopers.batch.job.rankMonthly.dto.MonthlyRankRow; -import com.loopers.batch.job.rankWeekly.step.dto.WeeklyAggRow; import com.loopers.batch.listener.JobListener; import com.loopers.batch.listener.StepMonitorListener;
๐งน Nitpick comments (30)
apps/commerce-batch/src/main/resources/application.yml (2)
20-23: ๋ฐฐ์น ๋ชจ๋ํฐ๋ง์ ์ํ ํฌ์ค ์ฒดํฌ ๊ณ ๋ ค ์ฌํญ๊ธฐ๋ณธ ํฌ์ค ์ฒดํฌ๊ฐ ๋นํ์ฑํ๋์ด ์์ต๋๋ค. ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ์ผ๋ฐ์ ์ด์ง๋ง,
monitoring.yml์์ ๋ฐฐ์น ์์ ์คํ ์ํ, ์คํจ์จ ๋ฑ์ ์ถ์ ํ ์ ์๋ ์ปค์คํ ํฌ์ค ์ธ๋์ผ์ดํฐ๋ ๋ฉํธ๋ฆญ์ด ๊ตฌ์ฑ๋์ด ์๋์ง ํ์ธํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
52-54: ์ค๋ณต๋ ์ค์ (์ ํ ์ฌํญ)
web-application-type: none์ผ๋ก ์ค์ ๋์ด ์์ด ์น ๋ ์ด์ด๊ฐ ์์ผ๋ฏ๋ก Springdoc ๋นํ์ฑํ ์ค์ ์ ๋ถํ์ํฉ๋๋ค. ํ์ง๋ง ์ด ์ค์ ์ ๋ฌดํดํ๋ฉฐ ๋ช ์์ ์๋๋ฅผ ํํํ๋ค๋ฉด ์ ์งํด๋ ๋ฉ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsModel.java (2)
31-41: ๋ถ์ฐ ํ๊ฒฝ์์ZoneId.systemDefault()์ฌ์ฉ ์ ์๊ฐ๋ ๋ถ์ผ์น ๊ฐ๋ฅ์ฑ์ด ์์ต๋๋ค.์ฌ๋ฌ ์๋ฒ์์ ๋ค๋ฅธ ์์คํ ์๊ฐ๋๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ
createdAt/updatedAt๊ฐ์ด ์ผ๊ด๋์ง ์์ ์ ์์ต๋๋ค.ZoneOffset.UTC์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.๐ UTC ์ฌ์ฉ ์ ์
+import java.time.ZoneOffset; + @PrePersist protected void onCreate() { - LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault()); + LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); this.createdAt = now; this.updatedAt = now; } @PreUpdate protected void onUpdate() { - this.updatedAt = LocalDateTime.now(ZoneId.systemDefault()); + this.updatedAt = LocalDateTime.now(ZoneOffset.UTC); }
50-52: ๋์์ฑ ์ ์ด ์์ดcount์ฆ๊ฐ ์ lost update ๊ฐ๋ฅ์ฑ์ด ์์ต๋๋ค.๋์ผ ์ํ์ ๋ํด ๋์์ ์ฌ๋ฌ ์ด๋ฒคํธ๊ฐ ์ฒ๋ฆฌ๋๋ฉด ์ผ๋ถ ์ฆ๊ฐ๊ฐ ์ ์ค๋ ์ ์์ต๋๋ค. ๋๊ด์ ๋ฝ(
@Version) ๋๋ ๋น๊ด์ ๋ฝ ์ ์ฉ์ ๊ณ ๋ คํด ์ฃผ์ธ์.๐ ๋๊ด์ ๋ฝ ์ถ๊ฐ ์์
+ @Version + private Long version; + public void increase() { this.count += 1; }apps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsId.java (2)
38-49: ํฉํ ๋ฆฌ ๋ฉ์๋๊ฐ ํ์ฌ ๋ ์ง๋ง ์ง์ํฉ๋๋ค.
of(Long, MetricsType)๋ฉ์๋๋ ํญ์ ์ค๋ ๋ ์ง๋ฅผ ์ฌ์ฉํฉ๋๋ค. ๋ฐฐ์น ์ฒ๋ฆฌ๋ ๊ณผ๊ฑฐ ๋ฐ์ดํฐ ์ฌ์ฒ๋ฆฌ๋ฅผ ์ํด ๋ ์ง๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋ ์ค๋ฒ๋ก๋ ๋ฉ์๋๊ฐ ํ์ํ ์ ์์ต๋๋ค.๐ ๋ ์ง ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ๋ ํฉํ ๋ฆฌ ๋ฉ์๋ ์ถ๊ฐ
public static ProductMetricsId of(Long productId, MetricsType metricsType) { String nowDate = convertDate(Instant.now()); return new ProductMetricsId(productId, nowDate, metricsType); } +public static ProductMetricsId of(Long productId, String metricsDate, MetricsType metricsType) { + return new ProductMetricsId(productId, metricsDate, metricsType); +}
44-49:ProductMetricsModel๊ณผ ๋์ผํ๊ฒZoneId.systemDefault()๋์ UTC ์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.๋ถ์ฐ ํ๊ฒฝ์์์ ์ผ๊ด์ฑ์ ์ํด ๋์ผํ ์๊ฐ๋ ์ ์ฑ ์ ์ ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.java (1)
16-16: ๋ ํฌ์งํ ๋ฆฌ ๋ณ์๋ช ์ด ์ค์ ์ญํ ๊ณผ ๋ง์ง ์์ต๋๋ค.
ProductMetricsRepository๋ ์ด์ ๋ชจ๋ ๋ฉํธ๋ฆญ ํ์ (VIEW, LIKE, ORDER_SUCCESS ๋ฑ)์ ์ฒ๋ฆฌํ์ง๋ง, ๋ณ์๋ช ์ดlikeMetricsRepository๋ก ๋์ด ์์ด ํผ๋์ ์ค ์ ์์ต๋๋ค.๐ ๋ณ์๋ช ์์ ์ ์
- private final ProductMetricsRepository likeMetricsRepository; + private final ProductMetricsRepository metricsRepository;๊ทธ๋ฆฌ๊ณ ๋ฉ์๋ ๋ด์์๋ ํด๋น ๋ณ์ ์ฌ์ฉ์ ์ ๋ฐ์ดํธํด ์ฃผ์ธ์.
apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java (1)
81-87: ์นด์ดํธ ์กฐํ ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๋ฉฐ, ๋ฉฑ๋ฑ์ฑ ๊ฒ์ฆ์ด ์ ์ ํฉ๋๋ค.์๋ก์ด ๋ชจ๋ธ์ getCount() ์ฌ์ฉ์ด ์ฌ๋ฐ๋ฅด๊ณ , ๋ก๊น ์ถ๊ฐ๊ฐ ๋๋ฒ๊น ์ ์ ์ฉํฉ๋๋ค. ๋ฉฑ๋ฑ์ฑ ๊ฒ์ฆ ๋ก์ง(์ค๋ณต ๋ฉ์์ง ์ ์ก ํ ์นด์ดํธ +1, ์ฒ๋ฆฌ ๊ธฐ๋ก 1๊ฑด)์ด ์ ํํฉ๋๋ค.
์ ํ์ ๊ฐ์ : Line 86์ "product-like-metrics" ๋ฌธ์์ด์ ์์๋ก ์ถ์ถํ๋ฉด ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ฉ๋๋ค.
๐ ์ ํ์ ๋ฆฌํฉํ ๋ง: ์ปจ์๋จธ ์ด๋ฆ ์์ํ
+ private static final String CONSUMER_NAME = "product-like-metrics"; + @Test @DisplayName("๋ฉฑ๋ฑ์ฑ ํ ์คํธ") void duplicate_message_should_be_applied_once() throws Exception { // ... existing code ... - long handledCount = handledRepo.countByConsumerNameAndEventId("product-like-metrics", "06b9f00c-04bb-40fb-a96c-a9da5d0ede53"); + long handledCount = handledRepo.countByConsumerNameAndEventId(CONSUMER_NAME, "06b9f00c-04bb-40fb-a96c-a9da5d0ede53"); assertThat(handledCount).isEqualTo(1); }apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (2)
19-23: ํ์์กด ์ค์ ์์ ์ main ๋ฉ์๋๋ก ์ด๋ ๊ถ์ฅ
TimeZone.setDefault()๋ JVM ์ ์ญ ์ค์ ์ด๋ฏ๋ก ๊ฐ๋ฅํ ํ ๋นจ๋ฆฌ ์ค์ ๋์ด์ผ ํฉ๋๋ค.@PostConstruct์์ ์ค์ ํ๋ฉด ๋ค๋ฅธ ๋น๋ค์ด ์ด๊ธฐํ๋ ๋ ์ด๋ฏธ ๋ ์ง/์๊ฐ ๊ด๋ จ ๊ฐ๋ค์ด ์์ฑ๋ ์ ์์ด ์ผ๊ด์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.๐ ๊ถ์ฅ ์์ ์ฌํญ
- @PostConstruct - public void started() { - // set timezone - TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); - } - public static void main(String[] args) { + // set timezone before Spring context initialization + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); int exitCode = SpringApplication.exit(SpringApplication.run(CommerceBatchApplication.class, args)); System.exit(exitCode); }๋๋ JVM ์ธ์๋ก ์ค์ ํ๋ ๋ฐฉ๋ฒ๋ ๊ณ ๋ คํ ์ ์์ต๋๋ค:
-Duser.timezone=Asia/Seoul
30-41: DataSource ๋ช ์์ ์ค์ ๋ฐฉ์ ๊ฒํ ๊ถ์ฅํ์ฌ
BeanFactoryPostProcessor๋ฅผ ํตํด DataSource ๋ณ์นญ์ ๋ฑ๋กํ๋ ๊ฒ์ Spring Batch๊ฐ "dataSource"๋ผ๋ ์ด๋ฆ์ ๋น์ ๊ธฐ๋ํ๊ธฐ ๋๋ฌธ์ธ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค. ์ด ๋ฐฉ์์ ๋์ํ์ง๋ง ์ฐํ์ ์ธ ์ ๊ทผ์ ๋๋ค.๋ค์๊ณผ ๊ฐ์ ๋์์ ๊ฒํ ํด๋ณด์ธ์:
application.yml์์spring.batch.jdbc.data-source๋ก ๋ช ์์ ์ผ๋ก DataSource ๋น ์ด๋ฆ ์ง์ BatchConfigurer๋ฅผ ์ปค์คํฐ๋ง์ด์งํ์ฌ DataSource๋ฅผ ๋ช ์์ ์ผ๋ก ์ค์ ํ์ฌ ๊ตฌํ์ ๋ฐฉ์ด์ ์ด๊ณ ์์ ํ์ง๋ง, ์ค์ ๊ธฐ๋ฐ ์ ๊ทผ์ด ๋ ๋ช ํํ ์ ์์ต๋๋ค.
apps/commerce-batch/build.gradle.kts (1)
14-14: QueryDSL ์์กด์ฑ์ ๋ฒ์ ๊ด๋ฆฌ๋ ์ ์์ด๋, ๋ช ์์ ์ ์ธ ์ถ์ฒ
querydsl-apt::jakarta๊ตฌ๋ฌธ์ Spring Boot์ ์์์ ์์กด์ฑ ๊ด๋ฆฌ๋ฅผ ํตํด ๋ฒ์ ์ด ๊ด๋ฆฌ๋๊ณ ์์ต๋๋ค. ํ๋ก์ ํธ์์ Spring Boot 3.4.4๋ฅผ ์ฌ์ฉ ์ค์ด๋ฉฐ,io.spring.dependency-managementํ๋ฌ๊ทธ์ธ์ด QueryDSL ๋ฒ์ ์ ์๋์ผ๋ก ๊ด๋ฆฌํ๊ณ ์์ผ๋ฏ๋ก ํ์ฌ ๊ตฌ์ฑ์ ๋์ํฉ๋๋ค.๋ค๋ง ๋ค์ ๊ฐ์ ์ ๊ถ์ฅํฉ๋๋ค:
jakartaclassifier๋ ์ฌ๋ฐ๋ฆ ๋๋ค - Spring Boot 3.x๋ Jakarta EE๋ฅผ ํ์ค์ผ๋ก ์ฌ์ฉํ๋ฏ๋ก ํ์ฌ ์ค์ ์ด ๋ง์ต๋๋ค.- ๋ฒ์ ๊ด๋ฆฌ๋ฅผ ๋ช ์์ ์ผ๋ก ์ ์ธํ๋ ๊ฒ์ด ๊ถ์ฅ๋ฉ๋๋ค - ๋ฃจํธ
build.gradle.kts์์ Spring Cloud BOM๋ง importํ๊ณ ์์ผ๋ฏ๋ก, ๋ ๋ช ํํ๊ฒ ํ๋ ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด Spring Boot BOM์ ๋ช ์์ ์ผ๋ก importํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์:dependencyManagement { imports { mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") } }ํ์ฌ ๊ตฌ์ฑ์ด ์ ์ ์๋ํ์ง๋ง, ์์กด์ฑ ๊ด๋ฆฌ์ ์ถ์ฒ๋ฅผ ๋ช ์์ ์ผ๋ก ์ ์ธํ๋ฉด ์ฝ๋ ๊ฐ๋ ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ฉ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java (1)
40-47: Step ๋ฉ์๋๋ช ๊ณผ ์ค์ ์ฉ๋๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.
categorySyncStep()๋ฉ์๋๋ช ์ ์นดํ ๊ณ ๋ฆฌ ๋๊ธฐํ๋ฅผ ์์ํ์ง๋ง, ์ค์ ๋ก๋demoSimpleTask๋ผ๋ ์ด๋ฆ์ ๋ฐ๋ชจ์ฉ Step์ ๋๋ค. ํผ๋์ ๋ฐฉ์งํ๊ธฐ ์ํด ๋ฉ์๋๋ช ์ Step์ ์ค์ ์ฉ๋์ ๋ง๊ฒ ๋ณ๊ฒฝํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์๋ ์์
@JobScope @Bean(STEP_DEMO_SIMPLE_TASK_NAME) - public Step categorySyncStep() { + public Step demoSimpleTaskStep() { return new StepBuilder(STEP_DEMO_SIMPLE_TASK_NAME, jobRepository) .tasklet(demoTasklet, new ResourcelessTransactionManager()) .listener(stepMonitorListener) .build(); }๊ทธ๋ฆฌ๊ณ
demoJob()๋ฉ์๋์์๋ ์ฐธ์กฐ๋ฅผ ์ ๋ฐ์ดํธ:@Bean(JOB_NAME) public Job demoJob() { return new JobBuilder(JOB_NAME, jobRepository) .incrementer(new RunIdIncrementer()) - .start(categorySyncStep()) + .start(demoSimpleTaskStep()) .listener(jobListener) .build(); }apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (2)
24-26: ๋ ๊ตฌ์ฒด์ ์ธ ์์ธ ํ์ ์ฌ์ฉ์ ๊ณ ๋ คํ์ธ์.
RuntimeException๋์IllegalArgumentException์ด๋ ์ปค์คํ ๋ฐฐ์น ์์ธ๋ฅผ ์ฌ์ฉํ๋ฉด ์์ธ์ ์์ธ์ ๋ ๋ช ํํ๊ฒ ํํํ ์ ์์ต๋๋ค.๐ ์ ์๋ ์์
if (requestDate == null) { - throw new RuntimeException("requestDate is null"); + throw new IllegalArgumentException("Required job parameter 'requestDate' is null"); }
27-29:System.out.println๋์ SLF4J ๋ก๊ฑฐ ์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.ํ๋ก๋์ ๋ฐฐ์น ํ๊ฒฝ์์๋ ๋ก๊ทธ ๋ ๋ฒจ ์ ์ด, ๋ก๊ทธ ์ง๊ณ, ๊ตฌ์กฐํ๋ ๋ก๊น ๋ฑ์ ์ํด ์ ์ ํ ๋ก๊น ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ๋ฐ๋ชจ ๋ชฉ์ ์ด๋ผ๋ ์ผ๊ด๋ ๋ก๊น ํจํด์ ์ ์งํ๋ฉด ์ค์ Job ๊ตฌํ ์ ์ฐธ๊ณ ํ๊ธฐ ์ข์ต๋๋ค.
๐ ์ ์๋ ์์
+import lombok.extern.slf4j.Slf4j; + +@Slf4j @StepScope @ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME) @RequiredArgsConstructor @Component public class DemoTasklet implements Tasklet { @Value("#{jobParameters['requestDate']}") private String requestDate; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { if (requestDate == null) { throw new RuntimeException("requestDate is null"); } - System.out.println("Demo Tasklet ์คํ (์คํ ์ผ์ : " + requestDate + ")"); + log.info("Demo Tasklet ์คํ (์คํ ์ผ์ : {})", requestDate); Thread.sleep(1000); - System.out.println("Demo Tasklet ์์ ์๋ฃ"); + log.info("Demo Tasklet ์์ ์๋ฃ"); return RepeatStatus.FINISHED; } }apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (3)
4-4: ์ฌ์ฉ๋์ง ์๋@RequiredArgsConstructor์ด๋ ธํ ์ด์ ์ ์ ๊ฑฐํ์ธ์.์ด ํด๋์ค์๋
finalํ๋๊ฐ ์๊ณ ๋ชจ๋ ์์กด์ฑ์ด@Autowired๋ก ์ฃผ์ ๋๋ฏ๋ก@RequiredArgsConstructor๊ฐ ๋ถํ์ํฉ๋๋ค.๐ ์ ์๋ ์์
import com.loopers.batch.job.demo.DemoJobConfig; -import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.BeforeEach;๊ทธ๋ฆฌ๊ณ ํด๋์ค ์ ์ธ์์:
@SpringBootTest @SpringBatchTest @TestPropertySource(properties = "spring.batch.job.name=" + DemoJobConfig.JOB_NAME) -@RequiredArgsConstructor class DemoJobE2ETest {
37-40: ๋น์ด ์๋@BeforeEach๋ฉ์๋๋ฅผ ์ ๊ฑฐํ์ธ์.์๋ฌด๋ฐ ์ค์ ์ ์ํํ์ง ์๋ ๋น ๋ฉ์๋๋ ์ฝ๋ ๊ฐ๋ ์ฑ์ ์ ํดํฉ๋๋ค.
๐ ์ ์๋ ์์
- @BeforeEach - void beforeEach() { - - } - @DisplayName("jobParameter ์ค requestDate ์ธ์๊ฐ ์ฃผ์ด์ง์ง ์์์ ๋, demoJob ๋ฐฐ์น๋ ์คํจํ๋ค.")
42-56: ํ ์คํธ ๋ฉ์๋๋ช ์ด ์ค์ ํ ์คํธ ๋์๊ณผ ์ผ์นํ์ง ์์ต๋๋ค.
shouldNotSaveCategories_whenApiError๋ ์นดํ ๊ณ ๋ฆฌ ์ ์ฅ๊ณผ API ์๋ฌ๋ฅผ ์์ํ์ง๋ง, ์ค์ ๋ก๋requestDateํ๋ผ๋ฏธํฐ๊ฐ ์์ ๋ Job์ด ์คํจํ๋์ง ๊ฒ์ฆํฉ๋๋ค.@DisplayName์ ๋ด์ฉ๊ณผ ์ผ์นํ๋๋ก ๋ฉ์๋๋ช ์ ์์ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์๋ ์์
@DisplayName("jobParameter ์ค requestDate ์ธ์๊ฐ ์ฃผ์ด์ง์ง ์์์ ๋, demoJob ๋ฐฐ์น๋ ์คํจํ๋ค.") @Test - void shouldNotSaveCategories_whenApiError() throws Exception { + void shouldFail_whenRequestDateParameterIsMissing() throws Exception { // arrange jobLauncherTestUtils.setJob(job);apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/dto/WeeklyAggRow.java (1)
3-9: LGTM - productId ํ์ ์ผ๊ด์ฑ ๊ณ ๋ ค ์ ์๋ ์ฝ๋ ๊ตฌ์กฐ๊ฐ ๋ช ํํ๊ณ ์ฃผ์๋ ์ ์ ํฉ๋๋ค.
๋ค๋ง ์ ํ์ ์ผ๋ก
productId๋ฅผLong๋์long์ผ๋ก ๋ณ๊ฒฝํ์ฌ count ํ๋๋ค๊ณผ ํ์ ์ผ๊ด์ฑ์ ๋ง์ถ๊ณ null ์์ ์ฑ์ ๋์ด๋ ๊ฒ์ ๊ณ ๋ คํด๋ณผ ์ ์์ต๋๋ค. ๋ฐฐ์น ์ฒ๋ฆฌ ์ปจํ ์คํธ์์ productId๊ฐ null์ผ ๊ฐ๋ฅ์ฑ์ ์์ผ๋ฏ๋ก, primitive ํ์ ์ฌ์ฉ์ด ๋ ์ ํฉํ ์ ์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessorConfig.java (1)
10-16: LGTM - job parameter ์ผ๊ด์ฑ ํ์ธ ์ ์Spring Batch์ step-scoped bean ์ค์ ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.
yearMonthํ๋ผ๋ฏธํฐ๋ฅผ ํตํด processor์ job ์ปจํ ์คํธ๋ฅผ ์ฃผ์ ํ๋ ํจํด์ด ์ ์ ํฉ๋๋ค.์ ํ์ ์ผ๋ก
RankMonthlyJobConfig์์ ์ด job parameter๋ค(yearMonth,startDate,endDate)์ด ๋ชจ๋ ์ผ๊ด๋๊ฒ ์ ๋ฌ๋๋์ง ํ์ธํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.#!/bin/bash # RankMonthlyJobConfig์์ job parameter ์ค์ ํ์ธ ast-grep --pattern 'class RankMonthlyJobConfig { $$$ }'apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQuery.java (1)
5-10: ์ ๋ ฅ ๊ฒ์ฆ ๋ก์ง ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํ์ธ์.
RankingQuery๋ ๋ฐ์ดํฐ ์ ์ก ๊ฐ์ฒด๋ก ์ ์ ํ์ง๋ง, ํ๋์ ๋ํ ๊ฒ์ฆ์ด ์์ต๋๋ค:
period,key,date๋ null์ด ๋ ์ ์์ต๋๋คlimit์ ์์๋ 0์ด ๋ ์ ์์ต๋๋ค
RankingFetchStrategyResolver.resolve()์์ ์ผ๋ถ ์ ๊ทํ๊ฐ ์ด๋ฃจ์ด์ง๋ ๊ฒ์ผ๋ก ๋ณด์ด์ง๋ง, ์ด record๊ฐ ๋ค๋ฅธ ๊ณณ์์ ์ง์ ์์ฑ๋ ๊ฒฝ์ฐ ์ ํจํ์ง ์์ ์ฟผ๋ฆฌ๊ฐ ์์ฑ๋ ์ ์์ต๋๋ค. Java Bean Validation (@NotNull,@Positive๋ฑ) ๋๋ ์ ์ ํฉํ ๋ฆฌ ๋ฉ์๋๋ฅผ ํตํ ๊ฒ์ฆ์ ๊ณ ๋ คํ์ธ์.๐ ๊ฒ์ฆ ๋ก์ง ์ถ๊ฐ ์์
์ ์ ํฉํ ๋ฆฌ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ๊ฒ์ฆ:
+import java.util.Objects; + public record RankingQuery( RankingPeriod period, String key, LocalDate date, int limit -) { } +) { + public RankingQuery { + Objects.requireNonNull(period, "period must not be null"); + Objects.requireNonNull(key, "key must not be null"); + Objects.requireNonNull(date, "date must not be null"); + if (limit <= 0) { + throw new IllegalArgumentException("limit must be positive"); + } + } +}apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessor.java (1)
30-35: ์ ์ ๊ณ์ฐ ๋ก์ง์ด PR ๋ช ์ธ์ ์ผ์นํฉ๋๋ค.๊ณ์ฐ ๊ณต์ (0.1view + 0.2like + 0.7*order)์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค. TODO ์ฝ๋ฉํธ์ ์ธ๊ธ๋ ์ ์ฑ ๋ถ๋ฆฌ๋ ํฅํ ๊ฐ์ ์ฌํญ์ผ๋ก ์ ์ ํฉ๋๋ค.
TODO ํญ๋ชฉ์ธ ์ ์ ๊ณ์ฐ ์ ์ฑ ๋ถ๋ฆฌ๋ฅผ ์ํ ์ ๋ต ํจํด ๊ตฌํ ์ฝ๋๋ฅผ ์์ฑํ๊ฑฐ๋, ๋ณ๋ ์ด์๋ก ์์ฑํ์๊ฒ ์ต๋๊น?
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (2)
31-38: ๋ก๊ทธ ๋ ๋ฒจ์error๋ก ๋ณ๊ฒฝ ๊ถ์ฅ์คํจ ์์ธ ์ ๋ณด๋ฅผ ๋ก๊น ํ ๋
log.info๋์log.error๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ ์ ํฉ๋๋ค. ๋ชจ๋ํฐ๋ง ์์คํ ์์ ์๋ฌ ๋ก๊ทธ๋ฅผ ํํฐ๋งํ๊ณ ์๋ฆผ์ ์ค์ ํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.๐ ๋ก๊ทธ ๋ ๋ฒจ ๋ณ๊ฒฝ ์ ์
- log.info( + log.error( """ [์๋ฌ ๋ฐ์] jobName: {} exceptions: {} """.trim(), jobName, exceptions );
13-16: ๋ถํ์ํ@RequiredArgsConstructor์ด๋ ธํ ์ด์ ํ์ฌ ํด๋์ค์ final ํ๋๊ฐ ์์ผ๋ฏ๋ก
@RequiredArgsConstructor๋ ๋ถํ์ํฉ๋๋ค. ์ ๊ฑฐํด๋ ๋์์ ์ํฅ์ด ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.java (1)
14-21:year_month_key์ปฌ๋ผ ๊ธธ์ด ์กฐ์ ๊ฒํ
yearMonthํ๋๋ ๋ฐฐ์น Writer์ Repository์์ ์ผ๊ด๋๊ฒYYYYMMํ์(6์๋ฆฌ, ์: "202601")์ผ๋ก ์ฌ์ฉ๋๊ณ ์์ต๋๋ค. ํ์ฌ ์ ์ธ๋ ์ปฌ๋ผ ๊ธธ์ดlength = 8์ ์ค์ ํ์๋ณด๋ค ์ฝ๊ฐ ํฌ๋ฏ๋ก, ์ ํ์ฑ์ ์ํดlength = 6์ผ๋ก ์กฐ์ ํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyReaderConfig.java (1)
45-74: ์๋ธ์ฟผ๋ฆฌ ์ง๊ณ ์ฟผ๋ฆฌ ๊ตฌ์กฐ ํ์ธ ์๋ฃ - ์ธ๋ฑ์ค ์ต์ ํ ๊ถ์ฅ์๋ธ์ฟผ๋ฆฌ ๊ธฐ๋ฐ ์ง๊ณ ์ฟผ๋ฆฌ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์์ต๋๋ค. ๋ค๋ง,
product_metricsํ ์ด๋ธ์ ๋ฐ์ดํฐ๊ฐ ์ฆ๊ฐํ ๊ฒฝ์ฐ ์ฟผ๋ฆฌ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํด(metrics_date, product_id, metrics_type)๋ณตํฉ ์ธ๋ฑ์ค ์์ฑ์ ๊ถ์ฅํฉ๋๋ค. ํ์ฌ ์ฟผ๋ฆฌ๋WHERE pm.metrics_date between :startDate and :endDate๋ก metrics_date๋ฅผ ํํฐ๋งํ๊ณGROUP BY pm.product_id๋ก ์ง๊ณํ๋ฏ๋ก, ์ด ์ธ๋ฑ์ค๋ ์ฟผ๋ฆฌ ์คํ ๊ณํ์ ๊ฐ์ ํ ์ ์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyProcessor.java (1)
23-28: ์ ์ ๊ณ์ฐ ๋ก์ง์ ์ ๋ต ํจํด ๋๋ ์ค์ ๊ธฐ๋ฐ์ผ๋ก ๋ถ๋ฆฌํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.TODO ์ฃผ์์์ ์ธ๊ธํ ๋๋ก, ๊ฐ์ค์น๊ฐ ํ๋์ฝ๋ฉ๋์ด ์์ด ์ ์ฑ ๋ณ๊ฒฝ ์ ์ฝ๋ ์์ ์ด ํ์ํฉ๋๋ค. ๋ค์ ์ ๊ทผ ๋ฐฉ์์ ๊ณ ๋ คํด๋ณด์ธ์:
- Option 1: ScoreCalculationStrategy ์ธํฐํ์ด์ค๋ฅผ ๋์ ํ๊ณ ๊ฐ์ค์น๋ฅผ ์ธ๋ถํ
- Option 2: application.yml์ ๊ฐ์ค์น๋ฅผ ์ค์ ํ๊ณ @ConfigurationProperties๋ก ์ฃผ์
๐ ์ ๋ต ํจํด ๊ธฐ๋ฐ ๋ฆฌํฉํ ๋ง ์์
// ScoreCalculationStrategy ์ธํฐํ์ด์ค ์์ฑ public interface ScoreCalculationStrategy { double calculate(long viewCount, long likeCount, long orderCount); } // ์ค์ ๊ธฐ๋ฐ ๊ตฌํ @Component @ConfigurationProperties(prefix = "ranking.score.weights") public class WeightedScoreCalculator implements ScoreCalculationStrategy { private double view = 0.1; private double like = 0.2; private double order = 0.7; @Override public double calculate(long viewCount, long likeCount, long orderCount) { return view * viewCount + like * likeCount + order * orderCount; } // getters and setters } // Processor์์ ์ฌ์ฉ @Component @RequiredArgsConstructor public class RankWeeklyProcessor implements ItemProcessor<WeeklyAggRow, WeeklyRankRow> { private final ScoreCalculationStrategy scoreCalculator; private double calculateScore(WeeklyAggRow item) { return scoreCalculator.calculate(item.viewCount(), item.likeCount(), item.orderCount()); } }apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java (1)
46-47: ํ์์กด ์ฒ๋ฆฌ์ ์ผ๊ด์ฑ์ ๊ณ ๋ คํ์ธ์.
ProductRankWeekly.java์์๋LocalDateTime.now(ZoneId.systemDefault())์ ์ฌ์ฉํ์ง๋ง, ์ฌ๊ธฐ์๋LocalDateTime.now()๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ๋์ผํ ํจํด์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ผ๊ด์ฑ ์ธก๋ฉด์์ ์ข์ต๋๋ค.๐ ์ผ๊ด์ฑ์ ์ํ ์์
- e.createdAt = LocalDateTime.now(); - e.updatedAt = LocalDateTime.now(); + e.createdAt = LocalDateTime.now(ZoneId.systemDefault()); + e.updatedAt = LocalDateTime.now(ZoneId.systemDefault());
overwrite()๋ฉ์๋์ 55๋ผ์ธ๋ ๋์ผํ๊ฒ ์์ ํ๋ฉด ์ข์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyWriterConfig.java (1)
36-36: assertUpdates(false) ์ค์ ์ด ์ ๋ฐ์ดํธ ์คํจ๋ฅผ ๊ฐ์งํ์ง ๋ชปํ๊ฒ ํฉ๋๋ค.
assertUpdates(false)๋ก ์ค์ ํ๋ฉด ์ค์ ๋ก ํ์ด ์ ๋ฐ์ดํธ๋์ง ์์๋ ๋ฐฐ์น๊ฐ ์คํจํ์ง ์์ต๋๋ค. Upsert ์์ ์ ํน์ฑ์ ์๋์ ์ผ ์ ์์ง๋ง, ์ค์ ์ค๋ฅ ์ํฉ๋ ๋ฌด์๋ ์ ์์ต๋๋ค.์ ๋ฐ์ดํธ ์คํจ๋ฅผ ๋ชจ๋ํฐ๋งํ ์ ์๋ ๋ค๋ฅธ ๋ฐฉ๋ฒ(๋ก๊น , ๋ฉํธ๋ฆญ ๋ฑ)์ด ์๋์ง ํ์ธํ์ธ์.
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java (1)
46-47: ํ์์คํฌํ ์์ฑ ๋ฐฉ์์ด ์ผ๊ด๋์ง ์์ต๋๋ค.Line 46-47์์๋
LocalDateTime.now(ZoneId.systemDefault())๋ฅผ ์ฌ์ฉํ์ง๋ง, Line 55์์๋LocalDateTime.now()๋ฅผ ์ฌ์ฉํฉ๋๋ค.LocalDateTime.now()๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์์คํ ๊ธฐ๋ณธ ZoneId๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ๋์์ ๋์ผํ์ง๋ง, ์ฝ๋ ์ผ๊ด์ฑ์ ์ํด ๋์ผํ ๋ฐฉ์์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.์ฐธ๊ณ ๋ก
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java์์๋ ๋ชจ๋ ํ์์คํฌํ ์์ฑ์LocalDateTime.now()๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค.๐ ์ผ๊ด์ฑ ๊ฐ์ ์ ์
์ต์ 1: ZoneId ๋ช ์ ์ ๊ฑฐ (ProductRankMonthly์ ์ผ๊ด์ฑ ์ ์ง)
- e.createdAt = LocalDateTime.now(ZoneId.systemDefault()); - e.updatedAt = LocalDateTime.now(ZoneId.systemDefault()); + e.createdAt = LocalDateTime.now(); + e.updatedAt = LocalDateTime.now();์ต์ 2: ZoneId ๋ช ์ ์ถ๊ฐ
- this.updatedAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(ZoneId.systemDefault());Also applies to: 55-55
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/RankingFetchStrategyResolver.java (1)
62-77: ์ฝ๋ ์ค๋ณต ๋ฐ ๋ฉ์๋ ๋ค์ด๋ฐ ๊ฐ์ ์ด ํ์ํฉ๋๋ค.
initLocalDate์hasValidDate๋ฉ์๋๊ฐapps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/DailyRankingFetcher.java(lines 19-26)์๋ ๋์ผํ๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค.๋ํ
hasValidDate๋ฉ์๋๋ช ์ด ํผ๋์ค๋ฝ์ต๋๋ค. ์ด ๋ฉ์๋๋ date๊ฐ null์ด๊ฑฐ๋ blank์ผ ๋ true๋ฅผ ๋ฐํํ๋๋ฐ, "์ ํจํ ๋ ์ง๊ฐ ์๋๊ฐ?"๋ผ๋ ์๋ฏธ์ ๋ฐ๋๋ก ๋์ํฉ๋๋ค.๐ ๊ฐ์ ์ ์
1. ๊ณตํต ์ ํธ๋ฆฌํฐ ํด๋์ค๋ก ์ถ์ถ:
์๋ก์ด ์ ํธ๋ฆฌํฐ ํด๋์ค ์์ฑ:
public class RankingDateUtils { private static final DateTimeFormatter BASIC_ISO_DATE = DateTimeFormatter.BASIC_ISO_DATE; public static LocalDate parseOrToday(String date) { return (date == null || date.isBlank()) ? LocalDate.now(ZoneId.systemDefault()) : LocalDate.parse(date, BASIC_ISO_DATE); } }์ดํ RankingFetchStrategyResolver์ DailyRankingFetcher์์ ์ด ์ ํธ๋ฆฌํฐ๋ฅผ ์ฌ์ฉํ๋๋ก ๋ณ๊ฒฝ:
-private LocalDate initLocalDate(String date) { - return (hasValidDate(date)) - ? LocalDate.now(ZoneId.systemDefault()) - : LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE); -} - -private boolean hasValidDate(String date) { - return date == null || date.isBlank(); -} +private LocalDate initLocalDate(String date) { + return RankingDateUtils.parseOrToday(date); +}2. ๋๋ ๋ฉ์๋๋ช ๊ฐ์ (์ต์ํ์ ๋ณ๊ฒฝ):
-private boolean hasValidDate(String date) { +private boolean isDateEmpty(String date) { return date == null || date.isBlank(); } private LocalDate initLocalDate(String date) { - return (hasValidDate(date)) + return (isDateEmpty(date)) ? LocalDate.now(ZoneId.systemDefault()) : LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE); }
๐ Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (51)
README.mdapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingPeriod.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQuery.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/DailyRankingFetcher.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/MonthlyRankingFetcher.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/RankingFetchStrategy.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/RankingFetchStrategyResolver.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/WeeklyRankingFetcher.javaapps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.javaapps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.javaapps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.javaapps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankMonthlyRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankWeeklyRepository.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.javaapps/commerce-batch/build.gradle.ktsapps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.javaapps/commerce-batch/src/main/java/com/loopers/batch/infrastructure/db/ProductMetricDataInitializer.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessor.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessorConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyReaderConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyWriterConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/dto/MonthlyAggRow.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/dto/MonthlyRankRow.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/RankWeeklyJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyProcessor.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyReaderConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyWriterConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/dto/WeeklyAggRow.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/dto/WeeklyRankRow.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.javaapps/commerce-batch/src/main/resources/application.ymlapps/commerce-batch/src/main/resources/db/fixtures/large-product-metric-data.sqlapps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.javaapps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.javaapps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.javaapps/commerce-streamer/src/main/java/com/loopers/domain/MetricsType.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ProductLikeMetricsModel.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsId.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsModel.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductLikeMetricsRepository.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.javaapps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.javasettings.gradle.kts
๐ค Files with no reviewable changes (2)
- apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductLikeMetricsRepository.java
- apps/commerce-streamer/src/main/java/com/loopers/domain/ProductLikeMetricsModel.java
๐งฐ Additional context used
๐ง Learnings (10)
๐ Learning: 2025-11-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product ์ํฐํฐ (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)๋ ์ ์ฆ์ผ์ด์ค๋ณ๋ก ์๋์ ์ผ๋ก ๋ค๋ฅธ ๋ฝ ์ ๋ต์ ์ฌ์ฉํ๋ค: ์ข์์ ๊ธฐ๋ฅ์๋ ๋น๊ด์ ๋ฝ(findByIdForUpdate)์, ์ฌ๊ณ ์ฐจ๊ฐ์๋ ๋๊ด์ ๋ฝ(Version + ์ฌ์๋)์ ์ฌ์ฉํ๋ค.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.javaapps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.javaapps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsId.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsModel.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-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.javaapps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.javaapps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ProductMetricsId.javaapps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.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-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankWeeklyRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankMonthlyRepository.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/resources/application.ymlapps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java
๐ Learning: 2025-12-19T21:30:16.024Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/outbox/OutboxEventListener.kt:0-0
Timestamp: 2025-12-19T21:30:16.024Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template Kafka event pipeline, Like events (LikeCreatedEventV1, LikeCanceledEventV1) intentionally use aggregateType="Like" with aggregateId=productId. The aggregateId serves as a partitioning/grouping key (not a unique Like entity identifier), ensuring all like events for the same product go to the same partition for ordering guarantees and aligning with ProductStatisticService's product-based aggregation logic. Using individual like_id would scatter events across partitions and break the statistics aggregation pattern.
Applied to files:
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.javaapps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
๐ Learning: 2026-01-01T14:12:04.385Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 76
File: apps/commerce-batch/src/main/kotlin/com/loopers/batch/listener/ChunkListener.kt:4-5
Timestamp: 2026-01-01T14:12:04.385Z
Learning: In Spring Batch 6.x, ChunkListener API changed from using ChunkContext to Chunk<T> parameters. The correct method signatures are: beforeChunk(Chunk<T> chunk), afterChunk(Chunk<T> chunk), and onChunkError(Exception exception, Chunk<T> chunk). The import path is org.springframework.batch.core.listener.ChunkListener.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
๐ Learning: 2026-01-01T14:12:07.943Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 76
File: apps/commerce-batch/src/main/kotlin/com/loopers/batch/listener/JobListener.kt:4-5
Timestamp: 2026-01-01T14:12:07.943Z
Learning: Spring Batch 6.x์์ JobExecution์ org.springframework.batch.core.job ํจํค์ง์, JobExecutionListener๋ org.springframework.batch.core.listener ํจํค์ง์ ์์นํฉ๋๋ค. Spring Batch 5.x์ ๋ฌ๋ฆฌ ํ์ ํจํค์ง๋ฅผ ์ฌ์ฉํฉ๋๋ค.
Applied to files:
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
๐ Learning: 2025-12-19T20:59:57.713Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: docs/week8/round8-detailed-design.md:151-178
Timestamp: 2025-12-19T20:59:57.713Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository's Kafka event pipeline, only 5 domain events are intentionally published to Kafka via CloudEventEnvelopeFactory: OrderPaidEventV1, LikeCreatedEventV1, LikeCanceledEventV1, ProductViewedEventV1, and StockDepletedEventV1. Other domain events (OrderCreatedEventV1, OrderCanceledEventV1, PaymentCreatedEventV1, PaymentPaidEventV1, PaymentFailedEventV1) are internal-only and intentionally not mapped in resolveMetadata(), which correctly returns null for them to exclude them from Outbox publication.
Applied to files:
apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
๐ Learning: 2025-12-19T23:39:20.851Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-streamer/src/main/kotlin/com/loopers/interfaces/consumer/product/ProductEventConsumer.kt:0-0
Timestamp: 2025-12-19T23:39:20.851Z
Learning: In Loopers-dev-lab/loopers-spring-kotlin-template, toongri prefers pragmatic idempotency handling in Kafka consumers: when idempotency key persistence fails after successful business logic execution, log a warning and continue (accepting low risk of duplicates on retry) rather than rolling back business data. This keeps business logic decoupled from idempotency store (which might be Redis/external system, not RDB), avoids over-engineering for rare edge cases, and prioritizes core business value over auxiliary metadata. Transaction-based atomicity between business data and idempotency keys is considered excessive when duplicate event probability is very low.
Applied to files:
apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
๐ Learning: 2025-12-19T03:23:54.756Z
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:54.756Z
Learning: In the loopers-spring-java-template project, test container configurations (MySQL, Redis, Kafka) use static initialization blocks for consistency across the template. This pattern should be maintained for uniformity, even if alternative lifecycle management approaches exist.
Applied to files:
apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.javaapps/commerce-batch/src/main/java/com/loopers/batch/infrastructure/db/ProductMetricDataInitializer.javaapps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java
๐งฌ Code graph analysis (17)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java (1)
Entity(12-65)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/RankWeeklyJobConfig.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyWriterConfig.java (1)
Configuration(12-39)apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyReaderConfig.java (1)
Configuration(16-80)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java (1)
ConditionalOnProperty(19-48)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (1)
Slf4j(9-21)apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
Slf4j(14-53)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyWriterConfig.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessorConfig.java (1)
Configuration(8-17)apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyReaderConfig.java (1)
Configuration(16-77)
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/RankingFetchStrategyResolver.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/DailyRankingFetcher.java (1)
Component(14-39)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
Slf4j(14-53)apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
Slf4j(13-44)
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/WeeklyRankingFetcher.java (3)
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/MonthlyRankingFetcher.java (1)
Slf4j(13-29)apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/DailyRankingFetcher.java (1)
Component(14-39)apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/RankingFetchStrategyResolver.java (1)
Component(16-78)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java (1)
Entity(13-65)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyReaderConfig.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
StepScope(14-32)apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyWriterConfig.java (1)
Configuration(12-39)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.java (1)
Embeddable(9-21)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyReaderConfig.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
StepScope(14-32)apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessorConfig.java (1)
Configuration(8-17)apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyWriterConfig.java (1)
Configuration(12-39)
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyProcessorConfig.java (3)
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (1)
StepScope(14-32)apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyReaderConfig.java (1)
Configuration(16-77)apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/RankMonthlyWriterConfig.java (1)
Configuration(12-39)
apps/commerce-batch/src/main/java/com/loopers/batch/infrastructure/db/ProductMetricDataInitializer.java (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/config/KafkaTopicsConfig.java (1)
Profile(9-37)
apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
SpringBootTest(6-10)
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
SpringBootTest(23-76)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthlyId.java (1)
Embeddable(9-21)
๐ Additional comments (36)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
9-9: ๋ณ๊ฒฝ ์ฌํญ ์น์ธ!
UserActionTypeimport ์ถ๊ฐ๊ฐ ์ ์ ํฉ๋๋ค. ์ด import๋ ๊ธฐ์กด@TrackUserAction์ด๋ ธํ ์ด์ (28๋ฒ, 42๋ฒ ๋ผ์ธ)์์ ์ฌ์ฉ๋๊ณ ์์ผ๋ฉฐ, ์ฝ๋์ ๋ช ์์ฑ์ ๊ฐ์ ํฉ๋๋ค.apps/commerce-batch/src/main/resources/application.yml (2)
1-18: ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ๊ธฐ๋ณธ ์ค์ ์ด ์ ์ ํฉ๋๋ค.
web-application-type: none๊ณผjob.name: ${job.name:NONE}์ค์ ์ด ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํ๋ฉฐ, ๊ธฐ๋ณธ๊ฐNONE์ ํตํด ์๋ํ์ง ์์ ์์ ์คํ์ ๋ฐฉ์งํ๋ ์์ ์ฅ์น๊ฐ ์ ๊ตฌํ๋์ด ์์ต๋๋ค.
17-18: Spring Batch ๋ฉํ๋ฐ์ดํฐ ํ ์ด๋ธ ์คํค๋ง ๊ด๋ฆฌ ์ ๋ต์ ๋ช ํํ ํ์ธ์.
local/testํ๋กํ์ผ์initialize-schema: always๋ก ์ค์ ๋์ง๋ง,dev/qa/prdํ๋กํ์ผ์ ๊ธฐ๋ณธ๊ฐnever๋ฅผ ์์ํฉ๋๋ค. ํ์ฌ ํ๋ก์ ํธ์์ Flyway๋ Liquibase ๊ฐ์ ์คํค๋ง ๋ง์ด๊ทธ๋ ์ด์ ๋๊ตฌ๊ฐ ๊ตฌ์ฑ๋์ง ์์๊ธฐ ๋๋ฌธ์, ์ด์ ํ๊ฒฝ์์ Spring Batch ๋ฉํ๋ฐ์ดํฐ ํ ์ด๋ธ(BATCH_JOB_INSTANCE,BATCH_JOB_EXECUTION๋ฑ)์ด ์ด๋ป๊ฒ ์์ฑ๋๊ณ ๊ด๋ฆฌ๋๋์ง ๋ช ์ํด์ผ ํฉ๋๋ค.ํด๋น ํ ์ด๋ธ์ด ๋ฏธ๋ฆฌ ์์ฑ๋์ด ์๊ฑฐ๋, ํฅํ ์คํค๋ง ๋ง์ด๊ทธ๋ ์ด์ ๋๊ตฌ ๋์ ๊ณํ์ด ์๋ค๋ฉด ์ฝ๋๋ ์ค์ ํ์ผ์ ๋ช ์ํ์ธ์.
โ 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/src/main/resources/db/fixtures/large-product-metric-data.sql (1)
5-44: Fixture ๋ฐ์ดํฐ ์์ฑ ๋ก์ง์ด ์ ์ ํฉ๋๋ค.Cross join์ ํ์ฉํ ๋ฒํฌ ๋ฐ์ดํฐ ์์ฑ ํจํด์ด ํจ์จ์ ์ด๋ฉฐ, metrics_type๋ณ๋ก ํ์ค์ ์ธ count ๋ฒ์๋ฅผ ์ค์ ํ ๊ฒ์ด ์ข์ต๋๋ค. Local ํ๋กํ์ผ์์๋ง ์คํ๋๋ฏ๋ก ์ด์ ํ๊ฒฝ์ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/domain/MetricsType.java (1)
1-8: LGTM!enum ์์๋ค์ด SQL fixture ๋ฐ ๋๋ฉ์ธ ์๊ตฌ์ฌํญ๊ณผ ์ผ์นํ๋ฉฐ, ํ์ฅ ๊ฐ๋ฅํ ๊ตฌ์กฐ์ ๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.java (1)
1-8: LGTM!๋ณตํฉ ํค(
ProductMetricsId)๋ฅผ ์ฌ์ฉํ๋ ํ์ค Spring Data JPA ๋ ํฌ์งํ ๋ฆฌ์ ๋๋ค. ํ์์ ์ปค์คํ ์ฟผ๋ฆฌ ๋ฉ์๋๋ฅผ ์ถ๊ฐํ ์ ์์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.java (1)
18-28: ๋ณตํฉ ํค ๊ธฐ๋ฐ ๋ฉํธ๋ฆญ ์ฒ๋ฆฌ ๋ก์ง์ด ์ ์ ํฉ๋๋ค.
findById().orElseGet()ํจํด์ผ๋ก ์กฐํ ๋๋ ์์ฑ ํincrease()๋ฅผ ํธ์ถํ๋ ๋ฐฉ์์ด JPA dirty checking๊ณผ ํจ๊ป ์ ์ ๋์ํฉ๋๋ค.@Transactional๋ด์์ ์ํฐํฐ ๋ณ๊ฒฝ์ด ํธ๋์ญ์ ์ข ๋ฃ ์ ์๋์ผ๋ก flush๋ฉ๋๋ค.apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java (4)
3-7: ์๋ก์ด ProductMetrics ๋๋ฉ์ธ ๋ชจ๋ธ๋ก์ ๋ง์ด๊ทธ๋ ์ด์ ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๋ฐ์๋์์ต๋๋ค.์ํฌํธ ๋ฐ ๋ก๊น ์ด๋ ธํ ์ด์ ์ถ๊ฐ๊ฐ ์ ์ ํฉ๋๋ค. MetricsType, ProductMetricsId, ProductMetricsModel๋ก์ ์ ํ์ด PR ๋ชฉํ์ ์ผ์นํฉ๋๋ค.
Also applies to: 10-10, 26-26
38-38: ๋ฆฌํฌ์งํ ๋ฆฌ ํ์ ์ ๋ฐ์ดํธ๊ฐ ์ฌ๋ฐ๋ฆ ๋๋ค.ProductMetricsRepository๋ก์ ์ ํ์ด ํตํฉ๋ ๋ฉํธ๋ฆญ ๋ชจ๋ธ๊ณผ ์ผ์นํฉ๋๋ค.
68-73: ๋น๋๊ธฐ ์ฒ๋ฆฌ ๋๊ธฐ๋ฅผ ์ํ Thread.sleep ์ฌ์ฉ์ ๋ํ TODO๊ฐ ์์ต๋๋ค.Thread.sleep์ ์ฌ์ฉํ ๋๊ธฐํ๋ ํ ์คํธ ์์ ์ฑ ์ธก๋ฉด์์ ์ทจ์ฝํ ์ ์์ผ๋, ์ด๋ฏธ TODO ์ฃผ์(line 72)์ผ๋ก ๊ฐ์ ํ์์ฑ์ด ๋ฌธ์ํ๋์ด ์์ต๋๋ค. ํฅํ Awaitility ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ TestContainers์ ์ปจ์๋จธ ๋๊ธฐํ ๋ฉ์ปค๋์ฆ์ ๊ณ ๋ คํด๋ณด์๊ธฐ ๋ฐ๋๋๋ค.
57-62: ๋ณตํฉ ํค ํจํด์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ ์ฉ๋์์ต๋๋ค.ProductMetricsId์ MetricsType.LIKE๋ฅผ ์ฌ์ฉํ ๊ฒ์ด ์ ์ ํ๋ฉฐ, ๋ก๊น ์ถ๊ฐ๊ฐ ํ ์คํธ ๋๋ฒ๊น ์ ์ ์ฉํฉ๋๋ค. ProductMetricsId.of(productId, metricsType) ํฉํ ๋ฆฌ ๋ฉ์๋๊ฐ ์๋์ผ๋ก metricsDate๋ฅผ ํ์ฌ ๋ ์ง์ yyyyMMdd ํ์์ผ๋ก ์ด๊ธฐํํ๋ฏ๋ก, ๋ณตํฉ ํค์ ์ธ ํ๋(productId, metricsDate, metricsType) ๋ชจ๋๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ๋ฉ๋๋ค. ๋ฐ์ดํฐ๋ฒ ์ด์ค์ NOT NULL ์ ์ฝ ์กฐ๊ฑด๋ ๋ง์กฑํฉ๋๋ค.
README.md (1)
30-30: LGTM! ๋ฌธ์ ์ ๋ฐ์ดํธ๊ฐ ์ ์ ํฉ๋๋ค.์๋ก์ด commerce-batch ๋ชจ๋์ด ํ๋ก์ ํธ ํธ๋ฆฌ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ถ๊ฐ๋์์ต๋๋ค.
settings.gradle.kts (1)
6-6: LGTM! ๋ชจ๋ ์ถ๊ฐ๊ฐ ์ ์ ํฉ๋๋ค.commerce-batch ๋ชจ๋์ด Gradle ๋ฉํฐ๋ชจ๋ ํ๋ก์ ํธ์ ์ฌ๋ฐ๋ฅด๊ฒ ๋ฑ๋ก๋์์ต๋๋ค.
apps/commerce-batch/build.gradle.kts (1)
1-21: ์์กด์ฑ ๊ตฌ์ฑ์ด ์ ๋ฐ์ ์ผ๋ก ์ ์ ํฉ๋๋ค.ํ์ํ ๋ชจ๋๋ค(jpa, redis, jackson, logging, monitoring)๊ณผ Spring Batch ์์กด์ฑ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค. test-fixtures ์ฌ์ฉ๋ ์ ์ ํฉ๋๋ค.
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
6-9: LGTM! ํ์ค ์ค๋ชจํฌ ํ ์คํธ ํจํด์ ๋๋ค.Spring Boot ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ๊ฐ ์ ์์ ์ผ๋ก ๋ก๋๋๋์ง ํ์ธํ๋ ๊ธฐ๋ณธ ํ ์คํธ์ ๋๋ค. ๋น ํ ์คํธ ๋ณธ๋ฌธ์ ์๋๋ ๊ฒ์ผ๋ก, ์ค์ ์ค๋ฅ๋ฅผ ์กฐ๊ธฐ์ ๋ฐ๊ฒฌํ๋ ๋ฐ ์ ์ฉํฉ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (1)
25-28: LGTM! ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํ ์ข ๋ฃ ํจํด์ ๋๋ค.
SpringApplication.exit()์System.exit()๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐฐ์น ์์ ์๋ฃ ํ ์ ์ ํ๊ฒ ์ข ๋ฃ ์ฝ๋๋ฅผ ์ ํํ๊ณ ์์ต๋๋ค.apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
58-75: ํ ์คํธ ๊ตฌ์กฐ์ ๊ฒ์ฆ ๋ก์ง์ด ์ ์ ํฉ๋๋ค.์ฑ๊ณต ์ผ์ด์ค์์
JobParametersBuilder๋ฅผ ์ฌ์ฉํ์ฌrequestDate๋ฅผ ์ค์ ํ๊ณ ,ExitStatus.COMPLETED๋ฅผ ๊ฒ์ฆํ๋ ๋ฐฉ์์ด ์ ๊ตฌํ๋์ด ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeeklyId.java (1)
9-14: LGTM!JPA ์๋ฒ ๋๋ ID ํด๋์ค ๊ตฌ์กฐ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.
Serializable๊ตฌํ,equals/hashCode์ค๋ฒ๋ผ์ด๋, ๊ทธ๋ฆฌ๊ณ protected ๊ธฐ๋ณธ ์์ฑ์๊ฐ ๋ชจ๋ ์ ์ ํ๊ฒ ํฌํจ๋์ด ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingPeriod.java (1)
3-7: LGTM!์ ๋ต ํจํด์์ ์ฌ์ฉ๋๋ ๊ธฐ๊ฐ ํ์ ์ ๋ช ํํ๊ฒ ์ ์ํ๋ ๊ฐ๊ฒฐํ enum์ ๋๋ค. ํ์ ์์ ์ฑ์ ์ ๊ณตํ๋ฉฐ ํฅํ ํ์ฅ์ด ์ฉ์ดํ ๊ตฌ์กฐ์ ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/dto/MonthlyRankRow.java (2)
3-10: ๊ฐ๊ฒฐํ๊ณ ๋ช ํํ DTO ์ค๊ณ์ ๋๋ค.Record๋ฅผ ์ฌ์ฉํ์ฌ ๋ถ๋ณ ๋ฐ์ดํฐ ์ ์ก ๊ฐ์ฒด๋ฅผ ๊ตฌํํ ๊ฒ์ ์ ์ ํฉ๋๋ค. ํ๋๋ช ๊ณผ ํ์ ์ด ๋ช ํํ๋ฉฐ ๋ฐฐ์น ์์ ์ ์ถ๋ ฅ ๋ชจ๋ธ๋ก ์ ํฉํฉ๋๋ค.
5-5:productId๊ฐ null์ด ๋ ์ ์๋ ๊ฒฝ๋ก๋ฅผ ๊ฒํ ํ์ธ์.Reader์ SQL ์ฟผ๋ฆฌ์์
GROUP BY pm.product_id๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ๋ง์ฝ ์๋ณธ ๋ฐ์ดํฐ์ nullproduct_id๊ฐ ์กด์ฌํ๋ฉด, SQL GROUP BY๋ null์ ๋ณ๋์ ๊ทธ๋ฃน์ผ๋ก ํฌํจ์ํต๋๋ค. ํ์ฌ ์ฝ๋๋:
MonthlyAggRow์MonthlyRankRow๋ชจ๋Long productId(nullable)- Reader:
rs.getLong()์ฌ์ฉ (nullable ์ปฌ๋ผ์ ๋ํด ๋ถ์ ์ ํ ์ ์์)- Processor: null ์ฒดํฌ ์์ด ๊ทธ๋๋ก ์ ๋ฌ
- Writer:
:productId๋ฅผ SQL์ ์ฌ์ฉ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ๋ ค๋ฉด, Reader ๋จ๊ณ์์ null
product_idํ์ ํํฐ๋งํ๊ฑฐ๋,productId๋ฅผ primitivelong์ผ๋ก ๋ณ๊ฒฝํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.โ Skipped due to learnings
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() ํฉํ ๋ฆฌ ๋ฉ์๋๋ง์ผ๋ก ์ถฉ๋ถํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.java (1)
28-28: ์ ๋ต ํจํด์ ์ํ ์ ์ ํ ์์กด์ฑ ์ถ๊ฐ์ ๋๋ค.
RankingFetchStrategyResolver์์กด์ฑ์ ์ฃผ์ ํ์ฌ ๊ธฐ๊ฐ๋ณ ๋ญํน ์กฐํ ์ ๋ต์ ์ ํํ ์ ์๋๋ก ํ ๊ฒ์ ์ข์ ์ค๊ณ์ ๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/job/rankMonthly/dto/MonthlyAggRow.java (2)
3-8: ๊ฐ๊ฒฐํ ์ ๋ ฅ DTO ์ค๊ณ์ ๋๋ค.๋ฐฐ์น Reader์ ์ ๋ ฅ ๋ชจ๋ธ๋ก ์ ์ ํ ๊ตฌ์กฐ์ ๋๋ค.
MonthlyRankRow์์ ๋ช ํํ ๊ตฌ๋ถ(yearMonth์ score ์์)์ด Reader-Processor-Writer ํ์ดํ๋ผ์ธ์ ์ฑ ์ ๋ถ๋ฆฌ๋ฅผ ์ ๋ํ๋ ๋๋ค.
4-4: productId๋ NULL ๊ฐ์ ๊ฐ์ง ์ ์์ผ๋ฏ๋ก ์ถ๊ฐ ์กฐ์น๊ฐ ๋ถํ์ํฉ๋๋ค.SQL ์ฟผ๋ฆฌ์
GROUP BY pm.product_id์ ๋๋ฌธ์ NULL productId๋ ๊ฒฐ๊ณผ ์งํฉ์ ํฌํจ๋์ง ์์ต๋๋ค. GROUP BY๋ NULL ๊ฐ์ ํํฐ๋งํ๋ฏ๋ก Reader๊ฐ ๋ฐํํ๋ ๋ชจ๋ productId๋ NULL์ด ๋ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ ํ์ฌ ์ฝ๋๋ NULL ์ฒ๋ฆฌ ๊ด์ ์์ ์์ ํฉ๋๋ค.๋ค๋ง, ํฅํ ์ฟผ๋ฆฌ ๊ตฌ์กฐ๊ฐ ๋ณ๊ฒฝ๋๊ฑฐ๋ LEFT JOIN ๋ฑ์ด ์ถ๊ฐ๋ ๊ฒฝ์ฐ๋ฅผ ๋๋นํด
rs.getLong()๋์rs.getObject("product_id", Long.class)์ฌ์ฉ์ ๊ณ ๋ คํ๊ฑฐ๋, productId๋ฅผ primitivelong์ผ๋ก ๋ณ๊ฒฝํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/WeeklyRankingFetcher.java (2)
13-29: ์ ๋ต ํจํด ๊ตฌํ์ด ์ผ๊ด๋๊ณ ๋ช ํํฉ๋๋ค.
WeeklyRankingFetcher๊ฐRankingFetchStrategy์ธํฐํ์ด์ค๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํํ์ผ๋ฉฐ,DailyRankingFetcher๋ฐMonthlyRankingFetcher์ ์ผ๊ด๋ ํจํด์ ๋ฐ๋ฆ ๋๋ค. ์ฝ๋๊ฐ ๊ฐ๊ฒฐํ๊ณ ์ฑ ์์ด ๋ช ํํฉ๋๋ค.
24-28: ๊ธ๋ก๋ฒ ์์ธ ํธ๋ค๋ฌ๊ฐ ์ด๋ฏธ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
weeklyRankingRepository.findTopByYearWeek()ํธ์ถ์์ ๋ฐ์ํ๋ ์์ธ๋ApiControllerAdvice์ ์ผ๋ฐ์ ์ธ Throwable ํธ๋ค๋ฌ(line 118-122)์ ์ํด ์๋์ผ๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๋ฅ๋ ์ฟผ๋ฆฌ ์ค๋ฅ๋ErrorType.INTERNAL_ERROR๋ก ์๋ต๋ฉ๋๋ค.ํ์ฌ ๊ตฌ์กฐ๋ ์ผ๊ด์ฑ ์๊ฒ ์ค๊ณ๋์์ผ๋, ๋ค์์ ๊ณ ๋ คํ์ธ์:
- ์ ์ฅ์ ์ค๋ฅ์ ๋ํ ํน์ ์๋ฌ ์๋ต์ด ํ์ํ๋ฉด
CoreException์ ๋ช ์์ ์ผ๋ก ๋์ง๊ฑฐ๋ApiControllerAdvice์ ๋ณ๋ ํธ๋ค๋ฌ ์ถ๊ฐ- yearWeek ํ์ ๊ฒ์ฆ์ด ํ์ํ๋ฉด ์ฟผ๋ฆฌ ์ ์ ์ ํจ์ฑ ๊ฒ์ฌ ์ถ๊ฐ
- ํ์ฌ๋ ๋ค๋ฅธ fetcher์ ๋์ผํ ํจํด์ด๋ฏ๋ก ์ผ๊ด์ฑ์ ์ ์ง๋จ
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
25-52: ์๊ฐ ๊ณ์ฐ ๋ฐ ๋ก๊น ๋ก์ง์ด ์ ํํฉ๋๋ค.Duration์ ์ฌ์ฉํ ์๊ฐ ๊ณ์ฐ๊ณผ ํฌ๋งทํ ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/dto/WeeklyRankRow.java (1)
1-10: Record ์ ์๊ฐ ์ ์ ํฉ๋๋ค.๋ฐฐ์น ์ฒ๋ฆฌ์ ์ฌ์ฉ๋๋ DTO๋ก์ ๋ถ๋ณ record ํ์ ์ด ์ ํฉํ๊ฒ ์ ํ๋์๊ณ , ํ๋ ํ์ ๋ ์ฌ๋ฐ๋ฅด๊ฒ ์ ์๋์ด ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/RankingFetchStrategy.java (1)
1-12: ์ ๋ต ํจํด ์ธํฐํ์ด์ค๊ฐ ์ ์ค๊ณ๋์์ต๋๋ค.period๋ณ ranking ์กฐํ ์ ๋ต์ ์ถ์ํํ ์ธํฐํ์ด์ค ์ค๊ณ๊ฐ ๋ช ํํ๊ณ ํ์ฅ ๊ฐ๋ฅํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankWeeklyRepository.java (1)
13-25: LGTM!๋ฆฌํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค์ JPQL ์ฟผ๋ฆฌ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.
RankingEntry์์ฑ์ ํํ์๊ณผPageable์ ํ์ฉํ ํ์ด์ง ์ฒ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankMonthlyRepository.java (1)
13-25: LGTM!
ProductRankWeeklyRepository์ ์ผ๊ด๋ ํจํด์ผ๋ก ๊ตฌํ๋์์ต๋๋ค. ์๊ฐ ์์ ์กฐํ๋ฅผ ์ํ ์ฟผ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/MonthlyRankingFetcher.java (1)
13-29: LGTM!์ ๋ต ํจํด์ ํ์ฉํ ๊น๋ํ ๊ตฌํ์ ๋๋ค.
WeeklyRankingFetcher์ ์ผ๊ด๋ ํจํด์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyProcessor.java (1)
8-21: ๊ตฌํ์ด ์ฌ๋ฐ๋ฆ ๋๋ค.๋ฐฐ์น ํ๋ก์ธ์๊ฐ WeeklyAggRow๋ฅผ WeeklyRankRow๋ก ์ ํํ๊ฒ ๋ณํํ๊ณ ์์ผ๋ฉฐ, ์ ์ ๊ณ์ฐ ๋ก์ง๋ ํตํฉ๋์ด ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java (1)
12-37: ์ํฐํฐ ๊ตฌ์กฐ๊ฐ ์ฌ๋ฐ๋ฆ ๋๋ค.๋ณตํฉ ํค์ ํ๋ ๋งคํ์ด ์ ์ ํ๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/DailyRankingFetcher.java (1)
14-28: ์ ๋ต ํจํด ๊ตฌํ์ด ์ฌ๋ฐ๋ฆ ๋๋ค.
RankingFetchStrategy์ธํฐํ์ด์ค๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํํ๊ณ ์์ผ๋ฉฐ, ์ผ์ผ ์์ ์กฐํ ๋ก์ง์ด ์ ์ ํ๊ฒ ์บก์ํ๋์ด ์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/RankWeeklyJobConfig.java (1)
23-56: Spring Batch ๊ตฌ์ฑ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์์ฑ๋์์ต๋๋ค.chunk ํฌ๊ธฐ 1000์ผ๋ก ์ค์ ๋ ํ์ค์ ์ธ chunk-oriented step ๊ตฌ์ฑ์ด๋ฉฐ, Reader/Processor/Writer๊ฐ ์ ์ ํ ์ฐ๊ฒฐ๋์ด ์์ต๋๋ค. Job๊ณผ Step ์์ค์ ๋ฆฌ์ค๋๋ ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ๋์์ต๋๋ค.
| private LocalDate initLocalDate(String date) { | ||
| return (hasValidDate(date)) | ||
| ? LocalDate.now(ZoneId.systemDefault()) | ||
| : LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE); | ||
| } | ||
|
|
||
| private boolean hasValidDate(String date) { | ||
| return date == null || date.isBlank(); | ||
| } |
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.
๋ฉ์๋ ์ด๋ฆ์ด ์ค์ ๋์๊ณผ ๋ฐ๋์ ๋๋ค.
hasValidDate() ๋ฉ์๋๋ date๊ฐ null์ด๊ฑฐ๋ blank์ผ ๋ true๋ฅผ ๋ฐํํฉ๋๋ค. ์ด๋ "์ ํจํ ๋ ์ง๊ฐ ์๋์ง"๋ฅผ ํ์ธํ๋ ๊ฒ์ด ์๋๋ผ "๋ ์ง๊ฐ ์๋์ง"๋ฅผ ํ์ธํ๋ ๊ฒ์ด๋ฏ๋ก, ๋ฉ์๋ ์ด๋ฆ์ด ๋ก์ง๊ณผ ์ผ์นํ์ง ์์ ํผ๋์ ์ผ๊ธฐํฉ๋๋ค.
๋ํ ๋ ์ง ํ์ฑ ์ DateTimeParseException์ ๋ํ ์์ธ ์ฒ๋ฆฌ๊ฐ ์์ด, ์๋ชป๋ ํ์์ ๋ ์ง๊ฐ ์ ๋ฌ๋ ๊ฒฝ์ฐ ์ ์ ํ ์๋ฌ ์๋ต ์์ด ์์ธ๊ฐ ๋ฐ์ํฉ๋๋ค.
๐ ์ ์ํ๋ ๊ฐ์ ์ฌํญ
private LocalDate initLocalDate(String date) {
- return (hasValidDate(date))
+ return (isDateMissing(date))
? LocalDate.now(ZoneId.systemDefault())
- : LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE);
+ : parseDate(date);
}
-private boolean hasValidDate(String date) {
+private boolean isDateMissing(String date) {
return date == null || date.isBlank();
}
+private LocalDate parseDate(String date) {
+ try {
+ return LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE);
+ } catch (DateTimeParseException e) {
+ throw new IllegalArgumentException(
+ "Invalid date format. Expected format: yyyyMMdd, but got: " + date, e);
+ }
+}๐ Committable suggestion
โผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private LocalDate initLocalDate(String date) { | |
| return (hasValidDate(date)) | |
| ? LocalDate.now(ZoneId.systemDefault()) | |
| : LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE); | |
| } | |
| private boolean hasValidDate(String date) { | |
| return date == null || date.isBlank(); | |
| } | |
| private LocalDate initLocalDate(String date) { | |
| return (isDateMissing(date)) | |
| ? LocalDate.now(ZoneId.systemDefault()) | |
| : parseDate(date); | |
| } | |
| private boolean isDateMissing(String date) { | |
| return date == null || date.isBlank(); | |
| } | |
| private LocalDate parseDate(String date) { | |
| try { | |
| return LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE); | |
| } catch (DateTimeParseException e) { | |
| throw new IllegalArgumentException( | |
| "Invalid date format. Expected format: yyyyMMdd, but got: " + date, e); | |
| } | |
| } |
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/strategy/DailyRankingFetcher.java
around lines 30-38, the helper method name and logic are inverted and there is
no parsing error handling: rename or flip hasValidDate so its name matches its
behavior (e.g., isBlankOrNull -> retains current boolean) or change its logic to
return true when the input is a valid non-blank date, then update initLocalDate
to use the corrected predicate; additionally wrap LocalDate.parse in a try/catch
for DateTimeParseException and handle it by throwing a controlled application
exception or returning a clear error value (or fallback) with a descriptive
message so malformed dates do not propagate raw exceptions.
| public static ProductRankMonthly of(String yearMonth, Long productId, | ||
| long viewCount, long likeCount, long orderCount) { | ||
| ProductRankMonthly e = new ProductRankMonthly(); | ||
| e.id = new ProductRankMonthlyId(yearMonth, productId); | ||
| e.viewCount = viewCount; | ||
| e.likeCount = likeCount; | ||
| e.orderCount = orderCount; | ||
| e.createdAt = LocalDateTime.now(); | ||
| e.updatedAt = LocalDateTime.now(); | ||
| return e; | ||
| } |
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.
์น๋ช ์ : score ํ๋๊ฐ ์ด๊ธฐํ๋์ง ์์ต๋๋ค.
score ํ๋๋ nullable = false๋ก ์ ์ธ๋์ด ์์ง๋ง of() ๋ฉ์๋์์ ์ด๊ธฐํ๋์ง ์์ต๋๋ค. ์ด๋ ์ํฐํฐ ์ ์ฅ ์ ์ ์ฝ์กฐ๊ฑด ์๋ฐ(constraint violation)์ ๋ฐ์์ํต๋๋ค.
๋ฐฐ์น ์์
์ RankMonthlyProcessor์์ ์ ์๋ฅผ ๊ณ์ฐํ๋ฏ๋ก, ํฉํ ๋ฆฌ ๋ฉ์๋๊ฐ score ๋งค๊ฐ๋ณ์๋ฅผ ๋ฐ์์ผ ํฉ๋๋ค.
๐ ์ ์ํ๋ ์์ ์ฌํญ
public static ProductRankMonthly of(String yearMonth, Long productId,
- long viewCount, long likeCount, long orderCount) {
+ long viewCount, long likeCount, long orderCount, double score) {
ProductRankMonthly e = new ProductRankMonthly();
e.id = new ProductRankMonthlyId(yearMonth, productId);
e.viewCount = viewCount;
e.likeCount = likeCount;
e.orderCount = orderCount;
+ e.score = score;
e.createdAt = LocalDateTime.now();
e.updatedAt = LocalDateTime.now();
return e;
}๋์ผํ ๋ฌธ์ ๊ฐ ProductRankWeekly.java์๋ ์กด์ฌํ๋ฏ๋ก ํจ๊ป ์์ ์ด ํ์ํฉ๋๋ค.
๐ 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.
| public static ProductRankMonthly of(String yearMonth, Long productId, | |
| long viewCount, long likeCount, long orderCount) { | |
| ProductRankMonthly e = new ProductRankMonthly(); | |
| e.id = new ProductRankMonthlyId(yearMonth, productId); | |
| e.viewCount = viewCount; | |
| e.likeCount = likeCount; | |
| e.orderCount = orderCount; | |
| e.createdAt = LocalDateTime.now(); | |
| e.updatedAt = LocalDateTime.now(); | |
| return e; | |
| } | |
| public static ProductRankMonthly of(String yearMonth, Long productId, | |
| long viewCount, long likeCount, long orderCount, double score) { | |
| ProductRankMonthly e = new ProductRankMonthly(); | |
| e.id = new ProductRankMonthlyId(yearMonth, productId); | |
| e.viewCount = viewCount; | |
| e.likeCount = likeCount; | |
| e.orderCount = orderCount; | |
| e.score = score; | |
| e.createdAt = LocalDateTime.now(); | |
| e.updatedAt = LocalDateTime.now(); | |
| return e; | |
| } |
๐ค Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java
around lines 39-49, the factory method of(...) fails to initialize the
non-nullable score field causing constraint violations; update the method
signature to accept a score parameter (e.g., double or appropriate numeric
type), set e.score = score before returning, and ensure callers (including
RankMonthlyProcessor batch code) pass the computed score; apply the identical
change to ProductRankWeekly to keep both factory methods consistent.
| public void overwrite(long viewCount, long likeCount, long orderCount) { | ||
| this.viewCount = viewCount; | ||
| this.likeCount = likeCount; | ||
| this.orderCount = orderCount; | ||
| this.updatedAt = LocalDateTime.now(); | ||
| } |
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.
์น๋ช ์ : score ํ๋๊ฐ ์ ๋ฐ์ดํธ๋์ง ์์ต๋๋ค.
overwrite() ๋ฉ์๋์์ ์นด์ดํธ ํ๋๋ค์ ์
๋ฐ์ดํธํ์ง๋ง score ํ๋๋ ์
๋ฐ์ดํธํ์ง ์์ต๋๋ค. ์นด์ดํธ๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์ ์๋ ์ฌ๊ณ์ฐ๋์ด์ผ ํ๋ฏ๋ก, score ๋งค๊ฐ๋ณ์๋ฅผ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
๐ ์ ์ํ๋ ์์ ์ฌํญ
-public void overwrite(long viewCount, long likeCount, long orderCount) {
+public void overwrite(long viewCount, long likeCount, long orderCount, double score) {
this.viewCount = viewCount;
this.likeCount = likeCount;
this.orderCount = orderCount;
+ this.score = score;
this.updatedAt = LocalDateTime.now();
}๋์ผํ ๋ฌธ์ ๊ฐ ProductRankWeekly.java์ overwrite() ๋ฉ์๋์๋ ์กด์ฌํฉ๋๋ค.
๐ 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.
| public void overwrite(long viewCount, long likeCount, long orderCount) { | |
| this.viewCount = viewCount; | |
| this.likeCount = likeCount; | |
| this.orderCount = orderCount; | |
| this.updatedAt = LocalDateTime.now(); | |
| } | |
| public void overwrite(long viewCount, long likeCount, long orderCount, double score) { | |
| this.viewCount = viewCount; | |
| this.likeCount = likeCount; | |
| this.orderCount = orderCount; | |
| this.score = score; | |
| this.updatedAt = LocalDateTime.now(); | |
| } |
๐ค Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankMonthly.java
around lines 51โ56, the overwrite(...) method updates viewCount, likeCount,
orderCount and updatedAt but does not update the score; change the method
signature to accept a score parameter (e.g., double or long matching the score
field type), assign this.score = score inside the method, and update updatedAt
as now; also search for and update all callsites to pass the recalculated score,
and apply the same change to ProductRankWeekly.java's overwrite(...) to keep
behavior consistent.
| public static ProductRankWeekly of(String yearWeek, Long productId, | ||
| long viewCount, long likeCount, long orderCount) { | ||
| ProductRankWeekly e = new ProductRankWeekly(); | ||
| e.id = new ProductRankWeeklyId(yearWeek, productId); | ||
| e.viewCount = viewCount; | ||
| e.likeCount = likeCount; | ||
| e.orderCount = orderCount; | ||
| e.createdAt = LocalDateTime.now(ZoneId.systemDefault()); | ||
| e.updatedAt = LocalDateTime.now(ZoneId.systemDefault()); | ||
| return e; | ||
| } |
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.
score ํ๋๊ฐ ์ด๊ธฐํ๋์ง ์์ต๋๋ค.
factory ๋ฉ์๋์์ viewCount, likeCount, orderCount๋ ์ค์ ํ์ง๋ง score ํ๋๋ ์ด๊ธฐํ๋์ง ์์ ๊ธฐ๋ณธ๊ฐ 0.0์ผ๋ก ๋จ๊ฒ ๋ฉ๋๋ค. PR ์์ฝ์ ๋ฐ๋ฅด๋ฉด score = 0.1view + 0.2like + 0.7*order๋ก ๊ณ์ฐ๋์ด์ผ ํฉ๋๋ค.
๐ score ๊ณ์ฐ ๋ก์ง ์ถ๊ฐ ์ ์
public static ProductRankWeekly of(String yearWeek, Long productId,
long viewCount, long likeCount, long orderCount) {
ProductRankWeekly e = new ProductRankWeekly();
e.id = new ProductRankWeeklyId(yearWeek, productId);
e.viewCount = viewCount;
e.likeCount = likeCount;
e.orderCount = orderCount;
+ e.score = 0.1 * viewCount + 0.2 * likeCount + 0.7 * orderCount;
e.createdAt = LocalDateTime.now(ZoneId.systemDefault());
e.updatedAt = LocalDateTime.now(ZoneId.systemDefault());
return e;
}๐ค Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java
around lines 39 to 49, the factory method does not set the score field so it
remains default 0.0; compute score using the formula score = 0.1 * viewCount +
0.2 * likeCount + 0.7 * orderCount (ensuring proper numeric types/casting to the
field type) and assign it to e.score before returning the instance.
| public void overwrite(long viewCount, long likeCount, long orderCount) { | ||
| this.viewCount = viewCount; | ||
| this.likeCount = likeCount; | ||
| this.orderCount = orderCount; | ||
| this.updatedAt = LocalDateTime.now(); | ||
| } |
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.
overwrite ๋ฉ์๋์์ score๊ฐ ๊ฐฑ์ ๋์ง ์์ต๋๋ค.
๋ฉํธ๋ฆญ ์นด์ดํธ๊ฐ ์ ๋ฐ์ดํธ๋ ๋ score๋ ํจ๊ป ์ฌ๊ณ์ฐ๋์ด์ผ ํ์ง๋ง ํ์ฌ๋ ์ด์ ๊ฐ์ ์ ์งํ๊ฒ ๋ฉ๋๋ค.
๐ score ์ฌ๊ณ์ฐ ๋ก์ง ์ถ๊ฐ ์ ์
public void overwrite(long viewCount, long likeCount, long orderCount) {
this.viewCount = viewCount;
this.likeCount = likeCount;
this.orderCount = orderCount;
+ this.score = 0.1 * viewCount + 0.2 * likeCount + 0.7 * orderCount;
this.updatedAt = LocalDateTime.now();
}๐ 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.
| public void overwrite(long viewCount, long likeCount, long orderCount) { | |
| this.viewCount = viewCount; | |
| this.likeCount = likeCount; | |
| this.orderCount = orderCount; | |
| this.updatedAt = LocalDateTime.now(); | |
| } | |
| public void overwrite(long viewCount, long likeCount, long orderCount) { | |
| this.viewCount = viewCount; | |
| this.likeCount = likeCount; | |
| this.orderCount = orderCount; | |
| this.score = 0.1 * viewCount + 0.2 * likeCount + 0.7 * orderCount; | |
| this.updatedAt = LocalDateTime.now(); | |
| } |
๐ค Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/mv/ProductRankWeekly.java
around lines 51-56, the overwrite method updates viewCount/likeCount/orderCount
and updatedAt but does not recompute score; modify the method to recalculate and
assign the score after updating the counts (e.g., call the existing score
calculation helper or compute score from the new view/like/order values and set
this.score = ...), then update updatedAtโif no helper exists, implement the same
score formula used elsewhere and assign it here.
| qp.setFromClause(""" | ||
| from ( | ||
| select | ||
| CONCAT( | ||
| SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 1, 4), | ||
| '-W', | ||
| LPAD(SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 5, 2), 2, '0') | ||
| ) as year_week, | ||
| pm.product_id as product_id, | ||
| SUM(CASE WHEN pm.metrics_type = 'VIEW' THEN pm.count ELSE 0 END) as view_count, | ||
| SUM(CASE WHEN pm.metrics_type = 'LIKE' THEN pm.count ELSE 0 END) as like_count, | ||
| SUM(CASE WHEN pm.metrics_type = 'ORDER_SUCCESS' THEN pm.count ELSE 0 END) as order_count | ||
| from product_metrics pm | ||
| group by year_week, pm.product_id | ||
| ) t | ||
| """); | ||
|
|
||
| qp.setWhereClause("where t.year_week = :yearWeek"); |
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.
์ฑ๋ฅ ์ด์: WHERE ์ ์ ์๋ธ์ฟผ๋ฆฌ ๋ด๋ถ๋ก ์ด๋ ํ์
ํ์ฌ ์ฟผ๋ฆฌ๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๊ทธ๋ฃนํํ ํ ์ธ๋ถ์์ year_week๋ก ํํฐ๋งํฉ๋๋ค. ์ด๋ ๋ฐ์ดํฐ๊ฐ ์ฆ๊ฐํ ์๋ก ๋ถํ์ํ ์ง๊ณ ์ฐ์ฐ์ด ๋ฐ์ํฉ๋๋ค. RankMonthlyReaderConfig์ฒ๋ผ ์๋ธ์ฟผ๋ฆฌ ๋ด๋ถ์์ ๋ ์ง ๋ฒ์๋ก ํํฐ๋งํ๋ ๊ฒ์ด ํจ์จ์ ์
๋๋ค.
๐ ์ฑ๋ฅ ๊ฐ์ ์ ์ํ ์ฟผ๋ฆฌ ์์ ์ ์
yearWeek ํ๋ผ๋ฏธํฐ ๋์ ํด๋น ์ฃผ์ ์์์ผ/์ข
๋ฃ์ผ์ ๊ณ์ฐํ์ฌ ์๋ธ์ฟผ๋ฆฌ ๋ด๋ถ์์ ํํฐ๋งํ๊ฑฐ๋, ์๋์ ๊ฐ์ด ์๋ธ์ฟผ๋ฆฌ ๋ด๋ถ์ WHERE ์ ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํ์ธ์:
qp.setFromClause("""
from (
select
CONCAT(
SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 1, 4),
'-W',
LPAD(SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 5, 2), 2, '0')
) as year_week,
pm.product_id as product_id,
SUM(CASE WHEN pm.metrics_type = 'VIEW' THEN pm.count ELSE 0 END) as view_count,
SUM(CASE WHEN pm.metrics_type = 'LIKE' THEN pm.count ELSE 0 END) as like_count,
SUM(CASE WHEN pm.metrics_type = 'ORDER_SUCCESS' THEN pm.count ELSE 0 END) as order_count
from product_metrics pm
+ where CONCAT(
+ SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 1, 4),
+ '-W',
+ LPAD(SUBSTRING(YEARWEEK(STR_TO_DATE(pm.metrics_date, '%Y%m%d'), 3), 5, 2), 2, '0')
+ ) = :yearWeek
group by year_week, pm.product_id
) t
""");
-
- qp.setWhereClause("where t.year_week = :yearWeek");๋๋ ์๊ฐ ๋ฆฌ๋์ฒ๋ผ startDate, endDate ํ๋ผ๋ฏธํฐ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ๋ ๊ฒ์ด ๋ ํจ์จ์ ์
๋๋ค.
๐ค Prompt for AI Agents
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyReaderConfig.java
lines 53-70: the current query groups the entire product_metrics table then
filters by year_week in the outer WHERE, causing large unnecessary aggregation;
move the filtering into the subquery so aggregation only runs on the target
periodโeither add a WHERE inside the subquery that filters metrics_date
(recommended: switch the reader to accept startDate and endDate parameters and
filter metrics_date BETWEEN :startDate AND :endDate inside the subquery) or, if
keeping yearWeek param, convert it to a date range inside Java and apply
STR_TO_DATE/metrics_date comparison inside the subquery; update setFromClause to
include that WHERE and remove or adjust the outer setWhereClause accordingly.
| String sql = """ | ||
| INSERT INTO mv_product_rank_weekly | ||
| (year_week_key, product_id, view_count, like_count, order_count, score, created_at, updated_at) | ||
| VALUES | ||
| (:yearWeek, :productId, :viewCount, :likeCount, :orderCount, :score, NOW(6), NOW(6)) | ||
| ON DUPLICATE KEY UPDATE | ||
| view_count = VALUES(view_count), | ||
| like_count = VALUES(like_count), | ||
| order_count = VALUES(order_count), | ||
| score=VALUES(score), | ||
| updated_at = NOW(6) | ||
| """; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Web query:
MySQL 8.0.20 VALUES function deprecated ON DUPLICATE KEY UPDATE
๐ก Result:
Yes โ starting in MySQL 8.0.20 the VALUES() function used inside INSERT ... ON DUPLICATE KEY UPDATE is deprecated and will be removed in a future release. Use a row (and optional column) alias for the VALUES() row instead.
- Deprecation announced in MySQL 8.0.20 release notes (Apr 27, 2020). [1]
- Reference manual: use VALUES(...) AS alias (or AS alias(col1,col2,...)) and then refer to alias.col in the UPDATE clause. Examples also show using a derived-subquery or SET form to avoid warnings. [2]
Example replacements
- Old (deprecated):
INSERT INTO t1 (a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c = VALUES(a)+VALUES(b); - New (use row alias):
INSERT INTO t1 (a,b,c) VALUES (1,2,3) AS new ON DUPLICATE KEY UPDATE c = new.a + new.b; - Or with column aliases:
INSERT INTO t1 (a,b,c) VALUES (1,2,3) AS new(m,n,p) ON DUPLICATE KEY UPDATE c = m + n. [2]
Sources:
[1] MySQL 8.0.20 release notes (Changes in MySQL 8.0.20).
[2] MySQL Reference Manual โ INSERT ... ON DUPLICATE KEY UPDATE.
MySQL 8.0.20+ ์ด์์์ VALUES() ํจ์๊ฐ deprecated๋์์ผ๋ฏ๋ก ์๋ก์ด ๋ฌธ๋ฒ์ผ๋ก ์์ ํ์ธ์.
๋ผ์ธ 25-28์์ ์ฌ์ฉ ์ค์ธ ON DUPLICATE KEY UPDATE์ VALUES() ํจ์๋ MySQL 8.0.20๋ถํฐ deprecated๋์์ผ๋ฉฐ ํฅํ ๋ฒ์ ์์ ์ ๊ฑฐ๋ ์์ ์
๋๋ค. ํ ๋ณ์นญ(row alias)์ ์ฌ์ฉํ๋ ์๋ก์ด ๋ฌธ๋ฒ์ผ๋ก ์
๋ฐ์ดํธํด์ผ ํฉ๋๋ค.
๐ ์์ ๋ฐฉ๋ฒ
VALUES ์ ์งํ์ AS new_values ๋ณ์นญ์ ์ถ๊ฐํ๊ณ , UPDATE ์ ์์ VALUES(column_name) ๋์ new_values.column_name ํ์์ผ๋ก ์ฐธ์กฐํ์ธ์:
String sql = """
INSERT INTO mv_product_rank_weekly
(year_week_key, product_id, view_count, like_count, order_count, score, created_at, updated_at)
VALUES
(:yearWeek, :productId, :viewCount, :likeCount, :orderCount, :score, NOW(6), NOW(6))
+ AS new_values
ON DUPLICATE KEY UPDATE
- view_count = VALUES(view_count),
- like_count = VALUES(like_count),
- order_count = VALUES(order_count),
- score=VALUES(score),
+ view_count = new_values.view_count,
+ like_count = new_values.like_count,
+ order_count = new_values.order_count,
+ score = new_values.score,
updated_at = NOW(6)
""";๐ 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.
| String sql = """ | |
| INSERT INTO mv_product_rank_weekly | |
| (year_week_key, product_id, view_count, like_count, order_count, score, created_at, updated_at) | |
| VALUES | |
| (:yearWeek, :productId, :viewCount, :likeCount, :orderCount, :score, NOW(6), NOW(6)) | |
| ON DUPLICATE KEY UPDATE | |
| view_count = VALUES(view_count), | |
| like_count = VALUES(like_count), | |
| order_count = VALUES(order_count), | |
| score=VALUES(score), | |
| updated_at = NOW(6) | |
| """; | |
| String sql = """ | |
| INSERT INTO mv_product_rank_weekly | |
| (year_week_key, product_id, view_count, like_count, order_count, score, created_at, updated_at) | |
| VALUES | |
| (:yearWeek, :productId, :viewCount, :likeCount, :orderCount, :score, NOW(6), NOW(6)) | |
| AS new_values | |
| ON DUPLICATE KEY UPDATE | |
| view_count = new_values.view_count, | |
| like_count = new_values.like_count, | |
| order_count = new_values.order_count, | |
| score = new_values.score, | |
| updated_at = NOW(6) | |
| """; |
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/job/rankWeekly/step/RankWeeklyWriterConfig.java
around lines 19 to 30, the SQL uses the deprecated VALUES() function in the ON
DUPLICATE KEY UPDATE clause; change the INSERT so the VALUES(...) row is given
an alias (e.g., AS new_values) immediately after the VALUES (...) list and then
replace each VALUES(column_name) in the UPDATE clause with
new_values.column_name for view_count, like_count, order_count and score,
keeping updated_at = NOW(6) as-is.
| @Slf4j | ||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class ChunkListener { |
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.
Spring Batch 6.x ChunkListener ์ธํฐํ์ด์ค ๊ตฌํ ํ์
Spring Batch 6.x์์๋ org.springframework.batch.core.listener.ChunkListener ์ธํฐํ์ด์ค๋ฅผ ๋ช
์์ ์ผ๋ก ๊ตฌํํด์ผ ํฉ๋๋ค.
๐ ์ ์๋ ์์
+import org.springframework.batch.core.listener.ChunkListener;
+
@Slf4j
@RequiredArgsConstructor
@Component
-public class ChunkListener {
+public class ChunkListener implements org.springframework.batch.core.listener.ChunkListener {Based on learnings, Spring Batch 6.x requires implementing the ChunkListener interface.
๐ 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 | |
| @RequiredArgsConstructor | |
| @Component | |
| public class ChunkListener { | |
| @Slf4j | |
| @RequiredArgsConstructor | |
| @Component | |
| public class ChunkListener implements org.springframework.batch.core.listener.ChunkListener { |
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java
around lines 9 to 12, the class is annotated but does not implement the Spring
Batch 6.x ChunkListener interface; update the class declaration to implement
org.springframework.batch.core.listener.ChunkListener and add the required
@Override methods (beforeChunk, afterChunk, and afterChunkError) with
appropriate signatures and simple no-op or logging bodies so the class satisfies
the interface; ensure proper imports and keep existing annotations (@Slf4j,
@RequiredArgsConstructor, @Component).
| @AfterChunk | ||
| void afterChunk(ChunkContext chunkContext) { | ||
| log.info( | ||
| "์ฒญํฌ ์ข ๋ฃ: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " + | ||
| "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}" | ||
| ); | ||
| } |
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.
๋ฉ์๋ ์๊ทธ๋์ฒ์ ๋ฌธ์์ด ๋ณด๊ฐ ๊ตฌ๋ฌธ ์์ ํ์
๋ค์ ๋ ๊ฐ์ง critical ์ด์๊ฐ ์์ต๋๋ค:
- Spring Batch 6.x API ๋ณ๊ฒฝ:
ChunkContext๋์Chunk<?>ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค. - Java ๋ฌธ์์ด ๋ณด๊ฐ ๊ตฌ๋ฌธ ์ค๋ฅ:
${...}๊ตฌ๋ฌธ์ Kotlin ๋ฌธ๋ฒ์ด๋ฉฐ Java์์๋ ์ฌ์ฉํ ์ ์์ต๋๋ค. Java์์๋ ๋ฌธ์์ด ์ฐ๊ฒฐ ๋๋String.format์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
๐ ์ ์๋ ์์
+import org.springframework.batch.core.scope.context.ChunkContext;
+import org.springframework.batch.item.Chunk;
+
@Slf4j
@RequiredArgsConstructor
@Component
-public class ChunkListener {
+public class ChunkListener implements org.springframework.batch.core.listener.ChunkListener {
@AfterChunk
- void afterChunk(ChunkContext chunkContext) {
+ public void afterChunk(ChunkContext context, Chunk<?> chunk) {
log.info(
- "์ฒญํฌ ์ข
๋ฃ: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " +
- "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}"
+ "์ฒญํฌ ์ข
๋ฃ: readCount: {}, writeCount: {}",
+ context.getStepContext().getStepExecution().getReadCount(),
+ context.getStepContext().getStepExecution().getWriteCount()
);
}
}Based on learnings, Spring Batch 6.x changed ChunkListener API to use Chunk parameters with signature: afterChunk(ChunkContext context, Chunk<T> chunk).
Committable suggestion skipped: line range outside the PR's diff.
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java
around lines 14-20, the method uses the old Spring Batch signature and Kotlin
string interpolation; change the method signature to accept the current
parameters (e.g. afterChunk(ChunkContext chunkContext, Chunk<?> chunk) or the
exact Spring Batch 6.x signature used in the project) and replace the
Kotlin-style "${...}" with Java getters and string construction โ use
chunkContext.getStepContext().getStepExecution().getReadCount() and
getWriteCount() and build the log message with String.format(...) or string
concatenation so the log prints the readCount and writeCount correctly.
| @BeforeJob | ||
| void beforeJob(JobExecution jobExecution) { | ||
| log.info("Job '${jobExecution.jobInstance.jobName}' ์์"); | ||
| jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis()); | ||
| } |
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.
Java ๋ฌธ์์ด ๋ณด๊ฐ ๊ตฌ๋ฌธ ์ค๋ฅ๋ฅผ ์์ ํ์ธ์.
Line 21์์ "${jobExecution.jobInstance.jobName}"๋ Java์์ ์ ํจํ์ง ์์ ๋ฌธ์์ด ๋ณด๊ฐ ๊ตฌ๋ฌธ์
๋๋ค. ์ด ์ฝ๋๋ ์ค์ Job ์ด๋ฆ ๋์ ๋ฆฌํฐ๋ด ๋ฌธ์์ด์ ๋ก๊ทธ์ ์ถ๋ ฅํฉ๋๋ค.
๐ ์์ ์ ์
@BeforeJob
void beforeJob(JobExecution jobExecution) {
- log.info("Job '${jobExecution.jobInstance.jobName}' ์์");
+ log.info("Job '{}' ์์", jobExecution.getJobInstance().getJobName());
jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis());
}๐ 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.
| @BeforeJob | |
| void beforeJob(JobExecution jobExecution) { | |
| log.info("Job '${jobExecution.jobInstance.jobName}' ์์"); | |
| jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis()); | |
| } | |
| @BeforeJob | |
| void beforeJob(JobExecution jobExecution) { | |
| log.info("Job '{}' ์์", jobExecution.getJobInstance().getJobName()); | |
| jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis()); | |
| } |
๐ค Prompt for AI Agents
In apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
around lines 19 to 23, the log call uses a Java-invalid string interpolation
("${jobExecution.jobInstance.jobName}") which prints the literal rather than the
job name; replace it with a proper Java expressionโeither use the SLF4J
parameterized logging placeholder and pass
jobExecution.getJobInstance().getJobName() as an argument, or build the string
with + (or String.format) using jobExecution.getJobInstance().getJobName();
ensure you call the correct getter methods to obtain the job name.
๐ Summary
๐ฌ Review Points
product_like_metrics ์ ์ผ๋ณ๋ก ๋ค์ํ ์ํ ๊ด๋ จ ๋ฉํธ๋ฆญ ์ ๋ณด๋ฅผ ์ง๊ณํ ์ ์๋๋ก
product_metrics๋ก ๋ฆฌํฉํ ๋ง ํ์์ต๋๋ค.MV ํ ์ด๋ธ์ ๋ค์๊ณผ ๊ฐ์ด ์ค๊ณํ์์ต๋๋ค.
erDiagram MV_PRODUCT_RANK_WEEKLY { varchar year_week PK bigint product_id PK bigint view_count bigint like_count bigint order_count double score datetime created_at datetime updated_at } MV_PRODUCT_RANK_MONTHLY { varchar year_month PK bigint product_id PK bigint view_count bigint like_count bigint order_count double score datetime created_at datetime updated_at }โ Checklist
๐งฑ Spring Batch
๐งฉ Ranking API
๐ References
์คํ๋ง๋ฐฐ์น https://youtu.be/gTQSKYL_AGw?si=MD0uBmLYBsnDVoRH