-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-9] Product Ranking with Redis #220
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: sylee6529
Are you sure you want to change the base?
Conversation
- RankingConfig: μ΄λ²€νΈλ³ κ°μ€μΉ μ€μ (view: 0.1, like: 0.2, order: 0.6) - TTL μ€μ (2μΌ)
- ProductRankingCache: ZINCRBYλ‘ μ μ μ μ¬
- CacheKeyGenerator: μΌκ° λνΉ ν€ μμ± (ranking:all:v1:{date})
- TTL race condition λ°©μ§ (AtomicReference)
- μ£Όλ¬Έ μ μ log μ κ·ν
- Redis Pipeline λ°°μΉ μ²λ¦¬ μ§μ
- μ‘°ν/μ’μμ/μ£Όλ¬Έ μ΄λ²€νΈ μ λνΉ μ μ μ μ¬ - μ’μμ μ·¨μ μ μ μ κ°μ λ°μ - μ£Όλ¬Έ μ΄λ²€νΈ Pipeline λ°°μΉ μ²λ¦¬
- ProductRankingCache: ZSET μ‘°ν (μμ, μ μ, νμ΄μ§λ€μ΄μ ) - CacheKeyGenerator: dailyRankingKey μΆκ°
- RankingFacade: λνΉ μ‘°ν λΉμ¦λμ€ λ‘μ§ - RankingInfo: λνΉ νμ΄μ§/μμ΄ν μ 보 DTO - μν/λΈλλ μ 보 Aggregation (N+1 λ°©μ§)
- GET /api/v1/rankings?date=&page=&size= - page νλΌλ―Έν° 1-based (API) β 0-based (λ΄λΆ) λ³ν
- ProductDetailInfo: ranking, brandId νλ μΆκ° - ProductReadService: brandId μ€μ - ProductV1Dto: ranking μλ΅ νλ μΆκ°
- brandId μΊμ μ¬μ©μΌλ‘ λΆνμν DB μ‘°ν μ κ±° - μ€μκ° λνΉ μ‘°ν μ°λ
|
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μ΄ PRμ Redis κΈ°λ° μ ν λνΉ μμ€ν μ λμ ν©λλ€. μ ν리μΌμ΄μ μ λ°μ ProductRankingCacheλ₯Ό ν΅ν©νμ¬ μ€μκ° λνΉ λ°μ΄ν°λ₯Ό μ 곡νκ³ , μλ‘μ΄ RankingFacade λ° API μλν¬μΈνΈλ‘ λνΉ μ‘°ν κΈ°λ₯μ λ ΈμΆνλ©°, λ©νΈλ¦ μμ§ μ λνΉ μ μλ₯Ό λμ μΌλ‘ μ λ°μ΄νΈν©λλ€. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant RankingV1Controller
participant RankingFacade
participant ProductRankingCache
participant ProductDB
participant BrandDB
Client->>RankingV1Controller: GET /api/v1/rankings?date=20251223&page=1&size=10
RankingV1Controller->>RankingFacade: getRankings("20251223", 0, 10)
rect rgb(200, 220, 255)
Note over RankingFacade,ProductRankingCache: μΊμλ λνΉ μ‘°ν
RankingFacade->>ProductRankingCache: getTopRankings("20251223", 0, 10)
ProductRankingCache-->>RankingFacade: List<RankingEntry>
end
alt λνΉ λ°μ΄ν° μ‘΄μ¬
rect rgb(220, 255, 220)
Note over RankingFacade,BrandDB: μ ν λ° λΈλλ μ‘°ν
RankingFacade->>ProductDB: findAllById(productIds)
ProductDB-->>RankingFacade: List<Product>
RankingFacade->>BrandDB: findAllById(brandIds)
BrandDB-->>RankingFacade: List<Brand>
end
rect rgb(255, 240, 200)
Note over RankingFacade: μλ΅ κ΅¬μ±
RankingFacade->>RankingFacade: construct RankingPageInfo<br/>with items & metadata
end
else λνΉ λ°μ΄ν° μμ
RankingFacade->>RankingFacade: return empty RankingPageInfo
end
RankingFacade-->>RankingV1Controller: RankingPageInfo
RankingV1Controller->>RankingV1Controller: map to RankingPageResponse
RankingV1Controller-->>Client: ApiResponse.success(RankingPageResponse)
sequenceDiagram
participant Event as Event Handler
participant MetricsAggregationService
participant MetricsService
participant ProductRankingCache
participant Redis
Event->>MetricsAggregationService: onProductLiked(productId)
MetricsAggregationService->>MetricsService: updateLikeCount(...)
MetricsService-->>MetricsAggregationService: complete
rect rgb(200, 220, 255)
Note over MetricsAggregationService,Redis: λνΉ μ μ μ
λ°μ΄νΈ
MetricsAggregationService->>ProductRankingCache: addLikeScore(productId)
ProductRankingCache->>Redis: ZINCRBY ranking:all:v1:{date} 0.2 {productId}
Redis-->>ProductRankingCache: success
end
Event->>MetricsAggregationService: onOrderCompleted(order)
MetricsAggregationService->>MetricsService: updateOrderCount(...)
MetricsService-->>MetricsAggregationService: complete
rect rgb(220, 255, 220)
Note over MetricsAggregationService,Redis: λ°°μΉ μ£Όλ¬Έ μ μ μ
λ°μ΄νΈ
MetricsAggregationService->>MetricsAggregationService: build OrderItemScore list
MetricsAggregationService->>ProductRankingCache: addOrderScoresBatch(items)
ProductRankingCache->>Redis: pipeline ZINCRBY x N
Redis-->>ProductRankingCache: success
end
Estimated code review effortπ― 3 (Moderate) | β±οΈ ~25 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touchesβ Failed checks (1 warning)
β Passed checks (2 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: 3
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/application/metrics/MetricsAggregationService.java (1)
38-62: Redis μμΈ μ²λ¦¬ λ°©μ μ¬κ²ν νμνμ¬ μ½λμμ
productRankingCache.addLikeScore()λ@Transactionalλ²μ λ΄μμ νΈμΆλκ³ μμ΅λλ€. ProductRankingCacheμincrementScoreλ©μλ(lines 147-156)λ Redis μμΈλ₯Ό catchνμ¬ κ²½κ³ λ§ λ‘κΉ νκ³ μ¬λ°μμν€μ§ μμ΅λλ€.μ΄λ λ€μ λ κ°μ§ λ¬Έμ λ₯Ό μΌκΈ°ν©λλ€:
- λ°μ΄ν° λΆμΌμΉ: Redis μ°μ° μ€ν¨ μμλ λ°μ΄ν°λ² μ΄μ€ λ³κ²½μ¬νμ΄ μ»€λ°λμ΄ μΊμμ DB κ° λΆμΌμΉ λ°μ
- μ묡μ μ€ν¨: μμΈκ° μ‘°μ©ν μΌμΌμ Έ λͺ¨λν°λ§ λ° λλ²κΉ μ΄ μ΄λ €μ
μ€κ³ μλλ₯Ό λͺ νν ν ν, νμμ λ€μ μ€ νλλ₯Ό κ³ λ €νμΈμ:
- Redis μ°μ°μ νΈλμμ μΈλΆλ‘ λΆλ¦¬ (μ΄λ²€νΈ λ°ν λλ λ³λ μ€λ λ)
- λͺ μμ μ¬μλ λ‘μ§ μΆκ° λ° μ΅μ’ μ€ν¨ μ κ²½κ³ κ°ν
- Compensating transaction ν¨ν΄ μ μ©
π§Ή Nitpick comments (3)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/cache/CacheKeyGenerator.java (1)
1-19: λͺ¨λ κ° ν€ μμ± μΌκ΄μ±μ΄ μ μ μ§λμμ΅λλ€.commerce-streamerμ commerce-api λͺ¨λμμ λμΌν ν€ νμμ μ¬μ©νμ¬ Redis ν€ λΆμΌμΉ λ¬Έμ λ₯Ό λ°©μ§ν©λλ€. μ£ΌμμΌλ‘ λͺ μμ μΌλ‘ μ€λͺ ν κ²λ μ’μ΅λλ€.
ν₯ν 리ν©ν λ§ μ κ³ λ €μ¬ν: λ λͺ¨λμμ λμΌν
CacheKeyGeneratorμ½λκ° μ€λ³΅λκ³ μμ΅λλ€. κ³΅ν΅ λͺ¨λλ‘ μΆμΆνλ©΄ μ μ§λ³΄μμ±μ΄ ν₯μλ μ μμ΅λλ€. λ€λ§ νμ¬ κ΅¬μ‘°μμλ λͺ¨λ κ° μμ‘΄μ±μ μ€μ΄κΈ° μν΄ μ€λ³΅μ νμ©νλ κ²λ ν©λ¦¬μ μΈ μ νμ λλ€.apps/commerce-streamer/src/main/java/com/loopers/config/RankingConfig.java (1)
14-15: μ€μ κ° κ²μ¦ μΆκ°λ₯Ό κΆμ₯ν©λλ€.
weightμ κ° κ°μ€μΉμttlDaysκ°μ λν κ²μ¦μ΄ μμ΅λλ€. μλͺ»λ μ€μ κ°(μμ, 0 λ±)μ΄ μ£Όμ λ κ²½μ° λ°νμ μ€λ₯λ μμμΉ λͺ»ν λμμ΄ λ°μν μ μμ΅λλ€.π κ²μ¦ λ‘μ§ μΆκ° μμ
+import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Positive; + @Getter @Setter @Configuration @ConfigurationProperties(prefix = "ranking") +@Validated public class RankingConfig { private Weight weight = new Weight(); + @Min(1) private int ttlDays = 2; @Getter @Setter public static class Weight { + @Positive private double view = 0.1; + @Positive private double like = 0.2; + @Positive private double order = 0.6; } }apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/ProductRankingCache.java (1)
105-135:NumberFormatExceptionμ²λ¦¬λ₯Ό νμΈνμΈμ.Line 123μμ
Long.parseLong(tuple.getValue().toString())μ νΈμΆν λ, κ°μ΄ μ«μκ° μλλ©΄NumberFormatExceptionμ΄ λ°μν©λλ€. νμ¬ μΈλΆ catch λΈλ‘μμ μ²λ¦¬λμ§λ§, μΌλΆ νλͺ©λ§ νμ± μ€ν¨νλ κ²½μ° μ 체 κ²°κ³Όκ° λΉ λ¦¬μ€νΈλ‘ λ°νλ©λλ€.λΆλΆ μ€ν¨λ₯Ό νμ©νλ €λ©΄ κ°λ³ νλͺ© νμ±μ try-catchλ‘ κ°μΈλ κ²μ κ³ λ €νμΈμ.
π κ°λ³ νλͺ© μμΈ μ²λ¦¬ μμ
for (ZSetOperations.TypedTuple<Object> tuple : results) { - Long productId = Long.parseLong(tuple.getValue().toString()); - Double score = tuple.getScore(); - entries.add(new RankingEntry(rank++, productId, score)); + try { + Long productId = Long.parseLong(tuple.getValue().toString()); + Double score = tuple.getScore(); + entries.add(new RankingEntry(rank++, productId, score)); + } catch (NumberFormatException e) { + log.warn("[Ranking] Invalid productId format - value: {}", tuple.getValue()); + rank++; // μμλ μ¦κ°μμΌ μΌκ΄μ± μ μ§ + } }
π Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (15)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.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/product/service/ProductReadService.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheKeyGenerator.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/cache/ProductRankingCache.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.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-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.javaapps/commerce-streamer/src/main/java/com/loopers/config/RankingConfig.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/cache/CacheKeyGenerator.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/cache/ProductRankingCache.javaapps/commerce-streamer/src/main/resources/application.yml
π§° Additional context used
π§ Learnings (6)
π 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-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/infrastructure/cache/CacheKeyGenerator.javaapps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/cache/ProductRankingCache.javaapps/commerce-api/src/main/java/com/loopers/domain/product/service/ProductReadService.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/application/metrics/MetricsAggregationService.java
π Learning: 2025-12-19T21:30:16.024Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/outbox/OutboxEventListener.kt:0-0
Timestamp: 2025-12-19T21:30:16.024Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template Kafka event pipeline, Like events (LikeCreatedEventV1, LikeCanceledEventV1) intentionally use aggregateType="Like" with aggregateId=productId. The aggregateId serves as a partitioning/grouping key (not a unique Like entity identifier), ensuring all like events for the same product go to the same partition for ordering guarantees and aligning with ProductStatisticService's product-based aggregation logic. Using individual like_id would scatter events across partitions and break the statistics aggregation pattern.
Applied to files:
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
π 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/ProductRankingCache.javaapps/commerce-api/src/main/java/com/loopers/domain/product/service/ProductReadService.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/cache/ProductRankingCache.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
π Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
𧬠Code graph analysis (4)
apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/ProductRankingCache.java (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheKeyGenerator.java (1)
CacheKeyGenerator(5-59)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/cache/ProductRankingCache.java (1)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/cache/CacheKeyGenerator.java (1)
CacheKeyGenerator(7-19)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java (1)
RankingInfo(7-40)apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
RankingV1Dto(9-57)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java (1)
RankingInfo(7-40)
π Additional comments (11)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java (1)
17-17: μλ‘μ΄ νλ μΆκ°κ° μ μ ν©λλ€.
brandIdμrankingνλκ° λͺ ννκ² μΆκ°λμμΌλ©°, νμ κ³Ό μ£Όμμ΄ μ μ ν©λλ€.rankingμ΄ μμκΆ λ°μΌ λnullμ λ°ννλ μ€κ³λ ν©λ¦¬μ μ λλ€.Also applies to: 24-24, 29-29, 39-40
apps/commerce-streamer/src/main/resources/application.yml (1)
32-37: λνΉ μ€μ μ΄ λͺ ννκ² μΆκ°λμμ΅λλ€.κ°μ€μΉμ TTL μ€μ μ΄ ν©λ¦¬μ μ λλ€. μ£Όλ¬Έ μ΄λ²€νΈμ κ°μ₯ λμ κ°μ€μΉ(0.6)λ₯Ό λΆμ¬ν κ²μ λΉμ¦λμ€ λ‘μ§μ μ μ ν©λλ€.
κ°μ€μΉ ν©κ³κ° 0.9λ‘ μλμ μΌλ‘ 1.0μ΄ μλμ§ νμΈνμΈμ. μΌλ°μ μΌλ‘ μ κ·νλ κ°μ€μΉ(ν©κ³ 1.0)λ₯Ό μ¬μ©νλ κ²μ΄ μ μ ν΄μμ λ λͺ νν μ μμ΅λλ€.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
42-43: DTOμ λνΉ νλκ° μ¬λ°λ₯΄κ² μΆκ°λμμ΅λλ€.
rankingνλκ° λ μ½λ λμ μΆκ°λμ΄ κΈ°μ‘΄ νλ μμλ₯Ό μ μ§νλ©°,from()λ©μλμμ μ¬λ°λ₯΄κ² λ§€νλ©λλ€.Also applies to: 55-56
apps/commerce-api/src/main/java/com/loopers/domain/product/service/ProductReadService.java (1)
54-54: νλ μΆκ°κ° μ μ νλ©° κ΄μ¬μ¬ λΆλ¦¬κ° λͺ νν©λλ€.
brandIdλ₯Ό μν°ν°μμ μΆμΆνκ³ ,rankingμnullλ‘ μ΄κΈ°ννλ κ²μ΄ ν©λ¦¬μ μ λλ€. μ£Όμμμ μ€λͺ ν λλ‘ Facade κ³μΈ΅μμ μ€μκ° λνΉμ μ‘°ννμ¬ κ΅μ²΄νλ μ€κ³κ° λͺ νν©λλ€.Also applies to: 61-61
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (3)
10-10: ProductRankingCache μμ‘΄μ± μΆκ°κ° μ μ ν©λλ€.μλ‘μ΄ λνΉ μΊμλ₯Ό ν΅ν΄ μ€μκ° λνΉ λ°μ΄ν°λ₯Ό μ‘°νν μ μλλ‘ κΉλνκ² ν΅ν©λμμ΅λλ€.
Also applies to: 31-31
124-130: μ΄λ²€νΈ λ°ν μ΅μ νκ° ν¨κ³Όμ μ λλ€.μΊμλ
brandIdλ₯Ό μ¬μ©νμ¬ μ΄λ²€νΈλ₯Ό λ°νν¨μΌλ‘μ¨ μΆκ° DB μ‘°νλ₯Ό λ°©μ§ν κ²μ΄ μ’μ κ°μ μ λλ€.
102-121: μ ν μμΈ μ 보 μ‘°ν νλ¦μ΄ μ κ°μ λμμ΅λλ€.μΊμλ μ 보λ₯Ό μ¬μ¬μ©νκ³ λμ λ°μ΄ν°(
isLikedByMember,ranking)λ§ μ€μκ°μΌλ‘ μ‘°ννλ μ€κ³κ° ν¨μ¨μ μ λλ€.brandIdλ₯Ό μΊμμμ κ°μ Έμ λΆνμν DB μ‘°νλ₯Ό μ κ±°ν κ²λ μ’μ μ΅μ νμ λλ€.ProductRankingCache.getRank()λ μμκΆ λ°μ μνμ λν΄ nullμ μ¬λ°λ₯΄κ² λ°ννλ―λ‘, ProductDetailInfoμ ranking νλκ° nullμΌ λμ ν΄λΌμ΄μΈνΈ μ²λ¦¬λ§ νμΈνλ©΄ λ©λλ€.apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsAggregationService.java (1)
98-139: λ°°μΉ μ²λ¦¬ λ‘μ§μ΄ μ ꡬνλμμ΅λλ€.μ£Όλ¬Έ μμ΄ν μ λνΉ μ μλ₯Ό λ°°μΉλ‘ μ²λ¦¬νμ¬ Redis λ€νΈμν¬ μ€λ²ν€λλ₯Ό μ€μΈ κ²μ μ’μ μ κ·Όμ λλ€.
addOrderScoresBatchμ fallback λ‘μ§λ κ²¬κ³ ν©λλ€.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
56-91: N+1 λ¬Έμ λ°©μ§κ° μ ꡬνλμμ΅λλ€.μνκ³Ό λΈλλ μ 보λ₯Ό μΌκ΄ μ‘°ννμ¬ N+1 λ¬Έμ λ₯Ό ν¨κ³Όμ μΌλ‘ λ°©μ§νμΌλ©°, λλ½λ λ°μ΄ν°μ λν null μ²λ¦¬λ μ μ ν©λλ€. νΉν λΈλλκ° μμ λ "Unknown" ν΄λ°±μ μ 곡νλ κ²μ μ¬μ©μ κ²½ν μΈ‘λ©΄μμ μ’μ μ νμ λλ€.
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/cache/ProductRankingCache.java (2)
86-95: λ‘κ·Έ μ κ·νλ₯Ό ν΅ν κ°κ²© νΈν₯ λ°©μ§κ° μ ꡬνλμμ΅λλ€.
Math.log1p(orderAmount)λ₯Ό μ¬μ©νμ¬ κ³ κ° μνμ κ³Όλν μ μ νΈμ€μ λ°©μ§ν κ²μ νλ₯ν μ€κ³μ λλ€. λ‘κ·Έ μ€μΌμΌ λ³νμΌλ‘ 곡μ ν λνΉ λ°μμ΄ κ°λ₯ν©λλ€.
103-137: Pipeline λ°°μΉ μ²λ¦¬μ fallback μ λ΅μ΄ κ²¬κ³ ν©λλ€.Redis Pipelineμ μ¬μ©ν λ°°μΉ μ²λ¦¬λ‘ λ€νΈμν¬ μ€λ²ν€λλ₯Ό μ€μμΌλ©°, μ€ν¨ μ κ°λ³ μ²λ¦¬λ‘ ν΄λ°±νλ μ λ΅μ΄ νλ₯ν©λλ€. μ΄λ μ λ’°μ±κ³Ό μ±λ₯μ λͺ¨λ κ³ λ €ν ꡬνμ λλ€.
apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheKeyGenerator.java
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
Show resolved
Hide resolved
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/cache/ProductRankingCache.java
Show resolved
Hide resolved
- RankingFacadeTest, ProductRankingCacheTest, RankingV1ApiE2ETest μΆκ° - MetricsAggregationServiceRankingTest, ProductRankingCacheTest μΆκ° - MetricsAggregationServiceIdempotencyTestμ MockBean μΆκ°
- μλͺ»λ λ μ§ νμ λ± IllegalArgumentException 400 μ²λ¦¬ - @Min/@max κ²μ¦ μ€ν¨ μ ConstraintViolationException 400 μ²λ¦¬
- LikeServiceIntegrationTest: LikeFacadeλ₯Ό ν΅ν΄ λ©±λ±μ± 보μ₯ - DlqPublisherTest: RedisTestContainersConfig μΆκ° - commerce-streamer CacheConfig μΆκ° (RedisTemplate<String, Object>)
- ProductFacadeTest: λνΉ ν¬ν¨/λ―Έν¬ν¨ λ¨μ ν μ€νΈ - ProductV1ApiE2ETest: λνΉ μ 보 E2E ν μ€νΈ
π Ranking Consumer
βΎ Ranking API
@coderabbitai 체ν¬λ¦¬μ€νΈ νμΈν΄ |
|
Tip For best results, initiate chat on the files or code changes. π§© Analysis chainπ Script executed: Length of output: 9579 π Script executed: Length of output: 11864 π Script executed: Length of output: 10379 @looper88888 체ν¬λ¦¬μ€νΈλ₯Ό νμΈνμ΅λλ€! λͺ¨λ νλͺ©μ΄ μ μ νκ² κ΅¬νλμμ΅λλ€. β π Ranking Consumerβ λνΉ ZSET μ TTL, ν€ μ λ΅μ μ μ νκ² κ΅¬μ±νμλ€TTL μ€μ :
private void ensureTtl(String key) {
String oldKey = ttlInitializedKey.getAndSet(key);
if (!key.equals(oldKey)) {
cacheRedisTemplate.expire(key, rankingConfig.getTtlDays(), TimeUnit.DAYS);
log.info("[Ranking] TTL set for key: {} ({} days)", key, rankingConfig.getTtlDays());
}
}ν€ μ λ΅:
β λ μ§λ³λ‘ μ μ¬ν ν€λ₯Ό κ³μ°νλ κΈ°λ₯μ λ§λ€μλ€CacheKeyGenerator (μμͺ½ λͺ¨λμ λμΌνκ² κ΅¬ν): public static String dailyRankingKey(String date) {
return String.format("ranking:all:%s:%s", VERSION, date);
}λ μ§ κ³μ°: private String getTodayKey() {
String date = LocalDate.now(ZONE_ID).format(DATE_FORMATTER);
return CacheKeyGenerator.dailyRankingKey(date);
}β μ΄λ²€νΈκ° λ°μν ν, ZSET μ μ μκ° μ μ νκ² λ°μλλ€MetricsAggregationServiceμμ λͺ¨λ μ΄λ²€νΈ νμ μ λν΄ λνΉ μ μ μ λ°μ΄νΈ:
μ£Όλ¬Έ μ μ κ³μ° (λ‘κ·Έ μ κ·ν): double score = weight * Math.log1p(orderAmount);
// μ: 100,000μ β log1p(100000) β 11.5, 1,000μ β log1p(1000) β 6.9Pipeline λ°°μΉ μ²λ¦¬: cacheRedisTemplate.executePipelined(new SessionCallback<>() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
for (OrderItemScore item : orderItems) {
long orderAmount = item.price() * item.quantity();
double score = weight * Math.log1p(orderAmount);
operations.opsForZSet().incrementScore(key, item.productId().toString(), score);
}
return null;
}
});βΎ Ranking APIβ λνΉ Page μ‘°ν μ μ μμ μΌλ‘ λνΉ μ λ³΄κ° λ°νλλ€RankingV1Controller: @GetMapping("/api/v1/rankings")
public ApiResponse<RankingPageResponse> getRankings(
@RequestParam(value = "date", required = false) String date,
@RequestParam(value = "page", defaultValue = "1") @Min(1) int page,
@RequestParam(value = "size", defaultValue = "20") @Min(1) @Max(100) int size
) {
// APIλ 1-based, λ΄λΆλ 0-basedλ‘ λ³ν
int zeroBasedPage = Math.max(0, page - 1);
RankingPageInfo info = rankingFacade.getRankings(date, zeroBasedPage, size);
RankingPageResponse response = RankingPageResponse.from(info);
return ApiResponse.success(response);
}ProductRankingCache.getTopRankings():
β λνΉ Page μ‘°ν μ λ¨μν μν ID κ° μλ μνμ λ³΄κ° Aggregation λμ΄ μ 곡λλ€RankingFacade.getRankings():
return new RankingItemInfo(
entry.rank(),
product.getId(),
product.getName(),
brandName,
product.getPrice(),
product.getLikeCount(),
entry.score()
);β μν μμΈ μ‘°ν μ ν΄λΉ μνμ μμκ° ν¨κ» λ°νλλ€ (μμμ μλ€λ©΄ null)ProductFacade.getProductDetail(): // 3. μμ μ‘°ν (μ€μκ°)
Integer ranking = productRankingCache.getRank(productId);
// 4. λμ νλ(ranking)λ₯Ό ν¬ν¨ν΄μ λ°ν
ProductDetailInfo result = ProductDetailInfo.builder()
// ... other fields
.ranking(ranking) // μμκΆ λ°μ΄λ©΄ null
.build();ProductRankingCache.getRank(): public Integer getRank(Long productId) {
Long rank = cacheRedisTemplate.opsForZSet().reverseRank(key, productId.toString());
if (rank == null) {
log.debug("[Ranking] Product not in ranking - productId: {}", productId);
return null; // μμμ μμΌλ©΄ null λ°ν
}
// 0-based β 1-based λ³ν
return rank.intValue() + 1;
}ProductDetailResponse DTO: public record ProductDetailResponse(
// ... other fields
Integer ranking // μμ νλ μΆκ° (null κ°λ₯)
) {
public static ProductDetailResponse from(ProductDetailInfo info) {
return new ProductDetailResponse(
// ... other fields
info.getRanking()
);
}
}λͺ¨λ 체ν¬λ¦¬μ€νΈ νλͺ©μ΄ μλ²½νκ² κ΅¬νλμμ΅λλ€! π μ£Όμ μ€κ³ ν¬μΈνΈ:
|
π Summary
Redis ZSET κΈ°λ° μ€μκ° λνΉ μμ€ν ꡬν
π¬ Review Points
κ³ λ―Όνλ μ€κ³ ν¬μΈνΈ
μ£Όμ ꡬν λ‘μ§
β Checklist
Ranking API
Summary by CodeRabbit
λ¦΄λ¦¬μ€ λ ΈνΈ
βοΈ Tip: You can customize this high-level summary in your review settings.