-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-10] Collect, Stack, Zip #234
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: sieun0322
Are you sure you want to change the base?
Conversation
WalkthroughOrder ์๋ณ์ ํ์ ์ Long์์ String์ผ๋ก ๋ณ๊ฒฝํ๊ณ , ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ๋ฅผ ๊ฐ์ ํ๋ฉฐ, ์ ํ ๋ญํน ์์คํ ์ ํ์ฅํ๊ณ , ์๋ก์ด ๋ฐฐ์น ์ฒ๋ฆฌ ๋ชจ๋์ ์ถ๊ฐํฉ๋๋ค. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Order as OrderEventHandler
participant Payment as PaymentService
participant Event as ApplicationEventPublisher
participant Cache as RedisCache
rect rgb(200, 220, 255)
Note over Client,Cache: ์ฃผ๋ฌธ ์์ฑ ๋ฐ ๊ฒฐ์ ์ฒ๋ฆฌ
end
Client->>Order: OrderCreatedEvent ๋ฐํ
activate Order
Order->>Payment: requestPayment(orderId)
activate Payment
Payment->>Event: publishEvent(PaymentSuccessEvent)
deactivate Payment
Event->>Event: ๊ฒฐ์ ์ฑ๊ณต ์ฒ๋ฆฌ
Event->>Event: publishEvent(OrderPaidEvent)
Event->>Cache: updateRankingCache()
deactivate Order
rect rgb(200, 255, 200)
Note over Client,Cache: ๊ฒฐ์ ์คํจ ์๋๋ฆฌ์ค
end
Order->>Payment: processPaymentCallback(orderId, FAILED)
activate Payment
Payment->>Event: publishEvent(PaymentFailureEvent)
deactivate Payment
Order->>Order: cancelPayment(orderId)
Event->>Event: publishEvent(OrderCancelledEvent)
sequenceDiagram
participant Batch as DailyRankingJob
participant DB as Database
participant Cache as Redis
participant Validation as DataValidator
rect rgb(255, 200, 200)
Note over Batch,Cache: ๋ฐฐ์น ์์
- ์ผ์ผ ๋ญํน ์ฒ๋ฆฌ
end
Batch->>DB: TRUNCATE product_metrics_daily_working
Batch->>DB: SELECT daily metrics by date
activate DB
DB-->>Batch: metrics data
deactivate DB
Batch->>Batch: Transform & Process data
Batch->>DB: INSERT INTO product_metrics_daily_working
Batch->>Validation: validateData()
activate Validation
Validation->>DB: COUNT records
Validation-->>Batch: validation result
deactivate Validation
alt ๊ฒ์ฆ ์ฑ๊ณต
Batch->>DB: RENAME daily tables (swap)
Batch->>Cache: invalidatePattern(ranking:daily:*)
else ๊ฒ์ฆ ์คํจ
Batch->>Batch: rollback & error
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: 12
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
๐ Major comments (15)
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java-40-48 (1)
40-48: ์ข์์ ์ทจ์ ์ด๋ฒคํธ์ ๋ฉฑ๋ฑ์ฑ์ ๋ณด์ฅํ์ธ์.
remove()๋ฉ์๋๋ ์ข์์๊ฐ ์ค์ ๋ก ์กด์ฌํ๋์ง ํ์ธํ์ง ์๊ณ ๋ฌด์กฐ๊ฑด UNLIKE ์ด๋ฒคํธ๋ฅผ ๋ฐํํฉ๋๋ค. ๋ง์ฝ ์ข์์๊ฐ ์ด๋ฏธ ์ญ์ ๋์๊ฑฐ๋ ์ ์ด์ ์กด์ฌํ์ง ์์๋ ๊ฒฝ์ฐ์๋ ์ด๋ฒคํธ๊ฐ ๋ฐํ๋์ด ๋ฐฐ์น ์ง๊ณ ์์คํ ์์ ์์ ์นด์ดํธ๋ ๋ถ์ ํํ ์ง๊ณ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
LikeFacade์์ ์บ์ ๊ธฐ๋ฐ์ผ๋ก ์ผ์ฐจ์ ์ธ ์ ์ด๋ฅผ ํ๊ณ ์์ง๋ง, DB ์ํ์ ์บ์ ๊ฐ ๋ถ์ผ์น ์ํฉ์์๋ ์ฌ์ ํ ์ค๋ณต ์ด๋ฒคํธ ๋ฐํ์ด ๊ฐ๋ฅํฉ๋๋ค.๐ ์กด์ฌ ์ฌ๋ถ ํ์ธ ํ ์ด๋ฒคํธ ๋ฐํํ๋ ์์ ์
@Transactional public void remove(Long userId, Long productId) { - + // ์ข์์๊ฐ ์ค์ ๋ก ์กด์ฌํ๋์ง ํ์ธ + Optional<Like> liked = likeRepository.findById(userId, productId); + if (!liked.isPresent()) { + log.debug("์ข์์๊ฐ ์กด์ฌํ์ง ์์ ์ด๋ฒคํธ ๋ฐํ ์๋ต - ์ฌ์ฉ์ID: {}, ์ํID: {}", userId, productId); + return; + } + likeRepository.remove(userId, productId); - + // ์ข์์ ์ทจ์ ์ด๋ฒคํธ ๋ฐํ (๋ฐฐ์น ์ฒ๋ฆฌ์ฉ) LikeEvent unlikeEvent = new LikeEvent(userId, productId, LikeEvent.LikeAction.UNLIKE); eventPublisher.publishEvent(unlikeEvent); log.debug("์ข์์ ์ทจ์ ์ด๋ฒคํธ ๋ฐํ - ์ฌ์ฉ์ID: {}, ์ํID: {}", userId, productId); }argo-workflows/weekly-ranking-workflow.yaml-49-68 (1)
49-68: JAVA_OPTS ํ๊ฒฝ ๋ณ์๊ฐ ์ค์ ๋ก ์ ์ฉ๋์ง ์์ต๋๋ค.
JAVA_OPTSํ๊ฒฝ ๋ณ์๋ฅผ ์ค์ ํ์ง๋ง,java๋ช ๋ น์ด์์ ํด๋น ๊ฐ์ ์ฐธ์กฐํ์ง ์์ JVM ์ต์ ์ด ์ ์ฉ๋์ง ์์ต๋๋ค.๐ ์ ์๋ ์์ ์ฌํญ
command์args๋ฅผ ์์ ํตํด ์คํํ๋๋ก ์์ ํ๊ฑฐ๋, entrypoint ์คํฌ๋ฆฝํธ๋ฅผ ์ฌ์ฉํ์ธ์:container: image: your-registry/commerce-batch:latest - command: ["java"] - args: - - "-jar" - - "commerce-batch.jar" - - "--spring.batch.job.names=dailyRankingDataProcessingJob" - - "--date={{ inputs.parameters.date }}" + command: ["/bin/sh", "-c"] + args: + - | + java $JAVA_OPTS -jar commerce-batch.jar \ + --spring.batch.job.names=dailyRankingDataProcessingJob \ + --date={{ inputs.parameters.date }}๋์ผํ ํจํด์
cache-update-step๊ณผcache-retry-step์๋ ์ ์ฉํ์ธ์.apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java-10-11 (1)
10-11: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ๋ง์ด๊ทธ๋ ์ด์ ์คํฌ๋ฆฝํธ ํ์ธ์ด ํ์ํฉ๋๋ค.Java ์ฝ๋์์ orderId๋ ์ด๋ฏธ ๋ชจ๋ ๊ณ์ธต(Order ์ํฐํฐ, Payment ์ํฐํฐ, ์๋น์ค, API, ์ด๋ฒคํธ)์์ String์ผ๋ก ์ผ๊ด๋๊ฒ ๋ณ๊ฒฝ๋์์ต๋๋ค. ๋ค๋ง ๋ค์์ ํ์ธํด์ผ ํฉ๋๋ค:
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์ ์คํฌ๋ฆฝํธ: Flyway/Liquibase ๋ง์ด๊ทธ๋ ์ด์ ํ์ผ์ด๋ SQL ์คํฌ๋ฆฝํธ์์
ordersํ ์ด๋ธ๊ณผcommerce_paymentsํ ์ด๋ธ์ orderId/order_id ์ปฌ๋ผ์ Long/BIGINT์์ VARCHAR๋ก ๋ณ๊ฒฝํ๋ ๋ง์ด๊ทธ๋ ์ด์ ์ด ์คํ๋์๋์ง ํ์ธํ์ธ์.- ๊ธฐ์กด ๋ฐ์ดํฐ: ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ด๋ฏธ ์กด์ฌํ๋ ์ฃผ๋ฌธ ๋ฐ์ดํฐ์ ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ต์ด ์ ์๋์ด ์๋์ง ํ์ธํ์ธ์.
- ์ธ๋ฑ์ค: orderId ์ปฌ๋ผ์ unique ์ธ๋ฑ์ค๊ฐ ์คํค๋ง ๋ณ๊ฒฝ ํ ์ฌ๋ฐ๋ฅด๊ฒ ์ ์ง๋์ด ์๋์ง ํ์ธํ์ธ์.
์ด๋ฒคํธ ์ง๋ ฌํ, API ํธํ์ฑ, ์ฑ๋ฅ์ ์ฝ๋ ์์ค์์ ์ด๋ฏธ String ํ์ ์ผ๋ก ์ฒ๋ฆฌ๋์ด ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java-56-58 (1)
56-58:generateOrderId()๋ฉ์๋๋ ์ถฉ๋ ์ํ์ด ์๋ ID ์์ฑ ์ ๋ต์ ์ฌ์ฉํฉ๋๋ค.UUID์ ์ฒซ 8์๋ง ์ฌ์ฉํ๋ฉด 32๋นํธ ์ํธ๋กํผ๋ง ์ ๊ณต๋์ด, ์ฝ 65,000๊ฐ ์ฃผ๋ฌธ์์ ์ถฉ๋ ํ๋ฅ ์ด ์ฆ๊ฐํฉ๋๋ค. ์ถฉ๋ ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ๋ํฌ ์ ์ฝ์กฐ๊ฑด์ ์ํด
DataIntegrityViolationException์ด ๋ฐ์ํ์ง๋ง, ํ์ฌ ์ฝ๋์๋ ์ด๋ฅผ ์ฒ๋ฆฌํ๋ ๋ก์ง์ด ์์ด ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋ฉ๋๋ค.๋ค์ ์ต์ ์ ๊ณ ๋ คํ์ธ์:
- ULIDGenerator ์ฌ์ฉ (์ด๋ฏธ ์ฝ๋๋ฒ ์ด์ค์ ๊ตฌํ๋จ): ํ์์คํฌํ ๊ธฐ๋ฐ์ผ๋ก ์ถฉ๋ ์ํ์ ๊ฑฐ์ ์ ๊ฑฐ
- ๋ ๊ธด ID ๊ธธ์ด (์: 12-16์)
CouponIssue์ฒ๋ผ ์ ์ฒด UUID ์ฌ์ฉapps/commerce-batch/src/main/java/com/loopers/batch/job/DailyRankingJobConfig.java-229-248 (1)
229-248: ํ ์ด๋ธ ์ค์ ํ Working ํ ์ด๋ธ ์ฌ์์ฑ ๋๋ฝํ ์ด๋ธ ์ค์ ํ
product_metrics_daily_workingํ ์ด๋ธ์ด ์กด์ฌํ์ง ์๊ฒ ๋์ด, ๋ค์ Job ์คํ ์ Step 1์TRUNCATE TABLE product_metrics_daily_working๊ฐ ์คํจํฉ๋๋ค.๐ ์์ ์ ์
try { // 1. ์ด์ ๋ฐฑ์ ์ญ์ jdbcTemplate.execute("DROP TABLE IF EXISTS product_metrics_daily_backup"); // 2. ์์์ ํ ์ด๋ธ ๊ต์ฒด jdbcTemplate.execute(""" RENAME TABLE product_metrics_daily TO product_metrics_daily_backup, product_metrics_daily_working TO product_metrics_daily """); + // 3. Working ํ ์ด๋ธ ์ฌ์์ฑ (backup ๊ตฌ์กฐ ๋ณต์ฌ) + jdbcTemplate.execute("CREATE TABLE product_metrics_daily_working LIKE product_metrics_daily"); log.info("Daily table swap completed successfully"); return RepeatStatus.FINISHED;apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java-47-49 (1)
47-49: v1 API์ Breaking Change: orderId ํ๋ผ๋ฏธํฐ ํ์ ๋ณ๊ฒฝ
orderId๊ฒฝ๋ก ๋ณ์์ ํ์ ์ดLong์์String์ผ๋ก ๋ณ๊ฒฝ๋์์ต๋๋ค. ์ด๋/api/v1/orders/{orderId}์๋ํฌ์ธํธ์ ๊ณ์ฝ์ ๋ณ๊ฒฝํ๋ breaking change์ ๋๋ค.์ฝ๋ ๊ตฌํ์ ์ผ๊ด์ฑ ์๊ฒ ์ฒ๋ฆฌ๋์ด ์์ต๋๋ค (OrderV1Controller, OrderV1ApiSpec, OrderFacade ๋ชจ๋ String ํ์ ์ฌ์ฉ). ๋ค๋ง ๋ค์ ์ฌํญ์ ํ์ธํ๊ณ ์กฐ์นํ์ธ์:
- API ๋ฒ์ ๊ด๋ฆฌ: ์ด breaking change๊ฐ v1์ ์ ์ฉ๋๊ณ ์์ต๋๋ค. ๊ธฐ์กด ํด๋ผ์ด์ธํธ ํธํ์ฑ ๋ฐ ๋ฒ์ ๊ด๋ฆฌ ์ ๋ต์ ์ฌ๊ฒํ ํ์ธ์.
- ๋ฌธ์ํ ๋ฐ ๋ณ๊ฒฝ ๋ก๊ทธ: API ๋ฌธ์์ ๋ณ๊ฒฝ ๋ก๊ทธ์ ์ด breaking change๋ฅผ ๋ช ์ํ๊ณ , ํด๋ผ์ด์ธํธ ๋ง์ด๊ทธ๋ ์ด์ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํ์ธ์.
- ํด๋ผ์ด์ธํธ ์ํฅ๋: ๋ชจ๋ API ์๋น์(ํ๋ก ํธ์๋, ๋ชจ๋ฐ์ผ ์ฑ, ์ธ๋ถ ํตํฉ)์ ์ ๋ฐ์ดํธ ๊ณํ์ ํ์ธํ์ธ์.
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductMetricsMonthly.java-10-13 (1)
10-13: ์๋ณ ์ํฐํฐ์ ๋ณตํฉ ์ธ๋ฑ์ค ๋ฐ ์ ๋ํฌ ์ ์ฝ์กฐ๊ฑด ๋๋ฝ
ProductMetricsWeekly์ํฐํฐ๋(productId, period_yyyyww)์ ๋ํ ๋ณตํฉ ์ธ๋ฑ์ค์ ์ ๋ํฌ ์ ์ฝ์กฐ๊ฑด์ ๊ฐ์ง๊ณ ์์ง๋ง, ์ดProductMetricsMonthly์ํฐํฐ์๋ ๋์ผํ ํจํด์ด ์ ์ฉ๋์ง ์์์ต๋๋ค. ์ด๋ก ์ธํด ์ค๋ณต ๋ฐ์ดํฐ ์ฝ์ ์ด ๊ฐ๋ฅํ๊ณ ์ฟผ๋ฆฌ ์ฑ๋ฅ์ด ์ ํ๋ ์ ์์ต๋๋ค.๐ ์ผ๊ด์ฑ์ ์ํ ์์ ์ ์
@Entity @Table(name = "product_metrics_monthly_working", indexes = { @Index(name = "idx_monthly_period_yyyymm", columnList = "period_yyyymm"), + @Index(name = "idx_monthly_product_month", columnList = "productId, period_yyyymm") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uk_product_month", columnNames = {"productId", "period_yyyymm"}) })apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java-22-26 (1)
22-26: ๋ ์ง ๋ฒ์ ์ํ ์กฐ๊ฑด ๋๋ฝ
WHERE pm.bucket_time_key >= :startTimeKey๋ง ์ฌ์ฉํ๋ฉด ์์ ์์ ์ดํ์ ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ํฌํจ๋ฉ๋๋ค. ์ฃผ๊ฐ/์๊ฐ ์ง๊ณ๋ฅผ ์ ํํ ํ๋ ค๋ฉด ์ํ ์กฐ๊ฑด(bucket_time_key < :endTimeKey๋๋BETWEEN)์ด ํ์ํฉ๋๋ค.๐ ์์ ์ ์
@Query(value = """ SELECT pm.product_id as productId, COALESCE(SUM(pm.like_count), 0) as likeCount, COALESCE(SUM(pm.sales_revenue), 0) as orderCount, COALESCE(SUM(pm.view_count), 0) as viewCount, :period as yearMonthWeek FROM product_metrics pm - WHERE pm.bucket_time_key >= :startTimeKey + WHERE pm.bucket_time_key >= :startTimeKey AND pm.bucket_time_key < :endTimeKey GROUP BY pm.product_id ORDER BY pm.product_id """, nativeQuery = true) - Page<ProductMetricsWeekly> findWeeklyMetrics(@Param("startTimeKey") String startTimeKey, @Param("period") String period, Pageable pageable); + Page<ProductMetricsWeekly> findWeeklyMetrics(@Param("startTimeKey") String startTimeKey, @Param("endTimeKey") String endTimeKey, @Param("period") String period, Pageable pageable);Committable suggestion skipped: line range outside the PR's diff.
apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java-15-27 (1)
15-27:sales_revenue๋ฅผorderCount๋ก ๋งคํํ๋ ๊ฒ์ ์๋ฏธ์ ์ค๋ฅ์ ๋๋ค.์ฟผ๋ฆฌ์์
SUM(pm.sales_revenue)(๋งค์ถ์ก)๋ฅผorderCount๋ก aliasํ๊ณ ์์ต๋๋ค. ํ์ง๋งProductMetricsWeekly์ProductMetricsMonthly์ํฐํฐ์orderCountํ๋๋ ์ฃผ๋ฌธ ์๋(Integer)์ ๋ํ๋ด์ผ ํ๋ฏ๋ก, ๋งค์ถ์ก(revenue)๊ณผ ์๋ฏธ์ ์ผ๋ก ๋ง์ง ์์ต๋๋ค.
product_metricsํ ์ด๋ธ์๋salesRevenue์ปฌ๋ผ๋ง ์๊ณorderCount์ปฌ๋ผ์ด ์์ต๋๋ค. ์ค์ ์ฃผ๋ฌธ ๊ฑด์๊ฐ ํ์ํ๋ค๋ฉด ๋ณ๋์order_count์ปฌ๋ผ์ ์ถ๊ฐํ๊ฑฐ๋, ํ๋๋ช ๋ฐ ์ฌ์ฉ์ฒ๋ฅผ ์ฌ์ ์ํด์ผ ํฉ๋๋ค.์ด ๋ฌธ์ ๋
findWeeklyMetrics(19์ค)๊ณผfindMonthlyMetrics(33์ค) ๋ ์ฟผ๋ฆฌ์ ๋ชจ๋ ์ํฅ์ ๋ฏธ์นฉ๋๋ค.apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java-10-17 (1)
10-17: @Index columnList์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปฌ๋ผ๋ช ์ฌ์ฉ ํ์JPA ๋ช ์ธ์ ๋ฐ๋ฅด๋ฉด
@Index์columnList๋ ๋ฐ๋์ ๋ฌผ๋ฆฌ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปฌ๋ผ๋ช ์ ์ฌ์ฉํด์ผ ํฉ๋๋ค. ํ์ฌ ์ฝ๋๋ Java ํ๋๋ช (productId,bucketTimeKey)์ ์ฌ์ฉํ๊ณ ์๋๋ฐ,@Column์ ๋ํ ์ด์ ์name์์ฑ์ด ์์ผ๋ฏ๋ก Spring Data์ ๋ช ๋ช ๊ท์น(camelCase โ snake_case)์ ์ํดproduct_id,bucket_time_key๋ก ๋ณํ๋ฉ๋๋ค.๋ค์๊ณผ ๊ฐ์ด ์์ ํ์ธ์:
์์ ๋ฐฉ์
@Index(name = "idx_product_bucket", columnList = "product_id, bucket_time_key"), @Index(name = "idx_bucket_time", columnList = "bucket_time_key")๋๋ ํ๋์ ๋ช ์์ ์ผ๋ก ์ปฌ๋ผ๋ช ์ ์ง์ :
@Column(name = "productId", nullable = false) private Long productId; @Column(name = "bucketTimeKey", nullable = false) private String bucketTimeKey;ํ์ฌ ์ค์ ์ผ๋ก๋ ์ธ๋ฑ์ค๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์์ฑ๋์ง ์์ ์ ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java-83-88 (1)
83-88: Locale ์์กด์ ์ฃผ์ฐจ ๊ณ์ฐ์ผ๋ก ์ธํ ๋ถ์ผ์น ๊ฐ๋ฅ์ฑ
WeekFields.of(Locale.getDefault())๋ ์๋ฒ ๋ก์ผ์ผ์ ๋ฐ๋ผ ์ฃผ์ฐจ ๊ณ์ฐ์ด ๋ฌ๋ผ์ง ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ๋ฏธ๊ตญ(์ผ์์ผ ์์)๊ณผ ํ๊ตญ/์ ๋ฝ(์์์ผ ์์)์์ ๋์ผ ๋ ์ง์ ์ฃผ์ฐจ๊ฐ ๋ค๋ฅด๊ฒ ๊ณ์ฐ๋ฉ๋๋ค.๐ ๊ถ์ฅ ์์ ์: ISO ์ฃผ์ฐจ ์ฌ์ฉ
private String convertDateToYearMonthWeek(String date) { LocalDate localDate = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyyMMdd")); - WeekFields weekFields = WeekFields.of(Locale.getDefault()); + WeekFields weekFields = WeekFields.ISO; // ISO-8601: ์์์ผ ์์, ์ฒซ ์ฃผ๋ ์ต์ 4์ผ ํฌํจ int weekOfYear = localDate.get(weekFields.weekOfYear()); return localDate.format(DateTimeFormatter.ofPattern("yyyy")) + String.format("%02d", weekOfYear); }apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingDateParser.java-147-171 (1)
147-171: ์ฃผ์ฐจโ๋ ์ง ๋ณํ ๋ก์ง์ด ISO ์ฃผ์ฐจ ํ์ค๊ณผ ๋ค๋ฆ
convertWeekToDate์ ๊ณ์ฐ์(week - 1) * 7 + 4๋ 1์ 1์ผ์ด ์ฒซ์งธ ์ฃผ ์์์ด๋ผ๊ณ ๊ฐ์ ํฉ๋๋ค. ISO-8601 ์ฃผ์ฐจ๋ 1์ 4์ผ์ ํฌํจํ๋ ์ฃผ๋ฅผ ์ฒซ์งธ ์ฃผ๋ก ์ ์ํ๋ฏ๋ก, ์ฐ์ด/์ฐ๋ง ๊ฒฝ๊ณ ์ฃผ์ฐจ์์ ๋ถ์ผ์น๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.๐ ๊ถ์ฅ ์์ ์: Java Time API ํ์ฉ
+import java.time.LocalDate; +import java.time.temporal.WeekFields; + private static String convertWeekToDate(int year, int week) { - // ํด๋น ์ฃผ์ฐจ์ ๋ํ ๋ ์ง ๊ณ์ฐ (๊ฐ๋จํ ๊ทผ์ฌ์น) - // 1์ ์ฒซ์งธ ์ฃผ๋ฅผ ๊ธฐ์ค์ผ๋ก ๊ณ์ฐ - int dayOfYear = (week - 1) * 7 + 4; // ํด๋น ์ฃผ์ ๋ชฉ์์ผ - - // ์ค๋ ๊ณ ๋ ค - int[] monthDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - if (isLeapYear(year)) { - monthDays[1] = 29; - } - - int month = 1; - while (month <= 12 && dayOfYear > monthDays[month - 1]) { - dayOfYear -= monthDays[month - 1]; - month++; - } - - // ๋ฒ์ ์ด๊ณผ ์ 12์ ๋ง์ง๋ง ๋ ๋ก ์กฐ์ - if (month > 12) { - month = 12; - dayOfYear = monthDays[11]; - } - - return String.format("%04d%02d%02d", year, month, dayOfYear); + // ISO-8601 ์ฃผ์ฐจ ๊ธฐ์ค์ผ๋ก ํด๋น ์ฃผ์ ๋ชฉ์์ผ ๋ฐํ + LocalDate date = LocalDate.of(year, 1, 4) // 1์ 4์ผ์ ํญ์ ์ฒซ์งธ ์ฃผ์ ํฌํจ + .with(WeekFields.ISO.weekOfWeekBasedYear(), week) + .with(WeekFields.ISO.dayOfWeek(), 4); // ๋ชฉ์์ผ + return date.format(DateTimeFormatter.ofPattern("yyyyMMdd")); }apps/commerce-api/src/main/java/com/loopers/application/event/DataTransferEventHandler.java-29-29 (1)
29-29: ์ํฐํฐ ์กฐํ ์ null ์ฒดํฌ๊ฐ ๋๋ฝ๋์์ต๋๋ค.๋ชจ๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ์์
paymentService.findPaymentByOrderId()๋ฐorderService.getOrder()๋ฅผ ํธ์ถํ๊ณ ์์ง๋ง, ์ํฐํฐ๊ฐ ์กด์ฌํ์ง ์์ ๊ฒฝ์ฐ์ ๋ํ null ์ฒดํฌ๋ ์์ธ ์ฒ๋ฆฌ๊ฐ ์์ต๋๋ค. ํธ๋์ญ์ ํ์ด๋ฐ ์ด์๋ ๋ฐ์ดํฐ ๋ถ์ผ์น๋ก ์ธํด ์ํฐํฐ๋ฅผ ์ฐพ์ง ๋ชปํ๋ฉด NPE๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.๐ ์ ์ํ๋ ์์ ์์
Payment payment = paymentService.findPaymentByOrderId(event.orderId()); +if (payment == null) { + log.error("Payment not found for orderId: {}", event.orderId()); + return; +}Also applies to: 59-59, 79-79, 96-96
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductMetricsWeekly.java-1-49 (1)
1-49: ์ค๋ณต๋ ์ํฐํฐ ํด๋์ค๋ฅผ ํตํฉํด์ฃผ์ธ์.
ProductMetricsWeekly์ํฐํฐ๊ฐ ์ฌ๋ฌ ๋ชจ๋๊ณผ ํจํค์ง์ ์ค๋ณต ์ ์๋์ด ์์ต๋๋ค:
apps/commerce-batch/.../domain/ranking/ProductMetricsWeekly.java(ํ ์ด๋ธ:product_metrics_weekly_working)apps/commerce-api/.../domain/ranking/ProductMetricsWeekly.java(ํ ์ด๋ธ:product_metrics_weekly)apps/commerce-api/.../domain/metrics/ProductMetricsWeekly.java(ํ ์ด๋ธ:product_metrics_weekly)์ด๋ฌํ ์ค๋ณต์ ์ ์ง๋ณด์์ฑ์ ์ ํดํ๊ณ ๋ถ์ผ์น๋ฅผ ์ผ๊ธฐํ ์ ์์ต๋๋ค. ํ ์ด๋ธ ์ด๋ฆ์ด ๋ค๋ฅธ ๊ฒ์ ์๋์ ์ผ ์ ์์ผ๋(working vs production), ์ํฐํฐ ํด๋์ค๋ ๊ณตํต ๋ชจ๋๋ก ์ถ์ถํ๊ฑฐ๋ ํ๋๋ก ํตํฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/ranking/ProductMetricsDailyService.java-102-103 (1)
102-103: ์ค์ฝ์ด ๊ณ์ฐ ๊ฐ์ค์น๊ฐ ๋ฐฐ์น ์์ ๊ณผ ๋ถ์ผ์น์ด ์๋น์ค์ ๊ฐ์ค์น(
like*3.0 + order*10.0 + view*1.0)๊ฐWeeklyRankingJobConfig์MonthlyRankingJobConfig์ ๊ฐ์ค์น(view*0.1 + like*0.2 + order*0.6)์ ๋ค๋ฆ ๋๋ค. ๋์ผํ ๋ญํน ๊ธฐ์ค์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.๐ ์์ ์ ์
๊ฐ์ค์น๋ฅผ ์์๋ก ์ถ์ถํ์ฌ ์ผ๊ด์ฑ์ ์ ์งํ์ธ์:
// ๊ณตํต ์์ ํด๋์ค ๋๋ ์ค์ public static final double VIEW_WEIGHT = 0.1; public static final double LIKE_WEIGHT = 0.2; public static final double ORDER_WEIGHT = 0.6;- double score = metric.getLikeCount() * 3.0 + metric.getOrderCount() * 10.0 + metric.getViewCount() * 1.0; + double score = metric.getViewCount() * VIEW_WEIGHT + metric.getLikeCount() * LIKE_WEIGHT + metric.getOrderCount() * ORDER_WEIGHT;Committable suggestion skipped: line range outside the PR's diff.
๐ก Minor comments (9)
apps/commerce-batch/src/main/resources/application.yml-63-82 (1)
63-82: ํ ์คํธ ํ๋กํ์ผ์ format_sql ์ค์ ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํ์ธ์.๋ก์ปฌ ํ๋กํ์ผ์๋
format_sql: true๊ฐ ์์ง๋ง ํ ์คํธ ํ๋กํ์ผ์๋ ๋๋ฝ๋์ด ์์ต๋๋ค. ํ ์คํธ ํ๊ฒฝ์์๋ SQL ๋๋ฒ๊น ์ด ํ์ํ ์ ์์ผ๋ฏ๋ก ์ผ๊ด์ฑ์ ์ํด ์ถ๊ฐํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ์ ์๋ ์์
jpa: show-sql: true hibernate: ddl-auto: none properties: hibernate: + format_sql: true jdbc: batch_size: 1000argo-workflows/weekly-ranking-workflow.yaml-1-5 (1)
1-5: ํ์ผ๋ช ๊ณผ ๋ด์ฉ์ด ๋ถ์ผ์นํฉ๋๋ค.ํ์ผ๋ช ์
weekly-ranking-workflow.yaml์ด์ง๋ง ์ค์ ๋ด์ฉ์daily-ranking-workflow์daily-ranking-cron์ ์ ์ํ๊ณ ์์ต๋๋ค. ์ ์ง๋ณด์์ฑ์ ์ํด ํ์ผ๋ช ์daily-ranking-workflow.yaml๋ก ๋ณ๊ฒฝํ๊ฑฐ๋, ์ฃผ๊ฐ ๋ญํน ์ํฌํ๋ก์ฐ ๋ด์ฉ์ผ๋ก ์์ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductMetricsDaily.java-25-32 (1)
25-32: ํ๋ ๊ธฐ๋ณธ๊ฐ์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ค์์๋ ์ ์ฉ๋์ง ์์ต๋๋ค.์ํฐํฐ ํ๋์
= 0๊ธฐ๋ณธ๊ฐ์ ์ค์ ํ์ง๋ง, ์ด๋ Java ๊ฐ์ฒด ๋ ๋ฒจ์์๋ง ๋์ํฉ๋๋ค. SQL์ ํตํด ์ง์ ์ฝ์ ๋๋ ๊ฒฝ์ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ ๊ธฐ๋ณธ๊ฐ์ด ์์ด NULL์ด ์ ์ฅ๋ ์ ์์ต๋๋ค.๋ค์์ ๊ณ ๋ คํ์ธ์:
- JPA
@Column์ด๋ ธํ ์ด์ ์columnDefinition = "INTEGER DEFAULT 0"์ถ๊ฐ- ๋๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์ ์คํฌ๋ฆฝํธ์์ DEFAULT ์ ์ฝ์กฐ๊ฑด ๋ช ์
๐ ์ ์ํ๋ ์์
-@Column(nullable = false) +@Column(nullable = false, columnDefinition = "INTEGER DEFAULT 0") private Integer likeCount = 0; -@Column(nullable = false) +@Column(nullable = false, columnDefinition = "INTEGER DEFAULT 0") private Integer orderCount = 0; -@Column(nullable = false) +@Column(nullable = false, columnDefinition = "INTEGER DEFAULT 0") private Integer viewCount = 0;apps/commerce-batch/src/main/java/com/loopers/batch/runner/BatchJobRunner.java-49-52 (1)
49-52: ๋ก๊ทธ ๋ฉ์์ง ์ค๋ฅ ์์ ํ์Line 49์ ๋ก๊ทธ ๋ฉ์์ง๊ฐ "Batch job completed"๋ก ๋์ด ์์ผ๋, ์ด ์์ ์์๋ ์์ง job์ด ์คํ๋๊ธฐ ์ ์ ๋๋ค. ์๋ํ ๋ด์ฉ์ job parameters ์ถ๋ ฅ์ธ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค.
๐ ์์ ์ ์
- log.info("Batch job completed: {}", builder.toJobParameters().toString()); + log.info("Job parameters: {}", builder.toJobParameters().toString()); rankingBatchScheduler.run(jobKey, builder.toJobParameters()); log.info("Batch job completed: {}", jobKey);apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetricsMonthly.java-9-45 (1)
9-45: ์ํฐํฐ ์ฝ๋ ์ค๋ณต: ์๋ก ๋ค๋ฅธ ํ ์ด๋ธ์ ๋งคํํ๋ ๊ตฌ์กฐ ์ฌ์ฌ์ฉ ๊ฒํ ํ์
commerce-batch๋ชจ๋์ProductMetricsMonthly์ ์ฝ๋ ๊ตฌ์กฐ๊ฐ ์์ ํ ๋์ผํ๋, ์๋ก ๋ค๋ฅธ ํ ์ด๋ธ์ ๋งคํํฉ๋๋ค:
commerce-batch:product_metrics_monthly_working(์ค๊ฐ ์์ ํ ์ด๋ธ)commerce-api:product_metrics_monthly(์ต์ข ๋ฉํธ๋ฆญ ํ ์ด๋ธ)ํ ์ด๋ธ์ ๋ค๋ฅด์ง๋ง ์ํฐํฐ ๊ตฌ์กฐ๊ฐ ์ค๋ณต๋์ด ์์ต๋๋ค. ๊ณตํต ๊ธฐ๋ณธ ์ํฐํฐ๋ฅผ ์ ์ํ๊ณ ๊ฐ ๋ชจ๋์์ ์์ํ๊ฑฐ๋, ๊ณต์ ๋ชจ๋์ ํตํด ๊ด๋ฆฌํ๋ ๊ฒ์ ๊ฒํ ํด๋ณด์ธ์.
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingDateParser.java-116-139 (1)
116-139: ์ผ๊ฐ ๋ ์ง ์ ํจ์ฑ ๊ฒ์ฆ ๋ถ์์ : ์กด์ฌํ์ง ์๋ ๋ ์ง ํ์ฉ
validateDailyDate์์ ์ผ(day)์ 1-31 ๋ฒ์๋ก๋ง ๊ฒ์ฆํ์ฌ20250231(2์ 31์ผ) ๊ฐ์ ์ ํจํ์ง ์์ ๋ ์ง๊ฐ ํต๊ณผ๋ฉ๋๋ค.๐ ๊ถ์ฅ ์์ ์: LocalDate ํ์ฑ์ผ๋ก ๊ฒ์ฆ
private static void validateDailyDate(String date) { try { - String year = date.substring(0, 4); - String month = date.substring(4, 6); - String day = date.substring(6, 8); - - int yearInt = Integer.parseInt(year); - int monthInt = Integer.parseInt(month); - int dayInt = Integer.parseInt(day); - - validateYear(yearInt); - - if (monthInt < 1 || monthInt > 12) { - throw new IllegalArgumentException("์ ํจํ์ง ์์ ์์ ๋๋ค: " + monthInt); - } - - if (dayInt < 1 || dayInt > 31) { - throw new IllegalArgumentException("์ ํจํ์ง ์์ ์ผ์ ๋๋ค: " + dayInt); - } - - } catch (NumberFormatException e) { + LocalDate parsedDate = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyyMMdd")); + validateYear(parsedDate.getYear()); + } catch (DateTimeParseException e) { throw new IllegalArgumentException("์ผ๊ฐ ๋ ์ง ํ์์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค: " + date); } }apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java-286-288 (1)
286-288: ์ฃผ๊ฐ/์๊ฐ ๋ญํน ์นด์ดํธ ํด๋ฐฑ ์ 0 ๋ฐํ์ผ๋ก ํ์ด์ง๋ค์ด์ ์ ๋ณด ๋ถ์ผ์นMV๊ฐ ์์ ๋
getTotalWeeklyRankingCountWithMV์getTotalMonthlyRankingCountWithMV๋ฉ์๋๋ 0L์ ๋ฐํํ์ง๋ง, ๋ฐ์ดํฐ ์กฐํ ๋ฉ์๋(getWeeklyRankingProductIdsWithMV,getMonthlyRankingProductIdsWithMV)๋ProductMetricsDailyService๋ก ํด๋ฐฑํ์ฌ ์ค์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํฉ๋๋ค. ์ด๋ก ์ธํด ํด๋ผ์ด์ธํธ๊ฐ ๋ฐ๋ ์ํ ๋ฐ์ดํฐ์ ์ด ๊ฐ์๊ฐ ๋ถ์ผ์นํ์ฌ ํ์ด์ง๋ค์ด์ ์ ๋ณด๊ฐ ์ ํํ์ง ์์ต๋๋ค.
ProductMetricsDailyRepository์findWeeklyAggregateByDateRange์findMonthlyAggregateByYearMonth์ฟผ๋ฆฌ ๊ฒฐ๊ณผ์์Page.getTotalElements()๋ฅผ ํ์ฉํ์ฌ ์นด์ดํธ ํด๋ฐฑ ๋ก์ง์ ๊ตฌํํด์ผ ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/ProductMetricsDailyService.java-44-44 (1)
44-44: ๋๋ฒ๊ทธ์ฉSystem.out.print๋ฌธ ์ ๊ฑฐ ํ์ํ๋ก๋์ ์ฝ๋์
System.out.print๋ฌธ์ด ๋จ์ ์์ต๋๋ค. ๋ก๊น ์ด ํ์ํ๋ฉด ์ด๋ฏธ ์ฌ์ฉ ์ค์ธlog.info()๋ฅผ ์ฌ์ฉํ์ธ์.๐ ์์ ์ ์
log.info("Week range: {} to {}", startDate, endDate); - System.out.print(startDate + endDate); // ProductMetricsDaily์์ ์ฃผ๊ฐ ์ง๊ณ ๋ฐ์ดํฐ ์กฐํ Page<ProductMetricsWeekly> weeklyMetrics = productMetricsDailyRepository .findWeeklyAggregateByDateRange(startDate, endDate, yearMonthWeek, Pageable.unpaged()); - System.out.print(weeklyMetrics.getTotalElements()); + log.debug("Weekly metrics count: {}", weeklyMetrics.getTotalElements());Also applies to: 49-49
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java-40-54 (1)
40-54:getOrder์getOrderByOrderId๊ฐ ์ผ๊ด์ฑ ์๋ ์๋ฌ ์ฒ๋ฆฌ
getOrder๋ ์ฃผ๋ฌธ์ ์ฐพ์ง ๋ชปํ๋ฉดnull์ ๋ฐํํ๊ณ ,getOrderByOrderId๋ ์์ธ๋ฅผ ๋์ง๋๋ค. ๋์ผํ ์กฐํ ๋ก์ง์ ๋ํด ์ผ๊ด๋ ๋์์ด ํ์ํฉ๋๋ค.๐ ์์ ์ ์
@Transactional(readOnly = true) public Order getOrder(String id) { if (id == null) { throw new CoreException(ErrorType.BAD_REQUEST, "ID๊ฐ ์์ต๋๋ค."); } - return orderRepository.findByOrderId(id).orElse(null); + return orderRepository.findByOrderId(id) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ฃผ๋ฌธ์ ์ฐพ์ ์ ์์ต๋๋ค: " + id)); }๋๋
getOrderByOrderId๋ฅผ ์ ๊ฑฐํ๊ณgetOrder๋ก ํตํฉํ์ธ์.Committable suggestion skipped: line range outside the PR's diff.
๐งน Nitpick comments (31)
apps/commerce-batch/src/main/resources/application.yml (3)
1-2: ํ๊ฒฝ๋ณ ํฌํธ ์ค์ ์ ๊ณ ๋ คํ์ธ์.๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ฌ๋ฌ ํ๊ฒฝ์์ ์คํ๋ ๋ ํฌํธ ์ถฉ๋์ ๋ฐฉ์งํ๊ธฐ ์ํด ํ๋กํ์ผ๋ณ๋ก ๋ค๋ฅธ ํฌํธ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
28-35: ํ๋ก๋์ ํ๊ฒฝ์ ํฌ์ค ์ฒดํฌ ์์ธ ์ ๋ณด ๋ ธ์ถ์ ๊ฒํ ํ์ธ์.
show-details: always๋ ๋ชจ๋ ํ๊ฒฝ์์ ํฌ์ค ์ฒดํฌ์ ์์ธ ์ ๋ณด๋ฅผ ๋ ธ์ถํฉ๋๋ค. ํ๋ก๋์ ํ๊ฒฝ์์๋when-authorized๋๋ ํ๋กํ์ผ๋ณ ์ค์ ์ ๊ณ ๋ คํ์ฌ ๋ฏผ๊ฐํ ์ ๋ณด ๋ ธ์ถ์ ์ ํํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
11-15: ๋ฐฐ์น ์ค์ ์ด ๋ญํน ์์คํ ์๊ตฌ์ฌํญ์ ์ ํฉํฉ๋๋ค.PR ๋ชฉํ์์ ์ค๋ช ํ ๊ณ์ธต์ ์ง๊ณ ํ์ดํ๋ผ์ธ(10๋ถ ์์ ๋ฉํธ๋ฆญ โ ์ผ์ผ ์ง๊ณ โ ์ฃผ๊ฐ/์๊ฐ MV)์ ์ง์ํ๊ธฐ ์ํ ๋ฐฐ์น ์ค์ ์ด ์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค:
job.enabled: false๋ก ์๋ ์คํ ์ ์ด- Hibernate ๋ฐฐ์น ์ต์ ํ(
batch_size: 1000,order_inserts/updates)๋ก ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ฑ๋ฅ ํ๋ณด๋ค๋ง, ์ด๋ฌํ ์ต์ ํ ์ค์ ์ด dev/qa/prd ํ๊ฒฝ์๋ ์ ์ฉ๋์ด์ผ ํฉ๋๋ค.
์ถ๊ฐ ๊ณ ๋ ค์ฌํญ:
- ๋ฐฐ์น ์์ ์ ๋์์ฑ ์ ์ด๋ฅผ ์ํด ๋ถ์ฐ ๋ฝ(Redis ๋๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ธฐ๋ฐ) ์ฌ์ฉ์ ๊ณ ๋ คํ์ธ์.
- ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ ์ฒญํฌ ํฌ๊ธฐ(
chunk)์ Hibernatebatch_size๋ฅผ ํจ๊ป ํ๋ํ์ธ์.- PR์์ ์ธ๊ธํ Argo Workflows ์ฌ์ฉ ์, ์์ ์ฌ์์์ฑ(idempotency)์ ๋ณด์ฅํ๋๋ก ์ค๊ณํ์ธ์.
Also applies to: 54-61
apps/commerce-batch/src/main/java/com/loopers/batch/scheduler/RankingBatchScheduler.java (2)
7-9: ์ฌ์ฉํ์ง ์๋ import ์ ๊ฑฐ ๊ถ์ฅ
JobParametersBuilder(Line 7)์@Qualifier(Line 9)๋ฅผ importํ์ง๋ง ์ฝ๋์์ ์ฌ์ฉํ์ง ์์ต๋๋ค.๐ ์ ์๋ ์์ ์ฌํญ
-import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component;
22-34: ๋ฐฐ์น ์์ ์คํ ๋ก๊น ์ถ๊ฐ ๊ถ์ฅ๋ฐฐ์น ์์ ์ ์์, ์๋ฃ, ์คํจ๋ฅผ ๋ก๊น ํ๋ฉด ์ด์ ์ค ๋ฌธ์ ์ถ์ ๊ณผ ๋ชจ๋ํฐ๋ง์ด ์ฉ์ดํฉ๋๋ค.
@Slf4j๋ฅผ ์ด๋ฏธ ์ฌ์ฉํ๊ณ ์์ผ๋ฏ๋ก ๋ก๊ทธ ์ถ๊ฐ๋ฅผ ๊ถ์ฅํฉ๋๋ค.๐ ์ ์๋ ์์ ์ฌํญ
public void run(String jobKey, JobParameters params) { + log.info("Starting batch job: {}, params: {}", jobKey, params); Job job = jobs.get(jobKey); if (job == null) { + log.error("Job not found: {}", jobKey); throw new IllegalArgumentException("No such job: " + jobKey); } try { - jobLauncher.run(job, params); + var execution = jobLauncher.run(job, params); + log.info("Batch job completed: {}, status: {}", jobKey, execution.getStatus()); } catch (Exception e) { + log.error("Failed to run batch job: {}", jobKey, e); throw new RuntimeException("Failed to run job: " + jobKey, e); } }apps/commerce-api/src/main/java/com/loopers/application/event/PaymentSuccessEvent.java (1)
1-12: ์ด๋ฒคํธ ๋ ์ฝ๋ ๊ตฌํ ์น์ธ
PaymentSuccessEvent๋ ์ฝ๋๊ฐ ๋ช ํํ๊ฒ ์ ์๋์์ต๋๋ค.Stringํ์ ์orderId๋ PR์ ๋ง์ด๊ทธ๋ ์ด์ ๊ณผ ์ผ๊ด๋๋ฉฐ, ๋ถ๋ณ์ฑ์ด ๋ณด์ฅ๋ฉ๋๋ค. ์ ํ์ ์ผ๋ก ์ด ์ด๋ฒคํธ๊ฐ ์ธ์ ๋ฐํ๋๋์ง ๋ช ์ํ๋ JavaDoc์ ์ถ๊ฐํ๋ฉด ๊ฐ๋ ์ฑ์ด ํฅ์๋ฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/event/PaymentFailureEvent.java (1)
1-11: ์ด๋ฒคํธ ๋ ์ฝ๋ ๊ตฌํ ์น์ธ, ์ ํ์ ๊ฐ์ ์ฌํญ ์ ์
PaymentFailureEvent๋ ์ฝ๋๊ฐ ๋ช ํํ๊ฒ ์ ์๋์์ต๋๋ค.Stringํ์ ์orderId๋ PR์ ๋ง์ด๊ทธ๋ ์ด์ ๊ณผ ์ผ๊ด๋๋ฉฐ ๋ถ๋ณ์ฑ์ด ๋ณด์ฅ๋ฉ๋๋ค.์ ํ์ ์ผ๋ก
reasonํ๋๋ฅผString๋์enum์ผ๋ก ๋ณ๊ฒฝํ๋ฉด ํ์ ์์ ์ฑ์ด ํฅ์๋๊ณ ์คํจ ์ฌ์ ๋ฅผ ํ์คํํ ์ ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java (1)
56-65: ๊ฐ์ค์น ์์๋ฅผ ๊ณต์ ์์น๋ก ์ถ์ถํ์ธ์.
MvProductRankMonthly.java์ ๋์ผํ ๊ฐ์ค์น ์์(VIEW_WEIGHT=0.1,LIKE_WEIGHT=0.2,ORDER_WEIGHT=0.6)์ ์ ์ ๊ณ์ฐ ๋ก์ง์ด ์ค๋ณต๋์ด ์์ต๋๋ค.๋ค์์ ๊ณ ๋ คํ์ธ์:
- ๊ฐ์ค์น ์์๋ฅผ ๊ณต์ ์ค์ ํด๋์ค๋ก ์ถ์ถ (์:
RankingWeightConfig)- ์ ์ ๊ณ์ฐ ๋ก์ง์ ๊ณตํต ์ ํธ๋ฆฌํฐ ๋ฉ์๋๋ก ์ถ์ถ
- ํฅํ ๊ฐ์ค์น ๋ณ๊ฒฝ ์ ๋จ์ผ ์ง์ ์์ ์์ ๊ฐ๋ฅํ๋๋ก ๊ฐ์
๐ ์ ์ํ๋ ๋ฆฌํฉํ ๋ง
๊ณต์ ์ค์ ํด๋์ค๋ฅผ ์์ฑํ์ธ์:
public class RankingScoreCalculator { private static final double VIEW_WEIGHT = 0.1; private static final double LIKE_WEIGHT = 0.2; private static final double ORDER_WEIGHT = 0.6; public static double calculateScore(int viewCount, int likeCount, int orderCount) { return (VIEW_WEIGHT * viewCount) + (LIKE_WEIGHT * likeCount) + (ORDER_WEIGHT * orderCount); } }๊ทธ ํ
create()๋ฉ์๋๋ฅผ ์์ ํ์ธ์:public static MvProductRankWeekly create(Long productId, String yearMonthWeek, Integer ranking, Integer likeCount, Integer orderCount, Integer viewCount) { - final double VIEW_WEIGHT = 0.1; - final double LIKE_WEIGHT = 0.2; - final double ORDER_WEIGHT = 0.6; - - double score = (VIEW_WEIGHT * viewCount) + (LIKE_WEIGHT * likeCount) + (ORDER_WEIGHT * orderCount); + double score = RankingScoreCalculator.calculateScore(viewCount, likeCount, orderCount); return new MvProductRankWeekly(productId, yearMonthWeek, ranking, score, likeCount, orderCount, viewCount); }
MvProductRankMonthly์๋ ๋์ผํ๊ฒ ์ ์ฉํ์ธ์.apps/commerce-api/src/main/java/com/loopers/application/event/CouponEventHandler.java (1)
26-26: ๋ฏธ์ฌ์ฉ ์์กด์ฑeventPublisher
OrderDataTransferEvent๋ฐํ ๋ก์ง์ด ์ ๊ฑฐ๋๋ฉด์ApplicationEventPublisher eventPublisher๊ฐ ๋ ์ด์ ์ฌ์ฉ๋์ง ์์ต๋๋ค.๐ ์์ ์ ์
@RequiredArgsConstructor @Component public class CouponEventHandler { private final CouponService couponService; private final PgClient pgClient; private final OrderService orderService; - private final ApplicationEventPublisher eventPublisher;
ApplicationEventPublisherimport๋ ํจ๊ป ์ ๊ฑฐํด ์ฃผ์ธ์.apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java (1)
14-15: orderId ํ์ ๋ณ๊ฒฝ์ด ์ผ๊ด๋๊ฒ ์ ์ฉ๋์์ต๋๋ค.ํ๋, ์์ฑ์, ํฉํ ๋ฆฌ ๋ฉ์๋ ๋ชจ๋
Stringํ์ ์ผ๋ก ์ผ๊ด๋๊ฒ ๋ณ๊ฒฝ๋์์ต๋๋ค. PR ์ ๋ฐ์LongโString์ ํ๊ณผ ์ ๋ง์ต๋๋ค.์ ํ์ ์ผ๋ก,
create()๋ฉ์๋์์orderId์ ๋ํ null/๋น ๋ฌธ์์ด ๊ฒ์ฆ ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํด ๋ณด์ธ์. ํ์ฌ๋ nullable=false ์ ์ฝ๋ง DB ๋ ๋ฒจ์์ ์ ์ฉ๋ฉ๋๋ค.Also applies to: 39-39, 49-51
apps/commerce-api/src/main/java/com/loopers/application/event/OrderDataTransferEvent.java (1)
6-6: ์ฌ์ฉ๋์ง ์๋ import์ผ ์ ์์ต๋๋ค.
totalAmount๊ฐBigDecimal์์Money๋ก ๋ณ๊ฒฝ๋์์ผ๋ฏ๋ก,java.math.BigDecimalimport๊ฐ ๋ ์ด์ ํ์ํ์ง ์์ ์ ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/payment/PgPayRequest.java (1)
12-14: ํ๋์ฝ๋ฉ๋ ์ฝ๋ฐฑ URL์ ํ๊ฒฝ ์ค์ ์ผ๋ก ์ธ๋ถํํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
localhost:8080URL์ด ํ๋์ฝ๋ฉ๋์ด ์์ด ๋ก์ปฌ ํ๊ฒฝ ์ธ์์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์ด ๊ฐ์ ํ๊ฒฝ ๋ณ์๋application.yml์ค์ ์ผ๋ก ์ฃผ์ ๋ฐ๋ ๊ฒ์ด ๊ถ์ฅ๋ฉ๋๋ค.๐ ์ค์ ์ธ๋ถํ ์์
// application.yml์์ ์ฃผ์ ๋ฐ๋ ๋ฐฉ์ ์์ // @Value("${payment.callback.url}") ๋๋ ConfigurationProperties ํ์ฉapps/commerce-batch/src/main/java/com/loopers/config/BatchJobProperties.java (1)
9-16: ์ ํ์ ๊ฐ์ : jobName์ ์ ํจ์ฑ ๊ฒ์ฆ ์ถ๊ฐ ๊ฒํ
BatchJobProperties๋CommerceBatchApplication์์@EnableConfigurationProperties(BatchJobProperties.class)๋ก ์ฌ๋ฐ๋ฅด๊ฒ ๋ฑ๋ก๋์ด ์์ต๋๋ค.๋ค๋ง,
jobName์์ฑ์ ํ๋ก๊ทธ๋๋งคํฑ ์ ํจ์ฑ ๊ฒ์ฆ์ ์ถ๊ฐํ๋ฉด ์๋ชป๋ ์ค์ ๊ฐ์ผ๋ก ์ธํ ๋ฐํ์ ์ค๋ฅ๋ฅผ ์กฐ๊ธฐ์ ๊ฐ์งํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด,@Pattern๋๋ ์ปค์คํ @Validated๊ฒ์ฆ์ ํตํด ํ์ฉ๋๋ ๋ฐฐ์น ์ก ์ด๋ฆ(dailyRankingDataProcessingJob, weeklyRankingMVUpdateJob, monthlyRankingMVUpdateJob)๋ง ์์ฉํ๋๋ก ์ ํํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (1)
56-58:null์ ๋ ฅ์ ๋ํ ๋ฐฉ์ด ์ฝ๋ ์ถ๊ฐ ๊ถ์ฅ
incrementSalesRevenue(Long revenue)๋ฉ์๋์์revenue๊ฐnull์ธ ๊ฒฝ์ฐNullPointerException์ด ๋ฐ์ํ ์ ์์ต๋๋ค.๐ ์์ ์ ์
public void incrementSalesRevenue(Long revenue) { + if (revenue == null || revenue <= 0) { + return; + } this.salesRevenue += revenue; }apps/commerce-api/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (2)
1-59: commerce-batch ๋ชจ๋๊ณผ ์ค๋ณต๋ ์ํฐํฐ
apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java์ ๋์ผํ ์ํฐํฐ๊ฐ ์กด์ฌํฉ๋๋ค. ๋ ๋ชจ๋์์ ๊ฐ์ ํ ์ด๋ธ์ ๋งคํํ๋ ์ํฐํฐ๋ฅผ ๊ฐ๊ฐ ๊ด๋ฆฌํ๋ฉด ๋ถ์ผ์น๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.๊ณตํต ๋๋ฉ์ธ ๋ชจ๋๋ก ์ถ์ถํ๊ฑฐ๋, ์๋๋ ์ค๊ณ์ธ ๊ฒฝ์ฐ ๋ ์ํฐํฐ์ ๋๊ธฐํ ๋ฐฉ์์ ๊ณ ๋ คํด ์ฃผ์ธ์.
56-58:null์ ๋ ฅ์ ๋ํ ๋ฐฉ์ด ์ฝ๋ ์ถ๊ฐ ๊ถ์ฅbatch ๋ชจ๋๊ณผ ๋์ผํ๊ฒ
revenue๊ฐnull์ธ ๊ฒฝ์ฐNullPointerException๋ฐฉ์ง๊ฐ ํ์ํฉ๋๋ค.๐ ์์ ์ ์
public void incrementSalesRevenue(Long revenue) { + if (revenue == null || revenue <= 0) { + return; + } this.salesRevenue += revenue; }apps/commerce-api/src/main/java/com/loopers/application/ranking/MvProductRankMonthlyService.java (1)
30-42: ์ ํ์ ๊ฐ์ : ์ค๋ณต DB ํธ์ถ ์ ๊ฑฐ ๊ณ ๋ ค
existsByYearMonth์ฒดํฌ ํfindByYearMonthOrderByRankingํธ์ถ๋ก ๋ ๋ฒ์ DB ์ ๊ทผ์ด ๋ฐ์ํฉ๋๋ค. MV ๋ฐ์ดํฐ๋ ๋ฐฐ์น๋ก ๊ฐฑ์ ๋์ด ์ค์ง์ ๋ฌธ์ ๋ ์์ง๋ง, ์กฐํ ๊ฒฐ๊ณผ๊ฐ ๋น์ด์๋์ง ์ง์ ํ์ธํ๋ ๋ฐฉ์์ผ๋ก ๋จ์ํํ ์ ์์ต๋๋ค.๐ ์ ํ์ ๊ฐ์ ์
public List<Long> getMonthlyRankingProductIds(String yearMonth, Pageable pageable) { - if (!existsByYearMonth(yearMonth)) { - log.debug("Monthly MV data not found for period: {}", yearMonth); - return List.of(); - } - Page<MvProductRankMonthly> mvResult = mvMonthlyRepository .findByYearMonthOrderByRanking(yearMonth, pageable); + if (mvResult.isEmpty()) { + log.debug("Monthly MV data not found for period: {}", yearMonth); + return List.of(); + } + return mvResult.getContent().stream() .map(MvProductRankMonthly::getProductId) .toList(); }apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (1)
193-271: ์ฝ๋ ์ค๋ณต: ์ฃผ๊ฐ/์๊ฐ ๋ญํน ์กฐํ ๋ฉ์๋
getWeeklyRankingProductIdsWithMV์getMonthlyRankingProductIdsWithMV๊ฐ๊ฐ 2๊ฐ์ ์ค๋ฒ๋ก๋๊ฐ ์์ผ๋ฉฐ, ๋๋ถ๋ถ์ ๋ก์ง์ด ์ค๋ณต๋ฉ๋๋ค.periodKey๋ฅผ ๋ฐ๋ ๋ฒ์ ์ผ๋ก ํต์ผํ๊ณ ๋ค๋ฅธ ๋ฒ์ ์์ ๋ณํ ํ ํธ์ถํ๋ ๋ฐฉ์์ผ๋ก ๋จ์ํํ ์ ์์ต๋๋ค.๐ ์ค๋ณต ์ ๊ฑฐ ์์ (์ฃผ๊ฐ)
private List<Long> getWeeklyRankingProductIdsWithMV(String date, Pageable pageable) { - // 1. Redis ์บ์์์ ์กฐํ ์๋ - List<Long> cachedProductIds = getWeeklyRankingFromCache(date, pageable); - if (cachedProductIds != null && !cachedProductIds.isEmpty()) { - log.debug("Weekly ranking found in cache for date: {}, size: {}", date, cachedProductIds.size()); - return cachedProductIds; - } - - // 2. MV์์ ์กฐํ ์๋ (์บ์ miss ์) String yearMonthWeek = convertDateToYearMonthWeek(date); - if (mvWeeklyService.existsByYearMonthWeek(yearMonthWeek)) { - log.debug("Weekly ranking found in MV for period: {}", yearMonthWeek); - return mvWeeklyService.getWeeklyRankingProductIds(yearMonthWeek, pageable); - } - - // 3. ProductMetricsDaily์์ ์ง๊ณํ์ฌ Redis์ ์ ์ฅ ํ ๋ฐํ (MV miss ์) - log.warn("Weekly ranking MV miss for period: {}, falling back to ProductMetricsDaily aggregation", yearMonthWeek); - return dailyService.calculateAndCacheWeeklyRanking(date, yearMonthWeek, pageable); + return getWeeklyRankingProductIdsWithMV(date, yearMonthWeek, pageable); }apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java (1)
46-72: PENDING ์ํ์ผ ๋ ๋ถํ์ํ Order ์กฐํ ๋ฐ์
status๊ฐPENDING์ผ ๋๋ Line 53์์orderService.getOrderByOrderId(orderId)๊ฐ ํธ์ถ๋ฉ๋๋ค. ์ด๋ฒคํธ ๋ฐํ์ด ํ์ ์๋ ๊ฒฝ์ฐ์ด๋ฏ๋ก ์กฐ๊ฑด๋ถ ์กฐํ๋ก ๋ณ๊ฒฝํ๋ฉด ๋ถํ์ํ DB ํธ์ถ์ ์ค์ผ ์ ์์ต๋๋ค.๐ ์ ํ์ ๊ฐ์ ์
@Transactional public void processPaymentCallback(String orderId, TransactionStatus status, String reason) { Payment payment = findPaymentByOrderId(orderId); payment.processCallbackStatus(status, reason); paymentRepository.save(payment); - // ์ํ์ ๋ฐ๋ฅธ ์ด๋ฒคํธ ๋ฐํ - Order order = orderService.getOrderByOrderId(orderId); - if (status == TransactionStatus.SUCCESS) { + Order order = orderService.getOrderByOrderId(orderId); eventPublisher.publishEvent(new PaymentSuccessEvent( order.getOrderId(), order.getRefUserId(), payment.getAmount(), reason, order.getTotalPrice() )); } else if (status == TransactionStatus.FAILED) { + Order order = orderService.getOrderByOrderId(orderId); eventPublisher.publishEvent(new PaymentFailureEvent( order.getOrderId(), order.getRefUserId(), payment.getAmount(), reason )); } // PENDING ์ํ๋ ๋ณ๋ ์ฒ๋ฆฌํ์ง ์์ }apps/commerce-api/src/main/java/com/loopers/application/event/OrderEventHandler.java (1)
57-61: ์๋ฌ ๋ก๊น ์ ์คํ ํธ๋ ์ด์ค ํฌํจ ๊ถ์ฅ
e.getMessage()๋ง ๋ก๊น ํ๋ฉด ์์ธ์ ๊ทผ๋ณธ ์์ธ ์ถ์ ์ด ์ด๋ ต์ต๋๋ค. ์คํ ํธ๋ ์ด์ค๋ฅผ ํฌํจํ์ฌ ๋๋ฒ๊น ์ ์ฉ์ดํ๊ฒ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ๊ถ์ฅ ์์ ์
} catch (Exception e) { - log.error("์ฃผ๋ฌธ ์ฒ๋ฆฌ ์คํจ - orderId: {}, userId: {}, error: {}", - event.orderId(), event.userId(), e.getMessage()); + log.error("์ฃผ๋ฌธ ์ฒ๋ฆฌ ์คํจ - orderId: {}, userId: {}", + event.orderId(), event.userId(), e); orderService.cancelPayment(event.orderId()); }apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepository.java (1)
19-19: @param ์ด๋ ธํ ์ด์ ์ด ํ์ ์ฟผ๋ฆฌ ๋ฉ์๋์์ ๋ถํ์ํฉ๋๋ค.Spring Data JPA์ ํ์ ์ฟผ๋ฆฌ ๋ฉ์๋(
findByYearMonthOrderByRanking)๋ ๋ฉ์๋ ์ด๋ฆ์์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๋ก ํ๋ฏ๋ก@Param์ด๋ ธํ ์ด์ ์ด ํ์ํ์ง ์์ต๋๋ค. ๋ช ์์ @Query์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ ๋ฉ์๋(Line 24, 30)์๋ง ํ์ํฉ๋๋ค.๐ ์ ์ํ๋ ์์ ์ฌํญ
- Page<MvProductRankMonthly> findByYearMonthOrderByRanking(@Param("yearMonth") String yearMonth, Pageable pageable); + Page<MvProductRankMonthly> findByYearMonthOrderByRanking(String yearMonth, Pageable pageable);apps/commerce-api/src/main/java/com/loopers/application/event/DataTransferEventHandler.java (1)
14-14: ์ฌ์ฉ๋์ง ์๋ import๋ฅผ ์ ๊ฑฐํด์ฃผ์ธ์.
BigDecimal์ด ์ฝ๋์์ ์ฌ์ฉ๋์ง ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java (1)
56-65: ๋ญํน ์ ์ ๊ฐ์ค์น๋ฅผ ๊ณตํต ์์๋ก ์ถ์ถํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
MvProductRankMonthly์MvProductRankWeekly์์ ๋์ผํ ๊ฐ์ค์น(VIEW_WEIGHT=0.1, LIKE_WEIGHT=0.2, ORDER_WEIGHT=0.6)๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ์ด๋ฌํ ๊ฐ์ค์น๊ฐ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ ๋ ๊ณณ์ ๋ชจ๋ ์์ ํด์ผ ํ๋ฉฐ, ๋ถ์ผ์น๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.๊ณตํต ์์ ํด๋์ค๋ ์ค์ ํ์ผ๋ก ์ถ์ถํ์ฌ ์ค์ ๊ด๋ฆฌํ๋ ๊ฒ์ ๊ณ ๋ คํด์ฃผ์ธ์.
๐ ์ ์ํ๋ ๋ฆฌํฉํ ๋ง ์์
๊ณตํต ์์ ํด๋์ค ์์ฑ:
public class RankingWeights { public static final double VIEW_WEIGHT = 0.1; public static final double LIKE_WEIGHT = 0.2; public static final double ORDER_WEIGHT = 0.6; public static double calculateScore(int viewCount, int likeCount, int orderCount) { return (VIEW_WEIGHT * viewCount) + (LIKE_WEIGHT * likeCount) + (ORDER_WEIGHT * orderCount); } }๊ทธ๋ฆฌ๊ณ ์์ชฝ ์ํฐํฐ์์ ํ์ฉ:
public static MvProductRankMonthly create(Long productId, String yearMonth, Integer ranking, Integer likeCount, Integer orderCount, Integer viewCount) { - final double VIEW_WEIGHT = 0.1; - final double LIKE_WEIGHT = 0.2; - final double ORDER_WEIGHT = 0.6; - - double score = (VIEW_WEIGHT * viewCount) + (LIKE_WEIGHT * likeCount) + (ORDER_WEIGHT * orderCount); + double score = RankingWeights.calculateScore(viewCount, likeCount, orderCount); return new MvProductRankMonthly(productId, yearMonth, ranking, score, likeCount, orderCount, viewCount); }apps/commerce-api/src/main/java/com/loopers/application/ranking/ProductMetricsDailyService.java (2)
47-60: ๋นํจ์จ์ ์ธ ํ์ด์ง๋ค์ด์ ํจํด
Pageable.unpaged()๋ก ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ์กฐํํ ํskip/limit์ผ๋ก ์๋ ํ์ด์ง๋ค์ด์ ํ๋ ๋ฐฉ์์ ๋ฉ๋ชจ๋ฆฌ ๋นํจ์จ์ ์ ๋๋ค. ๋์ฉ๋ ๋ฐ์ดํฐ์์ ๋ฌธ์ ๊ฐ ๋ ์ ์์ต๋๋ค.๐ ๊ฐ์ ๋ฐฉ์
Redis ์บ์ฑ์ฉ ์ ์ฒด ์กฐํ์ API ์๋ต์ฉ ํ์ด์ง๋ค์ด์ ์ ๋ถ๋ฆฌํ๊ฑฐ๋, DB ๋ ๋ฒจ์์ ํ์ด์ง๋ค์ด์ ์ ์ ์ฉํ์ธ์:
- Page<ProductMetricsWeekly> weeklyMetrics = productMetricsDailyRepository - .findWeeklyAggregateByDateRange(startDate, endDate, yearMonthWeek, Pageable.unpaged()); + // ์บ์ฑ์ฉ TOP N ์กฐํ (๋ณ๋ ์ฟผ๋ฆฌ) + List<ProductMetricsWeekly> allMetrics = productMetricsDailyRepository + .findWeeklyAggregateByDateRange(startDate, endDate, yearMonthWeek, PageRequest.of(0, 100)).getContent(); + + cacheWeeklyRankingToRedis(rankingKey, allMetrics); + + // ํ์ด์ง๋ค์ด์ ๋ ๊ฒฐ๊ณผ ๋ฐํ + return allMetrics.stream() + .skip((long) pageable.getPageNumber() * pageable.getPageSize()) + .limit(pageable.getPageSize()) + .map(ProductMetricsWeekly::getProductId) + .toList();
96-112: Redis ์บ์ ๊ฐฑ์ ์ ์์์ฑ ๋ฌธ์
deleteํ ์์ฐจ์ ์ผ๋กaddํ๋ ํจํด์ ์์์ ์ด์ง ์์, ๊ฐฑ์ ์ค ํด๋ผ์ด์ธํธ๊ฐ ๋น ๋ฐ์ดํฐ๋ ๋ถ๋ถ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์ ์์ต๋๋ค.๐ ๊ฐ์ ๋ฐฉ์
์ ํค์ ๋จผ์ ๋ฐ์ดํฐ๋ฅผ ์ด ํ
RENAME๋ช ๋ น์ผ๋ก ๊ต์ฒดํ๊ฑฐ๋, Redis ํ์ดํ๋ผ์ด๋์ ์ฌ์ฉํ์ธ์:private void cacheWeeklyRankingToRedis(String rankingKey, List<ProductMetricsWeekly> weeklyMetrics) { try { String tempKey = rankingKey + ":temp:" + System.currentTimeMillis(); // ์์ ํค์ ๋ฐ์ดํฐ ์ถ๊ฐ for (ProductMetricsWeekly metric : weeklyMetrics) { double score = /* ... */; redisTemplate.opsForZSet().add(tempKey, metric.getProductId().toString(), score); } // ์์์ ์ผ๋ก ํค ๊ต์ฒด redisTemplate.rename(tempKey, rankingKey); redisTemplate.expire(rankingKey, Duration.ofDays(7)); log.info("Cached {} weekly ranking items to Redis key: {}", weeklyMetrics.size(), rankingKey); } catch (Exception e) { log.error("Failed to cache weekly ranking to Redis: {}", rankingKey, e); } }apps/commerce-batch/src/main/java/com/loopers/batch/job/WeeklyRankingJobConfig.java (2)
120-129: ํ๋ก๋์ ํ๊ฒฝ์์ RedisKEYS๋ช ๋ น ์ฌ์ฉ ์ฃผ์
redisTemplate.keys(pattern)๋ ๋ด๋ถ์ ์ผ๋กKEYS๋ช ๋ น์ ์ฌ์ฉํ๋ฉฐ, ์ด๋ O(N) ๋ณต์ก๋๋ก Redis๋ฅผ ๋ธ๋กํนํฉ๋๋ค. ํ๋ก๋์ ์์๋SCAN๊ธฐ๋ฐ ์ญ์ ๋ฅผ ๊ถ์ฅํฉ๋๋ค.๐ ๊ฐ์ ๋ฐฉ์
private void deleteRedisCacheByPattern(String pattern) { try { ScanOptions scanOptions = ScanOptions.scanOptions().match(pattern).count(100).build(); Set<String> keysToDelete = new HashSet<>(); try (Cursor<String> cursor = redisTemplate.scan(scanOptions)) { while (cursor.hasNext()) { keysToDelete.add(cursor.next()); } } if (!keysToDelete.isEmpty()) { redisTemplate.delete(keysToDelete); log.debug("Deleted {} cache keys matching pattern: {}", keysToDelete.size(), pattern); } } catch (Exception e) { log.warn("Failed to clear Redis cache for pattern: {}", pattern, e); } }
36-36: ์ฌ์ฉ๋์ง ์๋ ์์กด์ฑ ์ ๊ฑฐ
entityManagerFactory๊ฐ ์ฃผ์ ๋์์ง๋ง ์ฌ์ฉ๋์ง ์์ต๋๋ค.๐ ์์ ์ ์
private final DataSource dataSource; - private final EntityManagerFactory entityManagerFactory; private final RedisTemplate<String, String> redisTemplate;apps/commerce-batch/src/main/java/com/loopers/batch/job/MonthlyRankingJobConfig.java (3)
59-61: ์๋ง ๋ ์ง ๊ณ์ฐ ๊ฐ์ ๊ถ์ฅ
endDate = yearMonth + "31"์ ๋๋ถ๋ถ์ DB์์ ๋์ํ์ง๋ง, ์ฝ๋ ์๋๊ฐ ๋ถ๋ช ํํฉ๋๋ค. ์ค์ ์๋ง ๋ ์ง๋ฅผ ๊ณ์ฐํ๋ ๊ฒ์ด ๋ ๋ช ํํฉ๋๋ค.๐ ์์ ์ ์
- String startDate = yearMonth + "01"; // ํด๋น ์ 1์ผ - String endDate = yearMonth + "31"; // ํด๋น ์ 31์ผ (31์ผ์ด ์๋ ๋ฌ๋ ํฌํจ) + LocalDate firstDay = LocalDate.parse(yearMonth + "01", DateTimeFormatter.ofPattern("yyyyMMdd")); + LocalDate lastDay = firstDay.withDayOfMonth(firstDay.lengthOfMonth()); + String startDate = firstDay.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String endDate = lastDay.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
35-35: ์ฌ์ฉ๋์ง ์๋ ์์กด์ฑ ์ ๊ฑฐ
WeeklyRankingJobConfig์ ๋ง์ฐฌ๊ฐ์ง๋กentityManagerFactory๊ฐ ์ฌ์ฉ๋์ง ์์ต๋๋ค.
110-120: Redis ์บ์ ์ญ์ ๋ก์ง ์ค๋ณต ๋ฐKEYS๋ช ๋ น ์ฌ์ฉ
WeeklyRankingJobConfig์ ๋์ผํdeleteRedisCacheByPattern๋ฉ์๋๊ฐ ์ค๋ณต๋์ด ์์ต๋๋ค. ๊ณตํต ์ ํธ๋ฆฌํฐ๋ก ์ถ์ถํ๊ณ ,KEYS๋์SCAN์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyRepository.java (1)
30-31:findByYearMonthWeekAndProductId๋ฐํ ํ์ ์Optional๋ก ๋ณ๊ฒฝ ๊ถ์ฅ๊ฒฐ๊ณผ๊ฐ ์์ ๋
null์ ๋ฐํํ๋ฉด NPE ์ํ์ด ์์ต๋๋ค.Optional<MvProductRankWeekly>๋ฅผ ๋ฐํํ๋ ๊ฒ์ด ๋ ์์ ํฉ๋๋ค.๐ ์์ ์ ์
- @Query("SELECT mv FROM MvProductRankWeekly mv WHERE mv.yearMonthWeek = :yearMonthWeek AND mv.productId = :productId") - MvProductRankWeekly findByYearMonthWeekAndProductId(@Param("yearMonthWeek") String yearMonthWeek, @Param("productId") Long productId); + @Query("SELECT mv FROM MvProductRankWeekly mv WHERE mv.yearMonthWeek = :yearMonthWeek AND mv.productId = :productId") + Optional<MvProductRankWeekly> findByYearMonthWeekAndProductId(@Param("yearMonthWeek") String yearMonthWeek, @Param("productId") Long productId);Import ์ถ๊ฐ:
import java.util.Optional;
|
|
||
| public record CouponUsedEvent( | ||
| Long orderId, | ||
| String orderId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
#!/bin/bash
# Find CouponUsedEvent consumers
echo "=== CouponUsedEvent ์๋น์ ๊ฒ์ ==="
rg -n -C3 --type=java 'CouponUsedEvent'
echo -e "\n=== orderId ๊ด๋ จ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ๊ฒ์ ==="
fd -e sql -e xml | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 4738
๐ Script executed:
#!/bin/bash
# Get full CouponService implementation
echo "=== CouponService.publishCouponUsedEvent ์ ์ฒด ์ฝ๋ ==="
rg -n -A10 'private void publishCouponUsedEvent' --type=java
echo -e "\n=== CouponEventHandler.handleCouponUsed ์ ์ฒด ์ฝ๋ ==="
rg -n -A15 'public void handleCouponUsed' --type=java
echo -e "\n=== BusinessActionEvent.couponUsed ์ ์ ์ฐพ๊ธฐ ==="
rg -n -B5 -A10 'couponUsed\(' --type=java | grep -A15 'static.*couponUsed'
echo -e "\n=== ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ํ์ผ ํ์ธ ==="
fd -e sql | xargs rg -l 'orders' -iRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 5305
๐ Script executed:
#!/bin/bash
# Find where CouponUsedEvent is instantiated
echo "=== CouponUsedEvent ์ธ์คํด์ค ์์ฑ ์์น ==="
rg -n 'new CouponUsedEvent' --type=java
echo -e "\n=== CouponUsedEvent ๋ ์ฝ๋ ์ ์ฒด ์ ์ ==="
rg -n -A10 'public record CouponUsedEvent' --type=java
echo -e "\n=== eventPublisher.publishEvent ํธ์ถ ์์น ==="
rg -n 'publishEvent\(' --type=java | head -20
echo -e "\n=== CouponEventHandler ์ ์ฒด ํด๋์ค ==="
cat apps/commerce-api/src/main/java/com/loopers/application/event/CouponEventHandler.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 5058
๐ Script executed:
#!/bin/bash
# Search for all places where CouponUsedEvent is published or created
echo "=== CouponUsedEvent๋ฅผ ๋ฐํํ๋ ์ฝ๋ ์ฐพ๊ธฐ ==="
rg -n 'publishEvent.*CouponUsedEvent|eventPublisher.*new CouponUsedEvent' --type=java
# Check if CouponUsedEvent is used anywhere
echo -e "\n=== CouponUsedEvent ๋ชจ๋ ์ฌ์ฉ ์์น ==="
rg -n 'CouponUsedEvent' --type=java
# Check OrderService.cancelPayment to see what type orderId should be
echo -e "\n=== OrderService.cancelPayment ๋ฉ์๋ ์๋ช
==="
rg -n -A5 'public.*cancelPayment' apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java
# Check PgClient.requestPayment to see what type orderId should be
echo -e "\n=== PgClient.requestPayment ๋ฉ์๋ ์๋ช
==="
rg -n -A5 'requestPayment' --type=java | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3639
๐ Script executed:
#!/bin/bash
# Check git history or comments that might explain CouponUsedEvent
echo "=== CouponUsedEvent ๊ด๋ จ ์ฝ๋ ๋งฅ๋ฝ ==="
cat apps/commerce-api/src/main/java/com/loopers/application/event/CouponUsedEvent.java
echo -e "\n=== CouponService ์ ์ฒด publishCouponUsedEvent ๋ฉ์๋ ==="
rg -n -B2 -A8 'private void publishCouponUsedEvent' apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java
echo -e "\n=== Order ์ํฐํฐ์์ orderId ํ์
ํ์ธ ==="
rg -n 'orderId|orderId' --type=java | grep -i 'order.*class\|private.*orderId\|String orderId\|Long orderId' | head -20
echo -e "\n=== Order ์ํฐํฐ ํ์ผ ์ฐพ๊ธฐ ==="
fd -n 'Order.java' --type f | head -5Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 4091
๐ Script executed:
#!/bin/bash
# Look at Order.java to confirm orderId type
echo "=== Order.java ํ์ผ ๋ด์ฉ ==="
cat apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java | head -50
echo -e "\n=== CouponService ์ ์ฒด ๋ด์ฉ ํ์ธ ==="
cat apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 5391
CouponUsedEvent๊ฐ ๋ฐํ๋์ง ์์ ํธ๋ค๋ฌ ์ฝ๋๊ฐ ์คํ๋์ง ์์
CouponUsedEvent๊ฐ ์ ์๋์ด ์์ง๋ง ์ค์ ๋ก ๋ฐํ๋์ง ์๋ ๊ตฌ์กฐ์ ๋ฌธ์ ๊ฐ ์์ต๋๋ค:
CouponService.publishCouponUsedEvent()๋ฉ์๋๋BusinessActionEvent๋ฅผ ๋ฐํํ๋ฉฐ,CouponUsedEvent๋ ์ ํ ๋ฐํ๋์ง ์์ต๋๋คCouponEventHandler.handleCouponUsed()๋ฆฌ์ค๋๋ ๋๋ฌ ๋ถ๊ฐ๋ฅํ ๋ฐ๋ ์ฝ๋์ ๋๋ค- ์ค์ ๋ฐํ๋๋
BusinessActionEvent๋ orderId๋ฅผnull๋ก ์ ๋ฌํ๋ฏ๋ก, ์๋ํ ๊ฒฐ์ ์ฒ๋ฆฌ ๋ก์ง(pgClient.requestPayment(),orderService.cancelPayment())์ด ์คํ๋์ง ์์ต๋๋ค
CouponUsedEvent๋ฅผ ์ค์ ๋ก ๋ฐํํ๋๋ก ์์ ํ๊ฑฐ๋, CouponEventHandler๋ฅผ ์ ๊ฑฐํด์ผ ํฉ๋๋ค.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/event/CouponUsedEvent.java
around line 7, CouponUsedEvent is never actually published so
CouponEventHandler.handleCouponUsed is dead code and BusinessActionEvent is
published with a null orderId; fix by changing
CouponService.publishCouponUsedEvent() to construct and publish a
CouponUsedEvent (copying the orderId and other needed fields out of the
BusinessActionEvent payload) instead of publishing BusinessActionEvent, ensure
the event payload includes a non-null orderId, and keep the existing handler
intact (or if you prefer to keep BusinessActionEvent as the canonical event,
remove the unused CouponUsedEvent class and its handler). Ensure the publish
call uses the correct event type and that downstream handler wiring remains
unchanged.
| public void save(Long userId, Long productId) { | ||
| Optional<Like> liked = likeRepository.findById(userId, productId); | ||
| if (!liked.isPresent()) { | ||
| likeRepository.save(userId, productId); | ||
| } | ||
|
|
||
| // ์ข์์ ์ด๋ฒคํธ ๋ฐํ (๋ฐฐ์น ์ฒ๋ฆฌ์ฉ) | ||
| LikeEvent likeEvent = new LikeEvent(userId, productId, LikeEvent.LikeAction.LIKE); | ||
| eventPublisher.publishEvent(likeEvent); | ||
| log.debug("์ข์์ ์ด๋ฒคํธ ๋ฐํ - ์ฌ์ฉ์ID: {}, ์ํID: {}", userId, productId); | ||
| } |
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.
์ค๋ณต ์ด๋ฒคํธ ๋ฐํ์ผ๋ก ์ธํ ์ง๊ณ ์ค๋ฅ ๋ฐ์.
ํ์ฌ ๊ตฌํ์ ์ด๋ฏธ ์กด์ฌํ๋ ์ข์์์ ๋ํด์๋ ์ด๋ฒคํธ๋ฅผ ๋ฐํํฉ๋๋ค:
- Line 28-30: ์ข์์๊ฐ ์ด๋ฏธ ์กด์ฌํ๋ฉด ์ ์ฅ์ ๊ฑด๋๋๋๋ค.
- Line 33-35: ํ์ง๋ง ์ด๋ฒคํธ๋ ๋ฌด์กฐ๊ฑด ๋ฐํ๋ฉ๋๋ค (if ๋ธ๋ก ๋ฐ์ ์์น).
์ด๋ก ์ธํด ๋์ผํ ์ฌ์ฉ์๊ฐ ๊ฐ์ ์ํ์ ๋ฐ๋ณต์ ์ผ๋ก ์ข์์๋ฅผ ์๋ํ๋ฉด ์ค์ ์ข์์๋ 1๊ฐ์ง๋ง ๋ฐฐ์น ์ง๊ณ ์์คํ ์๋ ์ฌ๋ฌ ๋ฒ ์นด์ดํธ๋์ด ๋ญํน ๋ฐ์ดํฐ๊ฐ ๋ถ์ ํํด์ง๋๋ค.
๐ ์ด๋ฒคํธ ๋ฐํ์ ์กฐ๊ฑด๋ถ ๋ธ๋ก ์์ผ๋ก ์ด๋ํ๋ ์์ ์
@Transactional
public void save(Long userId, Long productId) {
Optional<Like> liked = likeRepository.findById(userId, productId);
if (!liked.isPresent()) {
likeRepository.save(userId, productId);
+
+ // ์ข์์ ์ด๋ฒคํธ ๋ฐํ (๋ฐฐ์น ์ฒ๋ฆฌ์ฉ)
+ LikeEvent likeEvent = new LikeEvent(userId, productId, LikeEvent.LikeAction.LIKE);
+ eventPublisher.publishEvent(likeEvent);
+ log.debug("์ข์์ ์ด๋ฒคํธ ๋ฐํ - ์ฌ์ฉ์ID: {}, ์ํID: {}", userId, productId);
}
-
- // ์ข์์ ์ด๋ฒคํธ ๋ฐํ (๋ฐฐ์น ์ฒ๋ฆฌ์ฉ)
- LikeEvent likeEvent = new LikeEvent(userId, productId, LikeEvent.LikeAction.LIKE);
- eventPublisher.publishEvent(likeEvent);
- log.debug("์ข์์ ์ด๋ฒคํธ ๋ฐํ - ์ฌ์ฉ์ID: {}, ์ํID: {}", userId, productId);
}๐ค Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
around lines 26 to 36, the method currently publishes a LikeEvent regardless of
whether a new Like was actually saved, causing duplicate events and incorrect
batch aggregation; change the logic so that the event creation, publishEvent
call, and debug log only occur when likeRepository.save(userId, productId) is
executed (i.e., move those lines inside the branch that runs when the like did
not previously exist), ensuring no event is emitted for existing likes.
| void ์คํจ_์กด์ฌํ์ง_์๋_์ํID() { | ||
| // arrange | ||
| Long orderId = (long) -1; | ||
| String orderId = (long) -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
์ปดํ์ผ ์ค๋ฅ: ํ์ ๋ถ์ผ์น
(long) -1์ String ๋ณ์์ ํ ๋นํ๊ณ ์์ด ์ปดํ์ผ ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค. String ๋ฆฌํฐ๋ด๋ก ์์ ํด์ผ ํฉ๋๋ค.
๐ ์์ ์ ์
- String orderId = (long) -1;
+ String orderId = "invalid-order-id";๐ 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 orderId = (long) -1; | |
| String orderId = "invalid-order-id"; |
๐ค Prompt for AI Agents
In
apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java
around line 136, the code assigns (long) -1 to a String variable causing a
compile-time type mismatch; replace the numeric cast with a String literal
(e.g., "-1") or use String.valueOf(-1L) so the variable receives a valid String
value.
| void ์คํจ_์กด์ฌํ์ง_์๋_์ฃผ๋ฌธID() { | ||
| // arrange | ||
| Long orderId = (long) -1; | ||
| String orderId = (long) -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
์ปดํ์ผ ์ค๋ฅ: long์ String์ ํ ๋นํ ์ ์์ต๋๋ค.
(long) -1์ String ๋ณ์์ ์ง์ ํ ๋นํ๋ ๊ฒ์ Java์์ ํ์ฉ๋์ง ์์ ์ปดํ์ผ์ด ์คํจํฉ๋๋ค.
๐ ์์ ์ ์
- String orderId = (long) -1;
+ String orderId = "-1";๐ 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 orderId = (long) -1; | |
| String orderId = "-1"; |
๐ค Prompt for AI Agents
In
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java
around line 119, the test attempts to assign (long) -1 to a String variable
causing a compile error; replace the invalid assignment with a proper String
representation of -1 (e.g., use a string literal "-1" or convert the long via
String.valueOf(-1L) / Long.toString(-1L)) so orderId is a valid String.
| queryProvider.setSelectClause("pm.product_id, COALESCE(SUM(pm.like_count), 0) as like_count, COALESCE(SUM(pm.sales_revenue), 0) as order_count, COALESCE(SUM(pm.view_count), 0) as view_count"); | ||
| queryProvider.setFromClause("product_metrics pm"); | ||
|
|
||
| // ํน์ ์ผ์ ๋ฐ์ดํฐ๋ง ๊ฐ์ ธ์ค๊ธฐ (00:00๋ถํฐ 23:59๊น์ง) | ||
| String currentDate = date != null ? date : LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); | ||
| String startKey = currentDate + "0000"; // ํด๋น ์ผ 00:00 | ||
| String endKey = currentDate + "2359"; // ํด๋น ์ผ 23:59 | ||
|
|
||
| queryProvider.setWhereClause("pm.bucket_time_key >= '" + startKey + "' AND pm.bucket_time_key <= '" + endKey + "'"); | ||
| queryProvider.setGroupClause("pm.product_id"); | ||
| queryProvider.setSortKeys(Map.of("pm.product_id", Order.ASCENDING)); |
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.
SQL Injection ์ทจ์ฝ์ ๋ฐ ์ปฌ๋ผ ๋งคํ ์ค๋ฅ
๋ ๊ฐ์ง ๋ฌธ์ ๊ฐ ์์ต๋๋ค:
- SQL Injection:
setWhereClause์์ ๋ฌธ์์ด ์ฐ๊ฒฐ๋ก SQL์ ๊ตฌ์ฑํ๊ณ ์์ด ๋ณด์ ์ทจ์ฝ์ ์ด ์กด์ฌํฉ๋๋ค. - ์ปฌ๋ผ ๋งคํ ์ค๋ฅ:
sales_revenue๋ฅผorder_count๋ก ๋งคํํ๊ณ ์์ต๋๋ค. ํ๋งค ์์ต๊ณผ ์ฃผ๋ฌธ ์๋ ๋ค๋ฅธ ๊ฐ๋ ์ ๋๋ค.
๐ ์์ ์ ์
MySqlPagingQueryProvider queryProvider = new MySqlPagingQueryProvider();
-queryProvider.setSelectClause("pm.product_id, COALESCE(SUM(pm.like_count), 0) as like_count, COALESCE(SUM(pm.sales_revenue), 0) as order_count, COALESCE(SUM(pm.view_count), 0) as view_count");
+queryProvider.setSelectClause("pm.product_id, COALESCE(SUM(pm.like_count), 0) as like_count, COALESCE(COUNT(*), 0) as order_count, COALESCE(SUM(pm.view_count), 0) as view_count");
queryProvider.setFromClause("product_metrics pm");
String currentDate = date != null ? date : LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String startKey = currentDate + "0000";
String endKey = currentDate + "2359";
-queryProvider.setWhereClause("pm.bucket_time_key >= '" + startKey + "' AND pm.bucket_time_key <= '" + endKey + "'");
+queryProvider.setWhereClause("pm.bucket_time_key >= :startKey AND pm.bucket_time_key <= :endKey");ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ์ ์ํด JdbcPagingItemReaderBuilder์ parameterValues ์ค์ ์ด ํ์ํฉ๋๋ค:
return new JdbcPagingItemReaderBuilder<DailyMetricsDto>()
.name("dailyMetricsReader")
.dataSource(dataSource)
.queryProvider(queryProvider)
.parameterValues(Map.of("startKey", startKey, "endKey", endKey))
.pageSize(1000)
.rowMapper(new BeanPropertyRowMapper<>(DailyMetricsDto.class))
.build();Committable suggestion skipped: line range outside the PR's diff.
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/job/DailyRankingJobConfig.java
around lines 94 to 104, the code builds the WHERE clause via string
concatenation (SQL injection risk) and incorrectly aliases sales_revenue as
order_count; change the WHERE clause to use named parameters (e.g.
pm.bucket_time_key >= :startKey AND pm.bucket_time_key <= :endKey) and pass a
parameterValues map into the JdbcPagingItemReaderBuilder with startKey and
endKey, and fix the SELECT clause so sales_revenue is aliased to sales_revenue
(or add SUM(pm.order_count) as order_count if you intend to include order count
separately), ensuring the resulting column aliases match the fields of
DailyMetricsDto.
| data: | ||
| redis: | ||
| host: localhost | ||
| port: 6379 | ||
| timeout: 3000ms | ||
| lettuce: | ||
| pool: | ||
| max-active: 8 | ||
| max-idle: 8 | ||
| min-idle: 0 |
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.
Redis ์ค์ ์ ํ๊ฒฝ๋ณ๋ก ๋ถ๋ฆฌํ์ธ์.
๋ชจ๋ ํ๊ฒฝ์์ localhost:6379๋ฅผ ์ฌ์ฉํ๋๋ก ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. ๋ฐฐํฌ๋ ํ๊ฒฝ(dev/qa/prd)์์๋ ์ค์ Redis ์ธ์คํด์ค์ ์ฐ๊ฒฐํ ์ ์์ด ๋ญํน ์บ์ ์์คํ
์ด ๋์ํ์ง ์์ต๋๋ค.
๐ ํ๊ฒฝ ๋ณ์ ๋๋ ํ๋กํ์ผ๋ณ ์ค์ ์ ์
์ต์ 1: ํ๊ฒฝ ๋ณ์ ์ฌ์ฉ (๊ถ์ฅ)
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}์ต์
2: ํ๋กํ์ผ๋ณ ์ค์
๊ฐ ํ๋กํ์ผ(dev/qa/prd)์ ํด๋น ํ๊ฒฝ์ Redis ์ค์ ์ถ๊ฐ
๐ค Prompt for AI Agents
In apps/commerce-batch/src/main/resources/application.yml around lines 17-26 the
Redis host/port are hardcoded to localhost:6379 which breaks non-local
deployments; replace these literals with environment-variable placeholders (e.g.
REDIS_HOST, REDIS_PORT, optional REDIS_PASSWORD) or move Redis settings into
profile-specific files (application-dev.yml, application-qa.yml,
application-prod.yml) and reference them via spring.profiles.active; update
configuration to use the placeholders/defaults and add the profile files for
each environment, then verify the app reads the env vars or active profile
during startup.
| spring.config.activate.on-profile: dev | ||
|
|
||
| spring: | ||
| datasource: | ||
| url: jdbc:mysql://localhost:3306/loopers | ||
| username: application | ||
| password: application | ||
| driver-class-name: com.mysql.cj.jdbc.Driver | ||
|
|
||
| jpa: | ||
| hibernate: | ||
| ddl-auto: none |
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.
dev ํ๋กํ์ผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ ์ ์์ ํ์ธ์.
๋ค์ ๋ฌธ์ ๋ค์ด ํ์ธ๋์์ต๋๋ค:
- ๋ฐ์ดํฐ๋ฒ ์ด์ค URL:
localhost:3306์ ๋ฐฐํฌ๋ ํ๊ฒฝ์์ ์๋ํ์ง ์์ต๋๋ค. - ํ๋์ฝ๋ฉ๋ ์๊ฒฉ์ฆ๋ช : username/password๊ฐ ์์ค ์ฝ๋์ ๋ ธ์ถ๋์ด ์์ต๋๋ค.
- ๋๋ฝ๋ Hibernate ๋ฐฐ์น ์ต์ ํ: ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ์ ์ค์ํ ์ค์ ๋ค์ด ์์ต๋๋ค.
๐ ์ ์๋ ์์
spring:
datasource:
- url: jdbc:mysql://localhost:3306/loopers
- username: application
- password: application
+ url: ${DB_URL:jdbc:mysql://localhost:3306/loopers}
+ username: ${DB_USERNAME}
+ password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: none
+ properties:
+ hibernate:
+ jdbc:
+ batch_size: 1000
+ order_inserts: true
+ order_updates: true
+ batch_versioned_data: true๐ค Prompt for AI Agents
In apps/commerce-batch/src/main/resources/application.yml around lines 84-95,
the dev profile currently uses a hardcoded localhost URL and credentials and
lacks Hibernate batch optimization settings; change the datasource URL and
credentials to environment-backed placeholders (e.g. spring.datasource.url:
${SPRING_DATASOURCE_URL:jdbc:mysql://db:3306/loopers},
spring.datasource.username: ${SPRING_DATASOURCE_USERNAME:application},
spring.datasource.password: ${SPRING_DATASOURCE_PASSWORD:application}) so
deployment can supply real values rather than localhost/hardcoded secrets, and
add JPA/Hibernate batch properties suitable for batch jobs (e.g.
hibernate.jdbc.batch_size, hibernate.order_inserts, hibernate.order_updates,
hibernate.batch_versioned_data and the JDBC rewriteBatchedStatements option via
datasource URL) while keeping ddl-auto as none or validate; ensure placeholders
use secure environment variables and document required env names.
| spring.config.activate.on-profile: qa | ||
| spring: | ||
| datasource: | ||
| url: jdbc:mysql://localhost:3306/loopers | ||
| username: application | ||
| password: application |
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.
qa ํ๋กํ์ผ์ ์ค์ ์ ์์ฑํ์ธ์.
๋ค์ ๋ฌธ์ ๋ค์ด ํ์ธ๋์์ต๋๋ค:
- ๋ฐ์ดํฐ๋ฒ ์ด์ค URL:
localhost:3306์ ๋ฐฐํฌ๋ ํ๊ฒฝ์์ ์๋ํ์ง ์์ต๋๋ค. - ํ๋์ฝ๋ฉ๋ ์๊ฒฉ์ฆ๋ช : ๋ณด์ ์ทจ์ฝ์ ์ ๋๋ค.
- ๋๋ฝ๋ driver-class-name: ๋ฐํ์ ์ค๋ฅ๋ฅผ ์ ๋ฐํ ์ ์์ต๋๋ค.
- ๋๋ฝ๋ Hibernate ๋ฐฐ์น ์ต์ ํ: ๋ฐฐ์น ์ฒ๋ฆฌ ์ฑ๋ฅ์ ์ํฅ์ ์ค๋๋ค.
๐ ์ ์๋ ์์
spring:
datasource:
- url: jdbc:mysql://localhost:3306/loopers
- username: application
- password: application
+ url: ${DB_URL}
+ username: ${DB_USERNAME}
+ password: ${DB_PASSWORD}
+ driver-class-name: com.mysql.cj.jdbc.Driver
+
+ jpa:
+ hibernate:
+ ddl-auto: none
+ properties:
+ hibernate:
+ jdbc:
+ batch_size: 1000
+ order_inserts: true
+ order_updates: true
+ batch_versioned_data: true๐ 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.
| spring.config.activate.on-profile: qa | |
| spring: | |
| datasource: | |
| url: jdbc:mysql://localhost:3306/loopers | |
| username: application | |
| password: application | |
| spring.config.activate.on-profile: qa | |
| spring: | |
| datasource: | |
| url: ${DB_URL} | |
| username: ${DB_USERNAME} | |
| password: ${DB_PASSWORD} | |
| driver-class-name: com.mysql.cj.jdbc.Driver | |
| jpa: | |
| hibernate: | |
| ddl-auto: none | |
| properties: | |
| hibernate: | |
| jdbc: | |
| batch_size: 1000 | |
| order_inserts: true | |
| order_updates: true | |
| batch_versioned_data: true |
๐ค Prompt for AI Agents
In apps/commerce-batch/src/main/resources/application.yml around lines 97-102,
the qa profile uses localhost and hardcoded credentials and is missing
driver-class-name and Hibernate batch settings; change the datasource URL to
reference an environment-configurable host/port/database (do not hardcode
localhost), replace username/password with environment property placeholders or
a credentials store reference, add the proper driver-class-name for MySQL, and
add Hibernate batch tuning properties (e.g.,
spring.jpa.properties.hibernate.jdbc.batch_size and related
order_inserts/order_updates flags) to enable batch optimization.
| spring.config.activate.on-profile: prd | ||
| spring: | ||
| datasource: | ||
| url: jdbc:mysql://localhost:3306/loopers | ||
| username: application | ||
| password: application |
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.
prd ํ๋กํ์ผ์ ์ค์ ์ ์์ฑํ๊ณ ๋ณด์์ ๊ฐํํ์ธ์.
ํ๋ก๋์ ํ๊ฒฝ์ ๋ค์์ ์ฌ๊ฐํ ๋ฌธ์ ๋ค์ด ์์ต๋๋ค:
- ๋ฐ์ดํฐ๋ฒ ์ด์ค URL:
localhost:3306์ ํ๋ก๋์ ์์ ์๋ํ์ง ์์ต๋๋ค. - ํ๋์ฝ๋ฉ๋ ์๊ฒฉ์ฆ๋ช : ํ๋ก๋์ ์๊ฒฉ์ฆ๋ช ์ด ์์ค ์ฝ๋์ ๋ ธ์ถ๋์ด ์์ต๋๋ค. ๋ฐ๋์ ํ๊ฒฝ ๋ณ์๋ ์ํฌ๋ฆฟ ๊ด๋ฆฌ ์์คํ ์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
- ๋๋ฝ๋ driver-class-name: ํ์ ์ค์ ์ ๋๋ค.
- ๋๋ฝ๋ Hibernate ๋ฐฐ์น ์ต์ ํ: ๋ฐฐ์น ์์ ์ฑ๋ฅ์ ์ฌ๊ฐํ ์ํฅ์ ์ค๋๋ค.
- ๋๋ฝ๋ JPA ddl-auto ์ค์ : ๋ช
์์ ์ผ๋ก
none์ผ๋ก ์ค์ ํด์ผ ํฉ๋๋ค.
๐ ์ ์๋ ์์
spring:
datasource:
- url: jdbc:mysql://localhost:3306/loopers
- username: application
- password: application
+ url: ${DB_URL}
+ username: ${DB_USERNAME}
+ password: ${DB_PASSWORD}
+ driver-class-name: com.mysql.cj.jdbc.Driver
+
+ jpa:
+ hibernate:
+ ddl-auto: none
+ properties:
+ hibernate:
+ jdbc:
+ batch_size: 1000
+ order_inserts: true
+ order_updates: true
+ batch_versioned_data: true๋ํ ํ๋ก๋์ ํ๊ฒฝ์์๋ AWS Secrets Manager, HashiCorp Vault, ๋๋ Kubernetes Secrets ๋ฑ์ ์ฌ์ฉํ์ฌ ์๊ฒฉ์ฆ๋ช ์ ๊ด๋ฆฌํ๋ ๊ฒ์ ๊ฐ๋ ฅํ ๊ถ์ฅํฉ๋๋ค.
๐ 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.
| spring.config.activate.on-profile: prd | |
| spring: | |
| datasource: | |
| url: jdbc:mysql://localhost:3306/loopers | |
| username: application | |
| password: application | |
| spring.config.activate.on-profile: prd | |
| spring: | |
| datasource: | |
| url: ${DB_URL} | |
| username: ${DB_USERNAME} | |
| password: ${DB_PASSWORD} | |
| driver-class-name: com.mysql.cj.jdbc.Driver | |
| jpa: | |
| hibernate: | |
| ddl-auto: none | |
| properties: | |
| hibernate: | |
| jdbc: | |
| batch_size: 1000 | |
| order_inserts: true | |
| order_updates: true | |
| batch_versioned_data: true |
๐ค Prompt for AI Agents
In apps/commerce-batch/src/main/resources/application.yml around lines 104-109,
the prd profile currently uses localhost DB URL and hardcoded credentials and is
missing driver-class-name, Hibernate batch optimizations, and JPA ddl-auto;
update the prd profile to point to the real production DB host/port (do not use
localhost), remove hardcoded username/password and read them from environment
variables or a secrets manager (e.g., AWS Secrets Manager/HashiCorp
Vault/Kubernetes Secrets), add the appropriate driver-class-name for MySQL,
enable Hibernate batch settings (e.g., hibernate.jdbc.batch_size and
hibernate.order_inserts/order_updates) to optimize batch jobs, and explicitly
set spring.jpa.hibernate.ddl-auto: none.
| arguments: | ||
| parameters: | ||
| - name: date | ||
| # ํ์ฌ ๋ ์ง๋ฅผ YYYYMMDD ํ์์ผ๋ก ์์ฑ (์: 20240115) | ||
| value: "{{ workflow.creationTimestamp.strftime('%Y%m%d') }}" | ||
| workflowTemplateRef: | ||
| name: daily-ranking-workflow No newline at end of file |
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:
Argo Workflows template functions date formatting strftime
๐ก Result:
Short answer โ two ways:
- Built-in workflow variables (strftime-style parts)
- Argo exposes workflow.creationTimestamp and per-format-character fields like workflow.creationTimestamp.Y, .m, .d, .H, .M, .S etc (these are populated for each supported strftime character). Use those to build parts directly. [1]
- Templating with Sprig (recommended for full formatting)
- Use Sprig template functions to format/modify dates (sprig.date, sprig.dateModify, sprig.now(), sprig.unixEpoch, sprig.toDate). Note Sprig uses Goโs time layout (the reference date 2006-01-02 15:04:05) โ not strftime. Example: '{{=sprig.date("2006-01-02", workflow.creationTimestamp)}}'. [2][3]
References
- Argo workflow variables (creationTimestamp and per-character fields). [1]
- Examples and Sprig usage in Argo (date, dateModify, Go-layout example). [2][3]
๐ Web query:
Argo Workflows sprig template functions date format
๐ก Result:
Short answer โ use Sprig's date functions (Go layouts) inside Argo templates. Key functions: sprig.date, sprig.dateInZone, sprig.dateModify (aka date_modify), sprig.now, sprig.toDate, sprig.unixEpoch. Sprig uses Go's reference time (Mon Jan 2 15:04:05 MST 2006) for format strings.
Examples:
-
Format workflow creation timestamp as YYYY-MM-DD:
value: '{{=sprig.date("2006-01-02", workflow.creationTimestamp)}}'. [1][2] -
Get yesterday's date (YYYY-MM-DD):
value: '{{=sprig.date("2006-01-02", sprig.dateModify("-24h", sprig.now()))}}'. [1][4] -
Format in UTC:
value: '{{=sprig.dateInZone("2006-01-02T15:04:05Z07:00", sprig.now(), "UTC")}}'. [1] -
Parse a string then reformat:
value: '{{=sprig.toDate("2006-01-02T15:04:05Z07:00", inputs.parameters.dt) | sprig.date("02/01/2006")}}'. [1]
Common tokens: "2006"=year, "01"=month, "02"=day, "15:04:05"=24h time; RFC3339 layout is "2006-01-02T15:04:05Z07:00". [1][3]
References: Sprig date functions and layouts documentation. [1][3][2]
๐ Web query:
Argo Workflows creationTimestamp date format syntax
๐ก Result:
Short answer:
- Workflow.creationTimestamp is provided in RFC 3339 (UTC) (example: 2018-08-23T05:42:49Z). [1][2]
How to use/format it in Argo:
- Raw variable: {{workflow.creationTimestamp}}. [1]
- RFC3339 explicit: {{workflow.creationTimestamp.RFC3339}}. [1]
- Use Sprig/Go-style formatting: {{=sprig.date("2006-01-02 15:04:05", workflow.creationTimestamp)}} (Go reference date/layout semantics: 2006-01-02 15:04:05). [3]
Sources:
- Argo Workflow variables / creationTimestamp docs. [1]
- Argo ObjectMeta / creationTimestamp field (RFC3339, UTC). [2]
- Example showing Sprig/Go-layout formatting of workflow.creationTimestamp. [3]
Argo Workflows์์ ์ ํจํ์ง ์์ ํ ํ๋ฆฟ ๋ฌธ๋ฒ์ ๋๋ค.
workflow.creationTimestamp.strftime('%Y%m%d') ๋ฌธ๋ฒ์ Argo Workflows์์ ์ง์ํ์ง ์์ต๋๋ค. Argo๋ .strftime() ๋ฉ์๋๋ฅผ ์ ๊ณตํ์ง ์์ผ๋ฏ๋ก Sprig์ date ํจ์๋ก ๋ณ๊ฒฝํด์ผ ํฉ๋๋ค.
๐ ์ ์๋ ์์ ์ฌํญ
arguments:
parameters:
- name: date
- # ํ์ฌ ๋ ์ง๋ฅผ YYYYMMDD ํ์์ผ๋ก ์์ฑ (์: 20240115)
- value: "{{ workflow.creationTimestamp.strftime('%Y%m%d') }}"
+ # ํ์ฌ ๋ ์ง๋ฅผ YYYYMMDD ํ์์ผ๋ก ์์ฑ (์: 20240115)
+ value: "{{ sprig.date('20060102', workflow.creationTimestamp) }}"์ฐธ๊ณ : Go์ date ํฌ๋งท์ ๋ ํผ๋ฐ์ค ์๊ฐ Mon Jan 2 15:04:05 MST 2006์ ๊ธฐ์ค์ผ๋ก ํฉ๋๋ค. 20060102๋ YYYYMMDD ํ์์ ์๋ฏธํฉ๋๋ค.
๐ 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.
| arguments: | |
| parameters: | |
| - name: date | |
| # ํ์ฌ ๋ ์ง๋ฅผ YYYYMMDD ํ์์ผ๋ก ์์ฑ (์: 20240115) | |
| value: "{{ workflow.creationTimestamp.strftime('%Y%m%d') }}" | |
| workflowTemplateRef: | |
| name: daily-ranking-workflow | |
| arguments: | |
| parameters: | |
| - name: date | |
| # ํ์ฌ ๋ ์ง๋ฅผ YYYYMMDD ํ์์ผ๋ก ์์ฑ (์: 20240115) | |
| value: "{{ sprig.date('20060102', workflow.creationTimestamp) }}" | |
| workflowTemplateRef: | |
| name: daily-ranking-workflow |
๐ค Prompt for AI Agents
In argo-workflows/weekly-ranking-workflow.yaml around lines 139 to 145, the
parameter value uses unsupported Python-like .strftime() on
workflow.creationTimestamp; replace it with Argo/Sprig date formatting. Change
the value to use Sprig: value: "{{ workflow.creationTimestamp | toDate | date
\"20060102\" }}" so the timestamp is converted to a date and formatted as
YYYYMMDD (Go reference format 20060102).
๐ Summary
์ฃผ๊ฐ/์๊ฐ ์ํ ๋ญํน ์์คํ ๊ตฌํ
๐ฌ Review Points
1. ๋ญํน ์ง๊ณ ํ ์ด๋ธ ๊ตฌ์กฐ (commerce-batch)
Materialized View (MV) ํ ์ด๋ธ
mv_product_rank_weekly: ์ฃผ๊ฐ TOP 100mv_product_rank_monthly: ์๊ฐ TOP 100๊ณตํต ์ปฌ๋ผ
product_idrankinglike_countorder_countview_countperiod_yyyyww / period_yyyymm๋ฆฌ๋ทฐ ํฌ์ธํธ
period_yyyyww / period_yyyymm) ์ค๊ณ์ ํ์ฅ์ฑ2. ๋ญํน ์ง๊ณ ๋ฐฐ์น ์ฒ๋ฆฌ ํ๋ฆ
์ ์ฒด ์ง๊ณ ํ์ดํ๋ผ์ธ
10๋ถ ๋จ์ ์์ฒ ๋ฐ์ดํฐ ์ ์ฌ
product_metricsํ ์ด๋ธ์ 10๋ถ ๋จ์ ์งํ ์ ์ฅ์ผ๊ฐ ์ง๊ณ
product_metrics_daily์์ฑ์ฃผ๊ฐ/์๊ฐ ์ง๊ณ
product_metrics_daily๊ธฐ์ค์ผ๋ก TOP 100 ์ฐ์ถ๋ฆฌ๋ทฐ ํฌ์ธํธ
10๋ถ โ ์ผ๊ฐ โ ์ฃผ๊ฐ/์๊ฐ๊ตฌ์กฐ์ ์ ์ ์ฑ3. ๋ฐฐ์น ์คํ ๋ช ๋ น ๋ฐ Job ๊ตฌ์ฑ
์ผ๊ฐ ๋ญํน ๋ฐฐ์น
Step ๊ตฌ์ฑ
clearDailyWorkingTablesStepdailyRankingStepproduct_metrics๊ธฐ์ค ์ํ๋ณ ์ผ๊ฐ ์งํ ์ง๊ณdailyDataValidationStepdailyTableSwapStepproduct_metrics_daily๋ก ์์์ ๊ต์ฒด์ฃผ๊ฐ ๋ญํน ๋ฐฐ์น
Step ๊ตฌ์ฑ
weeklyDataProcessingStepweeklyTop100MVUpdateStep(
ranking:weekly:{YYYYWW}W:*)์๊ฐ ๋ญํน ๋ฐฐ์น
Step ๊ตฌ์ฑ
monthlyDataProcessingStepmonthlyTop100MVUpdateStep(
ranking:monthly:{YYYYMM}M:*)4. Redis ์บ์ ๊ตฌ์กฐ (commerce-api)
์ฃผ๊ฐ
ranking:weekly:{YYYYWW}W์๊ฐ
ranking:monthly:{YYYYMM}M๋ฆฌ๋ทฐ ํฌ์ธํธ
5. ๋ญํน API ์ค๊ณ
API Endpoint
๋ ์ง ํฌ๋งท๋ณ ์ฒ๋ฆฌ
YYYYMMDDโ ์ผ๊ฐYYYYWWโ ์ฃผ๊ฐYYYYMMโ ์๊ฐRankingDateParser๋ฅผ ํตํด ๋ ์ง ํ์ ๊ฒ์ฆ ๋ฐ ๋ถ๊ธฐ๋ฆฌ๋ทฐ ํฌ์ธํธ
6. Redis Fallback ์ ๋ต
โ ์ง๋ฌธ
Q1. ์ง๊ณ ํ ์ด๋ธ ๊ตฌ์กฐ ๊ด๋ จ
์ด๊ธฐ ์งํ ํ ์ด๋ธ์ 10๋ถ ๋จ์๋ก ์ค๊ณํ๋ฉด์, ์ฃผ๊ฐยท์๊ฐ ๋ญํน ์ง๊ณ ์ ๊ธฐ์ค์ผ๋ก ์ฌ์ฉํ ์ผ๊ฐ ๋งคํธ๋ฆญ์ค ํ ์ด๋ธ(
product_metrics_daily)์ ์ถ๊ฐ๋ก ์์ฑํ์ต๋๋ค. ์ด๋ก ์ธํด ์ง๊ณ ๋จ๊ณ๊ฐ10๋ถ โ ์ผ๊ฐ โ ์ฃผ๊ฐ/์๊ฐ์ผ๋ก ๋๋์๋๋ฐ, ํ์ฌ ๊ตฌ์กฐ๊ฐ ๋ถํ์ํ๊ฒ ๋ง์ ์ค๊ฐ ํ ์ด๋ธ์ ์ฌ์ฉํ๊ณ ์๋์ง ๊ณ ๋ฏผ์ด ์์ต๋๋ค.๋ง์ฝ 10๋ถ ๋จ์ ํ ์ด๋ธ์ ๊ธฐ์ค์ผ๋ก ๋ฐ๋ก ์ฃผ๊ฐยท์๊ฐ ๋ญํน์ ์ฐ์ถํ๋ค๋ฉด, ์ผ๋ฐ์ ์ผ๋ก๋ ์ด๋ค ๋ฐฉ์์ ์ค๊ณ๊ฐ ๋ ์ ์ ํ์ง ๊ถ๊ธํฉ๋๋ค.
Q2. ๋ญํน API ํตํฉ ์ค๊ณ ๊ด๋ จ
๊ณผ์ ์ ๋ณด์ "๊ธฐ์กด Ranking ์ ์ ๊ณตํ๋ API ๋ก ์ผ๊ฐ, ์ฃผ๊ฐ, ์๊ฐ ๋ญํน์ ์ ๊ณตํ ์ ์๋๋ก ๊ฐ์ ํฉ๋๋ค" ๋ผ๋ ๋ฌธ๊ฐ๊ฐ ์์ด ๋จ์ผ ์๋ํฌ์ธํธ๋ก ์ผ๊ฐยท์ฃผ๊ฐยท์๊ฐ ๋ญํน์ ๋ชจ๋ ์ ๊ณตํ๋๋ก ์ค๊ณํ์ต๋๋ค. ๊ธฐ๊ฐ๋ณ ๋ญํน์ ํ๋์ API๋ก ํตํฉํด์ผ ํ ์ด์ ๊ฐ ์์๋์ง, ์ผยท์ฃผยท์ ๋ญํน์ ๋ถ๋ฆฌ๋ API๋ก ์ ๊ณตํ๋ ๊ฒ๋ ์๊ด์๋ ๋ถ๋ถ์ด์๋์ง ๊ถ๊ธํฉ๋๋ค.
Q3. Argo Workflow ์ ์ฉ ์ฌ๋ถ ๊ด๋ จ
์ง๊ณ ๋ฐฐ์น ์ดํ ์บ์๋ฅผ ์์ฑํ๋ ๋ก์ง๊ณผ, 30๋ถ ๋จ์ ์ฌ์๋ ํ๋ฆ์ Argo Workflow๋ก ๊ตฌ์ฑํ์ฌ
Spring Batch ์๋ฒ๊ฐ ์ค๋จ๋๋๋ผ๋ ํ์ ์ฌ์๋๊ฐ ๊ฐ๋ฅํ๋๋ก ๊ตฌํํ๋ ๋ฐฉ์์ ๊ณ ๋ คํ์ต๋๋ค.
๋ค๋ง ํ์ฌ ์๊ตฌ์ฌํญ ๋๋น ๋ณต์ก๋๊ฐ ๊ณผ๋ํ๋ค๊ณ ํ๋จํด ์ด๋ฒ ๊ตฌํ์์๋ ์ ์ธํ์ต๋๋ค.
Argo Workflow๊ฐ ์ค์ ๋ก ๊ฐ์ ์ ๋ฐํํ๋ ๋ฌธ์ ์ ํ์ด๋, ๋ฐ๋์ ๋์ ์ ๊ณ ๋ คํด๋ณผ ๋งํ ํ๊ฒฝ์ด ์ด๋ค ๊ฒฝ์ฐ์ธ์ง ๋ฃ๊ณ ์ถ์ต๋๋ค.
โ Checklist
๐งฑ Spring Batch
๐งฉ Ranking API
๐ References
Summary by CodeRabbit
Release Notes
New Features
Improvements
โ๏ธ Tip: You can customize this high-level summary in your review settings.