-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-9] Product Ranking with Redis #215
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
1. kafka.yml ์ ๊ธฐ์ฌ๋
testImplementation(testFixtures(project(":modules:kafka")))
์ ๋ํ ์ค์ ์ฝ๋๋ค์ ์ถ๊ฐ.
2. ๊ทธ ํ ํ
์คํธ ์ฝ๋ ์ํ์,
- ์คํ๋ง์ปจํ
์คํธ, ์นดํ์นด ํ ํฝ, ์ปจ์๋จธ๊ทธ๋ฃน, ์ปจ์๋จธ ๋ฑ ์์ฑ -> ํ
์คํธ ๋ ์ ์๋๋ก ์ค์
- @beforeeach ์ ์๋์ ๊ฐ์ด ๋ณ๊ณ , ์ด๋ ์ปจ์๋จธ ์ปจํ
์ด๋๊ฐ ๋จผ์ ๋ ์ ํ ํฝ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋๋ฐ, ๊ทธ ์์ ์ ํ ํฝ์ด ์๊ธฐ ๋๋ฌธ์ด๋ค.
```
org.apache.kafka.clients.NetworkClient : [Consumer clientId=commerce-streamer-0, groupId=loopers-default-consumer] Error while fetching metadata with correlation id 2 : {order-events=UNKNOWN_TOPIC_OR_PARTITION}
```
> ์ผ๋จ ๋์๊ฐ๊ฒ ๊ตฌํ
1. ์ด๊ธฐ ๋ญํน ZSET ๋ฐ ํค ์ ๋ต
- ProductLike ์ด๋ฒคํธ์ ๋ํด์ weight=0.2, score = weight * 1 ๋ก ๊ณ์ฐํ์ฌ zset์ ์ง๊ณ
- TTL : 2Day / Key: ranking:all:{yyyyMMdd}
2. ์ปจ์๋จธ ๋ฉ์์ง ์ฒ๋ฆฌ ๋ฐ ์ง๊ณ
- (์ด๊ธฐ) streamer ๋ชจ๋์ ๋ญํน ์ง๊ณ ์๋น์ค ๊ตฌํ ๋ฐ ์ปจ์๋จธ ๋ฆฌ์ค๋์ ๋จ์ผ ๋ฉ์์ง ๋จ๊ฑด ์ฒ๋ฆฌ
Week9 feature redis ์ด๊ธฐ๊ตฌํ
> ์ผ๋จ ๋์๊ฐ๊ฒ ๊ตฌํ
์ค๋ ZSET ์ ์๋ฅผ 0.1 ๊ฐ์ค์น๋ก ๋ด์ผ ํค๋ก ๋ณต์ฌํ๊ณ TTL์ ์ค์
- Redis์์ EXPIRE๋ ์ฐ๊ธฐ ์ฐ์ฐ์ด๋ค. - ๋คํธ์ํฌ RTT - ๋งค๋ฒ ZINCRBY + EXPIRE = ์๋ณต 2๋ฒ - Kafka ์ด๋ฒคํธ๋น 1~2 RTT โ TPS ๋์์ง์๋ก ์ฒด๊ฐ
Week9 feature redis
Walkthrough์ผ์ผ ์ํ ์์ ์์คํ ์ ๊ตฌํํฉ๋๋ค. Redis ZSET ๊ธฐ๋ฐ์ผ๋ก ์ข์์ ์ด๋ฒคํธ๋ฅผ ์ถ์ ํ๊ณ , ๋งค์ผ ์ ํด์ง ์๊ฐ์ ์ ์๋ฅผ ์ด์ํฉ๋๋ค. REST API๋ฅผ ํตํด ์์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๊ณ , ์ํ ์์ธ ์กฐํ์ ์์ ์ ์๋ฅผ ํฌํจํฉ๋๋ค. Kafka ํ ์คํธ ์ธํ๋ผ๋ฅผ ๊ฐ์ ํฉ๋๋ค. Changes
Sequence Diagram(s)sequenceDiagram
participant PLE as ProductLikeEvent
participant Consumer as ProductLikeEventConsumer
participant RAS as RankingAggregationService
participant Redis as Redis ZSET
participant Scheduler as Scheduler (23:30)
PLE->>Consumer: ์ข์์ ์ด๋ฒคํธ ์์
Consumer->>RAS: applyLike(productId, occurredAt)
RAS->>Redis: ZSET ์ ์ ์ฆ๊ฐ (0.2 LIKE_WEIGHT)
Redis-->>RAS: ์๋ฃ
RAS->>Redis: ์ผ์ผ ํค TTL ์ค์ (2์ผ)
rect rgb(200, 220, 250)
Note over Scheduler,Redis: ๋งค์ผ 23:30 ์คํ
Scheduler->>RAS: carryOverDailyRanking()
RAS->>Redis: ZUNIONSTORE (๊ฐ์ค์น: 0.1)
RAS->>Redis: ๋ด์ผ ํค TTL ์ค์
Redis-->>RAS: ์ด์ ์๋ฃ
end
sequenceDiagram
participant Client as ํด๋ผ์ด์ธํธ
participant Controller as ProductV1Controller
participant RankingService as RankingQueryService
participant ProductService as ProductQueryService
participant Redis as Redis
Client->>Controller: ์ํ ์์ธ ์กฐํ ์์ฒญ
Controller->>RankingService: getDailyRankingScore(productId)
RankingService->>Redis: ์ผ์ผ ์์ ์ ์ ์กฐํ
Redis-->>RankingService: ์ ์ ๋ฐํ
Controller->>ProductService: ์ํ ์์ธ ์ ๋ณด ์กฐํ
ProductService-->>Controller: ์ํ ์ ๋ณด ๋ฐํ
Controller-->>Client: ProductDetailResponse (content + rankingScore)
Estimated code review effort๐ฏ 3 (Moderate) | โฑ๏ธ ~20 minutes 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: 5
๐งน Nitpick comments (13)
modules/redis/src/main/java/com/loopers/ranking/RankingPolicy.java (1)
1-7: ๋น ํ๋ ์ด์คํ๋ ํด๋์ค - ๊ตฌํ ํ์ํ์ฌ
RankingPolicyํด๋์ค๋ ์ฃผ์๋ง ์๊ณ ๊ตฌํ์ด ์๋ ํ๋ ์ด์คํ๋์ ๋๋ค. PR ์๊ตฌ์ฌํญ์ ๋ฐ๋ฅด๋ฉด ์ด๋ฒคํธ ํ์ ๋ณ๋ก ๊ฐ์ค์น๋ฅผ ๊ฒฐ์ ํ๋ ๋ก์ง์ด ํ์ํฉ๋๋ค (์: ProductLike โ 0.2, ์ผ์ผ ์ด์ โ 0.1).๋ค์๊ณผ ๊ฐ์ ๊ตฌํ ๋ฐฉ์์ ๊ณ ๋ คํด๋ณด์ธ์:
- Enum ๊ธฐ๋ฐ: ๊ฐ ์ด๋ฒคํธ ํ์ ์ ๊ฐ์ค์น๋ฅผ ๋งคํ
- Strategy ํจํด: ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด๋ก ๋ถ๋ฆฌ
- ์ ์ ์์/๋ฉ์๋: ๊ฐ๋จํ ๋งคํ ๋ก์ง
๊ตฌํ ์ฝ๋๋ฅผ ์์ฑํ๊ฑฐ๋, ์ด ์์ ์ ์ถ์ ํ๊ธฐ ์ํ ์ด์๋ฅผ ์์ฑํด๋๋ฆด๊น์?
modules/kafka/src/main/resources/kafka.yml (1)
18-18: TODO ์ฃผ์์ ํด๊ฒฐํ๊ฑฐ๋ ์ด์๋ก ์ถ์ ํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.ํ๋ก๋์์ acks, enable.idempotence, max.in.flight.requests.per.connection ์ค์ ์ ๋ฉ์์ง ์ ์ก ์ ๋ขฐ์ฑ๊ณผ ์์ ๋ณด์ฅ์ ์ค์ํ ์ํฅ์ ๋ฏธ์นฉ๋๋ค. ํ์ฌ ์ค์ ์ด ํ๋ก์ ํธ ์๊ตฌ์ฌํญ์ ์ ํฉํ์ง ๊ฒํ ํ๊ณ , TODO๋ฅผ ์ ๊ฑฐํ๊ฑฐ๋ ๋ณ๋ ์ด์๋ก ์ถ์ ํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
์ด ์ค์ ๋ค์ ๋ํ ๊ถ์ฅ ๊ตฌ์ฑ์ ์ ์ํ๊ฑฐ๋ ๊ด๋ จ ๋ฌธ์๋ฅผ ์ฐพ์๋๋ฆด๊น์?
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
7-7: ๋ฏธ์ฌ์ฉ import๋ฅผ ์ ๊ฑฐํ์ธ์.
RankingEntryimport๊ฐ ์ฌ์ฉ๋์ง ์์ต๋๋ค. Line 49์์ ์ฌ์ฉํ๋ ํ์ ์OptionalDouble์ ๋๋ค.๐ ์ ์ํ๋ ์์
-import com.loopers.ranking.RankingEntry;apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java (1)
44-44: ์ฃผ์ ์ฒ๋ฆฌ๋ ์ฝ๋์ ์๋๋ฅผ ๋ช ํํ ํ์ธ์.
resetAllTestTopics()ํธ์ถ์ด ์ฃผ์ ์ฒ๋ฆฌ๋์ด ์์ต๋๋ค. ์๋์ ์ธ ๋ณ๊ฒฝ์ธ์ง ํ์ธ์ด ํ์ํฉ๋๋ค. ํ ํฝ ๋ฆฌ์ ์ด ๋ถํ์ํ ๊ฒฝ์ฐ ์ฃผ์์ด ์๋ ์์ ํ ์ ๊ฑฐํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (2)
3-3: ๋ฏธ์ฌ์ฉ import๋ฅผ ์ ๊ฑฐํ์ธ์.
RankingEntryimport๊ฐ ์ด ํ์ผ์์ ์ฌ์ฉ๋์ง ์์ต๋๋ค.๐ ์ ์ํ๋ ์์
-import com.loopers.ranking.RankingEntry;
36-38: null ๋์ OptionalDouble.empty()๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.rankingScore๊ฐ ์์ ๋
null์ ๋ฐํํ๋ ๋์OptionalDouble.empty()๋ฅผ ์ฌ์ฉํ๋ฉด ์๋ฏธ๊ฐ ๋ ๋ช ํํด์ง๋๋ค.๐ ์ ์ํ๋ ์์
static <T> ProductDetailResponse<T> of(T content) { - return new ProductDetailResponse<>(content, null); + return new ProductDetailResponse<>(content, OptionalDouble.empty()); }modules/redis/src/main/java/com/loopers/ranking/RankingKey.java (1)
11-16: Javadoc์ ์์ฑํ์ธ์.๋ฉ์๋์
@param๊ณผ@returnํ๊ทธ์ ์ค๋ช ์ด ๋๋ฝ๋์ด ์์ต๋๋ค. ๊ฐ ํ๋ผ๋ฏธํฐ์ ๋ฐํ๊ฐ์ ์๋ฏธ๋ฅผ ๋ช ํํ ๋ฌธ์ํํ์ธ์.๐ ์ ์ํ๋ ์์
/** * ์ผ๋ณ ๋ญํน ๋ ๋์คํค๋ฅผ ์ ์ํฉ๋๋ค. * ranking:all:{yyyyMMdd} - * @param date - * @return + * @param date ๋ญํน์ ์กฐํํ ๋ ์ง + * @return Redis์ ์ ์ฅํ ์ผ๋ณ ๋ญํน ํค (ํ์: ranking:all:yyyyMMdd) */apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
18-24: ์๋ต ๋ํผ ์ผ๊ด์ฑ ๊ฒํ ํ์
ProductV1Controller๋ApiResponse<>๋ก ์๋ต์ ๊ฐ์ธ๊ณ ์์ง๋ง, ์ด ์ปจํธ๋กค๋ฌ๋RankingQueryResponse๋ฅผ ์ง์ ๋ฐํํฉ๋๋ค. API ์๋ต ํ์์ ์ผ๊ด์ฑ์ ์ํด ๋์ผํ ํจํด์ ์ ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์๋ ์์
- public RankingQueryResponse getDailyRanking( + public ApiResponse<RankingQueryResponse> getDailyRanking( @RequestParam(required = false, name = "date") String date, @RequestParam(defaultValue = "20", name = "size") int size ) { - return rankingQueryService.getDailyPopularProducts(date, size); + return ApiResponse.success(rankingQueryService.getDailyPopularProducts(date, size)); }apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryResponse.java (1)
9-16: ํ๋ ๋ช ๋ช ์ผ๊ด์ฑ: ๋ณต์ํ ์ฌ์ฉ ๊ถ์ฅ
productLikeSummaryํ๋๊ฐList<ProductLikeSummary>๋ฅผ ๋ด๊ณ ์์ผ๋ฏ๋ก, ๋ณต์ํproductLikeSummaries๋ก ๋ช ๋ช ํ๋ ๊ฒ์ด ๋ ๋ช ํํฉ๋๋ค.๐ ์ ์๋ ์์
public record RankingQueryResponse( LocalDate date, List<RankingEntry> rankingEntries, - List<ProductLikeSummary> productLikeSummary + List<ProductLikeSummary> productLikeSummaries ) { - public static RankingQueryResponse of(LocalDate date, List<RankingEntry> rankingEntries, List<ProductLikeSummary> productLikeSummary) { - return new RankingQueryResponse(date, rankingEntries, productLikeSummary); + public static RankingQueryResponse of(LocalDate date, List<RankingEntry> rankingEntries, List<ProductLikeSummary> productLikeSummaries) { + return new RankingQueryResponse(date, rankingEntries, productLikeSummaries); } }modules/redis/src/main/java/com/loopers/ranking/RankingZSetRepository.java (1)
30-33: TODO ์ฝ๋ฉํธ ํด๊ฒฐ ํ์"๋ญํน์ ๋ณด๊ฐ ์๋ค๋ฉด?" TODO๊ฐ ๋จ์์์ต๋๋ค. ๋น ๋ฆฌ์คํธ๋ฅผ ๋ฐํํ๋ ํ์ฌ ๋์์ด ์๋๋ ๊ฒ์ธ์ง, ์๋๋ฉด ์ถ๊ฐ ์ฒ๋ฆฌ๊ฐ ํ์ํ์ง ๋ช ํํ ํด์ฃผ์ธ์.
์ด TODO๋ฅผ ํด๊ฒฐํ๊ฑฐ๋ ์๋ก์ด ์ด์๋ก ๋ฑ๋กํด ๋๋ฆด๊น์?
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregationService.java (1)
23-32: ํ์์กด ๋ช ์์ ์ง์ ๊ถ์ฅ
ZoneId.systemDefault()๋ ์๋ฒ ํ๊ฒฝ์ ๋ฐ๋ผ ๋ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ ์ ์์ด ๋ถ์ฐ ์์คํ ์์ ์ผ๊ด์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์๋น์ค ์ง์ญ์ ๋ง๋ ๋ช ์์ ํ์์กด(์:ZoneId.of("Asia/Seoul")) ์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.๐ ์ ์๋ ์์
+ private static final ZoneId SERVICE_ZONE = ZoneId.of("Asia/Seoul"); + public void applyLike(long productId, Instant occurredAt) { - LocalDate day = occurredAt.atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate day = occurredAt.atZone(SERVICE_ZONE).toLocalDate(); String key = RankingKey.dailyAll(day);apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.java (1)
54-56: ๋ฉ์๋ ๋ช ๋ช ์ด ํผ๋์ค๋ฝ์ต๋๋ค
hasValidDate๊ฐ ๋ ์ง๊ฐnull์ด๊ฑฐ๋ ๋น์ด์์ ๋true๋ฅผ ๋ฐํํฉ๋๋ค. ์ด๋ฆ์ด ๋ก์ง๊ณผ ๋ฐ๋์ ๋๋ค.isNullOrBlank๋๋shouldUseDefaultDate์ ๊ฐ์ด ์๋๋ฅผ ๋ช ํํ ํ๋ ์ด๋ฆ์ ์ฌ์ฉํ์ธ์.๐ ์ ์๋ ์์
- private boolean hasValidDate(String date) { - return date == null || date.isBlank(); + private boolean shouldUseDefaultDate(String date) { + return date == null || date.isBlank(); }modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.java (1)
76-81: Thread.sleep ๋์ ํด๋ง ๋ฐฉ์ ๊ณ ๋ ค
Thread.sleep(1000)์ ํ ํฝ ์ญ์ ์๋ฃ๋ฅผ ๋ณด์ฅํ์ง ์์ต๋๋ค. ํ ํฝ ๋ชฉ๋ก์์ ์ญ์ ๋์๋์ง ํด๋งํ๋ ๋ฐฉ์์ด ๋ ์์ ์ ์ ๋๋ค. ๋ค๋ง, ํ ์คํธ ์ ํธ๋ฆฌํฐ์์๋ ํ์ฌ ์ ๊ทผ ๋ฐฉ์๋ ์ค์ฉ์ ์ ๋๋ค.
๐ 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/ranking/RankingQueryResponse.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.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-streamer/src/main/java/com/loopers/application/ranking/RankingAggregationService.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductLikeEventConsumer.javaapps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.javaapps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingAggregationServiceTest.javamodules/kafka/src/main/resources/kafka.ymlmodules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.javamodules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.javamodules/redis/src/main/java/com/loopers/ranking/DailyRankingResponse.javamodules/redis/src/main/java/com/loopers/ranking/RankingEntry.javamodules/redis/src/main/java/com/loopers/ranking/RankingKey.javamodules/redis/src/main/java/com/loopers/ranking/RankingPolicy.javamodules/redis/src/main/java/com/loopers/ranking/RankingZSetRepository.java
๐งฐ Additional context used
๐ง Learnings (7)
๐ 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/ProductLikeEventConsumer.javaapps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.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/ProductLikeEventConsumer.javaapps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.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/ProductLikeEventConsumer.java
๐ Learning: 2025-12-18T13:24:54.339Z
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:54.339Z
Learning: In this codebase, Kafka consumers delegate error handling and event tracking to the service layer via EventInboxAspect. Service methods annotated with InboxEvent are intercepted by the aspect, which handles failures by saving failed EventInbox entries and logging errors. This centralized approach avoids duplicating error handling logic across multiple consumers.
Applied to files:
apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
๐ Learning: 2025-12-19T23:39:20.851Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-streamer/src/main/kotlin/com/loopers/interfaces/consumer/product/ProductEventConsumer.kt:0-0
Timestamp: 2025-12-19T23:39:20.851Z
Learning: In Loopers-dev-lab/loopers-spring-kotlin-template, toongri prefers pragmatic idempotency handling in Kafka consumers: when idempotency key persistence fails after successful business logic execution, log a warning and continue (accepting low risk of duplicates on retry) rather than rolling back business data. This keeps business logic decoupled from idempotency store (which might be Redis/external system, not RDB), avoids over-engineering for rare edge cases, and prioritizes core business value over auxiliary metadata. Transaction-based atomicity between business data and idempotency keys is considered excessive when duplicate event probability is very low.
Applied to files:
apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.javamodules/kafka/src/main/resources/kafka.yml
๐ Learning: 2025-12-19T03:23:54.756Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 191
File: modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java:24-34
Timestamp: 2025-12-19T03:23:54.756Z
Learning: In the loopers-spring-java-template project, test container configurations (MySQL, Redis, Kafka) use static initialization blocks for consistency across the template. This pattern should be maintained for uniformity, even if alternative lifecycle management approaches exist.
Applied to files:
apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
๐ Learning: 2025-12-19T03:23:51.674Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 191
File: modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java:24-34
Timestamp: 2025-12-19T03:23:51.674Z
Learning: Maintain the use of static initialization blocks in test fixtures for testcontainers configurations (e.g., MySQL, Redis, Kafka) to ensure consistency and predictable initialization across the loopers-spring-java-template project. Apply this pattern to all test-fixture Java files under src/testFixtures/java to preserve uniform lifecycle behavior, unless there's a compelling, well-justified reason to diverge.
Applied to files:
modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.javamodules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java
๐งฌ Code graph analysis (6)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
RequiredArgsConstructor(19-52)apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
RequiredArgsConstructor(11-26)
apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingAggregationServiceTest.java (1)
modules/redis/src/main/java/com/loopers/ranking/RankingKey.java (1)
RankingKey(9-21)
modules/redis/src/main/java/com/loopers/ranking/RankingZSetRepository.java (1)
modules/redis/src/main/java/com/loopers/ranking/RankingKey.java (1)
RankingKey(9-21)
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregationService.java (1)
modules/redis/src/main/java/com/loopers/ranking/RankingKey.java (1)
RankingKey(9-21)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
RequiredArgsConstructor(19-52)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
RequiredArgsConstructor(11-26)apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
ProductV1Dto(9-44)
๐ Additional comments (9)
modules/redis/src/main/java/com/loopers/ranking/RankingEntry.java (1)
1-4: LGTM!๊ฐ๊ฒฐํ๊ณ ๋ช ํํ record ์ ์์ ๋๋ค. ๋ญํน ๋ฐ์ดํฐ๋ฅผ ์ํ double ํ์ ์ฌ์ฉ๋ ์ ์ ํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
47-51: LGTM!๋ญํน ์ ์๋ฅผ ์ํ ์์ธ ์ ๋ณด์ ํตํฉํ๋ ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.
OptionalDouble์ ์ฌ์ฉํ nullable ์ฒ๋ฆฌ๋ ์ ์ ํฉ๋๋ค.modules/redis/src/main/java/com/loopers/ranking/DailyRankingResponse.java (1)
1-7: LGTM!์ผ๋ณ ๋ญํน ๋ฐ์ดํฐ๋ฅผ ๋ด๋ ๊ฐ๊ฒฐํ record ์ ์์ ๋๋ค. LocalDate์ List ์กฐํฉ์ด ์ ์ ํฉ๋๋ค.
modules/redis/src/main/java/com/loopers/ranking/RankingKey.java (1)
17-19: LGTM!Redis ํค ์์ฑ ๋ก์ง์ด ๋ช ํํ๊ณ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.
BASIC_ISO_DATEํฌ๋งท ์ฌ์ฉ๋ ์ ์ ํฉ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductLikeEventConsumer.java (1)
26-26: LGTM!๋ญํน ์ง๊ณ ์๋น์ค๊ฐ ๊น๋ํ๊ฒ ํตํฉ๋์์ต๋๋ค. ๋ฉํธ๋ฆญ ์ง๊ณ ํ ๋ญํน ์ง๊ณ๋ฅผ ์ํํ๋ ์์๊ฐ ์ ์ ํ๋ฉฐ, ์๋ฌ ์ฒ๋ฆฌ๋ ๊ธฐ์กด ํจํด๋๋ก aspect์ ์์๋์ด ์ผ๊ด์ฑ์ด ์ ์ง๋ฉ๋๋ค.
Also applies to: 47-47
modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java (1)
28-46: LGTM!์ ์ ์ด๊ธฐํ ๋ธ๋ก ํจํด์ด ํ๋ก์ ํธ์ testcontainers ์ค์ ์ปจ๋ฒค์ ๊ณผ ์ผ์นํฉ๋๋ค. ์ปจํ ์ด๋ ์์, ๋์ ํฌํธ ์ฃผ์ , ํ ํฝ ๋น ์ ์๊ฐ ์ ์ ํ๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค. Based on learnings, ์ด ํจํด์ ํ๋ก์ ํธ ์ ๋ฐ์์ ์ผ๊ด๋๊ฒ ์ ์ง๋์ด์ผ ํฉ๋๋ค.
modules/redis/src/main/java/com/loopers/ranking/RankingZSetRepository.java (1)
35-40: NumberFormatException ์ฒ๋ฆฌ ํ์ZSET ๋ฉค๋ฒ๊ฐ ์ซ์๊ฐ ์๋ ๊ฐ์ ํฌํจํ ๊ฒฝ์ฐ
Long.parseLong()์์ ์์ธ๊ฐ ๋ฐ์ํฉ๋๋ค. ๋ฐฉ์ด์ ์ฝ๋ฉ์ ์ํด ์์ธ ์ฒ๋ฆฌ๋ฅผ ์ถ๊ฐํ๊ฑฐ๋, ๋ฐ์ดํฐ ์ ํฉ์ฑ์ด ๋ณด์ฅ๋๋์ง ํ์ธ์ด ํ์ํฉ๋๋ค.๐ ์ ์๋ ์์
for(ZSetOperations.TypedTuple<String> tuple : tuples) { if(tuple.getValue() == null) continue; - long productId = Long.parseLong(tuple.getValue()); - double score = tuple.getScore() == null ? 0d : tuple.getScore(); - result.add(new RankingEntry(productId, score)); + try { + long productId = Long.parseLong(tuple.getValue()); + double score = tuple.getScore() == null ? 0d : tuple.getScore(); + result.add(new RankingEntry(productId, score)); + } catch (NumberFormatException e) { + // ์๋ชป๋ ํ์์ ๋ฉค๋ฒ๋ ๊ฑด๋๋ + continue; + } }apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingAggregationServiceTest.java (1)
38-69: ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง ์ํธCarry-over ๋ก์ง์ ํต์ฌ ์๋๋ฆฌ์ค(๊ฐ์ค์น ์ ์ฉ ๋ฐ TTL ์ค์ )๋ฅผ ์ ์ ํ ๊ฒ์ฆํ๊ณ ์์ต๋๋ค. ์ถ๊ฐ๋ก ์ฃ์ง ์ผ์ด์ค(๋น ์์ค, ๊ธฐ์กด ํ๊ฒ ์กด์ฌ ์)์ ๋ํ ํ ์คํธ๋ ๊ณ ๋ คํด ๋ณด์ธ์.
modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.java (1)
31-46: ํ ์คํธ ๊ฒฉ๋ฆฌ ์ ํธ๋ฆฌํฐ ์ค๊ณ ์ํธ
KafkaAdmin์ ์ฃผ์ ๋ฐ์ ๋์ ์ผ๋ก AdminClient๋ฅผ ์์ฑํ๋ ๋ฐฉ์๊ณผ, ํ ํฝ/์ปจ์๋จธ ๊ทธ๋ฃน ๊ด๋ฆฌ ๋ฉ์๋๋ค์ด ์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค. ํ ์คํธ ํ๊ฒฝ ๊ฒฉ๋ฆฌ์ ํจ๊ณผ์ ์ธ ์ ํธ๋ฆฌํฐ์ ๋๋ค.
| private List<ProductLikeSummary> findProductSummaryFrom(List<RankingEntry> rankingEntries) { | ||
| List<ProductLikeSummary> result = new ArrayList<>(); | ||
|
|
||
| for(RankingEntry rankingEntry : rankingEntries) { | ||
| ProductLikeSummary summary; | ||
| try { | ||
| summary = productQueryService.getProductLikeSummary(rankingEntry.productId()); | ||
| } catch (CoreException e) { | ||
| if(e.getErrorType() == ErrorType.NOT_FOUND) { | ||
| log.error("Could not find product like summary for {}", rankingEntry.productId()); | ||
| } | ||
| summary = null; | ||
| } | ||
| result.add(summary); | ||
| } | ||
| return result; | ||
| } |
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.
N+1 ์ฟผ๋ฆฌ ํจํด ๋ฐ null ์ฒ๋ฆฌ ๊ฐ์ ํ์
-
N+1 ์ฟผ๋ฆฌ: ๊ฐ ๋ญํน ํญ๋ชฉ๋ง๋ค ๊ฐ๋ณ DB ์กฐํ๊ฐ ๋ฐ์ํฉ๋๋ค. productId ๋ชฉ๋ก์ผ๋ก ์ผ๊ด ์กฐํํ๋ ๊ฒ์ด ํจ์จ์ ์ ๋๋ค.
-
Null ์ฒ๋ฆฌ: ์ฐพ์ง ๋ชปํ ์ํ์ ๋ํด
null์ ๋ฆฌ์คํธ์ ์ถ๊ฐํ๋ฉด ํด๋ผ์ด์ธํธ์์ NPE๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ํํฐ๋งํ๊ฑฐ๋ ๋ช ์์ ์ธ "์ญ์ ๋จ" ํ์๋ฅผ ์ฌ์ฉํ์ธ์. -
์์ธ ์ฒ๋ฆฌ:
NOT_FOUND๊ฐ ์๋ ๋ค๋ฅธ ์์ธ๋ ๋ฌด์๋๊ณ ์์ต๋๋ค.
๐ ์ ์๋ ์์
private List<ProductLikeSummary> findProductSummaryFrom(List<RankingEntry> rankingEntries) {
- List<ProductLikeSummary> result = new ArrayList<>();
-
- for(RankingEntry rankingEntry : rankingEntries) {
- ProductLikeSummary summary;
- try {
- summary = productQueryService.getProductLikeSummary(rankingEntry.productId());
- } catch (CoreException e) {
- if(e.getErrorType() == ErrorType.NOT_FOUND) {
- log.error("Could not find product like summary for {}", rankingEntry.productId());
- }
- summary = null;
- }
- result.add(summary);
+ List<Long> productIds = rankingEntries.stream()
+ .map(RankingEntry::productId)
+ .toList();
+
+ // TODO: ProductQueryService์ ์ผ๊ด ์กฐํ ๋ฉ์๋ ์ถ๊ฐ ๊ณ ๋ ค
+ List<ProductLikeSummary> result = new ArrayList<>();
+ for (RankingEntry rankingEntry : rankingEntries) {
+ try {
+ ProductLikeSummary summary = productQueryService.getProductLikeSummary(rankingEntry.productId());
+ result.add(summary);
+ } catch (CoreException e) {
+ if (e.getErrorType() == ErrorType.NOT_FOUND) {
+ log.warn("Product not found for ranking entry: {}", rankingEntry.productId());
+ // null ๋์ ๊ฑด๋๋ฐ๊ธฐ
+ } else {
+ throw e; // NOT_FOUND๊ฐ ์๋ ์์ธ๋ ์ ํ
+ }
+ }
}
return result;
}๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingQueryService.java
around lines 65-81, the current loop causes N+1 queries, inserts nulls for
missing products and swallows non-NOT_FOUND exceptions; change to collect all
productIds from rankingEntries and call productQueryService to fetch summaries
in bulk, build a Map<productId,ProductLikeSummary>, iterate rankingEntries to
map to summaries while skipping or marking missing products (do not add nulls),
and for exceptions other than NOT_FOUND either log with details and rethrow or
propagate so failures aren't silently ignored; preserve original ranking order
in the final list.
| private void setTTLOnlyOnce(String key) { | ||
| Boolean exists = redisTemplate.hasKey(key); | ||
| if (Boolean.FALSE.equals(exists)) { | ||
| redisTemplate.expire(key, TTL); | ||
| } | ||
| } |
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.
TTL์ด ์ค์ ๋์ง ์๋ ์ฌ๊ฐํ ๋ฒ๊ทธ
setTTLOnlyOnce๊ฐ incrementScore ์ดํ์ ํธ์ถ๋๋ฏ๋ก, ์ด ์์ ์์ ํค๋ ํญ์ ์กด์ฌํฉ๋๋ค. ๋ฐ๋ผ์ hasKey๊ฐ ํญ์ true๋ฅผ ๋ฐํํ์ฌ TTL์ด ์ ๋ ์ค์ ๋์ง ์์ต๋๋ค.
๐ ์ ์๋ ์์
- private void setTTLOnlyOnce(String key) {
- Boolean exists = redisTemplate.hasKey(key);
- if (Boolean.FALSE.equals(exists)) {
- redisTemplate.expire(key, TTL);
- }
- }
+ /**
+ * TTL์ด ์ค์ ๋์ง ์์ ๊ฒฝ์ฐ์๋ง TTL์ ์ค์ ํฉ๋๋ค.
+ */
+ private void setTTLOnlyOnce(String key) {
+ Long currentTtl = redisTemplate.getExpire(key);
+ // TTL์ด -1์ด๋ฉด ๋ง๋ฃ ์๊ฐ์ด ์ค์ ๋์ง ์์ ์ํ
+ if (currentTtl != null && currentTtl == -1) {
+ redisTemplate.expire(key, TTL);
+ }
+ }๐ 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.
| private void setTTLOnlyOnce(String key) { | |
| Boolean exists = redisTemplate.hasKey(key); | |
| if (Boolean.FALSE.equals(exists)) { | |
| redisTemplate.expire(key, TTL); | |
| } | |
| } | |
| /** | |
| * TTL์ด ์ค์ ๋์ง ์์ ๊ฒฝ์ฐ์๋ง TTL์ ์ค์ ํฉ๋๋ค. | |
| */ | |
| private void setTTLOnlyOnce(String key) { | |
| Long currentTtl = redisTemplate.getExpire(key); | |
| // TTL์ด -1์ด๋ฉด ๋ง๋ฃ ์๊ฐ์ด ์ค์ ๋์ง ์์ ์ํ | |
| if (currentTtl != null && currentTtl == -1) { | |
| redisTemplate.expire(key, TTL); | |
| } | |
| } |
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregationService.java
around lines 71-76, setTTLOnlyOnce currently checks hasKey after incrementScore,
so the key always exists and TTL is never set; change the logic to set TTL when
the key is newly created โ e.g., after performing redis increment, check the
returned incremented value and call redisTemplate.expire(key, TTL) only if the
increment result equals 1 (meaning the key was just created), or alternatively
call a set-if-absent before increment and set TTL when that returns true; update
setTTLOnlyOnce or incrementScore accordingly so TTL is applied exactly once when
the key is created.
| Map<String, Object> consumerProps = kafkaProperties.buildConsumerProperties(); | ||
| consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "${spring.kafka.consumer.group-id}"); | ||
|
|
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.
consumer properties ์ค์ ์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.
Line 70์์ "${spring.kafka.consumer.group-id}" ๋ฌธ์์ด ๋ฆฌํฐ๋ด์ ์ฌ์ฉํ๊ณ ์๋๋ฐ, ์ด๋ Spring ํ๋ ์ด์คํ๋ ๋ฌธ๋ฒ์ผ๋ก ๋ฐํ์์ ํด์๋์ง ์์ต๋๋ค. ๋ํ ์ด๋ ๊ฒ ์์ฑํ consumerProps ๋งต์ด ์ค์ ๋ก ์ฌ์ฉ๋์ง ์๊ณ ์์ต๋๋ค.
์ค์ group-id ๊ฐ์ ์ฌ์ฉํ๋ ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์์ ํ์ธ์:
๐ ์ ์ํ๋ ์์
- Map<String, Object> consumerProps = kafkaProperties.buildConsumerProperties();
- consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "${spring.kafka.consumer.group-id}");
+ // ํ
์คํธ์์ ์ฌ์ฉํ ์ค์ consumer group id
+ String actualGroupId = kafkaProperties.getConsumer().getGroupId();๋ง์ฝ ์ด ์ฝ๋๊ฐ ์ค์ ๋ก ํ์ํ์ง ์๋ค๋ฉด ์ ๊ฑฐํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
Committable suggestion skipped: line range outside the PR's diff.
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/test/java/com/loopers/application/idempotency/EventHandledServiceTest.java
around lines 69 to 71, the test sets
consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG,
"${spring.kafka.consumer.group-id}") which incorrectly uses a Spring placeholder
string (and the consumerProps map is never used); either remove the unused
consumerProps block entirely, or replace the placeholder with the real group-id
value retrieved from the KafkaProperties object (e.g.,
kafkaProperties.getConsumer().getGroupId()) or from the Environment, and ensure
the populated consumerProps map is actually passed into the consumer setup so
the test uses the correct group-id at runtime.
| // TTL ํ์ธ (expire๋ฅผ ๋จผ์ ๊ฑธ๊ณ unionstore๋ฅผ ํ๋ฏ๋ก TTL์ด ์กด์ฌํด์ผ ํจ) | ||
| Long ttl = redisTemplate.getExpire(targetKey); // seconds | ||
| assertThat(ttl).isNotNull(); | ||
| assertThat(ttl).isGreaterThan(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.
์ฃผ์๊ณผ ์ค์ ์ฝ๋ ๋์ ๋ถ์ผ์น
์ฃผ์์ "expire๋ฅผ ๋จผ์ ๊ฑธ๊ณ unionstore๋ฅผ ํ๋ฏ๋ก"๋ผ๊ณ ๋์ด ์์ง๋ง, RankingAggregationService.carryOver()๋ ZUNIONSTORE ์คํ ํ EXPIRE๋ฅผ ์ค์ ํฉ๋๋ค. ์ฃผ์์ ์์ ํ๊ฑฐ๋ ์ ๊ฑฐํด ์ฃผ์ธ์.
๐ ์ ์๋ ์์
- // TTL ํ์ธ (expire๋ฅผ ๋จผ์ ๊ฑธ๊ณ unionstore๋ฅผ ํ๋ฏ๋ก TTL์ด ์กด์ฌํด์ผ ํจ)
+ // TTL ํ์ธ (ZUNIONSTORE ํ EXPIRE๋ฅผ ์ค์ ํ๋ฏ๋ก TTL์ด ์กด์ฌํด์ผ ํจ)
Long ttl = redisTemplate.getExpire(targetKey); // seconds๐ 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.
| // TTL ํ์ธ (expire๋ฅผ ๋จผ์ ๊ฑธ๊ณ unionstore๋ฅผ ํ๋ฏ๋ก TTL์ด ์กด์ฌํด์ผ ํจ) | |
| Long ttl = redisTemplate.getExpire(targetKey); // seconds | |
| assertThat(ttl).isNotNull(); | |
| assertThat(ttl).isGreaterThan(0); | |
| // TTL ํ์ธ (ZUNIONSTORE ํ EXPIRE๋ฅผ ์ค์ ํ๋ฏ๋ก TTL์ด ์กด์ฌํด์ผ ํจ) | |
| Long ttl = redisTemplate.getExpire(targetKey); // seconds | |
| assertThat(ttl).isNotNull(); | |
| assertThat(ttl).isGreaterThan(0); |
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/test/java/com/loopers/application/ranking/RankingAggregationServiceTest.java
around lines 65 to 68, the inline comment wrongly states that EXPIRE is set
before unionstore whereas RankingAggregationService.carryOver() actually calls
ZUNIONSTORE first and then sets EXPIRE; update or remove the comment to reflect
the real order (e.g., "EXPIRE is set after ZUNIONSTORE in carryOver() so TTL
must exist") and keep the existing TTL assertions unchanged.
| // ํ ํฝ ์์ฑ | ||
| NewTopic newTopic = TopicBuilder.name(topicName) | ||
| .partitions(3) | ||
| .replicas(1) | ||
| .config("min.insync.replicas", "1") | ||
| .build(); | ||
|
|
||
| adminClient.createTopics(Collections.singletonList(newTopic)) | ||
| .all() | ||
| .get(5, TimeUnit.SECONDS); |
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.
ํํฐ์ ์ ๋ถ์ผ์น
KafkaTestContainersConfig์์ product-like-events ํ ํฝ์ 1๊ฐ ํํฐ์
์ผ๋ก ์์ฑํ์ง๋ง, ์ด ์ ํธ๋ฆฌํฐ๋ ์ฌ์์ฑ ์ 3๊ฐ ํํฐ์
์ผ๋ก ์์ฑํฉ๋๋ค. ํ
์คํธ ๋์์ ์ผ๊ด์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
๐ ์ ์๋ ์์
ํ ํฝ๋ณ ์ค์ ์ ์ ์ํ๊ฑฐ๋, KafkaTestContainersConfig์ ๋์ผํ ํํฐ์
์๋ฅผ ์ฌ์ฉํ์ธ์:
NewTopic newTopic = TopicBuilder.name(topicName)
- .partitions(3)
+ .partitions(1)
.replicas(1)
.config("min.insync.replicas", "1")
.build();๐ค Prompt for AI Agents
In modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.java
around lines 104 to 113, the topic is being recreated with 3 partitions which
conflicts with KafkaTestContainersConfig that creates the product-like-events
topic with 1 partition; change the NewTopic creation to use the same partition
count as the test container config (e.g., set .partitions(1) for
product-like-events) or implement per-topic partition configuration (lookup the
expected partition count for each topic and use that value when building
NewTopic) so topic recreation matches the test environment.
๐ Summary
์ฐ๋ง ์ผ์ ์ด ์กฐ๊ธ ๋ฌด๋ฆฌ๊ฐ ๋์ด์
์ด๋ฒคํธ ์ ๋ํ ์นดํ์นด ์ปจ์์, ๋ ๋์ค๋ฅผ ํ์ฉํ ๋ญํน ์ง๊ณ/ ์กฐํ API ์ถ๊ฐ ๋ฑ์ ๊ณผ์ ๊ฒฝํ์ ๋ชฉํ๋ก,
๋จ๊ฑด ๋ฉ์์ง ์ปจ์ ํด์ Zset์ ๋ญํน ์ ์ฌ ํ๋๋ก ํ๊ณ ๊ทธ ์ธ ์๊ตฌ์ฌํญ๋ค์ ๊ตฌํํ์์ต๋๋ค.
๐ฌ Review Points
โ Checklist
๐ Ranking Consumer
โพ Ranking API
๐ References
Summary by CodeRabbit
Release Notes
New Features
Tests
โ๏ธ Tip: You can customize this high-level summary in your review settings.