-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-9] Product Ranking with Redis #212
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
WalkthroughKafka๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ๋ฅผ ๊ตฌํํ๊ณ , ์ํ ์์ ๋ฐ ๋ฉํธ๋ฆญ ์ถ์ ์์คํ ์ ์ถ๊ฐํ์ต๋๋ค. OutboxEventService๊ฐ Kafka๋ก ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๊ณ , ์๋ก์ด commerce-streamer ์๋น์ค๊ฐ ์ด๋ฒคํธ๋ฅผ ์๋นํ์ฌ ๋ฉํธ๋ฆญ๊ณผ ์์๋ฅผ ์ ๋ฐ์ดํธํฉ๋๋ค. ์ค๋ณต ์ ๊ฑฐ(Inbox) ๋ฐ ์คํจ ์ฒ๋ฆฌ(DLQ) ํจํด์ ๊ตฌํํ์ต๋๋ค. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant OrderFacade as OrderFacade
participant OutboxService as OutboxEventService
participant KafkaProducer as EventKafkaProducer
participant Kafka as Kafka Broker
Client->>OrderFacade: createOrder()
activate OrderFacade
OrderFacade->>OrderFacade: ์ฃผ๋ฌธ ์์ฑ, ์ฌ๊ณ ๊ฐ์
Note over OrderFacade: ์ฌ๊ณ ์์ง ์ ์บ์ ์ ๊ฑฐ
OrderFacade->>OutboxService: publishEvent(outbox)
deactivate OrderFacade
activate OutboxService
OutboxService->>KafkaProducer: publish(outbox)
deactivate OutboxService
activate KafkaProducer
rect rgb(200, 220, 240)
Note over KafkaProducer: ๋น๋๊ธฐ ๋ฐํ
KafkaProducer->>Kafka: ๋ฉ์์ง ๋ฐํ<br/>(aggregateId ํํฐ์
ํค)
end
Kafka-->>KafkaProducer: CompletableFuture
KafkaProducer->>OutboxService: ์ฑ๊ณต/์คํจ ์ฝ๋ฐฑ
deactivate KafkaProducer
activate OutboxService
OutboxService->>OutboxService: ์ํ ์
๋ฐ์ดํธ(Published/Failed)
deactivate OutboxService
sequenceDiagram
participant Kafka as Kafka Broker
participant CatalogConsumer as CatalogEventConsumer
participant InboxService as EventInboxService
participant MetricsService as ProductMetricsService
participant RankingAgg as RankingAggregator
participant DLQService as DeadLetterQueueService
Kafka->>CatalogConsumer: ์ด๋ฒคํธ ๋ฐฐ์น ์์
activate CatalogConsumer
loop ๊ฐ ์ด๋ฒคํธ
CatalogConsumer->>InboxService: isDuplicate(eventId)
activate InboxService
InboxService-->>CatalogConsumer: boolean
deactivate InboxService
alt ์ค๋ณต ์๋
CatalogConsumer->>InboxService: save(eventId, ...)
activate InboxService
InboxService-->>CatalogConsumer: ์ ์ฅ ์๋ฃ
deactivate InboxService
alt LikeCreatedEvent
CatalogConsumer->>MetricsService: incrementLikeCount(productId)
activate MetricsService
MetricsService-->>CatalogConsumer: ์
๋ฐ์ดํธ ์๋ฃ
deactivate MetricsService
CatalogConsumer->>RankingAgg: incrementLikeScore(productId)
activate RankingAgg
RankingAgg-->>CatalogConsumer: Redis ์
๋ฐ์ดํธ
deactivate RankingAgg
else ProductViewedEvent
CatalogConsumer->>MetricsService: incrementViewCount(productId)
CatalogConsumer->>RankingAgg: incrementViewScore(productId)
end
else ์ค๋ณต ๊ฐ์ง
Note over CatalogConsumer: ์ฒ๋ฆฌ ์คํต
end
end
rect rgb(240, 200, 200)
Note over CatalogConsumer: ์ฒ๋ฆฌ ์คํจ ์
CatalogConsumer->>DLQService: save(topic, eventId, error)
activate DLQService
DLQService-->>CatalogConsumer: DLQ ์ ์ฅ ์๋ฃ
deactivate DLQService
end
CatalogConsumer->>Kafka: ๋ฐฐ์น ํ์ธ
deactivate CatalogConsumer
sequenceDiagram
participant RankingScheduler as RankingScheduler<br/>(๋งค์ผ 23:50)
participant Redis as Redis
activate RankingScheduler
Note over RankingScheduler: prepareNextDayRanking() ์คํ
RankingScheduler->>Redis: ์ค๋ ๋ฐ์ดํฐ ์กฐํ<br/>(ranking:all:yyyyMMdd)
activate Redis
Redis-->>RankingScheduler: ์์ ๋ฐ์ดํฐ
deactivate Redis
alt ๋ฐ์ดํฐ ์กด์ฌ
RankingScheduler->>Redis: ZINTERSTORE๋ก ํฉ๋ณ<br/>(weight: 10% ์ ์ฉ)
activate Redis
Redis->>Redis: ๋ด์ผ ํค์<br/>๊ฐ์ค์น ์ ์ฉ ์ ์ ์ ์ฅ
deactivate Redis
RankingScheduler->>Redis: ๋ด์ผ ํค์ TTL ์ค์ <br/>(2์ผ)
activate Redis
Redis-->>RankingScheduler: TTL ์ค์ ์๋ฃ
deactivate Redis
end
deactivate RankingScheduler
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~60 minutes Possibly related PRs
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 |
- RankingKey: ์ผ๊ฐ/์๊ฐ๋ณ ๋ญํน ํค ์์ฑ ์ ๋ต - RankingScore: ๊ฐ์ค์น ๊ธฐ๋ฐ ์ ์ ๊ณ์ฐ (์กฐํ 0.1, ์ข์์ 0.2, ์ฃผ๋ฌธ 0.6) - RankingAggregator: Redis ZSET ๊ธฐ๋ฐ ๋ญํน ์ ์ ์ค์๊ฐ ์ง๊ณ - CatalogEventConsumer: ์ข์์/์กฐํ ์ด๋ฒคํธ โ ๋ญํน ์ ์ ๋ฐ์ - OrderEventConsumer: ์ฃผ๋ฌธ ์ด๋ฒคํธ โ ๋ญํน ์ ์ ๋ฐ์ (๋ก๊ทธ ์ ๊ทํ ์ ์ฉ) - ์ผ๊ฐ ๋ญํน (ranking:all:yyyyMMdd) ๋ฐ ์๊ฐ๋ณ ๋ญํน (ranking:realtime:yyyyMMddHH) ๋์ ์ง๊ณ - TTL: ์ผ๊ฐ 2์ผ, ์๊ฐ๋ณ 48์๊ฐ - GET /api/v1/rankings: ์ผ๊ฐ ๋ญํน ํ์ด์ง ์กฐํ (์ํ ์ ๋ณด ํฌํจ) - ์ํ ์์ธ ์กฐํ ์ ํ์ฌ ๋ญํน ์์ ์ ๋ณด ์ถ๊ฐ - RankingScheduler: ๋งค์ผ 23:50์ ๋ด์ผ ๋ญํน ํค ๋ฏธ๋ฆฌ ์์ฑ - Score Carry-Over: ์ ๋ ์ ์์ 10%๋ฅผ ๋ค์ ๋ ๋ก ๋ณต์ฌ - Redis ZSET (Sorted Set) - Spring Data Redis - Kafka Consumer ์ฐ๋ - Spring Scheduler
Consumer์ RankingAggregator ์์กด์ฑ์ด ์ถ๊ฐ๋๋ฉด์ ํ ์คํธ ์ฝ๋๋ ํจ๊ป ์์ ํ์ - CatalogEventConsumerTest: RankingAggregator Mock ์ถ๊ฐ - OrderEventConsumerTest: RankingAggregator Mock ์ถ๊ฐ
- ํ๋ก๋์ ํ๊ฒฝ์์์ ๋ก๊ทธ ์์ ์ฑ ๊ฐ์ - ๋ก๊ทธ ๋ถ์ ๋๊ตฌ ํธํ์ฑ ํฅ์ - ์ธ์ฝ๋ฉ ์ด์ ๋ฐฉ์ง ๋ณ๊ฒฝ๋ ํ์ผ: - RankingAggregator: ๋ฐฐ์น ์ ์ ๋ฐ์, ์ ์ ์ฆ๊ฐ ๋ก๊ทธ - RankingScheduler: ์ฝ๋ ์คํํธ ์ค์ผ์ค๋ฌ ๋ก๊ทธ - CatalogEventConsumer: Catalog ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก๊ทธ - OrderEventConsumer: Order ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก๊ทธ - RankingService: ๋ญํน ์กฐํ ๋ก๊ทธ - RankingFacade: ์ํ ์ ๋ณด ์กฐํฉ ๋ก๊ทธ
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: 16
Caution
Some comments are outside the diff and canโt be posted inline due to platform limitations.
โ ๏ธ Outside diff range comments (2)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
130-155: ์ฃผ๋ฌธ ์ทจ์ ์ ์บ์ ๊ฐฑ์ ๋๋ฝ
cancelOrder๋ฉ์๋์์ ์ฌ๊ณ ๋ฅผ ๋ณต๊ตฌํ ๋ (line 147) ์บ์ ๊ฐฑ์ ์ด ์์ต๋๋ค. ์ฌ๊ณ ๊ฐ 0์์ ์์๋ก ๋ณ๊ฒฝ๋ ๋๋ ์บ์๋ฅผ ์ญ์ ํด์ผ ๋ค์ ์กฐํ ์ ์ต์ ์ฌ๊ณ ์ ๋ณด๊ฐ ๋ฐ์๋ฉ๋๋ค.ํนํ ๋ค์ ์๋๋ฆฌ์ค์์ ๋ฌธ์ ๊ฐ ๋ฉ๋๋ค:
- ์ฌ๊ณ 0์ผ๋ก ์บ์ ์ญ์ โ DB์์ ์ฌ์กฐํ ์ stock=0 ์บ์ฑ
- ์ฃผ๋ฌธ ์ทจ์๋ก ์ฌ๊ณ ๋ณต๊ตฌ โ ์บ์๋ ์ฌ์ ํ stock=0
- ์ฌ์ฉ์๋ ํ์ ๋ก ํ์๋ ์ํ์ ๋ณผ ์ ์์
๐ ์ ์ํ๋ ์์
// ์ฌ๊ณ ๋ณต๊ตฌ (productId๋ก Product ์กฐํ) for (OrderItem orderItem : order.getOrderItems()) { Product product = productRepository.findById(orderItem.getProductId()) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ํ์ ์ฐพ์ ์ ์์ต๋๋ค.")); + + boolean wasOutOfStock = product.getStock() == 0; product.restoreStock(orderItem.getQuantity()); + + // ์ฌ๊ณ ๋ณต๊ตฌ ์ ์บ์ ๊ฐฑ์ + if (wasOutOfStock) { + log.info("์ฌ๊ณ ๋ณต๊ตฌ - ์บ์ ๊ฐฑ์ - productId: {}", product.getId()); + productCacheService.evictCache(product.getId()); + } }์ฐธ๊ณ : ์ ์์ ๋ ํธ๋์ญ์ ๊ฒฝ๊ณ ๋ฌธ์ ๊ฐ ์์ผ๋ฏ๋ก, ์์ ๋ฆฌ๋ทฐ ์ฝ๋ฉํธ์ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ ๊ทผ ๋ฐฉ์์ ํจ๊ป ์ ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
modules/kafka/src/main/resources/kafka.yml (1)
23-27: Consumer ์ค์ ์์ serializer ๋์ deserializer๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค.Line 26์
value-serializer๋ consumer ์น์ ์ ์์ผ๋ฏ๋กvalue-deserializer๋ก ์์ ๋์ด์ผ ํฉ๋๋ค.๐ ์ ์ํ๋ ์์
consumer: group-id: loopers-default-consumer key-deserializer: org.apache.kafka.common.serialization.StringDeserializer - value-serializer: org.apache.kafka.common.serialization.ByteArrayDeserializer + value-deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer properties: enable-auto-commit: false
๐งน Nitpick comments (33)
performance-test/README.md (2)
84-84: ์ฐธ๊ณ ๋ฌธ์ ์น์ ์ด ๋๋ฌด ๊ด๋ฒ์ํ๊ฒ ์ถ์ฝ๋จ์ด์ ์ ๊ตฌ์ฒด์ ์ธ ํ์ผ ์ฐธ์กฐ(
.claude/round-5-performance.md,docs/week5/)๊ฐ ์ ๊ฑฐ๋๊ณ "ํ๋ก์ ํธ ์ํค ์ฐธ์กฐ"๋ก ํตํฉ๋์์ต๋๋ค. ์ด๋ก ์ธํด ๊ฐ๋ฐ์๊ฐ ์์ธํ ์ฑ๋ฅ ๊ฐ์ ๋ด์ญ์ ์ฐพ๊ธฐ๊ฐ ์ด๋ ค์์ง ์ ์์ต๋๋ค.์ํค ํ์ด์ง์ ๋ช ํํ ๋งํฌ ๋๋ ์ต์ปค๊ฐ ์๋์ง ํ์ธํ๊ณ , ํ์์ ๋ ๊ตฌ์ฒด์ ์ธ ์ฐธ๊ณ ์ ๋ณด(์: ์ํค ํ์ด์ง ์ ๋ชฉ ๋๋ ์น์ ๋ช )๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
26-68: ํ๋์ฝ๋ฉ๋ ๋ก์ปฌ ๊ฐ๋ฐ ๊ฒฝ๋ก ์์ ๊ถ์ฅLine 30์ MySQL ๋ช ๋ น์ด๊ฐ ํน์ ๊ฐ๋ฐ์์ ๋ก์ปฌ ๊ฒฝ๋ก(
/Users/minu/Documents/project/...)๋ก ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. ๋ค๋ฅธ ๊ฐ๋ฐ์๋ CI/CD ํ๊ฒฝ์์ ์ด ๊ฐ์ด๋๋ฅผ ๋ฐ๋ฅด๊ธฐ ์ด๋ ค์ธ ์ ์์ต๋๋ค.์๋ ๊ฒฝ๋ก(
../๋๋./์ฌ์ฉ) ๋๋ ํ๊ฒฝ ๋ณ์ ๊ธฐ๋ฐ ์ ๊ทผ์ผ๋ก ์์ ํ์ฌ ์ฌ์ฌ์ฉ์ฑ์ ๋์ด๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ๊ถ์ฅ๋๋ ์์ ๋ฐฉ์
- cd /Users/minu/Documents/project/loopers/loopers-spring-java-template + # ํ๋ก์ ํธ ๋ฃจํธ ๋๋ ํ ๋ฆฌ๋ก ์ด๋ + cd $(git rev-parse --show-toplevel)๋๋ ์๋ ๊ฒฝ๋ก ์ฌ์ฉ:
- cd /Users/minu/Documents/project/loopers/loopers-spring-java-template + # ์ด README.md๊ฐ ์๋ ๋๋ ํ ๋ฆฌ์ ์์ ์์ ๋๋ ํ ๋ฆฌ๋ก ์ด๋ + cd ../../modules/redis/src/main/java/com/loopers/config/redis/RedisConfig.java (1)
71-86: Consumer ํจํด์ผ๋ก ๊น๋ํ ์ปค์คํฐ๋ง์ด์ ์ด์ ๊ตฌ์กฐ์ ๋๋ค.๋ค๋ง Line 78์ null ์ฒดํฌ๋ ํ์ฌ ๋ชจ๋ ํธ์ถ์ฒ์์ non-null Consumer๋ฅผ ์ ๊ณตํ๋ฏ๋ก ์๋ตํด๋ ์ข์ต๋๋ค.
๐ ์ ์ํ๋ ๋ฆฌํฉํ ๋ง
- if(customizer != null) customizer.accept(builder); + customizer.accept(builder);modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt (1)
5-8: ์ฐธ๊ณ : ์ด ํ์ผ์ ๋ณ๊ฒฝ์ฌํญ์ PR ๋ชฉ์ ๊ณผ ๋ฌด๊ดํด ๋ณด์ ๋๋ค.์ด ํ์ผ์ ๊ฒฐ์ API ์๋ฎฌ๋ ์ดํฐ์ด๋ฉฐ, PR์ ์ฃผ์ ๋ชฉํ(Redis ๊ธฐ๋ฐ ์ํ ๋ญํน ์์คํ )์ ์ง์ ์ ์ธ ์ฐ๊ด์ด ์์ด ๋ณด์ ๋๋ค. import ์ฌ๊ตฌ์ฑ๋ง ํฌํจ๋ ๊ฒ์ผ๋ก ๋ณด์ด๋๋ฐ, ์๋ํ์ง ์์ ๋ณ๊ฒฝ์ฌํญ์ด ํฌํจ๋์๋์ง ํ์ธํด ์ฃผ์ธ์.
์ ํ์ ์ ์: ์์ผ๋์นด๋ import ์ง์
Line 8์์
org.springframework.web.bind.annotation.*์์ผ๋์นด๋ import๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ๋ช ์์ ์ธ ๊ฐ๋ณ import๊ฐ ๋ค์๊ณผ ๊ฐ์ ์ด์ ๋ก ๊ถ์ฅ๋ฉ๋๋ค:
- ์ด๋ค ํด๋์ค๊ฐ ์ค์ ๋ก ์ฌ์ฉ๋๋์ง ๋ช ํํ๊ฒ ํ์ ๊ฐ๋ฅ
- ์ด๋ฆ ์ถฉ๋ ๊ฐ๋ฅ์ฑ ๊ฐ์
- ์์กด์ฑ ์ถ์ ์ฉ์ด
๐ ๋ช ์์ import๋ก ๋ณ๊ฒฝํ๋ ์ ์
-import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestControllermodules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt (1)
6-6: ๋ช ์์ import ์ฌ์ฉ์ ๊ณ ๋ คํด๋ณด์ธ์.ํ์ฌ
java.util.*์์ผ๋์นด๋ import๋ฅผ ์ฌ์ฉํ๊ณ ์์ง๋ง, ์ด ํ์ผ์์๋UUID๋ง ์ฌ์ฉ๋ฉ๋๋ค. ๋ช ์์ import (import java.util.UUID)๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๋์ ์์กด์ฑ์ ๋ ๋ช ํํ๊ฒ ํํํ ์ ์์ต๋๋ค.๋ค๋ง, AI ์์ฝ์ ๋ฐ๋ฅด๋ฉด ์ด๋ ๊ด๋ จ ์์ญ์ broader wildcard-import pattern์ ์ผ๋ถ์ด๋ฏ๋ก, ํ์ ์ผ๊ด๋ ์ฝ๋ ์คํ์ผ์ ๋ฐ๋ฅด๋ ๊ฒ์ด๋ผ๋ฉด ํ์ฌ ์ํ๋ฅผ ์ ์งํด๋ ๊ด์ฐฎ์ต๋๋ค.
๐ ๋ช ์์ import๋ก ๋ณ๊ฒฝํ๋ ์ ์
-import java.util.* +import java.util.UUIDmodules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt (1)
3-3: ์์ผ๋์นด๋ ์ํฌํธ ๋์ ๋ช ์์ ์ํฌํธ ๊ณ ๋ ค6๊ฐ์ ํด๋์ค๋ฅผ ๋จ์ผ ์์ผ๋์นด๋ ์ํฌํธ(
com.loopers.domain.payment.*)๋ก ๋์ฒดํ์ต๋๋ค. ์ํฌํธ ์๊ฐ ๋ง์ ๋๋ ์์ผ๋์นด๋๊ฐ ๊ฐ๊ฒฐํ์ง๋ง, ๋ช ์์ ์ํฌํธ๊ฐ ๋ค์๊ณผ ๊ฐ์ ์ด์ ์ ์ ๊ณตํฉ๋๋ค:
- ์ฌ์ฉ ์ค์ธ ํด๋์ค๋ฅผ ๋ช ํํ๊ฒ ํ์ธ ๊ฐ๋ฅ
- ๋ค์ด๋ฐ ์ถฉ๋ ๋ฐฉ์ง
- IDE์ ๋ฏธ์ฌ์ฉ ์ํฌํธ ๊ฐ์ง ์ฉ์ด
ํ์ฌ ์ฝ๋์์๋ ํฐ ๋ฌธ์ ๊ฐ ์์ง๋ง, ๋ช ์์ ์ํฌํธ๋ก ์ ์งํ๋ ๊ฒ์ ์ ํ์ ์ผ๋ก ๊ณ ๋ คํด๋ณด์ธ์.
๐ ๋ช ์์ ์ํฌํธ๋ก ๋๋๋ฆฌ๋ ์ ์
-import com.loopers.domain.payment.* +import com.loopers.domain.payment.Payment +import com.loopers.domain.payment.PaymentEvent +import com.loopers.domain.payment.PaymentEventPublisher +import com.loopers.domain.payment.PaymentRelay +import com.loopers.domain.payment.PaymentRepository +import com.loopers.domain.payment.TransactionKeyGeneratorapps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.java (1)
259-265: ์ฌ์ฉ๋์ง ์๋ ๋ฉ์๋ ์ ๊ฑฐ ๊ถ์ฅ
createSuccessResponse()๋ฉ์๋๊ฐ ์ ์๋์ด ์์ง๋ง ํ ์คํธ ํด๋์ค ๋ด์์ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ์ฑ๊ณต ์๋ต์thenAnswer์์ ์ธ๋ผ์ธ์ผ๋ก ์์ฑํ๊ณ ์์ผ๋ฏ๋ก ์ด ๋ฉ์๋๋ ์ ๊ฑฐํด๋ ๋ฉ๋๋ค.๐ ์ ๊ฑฐ ์ ์
- private PgPaymentResponse createSuccessResponse() { - return new PgPaymentResponse( - "TX-" + System.currentTimeMillis(), - "PENDING", - null - ); - } - private FeignException createFeignException() {apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java (1)
3-4: ๋ฏธ์ฌ์ฉ import ์ ๊ฑฐ ํ์Line 4์
Optionalimport๊ฐ ์๋ก ์ถ๊ฐ๋ ์ฝ๋์์ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ๊ธฐ์กด ์ฝ๋์์๋ ์ฌ์ฉ๋์ง ์๋๋ค๋ฉด ์ ๊ฑฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์ํ๋ ์์
import java.time.Duration; -import java.util.Optional; import lombok.RequiredArgsConstructor;apps/commerce-streamer/src/main/resources/application.yml (1)
28-32: ํ ํฝ ์ค์ ์ ๊ธฐ๋ฅ์ ์ผ๋ก ์ ์ ํ๋ ์ผ๊ด๋ ๋ฌธ์ํ๊ฐ ๋ถ์กฑํฉ๋๋ค.
kafka.yml์๋ Spring Kafka ํด๋ผ์ด์ธํธ ์ค์ ๋ง ์๊ณ , ํ ํฝ๋ณ ํํฐ์ ์, retention, replication factor ๊ฐ์ ์์ธ ์ค์ ์ด ์์ต๋๋ค. ๋ํapplication.yml์ ํ ํฝ ์ ์์๋ ์ด๋ค ์ด๋ฒคํธ๊ฐ ์ด๋ ํ ํฝ์ผ๋ก ๋ผ์ฐํ ๋๋์ง ์ฃผ์์ด ์์ต๋๋ค.๊ถ์ฅ์ฌํญ:
# Kafka Topics์ฃผ์ ์๋ ๊ฐ ํ ํฝ์ ์ฉ๋ ๊ฐ๋จํ ๋ช ์ (์:# catalog-events: ์ข์์, ์กฐํ์ ๋ฑ ์นดํ๋ก๊ทธ ๊ด๋ จ ์ด๋ฒคํธ)kafka.yml๋๋ ๋ณ๋ ํ์ผ์ ํ ํฝ์ ํํฐ์ , retention, replication factor ์ค์ ๋ฌธ์ํ (ํ์ฌauto.create.topics.enable: false์ด๋ฏ๋ก ํ ํฝ ์์ฑ ๋ฐฉ์ ํ์ธ)modules/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt (1)
8-8: ํน์ import ์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.์์ผ๋์นด๋ import(
import java.util.*) ๋์ ํ์ํ ํด๋์ค๋ง ๋ช ์์ ์ผ๋ก importํ๋ ๊ฒ์ด Kotlin ์ฝ๋ฉ ์คํ์ผ ๊ฐ์ด๋๋ผ์ธ์ ๋ถํฉํฉ๋๋ค. ํ์ฌTimeZone๋ง ์ฌ์ฉ๋๋ฏ๋ก ๊ตฌ์ฒด์ ์ธ import๊ฐ ๋ ๋ช ํํฉ๋๋ค.๐ ์ ์ํ๋ ์์
-import java.util.* +import java.util.TimeZonemodules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.java (1)
210-237: Helper ๋ฉ์๋์ ์ค๋ณต์ด ์ฌ๋ฌ ํ ์คํธ ํด๋์ค์์ ๋ฐ๊ฒฌ๋ฉ๋๋ค.
produceMessages(int)๋ฐcreateConsumerProps(String)๋ฉ์๋๊ฐ Experiment1, Experiment2, Experiment3, Experiment4 ํ ์คํธ ํด๋์ค ๋ชจ๋์์ ๋์ผํ๊ฒ ๋ฐ๋ณต๋๊ณ ์์ต๋๋ค.ํ์ต ํ ์คํธ์ ๋ ๋ฆฝ์ฑ์ ์ํด ์๋์ ์ผ๋ก ์ค๋ณต์ ์ ์งํ๋ ๊ฒ๋ ์ ํ์ง์ด์ง๋ง, ๊ณตํต Helper ํด๋์ค๋ Base ํ ์คํธ ํด๋์ค๋ก ์ถ์ถํ๋ฉด ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ฉ๋๋ค.
์ ํ์ ๋ฆฌํฉํ ๋ง: ๊ณตํต Helper ํด๋์ค ์ถ์ถ
modules/kafka/src/test/java/com/loopers/kafka/learning/KafkaTestHelper.java์์ฑ:package com.loopers.kafka.learning; import java.util.Properties; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class KafkaTestHelper { private static final Logger log = LoggerFactory.getLogger(KafkaTestHelper.class); private static final String BOOTSTRAP_SERVERS = "localhost:9092"; public static void produceMessages(String topic, int count) { Properties props = new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) { for (int i = 0; i < count; i++) { ProducerRecord<String, String> record = new ProducerRecord<>( topic, "key-" + i, "Message " + i ); producer.send(record); } producer.flush(); log.info("๋ฉ์์ง {}๊ฐ ์ ์ก ์๋ฃ", count); } } public static Properties createConsumerProps(String groupId) { Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); return props; } }๊ทธ๋ฐ ๋ค์ ๊ฐ ํ ์คํธ ํด๋์ค์์
KafkaTestHelper.produceMessages(TOPIC, count)๋ฐKafkaTestHelper.createConsumerProps(groupId)๋ก ํธ์ถํ ์ ์์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingScore.java (2)
20-28: ๋ถํ์ํ ๊ณฑ์ ์ฐ์ฐ์ ์ ๊ฑฐํ์ธ์.
viewScore()์likeScore()๋ฉ์๋์์* 1์ฐ์ฐ์ ๋ถํ์ํฉ๋๋ค. ๊ฐ๋ ์ฑ์ ์ํด ์ง์ ์์๋ฅผ ๋ฐํํ๋ ๊ฒ์ด ๋ ๋ช ํํฉ๋๋ค.๐ ์ ์ํ๋ ๋ฆฌํฉํ ๋ง
public static double viewScore() { - return WEIGHT_VIEW * 1; + return WEIGHT_VIEW; } public static double likeScore() { - return WEIGHT_LIKE * 1; + return WEIGHT_LIKE; }
7-7: ์ ํธ๋ฆฌํฐ ํด๋์ค๋ฅผ final๋ก ์ ์ธํ๊ณ private ์์ฑ์๋ฅผ ์ถ๊ฐํ์ธ์.๋ชจ๋ ๋ฉ์๋๊ฐ static์ธ ์ ํธ๋ฆฌํฐ ํด๋์ค๋ ์ธ์คํด์คํ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด
final๋ก ์ ์ธํ๊ณ private ์์ฑ์๋ฅผ ๊ฐ์ ธ์ผ ํฉ๋๋ค.๐ ์ ์ํ๋ ์์
-public class RankingScore { +public final class RankingScore { + + private RankingScore() { + throw new UnsupportedOperationException("Utility class"); + } // ๊ฐ์ค์น ์ค์ private static final double WEIGHT_VIEW = 0.1; // ์กฐํapps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java (2)
25-28: @transactional ๋กค๋ฐฑ๊ณผ ์ค๋ณต๋ ์ ๋ฆฌ ๋ก์ง๊ฐ ํ ์คํธ ๋ฉ์๋์
@Transactional์ด ์ ์ฉ๋์ด ์๋์ผ๋ก ๋กค๋ฐฑ๋๋ฏ๋ก,deleteAll()์ ๋ถํ์ํ ์ ์์ต๋๋ค. ๋ค๋ง, ํ ์คํธ ๊ฐ ๊ฒฉ๋ฆฌ๋ฅผ ๋ช ์์ ์ผ๋ก ๋ณด์ฅํ๋ ค๋ ์๋๋ผ๋ฉด ์ ์งํด๋ ๋ฌด๋ฐฉํฉ๋๋ค.
30-69: ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง ํ์ฅ ๊ถ์ฅํ์ฌ ๊ธฐ๋ณธ ํ๋ฆ๋ง ๊ฒ์ฆํ๊ณ ์์ต๋๋ค. ๋ค์ ์๋๋ฆฌ์ค ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํด๋ณด์ธ์:
eventId๊ฐ null์ด๊ฑฐ๋ ๋น ๋ฌธ์์ด์ธ ๊ฒฝ์ฐ- ๋์์ฑ ์๋๋ฆฌ์ค (๋์ผ eventId์ ๋ํ ๋์ save ํธ์ถ)
- ํธ๋์ญ์ ์คํจ ์๋๋ฆฌ์ค
docker/init-db.sql (2)
19-19: soft delete ๋ฏธ์ฌ์ฉ ์ deleted_at ์ ๊ฑฐ ๊ถ์ฅ
deleted_at์ปฌ๋ผ์ด ์ ์ธ๋์ด ์์ง๋ง, ๊ด๋ จ ์ธ๋ฑ์ค๋ ์ญ์ ๋ก์ง์ด ๋ณด์ด์ง ์์ต๋๋ค. Soft delete๋ฅผ ์ค์ ๋ก ์ฌ์ฉํ์ง ์๋๋ค๋ฉด ์ปฌ๋ผ ์ ๊ฑฐ๋ฅผ ๊ณ ๋ คํ์ธ์.Also applies to: 55-55
32-32: ํตํ/๋จ์ ์ ๋ณด ๋๋ฝ
sales_amount DECIMAL(15,2)๋ ๊ธ์ก์ ์ ์ฅํ์ง๋ง ํตํ ๋จ์๊ฐ ๋ช ์๋์ง ์์์ต๋๋ค. ๋ค์ค ํตํ๋ฅผ ์ง์ํ ๊ณํ์ด ์๋ค๋ฉดcurrency์ปฌ๋ผ ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํ์ธ์.apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (2)
46-47: ๋ก๊ทธ ๋ฉ์์ง์ ์ด๋ชจ์ง ์ ๊ฑฐ ๊ถ์ฅ๋ก๊ทธ ๋ฉ์์ง์ ์ด๋ชจ์ง
โ ๏ธ๊ฐ ํฌํจ๋์ด ์์ต๋๋ค. ์ผ๋ถ ๋ก๊ทธ ์์ง ์์คํ ์ด๋ ์ธ์ฝ๋ฉ ํ๊ฒฝ์์ ๋ฌธ์ ๋ฅผ ์ผ์ผํฌ ์ ์์ผ๋ฏ๋ก, ํ ์คํธ๋ก ๋์ฒดํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ์ ์ ์์
- log.error("โ ๏ธ DLQ์ ๋ฉ์์ง ์ ์ฅ - topic: {}, eventId: {}, retryCount: {}, error: {}", + log.error("[DLQ] ๋ฉ์์ง ์ ์ฅ - topic: {}, eventId: {}, retryCount: {}, error: {}", originalTopic, eventId, retryCount, errorMessage);
32-42: payload/errorMessage ํฌ๊ธฐ ์ ํ ๋๋ฝDB ์คํค๋ง์์
payload์error_message๋ TEXT ํ์ ์ด์ง๋ง, ๋งค์ฐ ํฐ ํ์ด๋ก๋๋ ์๋ฌ ๋ฉ์์ง๊ฐ ์ ๋ฌ๋ ๊ฒฝ์ฐ ์ ์ฅ ์คํจ ๊ฐ๋ฅ์ฑ์ด ์์ต๋๋ค. ํฌ๊ธฐ ์ ํ์ ์ถ๊ฐํ๊ฑฐ๋ truncate ๋ก์ง์ ๊ณ ๋ คํ์ธ์.apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java (1)
102-125: ์ฌ์ฉ๋์ง ์๋ ์ฝ๋ ์ ๊ฑฐ ๊ณ ๋ ค
deserializeEvent,getEventClass,getPackageName๋ฉ์๋๊ฐ ์ด ํด๋์ค ๋ด์์ ํธ์ถ๋์ง ์์ต๋๋ค. Kafka ๋ฐํ ๋ฐฉ์์ผ๋ก ๋ฆฌํฉํ ๋ง๋๋ฉด์ ๋ ์ด์ ํ์ํ์ง ์์ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค.apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java (1)
46-60: ์ฃ์ง ์ผ์ด์ค ํ ์คํธ ์ถ๊ฐ ๊ถ์ฅ
decrementLikeCount๊ฐ ์ข์์ ์๊ฐ 0์ผ ๋ ํธ์ถ๋๋ฉด ์ด๋ป๊ฒ ๋๋์ง ํ ์คํธ๊ฐ ์์ต๋๋ค. ์์ ๋ฐฉ์ง ๋ก์ง์ด ์๋น์ค์ ์๋์ง ํ์ธํ๊ณ , ํด๋น ๋์์ ๊ฒ์ฆํ๋ ํ ์คํธ๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์: ์ฃ์ง ์ผ์ด์ค ํ ์คํธ ์ถ๊ฐ
@Test void ์ข์์_์๊ฐ_0์ผ๋_๊ฐ์์_์์๊ฐ_๋์ง_์๋๋ค() { // Given Long productId = 5L; productMetricsService.incrementLikeCount(productId); // When productMetricsService.decrementLikeCount(productId); productMetricsService.decrementLikeCount(productId); // 0 ์ดํ๋ก ๊ฐ์ ์๋ // Then ProductMetrics metrics = productMetricsRepository.findByProductId(productId) .orElseThrow(); assertThat(metrics.getLikeCount()).isGreaterThanOrEqualTo(0); }apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java (1)
18-29:@Valueํ๋์@RequiredArgsConstructorํผ์ฉ ์ฃผ์
@RequiredArgsConstructor๋finalํ๋์ ๋ํด์๋ง ์์ฑ์๋ฅผ ์์ฑํ์ง๋ง,@Value์ด๋ ธํ ์ด์ ํ๋(catalogEventsTopic,orderEventsTopic)๋final์ด ์๋๋๋ค. Spring์ด ๋น ์์ฑ ํ@Valueํ๋๋ฅผ ์ฃผ์ ํ๋ฏ๋ก ํ์ฌ ์ฝ๋๋ ๋์ํ์ง๋ง, ๋ช ์์ ์์ฑ์ ์ฃผ์ ์ด๋@ConfigurationProperties๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ ๋ช ํํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
73-76: ๋ ์ง ํฌ๋งทํฐ ์ค๋ณต ์ ๊ฑฐ ๊ณ ๋ ค
DateTimeFormatter.ofPattern("yyyyMMdd")๊ฐRankingService์๋ ์กด์ฌํฉ๋๋ค. ๊ณตํต ์์๋RankingKey.dailyToday()๋ฉ์๋๋ฅผ ํ์ฉํ์ฌ ์ค๋ณต์ ์ค์ผ ์ ์์ต๋๋ค.๐ ์ ์: RankingKey ํ์ฉ
public List<RankingProductInfo> getTodayRanking(int page, int size) { - String today = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")); + String today = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")); + // ๋๋ RankingKey์์ ๋ ์ง ๋ฌธ์์ด ์ถ์ถ ๋ก์ง์ ๊ณต์ return getDailyRanking(today, page, size); }๋ ๋์ ๋ฐฉ๋ฒ์
RankingKeyํด๋์ค์ ๋ ์ง ํฌ๋งทํ ์ ํธ๋ฆฌํฐ๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ ๋๋ค.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingApi.java (1)
36-45: ํ์ด์ง ํ๋ผ๋ฏธํฐ ์ ํจ์ฑ ๊ฒ์ฆ ๊ถ์ฅ
page์size์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฆ์ด ์์ต๋๋ค. ์์ ๊ฐ์ด๋ ๊ณผ๋ํ๊ฒ ํฐsize๊ฐ ์ ๋ ฅ๋ ๊ฒฝ์ฐ Redis์ ๋ถํ๊ฐ ๋ฐ์ํ๊ฑฐ๋ ์๊ธฐ์น ์์ ๋์์ด ๋ฐ์ํ ์ ์์ต๋๋ค.๐ ์ ์: ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ ์ถ๊ฐ
@GetMapping public ResponseEntity<RankingResponse> getRankings( @Parameter(description = "์กฐํ ๋ ์ง (yyyyMMdd), ๋ฏธ์ ๋ ฅ ์ ์ค๋", example = "20250123") @RequestParam(required = false) String date, @Parameter(description = "ํ์ด์ง ๋ฒํธ (1๋ถํฐ ์์)", example = "1") - @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "1") @Min(1) int page, @Parameter(description = "ํ์ด์ง ํฌ๊ธฐ", example = "20") - @RequestParam(defaultValue = "20") int size + @RequestParam(defaultValue = "20") @Min(1) @Max(100) int size ) {
@Validated๋ฅผ ์ปจํธ๋กค๋ฌ ํด๋์ค์ ์ถ๊ฐํ๊ณjakarta.validation.constraints.Min,Max๋ฅผ importํ์ธ์.apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingKey.java (1)
10-16: ์ ํธ๋ฆฌํฐ ํด๋์ค ํจํด ๊ฐ์ ๊ถ์ฅ๋ชจ๋ ๋ฉ์๋๊ฐ static์ด๋ฏ๋ก ์ธ์คํด์คํ ๋ฐฉ์ง๋ฅผ ์ํด private ์์ฑ์๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๐ ์ ์: private ์์ฑ์ ์ถ๊ฐ
public class RankingKey { + + private RankingKey() { + // Utility class + } private static final String DAILY_PREFIX = "ranking:all:";apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (1)
125-126: ์ค๋ณต JSON ํ์ฑ
processOrderCreated์์ payload๋ฅผ ๋ค์ ํ์ฑํ๊ณ ์์ต๋๋ค.processEvent์์ ์ด๋ฏธ ํ์ฑ๋eventData๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌํ๋ฉด ๋ถํ์ํ ํ์ฑ์ ์ค์ผ ์ ์์ต๋๋ค.๐ ๋ฆฌํฉํ ๋ง ์ ์
- private void processOrderCreated(String payload) throws Exception { - Map<String, Object> eventData = objectMapper.readValue(payload, Map.class); + private void processOrderCreated(Map<String, Object> eventData) throws Exception {ํธ์ถ๋ถ:
if ("OrderCreatedEvent".equals(eventType)) { - processOrderCreated(payload); + processOrderCreated(eventData);apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (1)
41-48: Integer ์นด์ดํฐ ์ค๋ฒํ๋ก์ฐ ๊ฐ๋ฅ์ฑ
likeCount,viewCount,orderCount๊ฐIntegerํ์ ์ ๋๋ค. ์ธ๊ธฐ ์ํ์ ๊ฒฝ์ฐ ์กฐํ์๊ฐInteger.MAX_VALUE(์ฝ 21์ต)๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค. ํธ๋ํฝ ๊ท๋ชจ์ ๋ฐ๋ผLongํ์ ์ฌ์ฉ์ ๊ณ ๋ คํ์ธ์.apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (2)
35-35: ์ฌ์ฉ๋์ง ์๋ ์์MAX_RETRY
MAX_RETRY์์๊ฐ ์ ์๋์ด ์์ง๋ง ์ฝ๋์์ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ํฅํ ์ฌ์๋ ๋ก์ง์ ๊ตฌํํ ๊ณํ์ด ์๋๋ผ๋ฉด ์ ๊ฑฐํ์ธ์.
24-172:OrderEventConsumer์ ์ฝ๋ ์ค๋ณต
CatalogEventConsumer์OrderEventConsumer๊ฐ ๊ฑฐ์ ๋์ผํ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค. ๊ณตํต ๋ก์ง(๋ฐฐ์น ์ฒ๋ฆฌ, Inbox ์ฒดํฌ, DLQ ์ ์ก)์ ์ถ์ ๋ฒ ์ด์ค ํด๋์ค๋ ๊ณตํต ์ ํธ๋ฆฌํฐ๋ก ์ถ์ถํ๋ฉด ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ฉ๋๋ค.apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.java (1)
51-61:DeadLetterQueueService๊ฐ null๋ก ์ค์ ๋จ
DeadLetterQueueService๋ฅผ null๋ก ์ ๋ฌํ๋ฉด, ์์ธ ๋ฐ์ ์sendToDLQ์์NullPointerException์ด ๋ฐ์ํ ์ ์์ต๋๋ค. ๋จ์ ํ ์คํธ์์ ์์ธ ์ฒ๋ฆฌ ๊ฒฝ๋ก๋ฅผ ํ ์คํธํ๋ ค๋ฉด mock ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๊ฑฐ๋, ์์ธ ํ ์คํธ ์ผ์ด์ค๋ฅผ ๋ช ์์ ์ผ๋ก ๋ถ๋ฆฌํ์ธ์.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (3)
24-26: ์ฌ์ฉ๋์ง ์๋ ์์HOURLY_PREFIX
HOURLY_PREFIX๊ฐ ์ ์๋์ด ์์ง๋ง ์ฝ๋์์ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ํฅํ ์๊ฐ๋ณ ๋ญํน ์กฐํ ๊ธฐ๋ฅ์ ์ถ๊ฐํ ๊ณํ์ด๋ผ๋ฉด ์ ์งํ๊ณ , ์๋๋ผ๋ฉด ์ ๊ฑฐํ์ธ์.
44-47: ์์คํ ๊ธฐ๋ณธ ํ์์กด ์ฌ์ฉ
LocalDate.now()๋ ์์คํ ๊ธฐ๋ณธ ํ์์กด์ ์ฌ์ฉํฉ๋๋ค. ๋ถ์ฐ ํ๊ฒฝ์ด๋ ๋ค๋ฅธ ํ์์กด์ ์๋ฒ์์ ์ผ๊ด๋์ง ์์ ๊ฒฐ๊ณผ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ๋ช ์์ ํ์์กด ์ง์ ์ ๊ณ ๋ คํ์ธ์.LocalDate.now(ZoneId.of("Asia/Seoul"))Also applies to: 67-70
102-104:Long.parseLong์์ธ ์ฒ๋ฆฌ ๋๋ฝRedis ZSET์ ์ ์ฅ๋ ๊ฐ์ด ์ซ์๊ฐ ์๋ ๊ฒฝ์ฐ
NumberFormatException์ด ๋ฐ์ํฉ๋๋ค. ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ด ๋ณด์ฅ๋๋ค๋ฉด ํ์ฌ ๊ตฌํ๋ ๊ด์ฐฎ์ง๋ง, ๋ฐฉ์ด์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ค๋ฉด try-catch๋ฅผ ์ถ๊ฐํ์ธ์.
๐ Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (81)
apps/commerce-api/build.gradle.ktsapps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.javaapps/commerce-api/src/main/java/com/loopers/application/dataplatform/DataPlatformEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.javaapps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.javaapps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.javaapps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductService.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.javaapps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.javaapps/commerce-api/src/main/java/com/loopers/config/CacheConfig.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClientConfig.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgPaymentRequest.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentApi.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/RankingApi.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingResponse.javaapps/commerce-api/src/main/resources/application.ymlapps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.javaapps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceFallbackTest.javaapps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceRetryTest.javaapps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.javaapps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.javaapps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.javaapps/commerce-api/src/test/java/com/loopers/support/error/CoreExceptionTest.javaapps/commerce-streamer/build.gradle.ktsapps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.javaapps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.javaapps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.javaapps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.javaapps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregator.javaapps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingScheduler.javaapps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.javaapps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueueRepository.javaapps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.javaapps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInboxRepository.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingKey.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingScore.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueJpaRepository.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxJpaRepository.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.javaapps/commerce-streamer/src/main/resources/application.ymlapps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.javaapps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.javadocker/init-db.sqlmodules/jpa/src/main/java/com/loopers/domain/BaseEntity.javamodules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.javamodules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.javamodules/kafka/src/main/resources/kafka.ymlmodules/kafka/src/test/java/com/loopers/kafka/learning/Experiment1_AutoCommitTest.javamodules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.javamodules/kafka/src/test/java/com/loopers/kafka/learning/Experiment3_PollIntervalTest.javamodules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.javamodules/kafka/src/test/java/com/loopers/kafka/learning/README.mdmodules/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.ktmodules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.ktmodules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.ktmodules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.ktmodules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.ktmodules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.ktmodules/redis/src/main/java/com/loopers/config/redis/RedisConfig.javamodules/redis/src/main/java/com/loopers/config/redis/RedisProperties.javaperformance-test/README.mdperformance-test/k6/product-load-test-fixed.jsperformance-test/k6/product-load-test.js
๐ค Files with no reviewable changes (6)
- apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceFallbackTest.java
- apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentApi.java
- modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt
- apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceRetryTest.java
- apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgPaymentRequest.java
- apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
๐งฐ Additional context used
๐ง Learnings (17)
๐ Common learnings
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.
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:50.009Z
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.
๐ 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-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.javamodules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.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/application/product/ProductService.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.javaapps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.javaapps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.javaapps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.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/application/product/ProductService.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java
๐ Learning: 2025-12-19T20:59:50.009Z
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:50.009Z
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/CatalogEventConsumer.javadocker/init-db.sqlapps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.javaapps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.javaapps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInboxRepository.javaapps/commerce-streamer/src/main/resources/application.ymlapps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.javamodules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.ktapps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.javaapps/commerce-api/src/main/resources/application.ymlapps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.javaapps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.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/CatalogEventConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java
๐ Learning: 2025-12-19T21:30:08.018Z
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:08.018Z
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/CatalogEventConsumer.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.javaapps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java
๐ Learning: 2025-12-19T23:39:16.424Z
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:16.424Z
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/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.javadocker/init-db.sqlapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.javamodules/kafka/src/main/resources/kafka.ymlapps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.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/main/java/com/loopers/application/inbox/EventInboxService.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.javaapps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.javaapps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.javaapps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.javaapps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.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-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/application/dataplatform/DataPlatformEventHandler.javaapps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.javamodules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.ktapps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.javamodules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.ktapps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.javaapps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.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/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.javamodules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.ktmodules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt
๐ Learning: 2025-11-20T13:22:51.519Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 98
File: core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java:15-62
Timestamp: 2025-11-20T13:22:51.519Z
Learning: In the order domain coupon implementation, DefaultCoupon serves as an internal helper/DTO class to carry AbstractCoupon's base fields during database reconstruction. It is used as an intermediate object when converting CouponEntity to specific coupon types (RateDiscountCoupon or AmountDiscountCoupon). The actual discount information (rate/amount) is stored in separate tables (rate_discount_coupon and amount_discount_coupon) and fetched via RateDiscountCouponJpaRepository and AmountDiscountCouponJpaRepository. DefaultCoupon is never returned to callers directly.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.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/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java
๐ Learning: 2025-12-20T00:38:25.765Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/outbox/OutboxRelayService.kt:46-52
Timestamp: 2025-12-20T00:38:25.765Z
Learning: In OutboxRelayService in apps/commerce-api (Loopers-dev-lab/loopers-spring-kotlin-template), the scheduler uses fixedDelay which guarantees serial execution within a single JVM instance, so Volatile is sufficient for backoff state variables (consecutiveFailures, nextAllowedTime) and AtomicInteger/AtomicLong are not necessary for thread safety.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.javaapps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.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-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueJpaRepository.java
๐ Learning: 2025-12-22T16:33:42.960Z
Learnt from: jikimee64
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 71
File: apps/commerce-streamer/src/main/kotlin/com/loopers/application/ranking/RankingFacade.kt:167-173
Timestamp: 2025-12-22T16:33:42.960Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository, using system default timezone (ZonedDateTime.now() without explicit ZoneId) in date key calculations is an intentional design choice. The deployment ensures all instances share the same timezone configuration.
Applied to files:
modules/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.ktmodules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.ktapps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.javaapps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java
๐ Learning: 2025-12-07T15:47:18.277Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 52
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/payment/PgFeignConfig.kt:0-0
Timestamp: 2025-12-07T15:47:18.277Z
Learning: In Spring Cloud OpenFeign, configuration classes specified in FeignClient(configuration = ...) do NOT need the Configuration annotation. Spring Cloud OpenFeign creates a separate ApplicationContext for each named client and directly instantiates Bean methods from the configuration class. Adding Configuration would cause the class to be registered globally via ComponentScan, making the configuration apply to ALL Feign clients instead of just the specific client, which is usually undesirable.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClientConfig.javaapps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
๐งฌ Code graph analysis (19)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (1)
Slf4j(17-129)apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.java (1)
Service(19-100)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java (1)
ExampleV1Dto(5-15)
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregator.java (1)
modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt (1)
log(18-115)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java (1)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
Slf4j(31-169)
apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java (3)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1)
Slf4j(24-172)apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (1)
Slf4j(25-200)apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (1)
Slf4j(14-49)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java (2)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java (1)
Repository(11-21)apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
Repository(12-32)
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.java (2)
apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java (1)
SpringBootTest(14-70)apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java (1)
SpringBootTest(16-93)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
ProductV1Dto(10-90)
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java (1)
ProductMetricsRepository(9-15)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingApi.java (2)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
lombok(81-92)apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (1)
lombok(122-128)
apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java (2)
apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java (1)
SpringBootTest(14-70)apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.java (1)
SpringBootTest(23-51)
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment1_AutoCommitTest.java (4)
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.java (1)
DisplayName(30-238)modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment3_PollIntervalTest.java (1)
DisplayName(32-241)modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java (1)
DisplayName(30-357)modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt (1)
log(18-115)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java (3)
apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsJpaRepository.java (1)
ProductMetricsJpaRepository(8-10)apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java (1)
RequiredArgsConstructor(13-32)apps/commerce-api/src/main/java/com/loopers/domain/metrics/product/ProductMetricsRepository.java (1)
ProductMetricsRepository(9-15)
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (4)
apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java (1)
Entity(9-44)apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java (1)
Getter(18-64)apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java (1)
Getter(18-55)modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt (1)
name(8-81)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (3)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java (1)
Repository(11-21)apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java (1)
Repository(11-31)apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/product/ProductMetricsRepositoryImpl.java (1)
RequiredArgsConstructor(13-32)
apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java (4)
apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java (1)
Entity(9-44)apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java (1)
Getter(18-64)apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (1)
Getter(23-115)modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt (1)
name(8-81)
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingScheduler.java (2)
apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingKey.java (1)
RankingKey(10-71)apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingScore.java (1)
RankingScore(7-65)
apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java (1)
apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java (1)
Getter(18-55)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java (4)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (2)
lombok(81-92)Slf4j(18-93)apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
RequiredArgsConstructor(22-108)apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregator.java (1)
Slf4j(16-131)apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1)
Slf4j(24-172)
| private String getTopicByAggregateType(String aggregateType) { | ||
| return switch (aggregateType.toUpperCase()) { | ||
| case "ORDER", "PAYMENT" -> orderEventsTopic; | ||
| case "PRODUCT", "LIKE" -> catalogEventsTopic; | ||
| default -> throw new IllegalArgumentException("์ ์ ์๋ AggregateType: " + aggregateType); | ||
| }; | ||
| } |
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.
Null ์ฒดํฌ ๋๋ฝ
aggregateType์ด null์ธ ๊ฒฝ์ฐ toUpperCase() ํธ์ถ ์ NullPointerException์ด ๋ฐ์ํฉ๋๋ค. ๋ฐฉ์ด์ ์ผ๋ก null ์ฒดํฌ๋ฅผ ์ถ๊ฐํ๊ฑฐ๋, EventOutbox์์ aggregateType์ด ํญ์ non-null์์ ๋ณด์ฅํ๋์ง ํ์ธํ์ธ์.
๐ ์ ์: null ์ฒดํฌ ์ถ๊ฐ
private String getTopicByAggregateType(String aggregateType) {
+ if (aggregateType == null) {
+ throw new IllegalArgumentException("AggregateType์ null์ผ ์ ์์ต๋๋ค");
+ }
return switch (aggregateType.toUpperCase()) {๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java
around lines 63-69, the method calls aggregateType.toUpperCase() without
null-checking which causes NullPointerException when aggregateType is null; add
a defensive null check at the top (e.g., if (aggregateType == null) throw new
IllegalArgumentException("aggregateType must not be null") or use
Objects.requireNonNull with a clear message) and then proceed to use
aggregateType.toUpperCase(Locale.ROOT) (or toUpperCase()) for the switch; ensure
the exception message is descriptive so callers know the contract.
| // ์ฌ๊ณ ์์ง ์ ์บ์ ๊ฐฑ์ | ||
| if (product.getStock() == 0) { | ||
| log.info("์ฌ๊ณ ์์ง - ์บ์ ๊ฐฑ์ - productId: {}", product.getId()); | ||
| productCacheService.evictCache(product.getId()); | ||
| } |
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.
ํธ๋์ญ์ ์ปค๋ฐ ์ ์บ์ ์ญ์ ๋ก ์ธํ ๋ฐ์ดํฐ ์ ํฉ์ฑ ๋ฌธ์
์บ์ ์ญ์ ๊ฐ ํธ๋์ญ์ ์ปค๋ฐ ์ ์ ๋๊ธฐ์ ์ผ๋ก ์คํ๋์ด ๋ค์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค:
- ๋กค๋ฐฑ ์ ๋ถ์ผ์น: ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋์ด๋ ์บ์๋ ์ด๋ฏธ ์ญ์ ๋์ด ๋ค์ ์กฐํ ์ DB์์ ์๋ชป๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
- ์ฑ๋ฅ ์ํฅ: ๋๊ธฐ ์บ์ ์ญ์ ๊ฐ ํธ๋์ญ์ ์๊ฐ์ ๋๋ ค ๋ฝ ํ๋ฉ ์๊ฐ์ด ์ฆ๊ฐํฉ๋๋ค.
๊ถ์ฅ ํด๊ฒฐ ๋ฐฉ์:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)๋ฅผ ์ฌ์ฉํ์ฌ ํธ๋์ญ์ ์ปค๋ฐ ํ ์บ์๋ฅผ ์ญ์ - ๋๋ ๋น๋๊ธฐ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ์ฌ ์บ์ ์ญ์ ์ฒ๋ฆฌ
๐ ์ ์ํ๋ ์์ (์ด๋ฒคํธ ๊ธฐ๋ฐ ์ ๊ทผ)
๋๋ฉ์ธ ์ด๋ฒคํธ๋ฅผ ์ถ๊ฐํ๊ณ ์ด๋ฒคํธ ๋ฆฌ์ค๋์์ ์บ์ ์ญ์ :
// ์ฌ๊ณ ์์ง ์ ์ด๋ฒคํธ ๋ฐํ
if (product.getStock() == 0) {
log.info("์ฌ๊ณ ์์ง - productId: {}", product.getId());
// ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐํ (์: ProductStockDepletedEvent)
}๋ณ๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ํด๋์ค:
@Component
public class ProductCacheEventListener {
private final ProductCacheService productCacheService;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleStockDepleted(ProductStockDepletedEvent event) {
productCacheService.evictCache(event.getProductId());
}
}| try { | ||
| // ์ด๋ฒคํธ ํ์ ์ ๋ฐ๋ผ ์ค์ ์ด๋ฒคํธ ๊ฐ์ฒด๋ก ๋ณํ | ||
| Object event = deserializeEvent(outbox); | ||
|
|
||
| // Spring Event ๋ฐํ | ||
| eventPublisher.publishEvent(event); | ||
|
|
||
| // ๋ฐํ ์ฑ๊ณต ํ์ | ||
| outbox.markAsPublished(); | ||
| outboxRepository.save(outbox); | ||
|
|
||
| log.info("Outbox ์ด๋ฒคํธ ๋ฐํ ์๋ฃ - id: {}, type: {}", | ||
| outbox.getId(), outbox.getEventType()); | ||
| // Kafka๋ก ๋ฐํ | ||
| kafkaProducer.publish(outbox) | ||
| .thenAccept(result -> { | ||
| // ๋ฐํ ์ฑ๊ณต ์ Outbox ์ํ ์ ๋ฐ์ดํธ | ||
| outbox.markAsPublished(); | ||
| outboxRepository.save(outbox); | ||
|
|
||
| log.info("Outbox โ Kafka ๋ฐํ ์๋ฃ - outboxId: {}, eventType: {}, offset: {}", | ||
| outbox.getId(), | ||
| outbox.getEventType(), | ||
| result.getRecordMetadata().offset()); | ||
| }) | ||
| .exceptionally(ex -> { | ||
| // ๋ฐํ ์คํจ ์ Outbox ์ํ ์ ๋ฐ์ดํธ | ||
| outbox.markAsFailed(ex.getMessage()); | ||
| outboxRepository.save(outbox); | ||
|
|
||
| log.error("Outbox โ Kafka ๋ฐํ ์คํจ - outboxId: {}, error: {}", | ||
| outbox.getId(), ex.getMessage(), ex); | ||
|
|
||
| return null; | ||
| }) | ||
| .join(); // ๋๊ธฐ ๋๊ธฐ (ํธ๋์ญ์ ๋ด์์ ์๋ฃ ๋ณด์ฅ) |
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.
ํธ๋์ญ์ ๋ด ๋น๋๊ธฐ ์ฝ๋ฐฑ ์ฌ์ฉ ์ ์ฃผ์ ํ์
thenAccept์ exceptionally ์ฝ๋ฐฑ์์ outboxRepository.save(outbox)๋ฅผ ํธ์ถํ๊ณ ์์ต๋๋ค. join()์ด ๋๊ธฐ ๋๊ธฐํ๋ฏ๋ก ์ฝ๋ฐฑ์ join() ๋ฐํ ์ ์ ์คํ๋์ง๋ง, ์ฝ๋ฐฑ์ด Kafka producer ์ค๋ ๋์์ ์คํ๋ ์ ์์ด ํ์ฌ ํธ๋์ญ์
์ปจํ
์คํธ๊ฐ ์ ํ๋์ง ์์ ์ ์์ต๋๋ค.
๋ํ, exceptionally์์ ์์ธ๋ฅผ ์ผํค๊ณ null์ ๋ฐํํ๋ฏ๋ก ์ธ๋ถ catch ๋ธ๋ก(lines 91-99)์์ ์ค๋ณต์ผ๋ก ์ํ๋ฅผ ์ ์ฅํ๊ณ ์์ธ๋ฅผ ๋ค์ ๋์ง๋ ๋ก์ง์ด ์คํ๋์ง ์์ต๋๋ค.
๐ ์ ์: ๋๊ธฐ ๋ฐฉ์์ผ๋ก ๋ฆฌํฉํ ๋ง
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void publishEvent(EventOutbox outbox) {
try {
- // Kafka๋ก ๋ฐํ
- kafkaProducer.publish(outbox)
- .thenAccept(result -> {
- // ๋ฐํ ์ฑ๊ณต ์ Outbox ์ํ ์
๋ฐ์ดํธ
- outbox.markAsPublished();
- outboxRepository.save(outbox);
-
- log.info("Outbox โ Kafka ๋ฐํ ์๋ฃ - outboxId: {}, eventType: {}, offset: {}",
- outbox.getId(),
- outbox.getEventType(),
- result.getRecordMetadata().offset());
- })
- .exceptionally(ex -> {
- // ๋ฐํ ์คํจ ์ Outbox ์ํ ์
๋ฐ์ดํธ
- outbox.markAsFailed(ex.getMessage());
- outboxRepository.save(outbox);
-
- log.error("Outbox โ Kafka ๋ฐํ ์คํจ - outboxId: {}, error: {}",
- outbox.getId(), ex.getMessage(), ex);
-
- return null;
- })
- .join(); // ๋๊ธฐ ๋๊ธฐ (ํธ๋์ญ์
๋ด์์ ์๋ฃ ๋ณด์ฅ)
+ // Kafka๋ก ๋ฐํ (๋๊ธฐ ๋๊ธฐ)
+ SendResult<Object, Object> result = kafkaProducer.publish(outbox).join();
+
+ // ๋ฐํ ์ฑ๊ณต ์ Outbox ์ํ ์
๋ฐ์ดํธ
+ outbox.markAsPublished();
+ outboxRepository.save(outbox);
+
+ log.info("Outbox โ Kafka ๋ฐํ ์๋ฃ - outboxId: {}, eventType: {}, offset: {}",
+ outbox.getId(),
+ outbox.getEventType(),
+ result.getRecordMetadata().offset());
} catch (Exception e) {๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java
around lines 66-89, the current use of thenAccept/exceptionally on the Kafka
publish future runs callbacks on the producer thread (losing the transaction
context) and swallows exceptions by returning null; change this to a synchronous
publish flow that executes in the current thread: call the publish and get its
result synchronously (or block and rethrow the exception) so that
outboxRepository.save(...) is invoked inside the current transactional context
on success or failure, remove repository saves from async callbacks, and ensure
exceptions are propagated to the outer catch so the duplicated state-save and
rethrow logic there remains correct; keep equivalent info/error logging but
perform it in the same thread before committing/rolling back the transaction.
| /** | ||
| * ์ฌ๋ฌ ์ํ ID๋ก ์กฐํ (๋ญํน์ฉ) | ||
| */ | ||
| @Transactional(readOnly = true) | ||
| public List<ProductInfo> findByIds(List<Long> ids) { | ||
| List<Product> products = productRepository.findByIdIn(ids); | ||
| // Brand ๋ก๋ฉ ๋ฐ DTO ๋ณํ | ||
| return products.stream() | ||
| .map(product -> { | ||
| product.getBrand().getName(); // Brand ๋ก๋ฉ | ||
| return ProductInfo.from(product); | ||
| }) | ||
| .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.
๋ฐํ ์์๊ฐ ์ ๋ ฅ ID ์์์ ์ผ์นํ์ง ์์ ์ ์์
findByIdIn์ ์
๋ ฅ๋ ID ์์๋ฅผ ๋ณด์ฅํ์ง ์์ต๋๋ค. ๋ญํน API์์ ์์ ๋ญํน ์์๋๋ก ์ํ์ ์กฐํํ ๋, ๋ฐํ๋ ๋ฆฌ์คํธ๊ฐ ๋ญํน ์์์ ๋ค๋ฅผ ์ ์์ต๋๋ค.
๐ ์์ ๋ณด์ฅ์ ์ํ ์์ ์ ์
@Transactional(readOnly = true)
public List<ProductInfo> findByIds(List<Long> ids) {
+ if (ids.isEmpty()) {
+ return List.of();
+ }
List<Product> products = productRepository.findByIdIn(ids);
+ // ID ์์๋๋ก ์ ๋ ฌํ์ฌ ๋ญํน ์์ ๋ณด์ฅ
+ Map<Long, Product> productMap = products.stream()
+ .collect(Collectors.toMap(Product::getId, Function.identity()));
+
- // Brand ๋ก๋ฉ ๋ฐ DTO ๋ณํ
- return products.stream()
- .map(product -> {
- product.getBrand().getName(); // Brand ๋ก๋ฉ
- return ProductInfo.from(product);
- })
- .toList();
+ return ids.stream()
+ .map(productMap::get)
+ .filter(Objects::nonNull)
+ .map(product -> {
+ product.getBrand().getName(); // Brand ๋ก๋ฉ
+ return ProductInfo.from(product);
+ })
+ .toList();
}๐ 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.
| /** | |
| * ์ฌ๋ฌ ์ํ ID๋ก ์กฐํ (๋ญํน์ฉ) | |
| */ | |
| @Transactional(readOnly = true) | |
| public List<ProductInfo> findByIds(List<Long> ids) { | |
| List<Product> products = productRepository.findByIdIn(ids); | |
| // Brand ๋ก๋ฉ ๋ฐ DTO ๋ณํ | |
| return products.stream() | |
| .map(product -> { | |
| product.getBrand().getName(); // Brand ๋ก๋ฉ | |
| return ProductInfo.from(product); | |
| }) | |
| .toList(); | |
| } | |
| /** | |
| * ์ฌ๋ฌ ์ํ ID๋ก ์กฐํ (๋ญํน์ฉ) | |
| */ | |
| @Transactional(readOnly = true) | |
| public List<ProductInfo> findByIds(List<Long> ids) { | |
| if (ids.isEmpty()) { | |
| return List.of(); | |
| } | |
| List<Product> products = productRepository.findByIdIn(ids); | |
| // ID ์์๋๋ก ์ ๋ ฌํ์ฌ ๋ญํน ์์ ๋ณด์ฅ | |
| Map<Long, Product> productMap = products.stream() | |
| .collect(Collectors.toMap(Product::getId, Function.identity())); | |
| return ids.stream() | |
| .map(productMap::get) | |
| .filter(Objects::nonNull) | |
| .map(product -> { | |
| product.getBrand().getName(); // Brand ๋ก๋ฉ | |
| return ProductInfo.from(product); | |
| }) | |
| .toList(); | |
| } |
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.java
around lines 76 to 89, the current findByIds(List<Long> ids) uses
productRepository.findByIdIn(ids) which does not preserve the input ID order;
change the method to build a Map<Long,Product> from the repository result (keyed
by product.getId()), then iterate the original ids list to produce the returned
List<ProductInfo> in the same order (skipping or handling missing ids as
appropriate); ensure you still trigger brand loading for each Product before
converting to ProductInfo (or update the repo to use a fetch-join so brand is
preloaded) so the DTO conversion remains correct.
| public List<RankingItem> getDailyRanking(String date, int page, int size) { | ||
| String key = DAILY_PREFIX + date; | ||
| return getRanking(key, page, size); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
#!/bin/bash
# Search for RankingKey class definition
find . -name "RankingKey.java" -type fRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 169
๐ Script executed:
#!/bin/bash
# Search for DAILY_PREFIX usage
rg -n "DAILY_PREFIX" --type=java -B 2 -A 2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3855
๐ Script executed:
#!/bin/bash
# Search for dailyToday method and RankingKey usage
rg -n "dailyToday\|RankingKey" --type=java -B 1 -A 1Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
๐ Script executed:
#!/bin/bash
# Search for RankingAggregator class
find . -name "RankingAggregator.java" -type fRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 181
๐ Script executed:
#!/bin/bash
# Search for ranking:all: pattern
rg -n "ranking:all:" --type=javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 502
๐ Script executed:
#!/bin/bash
# Read full RankingKey class
cat apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingKey.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1864
๐ Script executed:
#!/bin/bash
# Read RankingAggregator class to see how it uses keys
cat apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregator.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 4303
๐ Script executed:
#!/bin/bash
# Also check RankingService for complete context
cat apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3826
RankingService๋ฅผ RankingKey๋ฅผ ํตํด ์ผ๊ด๋๊ฒ ๋ฆฌํฉํ ๋ง ํ์
RankingService๊ฐ DAILY_PREFIX๋ฅผ ์ค๋ณต ์ ์ํ๊ณ ํค๋ฅผ ์๋์ผ๋ก ๊ตฌ์ฑํ๋ ๋ฐ๋ฉด, RankingAggregator๋ RankingKey.dailyToday()๋ฅผ ์ฌ์ฉํฉ๋๋ค. ๋ ์ฌ๊ฐํ๊ฒ๋ RankingService.getDailyRanking(String date, ...)์ด ๋ ์ง ํ์ ๊ฒ์ฆ ์์ด ์ง์ ๋ฌธ์์ด์ ์ฐ๊ฒฐํ๋ฏ๋ก, ์๋ชป๋ ํ์์ date๊ฐ ์ ๋ฌ๋๋ฉด RankingAggregator๊ฐ ์์ฑํ ํค์ ์ผ์นํ์ง ์์ต๋๋ค.
RankingKey.daily(String dateString) ๋ฉ์๋๋ฅผ ํ์ฉํ์ฌ ๋ ์ง ํ์ฑ๊ณผ ํค ์์ฑ์ ์ค์ํํ์ธ์ (๋ผ์ธ 36-39, 56-59, 76-77).
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java
around lines 36-39, 56-59 and 76-77, the service builds daily ranking keys by
concatenating DAILY_PREFIX + date (and repeats similar manual key construction),
which bypasses date validation and causes mismatches with RankingAggregator;
replace those manual constructions to call RankingKey.daily(date) (or
RankingKey.dailyToday() where appropriate) so key creation and date
parsing/validation are centralized in RankingKey, updating all uses in the
listed lines to accept/propagate the RankingKey result instead of assembling raw
strings.
| import com.loopers.application.inbox.EventInboxService; | ||
| import com.loopers.application.metrics.ProductMetricsService; | ||
| import com.loopers.application.ranking.RankingAggregator; | ||
| import com.loopers.confg.kafka.KafkaConfig; |
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.
ํจํค์ง ๊ฒฝ๋ก ์คํ
confg๋ ์คํ์
๋๋ค. config๋ก ์์ ํด์ผ ํฉ๋๋ค.
-import com.loopers.confg.kafka.KafkaConfig;
+import com.loopers.config.kafka.KafkaConfig;๐ 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.
| import com.loopers.confg.kafka.KafkaConfig; | |
| import com.loopers.config.kafka.KafkaConfig; |
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java
around line 8, the import package name has a typo:
"com.loopers.confg.kafka.KafkaConfig" should be
"com.loopers.config.kafka.KafkaConfig"; update the import to the correct package
name and fix any other occurrences/usages in this file (and related files) so
they reference com.loopers.config.kafka.KafkaConfig.
| @Transactional | ||
| protected boolean processEvent(ConsumerRecord<Object, Object> record) throws Exception { |
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.
@Transactional์ด ํ๋ก์ ๊ฒฝ๊ณ ๋ด์์ ๋์ํ์ง ์์ ์ ์์
processEvent๋ protected ๋ฉ์๋์ด๋ฉฐ, ๋์ผ ํด๋์ค ๋ด์ consumeOrderEvents์์ ์ง์ ํธ์ถ๋ฉ๋๋ค. Spring AOP ํ๋ก์๋ ๋ด๋ถ ํธ์ถ์์๋ @Transactional์ ์ธํฐ์
ํธํ์ง ์์ผ๋ฏ๋ก ํธ๋์ญ์
์ด ์ ์ฉ๋์ง ์์ ์ ์์ต๋๋ค.
ํด๊ฒฐ ๋ฐฉ์:
processEvent๋ฅผ ๋ณ๋์ ์๋น์ค ํด๋์ค๋ก ์ถ์ถํ์ฌ ์ฃผ์- ๋๋
consumeOrderEvents์ ์ฒด๋ฅผ@Transactional๋ก ๊ฐ์ธ๊ธฐ (๋ฐฐ์น ์ฒ๋ฆฌ์ ๋ฐ๋ฅธ ํธ๋ ์ด๋์คํ ๊ณ ๋ ค)
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java
around lines 81-82, the @Transactional annotation on the protected method
processEvent will not be applied when called directly from consumeOrderEvents
due to Spring proxy internal-call limitations; fix by either extracting the
processing logic into a new @Service bean with a public method (move
processEvent there, inject that service into OrderEventConsumer and call the
service method so the transaction proxy is applied) or by moving @Transactional
to consumeOrderEvents itself (annotate consumeOrderEvents as @Transactional,
ensuring you accept the transactional scope/behavior implications for the whole
consumption loop).
|
|
||
| // ๋ญํน ์ ์ ๋ฐ์ (๋จ๊ฐ ๊ณ์ฐ) | ||
| int unitPrice = amount.divide(new BigDecimal(quantity), 0, BigDecimal.ROUND_HALF_UP).intValue(); | ||
| rankingAggregator.incrementOrderScore(productId, unitPrice, quantity); |
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.
Deprecated API ์ฌ์ฉ: BigDecimal.ROUND_HALF_UP
BigDecimal.ROUND_HALF_UP์ Java 9๋ถํฐ deprecated์
๋๋ค. RoundingMode.HALF_UP์ ์ฌ์ฉํ์ธ์.
๐ ์์ ์ ์
+import java.math.RoundingMode;
...
- int unitPrice = amount.divide(new BigDecimal(quantity), 0, BigDecimal.ROUND_HALF_UP).intValue();
+ int unitPrice = amount.divide(new BigDecimal(quantity), 0, RoundingMode.HALF_UP).intValue();๐ 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.
| // ๋ญํน ์ ์ ๋ฐ์ (๋จ๊ฐ ๊ณ์ฐ) | |
| int unitPrice = amount.divide(new BigDecimal(quantity), 0, BigDecimal.ROUND_HALF_UP).intValue(); | |
| rankingAggregator.incrementOrderScore(productId, unitPrice, quantity); | |
| // ๋ญํน ์ ์ ๋ฐ์ (๋จ๊ฐ ๊ณ์ฐ) | |
| int unitPrice = amount.divide(new BigDecimal(quantity), 0, RoundingMode.HALF_UP).intValue(); | |
| rankingAggregator.incrementOrderScore(productId, unitPrice, quantity); |
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java
around lines 156-159, replace the deprecated BigDecimal.ROUND_HALF_UP with
java.math.RoundingMode.HALF_UP when calling BigDecimal.divide (i.e., use
RoundingMode.HALF_UP as the roundingMode argument), and add or update the import
for java.math.RoundingMode; keep the same scale (0) and int conversion logic
otherwise.
| @Test | ||
| @DisplayName("์ฃผ๋ฌธ ์์ฑ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ผ๋ฉด ProductMetrics์ ์ฃผ๋ฌธ ์์ ํ๋งค ๊ธ์ก์ ์ฆ๊ฐ์ํจ๋ค") | ||
| void incrementOrderCountAndSalesAmount() throws Exception { | ||
| // Given: ์ฃผ๋ฌธ ์์ฑ ์ด๋ฒคํธ | ||
| Long productId = 1L; | ||
| int quantity = 3; | ||
| BigDecimal totalAmount = new BigDecimal("30000"); | ||
|
|
||
| String eventId = "order-event-001"; | ||
| Map<String, Object> orderData = new HashMap<>(); | ||
| orderData.put("productId", productId); | ||
| orderData.put("quantity", quantity); | ||
| orderData.put("totalAmount", totalAmount); | ||
|
|
||
| Map<String, Object> event = createEvent(eventId, "OrderCreatedEvent", "ORDER", "order-123"); | ||
| event.put("payload", orderData); | ||
|
|
||
| ConsumerRecord<Object, Object> record = createConsumerRecord("order-events", event); | ||
|
|
||
| when(eventInboxService.isDuplicate(eventId)).thenReturn(false); | ||
|
|
||
| // When: Consumer๊ฐ ์ด๋ฒคํธ ์ฒ๋ฆฌ | ||
| boolean result = orderEventConsumer.processEvent(record); | ||
|
|
||
| // Then: Inbox์ ์ ์ฅํ๊ณ , ์ฃผ๋ฌธ ์์ ํ๋งค ๊ธ์ก ์ฆ๊ฐ | ||
| assertThat(result).isTrue(); | ||
| verify(eventInboxService).save(eventId, "ORDER", "order-123", "OrderCreatedEvent"); | ||
| verify(productMetricsService).incrementOrderCount(eq(productId), eq(quantity), eq(totalAmount)); | ||
| } |
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.
RankingAggregator.incrementOrderScore ํธ์ถ ๊ฒ์ฆ ๋๋ฝ
OrderCreatedEvent ์ฒ๋ฆฌ ์ rankingAggregator.incrementOrderScore๊ฐ ํธ์ถ๋์ด์ผ ํ์ง๋ง, ํ
์คํธ์์ ์ด๋ฅผ ๊ฒ์ฆํ์ง ์์ต๋๋ค.
๐ ๊ฒ์ฆ ์ถ๊ฐ ์ ์
verify(eventInboxService).save(eventId, "ORDER", "order-123", "OrderCreatedEvent");
verify(productMetricsService).incrementOrderCount(eq(productId), eq(quantity), eq(totalAmount));
+ verify(rankingAggregator).incrementOrderScore(eq(productId), anyInt(), eq(quantity));๐ 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.
| @Test | |
| @DisplayName("์ฃผ๋ฌธ ์์ฑ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ผ๋ฉด ProductMetrics์ ์ฃผ๋ฌธ ์์ ํ๋งค ๊ธ์ก์ ์ฆ๊ฐ์ํจ๋ค") | |
| void incrementOrderCountAndSalesAmount() throws Exception { | |
| // Given: ์ฃผ๋ฌธ ์์ฑ ์ด๋ฒคํธ | |
| Long productId = 1L; | |
| int quantity = 3; | |
| BigDecimal totalAmount = new BigDecimal("30000"); | |
| String eventId = "order-event-001"; | |
| Map<String, Object> orderData = new HashMap<>(); | |
| orderData.put("productId", productId); | |
| orderData.put("quantity", quantity); | |
| orderData.put("totalAmount", totalAmount); | |
| Map<String, Object> event = createEvent(eventId, "OrderCreatedEvent", "ORDER", "order-123"); | |
| event.put("payload", orderData); | |
| ConsumerRecord<Object, Object> record = createConsumerRecord("order-events", event); | |
| when(eventInboxService.isDuplicate(eventId)).thenReturn(false); | |
| // When: Consumer๊ฐ ์ด๋ฒคํธ ์ฒ๋ฆฌ | |
| boolean result = orderEventConsumer.processEvent(record); | |
| // Then: Inbox์ ์ ์ฅํ๊ณ , ์ฃผ๋ฌธ ์์ ํ๋งค ๊ธ์ก ์ฆ๊ฐ | |
| assertThat(result).isTrue(); | |
| verify(eventInboxService).save(eventId, "ORDER", "order-123", "OrderCreatedEvent"); | |
| verify(productMetricsService).incrementOrderCount(eq(productId), eq(quantity), eq(totalAmount)); | |
| } | |
| @Test | |
| @DisplayName("์ฃผ๋ฌธ ์์ฑ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ผ๋ฉด ProductMetrics์ ์ฃผ๋ฌธ ์์ ํ๋งค ๊ธ์ก์ ์ฆ๊ฐ์ํจ๋ค") | |
| void incrementOrderCountAndSalesAmount() throws Exception { | |
| // Given: ์ฃผ๋ฌธ ์์ฑ ์ด๋ฒคํธ | |
| Long productId = 1L; | |
| int quantity = 3; | |
| BigDecimal totalAmount = new BigDecimal("30000"); | |
| String eventId = "order-event-001"; | |
| Map<String, Object> orderData = new HashMap<>(); | |
| orderData.put("productId", productId); | |
| orderData.put("quantity", quantity); | |
| orderData.put("totalAmount", totalAmount); | |
| Map<String, Object> event = createEvent(eventId, "OrderCreatedEvent", "ORDER", "order-123"); | |
| event.put("payload", orderData); | |
| ConsumerRecord<Object, Object> record = createConsumerRecord("order-events", event); | |
| when(eventInboxService.isDuplicate(eventId)).thenReturn(false); | |
| // When: Consumer๊ฐ ์ด๋ฒคํธ ์ฒ๋ฆฌ | |
| boolean result = orderEventConsumer.processEvent(record); | |
| // Then: Inbox์ ์ ์ฅํ๊ณ , ์ฃผ๋ฌธ ์์ ํ๋งค ๊ธ์ก ์ฆ๊ฐ | |
| assertThat(result).isTrue(); | |
| verify(eventInboxService).save(eventId, "ORDER", "order-123", "OrderCreatedEvent"); | |
| verify(productMetricsService).incrementOrderCount(eq(productId), eq(quantity), eq(totalAmount)); | |
| verify(rankingAggregator).incrementOrderScore(eq(productId), anyInt(), eq(quantity)); | |
| } |
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.java
around lines 67-95 the test for OrderCreatedEvent misses verification that
rankingAggregator.incrementOrderScore is invoked; add a verify call after
existing assertions to assert rankingAggregator.incrementOrderScore was called
with the expected arguments (at minimum the productId and quantity) using
Mockito matchers (e.g.,
verify(rankingAggregator).incrementOrderScore(eq(productId), eq(quantity))); if
the method also accepts totalAmount include eq(totalAmount) as the third
argument.
| ### 3. Consumer Group ๊ด๋ฆฌ | ||
|
|
||
| ```bash | ||
| # Consumer Group ๋ชฉ๋ก | ||
| kafka-consumer-groups.sh --bootstrap-server localhost:19092 --list | ||
|
|
||
| # ์คํ์ ํ์ธ | ||
| kafka-consumer-groups.sh --bootstrap-server localhost:19092 \ | ||
| --group learning-group \ | ||
| --describe | ||
|
|
||
| # Group ์ญ์ (์ฌ์คํ ์) | ||
| kafka-consumer-groups.sh --bootstrap-server localhost:19092 \ | ||
| --group learning-group \ | ||
| --delete | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## ๐ ๏ธ ํธ๋ฌ๋ธ์ํ | ||
|
|
||
| ### Consumer๊ฐ ๋ฉ์์ง๋ฅผ ๋ชป ์ฝ์ด์ | ||
|
|
||
| **ํ์ธ ์ฌํญ:** | ||
| 1. Kafka ์คํ ์ค? `docker ps | grep kafka` | ||
| 2. ํ ํฝ ์์ฑ๋จ? `kafka-topics.sh --list` | ||
| 3. `auto.offset.reset` ์ค์ ํ์ธ | ||
|
|
||
| **ํด๊ฒฐ:** | ||
| ```bash | ||
| # Consumer Group ๋ฆฌ์ | ||
| kafka-consumer-groups.sh --bootstrap-server localhost:19092 \ | ||
| --group ๊ทธ๋ฃน๋ช --reset-offsets --to-earliest --topic learning-topic --execute | ||
| ``` | ||
|
|
||
| ### ํ ์คํธ๊ฐ ๋ฉ์ถฐ์ | ||
|
|
||
| **์์ธ:** Kafka ์ฐ๊ฒฐ ์คํจ | ||
|
|
||
| **ํด๊ฒฐ:** | ||
| ```bash | ||
| docker-compose restart kafka | ||
| ``` | ||
|
|
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 Group ๊ด๋ฆฌ ๋ฐ ํธ๋ฌ๋ธ์ํ ๋ช ๋ น์ด๊ฐ EmbeddedKafka ํ๊ฒฝ๊ณผ ๋ถ์ผ์นํฉ๋๋ค.
README์ ์ฃผ์ ๋ด์ฉ์ EmbeddedKafka๋ฅผ ์ฌ์ฉํ์ฌ Docker ์์ด ํ
์คํธ๋ฅผ ์คํํ๋ ๊ฒ์ ๊ฐ์กฐํ๊ณ ์์ง๋ง, Consumer Group ๊ด๋ฆฌ (Line 166-181)์ ํธ๋ฌ๋ธ์ํ
์น์
(Line 185-209)์์ ์ ๊ณตํ๋ kafka-consumer-groups.sh ๋ฐ kafka-topics.sh ๋ช
๋ น์ด๋ ์ธ๋ถ Kafka ๋ธ๋ก์ปค์ ์ ๊ทผํ๋ ๊ฒ์ ์ ์ ๋ก ํฉ๋๋ค.
EmbeddedKafka๋ ํ ์คํธ ์คํ ์ค์๋ง ์กด์ฌํ๋ฉฐ ์ผ๋ฐ์ ์ธ Kafka CLI ๋๊ตฌ๋ก ์ง์ ์ ๊ทผํ ์ ์์ด ํด๋น ๋ช ๋ น์ด๋ค์ด ์๋ํ์ง ์์ต๋๋ค. ์ด๋ ํ์ต์์๊ฒ ํผ๋์ ์ค ์ ์์ต๋๋ค.
๋ค์ ์ค ํ๋๋ฅผ ๊ถ์ฅํฉ๋๋ค:
- ํด๋น ์น์ ์ "Docker Kafka๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ์๋ง ์ ์ฉ ๊ฐ๋ฅ" ์ฃผ์ ์ถ๊ฐ
- EmbeddedKafka ํ๊ฒฝ์์๋ ํ ์คํธ ์ฝ๋ ๋ด์์ ์ง์ ๋๋ฒ๊น ํ๋ ๊ฒ์ ๊ถ์ฅํ๋ ๋ด์ฉ์ผ๋ก ์์
f6c8979 to
d1fb5f4
Compare
๐ Summary
Redis ZSET์ ํ์ฉํ ์ค์๊ฐ ๋ญํน ์์คํ ์ ๊ตฌํํ์ต๋๋ค.
์ฃผ์ ๊ตฌํ ์ฌํญ
๐ฌ Review Points
์ฝ๋ ์คํํธ ๊ด๋ จ ์ง๋ฌธ์ ๋๋ค.
ํ์ฌ ์ ๋ ์ ์์ 10%๋ฅผ ๋ค์๋ ํค๋ก ๋ฏธ๋ฆฌ ๋ณต์ฌํ๋ ๋ฐฉ์์ผ๋ก ์ ์ ํ์ต๋๋ค. ์ ์ ํ ์ด์ ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. ๊ด์ฐฎ์๊น์?
ํค ์ค๊ณ์ ๊ดํ ๊ณ ๋ฏผ์ ๋๋ค.
์ฒ์์๋ ranking:all๋ก ํ๋์ ํค์ ๋์ ํ๊ณ ์ถ์์ต๋๋ค.
ํ์ง๋ง ์ด๋ ๊ฒ ํ ๊ฒฝ์ฐ ๋ฉํ ๋ง ๋ ๋์๋
์ค๋๋ ์ธ๊ธฐ ์ํ์ด ๊ณ์ ์์ ๋ ์์ผ๋ก ์ธํด ๋ ์ง๋ณ๋ก ํค๋ฅผ ๋ถ๋ฆฌํ์ต๋๋ค.๊ทธ๋ ๋ค๋ฉด ๋ ์ง๋ณ๋ก ํค๋ฅผ ๋ง๋ค๊ณ ๋ญํน์ ์ ๋ ฌํ ๋์๋ ๊ฑฑ์ ํด์ผ ๋ ๋ถ๋ถ์ด ์์ ๊ฒ ๊ฐ์๋ฐ.. ์ด๋ค ๋ถ๋ถ์ ๊ณ ๋ คํด์ผ ํ ๊น์?
์ฆ,
ranking:all๊ณผranking:time:{yyyyMMdd}์ด๋ ๊ฒ ํ ๋ ์ด๋ค ํธ๋ ์ด๋์คํ๋ฅผ ๊ณ ๋ คํ๋ฉด ์ข์๊น์?๋ฐฐ์น ์ฒ๋ฆฌ ํฌ๊ธฐ๋ฅผ ์ด๋ป๊ฒ ํ๋ฉด ์ข์๊น์?
ํ์ฌ Kafka ๋ฐฐ์น ๋ฆฌ์ค๋์ ํฌ๊ธฐ๋ฅผ 3000๊ฑด์ผ๋ก ์ค์ ํ์ต๋๋ค. ์ด๊ฑธ ์ค๋ฌด์์ ์ ํ๋ ๋ฐฉ๋ฒ? ๊ธฐ์ค? ๊ฐ์ ํ์ด ์์๊น์?
โ Checklist
๐ Ranking Consumer
โพ Ranking API
Summary by CodeRabbit
๋ฆด๋ฆฌ์ค ๋ ธํธ
์๋ก์ด ๊ธฐ๋ฅ
๊ฐ์ ์ฌํญ
โ๏ธ Tip: You can customize this high-level summary in your review settings.