Skip to content

Conversation

@jeonga1022
Copy link
Collaborator

@jeonga1022 jeonga1022 commented Dec 19, 2025

๐Ÿ“Œ Summary

  • Kafka๋ฅผ ํ™œ์šฉํ•œ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ ๊ตฌํ˜„
  • Producer: Transactional Outbox ํŒจํ„ด์œผ๋กœ ์ด๋ฒคํŠธ ์œ ์‹ค ๋ฐฉ์ง€
  • Consumer: event_handled ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ๋กœ ์ค‘๋ณต ๋ฐฉ์ง€
  • ์ˆœ์„œ ๋ณด์žฅ: Partition Key + timestamp ๋น„๊ต๋กœ ์ด์ค‘ ๋ฐฉ์–ด
  • ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์ƒํ’ˆ ์บ์‹œ ๋ฌดํšจํ™” ์—ฐ๋™

๐Ÿ’ฌ Review Points

Kafka ํ•™์Šต ๊นŠ์ด์— ๋Œ€ํ•œ ์กฐ์–ธ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค

Kafka๋ฅผ ์ด๋ฒˆ์— ์ฒ˜์Œ ์ ‘ํ–ˆ๊ณ , Outbox ํŒจํ„ด + Consumer ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ๊นŒ์ง€๋Š” ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
DLQ, ๋ชจ๋‹ˆํ„ฐ๋ง, traceId ์ „ํŒŒ ๊ฐ™์€ ์šด์˜ ๊ด€์ ์€ ์•„์ง ๊ฐœ๋…๋งŒ ์•„๋Š” ์ƒํƒœ์ž…๋‹ˆ๋‹ค.

ํ˜„์žฌ ํšŒ์‚ฌ์—์„œ๋Š” Kafka๋ฅผ ์“ธ ์ผ์ด ์—†๋Š”๋ฐ, ๋ฉด์ ‘ ๋Œ€๋น„๋กœ ์ง€๊ธˆ ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•ด์•ผ ํ• ์ง€ ๊ณ ๋ฏผ์ž…๋‹ˆ๋‹ค.
์•„๋‹ˆ๋ฉด ์ด์ง ํ›„ ํšŒ์‚ฌ์—์„œ ์“ฐ๊ฒŒ ๋œ๋‹ค๋ฉด ๊ทธ๋•Œ ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•ด๋„ ๊ดœ์ฐฎ์„์ง€ ์กฐ์–ธ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

โœ… Checklist

๐ŸŽพ Producer

  • ๋„๋ฉ”์ธ(์• ํ”Œ๋ฆฌ์ผ€์ด์…˜) ์ด๋ฒคํŠธ ์„ค๊ณ„
  • Producer ์•ฑ์—์„œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (catalog-events, order-events, ๋“ฑ)
  • PartitionKey ๊ธฐ๋ฐ˜์˜ ์ด๋ฒคํŠธ ์ˆœ์„œ ๋ณด์žฅ
  • ๋ฉ”์„ธ์ง€ ๋ฐœํ–‰์ด ์‹คํŒจํ–ˆ์„ ๊ฒฝ์šฐ์— ๋Œ€ํ•ด ๊ณ ๋ฏผํ•ด๋ณด๊ธฐ

โšพ Consumer

  • Consumer ๊ฐ€ Metrics ์ง‘๊ณ„ ์ฒ˜๋ฆฌ
  • event_handled ํ…Œ์ด๋ธ”์„ ํ†ตํ•œ ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
  • ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์ƒํ’ˆ ์บ์‹œ ๊ฐฑ์‹ 
  • ์ค‘๋ณต ๋ฉ”์„ธ์ง€ ์žฌ์ „์†ก ํ…Œ์ŠคํŠธ โ†’ ์ตœ์ข… ๊ฒฐ๊ณผ๊ฐ€ ํ•œ ๋ฒˆ๋งŒ ๋ฐ˜์˜๋˜๋Š”์ง€ ํ™•์ธ

๐Ÿ“Ž References

Summary by CodeRabbit

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

  • ์ƒˆ ๊ธฐ๋Šฅ

    • ์ƒํ’ˆ ์ข‹์•„์š” ๋ฐ ์ฃผ๋ฌธ ์ˆ˜๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.
    • ์ƒํ’ˆ ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์บ์‹œ๋ฅผ ์ž๋™์œผ๋กœ ๋ฌดํšจํ™”ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • Chores

    • Kafka ๋ฐ Zookeeper Docker ์„œ๋น„์Šค๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • Tests

    • ์ด๋ฒคํŠธ ์†Œ๋น„ ๋ฐ ์ง€ํ‘œ ๊ด€๋ฆฌ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 19, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR implements an event-driven architecture using the outbox pattern with Kafka integration. It introduces domain events (StockDepletedEvent), outbox entities for reliable event publishing, a relay component for Kafka distribution, and a consumer for processing events. It also adds ProductMetrics tracking and integrates Kafka infrastructure via Docker Compose.

Changes

Cohort / File(s) Summary
Kafka Dependency & Configuration
apps/commerce-api/build.gradle.kts, apps/commerce-api/src/main/resources/application.yml, apps/commerce-api/src/test/resources/application.yml, modules/kafka/src/main/resources/kafka.yml
Added Kafka module dependency; configured producer acks and idempotence; updated config imports to include kafka.yml; enabled producer idempotence and consumer deserializer settings.
Domain Event Publishing
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java, apps/commerce-api/src/main/java/com/loopers/domain/product/event/StockDepletedEvent.java
Added ApplicationEventPublisher to ProductDomainService; publishes StockDepletedEvent when product stock reaches zero; created new StockDepletedEvent value object with factory method and timestamp tracking.
Outbox Pattern Implementation
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/Outbox.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxStatus.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxRepository.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxRelay.java
Introduced Outbox JPA entity with PENDING/PROCESSED states; created repository for outbox queries; implemented OutboxEventHandler to persist domain events to outbox; built OutboxRelay to batch-process and publish outbox records to Kafka with header metadata.
Event Idempotency
apps/commerce-api/src/main/java/com/loopers/infrastructure/idempotent/EventHandled.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/idempotent/EventHandledRepository.java
Added EventHandled JPA entity to track processed event IDs; created repository for idempotency checks via existsByEventId query.
Product Metrics Tracking
apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetrics.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepository.java
Introduced ProductMetrics entity with like/order counters and timestamp-based update logic; added repository for product metrics lookups; includes incrementLikeCount, decrementLikeCount, addOrder, and updateLikeIfNewer methods.
Kafka Event Consumer
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/CatalogEventConsumer.java
Created CatalogEventConsumer Spring component to consume catalog-events; checks idempotency via EventHandledRepository; dispatches ProductLikedEvent to ProductMetrics updates and StockDepletedEvent to cache invalidation; extracts headers and persists processed event IDs.
Infrastructure & Docker
docker-compose.yml
Added Zookeeper (port 2181) and Kafka (port 19092โ†’9092) services with Confluent images (7.5.0); configured Kafka broker, Zookeeper connection, and replication factor.
Test Coverage
apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/CatalogEventConsumerTest.java, apps/commerce-api/src/test/java/com/loopers/infrastructure/idempotent/EventHandledTest.java, apps/commerce-api/src/test/java/com/loopers/infrastructure/metrics/ProductMetricsTest.java, apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventHandlerTest.java, apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxRelayTest.java, apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxTest.java
Added comprehensive unit and integration tests for event consumer (multi-scenario like/unlike/depletion/idempotent cases), idempotent event tracking, ProductMetrics mutations and timestamp logic, outbox event handler integration, outbox relay Kafka publishing, and outbox state transitions.

Sequence Diagram(s)

sequenceDiagram
    participant DS as ProductDomainService
    participant EP as ApplicationEventPublisher
    participant OEH as OutboxEventHandler
    participant OB as Outbox Repository
    participant OR as OutboxRelay
    participant KT as Kafka Template
    participant KC as CatalogEventConsumer
    participant PMR as ProductMetrics Repository
    participant PCS as ProductCacheService
    participant EHR as EventHandledRepository

    rect rgb(200, 220, 255)
        note over DS,EP: Event Publishing
        DS->>EP: publishEvent(StockDepletedEvent)
        EP->>OEH: onStockDepletedEvent()
    end

    rect rgb(220, 240, 200)
        note over OEH,OB: Outbox Persistence
        OEH->>OB: save(Outbox)
        OB-->>OEH: โœ“ saved with PENDING status
    end

    rect rgb(255, 240, 200)
        note over OR,KT: Relay & Kafka Publishing
        OR->>OB: findByStatus(PENDING, pageable)
        OB-->>OR: List<Outbox>
        OR->>KT: send(ProducerRecord with headers)
        KT-->>OR: โœ“ SendResult
        OR->>OB: update(Outbox.markProcessed())
        OB-->>OR: โœ“ PROCESSED
    end

    rect rgb(240, 200, 220)
        note over KC,EHR: Consumer Processing & Idempotency
        KC->>EHR: existsByEventId(eventId)
        EHR-->>KC: false (new event)
        KC->>PMR: findByProductId(productId)
        PMR-->>KC: Optional<ProductMetrics>
        KC->>PMR: save(metrics.updateLikeIfNewer(...))
        KC->>PCS: invalidate(productId)
        PCS-->>KC: โœ“ cache cleared
        KC->>EHR: save(EventHandled.create(eventId))
        EHR-->>KC: โœ“ idempotency tracked
    end
Loading

Estimated code review effort

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

Areas requiring extra attention:

  • CatalogEventConsumer: Multi-scenario event processing logic with branching paths for different event types, idempotency checks, and exception handling; verify correct header extraction and payload parsing.
  • OutboxRelay: Partition key derivation logic from JSON payload with fallback behavior; validate error handling when Kafka send fails and ensure transactionality of Outbox status updates.
  • ProductMetrics.updateLikeIfNewer(): Timestamp-based conditional update logic with state transitions (like/unlike toggling); ensure edge cases around first events and concurrent updates are handled correctly.
  • CatalogEventConsumer & EventHandledRepository interaction: Verify idempotency guarantees; check for race conditions between existence checks and saves in concurrent scenarios.
  • Outbox transaction boundaries: Confirm that OutboxEventHandler runs before transaction commit as intended, and that OutboxRelay's Outbox status updates are atomic with respect to Kafka publishing.

Possibly related PRs

  • PR #127: Introduces ProductCacheService and cache invalidation in OrderFacade; this PR consumes StockDepletedEvent and invalidates product caches via ProductCacheService.
  • PR #172: Implements outbox/event-driven integration with outbox entities, event publishing, relay components, and Kafka wiring patterns similar to this PR.
  • PR #115: Refactors ProductDomainService.decreaseStock with locking; this PR enhances the same service to publish StockDepletedEvent on stock depletion.

Suggested labels

enhancement, event-driven-architecture, kafka, outbox-pattern

Poem

๐Ÿฐ Events now flow through outbox gates,
Kafka relays what awaits,
Stock depleted, metrics counted bright,
Idempotent, replay-safe, right!
Outbox pattern's distributed might! ๐Ÿ“ฆโœจ

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 1.23% 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 ์ œ๋ชฉ์ด Kafka๋ฅผ ํ™œ์šฉํ•œ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ ๊ตฌํ˜„์ด๋ผ๋Š” ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์š”์•ฝํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
Description check โœ… Passed PR ์„ค๋ช…์ด ์š”๊ตฌ๋˜๋Š” ํ…œํ”Œ๋ฆฟ์˜ ์ฃผ์š” ์„น์…˜(Summary, Review Points, Checklist, References)์„ ๋ชจ๋‘ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๊ตฌ์ฒด์ ์ธ ๊ตฌํ˜„ ๋‚ด์šฉ๊ณผ ๊ฒ€ํ†  ํฌ์ธํŠธ๊ฐ€ ์ถฉ๋ถ„ํžˆ ์ž‘์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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.

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: 5

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (1)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java (1)

52-70: @transactional ์• ๋…ธํ…Œ์ด์…˜ ๋ˆ„๋ฝ์œผ๋กœ ์ธํ•œ ํŠธ๋žœ์žญ์…˜ ์ผ๊ด€์„ฑ ๋ฌธ์ œ

decreaseStock ๋ฉ”์„œ๋“œ์— @Transactional ์• ๋…ธํ…Œ์ด์…˜์ด ์—†์–ด์„œ transactional outbox ํŒจํ„ด์˜ ๋ณด์žฅ์ด ๊นจ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ์ :

  • Line 66์—์„œ eventPublisher.publishEvent()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด OutboxEventHandler๊ฐ€ ์ฆ‰์‹œ ์‹คํ–‰๋˜์–ด outbox ์—”ํŠธ๋ฆฌ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค
  • ํ•˜์ง€๋งŒ ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„๊ฐ€ ์•„๋‹ˆ๋ฏ€๋กœ, ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์˜ outbox ์ €์žฅ๊ณผ ์žฌ๊ณ  ๊ฐ์†Œ๊ฐ€ ๋ณ„๋„์˜ ํŠธ๋žœ์žญ์…˜์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค
  • ์žฌ๊ณ  ๊ฐ์†Œ๋Š” ์„ฑ๊ณตํ–ˆ์ง€๋งŒ outbox ์ €์žฅ์ด ์‹คํŒจํ•˜๊ฑฐ๋‚˜, ๊ทธ ๋ฐ˜๋Œ€์˜ ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

์ฐธ๊ณ : Line 75์˜ increaseStock ๋ฉ”์„œ๋“œ๋Š” @Transactional์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •์•ˆ
 /**
  * ์žฌ๊ณ  ์ฐจ๊ฐ
  */
+@Transactional
 public Product decreaseStock(Long productId, Long quantity) {
     Product product = getProductWithLock(productId);

     if (!product.hasEnoughStock(quantity)) {
๐Ÿงน Nitpick comments (7)
docker-compose.yml (1)

61-61: ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” replication factor๋ฅผ ์กฐ์ •ํ•˜์„ธ์š”.

KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1์€ ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์— ์ ํ•ฉํ•˜์ง€๋งŒ, ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ตœ์†Œ 3์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ๋ฐ์ดํ„ฐ ๋‚ด๊ตฌ์„ฑ๊ณผ ๊ณ ๊ฐ€์šฉ์„ฑ์„ ๋ณด์žฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/infrastructure/idempotent/EventHandledTest.java (1)

10-17: ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ํ™•์žฅ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

๊ธฐ๋ณธ์ ์ธ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ๋Š” ์ž˜ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ–ฅํ›„ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ๋ฅผ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”:

  • null ๋˜๋Š” ๋นˆ ๋ฌธ์ž์—ด eventId ์ฒ˜๋ฆฌ
  • ์ค‘๋ณต eventId ์ €์žฅ ์‹œ๋„ (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ œ์•ฝ ์กฐ๊ฑด ๊ฒ€์ฆ)
  • handledAt ํƒ€์ž„์Šคํƒฌํ”„์˜ ์ •ํ™•์„ฑ

ํ˜„์žฌ ๊ตฌํ˜„์œผ๋กœ๋„ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ, ์—ฃ์ง€ ์ผ€์ด์Šค ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋†’์ด๋ฉด ๋” ๊ฒฌ๊ณ ํ•œ idempotency ๋ณด์žฅ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java (1)

56-62: ์ง๋ ฌํ™” ์‹คํŒจ ์‹œ ๋” ๊ตฌ์ฒด์ ์ธ ์˜ˆ์™ธ ํƒ€์ž… ์‚ฌ์šฉ ๊ณ ๋ ค.

ํ˜„์žฌ ๊ตฌํ˜„์€ ์ •์ƒ ๋™์ž‘ํ•˜์ง€๋งŒ, ๋” ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ์ถ”์ ์„ ์œ„ํ•ด ์ปค์Šคํ…€ ์˜ˆ์™ธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

๐Ÿ”Ž ์˜ˆ์™ธ ํƒ€์ž… ๊ฐœ์„  ์ œ์•ˆ
 private String toJson(Object event) {
     try {
         return objectMapper.writeValueAsString(event);
     } catch (JsonProcessingException e) {
-        throw new RuntimeException("Failed to serialize event", e);
+        throw new IllegalStateException("Failed to serialize event: " + event.getClass().getSimpleName(), e);
     }
 }
apps/commerce-api/src/main/java/com/loopers/domain/product/event/StockDepletedEvent.java (1)

10-13: productId์— ๋Œ€ํ•œ null ๊ฒ€์ฆ ์ถ”๊ฐ€ ๊ณ ๋ ค.

๋„๋ฉ”์ธ ์ด๋ฒคํŠธ์˜ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด ์ƒ์„ฑ์ž์—์„œ null ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž null ๊ฒ€์ฆ ์ถ”๊ฐ€ ์ œ์•ˆ
 private StockDepletedEvent(Long productId) {
+    if (productId == null) {
+        throw new IllegalArgumentException("productId must not be null");
+    }
     this.productId = productId;
     this.occurredAt = LocalDateTime.now();
 }
apps/commerce-api/src/main/java/com/loopers/infrastructure/idempotent/EventHandled.java (1)

20-21: eventId ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค ์ถ”๊ฐ€๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

EventHandledRepository.existsByEventId()๊ฐ€ ์ž์ฃผ ํ˜ธ์ถœ๋  ๊ฒƒ์ด๋ฏ€๋กœ, eventId ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์กฐํšŒ ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค. unique = true๊ฐ€ ์ผ๋ถ€ DB์—์„œ ์ž๋™์œผ๋กœ ์ธ๋ฑ์Šค๋ฅผ ์ƒ์„ฑํ•˜์ง€๋งŒ, ๋ช…์‹œ์ ์œผ๋กœ ์„ ์–ธํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ธ๋ฑ์Šค ์ถ”๊ฐ€ ์ œ์•ˆ
 @Entity
-@Table(name = "event_handled")
+@Table(name = "event_handled", indexes = {
+    @Index(name = "idx_event_handled_event_id", columnList = "eventId")
+})
 public class EventHandled {
apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/CatalogEventConsumerTest.java (1)

99-120: consumeTest4์˜ ํ…Œ์ŠคํŠธ ์˜๋„๋ฅผ ๋” ๋ช…ํ™•ํžˆ ํ•˜๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ํ…Œ์ŠคํŠธ์—์„œ newRecord๊ฐ€ ๋จผ์ € ์ฒ˜๋ฆฌ๋˜๋ฉด metrics๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , ์ดํ›„ oldRecord๋Š” ์‹œ๊ฐ„ ๋น„๊ต๋กœ ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ mock ์„ค์ •์—์„œ productMetricsRepository.findByProductId(1L)๊ฐ€ ์ฒซ ํ˜ธ์ถœ์— Optional.empty()๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ๋‘ ๋ฒˆ์งธ ํ˜ธ์ถœ์— ๋ฏธ๋ฆฌ ์—…๋ฐ์ดํŠธ๋œ metrics๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ, ์ด๋Š” ์ฒซ ๋ฒˆ์งธ ํ˜ธ์ถœ์—์„œ ์ €์žฅ๋œ ๊ฐ์ฒด๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.

์‹ค์ œ ๋™์ž‘์„ ๋” ์ •ํ™•ํžˆ ๋ฐ˜์˜ํ•˜๋ ค๋ฉด ArgumentCaptor๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ mock ์„ค์ •์„ ์กฐ์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/Outbox.java (1)

14-35: ์—”ํ‹ฐํ‹ฐ ์„ค๊ณ„ ์ ์ ˆ, ์ปฌ๋Ÿผ ์ œ์•ฝ ์ถ”๊ฐ€ ๊ณ ๋ ค

Outbox ์—”ํ‹ฐํ‹ฐ ์„ค๊ณ„๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ, ์•„๋ž˜ ์‚ฌํ•ญ์„ ์„ ํƒ์ ์œผ๋กœ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ์ปฌ๋Ÿผ ๊ธธ์ด ๋ช…์‹œ: aggregateType, aggregateId, eventType, topic ํ•„๋“œ์— @Column(length = ...)๋ฅผ ์ง€์ •ํ•˜๋ฉด DB ์Šคํ‚ค๋งˆ๊ฐ€ ๋” ๋ช…ํ™•ํ•ด์ง‘๋‹ˆ๋‹ค.
  2. ๋‚™๊ด€์  ๋ฝํ‚น: ๋‹ค์ค‘ ์ธ์Šคํ„ด์Šค ํ™˜๊ฒฝ์—์„œ ๋™์ผ ๋ ˆ์ฝ”๋“œ ๋™์‹œ ์—…๋ฐ์ดํŠธ ๊ฐ์ง€๋ฅผ ์œ„ํ•ด @Version ํ•„๋“œ๋ฅผ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹จ, Consumer ์ธก idempotency๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ ํ˜„์žฌ ๊ตฌ์กฐ์—์„œ๋Š” ํ•„์ˆ˜๋Š” ์•„๋‹™๋‹ˆ๋‹ค.
๐Ÿ”Ž ์„ ํƒ์  ๊ฐœ์„ ์•ˆ
+    @Version
+    private Long version;
+
+    @Column(length = 50)
     private String aggregateType;
+    @Column(length = 100)
     private String aggregateId;
+    @Column(length = 100)
     private String eventType;
+    @Column(length = 100)
     private String topic;
๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between e0eb51f and 761204c.

๐Ÿ“’ Files selected for processing (23)
  • apps/commerce-api/build.gradle.kts (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java (3 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/product/event/StockDepletedEvent.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/CatalogEventConsumer.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/idempotent/EventHandled.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/idempotent/EventHandledRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetrics.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/Outbox.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxRelay.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxStatus.java (1 hunks)
  • apps/commerce-api/src/main/resources/application.yml (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/CatalogEventConsumerTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/idempotent/EventHandledTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/metrics/ProductMetricsTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventHandlerTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxRelayTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxTest.java (1 hunks)
  • apps/commerce-api/src/test/resources/application.yml (1 hunks)
  • docker-compose.yml (1 hunks)
  • modules/kafka/src/main/resources/kafka.yml (1 hunks)
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (5)
๐Ÿ““ 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.
๐Ÿ“š Learning: 2025-12-18T01:01:55.894Z
Learnt from: jikimee64
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 65
File: modules/kafka/src/main/resources/kafka.yml:25-32
Timestamp: 2025-12-18T01:01:55.894Z
Learning: In Spring Boot Kafka configuration YAML, properties inside spring.kafka.producer.properties and spring.kafka.consumer.properties maps must use exact Kafka client property names with dot notation, and must be quoted with bracket notation like "[enable.idempotence]": true and "[enable.auto.commit]": false to prevent YAML from parsing dots as nested keys. Spring Boot's relaxed binding only applies to top-level Spring Kafka properties, not to the properties map.

Applied to files:

  • apps/commerce-api/src/main/resources/application.yml
  • apps/commerce-api/src/test/resources/application.yml
  • modules/kafka/src/main/resources/kafka.yml
๐Ÿ“š 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-api/src/main/java/com/loopers/infrastructure/consumer/CatalogEventConsumer.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/CatalogEventConsumerTest.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.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/infrastructure/metrics/ProductMetrics.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/metrics/ProductMetricsTest.java
๐Ÿ“š Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxRepository.java
๐Ÿงฌ Code graph analysis (6)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/product/event/StockDepletedEvent.java (1)
  • StockDepletedEvent (5-26)
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/CatalogEventConsumer.java (2)
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxRelay.java (1)
  • Slf4j (16-69)
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java (1)
  • Component (13-63)
apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventHandlerTest.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/like/event/ProductLikedEvent.java (1)
  • ProductLikedEvent (5-42)
apps/commerce-api/src/main/java/com/loopers/domain/order/event/OrderCompletedEvent.java (1)
  • OrderCompletedEvent (7-52)
apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/CatalogEventConsumerTest.java (1)
apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxRelayTest.java (1)
  • ExtendWith (27-102)
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/like/event/ProductLikedEvent.java (1)
  • ProductLikedEvent (5-42)
apps/commerce-api/src/main/java/com/loopers/domain/order/event/OrderCompletedEvent.java (1)
  • OrderCompletedEvent (7-52)
apps/commerce-api/src/main/java/com/loopers/domain/product/event/StockDepletedEvent.java (1)
  • StockDepletedEvent (5-26)
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxRelay.java (2)
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/CatalogEventConsumer.java (1)
  • Slf4j (23-108)
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java (1)
  • Component (13-63)
๐Ÿ”‡ Additional comments (26)
modules/kafka/src/main/resources/kafka.yml (2)

17-18: ํ”„๋กœ๋“€์„œ ์•ˆ์ •์„ฑ ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

acks: all๊ณผ retries: 3 ์„ค์ •์€ ๋ฉ”์‹œ์ง€ ์†์‹ค ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์ ์ ˆํ•œ ๊ตฌ์„ฑ์ž…๋‹ˆ๋‹ค. idempotence์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์ •ํ™•ํžˆ ํ•œ ๋ฒˆ(exactly-once) ์ „์†ก ์‹œ๋งจํ‹ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.


24-24: ByteArrayDeserializer ์„ค์ •์€ ByteArrayJsonMessageConverter์™€ ํ•จ๊ป˜ ์ž‘๋™ํ•˜๋ฉฐ ์ž๋™์œผ๋กœ JSON ์—ญ์ง๋ ฌํ™”๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ปจ์Šˆ๋จธ๋Š” ์›์‹œ ๋ฐ”์ดํŠธ ๋ฐฐ์—ด์ด ์•„๋‹Œ String ํ˜•ํƒœ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•˜๋ฏ€๋กœ ์ถ”๊ฐ€ ์„ค์ •์ด ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

apps/commerce-api/src/test/resources/application.yml (1)

8-8: LGTM!

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์— Kafka ์„ค์ •์„ ์ž„ํฌํŠธํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/resources/application.yml (1)

24-24: LGTM!

๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ •์— Kafka ์„ค์ •์„ ์ž„ํฌํŠธํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/build.gradle.kts (1)

5-5: LGTM!

Kafka ๋ชจ๋“ˆ ์˜์กด์„ฑ ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•˜๋ฉฐ, ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ์™€ ์ผ๊ด€์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxStatus.java (1)

3-6: LGTM!

Outbox ํŒจํ„ด์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ๊ฐ„๊ฒฐํ•˜๊ณ  ๋ช…ํ™•ํ•œ enum ์ •์˜์ž…๋‹ˆ๋‹ค. PENDING๊ณผ PROCESSED ๋‘ ๊ฐ€์ง€ ์ƒํƒœ๋กœ transactional outbox ํŒจํ„ด์„ ์ถฉ๋ถ„ํžˆ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/product/ProductDomainService.java (1)

3-3: LGTM!

ApplicationEventPublisher๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฃผ์ž…ํ•˜์—ฌ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

Also applies to: 8-8, 20-20

apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxTest.java (1)

12-64: LGTM!

Outbox ์—”ํ‹ฐํ‹ฐ์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ํฌ๊ด„์ ์œผ๋กœ ๊ฒ€์ฆํ•˜๋Š” ์šฐ์ˆ˜ํ•œ ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค:

  • ์ƒ์„ฑ ์‹œ PENDING ์ƒํƒœ ์ดˆ๊ธฐํ™”
  • markProcessed() ํ˜ธ์ถœ ์‹œ PROCESSED ์ƒํƒœ ์ „ํ™˜ ๋ฐ processedAt ํƒ€์ž„์Šคํƒฌํ”„ ์„ค์ •
  • ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ: ์ค‘๋ณต markProcessed() ํ˜ธ์ถœ ์‹œ์—๋„ ์ตœ์ดˆ processedAt ์œ ์ง€

Transactional outbox ํŒจํ„ด์˜ ํ•ต์‹ฌ ๋™์ž‘์„ ์ž˜ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventHandlerTest.java (1)

29-53: LGTM!

ProductLikedEvent๊ฐ€ Outbox์— ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ €์žฅ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ์ž˜ ์ž‘์„ฑ๋œ ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค. ObjectMapper ์„ค์ •(JavaTimeModule, WRITE_DATES_AS_TIMESTAMPS ๋น„ํ™œ์„ฑํ™”)์ด ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ๊ณผ ์ผ์น˜ํ•˜๋ฉฐ, ๋ชจ๋“  ์ค‘์š” ํ•„๋“œ(aggregateType, aggregateId, eventType, topic, status)๋ฅผ ์ ์ ˆํžˆ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepository.java (1)

7-10: LGTM!

ProductMetrics๋ฅผ productId๋กœ ์กฐํšŒํ•˜๋Š” ๊ฐ„๊ฒฐํ•˜๊ณ  ๋ช…ํ™•ํ•œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. Spring Data JPA์˜ ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜์„ ์ž˜ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/idempotent/EventHandledRepository.java (1)

5-8: LGTM!

์ด๋ฒคํŠธ ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ๋ฅผ ์œ„ํ•œ ํšจ์œจ์ ์ธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. existsByEventId ๋ฉ”์„œ๋“œ๋Š” boolean ๋ฐ˜ํ™˜์œผ๋กœ ๋ถˆํ•„์š”ํ•œ ์—”ํ‹ฐํ‹ฐ ๋กœ๋”ฉ ์—†์ด ์ค‘๋ณต ์ฒ˜๋ฆฌ๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxRelayTest.java (4)

54-76: ํฌ๊ด„์ ์ธ ์„ฑ๊ณต ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.

PENDING ์ƒํƒœ์˜ Outbox๊ฐ€ Kafka๋กœ ๋ฐœํ–‰๋˜๊ณ  PROCESSED๋กœ ์ „ํ™˜๋˜๋Š” ์ „์ฒด ํ๋ฆ„์„ ์ž˜ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค:

  • Kafka ProducerRecord์˜ topic, key, value ๊ฒ€์ฆ
  • HEADER_OUTBOX_ID, HEADER_EVENT_TYPE ํ—ค๋” ์กด์žฌ ํ™•์ธ
  • ์ƒํƒœ ์ „ํ™˜ ๋ฐ ์ €์žฅ ํ˜ธ์ถœ ๊ฒ€์ฆ

Line 69์˜ partition key ํ˜•์‹ "1:100" (aggregateId:userId)์€ ์ด๋ฒคํŠธ ์ˆœ์„œ ๋ณด์žฅ์„ ์œ„ํ•œ ์ข‹์€ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.


78-87: LGTM!

PENDING ์ƒํƒœ์˜ Outbox๊ฐ€ ์—†์„ ๋•Œ ๋ถˆํ•„์š”ํ•œ Kafka ํ˜ธ์ถœ์„ ํ•˜์ง€ ์•Š๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ์ค‘์š”ํ•œ ์—ฃ์ง€ ์ผ€์ด์Šค์ž…๋‹ˆ๋‹ค.


89-101: ์‹ ๋ขฐ์„ฑ ๋ณด์žฅ์„ ์œ„ํ•œ ์ค‘์š”ํ•œ ์‹คํŒจ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.

Kafka ๋ฐœํ–‰ ์‹คํŒจ ์‹œ Outbox ์ƒํƒœ๋ฅผ PENDING์œผ๋กœ ์œ ์ง€ํ•˜์—ฌ ๋‹ค์Œ relay ์‚ฌ์ดํด์—์„œ ์žฌ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. At-least-once delivery ํŒจํ„ด์˜ ํ•ต์‹ฌ์ ์ธ ๋™์ž‘์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.


69-69: partition key ์ƒ์„ฑ ๋กœ์ง์€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

OutboxRelay์˜ buildPartitionKey() ๋ฉ”์„œ๋“œ๊ฐ€ ๋‹ค์–‘ํ•œ ์ด๋ฒคํŠธ ํƒ€์ž…์˜ payload ๊ตฌ์กฐ์—์„œ partition key๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค:

  • ์ •์ƒ ํ๋ฆ„: userId๊ฐ€ payload JSON์— ์กด์žฌํ•˜๋ฉด aggregateId:userId ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ (์˜ˆ: "1:100")
  • StockDepletedEvent (userId ์—†์Œ): userId๊ฐ€ null์ด๋ฉด aggregateId๋งŒ ๋ฐ˜ํ™˜
  • ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ: ๋ชจ๋“  ์˜ˆ์™ธ๋ฅผ catchํ•˜๊ณ  aggregateId๋กœ fallback

๊ตฌํ˜„๋œ fallback ๋กœ์ง์œผ๋กœ ์ธํ•ด event type๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ์•ˆ์ „ํ•˜๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxRepository.java (1)

8-11: LGTM!

๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Spring Data JPA ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ ๊ทœ์น™์„ ์ž˜ ๋”ฐ๋ฅด๊ณ  ์žˆ์œผ๋ฉฐ, Pageable ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ†ตํ•ด OutboxRelay์—์„œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventHandler.java (1)

20-30: LGTM - Transactional Outbox ํŒจํ„ด์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

TransactionPhase.BEFORE_COMMIT์„ ์‚ฌ์šฉํ•˜์—ฌ ๋น„์ฆˆ๋‹ˆ์Šค ํŠธ๋žœ์žญ์…˜๊ณผ Outbox ์ €์žฅ์ด ์›์ž์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ด๋ฒคํŠธ ์œ ์‹ค์„ ๋ฐฉ์ง€ํ•˜๋Š” ํ•ต์‹ฌ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/infrastructure/metrics/ProductMetricsTest.java (1)

10-123: LGTM - ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ์ž˜ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

ProductMetrics์˜ ํ•ต์‹ฌ ๋™์ž‘๋“ค์ด ์ž˜ ํ…Œ์ŠคํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

  • ์ดˆ๊ธฐํ™”, ์ฆ๊ฐ€/๊ฐ์†Œ, ๊ฒฝ๊ณ„๊ฐ’(0 ๋ฏธ๋งŒ ๋ฐฉ์ง€)
  • ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ˆœ์„œ ์ฒ˜๋ฆฌ (updateLikeIfNewer)
  • ์ข‹์•„์š”/์ทจ์†Œ ์‹œ๋‚˜๋ฆฌ์˜ค

ํŠนํžˆ updateLikeIfNewerTest2์—์„œ ์˜ค๋ž˜๋œ ์ด๋ฒคํŠธ ๋ฌด์‹œ ๋กœ์ง ๊ฒ€์ฆ์ด ์ž˜ ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/infrastructure/consumer/CatalogEventConsumerTest.java (1)

137-156: LGTM - ๋ฉฑ๋“ฑ์„ฑ ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

๋™์ผํ•œ eventId๋กœ ์ค‘๋ณต ๋ฉ”์‹œ์ง€๊ฐ€ ์ „์†ก๋  ๋•Œ ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ๋˜๊ณ , ์–‘์ชฝ ๋ชจ๋‘ acknowledge๋˜๋Š” ๊ฒƒ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/CatalogEventConsumer.java (1)

103-107: LGTM - ์žฌ๊ณ  ์†Œ์ง„ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์บ์‹œ ๋ฌดํšจํ™” ๋กœ์ง์ด ๋ช…ํ™•ํ•˜๊ณ , ์ƒํ’ˆ ์ƒ์„ธ ๋ฐ ๋ชฉ๋ก ์บ์‹œ๋ฅผ ๋ชจ๋‘ ๋ฌดํšจํ™”ํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxRelay.java (2)

29-54: ๋™์‹œ ์‹คํ–‰ ์‹œ ์ค‘๋ณต ๋ฐœํ–‰ ๊ฐ€๋Šฅ์„ฑ ๊ฒ€ํ†  ํ•„์š”

relay() ๋ฉ”์„œ๋“œ์— @Transactional์ด ์—†์–ด์„œ, ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋™์‹œ์— ์‹คํ–‰๋  ๊ฒฝ์šฐ ๊ฐ™์€ PENDING ๋ ˆ์ฝ”๋“œ๋ฅผ ์ค‘๋ณต์œผ๋กœ ๊ฐ€์ ธ์™€ ๋ฐœํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Consumer ์ธก์—์„œ idempotency ์ฒ˜๋ฆฌ(eventId ๊ธฐ๋ฐ˜)๊ฐ€ ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ๊ธฐ๋Šฅ์ƒ ๋ฌธ์ œ๋Š” ์—†์ง€๋งŒ, ๋ถˆํ•„์š”ํ•œ ์ค‘๋ณต ๋ฐœํ–‰์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ, ๋ฐœํ–‰ ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ œํ•œ์ด๋‚˜ DLQ(Dead Letter Queue) ์ฒ˜๋ฆฌ๊ฐ€ ์—†์–ด์„œ ์˜๊ตฌ์ ์œผ๋กœ ์‹คํŒจํ•˜๋Š” ๋ฉ”์‹œ์ง€๊ฐ€ ๋ฌดํ•œ ์žฌ์‹œ๋„๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

PR objectives์—์„œ "Operational concerns (DLQ, monitoring, traceId propagation) are acknowledged but currently only at a conceptual level"๋กœ ์–ธ๊ธ‰๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ, ํ˜„์žฌ ๋ฒ”์œ„์—์„œ๋Š” ์ธ์ง€๋œ ์ œํ•œ์‚ฌํ•ญ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ํ–ฅํ›„ ๊ฐœ์„  ์‹œ ๋‹ค์Œ ์‚ฌํ•ญ์„ ๊ณ ๋ คํ•˜์„ธ์š”:

  • SELECT ... FOR UPDATE SKIP LOCKED๋ฅผ ์‚ฌ์šฉํ•œ ๋ถ„์‚ฐ ๋ฝ
  • ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ํ•„๋“œ ์ถ”๊ฐ€ ๋ฐ ์ž„๊ณ„๊ฐ’ ์ดˆ๊ณผ ์‹œ FAILED ์ƒํƒœ๋กœ ์ „ํ™˜

57-68: LGTM!

ํŒŒํ‹ฐ์…˜ ํ‚ค ์ƒ์„ฑ ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. userId๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ aggregateId์™€ ์กฐํ•ฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๋ณ„ ์ˆœ์„œ ๋ณด์žฅ์„ ๊ฐ•ํ™”ํ•˜๊ณ , ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ aggregateId๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ํด๋ฐฑํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/Outbox.java (1)

49-59: LGTM!

ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์™€ markProcessed() ๊ตฌํ˜„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ markProcessed()๊ฐ€ idempotentํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์ค‘๋ณต ํ˜ธ์ถœ์—๋„ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/metrics/ProductMetrics.java (3)

57-68: ํƒ€์ž„์Šคํƒฌํ”„ ๊ธฐ๋ฐ˜ ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ ์ž˜ ๊ตฌํ˜„๋จ

updateLikeIfNewer ๋ฉ”์„œ๋“œ๊ฐ€ ์ด๋ฒคํŠธ ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ๋น„๊ตํ•˜์—ฌ ์ˆœ์„œ๊ฐ€ ๋’ค๋ฐ”๋€ ์ด๋ฒคํŠธ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฌด์‹œํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” Kafka ํŒŒํ‹ฐ์…˜ ๊ฐ„ ์ˆœ์„œ ๋ณด์žฅ์ด ์•ˆ ๋˜๋Š” ์ƒํ™ฉ์—์„œ ํšจ๊ณผ์ ์ธ ๋ฐฉ์–ด ๋กœ์ง์ž…๋‹ˆ๋‹ค.


16-26: ์—”ํ‹ฐํ‹ฐ ๊ตฌ์กฐ ์ ์ ˆํ•จ

productId์— unique constraint๊ฐ€ ์ ์ ˆํžˆ ์„ค์ •๋˜์–ด ์žˆ๊ณ , ํ•„๋“œ ์ดˆ๊ธฐํ™”๊ฐ€ ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

๋™์‹œ์„ฑ ์ œ์–ด๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ @Version ํ•„๋“œ ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


42-55: Kafka ํŒŒํ‹ฐ์…˜ ํ‚ค ์ „๋žต์œผ๋กœ ์ธํ•œ ๋™์‹œ์„ฑ ๋ณดํ˜ธ - ์ถ”๊ฐ€ ๋ฝ ๋ถˆํ•„์š”

incrementLikeCount, decrementLikeCount, addOrder ๋ฉ”์„œ๋“œ๋Š” ์ง์ ‘ ์นด์šดํ„ฐ๋ฅผ ์ฆ๊ฐํ•˜์ง€๋งŒ, ์‹ค์ œ๋กœ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

OutboxRelay๋Š” aggregateId(productId)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํŒŒํ‹ฐ์…˜ ํ‚ค๋ฅผ ์ƒ์„ฑํ•˜๋ฏ€๋กœ, ๋™์ผํ•œ productId์— ๋Œ€ํ•œ ๋ชจ๋“  ์ด๋ฒคํŠธ๋Š” ํ•ญ์ƒ ๋™์ผํ•œ Kafka ํŒŒํ‹ฐ์…˜์œผ๋กœ ๋ผ์šฐํŒ…๋ฉ๋‹ˆ๋‹ค. CatalogEventConsumer๋Š” ๋‹จ์ผ groupId="catalog-consumer"๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, Kafka์˜ ์†Œ๋น„์ž ๊ทธ๋ฃน ๋ชจ๋ธ์— ์˜ํ•ด ๊ฐ ํŒŒํ‹ฐ์…˜์€ ์ •ํ™•ํžˆ ํ•˜๋‚˜์˜ ์†Œ๋น„์ž ์ธ์Šคํ„ด์Šค์— ์˜ํ•ด ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๊ฐ™์€ productId์˜ ProductMetrics ์—”ํ‹ฐํ‹ฐ๋Š” ๋™์‹œ์— ์—…๋ฐ์ดํŠธ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

ProductMetrics๋Š” ๋น„์ •๊ทœํ™”๋œ ๋ฉ”ํŠธ๋ฆญ ํ…Œ์ด๋ธ”์ด๋ฉฐ, ์‹ค์ œ ์ข‹์•„์š” ๋ฐ์ดํ„ฐ์˜ ์›๋ณธ์ธ Product ์—”ํ‹ฐํ‹ฐ๋Š” ๋น„๊ด€์  ๋ฝ์œผ๋กœ ๋ณดํ˜ธ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์•„ํ‚คํ…์ฒ˜๋Š” ์˜๋„์ ์œผ๋กœ metrics์— ๋Œ€ํ•ด ์ตœ์ข… ์ผ๊ด€์„ฑ์„ ํ—ˆ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ, ProductMetricsRepository์— ์ถ”๊ฐ€ ๋ฝ์„ ๊ตฌํ˜„ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

Comment on lines +33 to +58
@KafkaListener(topics = "catalog-events", groupId = "catalog-consumer")
@Transactional
public void consume(ConsumerRecord<String, String> record, Acknowledgment acknowledgment) {
String eventId = extractEventId(record);
if (eventId == null) {
log.warn("outbox-id ํ—ค๋”๊ฐ€ ์—†๋Š” ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ : {}", record);
acknowledgment.acknowledge();
return;
}

if (eventHandledRepository.existsByEventId(eventId)) {
log.info("์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ์ด๋ฒคํŠธ ๋ฌด์‹œ: eventId={}", eventId);
acknowledgment.acknowledge();
return;
}

try {
String eventType = extractEventType(record);
processEvent(eventType, record.value());
eventHandledRepository.save(EventHandled.create(eventId));
acknowledgment.acknowledge();
log.info("์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ: eventId={}, eventType={}", eventId, eventType);
} catch (Exception e) {
log.error("์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์‹คํŒจ, ์žฌ์ฒ˜๋ฆฌ ์˜ˆ์ •: eventId={}", eventId, e);
}
}
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

ํŠธ๋žœ์žญ์…˜๊ณผ acknowledgment ์ˆœ์„œ์— ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„์—์„œ @Transactional ๋‚ด์—์„œ acknowledgment.acknowledge()๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ acknowledge ํ›„ ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์ „์— ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด, ๋ฉ”์‹œ์ง€๋Š” ack๋˜์—ˆ์ง€๋งŒ DB์—๋Š” ์ €์žฅ๋˜์ง€ ์•Š์•„ ๋ฉ”์‹œ์ง€๊ฐ€ ์œ ์‹ค๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ, ๋™์‹œ์— ๊ฐ™์€ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ existsByEventId ์ฒดํฌ์™€ save ์‚ฌ์ด์— race condition์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. EventHandled์˜ unique ์ œ์•ฝ์กฐ๊ฑด์œผ๋กœ ์ธํ•ด DataIntegrityViolationException์ด ๋ฐœ์ƒํ•˜๊ณ , ์ด๋Š” ์žฌ์ฒ˜๋ฆฌ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค (์•ˆ์ „ํ•˜์ง€๋งŒ ๋ช…์‹œ์  ์ฒ˜๋ฆฌ๊ฐ€ ์ข‹์Šต๋‹ˆ๋‹ค).

๐Ÿ”Ž ๊ฐœ์„  ์ œ์•ˆ
 @KafkaListener(topics = "catalog-events", groupId = "catalog-consumer")
 @Transactional
 public void consume(ConsumerRecord<String, String> record, Acknowledgment acknowledgment) {
     // ... existing code ...
     
     try {
         String eventType = extractEventType(record);
         processEvent(eventType, record.value());
         eventHandledRepository.save(EventHandled.create(eventId));
-        acknowledgment.acknowledge();
         log.info("์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ: eventId={}, eventType={}", eventId, eventType);
+    } catch (DataIntegrityViolationException e) {
+        log.info("์ด๋ฏธ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์—์„œ ์ฒ˜๋ฆฌ ์ค‘์ธ ์ด๋ฒคํŠธ: eventId={}", eventId);
     } catch (Exception e) {
         log.error("์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์‹คํŒจ, ์žฌ์ฒ˜๋ฆฌ ์˜ˆ์ •: eventId={}", eventId, e);
+        throw e; // rethrow to prevent ack
     }
+    acknowledgment.acknowledge();
 }

Comment on lines +76 to +89
private void processEvent(String eventType, String payload) {
try {
JsonNode node = objectMapper.readTree(payload);
Long productId = node.get("productId").asLong();

switch (eventType) {
case "ProductLikedEvent" -> processProductLikedEvent(node, productId);
case "StockDepletedEvent" -> processStockDepletedEvent(productId);
default -> log.warn("์•Œ ์ˆ˜ ์—†๋Š” ์ด๋ฒคํŠธ ํƒ€์ž…: {}", eventType);
}
} catch (Exception e) {
throw new RuntimeException("์ด๋ฒคํŠธ ํŒŒ์‹ฑ ์‹คํŒจ", e);
}
}
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 ์•ˆ์ „์„ฑ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

node.get() ๋ฉ”์„œ๋“œ๋Š” ํ•„๋“œ๊ฐ€ ์—†์œผ๋ฉด null์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ํ˜•์‹์˜ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ๋“ค์–ด์˜ค๋ฉด NullPointerException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž null ์•ˆ์ „์„ฑ ๊ฐœ์„  ์ œ์•ˆ
 private void processEvent(String eventType, String payload) {
     try {
         JsonNode node = objectMapper.readTree(payload);
-        Long productId = node.get("productId").asLong();
+        JsonNode productIdNode = node.get("productId");
+        if (productIdNode == null || productIdNode.isNull()) {
+            log.warn("productId๊ฐ€ ์—†๋Š” ์ด๋ฒคํŠธ: {}", payload);
+            return;
+        }
+        Long productId = productIdNode.asLong();

         switch (eventType) {
             case "ProductLikedEvent" -> processProductLikedEvent(node, productId);

๋˜๋Š” path() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด MissingNode๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ๋” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค:

Long productId = node.path("productId").asLong(-1L);
if (productId == -1L) {
    log.warn("productId๊ฐ€ ์—†๋Š” ์ด๋ฒคํŠธ: {}", payload);
    return;
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/CatalogEventConsumer.java
around lines 76-89, the payload parsing uses node.get(...) which can return null
and cause NPEs; replace node.get("productId").asLong() with
node.path("productId").asLong(-1L), check for the sentinel (-1L) and log a
warning with the payload and return early if missing/invalid, and apply the same
path()/default-check pattern for any other potentially missing fields before
processing; keep the existing exception handling for unexpected errors.

Comment on lines +91 to +101
private void processProductLikedEvent(JsonNode node, Long productId) {
boolean liked = node.get("liked").asBoolean();
LocalDateTime occurredAt = LocalDateTime.parse(node.get("occurredAt").asText());

ProductMetrics metrics = productMetricsRepository.findByProductId(productId)
.orElseGet(() -> ProductMetrics.create(productId));

if (metrics.updateLikeIfNewer(liked, occurredAt)) {
productMetricsRepository.save(metrics);
}
}
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

processProductLikedEvent์—์„œ๋„ null ์ฒดํฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

liked์™€ occurredAt ํ•„๋“œ ์ ‘๊ทผ ์‹œ์—๋„ ๋™์ผํ•œ NPE ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž null ์•ˆ์ „์„ฑ ๊ฐœ์„  ์ œ์•ˆ
 private void processProductLikedEvent(JsonNode node, Long productId) {
-    boolean liked = node.get("liked").asBoolean();
-    LocalDateTime occurredAt = LocalDateTime.parse(node.get("occurredAt").asText());
+    JsonNode likedNode = node.get("liked");
+    JsonNode occurredAtNode = node.get("occurredAt");
+    if (likedNode == null || occurredAtNode == null) {
+        log.warn("ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ - liked ๋˜๋Š” occurredAt: productId={}", productId);
+        return;
+    }
+    boolean liked = likedNode.asBoolean();
+    LocalDateTime occurredAt = LocalDateTime.parse(occurredAtNode.asText());

     ProductMetrics metrics = productMetricsRepository.findByProductId(productId)
             .orElseGet(() -> ProductMetrics.create(productId));
๐Ÿ“ 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
private void processProductLikedEvent(JsonNode node, Long productId) {
boolean liked = node.get("liked").asBoolean();
LocalDateTime occurredAt = LocalDateTime.parse(node.get("occurredAt").asText());
ProductMetrics metrics = productMetricsRepository.findByProductId(productId)
.orElseGet(() -> ProductMetrics.create(productId));
if (metrics.updateLikeIfNewer(liked, occurredAt)) {
productMetricsRepository.save(metrics);
}
}
private void processProductLikedEvent(JsonNode node, Long productId) {
JsonNode likedNode = node.get("liked");
JsonNode occurredAtNode = node.get("occurredAt");
if (likedNode == null || occurredAtNode == null) {
log.warn("ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ - liked ๋˜๋Š” occurredAt: productId={}", productId);
return;
}
boolean liked = likedNode.asBoolean();
LocalDateTime occurredAt = LocalDateTime.parse(occurredAtNode.asText());
ProductMetrics metrics = productMetricsRepository.findByProductId(productId)
.orElseGet(() -> ProductMetrics.create(productId));
if (metrics.updateLikeIfNewer(liked, occurredAt)) {
productMetricsRepository.save(metrics);
}
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/consumer/CatalogEventConsumer.java
around lines 91 to 101, add null and type checks for node.get("liked") and
node.get("occurredAt") before accessing them: verify the "liked" field exists
and is a boolean (or use asBoolean(default) only if a sensible default applies),
verify the "occurredAt" field exists and is textual, wrap LocalDateTime.parse in
a try/catch to handle parse errors, and if any check fails log a warning and
return early instead of proceeding; then only call metrics.updateLikeIfNewer and
save when both values are validated and parsed successfully.

Comment on lines +43 to +63
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
container_name: loopers-zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000

kafka:
image: confluentinc/cp-kafka:7.5.0
container_name: loopers-kafka
ports:
- "19092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:19092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
depends_on:
- zookeeper
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

Confluent Platform 7.5.0์€ ์ตœ์‹  ๋ฒ„์ „์ด ์•„๋‹ˆ๋ฉฐ ๋ณด์•ˆ ํŒจ์น˜ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Docker Compose ์„ค์ •์ด ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์— ์ ํ•ฉํ•˜๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํฌํŠธ ๋งคํ•‘๊ณผ ์˜์กด์„ฑ ๊ด€๊ณ„๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋‹ค๋งŒ ํ˜„์žฌ ์ตœ์‹  Confluent Platform ๋ฒ„์ „์€ 8.1์ด๋ฉฐ, 7.9.x๋„ ์ตœ๊ทผ ์•ˆ์ •ํ™” ๋ฒ„์ „์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ ์ค‘์ธ 7.5.0์€ 2023๋…„ ๋ฒ„์ „์œผ๋กœ ์ƒ๋‹นํžˆ ์˜ค๋ž˜๋œ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค. ๋ณด์•ˆ ๊ด€์ ์—์„œ ํ•ญ์ƒ ์ตœ์‹  ํŒจ์น˜ ๋ฒ„์ „์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ตœ์†Œํ•œ 7.5.10๊ณผ ๊ฐ™์€ ์ตœ์‹  ํŒจ์น˜ ๋ฒ„์ „์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๊ฑฐ๋‚˜, ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•์€ 7.9.x ์ด์ƒ์˜ ์ตœ์‹  ๋ฒ„์ „์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In docker-compose.yml around lines 43 to 63, the Confluent images are pinned to
an outdated 7.5.0 tag; update the kafka and zookeeper image tags to a currently
supported patched version (at minimum the latest 7.5.x patch, e.g. 7.5.10) or
preferably to a more recent stable release such as 7.9.x or 8.1, then
rebuild/test the compose stack to ensure compatibility (adjust any
connector/config differences between versions if needed) and update any project
documentation/CI that references these image tags.

@jeonga1022 jeonga1022 merged commit 1160a4e into Loopers-dev-lab:jeonga1022 Dec 25, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant