Skip to content

Conversation

@rkdalsdn94
Copy link
Collaborator

@rkdalsdn94 rkdalsdn94 commented Dec 25, 2025

๐Ÿ“Œ Summary

Redis ZSET์„ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ๋žญํ‚น ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ๊ตฌํ˜„ ์‚ฌํ•ญ

  • Kafka ๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ๋กœ ์กฐํšŒ/์ข‹์•„์š”/์ฃผ๋ฌธ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜์—ฌ Redis ZSET์— ์‹ค์‹œ๊ฐ„ ์ ์ˆ˜ ๋ฐ˜์˜
  • ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ์ ์ˆ˜ ๊ณ„์‚ฐ (์กฐํšŒ 0.1 / ์ข‹์•„์š” 0.2 / ์ฃผ๋ฌธ 0.6 + ๋กœ๊ทธ ์ •๊ทœํ™”)
  • ์ผ๊ฐ„/์‹ค์‹œ๊ฐ„ ์ด์ค‘ ๋žญํ‚น ์‹œ์Šคํ…œ (๋‚ ์งœ๋ณ„ ํ‚ค ๋ถ„๋ฆฌ)
  • ์ฝœ๋“œ ์Šคํƒ€ํŠธ ๋ฌธ์ œ ํ•ด๊ฒฐ (์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ํ†ตํ•œ Score Carry-Over)
  • ํŽ˜์ด์ง•์„ ํฌํ•จํ•œ ๋žญํ‚น ์กฐํšŒ API ๋ฐ ์ƒํ’ˆ ์ƒ์„ธ์— ์ˆœ์œ„ ์ •๋ณด ์ถ”๊ฐ€

๐Ÿ’ฌ Review Points

์ฝœ๋“œ ์Šคํƒ€ํŠธ ๊ด€๋ จ ์งˆ๋ฌธ์ž…๋‹ˆ๋‹ค.

ํ˜„์žฌ ์ „๋‚  ์ ์ˆ˜์˜ 10%๋ฅผ ๋‹ค์Œ๋‚  ํ‚ค๋กœ ๋ฏธ๋ฆฌ ๋ณต์‚ฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์„ ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ์„ ์ •ํ•œ ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๊ดœ์ฐฎ์„๊นŒ์š”?

  • ๋นˆ ํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ฉด์„œ, ์˜ค๋Š˜ ์ˆœ์œ„ ๊ณ„์‚ฐ์—์„œ ์—ญ์ „ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค๋ ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ดœ์ฐฎ์€ ๋ฐฉ๋ฒ•์ธ์ง€ ๊ถ๊ธˆํ•˜๊ณ , ์–ด๋–ค ๋ถ€๋ถ„์„ ๋” ๊ณ ๋ คํ•˜๋ฉด ์ข‹์„๊นŒ์š”?

ํ‚ค ์„ค๊ณ„์— ๊ด€ํ•œ ๊ณ ๋ฏผ์ž…๋‹ˆ๋‹ค.

์ฒ˜์Œ์—๋Š” ranking:all๋กœ ํ•˜๋‚˜์˜ ํ‚ค์— ๋ˆ„์ ํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ํ•  ๊ฒฝ์šฐ ๋ฉ˜ํ† ๋ง ๋•Œ ๋‚˜์™”๋˜ ์˜ค๋ž˜๋œ ์ธ๊ธฐ ์ƒํ’ˆ์ด ๊ณ„์† ์ƒ์œ„ ๋…์‹์œผ๋กœ ์ธํ•ด ๋‚ ์งœ๋ณ„๋กœ ํ‚ค๋ฅผ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
๊ทธ๋ ‡๋‹ค๋ฉด ๋‚ ์งœ๋ณ„๋กœ ํ‚ค๋ฅผ ๋งŒ๋“ค๊ณ  ๋žญํ‚น์„ ์ •๋ ฌํ•  ๋•Œ์—๋„ ๊ฑฑ์ •ํ•ด์•ผ ๋  ๋ถ€๋ถ„์ด ์žˆ์„ ๊ฒƒ ๊ฐ™์€๋ฐ.. ์–ด๋–ค ๋ถ€๋ถ„์„ ๊ณ ๋ คํ•ด์•ผ ํ• ๊นŒ์š”?
์ฆ‰, ranking:all ๊ณผ ranking:time:{yyyyMMdd} ์ด๋ ‡๊ฒŒ ํ•  ๋•Œ ์–ด๋–ค ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋ฅผ ๊ณ ๋ คํ•˜๋ฉด ์ข‹์„๊นŒ์š”?

๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํฌ๊ธฐ๋ฅผ ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ์ข‹์„๊นŒ์š”?

ํ˜„์žฌ Kafka ๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ์˜ ํฌ๊ธฐ๋ฅผ 3000๊ฑด์œผ๋กœ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฑธ ์‹ค๋ฌด์—์„œ ์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•? ๊ธฐ์ค€? ๊ฐ™์€ ํŒ์ด ์žˆ์„๊นŒ์š”?

โœ… Checklist

๐Ÿ“ˆ Ranking Consumer

  • ๋žญํ‚น ZSET ์˜ TTL, ํ‚ค ์ „๋žต์„ ์ ์ ˆํ•˜๊ฒŒ ๊ตฌ์„ฑํ•˜์˜€๋‹ค
  • ๋‚ ์งœ๋ณ„๋กœ ์ ์žฌํ•  ํ‚ค๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์—ˆ๋‹ค
  • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ํ›„, ZSET ์— ์ ์ˆ˜๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ฐ˜์˜๋œ๋‹ค

โšพ Ranking API

  • ๋žญํ‚น Page ์กฐํšŒ ์‹œ ์ •์ƒ์ ์œผ๋กœ ๋žญํ‚น ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค
  • ๋žญํ‚น Page ์กฐํšŒ ์‹œ ๋‹จ์ˆœํžˆ ์ƒํ’ˆ ID ๊ฐ€ ์•„๋‹Œ ์ƒํ’ˆ์ •๋ณด๊ฐ€ Aggregation ๋˜์–ด ์ œ๊ณต๋œ๋‹ค
  • ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ํ•ด๋‹น ์ƒํ’ˆ์˜ ์ˆœ์œ„๊ฐ€ ํ•จ๊ป˜ ๋ฐ˜ํ™˜๋œ๋‹ค (์ˆœ์œ„์— ์—†๋‹ค๋ฉด null)

Summary by CodeRabbit

๋ฆด๋ฆฌ์Šค ๋…ธํŠธ

  • ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ

    • Kafka ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฐ ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ ๊ตฌ์ถ•
    • ์ œํ’ˆ ์ˆœ์œ„ ์กฐํšŒ API ์ถ”๊ฐ€ (์ผ์ผ/์‹œ๊ฐ„๋ณ„ ์ˆœ์œ„)
    • ์ œํ’ˆ ์กฐํšŒ์ˆ˜, ์ข‹์•„์š”, ์ฃผ๋ฌธ ๋ฉ”ํŠธ๋ฆญ ์ถ”์ 
    • ์ด๋ฒคํŠธ ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐ ์‹คํŒจ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ
  • ๊ฐœ์„  ์‚ฌํ•ญ

    • ์ƒํ’ˆ ์ˆœ์œ„ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ์ œํ’ˆ ์ƒ์„ธ ์กฐํšŒ
    • ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์ œํ’ˆ ์บ์‹œ ์ž๋™ ๊ฐฑ์‹ 

โœ๏ธ Tip: You can customize this high-level summary in your review settings.

@rkdalsdn94 rkdalsdn94 self-assigned this Dec 25, 2025
@rkdalsdn94 rkdalsdn94 added the enhancement New feature or request label Dec 25, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 25, 2025

Walkthrough

Kafka๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ , ์ƒํ’ˆ ์ˆœ์œ„ ๋ฐ ๋ฉ”ํŠธ๋ฆญ ์ถ”์  ์‹œ์Šคํ…œ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. OutboxEventService๊ฐ€ Kafka๋กœ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๊ณ , ์ƒˆ๋กœ์šด commerce-streamer ์„œ๋น„์Šค๊ฐ€ ์ด๋ฒคํŠธ๋ฅผ ์†Œ๋น„ํ•˜์—ฌ ๋ฉ”ํŠธ๋ฆญ๊ณผ ์ˆœ์œ„๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ์ค‘๋ณต ์ œ๊ฑฐ(Inbox) ๋ฐ ์‹คํŒจ ์ฒ˜๋ฆฌ(DLQ) ํŒจํ„ด์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

Changes

์ฝ”ํ˜ธํŠธ / ํŒŒ์ผ ์š”์•ฝ
Kafka ๋ชจ๋“ˆ ๋ฐ ๋นŒ๋“œ ๊ตฌ์„ฑ
apps/commerce-api/build.gradle.kts, modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java, modules/kafka/src/main/resources/kafka.yml
Kafka ๋ชจ๋“ˆ ์˜์กด์„ฑ ์ถ”๊ฐ€, ๋ช…์‹œ์  Spring Kafka ์ž„ํฌํŠธ๋กœ ๋ณ€๊ฒฝ, ํ”„๋กœ๋“€์„œ ์„ค์ •(idempotence, snappy compression, acks=all) ์ถ”๊ฐ€, ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ offset reset ๊ตฌ์„ฑ
Outbox ์ด๋ฒคํŠธ ๋ฐœํ–‰์ž
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java, apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java, apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java
OutboxEventPublisher์— @ConditionalOnProperty ์ถ”๊ฐ€, OutboxEventService๊ฐ€ EventKafkaProducer ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝ, EventKafkaProducer ์‹ ๊ทœ ์ƒ์„ฑ(Kafka ํ† ํ”ฝ ๋ฐœํ–‰, ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ)
์ƒํ’ˆ ์บ์‹œ ๋ฐ ์ฃผ๋ฌธ ํ†ตํ•ฉ
apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java, apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
ProductCacheService์— evictCache(Long), evictAllCache() ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€, OrderFacade์—์„œ ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์บ์‹œ ์ œ๊ฑฐ ํ˜ธ์ถœ
์ƒํ’ˆ ์ˆœ์œ„ ์กฐํšŒ API
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java, apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingApi.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingResponse.java
Redis ZSET ๊ธฐ๋ฐ˜ RankingService(์ผ๊ฐ„/์‹ค์‹œ๊ฐ„ ์ˆœ์œ„ ์กฐํšŒ), RankingFacade(์ƒํ’ˆ ์ •๋ณด์™€ ๋ณ‘ํ•ฉ), RankingApi(REST ์—”๋“œํฌ์ธํŠธ) ์‹ ๊ทœ ์ƒ์„ฑ
์ƒํ’ˆ ์‘๋‹ต DTO ํ™•์žฅ
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java, apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java, apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.java
ProductV1Dto.ProductResponse์— rank ํ•„๋“œ ์ถ”๊ฐ€, ProductV1Controller์—์„œ RankingService ํ†ตํ•ฉ, ProductService.findByIds() ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
Streamer ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์„ฑ
apps/commerce-streamer/build.gradle.kts, apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java, apps/commerce-streamer/src/main/resources/application.yml
awaitility, testcontainers-kafka ์˜์กด์„ฑ ์ถ”๊ฐ€, @EnableScheduling ์ถ”๊ฐ€, Kafka ํ† ํ”ฝ ์„ค์ •
Streamer ์ด๋ฒคํŠธ ์†Œ๋น„์ž
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java, apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java
์นดํƒˆ๋กœ๊ทธ/์ฃผ๋ฌธ ์ด๋ฒคํŠธ ์†Œ๋น„, ์ค‘๋ณต ํ™•์ธ(Inbox), ๋ฉ”ํŠธ๋ฆญ/์ˆœ์œ„ ์—…๋ฐ์ดํŠธ, DLQ๋กœ ์‹คํŒจ ์ฒ˜๋ฆฌ
๋ฉ”ํŠธ๋ฆญ ์ถ”์  ๋„๋ฉ”์ธ
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java, apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java, apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java, apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetrics*.java
ProductMetrics ์—”ํ‹ฐํ‹ฐ(์ข‹์•„์š”, ์กฐํšŒ, ์ฃผ๋ฌธ ์ˆ˜), ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ ์„œ๋น„์Šค, JPA/๊ตฌํ˜„ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ
์ˆœ์œ„ ๊ณ„์‚ฐ ๋„๋ฉ”์ธ
apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingKey.java, apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingScore.java, apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregator.java, apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingScheduler.java
Redis ZSET ์ˆœ์œ„ ํ‚ค ์ƒ์„ฑ, ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ์ ์ˆ˜ ๊ณ„์‚ฐ(์กฐํšŒ 0.1, ์ข‹์•„์š” 0.2, ์ฃผ๋ฌธ 0.6), ์ผ์ผ ์ˆœ์œ„ ์—…๋ฐ์ดํŠธ, ์ผ์ผ ์ด์›” ์Šค์ผ€์ค„
Inbox/DLQ ์ค‘๋ณต ์ œ๊ฑฐ ๋ฐ ์‹คํŒจ ์ฒ˜๋ฆฌ
apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java, apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInboxRepository.java, apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java, apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java, apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueueRepository.java, apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java
EventInbox ์—”ํ‹ฐํ‹ฐ(์ค‘๋ณต ํ™•์ธ), DeadLetterQueue ์—”ํ‹ฐํ‹ฐ(์‹คํŒจ ๋ฉ”์‹œ์ง€), ๊ฐ๊ฐ์˜ ์„œ๋น„์Šค ๋ฐ JPA/๊ตฌํ˜„ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ
docker/init-db.sql
event_inbox, product_metrics, dead_letter_queue ํ…Œ์ด๋ธ” ์ถ”๊ฐ€
ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ
apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.java, apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java, apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java, apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/*Test.java
ํšŒ๋กœ ์ฐจ๋‹จ๊ธฐ ํ…Œ์ŠคํŠธ ์—…๋ฐ์ดํŠธ(atLeast ๊ฒ€์ฆ), Inbox/๋ฉ”ํŠธ๋ฆญ ์„œ๋น„์Šค ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ, ์†Œ๋น„์ž ๋‹จ์œ„/ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
Kafka ํ•™์Šต ํ…Œ์ŠคํŠธ
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment*.java, modules/kafka/src/test/java/com/loopers/kafka/learning/README.md
์ž๋™ ์ปค๋ฐ‹, offset reset, poll interval, ๋ฐฐ์น˜ ํฌ๊ธฐ ๋™์ž‘ ํ•™์Šต ํ…Œ์ŠคํŠธ ๋ฐ ๋ฌธ์„œ
์ž„ํฌํŠธ ์ •๋ฆฌ ๋ฐ ๊ธฐํƒ€
apps/commerce-api/src/main/java/com/loopers/.../, modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java, modules/redis/src/main/java/com/loopers/config/redis/Redis*.java, modules/pg-simulator/src/main/kotlin/com/loopers/, performance-test/
์ค‘๋ณต ์ž„ํฌํŠธ ์ œ๊ฑฐ, ์ž„ํฌํŠธ ์ˆœ์„œ ์ •๋ ฌ, ์™€์ผ๋“œ์นด๋“œ ์ž„ํฌํŠธ ํ†ตํ•ฉ, ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ

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
Loading
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
Loading
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
Loading

Estimated code review effort

๐ŸŽฏ 4 (Complex) | โฑ๏ธ ~60 minutes

Possibly related PRs

Poem

๐Ÿฐ Kafka์˜ ๊ฐ•๋ฌผ ๋”ฐ๋ผ

๋ฐ์ดํ„ฐ๋Š” ํ˜๋Ÿฌ Kafka ๊ฐ•์„ ๊ฑด๋„ˆ๊ณ ,
์ˆœ์œ„๋Š” Redis์— ๊ธˆ๋น›์œผ๋กœ ๋น›๋‚˜๋ฉฐ,
Inbox์™€ DLQ๋กœ ์ค‘๋ณต ์—†์ด ์ •๋ ฌ๋˜๊ณ ,
๋ฉ”ํŠธ๋ฆญ์€ ์„ธ์‹ฌํ•˜๊ฒŒ ์ถ”์ ๋˜๋ฆฌ.
๐ŸŒŸ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์˜ ์šฐ์•„ํ•œ ์—ฌ์ •์ด์—ฌ!

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 68.82% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
โœ… Passed checks (2 passed)
Check name Status Explanation
Title check โœ… Passed PR ์ œ๋ชฉ์ด Redis๋ฅผ ํ™œ์šฉํ•œ ์ƒํ’ˆ ๋žญํ‚น ์‹œ์Šคํ…œ ๊ตฌํ˜„์ด๋ผ๋Š” ์ฃผ์š” ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๋ช…ํ™•ํ•˜๊ณ  ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์š”์•ฝํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
Description check โœ… Passed PR ์„ค๋ช…์ด ํ…œํ”Œ๋ฆฟ์˜ ์ฃผ์š” ์„น์…˜์„ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๊ตฌํ˜„ ๋‚ด์šฉ๊ณผ ๋ฆฌ๋ทฐ ํฌ์ธํŠธ๋ฅผ ์ƒ์„ธํžˆ ์„ค๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
โœจ Finishing touches
  • ๐Ÿ“ Generate docstrings
๐Ÿงช Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

โค๏ธ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- 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: ์ƒํ’ˆ ์ •๋ณด ์กฐํ•ฉ ๋กœ๊ทธ
Copy link

@coderabbitai coderabbitai bot left a 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.RestController
modules/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.UUID
modules/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.TransactionKeyGenerator
apps/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์˜ Optional import๊ฐ€ ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด ์ฝ”๋“œ์—์„œ๋„ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
 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.TimeZone
modules/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

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 9c7961e and f6c8979.

๐Ÿ“’ Files selected for processing (81)
  • apps/commerce-api/build.gradle.kts
  • apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
  • apps/commerce-api/src/main/java/com/loopers/application/dataplatform/DataPlatformEventHandler.java
  • apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java
  • apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java
  • apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
  • apps/commerce-api/src/main/java/com/loopers/config/CacheConfig.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClientConfig.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgPaymentRequest.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentApi.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingApi.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingResponse.java
  • apps/commerce-api/src/main/resources/application.yml
  • apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceFallbackTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceRetryTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/support/error/CoreExceptionTest.java
  • apps/commerce-streamer/build.gradle.kts
  • apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregator.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingScheduler.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueueRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInboxRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingKey.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingScore.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueJpaRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxJpaRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java
  • apps/commerce-streamer/src/main/resources/application.yml
  • apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.java
  • docker/init-db.sql
  • modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java
  • modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java
  • modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java
  • modules/kafka/src/main/resources/kafka.yml
  • modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment1_AutoCommitTest.java
  • modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.java
  • modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment3_PollIntervalTest.java
  • modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java
  • modules/kafka/src/test/java/com/loopers/kafka/learning/README.md
  • modules/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt
  • modules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt
  • modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt
  • modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt
  • modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt
  • modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt
  • modules/redis/src/main/java/com/loopers/config/redis/RedisConfig.java
  • modules/redis/src/main/java/com/loopers/config/redis/RedisProperties.java
  • performance-test/README.md
  • performance-test/k6/product-load-test-fixed.js
  • performance-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.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.java
  • modules/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.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java
  • apps/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.java
  • apps/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.java
  • docker/init-db.sql
  • apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInboxRepository.java
  • apps/commerce-streamer/src/main/resources/application.yml
  • apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java
  • modules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt
  • apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java
  • apps/commerce-api/src/main/resources/application.yml
  • apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java
  • apps/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.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java
  • apps/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.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java
  • apps/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.java
  • docker/init-db.sql
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java
  • modules/kafka/src/main/resources/kafka.yml
  • apps/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.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java
  • apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java
  • apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java
  • apps/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.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/application/dataplatform/DataPlatformEventHandler.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java
  • modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt
  • apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java
  • modules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java
  • apps/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.java
  • modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt
  • modules/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.java
  • apps/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.kt
  • modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt
  • apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
  • apps/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.java
  • apps/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)

Comment on lines 63 to 69
private String getTopicByAggregateType(String aggregateType) {
return switch (aggregateType.toUpperCase()) {
case "ORDER", "PAYMENT" -> orderEventsTopic;
case "PRODUCT", "LIKE" -> catalogEventsTopic;
default -> throw new IllegalArgumentException("์•Œ ์ˆ˜ ์—†๋Š” AggregateType: " + aggregateType);
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor

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.

Comment on lines 98 to 102
// ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์บ์‹œ ๊ฐฑ์‹ 
if (product.getStock() == 0) {
log.info("์žฌ๊ณ  ์†Œ์ง„ - ์บ์‹œ ๊ฐฑ์‹  - productId: {}", product.getId());
productCacheService.evictCache(product.getId());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์ „ ์บ์‹œ ์‚ญ์ œ๋กœ ์ธํ•œ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ๋ฌธ์ œ

์บ์‹œ ์‚ญ์ œ๊ฐ€ ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์ „์— ๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด ๋‹ค์Œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ๋กค๋ฐฑ ์‹œ ๋ถˆ์ผ์น˜: ํŠธ๋žœ์žญ์…˜์ด ๋กค๋ฐฑ๋˜์–ด๋„ ์บ์‹œ๋Š” ์ด๋ฏธ ์‚ญ์ œ๋˜์–ด ๋‹ค์Œ ์กฐํšŒ ์‹œ DB์—์„œ ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  2. ์„ฑ๋Šฅ ์˜ํ–ฅ: ๋™๊ธฐ ์บ์‹œ ์‚ญ์ œ๊ฐ€ ํŠธ๋žœ์žญ์…˜ ์‹œ๊ฐ„์„ ๋Š˜๋ ค ๋ฝ ํ™€๋”ฉ ์‹œ๊ฐ„์ด ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

๊ถŒ์žฅ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ:

  • @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());
    }
}

Comment on lines 66 to 89
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(); // ๋™๊ธฐ ๋Œ€๊ธฐ (ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ์™„๋ฃŒ ๋ณด์žฅ)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

ํŠธ๋žœ์žญ์…˜ ๋‚ด ๋น„๋™๊ธฐ ์ฝœ๋ฐฑ ์‚ฌ์šฉ ์‹œ ์ฃผ์˜ ํ•„์š”

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.

Comment on lines +76 to +89
/**
* ์—ฌ๋Ÿฌ ์ƒํ’ˆ 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();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

๋ฐ˜ํ™˜ ์ˆœ์„œ๊ฐ€ ์ž…๋ ฅ 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.

Suggested change
/**
* ์—ฌ๋Ÿฌ ์ƒํ’ˆ 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.

Comment on lines +36 to +39
public List<RankingItem> getDailyRanking(String date, int page, int size) {
String key = DAILY_PREFIX + date;
return getRanking(key, page, size);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Search for RankingKey class definition
find . -name "RankingKey.java" -type f

Repository: 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 2

Repository: 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 1

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 70


๐Ÿ Script executed:

#!/bin/bash
# Search for RankingAggregator class
find . -name "RankingAggregator.java" -type f

Repository: 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=java

Repository: 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.java

Repository: 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.java

Repository: 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.java

Repository: 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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor

ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ ์˜คํƒ€

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.

Suggested change
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.

Comment on lines 81 to 82
@Transactional
protected boolean processEvent(ConsumerRecord<Object, Object> record) throws Exception {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

@Transactional์ด ํ”„๋ก์‹œ ๊ฒฝ๊ณ„ ๋‚ด์—์„œ ๋™์ž‘ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ

processEvent๋Š” protected ๋ฉ”์„œ๋“œ์ด๋ฉฐ, ๋™์ผ ํด๋ž˜์Šค ๋‚ด์˜ consumeOrderEvents์—์„œ ์ง์ ‘ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. Spring AOP ํ”„๋ก์‹œ๋Š” ๋‚ด๋ถ€ ํ˜ธ์ถœ์—์„œ๋Š” @Transactional์„ ์ธํ„ฐ์…‰ํŠธํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ํŠธ๋žœ์žญ์…˜์ด ์ ์šฉ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ:

  1. processEvent๋ฅผ ๋ณ„๋„์˜ ์„œ๋น„์Šค ํด๋ž˜์Šค๋กœ ์ถ”์ถœํ•˜์—ฌ ์ฃผ์ž…
  2. ๋˜๋Š” 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).

Comment on lines +156 to +159

// ๋žญํ‚น ์ ์ˆ˜ ๋ฐ˜์˜ (๋‹จ๊ฐ€ ๊ณ„์‚ฐ)
int unitPrice = amount.divide(new BigDecimal(quantity), 0, BigDecimal.ROUND_HALF_UP).intValue();
rankingAggregator.incrementOrderScore(productId, unitPrice, quantity);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor

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.

Suggested change
// ๋žญํ‚น ์ ์ˆ˜ ๋ฐ˜์˜ (๋‹จ๊ฐ€ ๊ณ„์‚ฐ)
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.

Comment on lines 67 to 95
@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));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor

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.

Suggested change
@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.

Comment on lines 166 to 209
### 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
```

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor

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 ํ™˜๊ฒฝ์—์„œ๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋‚ด์—์„œ ์ง์ ‘ ๋””๋ฒ„๊น…ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•˜๋Š” ๋‚ด์šฉ์œผ๋กœ ์ˆ˜์ •

@rkdalsdn94 rkdalsdn94 closed this Dec 25, 2025
@rkdalsdn94 rkdalsdn94 reopened this Dec 25, 2025
@rkdalsdn94 rkdalsdn94 merged commit af900e1 into Loopers-dev-lab:rkdalsdn94 Dec 30, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant