-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-9] Product Ranking with Redis #216
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
Conversation
Feature/week9 ranking
[volume-9] Product Ranking with Redis
Walkthroughμμ κ΄λ¦¬ κΈ°λ₯μ μΆκ°νμ¬ Redis μΊμ κΈ°λ°μ μΌμΌ μ ν μμ μ‘°ν λ° κ°±μ μ μ§μν©λλ€. μ ν μ‘°ν μ μμ μ 보λ₯Ό ν¬ν¨νλλ‘ ν΅ν©νκ³ , μ΄λ²€νΈ κΈ°λ° μ μ λμ λ° μλ μ΄μ κΈ°λ₯μ μ 곡ν©λλ€. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant RankingV1Controller
participant RankingFacade
participant RankingService
participant RankingCacheService
participant Redis
participant ProductService
participant Database
participant BrandService
Client->>RankingV1Controller: GET /api/v1/rankings?date=yyyyMMdd&page=1&size=20
activate RankingV1Controller
RankingV1Controller->>RankingFacade: getDailyRanking(GetDailyRankingCommand)
deactivate RankingV1Controller
activate RankingFacade
RankingFacade->>RankingService: getRanking(date, page, size)
activate RankingService
RankingService->>RankingCacheService: getRankingRange(date, start, end)
deactivate RankingService
activate RankingCacheService
RankingCacheService->>Redis: ZREVRANGE ranking:all:yyyyMMdd (with scores)
activate Redis
Redis-->>RankingCacheService: List[RankingItem]
deactivate Redis
RankingCacheService-->>RankingFacade: List[Ranking]
deactivate RankingCacheService
rect rgb(200, 220, 240)
note over RankingFacade: μν λ°μ΄ν° μ‘°ν λ° λ³΄κ°
RankingFacade->>ProductService: findProductsByIds(productIds)
activate ProductService
ProductService->>Database: Query Product entities
activate Database
Database-->>ProductService: List[Product]
deactivate Database
ProductService-->>RankingFacade: List[Product]
deactivate ProductService
RankingFacade->>BrandService: Brand name lookup
activate BrandService
BrandService->>Database: Query Brand entities
activate Database
Database-->>BrandService: Brand data
deactivate Database
BrandService-->>RankingFacade: Brand names
deactivate BrandService
end
RankingFacade-->>RankingV1Controller: RankingInfo (enriched with product details)
deactivate RankingFacade
activate RankingV1Controller
RankingV1Controller->>RankingV1Controller: Convert to DailyRankingListResponse
RankingV1Controller-->>Client: ApiResponse<DailyRankingListResponse> (HTTP 200)
deactivate RankingV1Controller
Estimated code review effortπ― 4 (Complex) | β±οΈ ~45 λΆ Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touchesβ Failed checks (1 warning)
β Passed checks (2 passed)
β¨ Finishing touches
π§ͺ Generate unit tests (beta)
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: 3
π§Ή Nitpick comments (7)
apps/commerce-api/src/main/java/com/loopers/interfaces/listener/KafkaOutboxEventListener.java (1)
25-32: μ€λ³΅ λ§€ν λ‘μ§μ ν¬νΌ λ©μλλ‘ μΆμΆνλ κ²μ κ³ λ €νμΈμ.
handleOrderCreatedμ lines 25-32μhandleOrderPaidμ lines 47-54μμ λμΌν λ§€ν λ‘μ§μ΄ λ°λ³΅λ©λλ€. μ μ§λ³΄μμ± ν₯μμ μν΄ κ³΅ν΅ ν¬νΌ λ©μλλ‘ μΆμΆν μ μμ΅λλ€.π μ€λ³΅ μ κ±°λ₯Ό μν ν¬νΌ λ©μλ μ μ
+ private List<KafkaEvent.OrderEvent.OrderItemInfo> mapToKafkaOrderItemInfos( + List<OrderEvent.OrderItemInfo> orderItems) { + return orderItems.stream() + .map(item -> new KafkaEvent.OrderEvent.OrderItemInfo( + item.productId(), + item.productName(), + item.price(), + item.quantity() + )) + .toList(); + } + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) public void handleOrderCreated(OrderEvent.OrderCreated event) { - List<KafkaEvent.OrderEvent.OrderItemInfo> orderItemInfos = event.orderItems().stream() - .map(item -> new KafkaEvent.OrderEvent.OrderItemInfo( - item.productId(), - item.productName(), - item.price(), - item.quantity() - )) - .toList(); + List<KafkaEvent.OrderEvent.OrderItemInfo> orderItemInfos = + mapToKafkaOrderItemInfos(event.orderItems()); KafkaEvent.OrderEvent.OrderCreated kafkaEvent = KafkaEvent.OrderEvent.OrderCreated.from( event.orderKey(), event.userId(), event.orderId(), event.originalTotalPrice(), event.discountPrice(), orderItemInfos ); outboxService.saveOutbox("order-created-events", event.orderKey(), kafkaEvent); } @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) public void handleOrderPaid(OrderEvent.OrderPaid event) { - List<KafkaEvent.OrderEvent.OrderItemInfo> orderItemInfos = event.orderItems().stream() - .map(item -> new KafkaEvent.OrderEvent.OrderItemInfo( - item.productId(), - item.productName(), - item.price(), - item.quantity() - )) - .toList(); + List<KafkaEvent.OrderEvent.OrderItemInfo> orderItemInfos = + mapToKafkaOrderItemInfos(event.orderItems()); KafkaEvent.OrderEvent.OrderPaid kafkaEvent = KafkaEvent.OrderEvent.OrderPaid.from( event.orderKey(), event.userId(), event.orderId(), event.totalPrice(), orderItemInfos ); outboxService.saveOutbox("order-paid-events", event.orderKey(), kafkaEvent); }Also applies to: 39-40
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (2)
62-87: μμ ν΅ν© λ‘μ§ νμΈ μλ£, 리ν©ν λ§ μ μ
RankingCacheServiceλ₯Ό ν΅ν μμ μ‘°ν λ°ProductInfoλ³΄κ° λ‘μ§μ΄ μ¬λ°λ₯΄κ² ꡬνλμμ΅λλ€. Null μ²λ¦¬λ μ μ ν©λλ€.νμ¬ κ΅¬νμμλ μμκ° μμ λ
ProductInfoμ λͺ¨λ νλλ₯Ό μλμΌλ‘ 볡μ¬νμ¬ μ μΈμ€ν΄μ€λ₯Ό μμ±νκ³ μμ΅λλ€. κ°λ μ±κ³Ό μ μ§λ³΄μμ± ν₯μμ μν΄ λ€μ 리ν©ν λ§μ κ³ λ €ν΄λ³΄μΈμ:π μ μνλ 리ν©ν λ§
μ΅μ 1: ProductInfoμ ν©ν 리 λ©μλ μΆκ°
ProductInfoν΄λμ€μ λ€μ λ©μλλ₯Ό μΆκ°:public ProductInfo withRank(Long rank) { return new ProductInfo( this.id, this.name, this.brandId, this.brandName, this.price, this.likeCount, this.stock, this.createdAt, rank ); }κ·Έλ¬λ©΄ ProductFacadeμμ:
- if (rank != null) { - return new ProductInfo( - productInfo.id(), - productInfo.name(), - productInfo.brandId(), - productInfo.brandName(), - productInfo.price(), - productInfo.likeCount(), - productInfo.stock(), - productInfo.createdAt(), - rank - ); - } - - return productInfo; + return rank != null ? productInfo.withRank(rank) : productInfo;μ΅μ 2 (λ κ°λ¨): μΌν μ°μ°μλ‘ κ°μν
Long rank = rankingCacheService.getProductRank(LocalDate.now(), productId); - - if (rank != null) { - return new ProductInfo( - productInfo.id(), - productInfo.name(), - productInfo.brandId(), - productInfo.brandName(), - productInfo.price(), - productInfo.likeCount(), - productInfo.stock(), - productInfo.createdAt(), - rank - ); - } - - return productInfo; + return rank != null + ? new ProductInfo(productInfo.id(), productInfo.name(), productInfo.brandId(), + productInfo.brandName(), productInfo.price(), productInfo.likeCount(), + productInfo.stock(), productInfo.createdAt(), rank) + : productInfo;
70-70: LocalDate.now() μ¬μ© νμΈμΌμΌ μμ μ‘°νλ₯Ό μν΄
LocalDate.now()λ₯Ό μ¬μ©νκ³ μμ΅λλ€. μ΄λ νμ¬ λ μ§ κΈ°μ€ μμλ₯Ό μ‘°ννλ μλμ λΆν©ν©λλ€. λ€λ§, ν μ€νΈ κ°λ₯μ±κ³Ό μκ°λ κ΄λ ¨ μ΄μλ₯Ό κ³ λ €νλ€λ©΄Clockμ£Όμ μ ν΅ν΄ λ μ§λ₯Ό μ μ΄ν μ μλλ‘ νλ κ²λ κ²ν ν΄λ³Ό μ μμ΅λλ€.π μ νμ κ°μ μ¬ν
ν μ€νΈ μ©μ΄μ±μ μν΄ Clockμ μ£Όμ λ°λ ν¨ν΄:
@RequiredArgsConstructor @Component public class ProductFacade { private final ProductService productService; private final BrandService brandService; private final ProductCacheService productCacheService; private final RankingCacheService rankingCacheService; + private final Clock clock; public ProductInfo getProduct(Long productId) { ProductInfo productInfo = productCacheService.getProduct(productId, () -> { Product product = productService.findProductById(productId) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "μνμ μ°Ύμ μ μμ΅λλ€.")); String brandName = brandService.findBrandNameById(product.getBrandId()); return ProductInfo.from(product, brandName); }); - Long rank = rankingCacheService.getProductRank(LocalDate.now(), productId); + Long rank = rankingCacheService.getProductRank(LocalDate.now(clock), productId); // ... } }Config ν΄λμ€μμ Bean λ±λ‘:
@Bean public Clock clock() { return Clock.systemDefaultZone(); }μ΄λ κ² νλ©΄ ν μ€νΈμμ κ³ μ λ μκ°μ μ£Όμ ν μ μμ΅λλ€.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
12-22: μ μ¬μ NullPointerException μν
item.rank().intValue()νΈμΆ μrank()κ° nullμ΄λ©΄ NPEκ° λ°μν©λλ€. λνΉ κ²°κ³Όμμ rankκ° νμ μ‘΄μ¬ν΄μΌ νμ§λ§, λ°©μ΄μ μ½λ©μ μν΄ null 체ν¬λ₯Ό κ³ λ €ν΄λ³΄μΈμ.π μ μλ μμ
.map(item -> new DailyRankingItem( item.productId(), item.productName(), item.brandName(), item.price(), item.likeCount(), - item.rank().intValue() + item.rank() != null ? item.rank().intValue() : null ))λλ
DailyRankingItemμrankνμ μLongμΌλ‘ ν΅μΌνλ κ²λ κ³ λ €ν΄λ³΄μΈμ.apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
30-43: λ§€ νΈμΆλ§λ€ TTL κ°±μ μ΅μ ν κ³ λ €
incrementScoreκ° νΈμΆλ λλ§λ€expire()λ₯Ό νΈμΆνμ¬ TTLμ κ°±μ ν©λλ€. κ³ λΉλ νΈμΆ νκ²½μμλ λΆνμν Redis λͺ λ Ήμ΄ λ μ μμ΅λλ€.π μ΅μ ν μ μ (μ νμ )
// μ΅μ 1: Lua μ€ν¬λ¦½νΈλ‘ ZINCRBYμ EXPIREλ₯Ό μμμ μΌλ‘ μ€ν // μ΅μ 2: TTLμ΄ μ§§μμ‘μ λλ§ κ°±μ (getExpire μ²΄ν¬ ν κ°±μ ) // νμ¬ κ΅¬νλ κΈ°λ₯μ μΌλ‘λ λ¬Έμ μμΌλ©°, μ΅μ νλ λΆν ν μ€νΈ ν κ²°μ κ°λ₯apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
41-43:Collectors.toMapμμ μ€λ³΅ ν€ λ°μ μ μμΈκ° λ°μν μ μμ΅λλ€.
ProductService.findProductsByIdsκ° μ€λ³΅ μνμ λ°ννλ κ²½μ°λ λλ¬Όμ§λ§, λ°©μ΄μ μΌλ‘ merge functionμ μΆκ°νλ κ²μ΄ μμ ν©λλ€.π μ μλ μμ
List<Product> products = productService.findProductsByIds(productIds); Map<Long, Product> productMap = products.stream() - .collect(Collectors.toMap(Product::getId, product -> product)); + .collect(Collectors.toMap(Product::getId, product -> product, (p1, p2) -> p1));apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingServiceIntegrationTest.java (1)
152-217: LGTM! Carry-over ν μ€νΈκ° μ ꡬνλμμ΅λλ€.μ λ μ μμ κ°μ€μΉ μ μ©κ³Ό μλ³Έ λ°μ΄ν° 보쑴μ μ¬λ°λ₯΄κ² κ²μ¦ν©λλ€.
μ νμ κ°μ μ μ: μ€λ ν€μ μ΄λ―Έ λ°μ΄ν°κ° μλ μνμμ carry-overκ° λ°μν λ μ μκ° μ¬λ°λ₯΄κ² ν©μ°λλμ§ κ²μ¦νλ ν μ€νΈ μΌμ΄μ€λ₯Ό μΆκ°νλ©΄ λ μλ²½ν 컀λ²λ¦¬μ§λ₯Ό μ 곡ν μ μμ΅λλ€.
π Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (33)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderEvent.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingCommand.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.javaapps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingItem.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/cache/RankingCacheService.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.javaapps/commerce-api/src/main/java/com/loopers/interfaces/listener/KafkaOutboxEventListener.javaapps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.javaapps/commerce-api/src/test/java/com/loopers/application/ranking/RankingFacadeIntegrationTest.javaapps/commerce-api/src/test/java/com/loopers/domain/ranking/RankingServiceIntegrationTest.javaapps/commerce-api/src/test/java/com/loopers/infrastructure/cache/ProductCacheServiceIntegrationTest.javaapps/commerce-api/src/test/java/com/loopers/infrastructure/cache/RankingCacheServiceIntegrationTest.javaapps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingV1ApiE2ETest.javaapps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingService.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingWeight.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/MetricsConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/scheduler/RankingScoreCarryOverScheduler.javaapps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingServiceIntegrationTest.javamodules/kafka/src/main/java/com/loopers/application/kafka/KafkaEvent.java
π§° Additional context used
π§ Learnings (7)
π Common learnings
Learnt from: junoade
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 0
File: :0-0
Timestamp: 2025-12-02T08:12:06.383Z
Learning: ProductQueryServiceμμ μν λͺ©λ‘ μ‘°ν μ Redis μΊμλ₯Ό μ μ©νμΌλ©°, μΊμ ν€λ brandId, sortType, pageNumber, pageSizeμ μ‘°ν©μΌλ‘ ꡬμ±λκ³ TTLμ 5λΆμΌλ‘ μ€μ λμ΄ μλ€.
π Learning: 2025-12-02T08:12:06.383Z
Learnt from: junoade
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 0
File: :0-0
Timestamp: 2025-12-02T08:12:06.383Z
Learning: ProductQueryServiceμμ μν λͺ©λ‘ μ‘°ν μ Redis μΊμλ₯Ό μ μ©νμΌλ©°, μΊμ ν€λ brandId, sortType, pageNumber, pageSizeμ μ‘°ν©μΌλ‘ ꡬμ±λκ³ TTLμ 5λΆμΌλ‘ μ€μ λμ΄ μλ€.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/RankingCacheService.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
π Learning: 2025-12-19T20:59:57.713Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: docs/week8/round8-detailed-design.md:151-178
Timestamp: 2025-12-19T20:59:57.713Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository's Kafka event pipeline, only 5 domain events are intentionally published to Kafka via CloudEventEnvelopeFactory: OrderPaidEventV1, LikeCreatedEventV1, LikeCanceledEventV1, ProductViewedEventV1, and StockDepletedEventV1. Other domain events (OrderCreatedEventV1, OrderCanceledEventV1, PaymentCreatedEventV1, PaymentPaidEventV1, PaymentFailedEventV1) are internal-only and intentionally not mapped in resolveMetadata(), which correctly returns null for them to exclude them from Outbox publication.
Applied to files:
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/MetricsConsumer.javaapps/commerce-api/src/main/java/com/loopers/interfaces/listener/KafkaOutboxEventListener.javamodules/kafka/src/main/java/com/loopers/application/kafka/KafkaEvent.java
π Learning: 2025-12-19T21:30:16.024Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/outbox/OutboxEventListener.kt:0-0
Timestamp: 2025-12-19T21:30:16.024Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template Kafka event pipeline, Like events (LikeCreatedEventV1, LikeCanceledEventV1) intentionally use aggregateType="Like" with aggregateId=productId. The aggregateId serves as a partitioning/grouping key (not a unique Like entity identifier), ensuring all like events for the same product go to the same partition for ordering guarantees and aligning with ProductStatisticService's product-based aggregation logic. Using individual like_id would scatter events across partitions and break the statistics aggregation pattern.
Applied to files:
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/MetricsConsumer.java
π Learning: 2025-12-18T13:24:51.650Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:51.650Z
Learning: Adopt centralized error handling for Kafka consumers by using the EventInboxAspect to intercept methods annotated with InboxEvent. Ensure that service methods annotated with InboxEvent save failed EventInbox entries and log errors, avoiding duplicating error handling logic in individual consumers. Apply this pattern broadly to similar consumer/service layers within the commerce-streamer module.
Applied to files:
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/MetricsConsumer.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/interfaces/listener/KafkaOutboxEventListener.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-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
𧬠Code graph analysis (5)
apps/commerce-api/src/main/java/com/loopers/interfaces/listener/KafkaOutboxEventListener.java (1)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderEvent.java (1)
OrderEvent(6-87)
apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/MetricsConsumer.java (1)
Slf4j(20-214)
apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1)
apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java (1)
ConfigurationPropertiesScan(12-28)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingItem.java (1)
k6_product_list.js (1)
productId(356-356)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
RequiredArgsConstructor(17-76)apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
RequiredArgsConstructor(15-41)
π Additional comments (33)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderEvent.java (1)
26-27: LGTM!
OrderCreatedμ΄λ²€νΈμ μ£Όλ¬Έ νλͺ© μ λ³΄κ° μ¬λ°λ₯΄κ² μΆκ°λμμ΅λλ€. λ§€ν λ‘μ§μ΄ μ ννλ©° κΈ°μ‘΄OrderPaidν¨ν΄κ³Ό μΌκ΄μ±μ μ μ§ν©λλ€.Also applies to: 30-37, 45-46
modules/kafka/src/main/java/com/loopers/application/kafka/KafkaEvent.java (1)
32-32: LGTM!Kafka μ΄λ²€νΈ
OrderCreatedμ μ£Όλ¬Έ νλͺ© μ λ³΄κ° μ¬λ°λ₯΄κ² μΆκ°λμμ΅λλ€. ν©ν 리 λ©μλ μκ·Έλμ²μ μμ±μ νΈμΆμ΄ μ ννκ² μ λ°μ΄νΈλμμ΅λλ€.Also applies to: 35-35, 43-43
apps/commerce-api/src/test/java/com/loopers/infrastructure/cache/ProductCacheServiceIntegrationTest.java (1)
328-342: ν μ€νΈ ν¬νΌ λ©μλ μ λ°μ΄νΈ νμΈ μλ£
createProductInfoν¬νΌ λ©μλκ° μλ‘μ΄rankνλλ₯Ό ν¬ν¨νλλ‘ μ¬λ°λ₯΄κ² μ λ°μ΄νΈλμμ΅λλ€. μΊμ ν μ€νΈμμλ μμκ° μ€μνμ§ μμΌλ―λ‘nullμ μ λ¬νλ κ²μ΄ μ μ ν©λλ€.apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
43-65: μν μμΈ μλ΅μ μμ νλ μΆκ° νμΈ μλ£
ProductResponseμrankνλκ° μ¬λ°λ₯΄κ² μΆκ°λμμΌλ©°,info.rank()λ₯Ό ν΅ν΄ μ μ ν λ§€νλκ³ μμ΅λλ€.ProductItem(λͺ©λ‘ μ‘°νμ©)μλ μμκ° ν¬ν¨λμ§ μλ κ²μ΄ μλλ μ€κ³λ‘ 보μ΄λ©°, μμΈ μ‘°νμμλ§ μμλ₯Ό μ 곡νλ κ²μ ν©λ¦¬μ μΈ μ νμ λλ€.apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingItem.java (1)
1-8: RankingItem λ μ½λ μ€κ³ νμΈ μλ£Redis ZSET λ°μ΄ν°λ₯Ό μ λ¬νκΈ° μν κ°λ¨ν λ°μ΄ν° μΊλ¦¬μ΄λ‘μ μ μ νκ² μ€κ³λμμ΅λλ€.
Doubleνμ μscoreνλλ Redis ZSETμ score κ°μ νννκΈ°μ μ ν©ν©λλ€.apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1)
7-13: μ€μΌμ€λ§ κΈ°λ₯ νμ±ν νμΈ μλ£
@EnableSchedulingμ΄λ Έν μ΄μ μ΄ μΆκ°λμ΄RankingScoreCarryOverSchedulerμ κ°μ μ€μΌμ€ μμ μ΄ μ€νλ μ μμ΅λλ€.CommerceApiApplicationκ³Ό μΌκ΄λκ² κ΅¬μ±λμμ΅λλ€.apps/commerce-api/src/main/java/com/loopers/domain/ranking/Ranking.java (1)
1-8: Ranking λλ©μΈ λ μ½λ μ€κ³ νμΈ μλ£
RankingItemκ³Ό λͺ νν ꡬλΆλλ λλ©μΈ λ μ½λλ‘μ μ μ νκ² μ€κ³λμμ΅λλ€.RankingItem(μΊμ κ³μΈ΅)μμRanking(λλ©μΈ κ³μΈ΅)μΌλ‘μ λ³ν μ μμ κ³μ°μ΄ μΆκ°λλ κ΅¬μ‘°κ° κΉλν©λλ€.apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java (1)
18-18:findProductsByIdsλ©μλλ null λ° λΉ λ¦¬μ€νΈ μ²λ¦¬κ° μ μ νκ² κ΅¬νλμμ΅λλ€.ꡬν체μμ
if (productIds == null || productIds.isEmpty())λ‘ μ λ ₯κ°μ κ²μ¦ν ν λΉ λ¦¬μ€νΈλ₯Ό λ°ννκ³ μμ΄ μ£μ§ μΌμ΄μ€λ₯Ό μμ νκ² μ²λ¦¬νκ³ μμ΅λλ€.apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (1)
56-59: λ°°μΉ μ‘°ν λ©μλ ꡬν νμΈ μλ£
@Transactional(readOnly = true)λ₯Ό μ¬μ©ν μ½κΈ° μ μ© λ©μλλ‘ μ μ νκ² κ΅¬νλμμ΅λλ€. 리ν¬μ§ν 리μProductRepositoryImpl.findProductsByIds()μμ nullκ³Ό λΉ λ¦¬μ€νΈλ₯Ό μμ νκ² μ²λ¦¬νκ³ μμΌλ―λ‘ μ£μ§ μΌμ΄μ€ μ²λ¦¬λ λ¬Έμ μμ΅λλ€.apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java (1)
28-29: LGTM!λ°°μΉ μ‘°νλ₯Ό μν νμ€μ μΈ IN μ ꡬνμ΄λ©°, μννΈ μμ λ λ μ½λλ₯Ό μ¬λ°λ₯΄κ² νν°λ§ν©λλ€.
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java (1)
42-48: LGTM!null/empty 체ν¬λ₯Ό ν΅ν λ°©μ΄μ νλ‘κ·Έλλ°μΌλ‘ λΆνμν DB 쿼리λ₯Ό λ°©μ§νκ³ NPEλ₯Ό μλ°©ν©λλ€.
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
28-34: LGTM!μ λ ₯ κ°μ΄ μ ν¨νλ€λ μ μ νμ μμ κ³μ° λ‘μ§(
start + 1 + i)μ μ¬λ°λ₯΄λ©°, 1-based μμλ₯Ό μ ννκ² μμ±ν©λλ€.apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java (1)
21-35: LGTM!JPAλ₯Ό μν no-arg μμ±μμ Builder ν¨ν΄ ꡬνμ΄ μ μ ν©λλ€.
createBrandν©ν 리 λ©μλλ λΉλ ν¨ν΄μ νΈμμ± λνΌλ‘ ν μ€νΈ μ½λ μμ± μ μ μ©ν©λλ€.apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingV1ApiE2ETest.java (1)
73-212: LGTM!ν¬κ΄μ μΈ E2E ν μ€νΈ 컀λ²λ¦¬μ§λ₯Ό μ 곡ν©λλ€:
- λνΉ λ°μ΄ν° μ‘΄μ¬ μ μ‘°ν
- λ°μ΄ν° μμ λ λΉ λ¦¬μ€νΈ λ°ν
- νμ΄μ§λ€μ΄μ λμ κ²μ¦
λͺ νν arrange-act-assert ꡬ쑰μ μ μ ν μ΄μ€μ μΌλ‘ μ£Όμ μλ리μ€λ₯Ό μ κ²μ¦νκ³ μμ΅λλ€.
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java (1)
5-18: LGTM!λνΉ μ 보λ₯Ό μ λ¬νκΈ° μν κΉλν λ μ½λ ꡬ쑰μ λλ€. μ€μ²© λ μ½λλ₯Ό μ¬μ©ν μΊ‘μνκ° μ μ νλ©°, λͺ¨λ νλμ νμ μ΄ μ ν©ν©λλ€.
apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingWeight.java (1)
1-19: LGTM!κ°μ€μΉ μμκ° λͺ ννκ² μ μλμ΄ μκ³ , μ£Όλ¬Έ μμ±(0.7) > μ’μμ(0.2) > μ‘°ν(0.1) μμΌλ‘ μ μ ν κ°μ€μΉ λ°°λΆμ λλ€.
@Getterνμ©λ μ μ ν©λλ€.apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/MetricsConsumer.java (3)
47-48: μ’μμ μ·¨μ μ λνΉ μ μ μ²λ¦¬ νμΈ νμ
handleProductLikedEventsμμλrankingService.incrementScoreλ₯Ό νΈμΆνμ§λ§,handleProductUnlikedEvents(λΌμΈ 66-87)μμλ λνΉ μ μλ₯Ό μ°¨κ°νμ§ μμ΅λλ€. μ΄κ²μ΄ μλλ μ€κ³μΈμ§ νμΈμ΄ νμν©λλ€.λ§μ½ λνΉμ΄ μμ κΈμ μ νΈλ§ λ°μνλ μ€κ³λΌλ©΄ νμ¬ κ΅¬νμ΄ λ§μ§λ§, μ’μμ μ·¨μκ° λνΉμ λ°μλμ΄μΌ νλ€λ©΄
decrementScoreλ©μλ μΆκ°κ° νμν©λλ€.
106-107: LGTM!μ‘°ν μ΄λ²€νΈμ λν λνΉ μ μ μ¦κ° λ‘μ§μ΄ κΈ°μ‘΄ ν¨ν΄μ λ°λ₯΄λ©° μ¬λ°λ₯΄κ² ν΅ν©λμμ΅λλ€.
136-146: LGTM!μ£Όλ¬Έ μμ± μ΄λ²€νΈμμ κ° μ£Όλ¬Έ νλͺ©λ³λ‘ λνΉ μ μλ₯Ό μ¦κ°μν€λ λ‘μ§μ΄ μ¬λ°λ₯΄κ² ꡬνλμμ΅λλ€.
ORDER_CREATEDκ°μ€μΉ(0.7)κ° κ°μ₯ λμ ꡬ맀 μ νμ΄ λνΉμ κ°μ₯ ν° μν₯μ λ―ΈμΉλλ‘ μ€κ³λμ΄ μμ΅λλ€.apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java (2)
56-57: LGTM!
RankingCacheServiceμ λν@MockitoSpyBeanμΆκ°κ° μ μ ν©λλ€. κΈ°μ‘΄ ν μ€νΈ ν¨ν΄κ³Ό μΌκ΄μ± μκ² κ΅¬νλμμ΅λλ€.
289-354: LGTM!λνΉμ΄ μλ κ²½μ°(
rank= null)μ λνΉμ΄ μλ κ²½μ°(rank= 5L) λ κ°μ§ μλ리μ€μ λν ν μ€νΈκ° μ ꡬνλμ΄ μμ΅λλ€.RankingCacheServiceμνΈμμ© κ²μ¦λ ν¬ν¨λμ΄ μμ΄ ν μ€νΈ 컀λ²λ¦¬μ§κ° μΆ©λΆν©λλ€.apps/commerce-api/src/test/java/com/loopers/application/ranking/RankingFacadeIntegrationTest.java (1)
1-213: LGTM!λνΉ νμ¬λμ λν ν΅ν© ν μ€νΈκ° ν¬κ΄μ μΌλ‘ ꡬνλμ΄ μμ΅λλ€:
- μ μ μ‘°ν μλ리μ€
- DBμ μλ μν μ μΈ λ‘μ§
- λΉ κ²°κ³Ό μ²λ¦¬
- νμ΄μ§λ€μ΄μ λμ
Redisμ DB μ 리 λ‘μ§λ
@AfterEachμ μ μ ν ν¬ν¨λμ΄ μμ΅λλ€.apps/commerce-api/src/test/java/com/loopers/domain/ranking/RankingServiceIntegrationTest.java (1)
1-158: LGTM!
RankingService.getRankingλ©μλμ λν ν΅ν© ν μ€νΈκ° μ ꡬνλμ΄ μμ΅λλ€:
- λ°μ΄ν° μ‘΄μ¬ μ μ μ μ‘°ν
- νμ΄μ§λ€μ΄μ λμ (3νμ΄μ§ ν μ€νΈ)
- λΉ λ°μ΄ν° μ²λ¦¬
- μμκ° 1λΆν° μμνλμ§ κ²μ¦
@BeforeEachμ@AfterEachμμ Redisλ₯Ό μ 리νμ¬ ν μ€νΈ κ²©λ¦¬κ° λ³΄μ₯λ©λλ€.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (1)
16-23: μ΄ λΆλΆμ μ€μ λ‘λ λ¬Έμ κ° μμ΅λλ€.RankingV1Controller.javaμμ ꡬν μ@DateTimeFormat(pattern = "yyyyMMdd")μ΄λ Έν μ΄μ μ μ¬μ©νκ³ μμΌλ―λ‘, API λͺ μΈμ μ€λͺ "yyyyMMdd νμ"κ³Ό μ€μ λμμ΄ μΌμΉν©λλ€.apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
45-58: carryOverScore λμ μμ νμΈ λ° ν μ€νΈ 컀λ²λ¦¬μ§ κ°μ μ μμ€μΌμ€λ¬κ° λ§€μΌ 23:50 PMμ μ€νλλ―λ‘ targetDate(λ΄μΌ)λ μμ§ νλ£¨κ° μμλμ§ μμ μνμ λλ€. λ°λΌμ μ€μ μ΄μ νκ²½μμλ toKeyμ κΈ°μ‘΄ λ°μ΄ν°κ° μμ κ°λ₯μ±μ΄ λμ΅λλ€.
λ€λ§, νμ¬ ν μ€νΈ μΌμ΄μ€λ toKeyκ° λΉμ΄μλ κ²½μ°λ§ κ²μ¦νκ³ μμ΅λλ€. νΉμ μ€λ₯ 볡ꡬλ μλ μ€ν λ±μΌλ‘ toKeyμ μ΄λ―Έ λμ λ μ μκ° μλ μν©μμ
unionAndStore(..., Aggregate.SUM, ...)μ΄ μ λλ‘ λ³ν©νλμ§ νμΈνλ ν μ€νΈ μΌμ΄μ€ μΆκ°λ₯Ό κΆμ₯ν©λλ€.// toKeyμ κΈ°μ‘΄ λ°μ΄ν°κ° μλ κ²½μ° λ³ν© λμ κ²μ¦ zSetOps.add(todayKey, "productId1", 50.0); // κΈ°μ‘΄ λΉμΌ μ μ rankingService.carryOverScore(yesterday, today); // κ²μ¦: κΈ°μ‘΄ μ μμ carry-over μ μκ° SUMμΌλ‘ ν©μ³μ§λκ°?apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
22-39: LGTM! 컨νΈλ‘€λ¬ ꡬνμ΄ μ μ ν©λλ€.νμ΄μ§ κΈ°λ³Έκ°κ³Ό λ μ§ ν¬λ§·μ΄ μ ꡬμ±λμ΄ μμ΅λλ€.
RankingV1ApiSpecμΈν°νμ΄μ€λ₯Ό ꡬννμ¬ API λ¬Έμνμ κ³μ½μ λΆλ¦¬ν μ λ μ’μ΅λλ€.apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java (1)
14-42: LGTM!rankνλ μΆκ°κ° κΉλνκ² κ΅¬νλμμ΅λλ€.κΈ°μ‘΄ 2κ° νλΌλ―Έν° ν©ν 리 λ©μλλ₯Ό μ μ§νλ©΄μ μλ‘μ΄ 3κ° νλΌλ―Έν° λ©μλλ₯Ό μΆκ°νμ¬ νμ νΈνμ±μ 보μ₯ν©λλ€.
apps/commerce-api/src/test/java/com/loopers/infrastructure/cache/RankingCacheServiceIntegrationTest.java (2)
43-114: LGTM! ν μ€νΈ 컀λ²λ¦¬μ§κ° μ μ ν©λλ€.λνΉ λ²μ μ‘°ν, νμ΄μ§, λΉ λ°μ΄ν° μΌμ΄μ€λ₯Ό ν¬κ΄μ μΌλ‘ ν μ€νΈν©λλ€.
RedisCleanUpμ νμ©ν teardownλ μ μ ν©λλ€.
117-160: LGTM! μν λνΉ μ‘°ν ν μ€νΈκ° μ μ ν©λλ€.1-based λνΉ λ³νκ³Ό μ‘΄μ¬νμ§ μλ μνμ λν null λ°νμ μ¬λ°λ₯΄κ² κ²μ¦ν©λλ€.
apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/RankingCacheService.java (2)
29-60: LGTM! Redis ZSET μ‘°ν λ‘μ§μ΄ μ μ νκ² κ΅¬νλμμ΅λλ€.
NumberFormatExceptionμ²λ¦¬μ null 체ν¬κ° λ°©μ΄μ μΌλ‘ μ ꡬνλμ΄ μμ΅λλ€.
62-75: LGTM! 1-based λνΉ λ³νμ΄ μ¬λ°λ₯΄κ² ꡬνλμμ΅λλ€.Redisμ 0-based
reverseRankλ₯Ό 1-based λνΉμΌλ‘ λ³ννλ λ‘μ§μ΄ λͺ νν©λλ€.apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingServiceIntegrationTest.java (1)
41-150: LGTM! μ μ μ¦κ° ν μ€νΈκ° ν¬κ΄μ μ λλ€.λ€μν μ΄λ²€νΈ νμ (LIKE, VIEW, ORDER_CREATED)μ λν μ μ μ¦κ°, λμ , λ μ§λ³ ν€ κ²©λ¦¬λ₯Ό μ κ²μ¦ν©λλ€. TTL κ²μ¦(2μΌ)λ μ μ ν©λλ€.
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
61-69: NPE μ°λ €λ κ·Όκ±°κ° μμ΅λλ€.Product μν°ν°μ
priceμlikeCountνλλ λ€μκ³Ό κ°μ΄ λλ©μΈ λΆλ³ 쑰건μΌλ‘ 보νΈλ©λλ€:
@AttributeOverrideμnullable = falseμ μ½- μμ±μμ λͺ μμ κ²μ¦ (validatePrice, validateLikeCount)
- λ°μ΄ν°λ² μ΄μ€ μ€ν€λ§μ NOT NULL μ μ½
λ°λΌμ Product μΈμ€ν΄μ€κ° μ‘΄μ¬νλ©΄ λ νλ λͺ¨λ νμ nullμ΄ μλλ©°, μ½λλ μμ ν©λλ€.
Likely an incorrect or invalid review comment.
π Summary
π¬ Review Points
1.
product_metricsμ μ©λνμ¬ κ΅¬νμμλ Kafka μ΄λ²€νΈ(μ’μμ λ±λ‘, μ‘°νμ, μ£Όλ¬Έ μμ±)λ₯Ό μμ νλ©΄
product_metricsν μ΄λΈμλ μ λ°μ΄νΈν©λλ€.λ°μ΄ν° νλ¦:
prodcut_metricsλ νμ¬ λμ λ°μ΄ν°λ§ μ μ₯νλ ꡬ쑰μΈλ°, μΌλ³ λνΉμ μν μΌλ³ λ³νλμ μΆμ νλ €λ©΄product_metricsν μ΄λΈμ μ μ₯ λ μ§ μ»¬λΌμ μΆκ°ν΄μΌ ν κΉμ?2. λμΌ μ μ μ²λ¦¬ λ°©μ
λ λμ€ zsetμμ μ μκ° λμΌνλ©΄ λ©€λ²μ μ¬μ μ μμλ‘ μ λ ¬λλ κ² κ°μ΅λλ€.
μ€λ¬΄μμλ λμΌ μ μμΈ κ²½μ° μ΄λ€ λΉμ¦λμ€ λ‘μ§μΌλ‘ μ²λ¦¬νλ κ²μ΄ μΌλ°μ μΈκ°μ?
3. λΉλκΈ° μ½λ°±μμ νΈλμμ κ΄λ¦¬
μ§λ μ£Ό κ³Όμ 리ν©ν λ§μ νλ©΄μ OutServiceμ μ΄λ²€νΈ λ°ν λ‘μ§μ λΉλκΈ° λ°©μμμ λκΈ° λ°©μμΌλ‘ λ³κ²½νμ΅λλ€.
λ³κ²½ μ :
λ³κ²½ ν:
whenCompleteμ½λ°±μ λΉλκΈ°λ‘ μ€νλλ―λ‘outboxRepository.markAsPublished()μmarkAsFailed()νΈμΆμ΄ νΈλμμ λ°μμ μ€νλ μ μμ΄μ, λκΈ° λ°©μμΌλ‘ λ³κ²½ν΄μ€¬μ΅λλ€.λΉλκΈ° λ°©μμ κ³μ μ¬μ©νλ €λ©΄ νΈλμμ κ΄λ¦¬λ₯Ό ν΄μ€μΌ νλλ°, κ°μ ν΄λμ€ λ΄μμ νΈλμμ κ΄λ¦¬λ₯Ό μν΄ λ³λμ Managerλ₯Ό μ¬μ©νλ λ°©μμ΄ μ€λ¬΄μμ λ§μ΄ μ¬μ©νλ λ°©μμΈκ°μ? μλλ©΄ μ§μνλ λ°©μμΈκ°μ?
β Checklist
π Ranking Consumer
βΎ Ranking API
Summary by CodeRabbit
λ¦΄λ¦¬μ€ λ ΈνΈ
βοΈ Tip: You can customize this high-level summary in your review settings.