-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-9] Product Ranking with Redis #218
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: lim-jaein
Are you sure you want to change the base?
Conversation
- 경로 : commerce-api -> shared - PaymentSucceededEvent 를 OrderPaidEvent로 변경
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Walkthrough이번 변경은 이벤트 패키지 구조를 재정렬하고, PaymentSucceededEvent를 주문 항목 정보를 포함하는 OrderPaidEvent로 대체하며, 상품 조회 이벤트 처리 기능을 추가하고, ProductMetrics에 판매액 추적 기능을 추가합니다. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
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: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/productmetrics/ProductMetricsJpaRepository.java (1)
17-26: 다른 INSERT 쿼리에 sales_amount 컬럼 누락
upsertLikeCount,upsertUnlikeCount,upsertViewCount쿼리의 INSERT 절에sales_amount컬럼이 포함되어 있지 않습니다. 엔티티에서salesAmount가nullable = false로 설정되어 있으므로, 해당 쿼리를 통해 새 레코드가 삽입되면 제약 조건 위반이 발생할 수 있습니다.🔎 수정 예시 (upsertLikeCount)
@Query(value = """ - INSERT INTO product_metrics (product_id, like_count, sales_count, view_count, created_at, updated_at) - VALUES (:productId, 1, 0, 0, NOW(), NOW()) + INSERT INTO product_metrics (product_id, like_count, sales_count, sales_amount, view_count, created_at, updated_at) + VALUES (:productId, 1, 0, 0, 0, NOW(), NOW()) ON DUPLICATE KEY UPDATE like_count = like_count + 1, updated_at = NOW() """,
upsertUnlikeCount와upsertViewCount에도 동일하게 적용이 필요합니다.
🧹 Nitpick comments (3)
modules/shared/src/main/java/com/loopers/messaging/event/OrderPaidEvent.java (1)
7-18:items리스트에 대한 방어적 복사를 고려해 주세요.현재
items리스트가 그대로 저장되므로, 호출자가 전달한 리스트를 수정하면 이벤트 내부 상태가 변경될 수 있습니다. 불변성을 보장하려면 방어적 복사를 적용하는 것이 좋습니다.🔎 방어적 복사 적용 예시
public static OrderPaidEvent of(Long orderId, List<OrderItemData> items) { return new OrderPaidEvent( orderId, - items, + items == null ? List.of() : List.copyOf(items), Instant.now() ); }apps/commerce-api/src/main/java/com/loopers/application/product/event/ProductViewedEventHandler.java (1)
24-24: 로그 레벨을debug로 낮추는 것을 고려해 주세요.상품 조회 이벤트는 빈번하게 발생할 수 있으므로, 프로덕션 환경에서
INFO레벨 로그가 과도하게 쌓일 수 있습니다.🔎 제안
- log.info("ProductViewedEvent received: productId={}, occurredAt={}", event.productId(), event.occurredAt()); + log.debug("ProductViewedEvent received: productId={}, occurredAt={}", event.productId(), event.occurredAt());apps/commerce-streamer/src/main/java/com/loopers/domain/productmetrics/ProductMetricsService.java (1)
47-49: 읽기 전용 트랜잭션 고려읽기 전용 쿼리에
@Transactional(readOnly = true)를 추가하면 최적화 힌트를 제공하고, 다른 메서드와 일관성을 유지할 수 있습니다.🔎 수정 제안
+ @Transactional(readOnly = true) public Optional<ProductMetrics> findByProductId(Long productId) { return productMetricsRepository.findByProductId(productId); }
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.javaapps/commerce-api/src/main/java/com/loopers/application/like/event/LikeEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/order/event/OrderEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/order/event/OrderPaidEvent.javaapps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.javaapps/commerce-api/src/main/java/com/loopers/application/payment/PaymentProcessService.javaapps/commerce-api/src/main/java/com/loopers/application/payment/event/PaymentSucceededEvent.javaapps/commerce-api/src/main/java/com/loopers/application/product/event/ProductViewedEventHandler.javaapps/commerce-streamer/src/main/java/com/loopers/domain/productmetrics/ProductMetrics.javaapps/commerce-streamer/src/main/java/com/loopers/domain/productmetrics/ProductMetricsRepository.javaapps/commerce-streamer/src/main/java/com/loopers/domain/productmetrics/ProductMetricsService.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/productmetrics/ProductMetricsJpaRepository.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/productmetrics/ProductMetricsRepositoryImpl.javamodules/shared/src/main/java/com/loopers/messaging/event/LikeCanceledEvent.javamodules/shared/src/main/java/com/loopers/messaging/event/LikeCreatedEvent.javamodules/shared/src/main/java/com/loopers/messaging/event/OrderPaidEvent.javamodules/shared/src/main/java/com/loopers/messaging/event/ProductViewedEvent.java
💤 Files with no reviewable changes (2)
- apps/commerce-api/src/main/java/com/loopers/application/payment/event/PaymentSucceededEvent.java
- apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderPaidEvent.java
🧰 Additional context used
🧠 Learnings (8)
📚 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:
modules/shared/src/main/java/com/loopers/messaging/event/OrderPaidEvent.javaapps/commerce-api/src/main/java/com/loopers/application/order/event/OrderEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.javamodules/shared/src/main/java/com/loopers/messaging/event/LikeCanceledEvent.javaapps/commerce-api/src/main/java/com/loopers/application/product/event/ProductViewedEventHandler.javamodules/shared/src/main/java/com/loopers/messaging/event/LikeCreatedEvent.javaapps/commerce-api/src/main/java/com/loopers/application/like/event/LikeEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java
📚 Learning: 2025-12-19T09:30:12.459Z
Learnt from: HongChangMo
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 198
File: apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventListener.java:0-0
Timestamp: 2025-12-19T09:30:12.459Z
Learning: In the loopers-spring-java-template repository's commerce-api module, when entities are managed within a transactional context (e.g., Transactional methods), prefer relying on JPA dirty checking for automatic persistence rather than explicit save() calls. Both Payment and Order entities in PaymentEventListener use this pattern, with state changes automatically flushed on transaction commit.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/product/event/ProductViewedEventHandler.javaapps/commerce-streamer/src/main/java/com/loopers/domain/productmetrics/ProductMetricsService.javaapps/commerce-api/src/main/java/com/loopers/application/like/event/LikeEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/payment/PaymentProcessService.javaapps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java
📚 Learning: 2025-11-30T15:44:28.336Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 48
File: apps/commerce-api/src/main/kotlin/com/loopers/application/order/OrderFacade.kt:80-83
Timestamp: 2025-11-30T15:44:28.336Z
Learning: User ghojeong prefers explicit state management over transaction rollback for handling payment failures in order creation flow. When payment fails after order creation, the order should be explicitly marked as cancelled and persisted, rather than relying on transaction rollback.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/payment/PaymentProcessService.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-api/src/main/java/com/loopers/application/like/LikeFacade.javamodules/shared/src/main/java/com/loopers/messaging/event/LikeCanceledEvent.javamodules/shared/src/main/java/com/loopers/messaging/event/LikeCreatedEvent.javaapps/commerce-api/src/main/java/com/loopers/application/like/event/LikeEventHandler.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/productmetrics/ProductMetricsService.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-api/src/main/java/com/loopers/application/like/event/LikeEventHandler.java
📚 Learning: 2025-12-07T12:45:45.556Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 52
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/payment/Payment.kt:160-181
Timestamp: 2025-12-07T12:45:45.556Z
Learning: In the Payment.confirmPayment method in com.loopers.domain.payment.Payment, the when-branch order is intentionally designed with business logic checks before timeout: (1) check SUCCESS/FAILED status first, (2) check for null match (business failure), (3) check timeout only as last resort for PENDING transactions. If matched transaction is PENDING and not timed out, no branch matches and payment stays IN_PROGRESS for scheduler retry. This design prioritizes business outcome determination over time-based fallback.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentProcessService.javaapps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java
📚 Learning: 2025-12-16T09:44:15.945Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 58
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/payment/PaymentService.kt:157-183
Timestamp: 2025-12-16T09:44:15.945Z
Learning: In PaymentService.requestPgPayment (apps/commerce-api/src/main/kotlin/com/loopers/domain/payment/PaymentService.kt), PG payment requests are idempotent using paymentId as the idempotency key, so Retryable covering the entire method including the pgClient.requestPayment call is safe and will not cause duplicate charges even if retries occur due to ObjectOptimisticLockingFailureException.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentProcessService.javaapps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java
🧬 Code graph analysis (2)
apps/commerce-api/src/main/java/com/loopers/application/product/event/ProductViewedEventHandler.java (2)
apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderEventHandler.java (1)
Slf4j(20-82)apps/commerce-api/src/main/java/com/loopers/application/like/event/LikeEventHandler.java (1)
Component(16-83)
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentProcessService.java (2)
apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderEventHandler.java (1)
Slf4j(20-82)apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java (1)
Slf4j(22-86)
🔇 Additional comments (15)
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (1)
8-9: LGTM!이벤트 클래스들이
com.loopers.messaging.event패키지로 이동한 것에 맞춰 import가 올바르게 업데이트되었습니다. 이는 이벤트를 공유 모듈로 이동시켜 모듈 간 재사용성을 높이는 PR의 목표와 일치합니다.apps/commerce-api/src/main/java/com/loopers/application/like/event/LikeEventHandler.java (1)
7-8: LGTM!
LikeCanceledEvent와LikeCreatedEvent의 import 경로가 공유 메시징 모듈로 올바르게 업데이트되었습니다.modules/shared/src/main/java/com/loopers/messaging/event/LikeCreatedEvent.java (1)
1-15: LGTM!
LikeCreatedEvent가 공유 메시징 모듈로 적절히 이동되었습니다. 이 구조는 Kafka 파이프라인에서 이 이벤트를 사용하는 패턴과 일치하며, 학습된 내용에 따르면 productId를 aggregateId로 사용하여 동일 상품의 좋아요 이벤트가 같은 파티션으로 전달되도록 보장합니다.modules/shared/src/main/java/com/loopers/messaging/event/LikeCanceledEvent.java (1)
1-15: LGTM!
LikeCanceledEvent가LikeCreatedEvent와 동일하게 공유 메시징 모듈로 이동되었습니다. 두 이벤트가 동일한 구조와 팩토리 패턴을 유지하고 있어 일관성이 좋습니다.apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java (1)
11-21: LGTM!
OrderPaidEvent로의 전환과 필요한 import 추가가 적절합니다.modules/shared/src/main/java/com/loopers/messaging/event/ProductViewedEvent.java (1)
1-15: LGTM!
ProductViewedEvent가 다른 메시징 이벤트(LikeCreatedEvent,LikeCanceledEvent)와 동일한 패턴으로 구현되었습니다. 학습된 내용에 따르면 이 이벤트는 Kafka로 발행되는 5개의 도메인 이벤트 중 하나입니다.modules/shared/src/main/java/com/loopers/messaging/event/OrderPaidEvent.java (1)
20-24: LGTM!
OrderItemData내부 레코드가BigDecimal을 사용하여 금액을 표현하고 있어 정밀도 손실 없이 금액 계산이 가능합니다.apps/commerce-api/src/main/java/com/loopers/application/product/event/ProductViewedEventHandler.java (1)
22-41: LGTM!
LikeEventHandler와 동일한 패턴으로 outbox 이벤트를 처리하고 있습니다.catalog-events토픽을 사용하고 aggregate type으로 "PRODUCT"를 지정한 것이 일관성 있습니다.apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentProcessService.java (3)
4-7: 필요한 import가 올바르게 추가되었습니다.새로운 이벤트 구조와 주문 조회 로직에 필요한 import가 적절하게 추가되었습니다.
Also applies to: 14-15
23-23: OrderService 의존성 주입이 적절합니다.PG 결제 후 주문 정보 조회를 위한 OrderService 의존성이 올바르게 추가되었습니다.
38-38: 메서드 시그니처 변경이 적절합니다.userId 파라미터 추가는 실패 이벤트 처리(line 52)에 필요하며, 올바른 변경입니다.
apps/commerce-api/src/main/java/com/loopers/application/order/event/OrderEventHandler.java (1)
9-9: 이벤트 타입 리팩토링이 일관되게 적용되었습니다.PaymentSucceededEvent에서 OrderPaidEvent로의 전환이 세 개의 이벤트 핸들러 메서드에 일관되게 적용되었습니다. 트랜잭션 처리, 비동기 실행, outbox 패턴이 모두 올바르게 유지되고 있습니다.
Also applies to: 34-35, 51-51, 64-64, 66-66
apps/commerce-streamer/src/main/java/com/loopers/domain/productmetrics/ProductMetrics.java (1)
29-31: 타입 일관성 확인 필요엔티티에서는
salesAmount를long타입으로 저장하고 있지만, 서비스/리포지토리 레이어에서는BigDecimal을 사용합니다. 금액이 최소 화폐 단위(예: 원)로 저장되어 소수점이 필요 없다면 문제없지만, 소수점 금액이 발생할 수 있다면 정밀도 손실이 발생할 수 있습니다.apps/commerce-streamer/src/main/java/com/loopers/infrastructure/productmetrics/ProductMetricsRepositoryImpl.java (1)
31-34: LGTM!새로운
amount파라미터를 올바르게 전달하고 있습니다.apps/commerce-streamer/src/main/java/com/loopers/domain/productmetrics/ProductMetricsRepository.java (1)
14-14: LGTM!인터페이스 시그니처가 구현체와 일관되게 업데이트되었습니다.
| List<OrderPaidEvent.OrderItemData> items = | ||
| order.getItems().stream() | ||
| .map(item -> new OrderPaidEvent.OrderItemData( | ||
| item.getProductId(), | ||
| item.getQuantity(), | ||
| item.getTotalPrice().getAmount() | ||
| )).toList(); |
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 OrderItem class and price-related methods
find . -name "*.java" -path "*/domain/order/*" | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 876
🏁 Script executed:
# Search for OrderItem class definition
rg -l "class OrderItem" --type=javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 235
🏁 Script executed:
# Look for getTotalPrice and getUnitPrice methods in order-related classes
rg -n "getTotalPrice|getUnitPrice|getPrice\(" --type=java apps/commerce-api/src/main/java/com/loopers/domain/order/ -A 3 -B 1Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 597
🏁 Script executed:
# Search for OrderItemData class definition
rg -n "class OrderItemData" --type=java -A 10Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
🏁 Script executed:
# Search for OrderItemData anywhere in the codebase
rg -n "OrderItemData" --type=java -B 2 -A 5Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 6069
🏁 Script executed:
# Read the OrderItem class to understand its structure
cat -n apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2326
🏁 Script executed:
# Search for all references to unitPrice or totalPrice in OrderItem-related files
rg -n "unitPrice|totalPrice" --type=java apps/commerce-api/src/main/java/com/loopers/domain/order/Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1770
getTotalPrice() 대신 getUnitPrice()를 사용하세요.
OrderItemData의 unitPrice 필드는 상품 단가를 의도하고 있으나, 현재 코드는 item.getTotalPrice().getAmount()를 전달하고 있습니다. OrderItem의 totalPrice는 수량 × 단가이므로, 다운스트림 컨슈머에서 잘못된 계산이 발생할 수 있습니다.
같은 로직이 PaymentProcessService에서는 item.getUnitPrice().getAmount()로 올바르게 구현되어 있습니다. 일관성을 위해 62번 줄을 item.getUnitPrice().getAmount()로 변경하세요.
🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java
around lines 57 to 63, the code passes item.getTotalPrice().getAmount() into
OrderItemData.unitPrice which should be the per-item unit price; replace that
call with item.getUnitPrice().getAmount() so the unitPrice field receives the
single-item price (matching PaymentProcessService) and avoid downstream
miscalculations.
| Order order = orderService.findOrderById(orderId) | ||
| .orElseThrow(() -> new IllegalStateException("결제된 주문 정보를 찾을 수 없습니다. orderId: " + orderId)); | ||
|
|
||
| List<OrderItemData> orderItemDataList = order.getItems().stream() | ||
| .map(item -> new OrderItemData(item.getProductId(), item.getQuantity(), item.getUnitPrice().getAmount())) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| eventPublisher.publishEvent(OrderPaidEvent.of(orderId, orderItemDataList)); |
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.
중요: OrderItemData 생성 시 totalPrice를 사용해야 합니다.
Line 46에서 item.getUnitPrice().getAmount()를 사용하고 있지만, PaymentFacade.java(line 44)에서는 동일한 목적으로 item.getTotalPrice().getAmount()를 사용합니다. OrderItemData의 세 번째 파라미터는 해당 주문 항목의 총 금액(수량 × 단가)을 나타내야 하므로, unitPrice 대신 totalPrice를 사용해야 합니다.
이 불일치로 인해:
- 판매 데이터가 실제보다 적게 집계됩니다
- POINT 결제(PaymentFacade)와 PG 결제(PaymentProcessService) 간 데이터 불일치가 발생합니다
- 상품 랭킹 집계에 오류가 발생합니다
🔎 수정 제안
List<OrderItemData> orderItemDataList = order.getItems().stream()
- .map(item -> new OrderItemData(item.getProductId(), item.getQuantity(), item.getUnitPrice().getAmount()))
+ .map(item -> new OrderItemData(item.getProductId(), item.getQuantity(), item.getTotalPrice().getAmount()))
.collect(Collectors.toList());🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentProcessService.java
around lines 42 to 49, OrderItemData is being constructed with
item.getUnitPrice().getAmount() but must use the item's total price; replace the
third constructor argument to use item.getTotalPrice().getAmount() so the
OrderItemData reflects quantity × unit price (matching PaymentFacade) and then
publish the event with the corrected list.
| @RequiredArgsConstructor | ||
| public class ProductMetricsService { | ||
|
|
||
| private final ProductMetricsJpaRepository productMetricsRepository; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
추상화 위반: 구현체 직접 주입
ProductMetricsRepository 인터페이스 대신 ProductMetricsJpaRepository 구현체를 직접 주입하고 있습니다. 도메인 레이어에서 인프라 레이어에 직접 의존하면 테스트 용이성과 유연성이 저하됩니다.
🔎 수정 제안
- private final ProductMetricsJpaRepository productMetricsRepository;
+ private final ProductMetricsRepository productMetricsRepository;import 문도 함께 수정해야 합니다:
-import com.loopers.infrastructure.productmetrics.ProductMetricsJpaRepository;
+import com.loopers.domain.productmetrics.ProductMetricsRepository;Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/domain/productmetrics/ProductMetricsService.java
around line 17, the service is directly depending on the implementation
ProductMetricsJpaRepository which breaks abstraction; change the field and
constructor parameter type to the interface ProductMetricsRepository and update
the import to the interface package; ensure any casts or Jpa-specific calls are
removed or moved to the infrastructure layer so the service only uses the
repository interface methods, and update tests/beans to provide the Jpa
implementation where needed.
| if (amount.compareTo(BigDecimal.ZERO) < 0){ | ||
| log.warn("판매 금액이 0 이하일 수 없습니다. 금액:{}, 상품ID:{}", amount, productId); | ||
| return; | ||
| } |
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.
유효성 검사 로직과 로그 메시지 불일치
조건은 < 0 (음수만 거부)이지만, 로그 메시지는 "0 이하일 수 없습니다"로 되어 있어 불일치합니다. 또한 amount가 null일 경우 NullPointerException이 발생합니다.
🔎 수정 제안
- if (amount.compareTo(BigDecimal.ZERO) < 0){
- log.warn("판매 금액이 0 이하일 수 없습니다. 금액:{}, 상품ID:{}", amount, productId);
+ if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
+ log.warn("판매 금액이 0 미만일 수 없습니다. 금액:{}, 상품ID:{}", amount, productId);
return;
}또는 0도 거부해야 한다면:
- if (amount.compareTo(BigDecimal.ZERO) < 0){
+ if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {🤖 Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/domain/productmetrics/ProductMetricsService.java
around lines 25 to 28, the validation condition currently checks
amount.compareTo(BigDecimal.ZERO) < 0 but the log states "0 이하일 수 없습니다" and a
null amount will throw NullPointerException; update the validation to first
guard against a null amount (log and return when amount == null), then either
change the condition to <= 0 if zero should also be rejected (keep the existing
log) or keep < 0 and change the log text to indicate "음수일 수 없습니다" so message
matches the check; implement one of these two fixes consistently.
| INSERT INTO product_metrics (product_id, like_count, sales_count, sales_amount, view_count, created_at, updated_at) | ||
| VALUES (:productId, 0, :quantity, amount, 0, NOW(), 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.
SQL 파라미터 바인딩 오류
VALUES 절에서 :amount 대신 amount로 작성되어 있어 파라미터 바인딩이 되지 않습니다. 런타임에 SQL 오류가 발생합니다.
🔎 수정 제안
- VALUES (:productId, 0, :quantity, amount, 0, NOW(), NOW())
+ VALUES (:productId, 0, :quantity, :amount, 0, NOW(), NOW())📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| INSERT INTO product_metrics (product_id, like_count, sales_count, sales_amount, view_count, created_at, updated_at) | |
| VALUES (:productId, 0, :quantity, amount, 0, NOW(), NOW()) | |
| INSERT INTO product_metrics (product_id, like_count, sales_count, sales_amount, view_count, created_at, updated_at) | |
| VALUES (:productId, 0, :quantity, :amount, 0, NOW(), NOW()) |
🤖 Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/productmetrics/ProductMetricsJpaRepository.java
around lines 42 to 43, the INSERT uses an unbound literal "amount" instead of
the named parameter ":amount", causing SQL parameter binding to fail at runtime;
change "amount" to ":amount" in the VALUES clause and ensure the repository
method supplies a matching ":amount" parameter (correct name and type) when
executing the query.
- 랭킹 Page 조회 시 상품정보도 함께 출력
- 5분마다 스케줄러를 통해 동기화
📈 Ranking Consumer
⚾ Ranking API
@coderabbitai 체크리스트 확인해 |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 64070 🏁 Script executed: Length of output: 10544 @looper88888 체크리스트를 확인했습니다! 각 항목별로 검증 결과를 알려드릴게요. 📈 Ranking Consumer✅ 랭킹 ZSET 의 TTL, 키 전략을 적절하게 구성하였다TTL 설정:
키 전략:
✅ 날짜별로 적재할 키를 계산하는 기능을 만들었다
public String rankingKey(LocalDate date) {
return "ranking:all:" + date.format(DateTimeFormatter.BASIC_ISO_DATE);
}✅ 이벤트가 발생한 후, ZSET 에 점수가 적절하게 반영된다점수 가중치:
ZSET 업데이트:
⚾ Ranking API✅ 랭킹 Page 조회 시 정상적으로 랭킹 정보가 반환된다
✅ 랭킹 Page 조회 시 단순히 상품 ID 가 아닌 상품정보가 Aggregation 되어 제공된다RankingFacade에서 다음 정보들을 aggregation:
응답에 포함되는 정보:
✅ 상품 상세 조회 시 해당 상품의 순위가 함께 반환된다 (순위에 없다면 null)
Long rank = rankingService.getRanking(LocalDate.now(), productId);
ProductDetailInfo.of(productService.getProductDetail(productId), rank)
모든 체크리스트 항목이 올바르게 구현되었습니다! 🎉 |
📌 Summary
조회/좋아요/주문 이벤트를 컨슘하여 Redis ZSET 기반 랭킹 점수에 반영했습니다.
각 이벤트 발생 시 ZINCRBY를 사용해 점수를 실시간으로 누적하며, 5분마다 스케줄러를 통해 랭킹 데이터를 재집계 및 동기화합니다.
랭킹 점수는 아래와 같이 계산했습니다.
또한 콜드스타트를 해소하고, 단기 트렌드를 반영하기 위해 날짜별 점수에 가중치를 적용했습니다.
랭킹 Page를 조회할 수 있는 API를 구현하고, 상품 상세 조회 시 해당 상품의 랭킹 정보를 추가했습니다.
💬 Review Points
랭킹 멱등성 처리를 하는게 좋을지?
이전 주차에서 진행했던 집계 이벤트의 멱등성 처리가 이미 되어있으니, 그 부분에 랭킹 스코어 처리를 넣을지 고민을 했었는데, 이벤트 컨슘 시 하나의 책임만 가지게하는 것이 성능적으로도 의미적으로도 맞을 것 같아 별도의 컨슈머를 두었습니다.
다만 멱등성 처리를 추가로 해야할 지 고민하다가 5분 마다 동기화하는 스케줄러를 두어 해결해보려고 했습니다.
실무에서는 랭킹에 대한 멱등성 관련 처리를 더 타이트하게 가져가시는지 궁금합니다.
Long-tail 해소 방안이 어떤게 있을까요?
랭킹을 100개만 뽑아서 본다고해도, 아예 랭킹에 오를 기회가 없는 상품들이 있을텐데 이런 건들은 따로 고려하는 방식이 있을지 궁금합니다.
✅ Checklist
📈 Ranking Consumer
⚾ Ranking API
📎 References
Summary by CodeRabbit
릴리스 노트
새로운 기능
리팩토링
✏️ Tip: You can customize this high-level summary in your review settings.