-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-10] Collect, Stack, Zip #237
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: yeonjiyeon
Are you sure you want to change the base?
[volume-10] Collect, Stack, Zip #237
Conversation
ํ์ ๊ฐ์ ์ User ์ ์ฅ์ด ์ํ๋๋ค. ( spy ๊ฒ์ฆ ) ์ด๋ฏธ ๊ฐ์ ๋ ID ๋ก ํ์๊ฐ์ ์๋ ์, ์คํจํ๋ค.
ํ์ ๊ฐ์ ์ด ์ฑ๊ณตํ ๊ฒฝ์ฐ, ์์ฑ๋ ์ ์ ์ ๋ณด๋ฅผ ์๋ต์ผ๋ก ๋ฐํํ๋ค.
ํ์ ๊ฐ์ ์์ ์ฑ๋ณ์ด ์์ ๊ฒฝ์ฐ, `400 Bad Request` ์๋ต์ ๋ฐํํ๋ค.
ํด๋น ID ์ ํ์์ด ์กด์ฌํ ๊ฒฝ์ฐ, ํ์ ์ ๋ณด๊ฐ ๋ฐํ๋๋ค. ํด๋น ID ์ ํ์์ด ์กด์ฌํ์ง ์์ ๊ฒฝ์ฐ, null ์ด ๋ฐํ๋๋ค. ๋ด ์ ๋ณด ์กฐํ์ ์ฑ๊ณตํ ๊ฒฝ์ฐ, ํด๋นํ๋ ์ ์ ์ ๋ณด๋ฅผ ์๋ต์ผ๋ก ๋ฐํํ๋ค. ์กด์ฌํ์ง ์๋ ID ๋ก ์กฐํํ ๊ฒฝ์ฐ, `404 Not Found` ์๋ต์ ๋ฐํํ๋ค.
ํด๋น ID ์ ํ์์ด ์กด์ฌํ ๊ฒฝ์ฐ, ๋ณด์ ํฌ์ธํธ๊ฐ ๋ฐํ๋๋ค. ํด๋น ID ์ ํ์์ด ์กด์ฌํ์ง ์์ ๊ฒฝ์ฐ, null ์ด ๋ฐํ๋๋ค. ํฌ์ธํธ ์กฐํ์ ์ฑ๊ณตํ ๊ฒฝ์ฐ, ๋ณด์ ํฌ์ธํธ๋ฅผ ์๋ต์ผ๋ก ๋ฐํํ๋ค. `X-USER-ID` ํค๋๊ฐ ์์ ๊ฒฝ์ฐ, `400 Bad Request` ์๋ต์ ๋ฐํํ๋ค.
0 ์ดํ์ ์ ์๋ก ํฌ์ธํธ๋ฅผ ์ถฉ์ ์ ์คํจํ๋ค. ์กด์ฌํ์ง ์๋ ์ ์ ID ๋ก ์ถฉ์ ์ ์๋ํ ๊ฒฝ์ฐ, ์คํจํ๋ค. ์กด์ฌํ๋ ์ ์ ๊ฐ 1000์์ ์ถฉ์ ํ ๊ฒฝ์ฐ, ์ถฉ์ ๋ ๋ณด์ ์ด๋์ ์๋ต์ผ๋ก ๋ฐํํ๋ค. ์กด์ฌํ์ง ์๋ ์ ์ ๋ก ์์ฒญํ ๊ฒฝ์ฐ, `404 Not Found` ์๋ต์ ๋ฐํํ๋ค.
- PaymentRepository ๋ฐ ๊ด๋ จ ๋ฉ์๋์์ LocalDateTime ๋์ ZonedDateTime ์ฌ์ฉํ๋๋ก ์์ - ์ด๋ฒคํธ ๋ฆฌ์ค๋์ ํธ๋์ญ์ ์ ํ ๋ฐฉ์์ Propagation.REQUIRES_NEW๋ก ๊ฐ์
- ์ฌ์ฉ๋์ง ์๋ ๋ฐ์ดํฐ์์ค ์ค์ ํ์ผ(jpa.yml, redis.yml) ์ญ์ - KafkaConfig ํจํค์ง๋ช ์คํ ์์ (confg โ config) - build.gradle.kts์ kafka ๋ชจ๋ ์์กด์ฑ ์ถ๊ฐ ๋ฐ application.yml ์ค์ ๋ฐ์
- `OutboxEvent` ์ํฐํฐ ๋ฐ JPA ๋งคํ ์ถ๊ฐ๋ก Outbox ๊ธฐ๋ฐ ์ด๋ฒคํธ ๊ด๋ฆฌ ๊ตฌํ - `OutboxService`๋ฅผ ํตํด ์ด๋ฒคํธ ์ ์ฅ ๋ฐ ์ง๋ ฌํ ๋ก์ง ์ฒ๋ฆฌ - `OutboxRepository`๋ก ๋น๋๊ธฐ ์ด๋ฒคํธ ์กฐํ ๋ฐ ๊ด๋ฆฌ ๊ฐ๋ฅ
- `OutboxEvent` ๊ตฌ์กฐ ํ์ฅ: ๊ณ ์ `eventId` ํ๋ ์ถ๊ฐ ๋ฐ ์ ์ฅ ๋ก์ง ๊ฐ์ . - `LikeCreatedEvent`์ ์์ฑ ๋ฉ์๋(`of`) ๋์ ์ผ๋ก eventId ์๋ ์์ฑ ๋ฐ ํ์์คํฌํ ์ถ๊ฐ. - Kafka ์ฐ๋์ ์ํ `LikeEventOutboxHandler` ์ปดํฌ๋ํธ ์ถ๊ฐ. - `OutboxService`์ ์ด๋ฒคํธ ๋ฐํ ์๋ฃ ์ํ ๊ด๋ฆฌ ๋ก์ง(`markPublished`) ์ถ๊ฐ. - `LikeFacade` ๋ฐ `LikeCountAggregateListener`์์ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก์ง ํ์ฅ ๋ฐ ๊ด๋ จ ํด๋์ค ํจํค์ง ๊ตฌ์กฐ ์ ๋ฆฌ.
- `LikeEventConsumer` ์ถ๊ฐ๋ก Kafka ์ด๋ฒคํธ ์์ ๋ฐ ์ฒ๋ฆฌ ๋ก์ง ๊ตฌํ. - `ProductMetricsService`๋ฅผ ํตํด ์ข์์ ์ ์ ๋ฐ์ดํธ ์ฒ๋ฆฌ. - `ProductMetrics` ๋ฐ `EventHandled` ์ํฐํฐ ์ถ๊ฐ๋ก ์ด๋ฒคํธ ์ค๋ณต ์ฒ๋ฆฌ ๋ฐฉ์ง ๋ฐ ๋ฉํธ๋ฆญ ์ ์ฅ. - ๊ด๋ จ JPA ๋ฆฌํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค (`ProductMetricsRepository`, `EventHandledRepository`) ์ถ๊ฐ.
- ์ด์ ๋ฐ๋ฅธ ์ปจ์๋จธ ๋ฐ ํผ๋ธ๋ฆฌ์ ๊ตฌํ์ฒด ์์ - OrderCreatedEvent, LikeEventOutboxHandler, LikeCountAggregateListener ํจํค์ง ๊ตฌ์กฐ ์ ๋ฆฌ
- ์ํ ์กฐํ ์ก์ ์ ๊ธฐ๋กํ๊ธฐ ์ํ ProductViewEvent ๋์ - ProductViewEvent๋ฅผ ์ฒ๋ฆฌํ๊ณ Kafka์ ์ฐ๋ํ๋ ProductEventOutboxHandler ๊ตฌํ - ์ํ ์ ๋ณด ์กฐํ ์ ProductFacade์์ ProductViewEvent ๋ฐํํ๋๋ก ์์ - ์ด๋ฒคํธ ์ ์ฅ, ์ค๋ณต ์ ๊ฑฐ, Kafka ๋ฐํ ๋ก์ง ํฌํจ
- `OrderEventOutboxHandler` ์ถ๊ฐ๋ก ์ฃผ๋ฌธ ์์ฑ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ฐ Kafka ๋ฐํ ๋ก์ง ๊ตฌํ. - `OrderSalesAggregateListener`๋ฅผ ํตํด ํ๋งค ์ง๊ณ Kafka ์ด๋ฒคํธ ๋ฐํ ๋ฐ ๋น๋๊ธฐ ์ฒ๋ฆฌ ์ง์. - `OrderCreatedEvent` ํ์ฅ: ๊ณ ์ `eventId` ์ถ๊ฐ ๋ฐ ์ฃผ๋ฌธ ํ๋ชฉ ์ ๋ฌ ์ ๋ณด ๊ตฌ์กฐ ๋ณ๊ฒฝ. - ์ ๊ท `SalesCountEvent` ์ถ๊ฐ๋ก Kafka ๊ธฐ๋ฐ ํ๋งค ๋ฉํธ๋ฆญ ๊ด๋ฆฌ ๊ฐ๋ฅ. - `OrderFacade`, `PointPaymentEventListener`, `PgPaymentEventListener` ๋ฑ ๊ด๋ จ ํด๋์ค ์์ ๋ฐ ํจํค์ง ์ ๋ฆฌ.
- `LikeEventConsumer`๋ฅผ `MetricsEventConsumer`๋ก ๋ณ๊ฒฝํ๊ณ ์ํ ์งํ(์ข์์, ์กฐํ์, ํ๋งค๋) ์ฒ๋ฆฌ ์ง์. - `ProductMetricsService` ํ์ฅ: ์กฐํ์(`ProductViewEvent`), ํ๋งค๋(`SalesCountEvent`) ๋ฉํธ๋ฆญ ์ฒ๋ฆฌ ๋ก์ง ์ถ๊ฐ. - `ProductMetrics` ์ํฐํฐ์ ์กฐํ์์ ํ๋งค๋ ์์ฑ ๋ฐ ๊ด๋ จ ์ ๋ฐ์ดํธ ๋ฉ์๋ ์ถ๊ฐ.
- `OutboxEvent`, `ProductMetrics`, `EventHandled` ์ํฐํฐ์ @table ์ ๋ํ ์ด์ ์ถ๊ฐ. - `IdempotencyIntegrationTest` ๊ตฌํ์ผ๋ก ์ค๋ณต ์ด๋ฒคํธ ์ฒ๋ฆฌ ๊ฒ์ฆ.
โฆ ๋ก์ง ๊ฐ์ - SalesCountEvent๋ฅผ ์ ๊ฑฐํ๊ณ ๊ด๋ จ ๋ก์ง์ ProductStockEvent๋ก ์ ๋ฉด ๋์ฒด - MetricsEventConsumer์์ ProductStockEvent๋ฅผ ์ฒ๋ฆฌํ๋๋ก ์์ - ProductMetricsService๋ฅผ ํ์ฅํ์ฌ ์ฌ๊ณ ๊ธฐ๋ฐ ์งํ ๊ด๋ฆฌ ๋ฐ ์ฌ๊ณ ์์ง ์ Redis ์บ์ ์ญ์ ๋ก์ง ์ถ๊ฐ - ProductRepository์ findStockById ๋ฉ์๋, ProductService์ getStock ๋ฉ์๋ ์ถ๊ฐ - Redis ์บ์์์ ํ์ด์ง ์๋ต์ ๊ฐ์ธ๊ธฐ ์ํ PageWrapper ๋์ - ํจํด ๊ธฐ๋ฐ ์บ์ ์ญ์ ๋ฅผ ์ง์ํ๋๋ก RedisCacheHandler ์ด๋ ๋ฐ ๊ธฐ๋ฅ ํ์ฅ
- `OutboxEvent`์ `OutboxStatus` ๋ฐ `retryCount` ํ๋ ์ถ๊ฐ๋ก ์ํ ๊ด๋ฆฌ ์ง์. - ์คํจ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํ `markFailed` ๋ฉ์๋ ๊ตฌํ. - `OutboxRelay` ์ปดํฌ๋ํธ ์ถ๊ฐ๋ก ์คํจ ์ด๋ฒคํธ ์ฌ๋ฐํ ์ค์ผ์ค๋ง ๋ฐ ์ฒ๋ฆฌ ๋ก์ง ๊ตฌํ. - ๊ด๋ จ ์๋น์ค ๋ฐ ํธ๋ค๋ฌ์ ์คํจ ์ํ ์ ๋ฐ์ดํธ ๋ก์ง ํตํฉ (`markFailed` ํธ์ถ ์ถ๊ฐ).
- Kafka ์ค์ ์ enable.idempotence=true ๋ฐ acks=all ํ๋ก๋์ ์ต์ ์ถ๊ฐ - OutboxService์์ ์ฌ์ฉํ์ง ์๋ import ์ ๊ฑฐ
- ๋ฏธ์ฌ์ฉ ํด๋์ค(DemoKafkaConsumer) ์ญ์ ๋ก ์ฝ๋ ์ ๋ฆฌ. - ๊ด๋ จ Kafka ๋ฆฌ์ค๋ ๋ฐ ์์กด์ฑ ์ ๊ฑฐ.
- ์ด๋ฒคํธ ๋ฆฌ์ค๋์ ์ธ๋ถ ์๋น์ค ์์กด์ฑ ์ ๊ฑฐ (Decoupling) - ์ด๋ฒคํธ ๊ฐ์ฒด๊ฐ ๋ฐ์ ์์ ์ ์ํ(์ฌ๊ณ )๋ฅผ ์จ์ ํ ํํํ๋๋ก ์์ - ๋น๋๊ธฐ ์ฒ๋ฆฌ ์ ๋ฐ์ํ ์ ์๋ ๋ฐ์ดํฐ ๋ถ์ผ์น ๊ฐ๋ฅ์ฑ ๋ฐฉ์ง
- ProductStockEvent์ ๊ฐ๊ฒฉ(price) ํ๋ ์ถ๊ฐ๋ก ์ด๋ฒคํธ ์ ๋ณด ํ์ฅ - OrderCreatedEvent์์ ์ํ๊ณผ ๊ฐ๊ฒฉ ๋ฐ์ดํฐ๋ฅผ ํจ๊ป ๋งคํํ๋๋ก ์์ - OrderSalesAggregateListener์์ ์ด๋ฒคํธ ๋ฐํ ์ ๊ฐ๊ฒฉ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ๋๋ก ๊ฐ์
- ์ด๋ฒคํธ ํ์์คํฌํ ํ๋๋ฅผ `LocalDateTime`์ผ๋ก ๋ณ๊ฒฝํ๊ณ ์ด๋ฒคํธ ์์ฑ ๋ก์ง ์ ๋ฐ์ดํธ - `RankingService` ์ถ๊ฐ๋ก ์ํ ์ ์ ๊ด๋ฆฌ ๋ก์ง ๊ตฌํ (์กฐํ์, ์ข์์, ํ๋งค๋ ๊ฐ์ค์น ์ ์ฉ) - MetricsEventConsumer ํ์ฅ: ๋ญํน ์ ์ ๊ณ์ฐ ๋ฐ Redis ์ ๋ฐ์ดํธ ์ง์ - Kafka ๋ฆฌ์ค๋ ๋ฐ ๋ฐฐ์น ์ฒ๋ฆฌ ๋ก์ง ๊ฐ์ , ์ด๋ฒคํธ๋ณ ์ ์ ์ ๋ฐ์ดํธ ์ฒ๋ฆฌ ์ถ๊ฐ - OutboxHandler ์์ : ์ด๋ฒคํธ ํ์ ๋ณ๊ฒฝ ๋ฐ ์นดํ๋ก๊ทธ ์ด๋ฒคํธ ๋ฐํ ์ ๋ฆฌ
- `RankingFacade` ๋ฐ `RankingService` ์ถ๊ฐ๋ก ์ํ ๋ญํน ์กฐํ ๋ก์ง ๊ตฌํ - Redis ๊ธฐ๋ฐ ๋ญํน ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋ฐ ์ํ ์์ธ ์ ๋ณด ๋งคํ ์ง์ - `RankingV1Controller`์ `RankingV1ApiSpec` ์ถ๊ฐ๋ก REST API ์ ๊ณต - `RankingV1Dto`์ `RankingInfo` ๊ตฌํ์ผ๋ก ์๋ต ๋ฐ์ดํฐ ๊ตฌ์กฐํ
- `RankingService` ๋ฉ์๋ `getProductRank` ๊ตฌํ์ผ๋ก Redis ๊ธฐ๋ฐ ์ํ ๋ญํน ์กฐํ ์ง์ - ์ํ ์์ธ ์กฐํ ์ ํ์ฌ ๋ญํน ์ ๋ณด๋ฅผ ํฌํจํ๋๋ก `ProductInfo` ๋ฐ `ProductFacade` ์์ - ๊ธฐ์กด `RankingService` ํจํค์ง ์์น๋ฅผ `infrastructure`๋ก ๋ณ๊ฒฝํ์ฌ ๊ด๋ฆฌ ๊ฐ์
- `carryOverRanking` ๋ฉ์๋๋ฅผ `RankingService`์ ์ถ๊ฐํ์ฌ ๋ญํน ๋ฐ์ดํฐ ์ด์ ์ง์ - ์ด์ ์์ ์๋ํ๋ฅผ ์ํ `RankingScheduler` ๊ตฌํ ๋ฐ Cron ์ค์ผ์ค ์ค์ - Redis ์ฐ์ฐ์ ํ์ฉํ ์ด์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋ฐ ๋ง๋ฃ ์๊ฐ ์ค์ ๋ก์ง ์ถ๊ฐ
โฆ์ค์ผ์ค๋ฌ ์ฌ์ฉ์ ์ํ RankingService ๊ฐ์ - ๋์ ์ด๊ณ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ญํน ํค ์์ฑ์ ์ํด RankingKeyGenerator ์ธํฐํ์ด์ค์ RedisRankingKeyGenerator ๊ตฌํ์ฒด ๋์ - ์ ์ง๋ณด์์ฑ ํฅ์์ ์ํด RankingService์์ RankingKeyGenerator๋ฅผ ์ฌ์ฉํ๋๋ก ๋ฆฌํฉํ ๋ง - commerce-api์ ์๋ ์ค๋ณต carryOverRanking ๋ก์ง ์ ๊ฑฐ ํ commerce-streamer ๋ชจ๋๋ก ํตํฉ - ๋ฆฌํฉํ ๋ง๋ RankingService๋ฅผ ์ฌ์ฉํ๋๋ก RankingScheduler ์์
- `ProductStockEvent`์ `OrderCreatedEvent`์ ์ํ๋ช (`productName`) ํ๋ ์ถ๊ฐ๋ก ์ด๋ฒคํธ ์ ๋ณด๋ฅผ ํ์ฅ - ๋ญํน ์ํฐํฐ ๋งคํ์ ์ํ `RankingInfo` ์์ฑ ๋ฉ์๋ ํ์ฅ (์ฃผ๊ฐ ๋ฐ ์๊ฐ ๋ญํน ์ง์)
- `productName`, `price`, `isSoldOut` ํ๋ ์ถ๊ฐ๋ก ์ํ ๋ฐ์ดํฐ ํ์ฅ - `updateProductSnapshot` ๋ฉ์๋ ๊ตฌํ์ผ๋ก ์ํ ์ํ ๋๊ธฐํ ์ง์ - ํจํค์ง ๊ตฌ์กฐ ๊ฐ์ : `ProductMetrics`๋ฅผ `metrics`์์ ์ต์์ `domain`์ผ๋ก ์ด๋ - `ProductMetricsService`์์ ์ํ ์ ๋ณด ์ ๋ฐ์ดํธ ๋ก์ง ํตํฉ ๋ฐ Redis ์บ์ ์ฒ๋ฆฌ ๊ฐํ
- RankingService ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ RankingServiceImpl ์ถ๊ฐ - ์ฃผ๊ฐยท์๊ฐ ๋ญํน ์กฐํ๋ฅผ ์ํ getWeeklyRankings, getMonthlyRankings ๋ฉ์๋ ๊ตฌํ - ์ ๋ ฅ๊ฐ์ ๋ฐ๋ผ ๋ญํน ํ์ ์ ๋์ ์ผ๋ก ๋ถ๊ธฐํ๋๋ก RankingFacade ๊ฐ์ - DAILY, WEEKLY, MONTHLY ๋ญํน ํ์ ์ ์ง์ํ๋๋ก RankingV1ApiSpec ๋ฐ RankingV1Controller ํ์ฅ
- ์ฃผ๊ฐ ๋ญํน ์ฒ๋ฆฌ๋ฅผ ์ํ ๋๋ฉ์ธ ์ํฐํฐ(WeeklyRankingMV, WeeklyRankingWork) ์ถ๊ฐ - WeeklyRankingJobConfig ๋ฐ RankingChunkConfig๋ก ๋ฐฐ์น ์ก ๊ตฌ์ฑ - WeeklyRankingProcessor ๋ฐ Tasklet์ ํตํด ์ ์ ๊ณ์ฐ, ๋ฐ์ดํฐ ์ค๋น ๋ฐ ์ค์ ์ฒ๋ฆฌ ๊ตฌํ - ์ค์ผ์ค๋ง์ ํตํด ๋ฐฐ์น ์ก ์๋ ์คํ (๋งค์ฃผ ์์์ผ 2์ ์ค์ ) - MonthlyRankingJob ์ค์ผ๋ ํค ์ถ๊ฐ, ํฅํ ํ์ฅ ๊ณ ๋ ค
- ์๊ฐ ๋ญํน ์ฒ๋ฆฌ๋ฅผ ์ํ ๋๋ฉ์ธ ์ํฐํฐ(MonthlyRankingMV) ๋ฐ ๋ ํฌ์งํ ๋ฆฌ ์ถ๊ฐ - MonthlyRankingJobConfig์ Tasklet์ผ๋ก ๋ฐฐ์น ์ก ๊ตฌ์ฑ - ์๊ฐ ์ ์ ๊ณ์ฐ ๋ฐ ๋ญํน ๋ฐ์ดํฐ ์ค๋น๋ฅผ ์ํ Processing ๋ก์ง ๊ตฌํ - Redis๋ฅผ ํ์ฉํ ์ค๋ ์ท ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋ฐ ๋ญํน ๋ฐ์ดํฐ ์ค์ ๋ก์ง ์ถ๊ฐ
WalkthroughGitHub Actions ์ํฌํ๋ก์ฐ๋ฅผ ์ถ๊ฐํ๊ณ ์์ ์์คํ ์ ์ฌ๊ตฌ์กฐํํ๋ฉฐ ์๋ก์ด commerce-batch ๋ชจ๋์ ๋์ ํฉ๋๋ค. ์ฃผ๊ฐ/์๊ฐ ์์ ๋ฐฐ์น ์์ , ์ฃผ๊ธฐ์ ์์ ์ค์ผ์ค๋ฌ, ๊ด๋ จ JPA ์ํฐํฐ ๋ฐ ์ ์ฅ์๋ฅผ ํฌํจํฉ๋๋ค. ๋ํ OrderCreatedEvent์ ProductStockEvent์ productName ํ๋๋ฅผ ์ถ๊ฐํ๊ณ API ์๋ต์์ stock์ isSoldOut ๋ถ๋ฆฐ ๊ฐ์ผ๋ก ๋ณ๊ฒฝํฉ๋๋ค. Changes
Sequence Diagram(s)sequenceDiagram
participant Scheduler as RankingScheduler
participant JobLauncher as JobLauncher
participant PrepareStep as Prepare Step<br/>(RankingPrepareTasklet)
participant CalcStep as Calculation Step<br/>(Chunk Processing)
participant SwapStep as Table Swap Step<br/>(RankingTableSwapTasklet)
participant Repo as Repository<br/>(WeeklyRankingWork/MV)
participant Redis as Redis
participant DB as Database<br/>(ProductMetrics)
rect rgba(100, 150, 200, 0.2)
Note over Scheduler,DB: Weekly Ranking Job Flow
Scheduler->>JobLauncher: runWeeklyRankingJob<br/>(with requestDate param)
JobLauncher->>PrepareStep: Execute Prepare Step
PrepareStep->>Repo: deleteAllInBatch()<br/>(WeeklyRankingWork)
Repo-->>PrepareStep: Cleared
PrepareStep->>CalcStep: Step Completed
CalcStep->>DB: Read ProductMetrics<br/>(past 7 days)
DB-->>CalcStep: Metrics List
CalcStep->>CalcStep: Process: Calculate<br/>weighted score
CalcStep->>Repo: Write WeeklyRankingWork<br/>(chunks of 100)
Repo-->>CalcStep: Saved
CalcStep->>SwapStep: Step Completed
SwapStep->>Repo: findAll()<br/>(WeeklyRankingWork)
Repo-->>SwapStep: Work Records
SwapStep->>Repo: saveAll()<br/>(WeeklyRankingMV)
Repo-->>SwapStep: MV Created
SwapStep-->>JobLauncher: Job Complete
end
rect rgba(150, 100, 200, 0.2)
Note over Scheduler,DB: Daily Ranking Carry-Over
Scheduler->>RankingScheduler: scheduleRankingCarryOver<br/>(23:50 daily)
RankingScheduler->>Redis: ZUNIONSTORE<br/>(today โ tomorrow<br/>with weight 0.1)
Redis-->>RankingScheduler: Merged Ranking
RankingScheduler->>Redis: EXPIRE<br/>(tomorrow key)
Redis-->>RankingScheduler: TTL Set
end
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~75 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: 16
๐งน Nitpick comments (20)
apps/commerce-api/build.gradle.kts (1)
54-54: ์ ์์กด์ฑ์ ์ ํจํ์ง๋ง ๋ฐฐ์น๋ฅผ ์กฐ์ ํ์ธ์.
spring-cloud-starter-circuitbreaker-resilience4j์์กด์ฑ ์ถ๊ฐ๋ ์ ์ ํฉ๋๋ค. Resilience4j์์ Spring Cloud CircuitBreaker ํตํฉ์ ์ง์ํฉ๋๋ค. ์ค๋ณต ์์กด์ฑ์ ์ ๊ฑฐํ ํ, ์ด ์ ์ธ์ 26๋ฒ ๋ผ์ธ ๊ทผ์ฒ์ ๋ค๋ฅธ Spring Cloud ์์กด์ฑ๋ค๊ณผ ํจ๊ป ๋ฐฐ์นํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ๊ถ์ฅ ๋ฐฐ์น
์ค๋ณต ์ ๊ฑฐ ํ ๋ค์๊ณผ ๊ฐ์ด ๋ฐฐ์น:
//Spring Cloud OpenFeign implementation("org.springframework.cloud:spring-cloud-starter-openfeign") + +//Spring Cloud CircuitBreaker +implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingKeyGenerator.java (1)
3-5: ์ฌ์ฉ๋์ง ์๋ import ์ ๊ฑฐ ํ์
org.springframework.stereotype.Componentimport๊ฐ ์ ์ธ๋์ด ์์ง๋ง ์ธํฐํ์ด์ค์์ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ๊ตฌํ์ฒด(RedisRankingKeyGenerator)์@Component๊ฐ ์ ์ฉ๋์ด ์์ผ๋ฏ๋ก ์ด import๋ ์ ๊ฑฐํด๋ ๋ฉ๋๋ค.๐ ์์ ์ ์
package com.loopers.domain.rank; -import org.springframework.stereotype.Component; - public interface RankingKeyGenerator { String generateDailyKey(String date); }apps/commerce-streamer/src/main/java/com/loopers/application/rank/RankingScheduler.java (1)
18-34: ๊ฐ์ค์น ๋งค์ง ๋๋ฒ๋ฅผ ์์๋ก ์ถ์ถํ๋ ๊ฒ์ ๊ณ ๋ คํด ์ฃผ์ธ์
0.1๊ฐ์ค์น ๊ฐ์ด ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. ์ด ๊ฐ์ ์์๋ก ์ถ์ถํ๋ฉด ์๋ฏธ๊ฐ ๋ช ํํด์ง๊ณ ์ถํ ์์ ์ด ์ฉ์ดํฉ๋๋ค. ๋ํ,DateTimeFormatter๋ ์ค๋ ๋ ์ธ์ดํํ๋ฏ๋ก ์ ์ ์์๋ก ์ ์ธํ์ฌ ๋งค ํธ์ถ๋ง๋ค ์์ฑํ์ง ์๋๋ก ํ ์ ์์ต๋๋ค.๐ ์์ ์ ์
@Slf4j @Component @RequiredArgsConstructor public class RankingScheduler { private final RankingService rankingService; + private static final double CARRY_OVER_WEIGHT = 0.1; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); @Scheduled(cron = "0 50 23 * * *") public void scheduleRankingCarryOver() { LocalDateTime now = LocalDateTime.now(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); - String today = now.format(formatter); - String tomorrow = now.plusDays(1).format(formatter); + String today = now.format(DATE_FORMATTER); + String tomorrow = now.plusDays(1).format(DATE_FORMATTER); log.info("Starting Ranking Carry-Over: {} -> {}", today, tomorrow); try { - rankingService.carryOverRanking(today, tomorrow, 0.1); + rankingService.carryOverRanking(today, tomorrow, CARRY_OVER_WEIGHT); log.info("Ranking Carry-Over completed successfully."); } catch (Exception e) { log.error("Ranking Carry-Over failed", e); } } }.github/workflows/main.yml (1)
10-10: ์ก์ ๋ฒ์ ์ ๊ณ ์ ํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
@main๋ธ๋์น ๋์ ํน์ ๋ฆด๋ฆฌ์ค ํ๊ทธ๋ ์ปค๋ฐ SHA๋ฅผ ์ฌ์ฉํ๋ฉด ์ํฌํ๋ก์ฐ์ ์์ ์ฑ๊ณผ ์ฌํ์ฑ์ด ํฅ์๋ฉ๋๋ค.๐ ๊ถ์ฅ ์์
- - name: PR Agent action step - uses: Codium-ai/pr-agent@main + - name: PR Agent action step + uses: Codium-ai/[email protected] # ์ต์ ์์ ๋ฒ์ ์ผ๋ก ๊ต์ฒดapps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (2)
27-29: ๋ก๊น ํ๋ ์์ํฌ ์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋คSystem.out.println ๋์ Slf4j ๋ก๊ฑฐ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ํด๋์ค์ ์ด๋ฏธ Lombok์ @requiredargsconstructor๊ฐ ์์ผ๋ฏ๋ก @slf4j๋ฅผ ์ถ๊ฐํ๊ณ log.info()๋ฅผ ์ฌ์ฉํ์ธ์.
๐ ์ ์ํ๋ ์์ ์
ํด๋์ค ๋ ๋ฒจ์ @slf4j ์ถ๊ฐ:
+import lombok.extern.slf4j.Slf4j; + +@Slf4j @StepScope @ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME) @RequiredArgsConstructor @Component public class DemoTasklet implements Tasklet {execute ๋ฉ์๋ ๋ด ๋ก๊น ๋ณ๊ฒฝ:
- System.out.println("Demo Tasklet ์คํ (์คํ ์ผ์ : " + requestDate + ")"); + log.info("Demo Tasklet ์คํ (์คํ ์ผ์ : {})", requestDate); Thread.sleep(1000); - System.out.println("Demo Tasklet ์์ ์๋ฃ"); + log.info("Demo Tasklet ์์ ์๋ฃ");
28-28: Thread.sleep ์ฌ์ฉ์ ๋ํ ๊ฒํ๋ฐ๋ชจ ๋ชฉ์ ์ Tasklet์ด์ง๋ง Thread.sleep(1000)์ ๋ฐฐ์น ์ฒ๋ฆฌ ์ค๋ ๋๋ฅผ ๋ธ๋กํนํฉ๋๋ค. ์ค์ ์์ ์ ์๋ฎฌ๋ ์ด์ ํ๋ ค๋ ์๋๋ผ๋ฉด ๊ด์ฐฎ์ง๋ง, ํ๋ก๋์ ์ฝ๋์์๋ ์ ๊ฑฐํ๊ฑฐ๋ ์ค์ ๋น์ฆ๋์ค ๋ก์ง์ผ๋ก ๋์ฒดํด์ผ ํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/rank/RankingFacade.java (1)
43-49: type ํ๋ผ๋ฏธํฐ์ ๋ฐฉ์ด์ null ์ฒ๋ฆฌ๋ฅผ ์ถ๊ฐํ์ธ์.
type.toUpperCase()๋ type์ด null์ผ ๊ฒฝ์ฐNullPointerException์ ๋ฐ์์ํต๋๋ค. ํ์ฌ ์ปจํธ๋กค๋ฌ์์ ๊ธฐ๋ณธ๊ฐ์ ์ ๊ณตํ์ง๋ง,RankingFacade๋ public ์ปดํฌ๋ํธ์ด๋ฏ๋ก ๋ค๋ฅธ ๊ณณ์์๋ ํธ์ถ๋ ์ ์์ต๋๋ค.๐ ๋ฐฉ์ด์ null ์ฒ๋ฆฌ ์ ์
public List<RankingInfo> getRankings(String type, String date, int page, int size) { - return switch (type.toUpperCase()) { + String normalizedType = (type != null) ? type.toUpperCase() : "DAILY"; + return switch (normalizedType) { case "WEEKLY" -> rankingService.getWeeklyRankings(date, page, size); case "MONTHLY" -> rankingService.getMonthlyRankings(date, page, size); default -> getTopRankings(date, page, size); }; }๋๋ Java Objects ์ ํธ๋ฆฌํฐ ์ฌ์ฉ:
+import java.util.Objects; + public List<RankingInfo> getRankings(String type, String date, int page, int size) { - return switch (type.toUpperCase()) { + return switch (Objects.requireNonNullElse(type, "DAILY").toUpperCase()) { case "WEEKLY" -> rankingService.getWeeklyRankings(date, page, size); case "MONTHLY" -> rankingService.getMonthlyRankings(date, page, size); default -> getTopRankings(date, page, size); }; }apps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1Controller.java (1)
23-29: ์ ํ ์ฌํญ: ํ์ ์์ ์ฑ์ ์ํด enum ์ฌ์ฉ์ ๊ณ ๋ คํ์ธ์.ํ์ฌ
type์ String์ผ๋ก ์ฒ๋ฆฌํ๊ณ ์์ด ์ ์ฐํ์ง๋ง, enum์ ์ฌ์ฉํ๋ฉด ์ปดํ์ผ ํ์ ์์ ์ฑ๊ณผ ๋ช ํํ API ๋ฌธ์ํ๋ฅผ ์ ๊ณตํ ์ ์์ต๋๋ค.๐ Enum ๊ธฐ๋ฐ ๋ฆฌํฉํ ๋ง ์ ์
RankingType enum ์์ฑ:
public enum RankingType { DAILY, WEEKLY, MONTHLY }์ปจํธ๋กค๋ฌ ์์ :
public ApiResponse<List<RankingResponse>> getRankings( - @RequestParam(value = "type", defaultValue = "DAILY") String type, + @RequestParam(value = "type", defaultValue = "DAILY") RankingType type, @RequestParam(value = "date") String date, @RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "size", defaultValue = "20") int size ) { - List<RankingInfo> infos = rankingFacade.getRankings(type, date, page, size); + List<RankingInfo> infos = rankingFacade.getRankings(type.name(), date, page, size);์ด๋ ๊ฒ ํ๋ฉด:
- ์๋ชป๋ type ๊ฐ์ ๋ํด Spring์ด ์๋์ผ๋ก 400 ์๋ฌ ๋ฐํ
- API ๋ฌธ์์์ ํ์ฉ๋ ๊ฐ์ด ๋ช ํํ๊ฒ ํ์๋จ
- IDE ์๋์์ฑ ์ง์
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/scheduler/RankingScheduler.java (2)
37-39: ํฅํ ๋ชจ๋ํฐ๋ง ๊ฐํ ๊ณ ๋ ค์์ธ ๋ฐ์ ์ ๋ก๊ทธ๋ง ๋จ๊ธฐ๊ณ ์์ต๋๋ค. ์ฃผ์์ ์ธ๊ธ๋ ๊ฒ์ฒ๋ผ Slack ๋ฑ ์ธ๋ถ ์๋ฆผ ์ฑ๋๋ก ์๋ฌ๋ฅผ ์ ์กํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ฉด ์ด์ ์ค ๋ฌธ์ ๊ฐ์ง๊ฐ ๋ ๋นจ๋ผ์ง ๊ฒ์ ๋๋ค.
Slack ์๋ฆผ ๊ธฐ๋ฅ ๊ตฌํ์ ๋์๋๋ฆด๊น์? Webhook ๊ธฐ๋ฐ ์๋ฆผ ์ฝ๋๋ฅผ ์์ฑํด ๋๋ฆด ์ ์์ต๋๋ค.
Also applies to: 55-57
27-40: ๋ฐฐ์น ์ก ์ค๋ณต ์คํ ๋ฐฉ์ง ํ์ธ ๊ถ์ฅ์ค์ผ์ค๋ฌ์ ๋์ ์คํ ๋ฐฉ์ง ๋ก์ง์ด ๋ณด์ด์ง ์์ต๋๋ค. ๋ง์ฝ ๋ฐฐ์น ์ก์ด ์์๋ณด๋ค ์ค๋ ์คํ๋์ด ๋ค์ ์ค์ผ์ค ์๊ฐ๊ณผ ๊ฒน์น๋ฉด ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
Spring Batch์ ๊ธฐ๋ณธ
JobLauncher๋ ๋์ผํ ํ๋ผ๋ฏธํฐ๋ก ์๋ฃ๋ ์ก์ ์ฌ์คํ์ ๋ฐฉ์งํ์ง๋ง, ์คํ ์ค์ธ ์ก๊ณผ์ ๋์ ์คํ์ ๋ง์ง ์์ ์ ์์ต๋๋ค. ๋ค์ ์ค ํ๋๋ฅผ ๊ณ ๋ คํ์ธ์:
- Spring์
@Scheduled์fixedDelay์ฌ์ฉ (์ด์ ์คํ ์๋ฃ ํ ๋๊ธฐ)ShedLock๊ฐ์ ๋ถ์ฐ ๋ฝ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ- ๋ฐฐ์น ์ก ๋ด๋ถ์์ ์ด๋ฏธ ์คํ ์ค์ธ ์ธ์คํด์ค ํ์ธ
ํ์ฌ ์ฃผ๊ฐ/์๊ฐ ๋ฐฐ์น๋ผ์ ์ค๋ณต ์คํ ๊ฐ๋ฅ์ฑ์ ๋ฎ์ง๋ง, ์ด์ ํ๊ฒฝ์์์ ์์ ์ฑ์ ์ํด ๊ฒํ ๋ฅผ ๊ถ์ฅํฉ๋๋ค.
Also applies to: 42-58
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingTableSwapTasklet.java (1)
29-37: ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ ๋ฉ๋ชจ๋ฆฌ ๋ฌธ์ ๊ฐ๋ฅ์ฑ
workRepository.findAll()๊ณผmvRepository.saveAll(newData)๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ๋ก๋ํฉ๋๋ค. ์์ 100๊ฐ ์ ํ๋ง ์ฒ๋ฆฌํ๋ ํ์ฌ ์ค๊ณ์์๋ ๋ฌธ์ ์์ง๋ง, ๋ฐ์ดํฐ๊ฐ ์ฆ๊ฐํ ๊ฒฝ์ฐ ๋ฐฐ์น ์ฒญํฌ ์ฒ๋ฆฌ๋ ํ์ด์ง์ ๊ณ ๋ คํ์ธ์.๋ํ
deleteAllInBatch()ํsaveAll()์ฌ์ด์ ์ฅ์ ๋ฐ์ ์ ๋ฐ์ดํฐ ์์ค ๊ฐ๋ฅ์ฑ์ด ์์ต๋๋ค. ์ค์ํ ๊ฒฝ์ฐ ํธ๋์ญ์ ๋ฒ์ ๋๋ swap ์ ๋ต(์์ ํ ์ด๋ธ ํ์ฉ)์ ๊ฒํ ํ์ธ์.apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java (2)
37-45: WeeklyRankingJobConfig์ ๋ฌ๋ฆฌ JobListener๊ฐ ๋๋ฝ๋์์ต๋๋ค.
WeeklyRankingJobConfig์์๋.listener(jobListener)๋ฅผ ์ฌ์ฉํ์ฌ Job ์คํ์ ๋ชจ๋ํฐ๋งํ์ง๋ง,MonthlyRankingJobConfig์๋ ๋๋ฝ๋์ด ์์ต๋๋ค. ์ผ๊ด์ฑ์ ์ํด ์ถ๊ฐ๋ฅผ ๊ถ์ฅํฉ๋๋ค.๐ ์์ ์ ์
+ private final JobListener jobListener; + @Bean(JOB_NAME) public Job monthlyRankingJob() { return new JobBuilder(JOB_NAME, jobRepository) .incrementer(new RunIdIncrementer()) .start(monthlyPrepareStep()) .next(monthlyCalculationStep()) .next(monthlyTableSwapStep()) + .listener(jobListener) .build(); }
30-35: ์ค์ผ์ค๋ฌ ํ์ด๋ฐ์ผ๋ก ํ์ฌ ๋์ ์คํ ๋ฐฉ์ง๋์ด ์์ผ๋, ์ฝ๋ ์์ค์ ๋ณดํธ ์์ด ์ค๊ณ ์ํ ์กด์ฌํ์ฌ ์ค์ผ์ค๋ฌ ์ค์ (
weeklyRankingJob์ ๋งค์ฃผ ์์์ผ 02:00,monthlyRankingJob์ ๋งค์ 1์ผ 03:00)์ผ๋ก ์ธํด ๋ ์์ ์ ๋์ ์คํ์ด ์ค์ ๋ก๋ ๋ฐฉ์ง๋๊ณ ์์ต๋๋ค.๊ทธ๋ฌ๋
RankingPrepareTasklet๊ณผMonthlyRankingTableSwapTasklet์ด ๋์ผํWeeklyRankingWorkRepository(weekly_ranking_work ํ ์ด๋ธ)๋ฅผ ๊ณต์ ํ๋ ๊ตฌ์กฐ๋ ์ฌ์ ํ ์ค๊ณ์ ์ํ์ ์๊ณ ์์ต๋๋ค. ์ค์ผ์ค๋ฌ ์ค์ ๋ณ๊ฒฝ์ด๋ ์๋ ์ก ์คํ ์ ๋ฐ์ดํฐ ์ถฉ๋ ๊ฐ๋ฅ์ฑ์ด ์์ผ๋ฏ๋ก, ๋ณ๋์ Monthly์ฉ ์์ ํ ์ด๋ธ ๋์ ์ ๊ถ์ฅํฉ๋๋ค.modules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMV.java (1)
31-43: WeeklyRankingMV์ MonthlyRankingMV์ ํฉํ ๋ฆฌ ๋ฉ์๋ ์ ๊ทผ ๋ฐฉ์ ์ฐจ์ด ๊ฒํ ํ์MonthlyRankingMV.createFromWork๊ฐ productName, price, isSoldOut์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋ ๊ฒ์ ์ฌ๋ฐ๋ฅธ ์ค๊ณ์ ๋๋ค.
๋ค๋ง WeeklyRankingMV.createFromWork๋ ํ์ฌ "์ํ๋ช ์์", 0L, false๋ก ํ๋์ฝ๋ฉ๋ ํ๋ ์ด์คํ๋๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค (modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.java:48-50, ์ฝ๋ ์ฃผ์ "์ค์ ๊ตฌํ ์ Product ์ ๋ณด ๊ฒฐํฉ ํ์").
์ด ๋ถ์ผ์น๋ฅผ ๊ฐ์ ํ๋ ค๋ฉด RankingTableSwapTasklet์์ Product ์ ๋ณด๋ฅผ ํจ๊ป ์กฐํํ์ฌ ์ ๋ฌํด์ผ ํฉ๋๋ค. ํ์ฌ๋ WeeklyRankingWork ๋ฐ์ดํฐ๋ง ์ฌ์ฉ ๊ฐ๋ฅํ๋ฏ๋ก, ์๋ณธ Design์ด ์๋ํ ๊ฒ์ธ์ง ํ์ธํ๊ณ ํ์์ productId ๊ธฐ๋ฐ Product ์กฐํ ๋ก์ง์ ์ถ๊ฐํ๋ ๊ฒ์ ๊ฒํ ํด์ฃผ์ธ์.
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java (1)
40-47: Step ์ด๋ฆ๊ณผ ๋ฉ์๋๋ช ๋ถ์ผ์น ํ์ธ ํ์Step ๋น ์ด๋ฆ์
demoSimpleTask์ด์ง๋ง ๋ฉ์๋๋ช ์categorySyncStep์ ๋๋ค. ๋ฐ๋ชจ ๋ชฉ์ ์ ์ฝ๋๋ผ๋ฉด ํฐ ๋ฌธ์ ๋ ์๋์ง๋ง, ๋ฉ์๋๋ช ์demoSimpleTaskStep๋๋ ์ด์ ์ ์ฌํ๊ฒ ๋ณ๊ฒฝํ๋ฉด ์ฝ๋ ๊ฐ๋ ์ฑ์ด ํฅ์๋ฉ๋๋ค.๐ ์ ์๋ ์์
@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(); }apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/RankingChunkConfig.java (1)
27-37: ์ฃผ๊ฐ ๋ญํน๋ Job ํ๋ผ๋ฏธํฐ๋ก ๋ ์ง๋ฅผ ๋ฐ๋ ๊ฒ์ ๊ณ ๋ คํด ๋ณด์ธ์
monthlyRankingReader๋startDateJob ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ์ฉํ๋ ๋ฐ๋ฉด,rankingReader๋LocalDateTime.now().minusDays(7)๋ฅผ ํ๋์ฝ๋ฉํ๊ณ ์์ต๋๋ค. ์ฌ์ฒ๋ฆฌ๋ ํ ์คํธ๋ฅผ ์ํด ๋ ์ง๋ฅผ ์ธ๋ถ์์ ์ฃผ์ ๋ฐ์ผ๋ฉด ๋ ์ ์ฐํด์ง๋๋ค.apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/RankingServiceImpl.java (2)
56-64: DateTimeFormatter ์ธ์คํด์ค๋ฅผ static final๋ก ์ถ์ถํ์ธ์
DateTimeFormatter.ofPattern("yyyyMMdd")๊ฐ ๋งค ํธ์ถ๋ง๋ค ์์ฑ๋ฉ๋๋ค. ํด๋์ค ๋ ๋ฒจ ์์๋ก ์ถ์ถํ๋ฉด ๋ถํ์ํ ๊ฐ์ฒด ์์ฑ์ ํผํ ์ ์์ต๋๋ค.๐ ์ ์๋ ์์
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + @Override public Integer getProductRank(Long productId) { - String today = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String today = LocalDateTime.now().format(DATE_FORMATTER); String key = "ranking:all:" + today;
23-38: Redis ํค ์์ฑ ๋ก์ง ํ๋์ฝ๋ฉ ๋ฐ NumberFormatException ์ํ์ฑ
getTopRankingIds๋ฉ์๋(25ํ)์getProductRank๋ฉ์๋(59ํ)์์"ranking:all:"ํค ํจํด์ด ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. ๋์ผํ ํจํด์ด ๋ค๋ฅธ ํ์ผ(MetricsEventConsumer 66ํ ๋ฑ)์์๋ ๋ฐ๋ณต๋๊ณ ์์ผ๋ฉฐ, ๋ณ๋ ๋ชจ๋์RankingKeyGenerator์ธํฐํ์ด์ค(generateDailyKey๋ฉ์๋ ์ ๊ณต)์ ๋ถ์ผ์นํฉ๋๋ค. ์ค์ํ๋ ํค ์์ฑ ๋ก์ง์ผ๋ก ํตํฉํ์ญ์์ค.๋ํ 36ํ์
Long::valueOf๋ ์๋ฌ ์ฒ๋ฆฌ๊ฐ ์์ด Redis์ ์ ํจํ์ง ์์ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋ ๊ฒฝ์ฐNumberFormatException์ ๋ฐ์์ํฌ ์ ์์ต๋๋ค. ๋ณํ ์ ๋ฐ์ดํฐ ๊ฒ์ฆ ๋๋ ์์ธ ์ฒ๋ฆฌ๋ฅผ ์ถ๊ฐํ์ญ์์ค.apps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1ApiSpec.java (1)
14-19: type ํ๋ผ๋ฏธํฐ์ Enum ์ฌ์ฉ์ ๊ณ ๋ คํด ๋ณด์ธ์
typeํ๋ผ๋ฏธํฐ๊ฐString์ผ๋ก ์ ์๋์ด ์์ด ๋ฐํ์์ ์๋ชป๋ ๊ฐ์ด ์ ๋ฌ๋ ์ ์์ต๋๋ค.RankingTypeenum์ ์ ์ํ๋ฉด ์ปดํ์ผ ํ์ ์์ ์ฑ๊ณผ Swagger ๋ฌธ์ ์๋ํ ์ด์ ์ ์ป์ ์ ์์ต๋๋ค.๐ Enum ์ ์ ์์
public enum RankingType { DAILY, WEEKLY, MONTHLY }ApiResponse<List<RankingResponse>> getRankings( - @Parameter(description = "๋ญํน ํ์ (DAILY, WEEKLY, MONTHLY)", example = "WEEKLY") String type, + @Parameter(description = "๋ญํน ํ์ ", example = "WEEKLY") RankingType type, @Parameter(description = "์กฐํ ๋ ์ง (yyyyMMdd)", example = "20251225") String date, @Parameter(description = "ํ์ด์ง ๋ฒํธ") int page, @Parameter(description = "ํ์ด์ง ํฌ๊ธฐ") int size );apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/monthly/MonthlyRankingTableSwapTasklet.java (1)
32-34: Redis ํค ํจํด์ ์์๋ก ์ถ์ถ ๊ถ์ฅ
"product:snapshot:"ํค ํจํด์ด ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. ๋ค๋ฅธ ๊ณณ์์๋ ๋์ผํ ํจํด์ ์ฌ์ฉํ๋ค๋ฉด ์์๋ ๊ณตํต ์ ํธ๋ฆฌํฐ๋ก ์ถ์ถํ์ฌ ์ผ๊ด์ฑ์ ์ ์งํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๐ Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (49)
.github/workflows/main.ymlREADME.mdapps/commerce-api/build.gradle.ktsapps/commerce-api/src/main/java/com/loopers/application/order/event/OrderCreatedEvent.javaapps/commerce-api/src/main/java/com/loopers/application/order/event/OrderSalesAggregateListener.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.javaapps/commerce-api/src/main/java/com/loopers/application/rank/RankingFacade.javaapps/commerce-api/src/main/java/com/loopers/application/rank/RankingInfo.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/rank/RankingService.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/rank/RankingServiceImpl.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1ApiSpec.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1Dto.javaapps/commerce-api/src/main/resources/application.ymlapps/commerce-batch/build.gradle.ktsapps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.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/ranking/MonthlyRankingJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/RankingChunkConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/scheduler/RankingScheduler.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingPrepareTasklet.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingTableSwapTasklet.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/monthly/MonthlyRankingTableSwapTasklet.javaapps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/weekly/WeeklyRankingProcessor.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/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/rank/RankingScheduler.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.javaapps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingKeyGenerator.javaapps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingService.javaapps/commerce-streamer/src/main/java/com/loopers/domain/rank/RedisRankingKeyGenerator.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.javaapps/pg-simulator/src/main/resources/application.ymlmodules/jpa/src/main/java/com/loopers/domain/ProductMetrics.javamodules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMV.javamodules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMVRepository.javamodules/jpa/src/main/java/com/loopers/domain/rank/monthly/ProductSnapshot.javamodules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.javamodules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMVRepository.javamodules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWork.javamodules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWorkRepository.javamodules/kafka/src/main/java/com/loopers/event/ProductStockEvent.javasettings.gradle.kts
๐ค Files with no reviewable changes (1)
- apps/commerce-api/src/main/resources/application.yml
๐งฐ Additional context used
๐ง Learnings (8)
๐ 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:
modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWorkRepository.javamodules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMVRepository.javamodules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMVRepository.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.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/ChunkListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.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-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:
modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.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/main/java/com/loopers/domain/metrics/ProductMetricsService.javaapps/commerce-api/src/main/java/com/loopers/application/order/event/OrderSalesAggregateListener.javaapps/commerce-api/src/main/java/com/loopers/application/order/event/OrderCreatedEvent.javamodules/kafka/src/main/java/com/loopers/event/ProductStockEvent.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/domain/metrics/ProductMetricsService.javamodules/jpa/src/main/java/com/loopers/domain/ProductMetrics.java
๐ Learning: 2025-11-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product ์ํฐํฐ (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)๋ ์ ์ฆ์ผ์ด์ค๋ณ๋ก ์๋์ ์ผ๋ก ๋ค๋ฅธ ๋ฝ ์ ๋ต์ ์ฌ์ฉํ๋ค: ์ข์์ ๊ธฐ๋ฅ์๋ ๋น๊ด์ ๋ฝ(findByIdForUpdate)์, ์ฌ๊ณ ์ฐจ๊ฐ์๋ ๋๊ด์ ๋ฝ(Version + ์ฌ์๋)์ ์ฌ์ฉํ๋ค.
Applied to files:
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.javamodules/jpa/src/main/java/com/loopers/domain/ProductMetrics.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.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/JobListener.javaapps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java
๐งฌ Code graph analysis (14)
apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RedisRankingKeyGenerator.java (1)
apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingService.java (1)
Component(14-50)
apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingKeyGenerator.java (2)
apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingService.java (1)
Component(14-50)apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RedisRankingKeyGenerator.java (1)
Component(5-14)
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-batch/src/main/java/com/loopers/batch/job/ranking/RankingChunkConfig.java (1)
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/ranking/step/RankingTableSwapTasklet.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/ranking/step/RankingPrepareTasklet.java (1)
Component(12-24)apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/weekly/WeeklyRankingProcessor.java (1)
Component(9-30)
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)
modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.java (2)
modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWork.java (1)
Entity(12-30)modules/jpa/src/main/java/com/loopers/domain/rank/monthly/ProductSnapshot.java (1)
Getter(8-19)
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingPrepareTasklet.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingTableSwapTasklet.java (1)
Component(17-40)apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/weekly/WeeklyRankingProcessor.java (1)
Component(9-30)
modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWork.java (1)
modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.java (1)
Entity(12-53)
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (2)
apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1)
ConfigurationPropertiesScan(10-22)apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt (1)
main(22-24)
modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.java (1)
apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt (1)
name(14-87)
apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.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/StepMonitorListener.java (1)
Slf4j(13-44)
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/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)
๐ Additional comments (44)
README.md (1)
30-30: ๋ฌธ์ ์ ๋ฐ์ดํธ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๋ฐ์๋์์ต๋๋ค.์๋ก์ด
commerce-batch๋ชจ๋ ์ถ๊ฐ๋ฅผ ๋ฌธ์์ ์ ํํ๊ฒ ๋ฐ์ํ์ต๋๋ค. settings.gradle.kts์ ์ค์ ๋ชจ๋ ํฌํจ๊ณผ ์ผ๊ด์ฑ ์๊ฒ ์์ฑ๋์์ต๋๋ค.settings.gradle.kts (1)
7-7: ๋ชจ๋ ์ถ๊ฐ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์์ต๋๋ค.
commerce-batch๋ชจ๋์ด Gradle ๋ฉํฐ๋ชจ๋ ํ๋ก์ ํธ์ ์ฌ๋ฐ๋ฅด๊ฒ ํฌํจ๋์์ต๋๋ค. ๋ชจ๋ ๊ฒฝ๋ก ํ์๊ณผ ์์น๊ฐ ๊ธฐ์กด ๊ตฌ์กฐ์ ์ผ๊ด์ฑ ์๊ฒ ์์ฑ๋์์ต๋๋ค.apps/commerce-batch/build.gradle.kts (1)
1-21: LGTM! ์์กด์ฑ ๊ตฌ์ฑ์ด ์ ์ ํฉ๋๋ค.๋ฐฐ์น ๋ชจ๋์ ์์กด์ฑ ๊ตฌ์ฑ์ด PR ๋ชฉํ์ ์ ์ ๋ ฌ๋์ด ์์ต๋๋ค:
- Spring Batch ํ๋ ์์ํฌ (starter ๋ฐ test ์ง์)
- ๋ด๋ถ ๋ชจ๋ ์์กด์ฑ (JPA, Redis, Jackson ๋ฑ)์ด ์ฃผ๊ฐ/์๊ฐ ๋ญํน ๋ฐฐ์น ์์ ์ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑ
- QueryDSL ์ค์ ์ด Jakarta ๋ค์์คํ์ด์ค๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉ (
::jakartaclassifier)- ํ ์คํธ ํฝ์ค์ฒ ์ฌ์ฌ์ฉ์ผ๋ก ์ผ๊ด์ฑ ํ๋ณด
apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderCreatedEvent.java (2)
48-48: ์ผ๊ด๋ null ์ฒ๋ฆฌ ๊ตฌํ
productName์ถ์ถ ๋ก์ง์ด ๋ค๋ฅธ ํ๋๋ค(price, stock)๊ณผ ๋์ผํ ํจํด์ผ๋ก null-safeํ๊ฒ ๊ตฌํ๋์ด ์ผ๊ด์ฑ์ด ์ ์ง๋๊ณ ์์ต๋๋ค.์ฐธ๊ณ : "Unknown" ๋ฌธ์์ด์ด ํ๋์ฝ๋ฉ๋์ด ์์ผ๋, ํ์ต๋ ๋ด์ฉ์ ๋ฐ๋ฅด๋ฉด ์ด ์ด๋ฒคํธ๋ ๋ด๋ถ ์ ์ฉ์ด๋ฏ๋ก ํ์ฌ ๊ตฌํ์ด ์ ์ ํฉ๋๋ค.
24-24: ์ด ์ฝ๋ฉํธ๋ ๊ฒํ ๋ถํ์ํฉ๋๋ค.
OrderCreatedEvent๋ด์OrderItemInfo๋OrderInfo.java์OrderItemInfo์ ๋ณ๊ฐ์ ์ค์ฒฉ ๋ ์ฝ๋์ ๋๋ค. ๊ฐ๊ฐ ๋ค๋ฅธ ๋ชฉ์ ๊ณผ ํ๋๋ฅผ ๊ฐ์ง๊ณ ์์ผ๋ฉฐ, ๊ฐ์ ์๊ทธ๋์ฒ๋ฅผ ๊ณต์ ํ์ง ์์ต๋๋ค.OrderCreatedEvent๋ด์ ๋ชจ๋OrderItemInfo์์ฑ์ ํธ์ถ(46-52์ค)์ ์๋ก์ด 5๊ฐ ํ๋ ์๊ทธ๋์ฒ์ ์ ํํ ์ผ์นํฉ๋๋ค. ๋ฐ๋ผ์ ๋ ์ฝ๋ ์๊ทธ๋์ฒ ๋ณ๊ฒฝ์ผ๋ก ์ธํ ํธํ์ฑ ๋ฌธ์ ๋ ์์ต๋๋ค.Likely an incorrect or invalid review comment.
apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RedisRankingKeyGenerator.java (1)
5-13: LGTM!
RankingKeyGenerator์ธํฐํ์ด์ค์ ๊น๋ํ ๊ตฌํ์ ๋๋ค. ํค prefix๊ฐ ์์๋ก ๋ถ๋ฆฌ๋์ด ์์ด ์ ์ง๋ณด์์ฑ์ด ์ข์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/domain/rank/RankingService.java (2)
19-25: LGTM!
RankingKeyGenerator๋ฅผ ํตํ ํค ์์ฑ ๋ก์ง ์ถ์ํ๊ฐ ์ ์ ์ฉ๋์์ต๋๋ค. ํ๋์ฝ๋ฉ๋ prefix ๋์ ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํจ์ผ๋ก์จ ํ ์คํธ ์ฉ์ด์ฑ๊ณผ ์ ์ฐ์ฑ์ด ํฅ์๋์์ต๋๋ค.
41-49:unionAndStore๋์ ํ์ธ ํ์
unionAndStore๋targetKey์ ์ด๋ฏธ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ ๊ฒฝ์ฐ ํด๋น ๋ฐ์ดํฐ๋ฅผ ๋ฎ์ด์๋๋ค. ๋ง์ฝ ๋น์ผ ์์ ์ ์ ์ด๋ฏธ ์ต์ผ(targetKey) ๋ญํน ๋ฐ์ดํฐ๊ฐ ์ถ์ ๋์ด ์๋ค๋ฉด, ์ด ๋ฐ์ดํฐ๊ฐ ์์ค๋ ์ ์์ต๋๋ค.ํ์ฌ ์ค์ผ์ค๋ฌ๊ฐ 23:50์ ์คํ๋๋ฏ๋ก, ์ต์ผ ํค์ ๋ฐ์ดํฐ๊ฐ ์์ ๊ฐ๋ฅ์ฑ์ ๋ฎ์ง๋ง, ์๋๋ ๋์์ธ์ง ํ์ธํด ์ฃผ์ธ์. ๋ง์ฝ ๊ธฐ์กด ๋ฐ์ดํฐ์ ๋ณํฉํด์ผ ํ๋ค๋ฉด
targetKey๋ union ๋์์ ํฌํจํด์ผ ํฉ๋๋ค.๐ ๊ธฐ์กด ๋ฐ์ดํฐ ๋ณด์กด์ด ํ์ํ ๊ฒฝ์ฐ ์์ ์ ์
public void carryOverRanking(String sourceDate, String targetDate, double weight) { String sourceKey = rankingKeyGenerator.generateDailyKey(sourceDate); String targetKey = rankingKeyGenerator.generateDailyKey(targetDate); - redisTemplate.opsForZSet().unionAndStore(sourceKey, List.of(), targetKey, - Aggregate.SUM, Weights.of(weight)); + // targetKey ๊ธฐ์กด ๋ฐ์ดํฐ์ ๋ณํฉ (source: weight ์ ์ฉ, target: 1.0 ์ ์ง) + redisTemplate.opsForZSet().unionAndStore(sourceKey, List.of(targetKey), targetKey, + Aggregate.SUM, Weights.of(weight, 1.0)); redisTemplate.expire(targetKey, 2, TimeUnit.DAYS); }apps/pg-simulator/src/main/resources/application.yml (1)
26-32: Feign ํด๋ผ์ด์ธํธ ํ์์์ ์ค์ ์ด ์ง๋์น๊ฒ ์งง๊ณ ์ผ๊ด๋์ง ์์ต๋๋ค.connectTimeout๊ณผ readTimeout์ด ๊ฐ๊ฐ 1์ด๋ก ์ค์ ๋์ด ์์ด ์ ์์ ์ธ ๋คํธ์ํฌ ์ง์ฐ ์ํฉ์์๋ ์์ฒญ์ด ์คํจํ ์ ์์ต๋๋ค. ํ์ฌ Tomcat์ connection-timeout์ด 60์ด(Line 7)๋ก ์ค์ ๋ ๊ฒ๊ณผ ๋น๊ตํ๋ฉด ์ฌ๊ฐํ ๋ถ์ผ์น๊ฐ ์์ต๋๋ค.
์ถ๊ฐ ๋ฌธ์ : ๋ถ์์ ํ ํ๋ณต๋ ฅ ๋ฉ์ปค๋์ฆ
์ผ๋ถ Feign ํด๋ผ์ด์ธํธ ๋ฉ์๋์๋ง
@Retry์@CircuitBreaker๊ฐ ์ ์ฉ๋์ด ์์ต๋๋ค. ์๋ฅผ ๋ค์ดPgClient์requestPayment()๋ฉ์๋๋ ๋ณดํธ๋์ง๋ง,getTransactionsByOrder()๋ฉ์๋๋ ์๋ฌด๋ฐ ๋ณดํธ๋ ์์ต๋๋ค. ๋ชจ๋ ์ธ๋ถ API ํธ์ถ์ ๋ํด ์ผ๊ด๋ ํ๋ณต๋ ฅ ์ ๋ต์ด ํ์ํฉ๋๋ค.๊ถ์ฅ ์กฐ์น:
- Feign ํ์์์์ ์ ์ ํ ์์ค์ผ๋ก ์ํฅ ์กฐ์ (connectTimeout: 3
5์ด, readTimeout: 1030์ด)- ๋ชจ๋ Feign ํด๋ผ์ด์ธํธ ๋ฉ์๋์
@Retry์@CircuitBreaker์ ์ฉ- Tomcat๊ณผ Feign ํ์์์ ์ค์ ์ ์ผ๊ด์ฑ ๊ฒํ
โ Skipped due to learnings
Learnt from: HongChangMo Repo: Loopers-dev-lab/loopers-spring-java-template PR: 156 File: apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt:51-59 Timestamp: 2025-12-05T04:37:02.942Z Learning: User HongChangMo prefers to skip detailed code reviews for the pg-simulator module (apps/pg-simulator/**) in the loopers-spring-java-template repository.apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (1)
16-20: ํ์์กด ์ค์ ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.
@PostConstruct๋ฅผ ์ฌ์ฉํ์ฌ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ ํ์์กด์Asia/Seoul๋ก ์ค์ ํ๋ ๊ฒ์ ๋ฐฐ์น ์์ ์ ์๊ฐ ๊ด๋ จ ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ๋ณด์ฅํ๋ ์ข์ ๋ฐฉ๋ฒ์ ๋๋ค. CommerceStreamerApplication๊ณผ ๋์ผํ ํจํด์ ๋ฐ๋ฅด๊ณ ์์ด ์ผ๊ด์ฑ๋ ์ ์ง๋ฉ๋๋ค..github/workflows/main.yml (1)
1-4: ์ํฌํ๋ก์ฐ ํธ๋ฆฌ๊ฑฐ ์ค์ ์ด ์ ์ ํฉ๋๋ค.PR์ด ์ด๋ฆฌ๊ฑฐ๋ ๋๊ธฐํ๋ ๋ ์๋์ผ๋ก ์คํ๋๋ ์ค์ ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java (1)
6-10: ๊ธฐ๋ณธ ์ปจํ ์คํธ ๋ก๋ฉ ํ ์คํธ ํ์ธ๋จ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ๊ฐ ์ ์์ ์ผ๋ก ๋ก๋๋๋์ง ํ์ธํ๋ ํ์ค ์ค๋ชจํฌ ํ ์คํธ์ ๋๋ค. DemoJobE2ETest์ ํจ๊ป ๊ธฐ๋ณธ์ ์ธ ํตํฉ ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ ๊ณตํฉ๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/step/DemoTasklet.java (2)
14-17: ์ปดํฌ๋ํธ ์ค์ ํ์ธ๋จ@StepScope์ @ConditionalOnProperty๋ฅผ ์ฌ์ฉํ ์กฐ๊ฑด๋ถ ๋น ๋ฑ๋ก์ด ์ ์ ํฉ๋๋ค. DemoJobConfig.JOB_NAME๊ณผ ์ฐ๋๋์ด ํน์ ์ก ์คํ ์์๋ง ํ์ฑํ๋ฉ๋๋ค.
19-26: Job ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ ๋ก์ง ํ์ธ๋จSpEL์ ํตํ job parameter ์ฃผ์ ๊ณผ null ์ฒดํฌ๊ฐ ์ ์ ํฉ๋๋ค. DemoJobE2ETest์ ํ ์คํธ ์ผ์ด์ค์ ์ผ์นํ๋ ๊ฒ์ฆ ๋ก์ง์ ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)
7-7: Import ์ฌ์ ๋ ฌ ํ์ธRankingService import์ ์์น๋ง ๋ณ๊ฒฝ๋ ๊ฒ์ผ๋ก, ๊ธฐ๋ฅ์ ๋ณ๊ฒฝ์ ์์ต๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.java (1)
3-3: ํจํค์ง ์ฌ๊ตฌ์ฑ ํ์ธ๋จProductMetrics ์ํฐํฐ์ ํจํค์ง ๊ฒฝ๋ก๊ฐ
com.loopers.domain.metrics์์com.loopers.domain์ผ๋ก ๋ณ๊ฒฝ๋์์ต๋๋ค. ๋ ํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค์ ๊ธฐ๋ฅ์ ๋ณ๊ฒฝ์ ์์ผ๋ฉฐ, PR์ ์ ๋ฐ์ ์ธ ๋๋ฉ์ธ ๊ตฌ์กฐ ์ฌ๊ตฌ์ฑ๊ณผ ์ผ์นํฉ๋๋ค.modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWorkRepository.java (1)
1-9: LGTM!ํ์ค Spring Data JPA ๋ฆฌํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค๋ก ์ ์ ํ๊ฒ ๊ตฌํ๋์์ต๋๋ค. ๋ฐฐ์น ์์ ์์
WeeklyRankingWork์ํฐํฐ๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐ ํ์ํ CRUD ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1Dto.java (1)
7-24: API ํธํ์ฑ์ ๊ณ ๋ คํ ๋ณ๊ฒฝ ์ ๋ต์ ๊ฒํ ํ์ธ์.
stock(int) ํ๋๋ฅผisSoldOut(boolean)์ผ๋ก ๋ณ๊ฒฝํ๋ ๊ฒ์ ๊ธฐ์กด API ์๋น์์๊ฒ breaking change์ ๋๋ค.์ด ๋ณ๊ฒฝ์ฌํญ์ด V1 API (
/api/v1/rankings)์ ์ ์ฉ๋๋ฏ๋ก, ๋ค์์ ๊ณ ๋ คํด์ผ ํฉ๋๋ค:
- ๊ธฐ์กด ํด๋ผ์ด์ธํธ๊ฐ
stockํ๋๋ฅผ ์์ํ๊ณ ์๋ค๋ฉด ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค- API ๋ฒ์ ์ ๊ทธ๋ ์ด๋(V2) ๋๋ deprecation ๊ธฐ๊ฐ์ ๊ณ ๋ คํ์ธ์
- ๋๋ ๋ ํ๋๋ฅผ ๋ชจ๋ ์ ๊ณตํ๋ ์ ์ง์ ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ต์ ๊ฒํ ํ์ธ์
์๋์ ์ธ breaking change๋ผ๋ฉด API ๋ณ๊ฒฝ ์ฌํญ์ ๋ฌธ์ํํ๊ณ ํด๋ผ์ด์ธํธ์๊ฒ ๊ณต์งํด์ผ ํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/rank/RankingV1Controller.java (1)
24-24: LGTM! ํ์ ํธํ์ฑ์ด ์ ์ ์ง๋์์ต๋๋ค.
typeํ๋ผ๋ฏธํฐ์defaultValue = "DAILY"๋ฅผ ์ง์ ํ์ฌ ๊ธฐ์กด ํด๋ผ์ด์ธํธ๊ฐ ํ๋ผ๋ฏธํฐ๋ฅผ ์๋ตํด๋ ๊ธฐ๋ณธ ๋์(์ผ๊ฐ ๋ญํน)์ด ์ ์ง๋ฉ๋๋ค. ์ด๋ ํ์ ํธํ์ฑ์ ๊ณ ๋ คํ ์ข์ ์ค๊ณ์ ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderSalesAggregateListener.java (1)
25-31: LGTM! ์ด๋ฒคํธ ๊ฐํ(Event Enrichment) ํจํด์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ ์ฉ๋์์ต๋๋ค.
ProductStockEvent๊ฐ ์ด์ productName์ ํฌํจํ๋๋ก ํ์ฅ๋์ด, commerce-batch ๋ชจ๋์ด commerce-api ๋๋ฉ์ธ์์ ๋ถ๋ฆฌ๋ ์ ์์ต๋๋ค. ์ด๋ PR ๋ชฉํ์ ๋ช ์๋ ์ด๋ฒคํธ ๊ฐํ ์ ๋ต๊ณผ ์ผ์นํฉ๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java (1)
25-52: afterJob ๊ตฌํ์ด ์ ํํฉ๋๋ค.์๊ฐ ๊ณ์ฐ ๋ฐ ํ์ํ ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค. Duration API๋ฅผ ์ฌ์ฉํ ์๊ฐ ๊ณ์ฐ๊ณผ ํฌ๋งทํ ์ด ์ ์ ํฉ๋๋ค.
modules/kafka/src/main/java/com/loopers/event/ProductStockEvent.java (1)
6-25: LGTM! ์ด๋ฒคํธ ๊ฐํ๋ฅผ ์ํ productName ํ๋ ์ถ๊ฐ๊ฐ ์ฌ๋ฐ๋ฆ ๋๋ค.
ProductStockEvent์productNameํ๋๋ฅผ ์ถ๊ฐํ์ฌ ์ด๋ฒคํธ๊ฐ ์ ํ ์ค๋ ์ท ๋ฐ์ดํฐ(์ด๋ฆ, ๊ฐ๊ฒฉ)๋ฅผ ์ ๋ฌํ๋๋ก ํ์ฅํ์ต๋๋ค. ์ด๋ PR ๋ชฉํ์ ๋ช ์๋ ๋๋ก commerce-batch ๋ชจ๋์ด commerce-api ๋๋ฉ์ธ์์ ๋ถ๋ฆฌ๋ ์ ์๋๋ก ํฉ๋๋ค. ํฉํ ๋ฆฌ ๋ฉ์๋ ์๊ทธ๋์ฒ์ ๋ ์ฝ๋ ๊ตฌ์กฐ๊ฐ ์ผ๊ด์ฑ ์๊ฒ ์ ๋ฐ์ดํธ๋์์ต๋๋ค.modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMVRepository.java (1)
8-12: LGTM! Spring Data JPA ๋ฆฌํฌ์งํ ๋ฆฌ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.
WeeklyRankingMVRepository๋ Spring Data JPA ๊ท์น์ ๋ฐ๋ฅด๋ฉฐ, ํ์ ์ฟผ๋ฆฌ ๋ฉ์๋findByBaseDateOrderByCurrentRankAsc๊ฐ ์ ์ ํ๊ฒ ์ ์๋์์ต๋๋ค. ํ์ต๋ ๋ด์ฉ์ ๋ฐ๋ผ ์ด ์ฝ๋๋ฒ ์ด์ค์์๋ ๋๋ฉ์ธ ๋ฆฌํฌ์งํ ๋ฆฌ์Pageable์ ์ฌ์ฉํ๋ ๊ฒ์ด ํ์ฉ๋ ์ํคํ ์ฒ ๊ฒฐ์ ์ ๋๋ค.apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
58-75: LGTM! ์ฑ๊ณต ์ผ์ด์ค ํ ์คํธ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.
JobParametersBuilder๋ฅผ ์ฌ์ฉํ์ฌLocalDateํ๋ผ๋ฏธํฐ๋ฅผ ์ ๋ฌํ๊ณ , ๋ฐฐ์น ์์ ์ ์ฑ๊ณต์ ์ธ ์๋ฃ๋ฅผ ๊ฒ์ฆํ๋ ํ ์คํธ ๊ตฌ์กฐ๊ฐ ์ ์ ํฉ๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/weekly/WeeklyRankingProcessor.java (2)
18-22: ๊ฐ์ค์น ๊ณต์๊ณผ 100๊ฐ ์ ํ ๊ฒ์ฆ ๊ถ์ฅ์ ์ ๊ณ์ฐ ๊ฐ์ค์น(์กฐํ์ 0.1, ์ข์์ 0.2, ํ๋งค์ 0.6)์ ์์ 100๊ฐ ์ ํ์ด ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ๊ณผ ์ผ์นํ๋์ง ํ์ธ์ด ํ์ํฉ๋๋ค. ํนํ 100๊ฐ ์ ํ์ด ํ๋์ฝ๋ฉ๋์ด ์์ด ํฅํ ๋ณ๊ฒฝ ์ ์ฝ๋ ์์ ์ด ํ์ํฉ๋๋ค.
์๊ตฌ์ฌํญ ๋ฌธ์๋ ๊ด๋ จ ์ด์์์ ๋ค์์ ํ์ธํ์ธ์:
- ๊ฐ์ค์น ๋น์จ์ด ์ฌ๋ฐ๋ฅธ์ง
- ์์ ๋ญํน ๊ฐ์๊ฐ 100๊ฐ๋ก ๊ณ ์ ๋์ด์ผ ํ๋์ง, ์๋๋ฉด ์ค์ ๊ฐ๋ฅํด์ผ ํ๋์ง
13-20: ๋ฉํฐ์ค๋ ๋ TaskExecutor ์ค์ ์ด ์์ด ๋์์ฑ ๋ฌธ์ ์ฐ๋ ค๋ ๊ทผ๊ฑฐ๊ฐ ์์WeeklyRankingJobConfig์
calculationStep()๋ฉ์๋๋ฅผ ํ์ธํ ๊ฒฐ๊ณผ,.taskExecutor()ํธ์ถ์ด ์์ต๋๋ค. Spring Batch๋ taskExecutor๊ฐ ๋ช ์์ ์ผ๋ก ๊ตฌ์ฑ๋์ง ์์ผ๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก ๋จ์ผ ์ค๋ ๋๋ก ์ฒ๋ฆฌํฉ๋๋ค. ๋ฐ๋ผ์rankCounterํ๋์ ํ์ฌ ๊ตฌํ์ ๋์์ฑ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.@StepScope ์ด๋ ธํ ์ด์ ์ ๊ฐ Step ์คํ๋ง๋ค ์๋ก์ด processor ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ฏ๋ก,
rankCounter๋ ๋จ์ผ ์ค๋ ๋์์๋ง ์ ๊ทผ๋ฉ๋๋ค.modules/jpa/src/main/java/com/loopers/domain/rank/monthly/ProductSnapshot.java (1)
1-19: LGTM!๊ฐ๋จํ๊ณ ๋ช ํํ ๊ฐ ๊ฐ์ฒด์ ๋๋ค.
Serializable๊ตฌํ์ Redis ์บ์ฑ์ ์ ํฉํ๋ฉฐ, Lombok ์ฌ์ฉ์ผ๋ก ๋ณด์ผ๋ฌํ๋ ์ดํธ๊ฐ ์ต์ํ๋์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/listener/StepMonitorListener.java (1)
18-21: LGTM!์คํ ์์ ๋ก๊น ์ด ๋ช ํํ๊ณ ์ ์ ํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/rank/RankingInfo.java (2)
11-11: API ๊ฐ์ - stock์์ isSoldOut์ผ๋ก ๋ณ๊ฒฝ
stock์ ์ ๊ฐ์isSoldOut๋ถ๋ฆฐ์ผ๋ก ๋ณ๊ฒฝํ ๊ฒ์ ์ข์ ์บก์ํ์ ๋๋ค. API ์๋น์๋ ์ฌ๊ณ ์๋ ๋์ ํ์ ์ฌ๋ถ๋ง ์๋ฉด ๋๋ฏ๋ก ์ ๋ณด ๋ ธ์ถ์ด ์ต์ํ๋ฉ๋๋ค.Also applies to: 20-20
25-43: LGTM!
WeeklyRankingMV์MonthlyRankingMV๋ก๋ถํฐRankingInfo๋ฅผ ์์ฑํ๋ ํฉํ ๋ฆฌ ๋ฉ์๋๊ฐ ๋ช ํํ๊ณ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค. MV ๊ธฐ๋ฐ ๋ญํน ์กฐํ ํ์ดํ๋ผ์ธ๊ณผ ์ ํตํฉ๋ฉ๋๋ค.modules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMVRepository.java (1)
8-12: LGTM!Spring Data JPA ๊ท์น์ ์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.
findByBaseDateOrderByCurrentRankAsc๋ฉ์๋๊ฐ ์์๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ค๋ฆ์ฐจ์์ผ๋ก ์ ๋ ฌํฉ๋๋ค.์ฐธ๊ณ : ํ์ฌ
List๋ฐํ ํ์ ์ ํ์ด์ง๋ค์ด์ ๋ฉํ๋ฐ์ดํฐ(์ด ๊ฐ์ ๋ฑ)๊ฐ ํ์ ์๋ ๊ฒฝ์ฐ ์ ํฉํฉ๋๋ค. ๋์ค์ ๋ฉํ๋ฐ์ดํฐ๊ฐ ํ์ํ๋ฉดPage<MonthlyRankingMV>๋ก ๋ณ๊ฒฝ์ ๊ณ ๋ คํ์ธ์.modules/jpa/src/main/java/com/loopers/domain/rank/monthly/MonthlyRankingMV.java (1)
17-29: ์ํฐํฐ ๊ตฌ์กฐ๋ ์ ์ ํฉ๋๋ค.MV ์ํฐํฐ๋ก์ ํ์ํ ํ๋๋ค์ด ์ ์ ์๋์ด ์์ต๋๋ค. ์ฑ๋ฅ ์ต์ ํ๊ฐ ํ์ํ ๊ฒฝ์ฐ,
baseDate์currentRank์ปฌ๋ผ์ ๋ณตํฉ ์ธ๋ฑ์ค ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํด๋ณด์ธ์ (์ฟผ๋ฆฌ ํจํด:findByBaseDateOrderByCurrentRankAsc).modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingWork.java (1)
12-29: LGTM!๋ฐฐ์น ์์ ์ ์ํ ์์ ํ ์ด๋ธ ์ํฐํฐ๊ฐ ์ ๊ตฌํ๋์์ต๋๋ค.
RankingPrepareTasklet์์deleteAllInBatch()๋ก ์ ๋ฆฌํ ํ ๋ฐ์ดํฐ๋ฅผ ์ ์ฌํ๋ ํ๋ฆ์ด๋ฏ๋ก ์ค๋ณต ๋ฐฉ์ง ๋ก์ง์ด ์ ์ ํ ์ฒ๋ฆฌ๋ฉ๋๋ค.์ฐธ๊ณ :
productId์ unique ์ ์ฝ์กฐ๊ฑด์ ์์ง๋ง, ๋ฐฐ์น ์์ ์ ํ ์ด๋ธ์ ๋น์ฐ๋ ํ์ฌ ์ค๊ณ์์๋ ๋ฌธ์ ์์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java (2)
41-50: LGTM!Job ๊ตฌ์ฑ์ด ์ ์ค๊ณ๋์์ต๋๋ค.
RunIdIncrementer๋ก ์ฌ์คํ์ ์ง์ํ๊ณ , 3๋จ๊ณ(prepare โ calculation โ tableSwap) ํ๋ฆ์ด ๋ช ํํฉ๋๋ค.JobListener๋ก ๋ชจ๋ํฐ๋ง๋ ๊ฐ๋ฅํฉ๋๋ค.
59-67: ์ฒญํฌ ์ฒ๋ฆฌ ๊ตฌ์ฑ์ด ์ ์ ํฉ๋๋ค.์ฒญํฌ ํฌ๊ธฐ 100์ ํฉ๋ฆฌ์ ์ธ ์ ํ์ ๋๋ค.
WeeklyRankingProcessor์์ ์์ 100๊ฐ๋ง ์ฒ๋ฆฌํ๋ ๋ก์ง๊ณผ ์ ๋ง์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/RankingPrepareTasklet.java (1)
12-23: LGTM!
deleteAllInBatch()๋ฅผ ์ฌ์ฉํ ํจ์จ์ ์ธ ํ ์ด๋ธ ์ ๋ฆฌ ๊ตฌํ์ ๋๋ค. Weekly์ Monthly ์์ ๋ชจ๋์์ ์ฌ์ฌ์ฉ๋๋ ์ค๊ณ๊ฐ ์ข์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/infrastructure/rank/RankingService.java (1)
6-15: LGTM! ์ธํฐํ์ด์ค ์ถ์ถ์ด ์ ๋์์ต๋๋ค.๊ตฌ์ฒด ํด๋์ค์์ ์ธํฐํ์ด์ค๋ก ์ ํํ์ฌ ๊ด์ฌ์ฌ ๋ถ๋ฆฌ๊ฐ ๋ช ํํด์ก์ต๋๋ค. ์ผ๊ฐ(Redis), ์ฃผ๊ฐ/์๊ฐ(MV) ๋ญํน์ ๋ํ ๋ฉ์๋ ์๊ทธ๋์ฒ๊ฐ ์ ์ ํ๊ฒ ์ ์๋์ด ์์ต๋๋ค.
modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.java (1)
29-31: ์ด๊ธฐํ ์ updatedAt์ด null๋ก ๋จ์ต๋๋ค์์ฑ์์์
updatedAt์ ์ด๊ธฐํํ์ง ์์, ์ฒซ ๋ฒ์งธupdateLikeCountํธ์ถ ์ ๊น์งupdatedAt์ดnull์ ๋๋ค. ์ด๋ ์๋๋ ๋์์ผ ์ ์์ง๋ง, ๋ฐฐ์น ์์ ์์updatedAt๊ธฐ๋ฐ ์ฟผ๋ฆฌ ์ ์๋ก ์์ฑ๋ ๋ฉํธ๋ฆญ์ด ๋๋ฝ๋ ์ ์์ต๋๋ค.๐ ์ ์๋ ์์
public ProductMetrics(Long productId) { this.productId = productId; + this.updatedAt = LocalDateTime.now(); }apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/monthly/MonthlyRankingTableSwapTasklet.java (1)
28-30: ํธ๋์ญ์ ๋ณดํธ๋ก ์ธํด delete-then-insert ํจํด์ ๋ฐ์ดํฐ ์์ค ์ํ์ ์์์ด Tasklet์
MonthlyRankingJobConfig์์.tasklet(tableSwapTasklet, transactionManager)๋ก ์ค์ ๋์ด ์์ด,execute()๋ฉ์๋ ์ ์ฒด๊ฐ Spring Batch์ ํธ๋์ญ์ ์ผ๋ก ๋ณดํธ๋ฉ๋๋ค(67๋ฒ ๋ผ์ธ). ๋ฐ๋ผ์deleteAllInBatch()์คํ ํsaveAll()์ ์ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด delete๋ ํจ๊ป ๋กค๋ฐฑ๋๋ฏ๋ก ๋ฐ์ดํฐ ์์ค ์ํ์ ์์ต๋๋ค.๋ค๋ง, Redis ์กฐํ ์คํจ(36๋ฒ ๋ผ์ธ) ๋๋ ๋ฐ์ดํฐ ๋ณํ ๊ณผ์ (42-58๋ฒ ๋ผ์ธ)์์์ ์์ธ๋ ์ ์ฒด Step์ ์คํจ์ํค๋ฏ๋ก, ์คํจ ์ฒ๋ฆฌ ์ ๋ต๊ณผ ์๋ฌ ๋ก๊น ์ ํ์ธํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
apps/commerce-batch/src/main/resources/application.yml (5)
1-18: ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ๊ธฐ๋ณธ ์ค์ ์ด ์ ์ ํฉ๋๋ค.
web-application-type: none์ค์ ๊ณผ ๋งค๊ฐ๋ณ์ํ๋job.name์ Spring Batch ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํ๋ฉฐ,initialize-schema: never๋ ํ๋ก๋์ ํ๊ฒฝ์์์ ์๋ ์คํค๋ง ์ด๊ธฐํ๋ฅผ ๋ฐฉ์งํ๋ ์ฌ๋ฐ๋ฅธ ๊ธฐ๋ณธ๊ฐ์ ๋๋ค.
25-32: ๋ก์ปฌ/ํ ์คํธ ํ๊ฒฝ์ ์คํค๋ง ์ด๊ธฐํ ์ ๋ต์ด ์ ์ ํฉ๋๋ค.๋ก์ปฌ ๋ฐ ํ ์คํธ ํ๋กํ์ผ์์
initialize-schema: always๋ก ์ค์ ํ์ฌ ๊ฐ๋ฐ ํ๊ฒฝ์์ ๋ฐฐ์น ๋ฉํ๋ฐ์ดํฐ ํ ์ด๋ธ์ ์๋ ์์ฑํ๋ ๊ฒ์ ์ ์ ํ ๊ตฌ์ฑ์ ๋๋ค.
34-50: ํ๊ฒฝ๋ณ ํ๋กํ์ผ ์ ์ธ์ด ์ฌ๋ฐ๋ฆ ๋๋ค.dev, qa, prd ํ๋กํ์ผ์ด ๋ช ์์ ์ค๋ฒ๋ผ์ด๋ ์์ด ์ ์ธ๋์ด ์์ผ๋ฉฐ, ์ด๋ ๊ธฐ๋ณธ ์ค์ ์ ์์ํ๊ณ ํ๊ฒฝ๋ณ ์ค์ ์ ์ํฌํธ๋ ํ์ผ(jpa.yml, redis.yml ๋ฑ)์์ ๊ด๋ฆฌํ๋ ํจํด์ผ๋ก ์ ์ ํฉ๋๋ค.
52-54: ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ SpringDoc ๋นํ์ฑํ๊ฐ ์ ์ ํฉ๋๋ค.
web-application-type: none๊ณผ ์ผ๊ด๋๊ฒ API ๋ฌธ์๋ฅผ ๋นํ์ฑํํ์ฌ ๋ถํ์ํ ์ค๋ฒํค๋๋ฅผ ์ ๊ฑฐํ ๊ฒ์ ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํ ๊ตฌ์ฑ์ ๋๋ค.
20-23: ํฌ์ค ์๋ํฌ์ธํธ ๋นํ์ฑํ๋ก ์ดํด๋ ๋ถ๋ถ์ ์ฌ๊ฒํ ํด์ฃผ์ธ์.
management.health.defaults.enabled: false๋ ๊ธฐ๋ณธ ํฌ์ค ์งํ(๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋์คํฌ ๊ณต๊ฐ ๋ฑ)๋ฅผ ๋นํ์ฑํํ๋ ๊ฒ์ด๋ฉฐ, ํฌ์ค ์๋ํฌ์ธํธ ์์ฒด๋ฅผ ๋นํ์ฑํํ์ง ์์ต๋๋ค.imported๋
monitoring.yml์์ ๋ค์์ด ๋ช ์์ ์ผ๋ก ์ค์ ๋์ด ์์ต๋๋ค:
management.endpoints.web.exposure.include: [health, prometheus]- ํฌ์ค ์๋ํฌ์ธํธ ๋ ธ์ถmanagement.endpoint.health.probes.enabled: true- ํฌ์ค ํ๋ก๋ธ ํ์ฑํmanagement.health.livenessState.enabled: true,readinessState.enabled: true- ์ฟ ๋ฒ๋คํฐ์ค ํ๋ก๋ธ ํ์ฑํ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ (web-application-type: none)์ ๊ฒฝ์ฐ ๊ธฐ๋ณธ ์งํ๋ฅผ ๋นํ์ฑํํ๋ ๊ฒ์ด ์ ์ ํฉ๋๋ค. ๋ฐฐ์น ์์ ์ ๊ฐํ์ ์ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ๋ก ์ธํ ๋ถํ์ํ ํฌ์ค ์ฒดํฌ ์คํจ๋ฅผ ๋ฐฉ์งํ๋ฉด์๋,
/actuator/health์๋ํฌ์ธํธ(ํฌํธ 8081)๋ ์ฟ ๋ฒ๋คํฐ์ค readiness/liveness probe๋ฅผ ์ํด ์ ์ ์๋ํฉ๋๋ค.Likely an incorrect or invalid review comment.
| env: | ||
| OPENAI_KEY: ${{ secrets.OPENAI_KEY }} | ||
| GITHUB_TOKEN: ${{ secrets.G_TOKEN }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
cd .github/workflows && find . -name "main.yml" -exec cat -n {} \;Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 487
๐ Script executed:
# Check if there are any other references to G_TOKEN or pr-agent in the repo
rg -i "g_token|pr-agent|openai" --type yaml --type md -A 2 -B 2 2>/dev/null || echo "Search completed"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 104
๐ Web query:
Codium-ai pr-agent GitHub action trustworthiness security
๐ก Result:
Short answer: PR-Agent (CodiumโAI / now Qodo) has had real, documented security issues; two vulnerabilities (CVEโ2024โ51355 and CVEโ2024โ51356) were reported and responsible disclosures and fixes were published โ so treat the GitHub Action as potentially risky unless you follow mitigation steps and use patched/pinned images or selfโhost. Sources: independent security research and the project's security notes. [1][2][3]
Key points and what to do
- History: Multiple serious vulnerabilities were published by Kudelski Security in AugโNov 2024 (PoC showed secret exfiltration and repo write risks); CVEs were assigned. The researcher showed many repos using the action could be affected. [1]
- Project response: The PRโAgent repo/project (now Qodo) has published security guidance, release notes and recommends using fixed releases or pinned Docker digests; they also provide a security contact. Confirm you run a version that includes the fixes. [2][3]
- Immediate mitigations (minimum best practice):
- Do not use unpinned uses: avoid using uses: codiumai/pr-agent@main. Pin to a specific, patched release or to the Docker image digest. [2]
- Selfโhost the agent or run the Docker image in your own environment where you control secrets and network egress. [2]
- Limit GitHub Actions permissions and tokens: restrict GITHUB_TOKEN permissions (no write unless absolutely needed) and avoid passing highโprivilege secrets into the action. Use least privilege for workflow tokens. (This is standard Actions hardening and directly mitigates the PoC.) [1][2]
- Upgrade to a version >= the patched release (check project release notes / changelog) and monitor the repo for new advisories. [2][3]
- Scan your repo and workflows for other uses of PRโAgent and rotated any secrets that may have been exposed if you used vulnerable versions. [1]
- When to avoid it: do not install the action on sensitive/private repos or orgs that contain production secrets unless you selfโhost or have confirmed the version is patched and permissions are locked down. [1][2]
Want me to:
- check the exact latest patched release number and a recommended digest right now, or
- scan a workflow file you have to point out risky config?
๐ Script executed:
# Check for other workflow files
find .github/workflows -type f -name "*.yml" -o -name "*.yaml" | head -20
# Check for any documentation about tokens
find . -name "README*" -o -name "SECURITY*" -o -name "CONTRIBUTING*" | xargs ls -la 2>/dev/null || echo "No docs found"
# Check git history for when G_TOKEN was introduced
git log --all --full-history -p -- ".github/workflows/main.yml" 2>/dev/null | head -50 || echo "Git history check skipped"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1682
๐ Web query:
CVE-2024-51355 CVE-2024-51356 pr-agent codium-ai patch fix version
๐ก Result:
CVE-2024-51355 and CVE-2024-51356 affect PRโAgent (CodiumAI / Qodo Merge). The upstream fix was released in PRโAgent v0.26 (and included in later releases). [1][2][3]
Sources:
- Kudelski Security writeup noting the two CVEs and the issue timeline. [1]
- PRโAgent GitHub releases (v0.26 release notes). [2]
- PRโAgent / Qodo security/release guidance referencing the v0.26 fix and later images. [3]
Codium-ai/pr-agent ์ก์ ์ ์๋ ค์ง ๋ณด์ ์ทจ์ฝ์ ์ผ๋ก ์ธํด ๊ธด๊ธ ์กฐ์น๊ฐ ํ์ํฉ๋๋ค.
Codium-ai/pr-agent์์ ๋ฐ๊ฒฌ๋ CVE-2024-51355์ CVE-2024-51356 (๋น๋ฐ ์ ๋ณด ํ์ทจ ๋ฐ ์ ์ฅ์ ์ฐ๊ธฐ ๊ถํ ์ ์ฉ ์ํ)์ ๊ณ ๋ คํ๋ฉด, ํ์ฌ ์ํฌํ๋ก์ฐ ๊ตฌ์ฑ์ด ์ํํฉ๋๋ค:
@main์ฌ์ฉ ์ค๋จ: ํน์ ๋ฒ์ (v0.26 ์ด์์ ํจ์น๋ ๋ฆด๋ฆฌ์ค)์ผ๋ก ๊ณ ์ ํ์ธ์. ํ์ฌ@main์ ๋ฏธํจ์น ๋ฒ์ ์ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.permissions๋ธ๋ก ์ถ๊ฐ: PR ์ฝ๋ฉํธ ์์ฑ๋ง ํ์ํ๋ฉด ์ต์ ๊ถํ์ผ๋ก ์ ํํ์ธ์:permissions: pull-requests: write contents: read
- OPENAI_KEY ๋ ธ์ถ: ์ด ์ก์ ์ ๋ฏผ๊ฐํ API ํค๋ฅผ ์ ๋ฌํ์ง ์๋๋ก ์ฃผ์ํ์ธ์. ๊ฐ๋ฅํ๋ฉด ํ๊ฒฝ๋ณ๋ก ๊ฒฉ๋ฆฌ๋ ๊ตฌ์ฑ ์ฌ์ฉ์ ๊ฒํ ํ์ธ์.
๐ค Prompt for AI Agents
In .github/workflows/main.yml around lines 11 to 13, the workflow currently uses
the codium-ai/pr-agent action via @main and exposes OPENAI_KEY to the action,
which is vulnerable (CVE-2024-51355/51356); update the action to a specific
patched release (pin to a fixed tag or commit >= v0.26) instead of @main, add a
minimal permissions block restricting rights to only whatโs needed (e.g.,
pull-requests: write and contents: read), and remove or stop passing OPENAI_KEY
into this action (use an isolated, scoped secret or omit the secret for this
job) so the action cannot access sensitive API keys.
| // Resilience4j (Spring Boot 3.x ๊ธฐ์ค) | ||
| implementation("io.github.resilience4j:resilience4j-spring-boot3") | ||
|
|
||
| // AOP | ||
| implementation("org.springframework.boot:spring-boot-starter-aop") | ||
|
|
||
| // actuator | ||
| implementation("org.springframework.boot:spring-boot-starter-actuator") | ||
|
|
||
| //Micrometer Prometheus | ||
| implementation("io.micrometer:micrometer-registry-prometheus") | ||
|
|
||
| //Spring Cloud OpenFeign | ||
| implementation("org.springframework.cloud:spring-cloud-starter-openfeign") | ||
|
|
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.
์ค๋ณต๋ ์์กด์ฑ ์ ์ธ์ ์ ๊ฑฐํ์ธ์.
๋ค์ ์์กด์ฑ๋ค์ด ์ด๋ฏธ ํ์ผ ์๋จ์ ์ ์ธ๋์ด ์์ต๋๋ค:
resilience4j-spring-boot3(17๋ฒ ๋ผ์ธ)spring-boot-starter-aop(20๋ฒ ๋ผ์ธ)spring-boot-starter-actuator(13๋ฒ ๋ผ์ธ)micrometer-registry-prometheus(23๋ฒ ๋ผ์ธ)spring-cloud-starter-openfeign(26๋ฒ ๋ผ์ธ)
Gradle์ ์ค๋ณต ์ ์ธ์ ๋ฌด์ํ์ง๋ง, ์ ์ง๋ณด์ ๋ถ๋ด๊ณผ ํผ๋์ ์ผ๊ธฐํ ์ ์์ต๋๋ค.
๐ ์ค๋ณต ์ ๊ฑฐ ์ ์
-
- // Resilience4j (Spring Boot 3.x ๊ธฐ์ค)
- implementation("io.github.resilience4j:resilience4j-spring-boot3")
-
- // AOP
- implementation("org.springframework.boot:spring-boot-starter-aop")
-
- // actuator
- implementation("org.springframework.boot:spring-boot-starter-actuator")
-
- //Micrometer Prometheus
- implementation("io.micrometer:micrometer-registry-prometheus")
-
- //Spring Cloud OpenFeign
- implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
-๐ 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.
| // Resilience4j (Spring Boot 3.x ๊ธฐ์ค) | |
| implementation("io.github.resilience4j:resilience4j-spring-boot3") | |
| // AOP | |
| implementation("org.springframework.boot:spring-boot-starter-aop") | |
| // actuator | |
| implementation("org.springframework.boot:spring-boot-starter-actuator") | |
| //Micrometer Prometheus | |
| implementation("io.micrometer:micrometer-registry-prometheus") | |
| //Spring Cloud OpenFeign | |
| implementation("org.springframework.cloud:spring-cloud-starter-openfeign") |
๐ค Prompt for AI Agents
In apps/commerce-api/build.gradle.kts around lines 38 to 52, the block
re-declares dependencies that are already defined earlier in the file
(resilience4j-spring-boot3 at line 17, spring-boot-starter-aop at line 20,
spring-boot-starter-actuator at line 13, micrometer-registry-prometheus at line
23, and spring-cloud-starter-openfeign at line 26); remove these duplicate
implementation(...) entries from lines 38โ52 so each dependency is declared only
once, keeping the original declarations and deleting the repeated ones to avoid
redundancy and maintenance confusion.
| @Bean | ||
| @StepScope | ||
| public JpaPagingItemReader<ProductMetrics> monthlyRankingReader( | ||
| @Value("#{jobParameters['startDate']}") String startDate | ||
| ) { | ||
| return new JpaPagingItemReaderBuilder<ProductMetrics>() | ||
| .name("monthlyRankingReader") | ||
| .entityManagerFactory(emf) | ||
| .queryString("SELECT m FROM ProductMetrics m WHERE m.updatedAt >= :startDate") | ||
| .parameterValues(Map.of("startDate", LocalDateTime.parse(startDate))) | ||
| .pageSize(100) | ||
| .build(); | ||
| } |
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.
startDate ํ๋ผ๋ฏธํฐ null ์ฒดํฌ ๋ฐ ํ์ฑ ์์ธ ์ฒ๋ฆฌ ํ์
startDate Job ํ๋ผ๋ฏธํฐ๊ฐ null์ด๊ฑฐ๋ ์๋ชป๋ ํ์์ผ ๊ฒฝ์ฐ DateTimeParseException์ด ๋ฐ์ํฉ๋๋ค. DemoTasklet.java์ ํจํด์ฒ๋ผ null ๊ฒ์ฆ์ ์ถ๊ฐํ๊ฑฐ๋, ํ์ฑ ์คํจ ์ ๋ช
ํํ ์๋ฌ ๋ฉ์์ง๋ฅผ ์ ๊ณตํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๐ ์ ์๋ ์์
@Bean
@StepScope
public JpaPagingItemReader<ProductMetrics> monthlyRankingReader(
@Value("#{jobParameters['startDate']}") String startDate
) {
+ if (startDate == null || startDate.isBlank()) {
+ throw new IllegalArgumentException("startDate job parameter is required");
+ }
+ LocalDateTime parsedDate;
+ try {
+ parsedDate = LocalDateTime.parse(startDate);
+ } catch (DateTimeParseException e) {
+ throw new IllegalArgumentException("Invalid startDate format: " + startDate, e);
+ }
return new JpaPagingItemReaderBuilder<ProductMetrics>()
.name("monthlyRankingReader")
.entityManagerFactory(emf)
.queryString("SELECT m FROM ProductMetrics m WHERE m.updatedAt >= :startDate")
- .parameterValues(Map.of("startDate", LocalDateTime.parse(startDate)))
+ .parameterValues(Map.of("startDate", parsedDate))
.pageSize(100)
.build();
}๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/RankingChunkConfig.java
around lines 52-64, the job parameter startDate is used directly and can be null
or malformatted causing DateTimeParseException; add a null/blank check for
startDate and parse it inside a try/catch, throwing a clear
IllegalArgumentException (or JobParametersInvalidException) if missing or
unparsable, then pass the successfully parsed LocalDateTime into the reader's
parameterValues instead of parsing inline.
|
|
||
| @Scheduled(cron = "0 0 2 * * MON") | ||
| public void runWeeklyRankingJob() { | ||
| String requestDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
# First, examine the RankingScheduler.java file around lines 29 and 44-45
cat -n apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/scheduler/RankingScheduler.java | sed -n '20,55p'Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1694
๐ Script executed:
# Check for WeeklyRankingJobConfig and MonthlyRankingJobConfig
fd -t f "RankingJobConfig" apps/commerce-batch/src/main/java/Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 272
๐ Script executed:
# Check how requestDate and startDate parameters are used in config files
rg -n -C5 'requestDate|startDate' apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/config/ 2>/dev/null || echo "Pattern search in config directory"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 122
๐ Script executed:
# Also search more broadly for parameter handling
rg -n -B3 -A3 'requestDate|startDate' apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/ --type javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 7724
์ผ๊ด๋์ง ์์ ๋ ์ง ํฌ๋งท ์์ ํ์
requestDate๋ "yyyyMMdd" ํฌ๋งท์ผ๋ก ์ ๋ฌ๋์ง๋ง, MonthlyRankingTableSwapTasklet์์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ค์ ๋ "2026-01" (YYYY-MM ํฌ๋งท)๊ณผ ์ผ์นํ์ง ์์ต๋๋ค. ๋ํ startDate๋ ISO_LOCAL_DATE_TIME ํฌ๋งท์ ์ฌ์ฉํ๋ฏ๋ก, ๋ ํ๋ผ๋ฏธํฐ์ ํฌ๋งท์ ํต์ผํ๊ฑฐ๋ ๊ฐ ์ฌ์ฉ ์์น์์ ๋ช
์์ ์ผ๋ก ํ์ฑํ์ธ์.
| .map(work -> "product:snapshot:" + work.getProductId()) | ||
| .toList(); | ||
|
|
||
| List<Object> snapshots = redisTemplate.opsForValue().multiGet(keys); |
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.
multiGet ๊ฒฐ๊ณผ๊ฐ null์ผ ๊ฒฝ์ฐ NPE ๋ฐ์ ๊ฐ๋ฅ
Redis ์ฐ๊ฒฐ ์คํจ ๋๋ ๊ธฐํ ์ค๋ฅ ๋ฐ์ ์ multiGet์ด null์ ๋ฐํํ ์ ์์ต๋๋ค. ์ด ๊ฒฝ์ฐ Line 45์์ snapshots.get(i) ํธ์ถ ์ NullPointerException์ด ๋ฐ์ํฉ๋๋ค.
๐ ์์ ์ ์
List<Object> snapshots = redisTemplate.opsForValue().multiGet(keys);
+ if (snapshots == null) {
+ snapshots = java.util.Collections.nCopies(keys.size(), null);
+ }๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/step/monthly/MonthlyRankingTableSwapTasklet.java
around line 36, multiGet can return null (e.g., on Redis failure) causing a
NullPointerException when calling snapshots.get(i); change the code to
null-check the returned List before using it (if null, log an error and either
treat as Collections.emptyList() or fail the step with a clear exception), and
also guard individual snapshot elements when iterating (skip or handle null
entries) so that accessing snapshots.get(i) never throws an NPE.
| @EnableScheduling | ||
| @ConfigurationPropertiesScan | ||
| @SpringBootApplication | ||
| public class CommerceBatchApplication { | ||
|
|
||
| @PostConstruct | ||
| public void started() { | ||
| // set timezone | ||
| TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); | ||
| } | ||
|
|
||
| public static void main(String[] args) { | ||
| int exitCode = SpringApplication.exit(SpringApplication.run(CommerceBatchApplication.class, args)); | ||
| System.exit(exitCode); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
#!/bin/bash
# commerce-batch ๋ชจ๋์์ @Scheduled ๋ฉ์๋ ๊ฒ์
rg -n --type=java '@Scheduled' apps/commerce-batch/Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 357
๐ Script executed:
#!/bin/bash
# commerce-batch ๋ชจ๋์ ์ ์ฒด Java ํ์ผ ๋ชฉ๋ก ํ์ธ
find apps/commerce-batch/src -name "*.java" -type f | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1505
๐ Script executed:
#!/bin/bash
# commerce-batch ๋ชจ๋ ๊ตฌ์กฐ ํ์ธ
ls -la apps/commerce-batch/src/main/java/com/loopers/Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 328
@EnableScheduling๊ณผ System.exit() ํจํด์ ์ค๊ณ ๋ชจ์์ ์ฆ์ ํด๊ฒฐํ์ธ์.
RankingScheduler์ @Scheduled(cron = "0 0 2 * * MON")๊ณผ @Scheduled(cron = "0 0 3 1 * *")๋ก ์ ์๋ ์ฃผ๊ธฐ์ ์ค์ผ์ค ์์
์ด ์กด์ฌํฉ๋๋ค. ๊ทธ๋ฌ๋ CommerceBatchApplication์ main() ๋ฉ์๋์์ System.exit(exitCode)๋ฅผ ํธ์ถํ๋ฉด Spring Boot ์ ํ๋ฆฌ์ผ์ด์
์ด ์์ ํ ์ฆ์ ์ข
๋ฃ๋์ด, ์ค์ผ์ค๋ ์์
๋ค์ด ์คํ๋ ์ ์์ต๋๋ค.
๋ฌธ์ :
@EnableScheduling์ ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ฅ๊ธฐ ์คํ๋์ด์ผ ํจ์ ์๋ฏธSystem.exit(exitCode)๋ JVM์ ์ฆ์ ์ข ๋ฃ์ํด- ๊ฒฐ๊ณผ: ์ค์ผ์ค๋ ์์ ์ด ์ ๊ธฐ์ ์ผ๋ก ์คํ๋์ง ์์
ํด๊ฒฐ ๋ฐฉ์:
main()๋ฉ์๋์์System.exit()์ ๊ฑฐ โ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ณ์ ์คํ ์ํ๋ก ์ ์ง- ๋๋ ๋ค๋ฅธ ์ข ๋ฃ ๋ฉ์ปค๋์ฆ(์: shutdown hook, graceful shutdown) ์ ์ฉ
๐ค Prompt for AI Agents
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java
around lines 11-25: the main method calls System.exit(...) which immediately
terminates the JVM and prevents @EnableScheduling cron jobs (e.g.,
RankingScheduler) from running; remove the System.exit call (and avoid forcing
SpringApplication.exit on startup) so the application remains running to execute
scheduled tasksโif you need to handle shutdown/exit codes, implement a proper
shutdown hook or call SpringApplication.exit only on explicit termination events
rather than at startup.
| @DisplayName("jobParameter ์ค requestDate ์ธ์๊ฐ ์ฃผ์ด์ง์ง ์์์ ๋, demoJob ๋ฐฐ์น๋ ์คํจํ๋ค.") | ||
| @Test | ||
| void shouldNotSaveCategories_whenApiError() throws Exception { | ||
| // arrange | ||
| jobLauncherTestUtils.setJob(job); | ||
|
|
||
| // act | ||
| var jobExecution = jobLauncherTestUtils.launchJob(); | ||
|
|
||
| // assert | ||
| assertAll( | ||
| () -> assertThat(jobExecution).isNotNull(), | ||
| () -> assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode()) | ||
| ); | ||
| } |
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.
ํ ์คํธ ๋ฉ์๋ ์ด๋ฆ์ด ํ ์คํธ ๋ด์ฉ๊ณผ ์ผ์นํ์ง ์์ต๋๋ค.
@DisplayName์ "requestDate ์ธ์๊ฐ ์ฃผ์ด์ง์ง ์์์ ๋" ์คํจ๋ฅผ ํ
์คํธํ๋ค๊ณ ๋ช
์ํ์ง๋ง, ๋ฉ์๋ ์ด๋ฆ shouldNotSaveCategories_whenApiError๋ API ์ค๋ฅ์ ์นดํ
๊ณ ๋ฆฌ ์ ์ฅ์ ์ธ๊ธํฉ๋๋ค. ํ
์คํธ ๋ฉ์๋ ์ด๋ฆ์ด ์ค์ ๋์์ ๋ฐ์ํ๋๋ก ๋ณ๊ฒฝํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
๐ ์ ์ํ๋ ๋ฉ์๋ ์ด๋ฆ ๋ณ๊ฒฝ
- void shouldNotSaveCategories_whenApiError() throws Exception {
+ void shouldFail_whenRequestDateParameterIsMissing() throws Exception {๐ค Prompt for AI Agents
In apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java
around lines 42 to 56, the test method name should reflect the scenario
described by the @DisplayName (failure when requestDate job parameter is
missing); rename the method from shouldNotSaveCategories_whenApiError to a name
that clearly indicates the missing requestDate causes the job to fail (e.g.,
shouldFailWhenRequestDateIsMissing or whenRequestDateIsMissing_thenJobFails) and
update any references/imports if necessary.
| metrics.updateProductSnapshot( | ||
| event.productName(), | ||
| event.price(), | ||
| event.currentStock() | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
find . -type f -name "ProductMetricsService.java" | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 180
๐ Script executed:
find . -type f -name "ProductMetrics.java" | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 240
๐ Script executed:
find . -type f -name "*ProductStockEvent*" | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 158
๐ Script executed:
cat -n ./apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3197
๐ Script executed:
cat -n ./apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1501
๐ Script executed:
cat -n ./modules/kafka/src/main/java/com/loopers/event/ProductStockEvent.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 877
๐ Script executed:
cat -n ./modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1881
๐ Script executed:
rg -A 5 -B 5 "ProductStockEvent\(" --type java | head -100Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2088
๐ Script executed:
rg "enrichment|Enrich" -A 3 -B 3 --type java | head -100Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
๐ Script executed:
rg "ProductStockEvent\.of|new ProductStockEvent" -B 5 -A 5 --type java | head -150Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 4042
๐ Script executed:
cat -n ./apps/commerce-streamer/src/test/java/com/loopers/domain/metrics/IdempotencyIntegrationTest.java | head -80Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2478
๐ Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderSalesAggregateListener.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1549
productName null ์์ ์ฑ ๊ฒ์ฆ ํ์
ProductMetrics.updateProductSnapshot() ๋ฉ์๋(modules/jpa)๊ฐ productName ๋งค๊ฐ๋ณ์์ ๋ํ null ์ฒดํฌ๋ฅผ ์ํํ์ง ์์ต๋๋ค. ํ์ฌ ๊ตฌํ์์๋ null ๊ฐ์ด ์ง์ ํ๋์ ํ ๋น๋์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋ ์ ์์ต๋๋ค.
ProductStockEvent์ productName์ด null์ผ ์ ์๋ ๊ฒฝ์ฐ๋ฅผ ๋๋นํด, updateProductSnapshot ๋ฉ์๋์ null ์ฒดํฌ๋ฅผ ์ถ๊ฐํ๊ฑฐ๋ ํธ์ถ ์ ์ ๊ฒ์ฆํ๊ธฐ๋ฅผ ๊ถ์ฅํฉ๋๋ค.
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java
around lines 54-58, the call to
metrics.updateProductSnapshot(event.productName(), event.price(),
event.currentStock()) can pass a null productName and the downstream method does
not validate it; add a null-safety guard: either validate event.productName()
before calling (log and return/skip processing for null) or update
ProductMetrics.updateProductSnapshot to perform
Objects.requireNonNull(productName, "...") or a null check that throws
IllegalArgumentException or skips persistence so null never gets written to the
DB; ensure the chosen approach logs context (event id) and prevents saving a
null productName.
| public void incrementViewCount() { | ||
| this.viewCount += 1; | ||
| this.updatedAt = LocalDateTime.now(); | ||
| } | ||
|
|
||
| public void addSalesCount(int quantity) { | ||
| this.salesCount += quantity; | ||
| 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.
๐งฉ Analysis chain
๐ Script executed:
# Find ProductMetrics usage patterns
rg "ProductMetrics" --type java -B 2 -A 2 | head -100Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 11143
๐ Script executed:
# Check for @Version or lock-related annotations in ProductMetrics file
cat modules/jpa/src/main/java/com/loopers/domain/ProductMetrics.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1482
๐ Script executed:
# Search for repository/service methods that call incrementViewCount or addSalesCount
rg "incrementViewCount|addSalesCount" --type java -B 3 -A 3Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 4036
๐ Script executed:
# Check repository patterns for ProductMetrics updates
rg "ProductMetricsRepository|productMetrics.*save|productMetrics.*update" --type java -B 2 -A 5Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 4824
๐ Script executed:
# Get the full ProductMetricsService implementation
cat apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2623
๐ Script executed:
# Check for custom repository methods with lock annotations
rg "findById|findByProductId|Lock|@Query" apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricsRepository.java -A 3Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
๐ Script executed:
# Check the IdempotencyIntegrationTest to understand concurrency testing
head -150 apps/commerce-streamer/src/test/java/com/loopers/domain/metrics/IdempotencyIntegrationTest.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2030
์ด๋ฒคํธ ์ค๋ณต์ ๋ฉฑ๋ฑ์ฑ์ผ๋ก ๋ฐฉ์ง๋์ง๋ง, ๋์ ์ด๋ฒคํธ์์ ์์ค ๊ฐ๋ฅ์ฑ ์กด์ฌ
ํ์ฌ ๊ตฌํ์ EventHandled ํ
์ด๋ธ์ ํตํด ์ด๋ฒคํธ ID ์ค๋ณต์ ๊ฐ์งํ๊ณ ์ค๋ณต ์ฒ๋ฆฌ๋ฅผ ๋ฐฉ์งํ๋ฏ๋ก Kafka ์ฌ์ ์ก ์๋๋ฆฌ์ค๋ ์์ ํฉ๋๋ค. ๋ค๋ง ์๋ก ๋ค๋ฅธ ์ด๋ฒคํธ๊ฐ ๋์ผ ์ํ์ ๋์์ ์
๋ฐ์ดํธํ ๊ฒฝ์ฐ findById().save() ํจํด์ผ๋ก๋ ์์ค์ด ๋ฐ์ํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด ์กฐํ ์ด๋ฒคํธ์ ํ๋งค ์ด๋ฒคํธ๊ฐ ๋์์ ์ฒ๋ฆฌ๋๋ฉด, ๋จผ์ ๋ก๋๋ ์ํฐํฐ์ ๋ณ๊ฒฝ์ฌํญ์ด ๋ฎ์ด์์์ง ์ ์์ต๋๋ค.
๋ฉํธ๋ฆญ ๋ฐ์ดํฐ์ ์ต์ข
์ผ๊ด์ฑ์ด ์์ฉ ๊ฐ๋ฅํ๋ค๋ฉด ํ์ฌ ์ค๊ณ๋ก ์ถฉ๋ถํ์ง๋ง, ์ ํ์ฑ์ด ์ค์ํ๋ค๋ฉด @Version ํ๋๋ฅผ ์ถ๊ฐํ์ฌ ๋๊ด์ ๋ฝ์ ์ ์ฉํ๊ฑฐ๋, ์์์ ์
๋ฐ์ดํธ ์ฟผ๋ฆฌ(์: UPDATE product_metrics SET view_count = view_count + 1 WHERE product_id = ?)๋ฅผ ๊ณ ๋ คํ์ธ์.
| // ์ ์ ํฉํ ๋ฆฌ ๋ฉ์๋ (์๋ฏธ ์๋ ์์ฑ ๋ฐฉ์ ์ ๊ณต) | ||
| public static WeeklyRankingMV createFromWork(WeeklyRankingWork work, String baseDate) { | ||
| return new WeeklyRankingMV( | ||
| baseDate, | ||
| work.getProductId(), | ||
| work.getScore(), | ||
| work.getRanking(), | ||
| "์ํ๋ช ์์", // ์ค์ ๊ตฌํ ์ Product ์ ๋ณด ๊ฒฐํฉ ํ์ | ||
| 0L, // ์ค์ ๊ตฌํ ์ Product ์ ๋ณด ๊ฒฐํฉ ํ์ | ||
| false // ์ค์ ๊ตฌํ ์ Product ์ ๋ณด ๊ฒฐํฉ ํ์ | ||
| ); | ||
| } |
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.
์ํ ์ ๋ณด ํ๋ ์ด์คํ๋ ๊ตฌํ ์๋ฃ ํ์
createFromWork ๋ฉ์๋์์ productName, price, isSoldOut์ด ํ๋์ฝ๋ฉ๋ ์์ ๊ฐ์ ์ฌ์ฉํฉ๋๋ค. MV์ ์๋ชป๋ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋๋ฉด API ์๋ต ํ์ง์ ์ํฅ์ ์ค๋๋ค. ProductSnapshot ํด๋์ค๊ฐ ์ด๋ฏธ ์กด์ฌํ๋ฏ๋ก(MonthlyRankingMV ํจํค์ง ์ฐธ์กฐ), ์ด๋ฅผ ํ์ฉํ์ฌ ์ค์ ์ํ ์ ๋ณด๋ฅผ ์ฃผ์
ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
์ด ๊ธฐ๋ฅ์ ์๋ฃํ๊ธฐ ์ํ ๊ตฌํ์ ์์ฑํด ๋๋ฆด๊น์, ์๋๋ฉด ์ถ์ ์ ์ํ ์ ์ด์๋ฅผ ์ด์ด ๋๋ฆด๊น์?
๐ค Prompt for AI Agents
In modules/jpa/src/main/java/com/loopers/domain/rank/weekly/WeeklyRankingMV.java
around lines 41 to 52, the createFromWork method currently uses hardcoded
placeholders for productName, price, and isSoldOut; replace these placeholders
by obtaining a ProductSnapshot and populating those fields. Specifically, update
createFromWork to (1) attempt to get a ProductSnapshot from the
WeeklyRankingWork (if the work already carries a snapshot) or, if not present,
fetch the snapshot via the appropriate ProductRepository/ProductService method,
(2) extract productName, price, and isSoldOut from that snapshot and pass them
into the WeeklyRankingMV constructor, and (3) handle null/missing snapshot
defensively (log a warning and use sensible defaults) so the method no longer
stores hardcoded placeholder values.
๐ Summary
Spring Batch๋ฅผ ํ์ฉํ์ฌ ์ฃผ๊ฐ/์๊ฐ ์ํ ๋ญํน ์์คํ ์ ๊ตฌ์ถํ์ต๋๋ค. ์ค์๊ฐ ์ง๊ณ ์ ๋ณด(
ProductMetrics)๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ญํน ์ ์๋ฅผ ์ฐ์ถํ๊ณ , Materialized View(MV) ๊ตฌ์กฐ๋ฅผ ์ค๊ณ ๋ฐ ์ ์ฉํ์ต๋๋ค.mv_product_rank_weekly/monthly) ์ค๊ณ ๋ฐ ๋ฐ์ดํฐ ์ ์ฌ ํ๋ก์ธ์ค ๊ตฌํRankingFacade๊ฐ์๐ฌ Review Points
(1) ๋๋ฉ์ธ ๊ฒฉ๋ฆฌ๋ฅผ ๊ณ ๋ คํ ๋ฐ์ดํฐ ํ๋ฆ ์ค๊ณ
commerce-batch๋ชจ๋์ดcommerce-api์๋ง ์กด์ฌํ๋Product๋๋ฉ์ธ(DB, Service)์ ์ง์ ์์กดํ์ง ์๋๋ก ์ค๊ณํ๋ ๋ฐ ๋ง์ ๊ณ ๋ฏผ์ ํ์ต๋๋ค. ์ด๋ฅผ ์ํด ์ฃผ๋ฌธ ๋ฐ์ ์ ๋ฐํ๋๋ProductStockEvent์ ์ํ๋ช ๊ณผ ๊ฐ๊ฒฉ ์ ๋ณด๋ฅผ ํฌํจํ๋ Event Enrichment ํจํด์ ์ ์ฉํ์ต๋๋ค.[๊ด๋ จ ์ง๋ฌธ]
ํด๋น ์ค๊ณ ๊ณผ์ ์์ Event Enrichment๋ฅผ ํตํด ๋ฐฐ์น์ ๋ ๋ฆฝ์ฑ์ ํ๋ณดํ์ผ๋, ์ํ๋ช ์ด๋ ๊ฐ๊ฒฉ์ด ๋น๋ฒํ๊ฒ ๋ณ๊ฒฝ๋๋ ๋๋ฉ์ธ์ผ ๊ฒฝ์ฐ ์ด๋ฒคํธ ๋ฉ์์ง์ ํฌ๊ธฐ๊ฐ ์ปค์ง๊ฑฐ๋ ๋ฐ์ดํฐ ๋ถ์ผ์น ๊ธฐ๊ฐ์ด ๊ธธ์ด์ง ์ ์์ ๊ฒ ๊ฐ์๋ฐ ์ค๋ฌด์์๋ ์ด๋ค ๋ฐฉ์์ผ๋ก ์ด๋ฅผ ํด๊ฒฐ์ ํ๋์ง ๊ถ๊ธํฉ๋๋ค.
๋ฆฌ๋ฒ ์ด์ค ๊ณผ์ ์์ ์ด์ ์ฃผ์ฐจ๋ค์ ์ปค๋ฐ๋ค์ด ํฌํจ๋์์ต๋๋ค. ์ด๋ฒ ๊ณผ์ (์ฃผ๊ฐ/์๊ฐ ๋ญํน)์ ์ค์ง์ ์ธ ๋ฆฌ๋ทฐ๋ ์๋ ์ปค๋ฐ๋ถํฐ ํ์ธํด ์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค.
์์ ์ปค๋ฐ: ee16413 ([refactor: ๋์ ๋ญํน ํค ์์ฑ์ ์ํ RankingKeyGenerator ๋ถ๋ฆฌ...])
์ฃผ์ ๊ตฌํ ๋ฒ์: ์ดํ ์งํ๋ ์ฃผ๊ฐ/์๊ฐ ๋ฐฐ์น Job ๊ตฌ์ฑ, MV ์ค๊ณ, API ํ์ฅ ๊ด๋ จ ์ปค๋ฐ๋ค์ ๋๋ค.
โ Checklist
๐งฑ Spring Batch
๐งฉ Ranking API
Summary by CodeRabbit
๋ฆด๋ฆฌ์ค ๋ ธํธ
์๋ก์ด ๊ธฐ๋ฅ
๊ฐ์ ์ฌํญ
๋ฌธ์
โ๏ธ Tip: You can customize this high-level summary in your review settings.