Skip to content

Conversation

@rnqhstmd
Copy link
Collaborator

@rnqhstmd rnqhstmd commented Dec 19, 2025

๐Ÿ“Œ Summary

  • Kafka ์—ฐ๋™
  • kafka ๋ชจ๋“ˆ, commerce-streamer ์•ฑ ์—ฐ๋™
  • Partition ํ™•์žฅ ๋ฐ ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ ์‹œ retry, fallback ์ฒ˜๋ฆฌ ์ ์šฉ
  • ๊ฐ์‚ฌ ๋กœ๊ทธ, ์ง‘๊ณ„ ์ด๋ฒคํŠธ Kafka ์ „์†ก
  • ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์•ˆ์ •์„ฑ
  • event handled ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ
  • DLQ ์ฒ˜๋ฆฌ
  • ์žฌ์‹œ๋„ ํ›„ ์‹คํŒจํ•œ ๋ฉ”์„ธ์ง€ DLT ํ† ํ”ฝ ์ „์†ก ๋ฐ ์ €์žฅ

๐Ÿ’ฌ Review Points

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

โœ… Checklist

๐ŸŽพ Producer

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

โšพ Consumer

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

Summary by CodeRabbit

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

  • ์ƒˆ ๊ธฐ๋Šฅ

    • ์ƒํ’ˆ ์กฐํšŒ, ์ข‹์•„์š”, ์žฌ๊ณ  ๋ณ€๋™ ๋“ฑ์˜ ์ด๋ฒคํŠธ ์ถ”์  ๋ฐ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
    • ๊ฐ์‚ฌ ๋กœ๊ทธ๋ฅผ ํ†ตํ•œ ์‚ฌ์šฉ์ž ํ™œ๋™ ๊ธฐ๋ก ๊ฐœ์„ 
    • ์‹คํŒจํ•œ ์ž‘์—… ์ž๋™ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ฐ•ํ™”
  • ๋ฒ„๊ทธ ์ˆ˜์ •

    • ์ฟ ํฐ ์ค‘๋ณต ์‚ฌ์šฉ ๋ฐฉ์ง€ ๋กœ์ง ์ถ”๊ฐ€

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

โ€ฆryImpl ํด๋ž˜์Šค ์ถ”๊ฐ€ ๋ฐ ์ €์žฅ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„
โ€ฆpositoryImpl ํด๋ž˜์Šค ์ถ”๊ฐ€ ๋ฐ DLQ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„
โ€ฆdledRepositoryImpl ํด๋ž˜์Šค ์ถ”๊ฐ€ ๋ฐ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„
@rnqhstmd rnqhstmd self-assigned this Dec 19, 2025
@rnqhstmd rnqhstmd added the enhancement New feature or request label Dec 19, 2025
@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.

์š”์•ฝ

์ด PR์€ Kafka ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ฃผ๋„ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋„์ž…ํ•˜์—ฌ ์ฃผ๋ฌธ, ๊ฒฐ์ œ, ์ƒํ’ˆ ์ด๋ฒคํŠธ๋ฅผ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Outbox ํŒจํ„ด์„ ์ ์šฉํ•˜๊ณ  ์ƒˆ๋กœ์šด commerce-streamer ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ฉ”ํŠธ๋ฆญ, ๊ฐ์‚ฌ ๋กœ๊ทธ, DLQ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.


Walkthrough

commerce-api์™€ ์ƒˆ๋กœ์šด commerce-streamer ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— Kafka ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ์„ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค. Outbox ํŒจํ„ด์œผ๋กœ ์ด๋ฒคํŠธ ์ง€์†์„ฑ์„ ๋ณด์žฅํ•˜๊ณ , ์—ฌ๋Ÿฌ Kafka ํ”„๋กœ๋“€์„œ๋กœ ์ฃผ๋ฌธยท๊ฒฐ์ œยท์ƒํ’ˆ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉฐ, ์ŠคํŠธ๋ฆฌ๋จธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ฉ”ํŠธ๋ฆญ, ๊ฐ์‚ฌ ๋กœ๊ทธ, DLQ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.


Changes

๋ณ€๊ฒฝ ๋Œ€์ƒ / ํŒŒ์ผ(๋“ค) ์š”์•ฝ
Kafka ์˜์กด์„ฑ ๋ฐ ์„ค์ •
apps/commerce-api/build.gradle.kts, apps/commerce-api/src/main/resources/application.yml, modules/kafka/src/main/resources/kafka.yml, modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java
Kafka ๋ชจ๋“ˆ ๋ฐ ํ…Œ์ŠคํŠธ ์˜์กด์„ฑ ์ถ”๊ฐ€. DLQ ์ฒ˜๋ฆฌ, ์—๋Ÿฌ ํ•ธ๋“ค๋ง, ๋ฉฑ๋“ฑ์„ฑ ๋ฐ acks ์„ค์ • ๊ตฌ์„ฑ ์ถ”๊ฐ€.
Outbox ์ด๋ฒคํŠธ ์ธํ”„๋ผ
apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEvent.java, OutboxEventRepository.java, OutboxService.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventJpaRepository.java, OutboxEventRepositoryImpl.java, OutboxEventPublisher.java
Outbox ์—”ํ‹ฐํ‹ฐ, ๋ฆฌํฌ์ง€ํ† ๋ฆฌ, ์„œ๋น„์Šค, JPA ๊ตฌํ˜„ ๋ฐ ์˜ˆ์•ฝ๋œ ๋ฐœํ–‰์ž ์ถ”๊ฐ€. PENDING/PROCESSED/FAILED ์ƒํƒœ ๊ด€๋ฆฌ.
์ด๋ฒคํŠธ DTO ๋ฐ ํ”„๋กœ๋“€์„œ
apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/*Dto.java, apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/*Producer.java
OrderEventDto, PaymentEventDto, LikeChangedDto, StockChangedDto, ProductViewedDto, UserActionDto ๋ ˆ์ฝ”๋“œ ์ถ”๊ฐ€. ๊ฐ ์ด๋ฒคํŠธ ํƒ€์ž…๋ณ„ Kafka ํ”„๋กœ๋“€์„œ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (์žฌ์‹œ๋„ ์ •์ฑ… ํฌํ•จ).
์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/OrderEventListener.java, PaymentEventListener.java, ProductLikeEventListener.java, UserActionEventListener.java
์ฃผ๋ฌธ ์ƒ์„ฑ, ๊ฒฐ์ œ ์„ฑ๊ณต/์‹คํŒจ, ์ƒํ’ˆ ์ข‹์•„์š” ์ด๋ฒคํŠธ์— Outbox ๋ฐ Kafka ๋ฐœํ–‰ ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€.
๋„๋ฉ”์ธ ๋กœ์ง ์ˆ˜์ •
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java, apps/commerce-api/src/main/java/com/loopers/domain/payment/event/PaymentSucceededEvent.java
useCoupon์— ๋ฉฑ๋“ฑ์„ฑ ๊ฐ€๋“œ ์ถ”๊ฐ€. PaymentSucceededEvent์— transactionId ํ•„๋“œ ์ถ”๊ฐ€.
commerce-streamer ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ - ๋„๋ฉ”์ธ
apps/commerce-streamer/src/main/java/com/loopers/domain/auditlog/*, apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/*, apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/*, apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/*
AuditLog, DlqMessage, EventHandled, ProductMetrics ์—”ํ‹ฐํ‹ฐ ๋ฐ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ถ”๊ฐ€.
commerce-streamer ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ - ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณ„์ธต
apps/commerce-streamer/src/main/java/com/loopers/application/auditlog/AuditLogFacade.java, AuditLogCommand.java, apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java, ProductMetricsCommand.java, MetricsType.java
๊ฐ์‚ฌ ๋กœ๊ทธ ๋ฐ ๋ฉ”ํŠธ๋ฆญ ์ฒ˜๋ฆฌ ํŒŒ์‚ฌ๋“œ, ์ปค๋งจ๋“œ ๊ฐ์ฒด, ๋ฉ”ํŠธ๋ฆญ ํƒ€์ž… ์—ด๊ฑฐํ˜• ์ถ”๊ฐ€.
commerce-streamer ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ - ์ธํ”„๋ผ ๋ฐ ์ปจ์Šˆ๋จธ
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/auditlog/*, apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/*, apps/commerce-streamer/src/main/java/com/loopers/infrastructure/eventhandled/*, apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/*, apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/*
JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„์ฒด, Kafka ์ปจ์Šˆ๋จธ(AuditLogConsumer, DlqConsumer, ProductMetricsConsumer) ์ถ”๊ฐ€. DemoKafkaConsumer ์ œ๊ฑฐ.
commerce-streamer ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ - DTO ๋ฐ ์„ค์ •
apps/commerce-streamer/src/main/java/com/loopers/interfaces/dto/*, apps/commerce-streamer/src/main/resources/application.yml, apps/commerce-streamer/src/main/java/com/loopers/support/config/TimeConfig.java
ProductLikePayload, ProductStockPayload, ProductViewPayload ๋ ˆ์ฝ”๋“œ ์ถ”๊ฐ€. Kafka ํ† ํ”ฝ ๋ฐ ์ปจ์Šˆ๋จธ ๊ทธ๋ฃน ์„ค์ •. Clock ๋นˆ ์ถ”๊ฐ€.
ํ…Œ์ŠคํŠธ
apps/commerce-streamer/src/test/java/com/loopers/*
CommerceStreamerContextTest, AuditLogConsumerTest, DlqConsumerTest, ProductMetricsConsumerTest ์ถ”๊ฐ€.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant OrderAPI as Order API
    participant OrderListener as OrderEventListener
    participant OutboxService as OutboxService
    participant OutboxPublisher as OutboxEventPublisher
    participant Kafka as Kafka
    participant StockProducer as StockChangedEventProducer

    User->>OrderAPI: POST /orders
    activate OrderAPI
    OrderAPI->>OrderAPI: Create Order
    OrderAPI->>OrderListener: Order Created Event
    deactivate OrderAPI

    activate OrderListener
    OrderListener->>OutboxService: Save Outbox Event
    activate OutboxService
    OutboxService->>OutboxService: Serialize Payload
    OutboxService->>OrderListener: Event Saved
    deactivate OutboxService

    OrderListener->>StockProducer: Send Stock Changed Events
    activate StockProducer
    StockProducer->>Kafka: Publish (with retry)
    Kafka-->>StockProducer: Ack/Nack
    deactivate StockProducer
    deactivate OrderListener

    activate OutboxPublisher
    OutboxPublisher->>OutboxPublisher: Poll Pending Events (every 1s)
    OutboxPublisher->>Kafka: Send to Topic
    Kafka-->>OutboxPublisher: Success/Failure
    OutboxPublisher->>OutboxPublisher: Mark as PROCESSED/FAILED
    deactivate OutboxPublisher
Loading
sequenceDiagram
    participant Kafka as Kafka Topics
    participant Consumer as ProductMetricsConsumer
    participant Facade as ProductMetricsFacade
    participant MetricsService as ProductMetricsService
    participant Repository as ProductMetricsRepository
    participant Cache as Redis Cache

    Kafka->>Consumer: Batch Messages (like/stock/view)
    activate Consumer
    
    loop For Each Record
        Consumer->>Facade: processLikeMetrics/Stock/View
        activate Facade
        Facade->>MetricsService: Process Metrics
        activate MetricsService
        MetricsService->>Repository: Get or Create ProductMetrics
        Repository-->>MetricsService: ProductMetrics
        MetricsService->>MetricsService: Update Delta (likes/sales/views)
        MetricsService->>Repository: Save
        Repository-->>MetricsService: Saved
        deactivate MetricsService
        
        alt Stock Metrics
            Facade->>Cache: Invalidate Product Caches
            Cache-->>Facade: Done
        end
        Facade-->>Consumer: Complete
        deactivate Facade
    end
    
    Consumer->>Kafka: Acknowledge Batch
    deactivate Consumer
Loading

Estimated code review effort

๐ŸŽฏ 5 (Critical) | โฑ๏ธ ~120+ ๋ถ„

์ถ”๊ฐ€ ๊ฒ€ํ†  ํ•„์š” ์‚ฌํ•ญ:

  • ์•„์›ƒ๋ฐ•์Šค ํŒจํ„ด ๊ตฌํ˜„: OutboxEventPublisher์˜ ์˜ˆ์•ฝ๋œ ์ž‘์—… ๋กœ์ง๊ณผ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ํ™•์ธ ํ•„์š” (OutboxEventPublisher.java)
  • Kafka ํ”„๋กœ๋“€์„œ ์žฌ์‹œ๋„ ์ •์ฑ…: ๋ชจ๋“  ํ”„๋กœ๋“€์„œ์˜ @Retry ์–ด๋…ธํ…Œ์ด์…˜ ์„ค์ • ๋ฐ ํด๋ฐฑ ๋ฉ”์„œ๋“œ ์ผ๊ด€์„ฑ ๊ฒ€์ฆ (*Producer.java ํŒŒ์ผ๋“ค)
  • ์ปจ์Šˆ๋จธ ์—๋Ÿฌ ํ•ธ๋“ค๋ง: DLQ ๋ผ์šฐํŒ…, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ค‘ ๊ฐœ๋ณ„ ๋ ˆ์ฝ”๋“œ ์‹คํŒจ ์ฒ˜๋ฆฌ, ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ ๊ฒ€์ฆ (*Consumer.java, KafkaConfig.java)
  • ์ƒˆ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ†ตํ•ฉ: commerce-streamer์˜ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ, ์„œ๋น„์Šค ๊ณ„์ธต, ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„ ์ „๋ฐ˜์  ๊ฒ€ํ† 
  • ๋ฉ€ํ‹ฐ ๋ชจ๋“ˆ ์˜์กด์„ฑ: gradle ์˜์กด์„ฑ ์ถ”๊ฐ€ ๋ฐ ๋ชจ๋“ˆ ๊ฐ„ ์ˆœํ™˜ ์˜์กด์„ฑ ์—†์Œ ํ™•์ธ (build.gradle.kts)
  • ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ: Outbox ์ƒํƒœ ์ „ํ™˜, ๋ฉ”ํŠธ๋ฆญ ๊ณ„์‚ฐ ๋กœ์ง, ๊ฐ์‚ฌ ๋กœ๊ทธ ๋ ˆ์ฝ”๋“œ ์ •ํ™•์„ฑ
  • ๊ตฌ์„ฑ ๊ด€๋ฆฌ: ํ† ํ”ฝ ์ด๋ฆ„, ์ปจ์Šˆ๋จธ ๊ทธ๋ฃน, ํƒ€์ž„์•„์›ƒ ์„ค์ •์˜ ํ™˜๊ฒฝ๋ณ„ ์ผ๊ด€์„ฑ

Possibly related PRs


Poem

๐Ÿฐ ํ† ๋ผ์˜ ์ถ•ํ•˜ ์‹œ

์ด๋ฒคํŠธ์˜ ํ™€์„ ์žฅ์‹ํ•˜๋‹ˆ,
์•„์›ƒ๋ฐ•์Šค๋Š” ์•ฝ์†์„ ์ง€ํ‚ค๊ณ ,
์ŠคํŠธ๋ฆฌ๋จธ๊ฐ€ ๋ฉ”ํŠธ๋ฆญ์„ ๋…ธ๋ž˜ํ•˜๋„ค.
Kafka์˜ ํ๋ฆ„ ์†์—์„œ,
๋ฉฑ๋“ฑ์„ฑ ์žˆ๋Š” ์ถค์„ ์ถ˜๋‹ค! ๐ŸŽ‰

Pre-merge checks and finishing touches

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

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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

โš ๏ธ Outside diff range comments (1)
apps/commerce-streamer/src/main/resources/application.yml (1)

16-17: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ด๋ฆ„์ด ์ž˜๋ชป ์„ค์ •๋จ

์ด ํŒŒ์ผ์€ commerce-streamer ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์šฉ์ด์ง€๋งŒ spring.application.name์ด commerce-api๋กœ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ๋ชจ๋‹ˆํ„ฐ๋ง, ๋กœ๊น…, Kafka consumer group ์‹๋ณ„์— ํ˜ผ๋ž€์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
 spring:
   application:
-    name: commerce-api
+    name: commerce-streamer
๐ŸŸก Minor comments (4)
apps/commerce-streamer/src/main/resources/application.yml-27-27 (1)

27-27: YAML ๊ตฌ๋ฌธ ์˜ค๋ฅ˜: ๋นˆ ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ

Line 27์˜ ๋‹จ๋… -๋Š” ๋นˆ ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์˜๋„์น˜ ์•Š์€ ๊ตฌ๋ฌธ์ด๋ผ๋ฉด ์ œ๊ฑฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
       - kafka.yml
       - logging.yml
       - monitoring.yml
-      -
 kafka:
apps/commerce-streamer/src/test/java/com/loopers/interfaces/AuditLogConsumerTest.java-32-84 (1)

32-84: acknowledge() ํ˜ธ์ถœ ๊ฒ€์ฆ ์ถ”๊ฐ€ ํ•„์š”

Happy path์™€ edge case(blank userId) ๋ชจ๋‘ ์ž˜ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ AuditLogConsumer ๊ตฌํ˜„์—์„œ finally ๋ธ”๋ก(line 54)์—์„œ ํ•ญ์ƒ acknowledgment.acknowledge()๋ฅผ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ, ํ…Œ์ŠคํŠธ์—์„œ๋„ ์ด๋ฅผ ๊ฒ€์ฆํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ๋ชจ๋‘์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

verify(acknowledgment, times(1)).acknowledge();
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventJpaRepository.java-12-16 (1)

12-16: JPQL LIMIT ์ ˆ ์‚ฌ์šฉ ๊ถŒ์žฅ์‚ฌํ•ญ ๊ฒ€ํ† 

ํ˜„์žฌ ์ฝ”๋“œ๊ฐ€ Hibernate 6.6+์—์„œ ์ง€์›ํ•˜๋Š” LIMIT ์ ˆ์„ ์‚ฌ์šฉ ์ค‘์ด๋ฏ€๋กœ ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ LIMIT์€ ํ‘œ์ค€ JPQL ์ŠคํŽ™์˜ ์ผ๋ถ€๊ฐ€ ์•„๋‹ˆ๋ฏ€๋กœ, ํ˜ธํ™˜์„ฑ๊ณผ ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด Pageable์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค:

-@Query("SELECT o FROM OutboxEvent o WHERE o.status = 'PENDING' ORDER BY o.createdAt ASC LIMIT :limit")
-List<OutboxEvent> findPendingEvents(@Param("limit") int limit);
+@Query("SELECT o FROM OutboxEvent o WHERE o.status = 'PENDING' ORDER BY o.createdAt ASC")
+List<OutboxEvent> findPendingEvents(Pageable pageable);

-@Query("SELECT o FROM OutboxEvent o WHERE o.status = 'FAILED' AND o.retryCount < :maxRetryCount ORDER BY o.createdAt ASC LIMIT :limit")
-List<OutboxEvent> findFailedEventsForRetry(@Param("maxRetryCount") int maxRetryCount, @Param("limit") int limit);
+@Query("SELECT o FROM OutboxEvent o WHERE o.status = 'FAILED' AND o.retryCount < :maxRetryCount ORDER BY o.createdAt ASC")
+List<OutboxEvent> findFailedEventsForRetry(@Param("maxRetryCount") int maxRetryCount, Pageable pageable);

ํ˜ธ์ถœ ์‹œ: PageRequest.of(0, limit)

apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/OrderEventProducer.java-31-34 (1)

31-34: ๋น„๋™๊ธฐ ์ „์†ก ์™„๋ฃŒ ์ „ ์„ฑ๊ณต ๋กœ๊ทธ ์ถœ๋ ฅ

kafkaTemplate.send()๋Š” ๋น„๋™๊ธฐ ๋ฉ”์„œ๋“œ๋กœ CompletableFuture๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ฝ”๋“œ๋Š” ์‹ค์ œ ์ „์†ก ์™„๋ฃŒ ์—ฌ๋ถ€์™€ ๊ด€๊ณ„์—†์ด ์ฆ‰์‹œ ์„ฑ๊ณต ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•˜๊ณ  ์žˆ์–ด, ์‹ค์ œ๋กœ ์‹คํŒจํ•œ ๊ฒฝ์šฐ์—๋„ ์„ฑ๊ณต ๋กœ๊ทธ๊ฐ€ ๋‚จ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ฝœ๋ฐฑ์„ ํ†ตํ•œ ์ •ํ™•ํ•œ ๋กœ๊น… ์ œ์•ˆ
-    kafkaTemplate.send(orderEventsTopic, orderId.toString(), event);
-    log.info("์ฃผ๋ฌธ ์ƒ์„ฑ ์ด๋ฒคํŠธ ๋ฐœํ–‰: orderId={}, userId={}", orderId, userId);
+    kafkaTemplate.send(orderEventsTopic, orderId.toString(), event)
+        .whenComplete((result, ex) -> {
+            if (ex == null) {
+                log.info("์ฃผ๋ฌธ ์ƒ์„ฑ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์™„๋ฃŒ: orderId={}, userId={}", orderId, userId);
+            } else {
+                log.error("์ฃผ๋ฌธ ์ƒ์„ฑ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์‹คํŒจ: orderId={}, userId={}", orderId, userId, ex);
+            }
+        });

๋‹ค๋ฅธ send ๋ฉ”์„œ๋“œ๋“ค(sendOrderCompletedEvent, sendOrderFailedEvent)์—๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿงน Nitpick comments (37)
modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java (1)

77-85: Non-retryable ์˜ˆ์™ธ ์ถ”๊ฐ€ ๊ณ ๋ ค

IllegalArgumentException ์™ธ์—๋„ ์žฌ์‹œ๋„ํ•ด๋„ ํ•ด๊ฒฐ๋˜์ง€ ์•Š๋Š” ์˜ˆ์™ธ๋“ค์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ์˜ˆ: NullPointerException, ClassCastException, deserialization ๊ด€๋ จ ์˜ˆ์™ธ ๋“ฑ.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •์•ˆ
     @Bean
     public CommonErrorHandler errorHandler(DeadLetterPublishingRecoverer recoverer) {
         DefaultErrorHandler errorHandler = new DefaultErrorHandler(
                 recoverer,
                 new FixedBackOff(DLQ_RETRY_INTERVAL, DLQ_MAX_ATTEMPTS)
         );
-        errorHandler.addNotRetryableExceptions(IllegalArgumentException.class);
+        errorHandler.addNotRetryableExceptions(
+                IllegalArgumentException.class,
+                NullPointerException.class,
+                ClassCastException.class
+        );
         return errorHandler;
     }
apps/commerce-streamer/src/main/java/com/loopers/application/auditlog/AuditLogCommand.java (1)

22-24: eventType()๊ณผ actionType ๋ช…๋ช… ํ˜ผ๋ž€

eventType() ๋ฉ”์„œ๋“œ๊ฐ€ actionType์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์€ ๋ช…๋ช… ๋ถˆ์ผ์น˜๋ฅผ ์œ ๋ฐœํ•ฉ๋‹ˆ๋‹ค. record์˜ ๊ธฐ๋ณธ ์ ‘๊ทผ์ž์ธ actionType()์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, ํ•„๋“œ๋ช… ์ž์ฒด๋ฅผ eventType์œผ๋กœ ํ†ต์ผํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋…์„ฑ์— ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/auditlog/AuditLog.java (1)

56-56: ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ์œ„ํ•œ Clock ์ฃผ์ž… ๊ณ ๋ ค

LocalDateTime.now()๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์‹œ๊ฐ„ ๊ด€๋ จ ๊ฒ€์ฆ์ด ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ Clock ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›๋„๋ก ๋ฆฌํŒฉํ† ๋ง์„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/LikeChangedDto.java (1)

8-8: likeType์— enum ์‚ฌ์šฉ ๊ณ ๋ ค

likeType์ด String์œผ๋กœ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํƒ€์ž… ์•ˆ์ „์„ฑ๊ณผ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ๊ฐ•ํ™”ํ•˜๋ ค๋ฉด enum ํƒ€์ž…(์˜ˆ: LikeType.LIKE, LikeType.UNLIKE)์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEvent.java (1)

51-52: lastError ์ปฌ๋Ÿผ์— ๊ธธ์ด ์ œํ•œ ๊ณ ๋ ค ํ•„์š”

์Šคํƒ ํŠธ๋ ˆ์ด์Šค๋‚˜ ๊ธด ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ์ €์žฅ๋  ๊ฒฝ์šฐ, ์ปฌ๋Ÿผ ๊ธธ์ด ์ œํ•œ์ด ์—†์œผ๋ฉด DB์— ๋”ฐ๋ผ ์˜ˆ๊ธฐ์น˜ ์•Š์€ ์ž˜๋ฆผ์ด๋‚˜ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ธธ์ด ์ œํ•œ ๋˜๋Š” TEXT ํƒ€์ž… ๋ช…์‹œ ์ œ์•ˆ
-    @Column(name = "last_error")
+    @Column(name = "last_error", columnDefinition = "TEXT")
     private String lastError;
apps/commerce-streamer/src/main/java/com/loopers/support/config/TimeConfig.java (1)

8-15: ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ์œ„ํ•œ Clock ๋นˆ ์„ค์ • - ์ข‹์€ ์ ‘๊ทผ์ž…๋‹ˆ๋‹ค.

Clock์„ ๋นˆ์œผ๋กœ ์ฃผ์ž…๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์—ฌ ํ…Œ์ŠคํŠธ์—์„œ ์‹œ๊ฐ„์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ถ„์‚ฐ ์‹œ์Šคํ…œ์—์„œ ํƒ€์ž„์กด ์ผ๊ด€์„ฑ์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ, Clock.system(ZoneId.of("Asia/Seoul")) ๋˜๋Š” UTC ๊ธฐ๋ฐ˜ Clock ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ systemDefaultZone()์€ ์„œ๋ฒ„ ๋กœ์ผ€์ผ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/interfaces/dto/ProductStockPayload.java (1)

3-9: LGTM!

Kafka ๋ฉ”์‹œ์ง€ ํŽ˜์ด๋กœ๋“œ๋กœ ์ ํ•ฉํ•œ ๋ถˆ๋ณ€ ๋ ˆ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. eventId๊ฐ€ String ํƒ€์ž…์œผ๋กœ OutboxEvent์˜ UUID ๊ธฐ๋ฐ˜ ID์™€ ์ผ๊ด€์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

changedType์ด ๊ณ ์ •๋œ ๊ฐ’ ์ง‘ํ•ฉ(์˜ˆ: INCREASE, DECREASE)์ด๋ผ๋ฉด, ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•ด enum ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ Kafka ์—ญ์ง๋ ฌํ™” ์‹œ ์œ ์—ฐ์„ฑ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ํ˜„์žฌ String ํƒ€์ž…๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

38-44: Kafka Producer ์žฌ์‹œ๋„ ์„ค์ •์— ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

ํ˜„์žฌ kafkaProducer ์„ค์ •์€ ๊ณ ์ • ๋Œ€๊ธฐ ์‹œ๊ฐ„(wait-duration: 1s)์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜ pgRetry ์„ค์ •์ฒ˜๋Ÿผ ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„๋ฅผ ์ ์šฉํ•˜๋ฉด ์ผ์‹œ์ ์ธ ๋ธŒ๋กœ์ปค ๊ณผ๋ถ€ํ•˜ ์ƒํ™ฉ์—์„œ ๋” ํšจ๊ณผ์ ์œผ๋กœ ๋ณต๊ตฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ์„ค์ • ์˜ˆ์‹œ
 kafkaProducer:
   max-attempts: 3
   wait-duration: 1s
   retry-exceptions:
     - org.apache.kafka.common.errors.TimeoutException
     - org.apache.kafka.common.errors.NetworkException
   fail-after-max-attempts: true
+  exponential-backoff-multiplier: 2
+  enable-exponential-backoff: true
apps/commerce-streamer/src/main/java/com/loopers/interfaces/dto/ProductLikePayload.java (1)

3-8: LGTM!

Kafka ๋ฉ”์‹œ์ง€ ํŽ˜์ด๋กœ๋“œ๋กœ ์ ํ•ฉํ•œ immutable record ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. eventId๋ฅผ ํ†ตํ•ด ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ณ , ์ปดํฌ๋„ŒํŠธ ๊ตฌ์„ฑ์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

์„ ํƒ์  ๊ณ ๋ ค์‚ฌํ•ญ: likeType์ด ๊ณ ์ •๋œ ๊ฐ’ ์ง‘ํ•ฉ(์˜ˆ: LIKE, UNLIKE)์ด๋ผ๋ฉด enum ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹จ, ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ์˜ ํ˜ธํ™˜์„ฑ์ด๋‚˜ ํ™•์žฅ์„ฑ์„ ๊ณ ๋ คํ•ด String์œผ๋กœ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ๋„ ํ•ฉ๋ฆฌ์ ์ธ ์„ ํƒ์ž…๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/auditlog/AuditLogService.java (1)

14-21: ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ๋งŽ์•„ ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฉ”์„œ๋“œ์— 6๊ฐœ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ์žˆ์–ด ํ˜ธ์ถœ ์‹œ ์ˆœ์„œ๋ฅผ ํ˜ผ๋™ํ•˜๊ธฐ ์‰ฝ๊ณ , ํ–ฅํ›„ ํ•„๋“œ ์ถ”๊ฐ€ ์‹œ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜๊ฐ€ ๊ณ„์† ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ปค๋งจ๋“œ ๊ฐ์ฒด๋‚˜ DTO๋ฅผ ๋„์ž…ํ•˜์—ฌ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋ฃนํ™”ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

๐Ÿ”Ž DTO ํŒจํ„ด ์ ์šฉ ์˜ˆ์‹œ
+public record SaveAuditLogCommand(
+    String eventId,
+    Long userId,
+    String actionType,
+    String targetType,
+    Long targetId,
+    String payload
+) {}
+
 @Transactional
-public void saveAuditLog(
-        String eventId,
-        Long userId,
-        String actionType,
-        String targetType,
-        Long targetId,
-        String payload
-) {
-    AuditLog auditLog = AuditLog.create(eventId, userId, actionType, targetType, targetId, payload);
+public void saveAuditLog(SaveAuditLogCommand command) {
+    AuditLog auditLog = AuditLog.create(
+        command.eventId(), 
+        command.userId(), 
+        command.actionType(), 
+        command.targetType(), 
+        command.targetId(), 
+        command.payload()
+    );
     auditLogRepository.save(auditLog);
 }
apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxService.java (2)

27-44: ์ง๋ ฌํ™” ์‹คํŒจ ์‹œ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ „๋žต์„ ๊ฒ€ํ† ํ•˜์„ธ์š”.

ํ˜„์žฌ ๊ตฌํ˜„์€ JSON ์ง๋ ฌํ™” ์‹คํŒจ ์‹œ RuntimeException์„ ๋˜์ ธ ํŠธ๋žœ์žญ์…˜์„ ๋กค๋ฐฑ์‹œํ‚ต๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์ ์ ˆํ•˜์ง€๋งŒ, ๋‹ค์Œ ์‚ฌํ•ญ๋“ค์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”:

  1. ์ง๋ ฌํ™” ์‹คํŒจ๊ฐ€ ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ์„ ์œ ๋ฐœํ•ด์•ผ ํ•˜๋Š”์ง€ ํ™•์ธ
  2. ๋” ๊ตฌ์ฒด์ ์ธ ์ปค์Šคํ…€ ์˜ˆ์™ธ ํƒ€์ž… ์‚ฌ์šฉ ๊ณ ๋ ค (์˜ˆ: EventSerializationException)
  3. ์ง๋ ฌํ™” ์‹คํŒจํ•œ ์ด๋ฒคํŠธ๋ฅผ ๋ณ„๋„๋กœ ์ถ”์ ํ•  ํ•„์š”์„ฑ ๊ฒ€ํ† 

ํ˜„์žฌ ๊ตฌํ˜„์€ ๋™์ž‘ํ•˜์ง€๋งŒ, ์šด์˜ ์ค‘ ์ง๋ ฌํ™” ์‹คํŒจ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์›๋ณธ ํŠธ๋žœ์žญ์…˜๋„ ์‹คํŒจํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ปค์Šคํ…€ ์˜ˆ์™ธ๋ฅผ ์‚ฌ์šฉํ•œ ๊ฐœ์„  ์˜ˆ์‹œ

๋„๋ฉ”์ธ ๋ ˆ์ด์–ด์— ์ปค์Šคํ…€ ์˜ˆ์™ธ ์ถ”๊ฐ€:

public class EventSerializationException extends RuntimeException {
    public EventSerializationException(String message, Throwable cause) {
        super(message, cause);
    }
}

์„œ๋น„์Šค ์ฝ”๋“œ ์ˆ˜์ •:

 } catch (JsonProcessingException e) {
     log.error("Outbox ์ด๋ฒคํŠธ ์ง๋ ฌํ™” ์‹คํŒจ: aggregateType={}, aggregateId={}",
             aggregateType, aggregateId, e);
-    throw new RuntimeException("์ด๋ฒคํŠธ ์ง๋ ฌํ™” ์‹คํŒจ", e);
+    throw new EventSerializationException(
+        String.format("์ด๋ฒคํŠธ ์ง๋ ฌํ™” ์‹คํŒจ: aggregateType=%s, aggregateId=%s", aggregateType, aggregateId), 
+        e
+    );
 }

19-26: ๋งค๊ฐœ๋ณ€์ˆ˜ ์ˆ˜๊ฐ€ ๋งŽ์•„ ์‹ค์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค.

AuditLogService์™€ ์œ ์‚ฌํ•˜๊ฒŒ, ์ด ๋ฉ”์„œ๋“œ๋„ 6๊ฐœ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ฐ›๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ปค๋งจ๋“œ ๊ฐ์ฒด๋‚˜ ๋นŒ๋” ํŒจํ„ด์„ ๋„์ž…ํ•˜๋ฉด ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

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

56-66: ์žฌ์‹œ๋„ ํ”Œ๋กœ์šฐ ๋ช…ํ™•์„ฑ ๊ฐœ์„  ์ œ์•ˆ

retryFailedEvents ๋ฉ”์„œ๋“œ๋Š” ์‹คํŒจํ•œ ์ด๋ฒคํŠธ๋ฅผ markForRetry()๋กœ ํ‘œ์‹œ๋งŒ ํ•˜๊ณ , ์‹ค์ œ ์žฌ์ „์†ก์€ publishPendingEvents๊ฐ€ ๋‹ค์‹œ ์„ ํƒํ•  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ์ด 2๋‹จ๊ณ„ ์ ‘๊ทผ ๋ฐฉ์‹์€ ์˜๋„์  ์„ค๊ณ„๋กœ ๋ณด์ด์ง€๋งŒ, ๋‹ค์Œ ์‚ฌํ•ญ์„ ๊ณ ๋ คํ•˜์„ธ์š”:

  • ์žฌ์‹œ๋„ ๋Œ€๊ธฐ ์‹œ๊ฐ„: ์ตœ๋Œ€ 5๋ถ„(fixedDelay) + 1์ดˆ(publishPendingEvents ์ฃผ๊ธฐ) ์ง€์—ฐ ๋ฐœ์ƒ
  • ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ: retryFailedEvents๋Š” ์ฆ‰์‹œ ์žฌ์‹œ๋„๋ฅผ ์ˆ˜ํ–‰ํ•  ๊ฒƒ ๊ฐ™์€ ์ธ์ƒ์„ ์ฃผ์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” ๋งˆํ‚น๋งŒ ์ˆ˜ํ–‰

๋” ๋ช…ํ™•ํ•œ ์˜๋„ ์ „๋‹ฌ์„ ์œ„ํ•ด ๋ฉ”์„œ๋“œ๋ช…์„ markFailedEventsForRetry ๋˜๋Š” Javadoc์— ํ”Œ๋กœ์šฐ๋ฅผ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/interfaces/AuditLogConsumerTest.java (1)

17-20: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ํ’ˆ์งˆ ์–‘ํ˜ธ

ํ…Œ์ŠคํŠธ ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ๋ชจํ‚น์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ObjectMapper๋ฅผ ๋งค ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ์ƒˆ๋กœ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์€ ๋ฌธ์ œ์—†์ง€๋งŒ, ์ •์  ํ•„๋“œ๋กœ ์ถ”์ถœํ•˜๋ฉด ํ…Œ์ŠคํŠธ ๊ฐ„ ์ผ๊ด€์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ObjectMapper ์ •์  ํ•„๋“œ ์ถ”์ถœ ์ œ์•ˆ (์„ ํƒ์‚ฌํ•ญ)
 class AuditLogConsumerTest {
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
 
     private final AuditLogFacade facade = mock(AuditLogFacade.class);
-    private final AuditLogConsumer consumer = new AuditLogConsumer(facade, new com.fasterxml.jackson.databind.ObjectMapper());
+    private final AuditLogConsumer consumer = new AuditLogConsumer(facade, OBJECT_MAPPER);
apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/ProductViewedEventProducer.java (1)

21-30: Retry ์ ์šฉ์€ ์ข‹์œผ๋‚˜ ์ตœ์ข… ์‹คํŒจ ์‹œ ์ฒ˜๋ฆฌ ๊ณ ๋ ค ํ•„์š”

Resilience4j @Retry์™€ fallback ๋ฉ”์„œ๋“œ๋ฅผ ์ ์šฉํ•œ ๊ฒƒ์€ ์ข‹์€ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ fallback์—์„œ๋Š” ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์žˆ์–ด, ์žฌ์‹œ๋„ ์†Œ์ง„ ํ›„ ์ด๋ฒคํŠธ๊ฐ€ ์™„์ „ํžˆ ์œ ์‹ค๋ฉ๋‹ˆ๋‹ค.

์ƒํ’ˆ ์กฐํšŒ ์ด๋ฒคํŠธ๋Š” ์ฃผ๋ฌธ/๊ฒฐ์ œ๋ณด๋‹ค ์ค‘์š”๋„๊ฐ€ ๋‚ฎ์„ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋‹ค์Œ์„ ๊ณ ๋ คํ•˜์„ธ์š”:

  • ์ผ๊ด€์„ฑ: PR์—์„œ DLQ ์ฒ˜๋ฆฌ๋ฅผ ๋„์ž…ํ–ˆ์œผ๋ฏ€๋กœ, ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด producer ์‹คํŒจ๋„ DLQ๋‚˜ ๋ณ„๋„ ํ…Œ์ด๋ธ”์— ๊ธฐ๋ก
  • ๋ชจ๋‹ˆํ„ฐ๋ง: ์‹คํŒจ ์ด๋ฒคํŠธ๋ฅผ ์ง‘๊ณ„ํ•˜์—ฌ Kafka ๋ธŒ๋กœ์ปค ์ด์Šˆ๋‚˜ ์„ค์ • ๋ฌธ์ œ๋ฅผ ์กฐ๊ธฐ ๊ฐ์ง€
๐Ÿ”Ž DLQ ๋˜๋Š” ์‹คํŒจ ์ด๋ฒคํŠธ ์ €์žฅ ํŒจํ„ด ์ ์šฉ ์ œ์•ˆ
 public void productViewedFallback(Long productId, Throwable ex) {
     log.error("์ƒํ’ˆ ์กฐํšŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์‹คํŒจ (์žฌ์‹œ๋„ ํ›„): productId={}", productId, ex);
+    // TODO: DLQ ๋˜๋Š” ์‹คํŒจ ์ด๋ฒคํŠธ ์ €์žฅ ๋กœ์ง ์ถ”๊ฐ€ ๊ณ ๋ ค
+    // dlqMessageRepository.save(DlqMessage.create(productViewTopic, productId.toString(), event, ex.getMessage()));
 }
apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/StockChangedDto.java (1)

5-10: changedType์„ Enum์œผ๋กœ ๋ณ€๊ฒฝ ๊ถŒ์žฅ

changedType ํ•„๋“œ๊ฐ€ String์œผ๋กœ ์ •์˜๋˜์–ด ์žˆ์–ด ํƒ€์ž… ์•ˆ์ „์„ฑ์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. ํ—ˆ์šฉ๋˜๋Š” ๊ฐ’๋“ค(์˜ˆ: "INCREASE", "DECREASE", "SOLD_OUT" ๋“ฑ)์„ ๋ช…์‹œ์ ์œผ๋กœ ์ •์˜ํ•˜๋Š” enum์„ ์‚ฌ์šฉํ•˜๋ฉด ์ปดํŒŒ์ผ ํƒ€์ž„์— ์˜ค๋ฅ˜๋ฅผ ์žก์„ ์ˆ˜ ์žˆ๊ณ , ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž Enum ํƒ€์ž… ์ ์šฉ ์˜ˆ์‹œ

๋จผ์ € enum์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค:

public enum StockChangeType {
    INCREASE,
    DECREASE,
    SOLD_OUT
}

๊ทธ๋ฆฌ๊ณ  record๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค:

 public record StockChangedDto(
         String eventId,
         Long productId,
         int stock,
-        String changedType
+        StockChangeType changedType
 ) {
-    public static StockChangedDto of(Long productId, int stock, String changedType) {
+    public static StockChangedDto of(Long productId, int stock, StockChangeType changedType) {
         return new StockChangedDto(
                 UUID.randomUUID().toString(),
                 productId,
                 stock,
                 changedType
         );
     }
 }
apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandled.java (1)

29-36: ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ์œ„ํ•ด Clock ์ฃผ์ž… ๊ณ ๋ ค

Line 34์—์„œ LocalDateTime.now()๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด ํ…Œ์ŠคํŠธ ์‹œ ์‹œ๊ฐ„์„ ์ œ์–ดํ•˜๊ธฐ ์–ด๋ ต๊ณ , ํƒ€์ž„์กด ๊ด€๋ จ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋” ๋‚˜์€ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ๊ณผ ์‹œ๊ฐ„ ์ œ์–ด๋ฅผ ์œ„ํ•ด Clock์„ ์ฃผ์ž…๋ฐ›๋Š” ๋ฐฉ์‹์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

๐Ÿ”Ž Clock ์ฃผ์ž… ๋ฐฉ์‹ ์˜ˆ์‹œ
 @Entity
 @Getter
 @Table(name = "event_handled")
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 public class EventHandled {

     @Id
     private String eventId;

     @Column(name = "domain_type")
     @Enumerated(EnumType.STRING)
     private EventHandledDomainType domainType;

     @Column(name = "event_type")
     private String eventType;

     @Column(name = "processed_at")
     private LocalDateTime processedAt;

-    public static EventHandled create(String eventId, EventHandledDomainType domainType, String eventType) {
+    public static EventHandled create(String eventId, EventHandledDomainType domainType, String eventType, Clock clock) {
         EventHandled eventHandled = new EventHandled();
         eventHandled.eventId = eventId;
         eventHandled.domainType = domainType;
         eventHandled.eventType = eventType;
-        eventHandled.processedAt = LocalDateTime.now();
+        eventHandled.processedAt = LocalDateTime.now(clock);
         return eventHandled;
     }
 }

์„œ๋น„์Šค ๋ ˆ์ด์–ด์—์„œ Clock.systemDefaultZone() ๋˜๋Š” ํ…Œ์ŠคํŠธ์šฉ ๊ณ ์ • Clock์„ ์ฃผ์ž…ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DlqConsumer.java (1)

46-61: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€

Line 56์˜ "Message failed after max retries" ๋ฉ”์‹œ์ง€๊ฐ€ ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์–‘ํ•œ ์‹คํŒจ ์›์ธ์„ ๊ตฌ๋ถ„ํ•˜๊ฑฐ๋‚˜ ๋‹ค๊ตญ์–ด ์ง€์›์ด ํ•„์š”ํ•  ๊ฒฝ์šฐ ์„ค์ • ํŒŒ์ผ๋กœ ์™ธ๋ถ€ํ™”ํ•˜๊ฑฐ๋‚˜, ์ƒ์œ„ ๋ ˆ์ด์–ด์—์„œ ์ „๋‹ฌ๋ฐ›๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/PaymentEventListener.java (1)

57-79: Kafka ๋ฐœํ–‰ ์‹คํŒจ ์‹œ ๋ณต๊ตฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ฐ•ํ™” ๊ถŒ์žฅ

ํ˜„์žฌ Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์‹คํŒจ ์‹œ ๋กœ๊น…๋งŒ ์ˆ˜ํ–‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ๋“€์„œ ๋ ˆ๋ฒจ์—์„œ @Retry๋ฅผ ํ†ตํ•ด ์žฌ์‹œ๋„๊ฐ€ ์žˆ๋”๋ผ๋„, ์ตœ์ข… ์‹คํŒจ ์‹œ ์ด๋ฒคํŠธ๊ฐ€ ์†์‹ค๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. PR ์„ค๋ช…์—์„œ Outbox ํŒจํ„ด์„ ์–ธ๊ธ‰ํ•˜์…จ๋Š”๋ฐ, ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”:

  1. Outbox ํ…Œ์ด๋ธ” ํ™œ์šฉ: ์ด๋ฒคํŠธ๋ฅผ DB์— ๋จผ์ € ์ €์žฅํ•˜๊ณ  ๋ณ„๋„ ํ”„๋กœ์„ธ์Šค๋กœ ๋ฐœํ–‰
  2. DLT (Dead Letter Topic): ํ”„๋กœ๋“€์„œ fallback์—์„œ ์‹คํŒจ ์ด๋ฒคํŠธ๋ฅผ DLT์— ์ €์žฅ
  3. ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ํŠธ๋ฆฌ๊ฑฐ: ๋ฐœํ–‰ ์‹คํŒจ ์‹œ ์•Œ๋ฆผ/๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ํ˜ธ์ถœ

ํ˜„์žฌ ๊ตฌ์กฐ์—์„œ๋Š” ํ”„๋กœ๋“€์„œ์˜ fallback ๋ฉ”์„œ๋“œ์—์„œ DLT๋‚˜ outbox์— ์ €์žฅํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ํ˜„์‹ค์ ์ž…๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/interfaces/ProductMetricsConsumerTest.java (1)

1-141: ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๊ฐœ์„  ๊ถŒ์žฅ

ํ˜„์žฌ ์ •์ƒ ์ผ€์ด์Šค๋งŒ ํ…Œ์ŠคํŠธ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. PR ์ฒดํฌ๋ฆฌ์ŠคํŠธ์— ์–ธ๊ธ‰๋œ "์ค‘๋ณต ๋ฉ”์‹œ์ง€ ์žฌ์ „์†ก ํ…Œ์ŠคํŠธ"์™€ ๋‹ค์Œ ์‹œ๋‚˜๋ฆฌ์˜ค ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”:

  1. ํŒŒ์‹ฑ ์‹คํŒจ: ์ž˜๋ชป๋œ JSON ํ˜•์‹
  2. Null ํ† ํ”ฝ: Line 42-44์˜ ๋ฐฉ์–ด ๋กœ์ง ๊ฒ€์ฆ
  3. Facade ์˜ˆ์™ธ: processLikeMetrics() ์‹คํŒจ ์‹œ์—๋„ acknowledge() ํ˜ธ์ถœ ํ™•์ธ
  4. ๋ฉฑ๋“ฑ์„ฑ: ๋™์ผํ•œ eventId๋ฅผ ๊ฐ€์ง„ ์ค‘๋ณต ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
๐Ÿ”Ž ์˜ˆ์‹œ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
@Test
@DisplayName("์ค‘๋ณต eventId ๋ฉ”์‹œ์ง€ โ†’ ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ")
void listen_duplicateEventId() {
    // given
    String value = """
        {"eventId": "evt-001", "productId": 123, "likeType": "LIKED"}
        """;
    ConsumerRecord<String, String> record1 = makeRecord("product-like-metrics", "123", value);
    ConsumerRecord<String, String> record2 = makeRecord("product-like-metrics", "123", value);
    
    // when
    consumer.listen(List.of(record1, record2), mock(Acknowledgment.class));
    
    // then - facade๋Š” 2๋ฒˆ ํ˜ธ์ถœ๋˜์ง€๋งŒ ๋‚ด๋ถ€์—์„œ 1๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ๋˜์–ด์•ผ ํ•จ
    verify(facade, times(2)).processLikeMetrics(any());
}
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java (1)

14-15: LGTM - ์ธ๋ฑ์Šค ์„ค์ • ๊ถŒ์žฅ

๋ณตํ•ฉํ‚ค ์กฐํšŒ ์ฟผ๋ฆฌ๊ฐ€ ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ, productId์™€ metricsDate๋ฅผ ์‚ฌ์šฉํ•œ ๋นˆ๋ฒˆํ•œ ์กฐํšŒ๊ฐ€ ์˜ˆ์ƒ๋˜๋ฏ€๋กœ, ProductMetrics ์—”ํ‹ฐํ‹ฐ์— ๋ณตํ•ฉ ์ธ๋ฑ์Šค ์„ค์ •์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

@Table(name = "product_metrics", indexes = {
    @Index(name = "idx_product_metrics_lookup", columnList = "product_id, metrics_date")
})
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/AuditLogConsumer.java (1)

40-43: ํ—ค๋” ๊ฐ’ ์ฒ˜๋ฆฌ ๊ฐœ์„  ๊ถŒ์žฅ

eventType์ด ๋นˆ ๋ฌธ์ž์—ด๋กœ ์ดˆ๊ธฐํ™”๋˜์–ด ์žˆ์–ด, ํ—ค๋”๊ฐ€ ์—†์„ ๋•Œ actionType์ด ๋นˆ ๋ฌธ์ž์—ด๋กœ ์„ค์ •๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์˜๋„ํ•œ ๋™์ž‘์ผ ์ˆ˜ ์žˆ์œผ๋‚˜, null ๊ตฌ๋ณ„์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

๐Ÿ”Ž ๊ฐœ์„ ์•ˆ
-String eventType = "";
+String eventType = null;
 if (record.headers().lastHeader("eventType") != null) {
+    byte[] headerValue = record.headers().lastHeader("eventType").value();
+    if (headerValue != null && headerValue.length > 0) {
-        eventType = new String(record.headers().lastHeader("eventType").value(), StandardCharsets.UTF_8);
+        eventType = new String(headerValue, StandardCharsets.UTF_8);
+    }
 }

๊ทธ๋ฆฌ๊ณ  Line 68์—์„œ:

-node.has("actionType") ? node.get("actionType").asText() : eventType,
+node.has("actionType") ? node.get("actionType").asText() : 
+    (eventType != null ? eventType : "UNKNOWN"),
apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/StockChangedEventProducer.java (1)

21-32: ์žฌ์‹œ๋„ ํ›„ ์‹คํŒจ ์ฒ˜๋ฆฌ ๊ฐ•ํ™” ๊ถŒ์žฅ

@Retry ์• ๋…ธํ…Œ์ด์…˜์œผ๋กœ ์žฌ์‹œ๋„๋ฅผ ๊ตฌํ˜„ํ–ˆ์œผ๋‚˜, fallback ๋ฉ”์„œ๋“œ์—์„œ๋Š” ๋กœ๊น…๋งŒ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์žฌ๊ณ  ๋ณ€๊ฒฝ์€ ๋น„์ฆˆ๋‹ˆ์Šค ํฌ๋ฆฌํ‹ฐ์ปฌํ•œ ์ด๋ฒคํŠธ์ด๋ฏ€๋กœ, ์ตœ์ข… ์‹คํŒจ ์‹œ ๋ณต๊ตฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”:

  1. DLT (Dead Letter Topic) ์— ์ด๋ฒคํŠธ ์ €์žฅ
  2. Outbox ํ…Œ์ด๋ธ” ์— ์‹คํŒจ ์ด๋ฒคํŠธ ๊ธฐ๋ก ํ›„ ๋ณ„๋„ ๋ฐฐ์น˜ ์žฌ์ฒ˜๋ฆฌ
  3. ์•Œ๋ฆผ ์‹œ์Šคํ…œ ์—ฐ๋™
๐Ÿ”Ž DLT ์ €์žฅ ์˜ˆ์‹œ
public void stockChangedFallback(Long productId, int stock, String changedType, Throwable ex) {
    log.error("์žฌ๊ณ  ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์‹คํŒจ (์žฌ์‹œ๋„ ํ›„): productId={}, stock={}, changedType={}",
            productId, stock, changedType, ex);
    
    // DLT์— ์ €์žฅ
    try {
        StockChangedDto event = StockChangedDto.of(productId, stock, changedType);
        kafkaTemplate.send(stockChangedTopic + ".DLT", productId.toString(), event);
    } catch (Exception dltEx) {
        log.error("DLT ์ €์žฅ๋„ ์‹คํŒจ: productId={}", productId, dltEx);
        // ์ตœํ›„์˜ ์ˆ˜๋‹จ: DB outbox์— ์ €์žฅ
    }
}
apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/LikeChangedEventProducer.java (1)

21-31: ์ผ๊ด€๋œ ์‹คํŒจ ์ฒ˜๋ฆฌ ํŒจํ„ด ์ ์šฉ ๊ถŒ์žฅ

StockChangedEventProducer์™€ ๋™์ผํ•œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. ์ข‹์•„์š” ์ด๋ฒคํŠธ๊ฐ€ ์žฌ๊ณ ๋งŒํผ ํฌ๋ฆฌํ‹ฐ์ปฌํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์œผ๋‚˜, ์ผ๊ด€์„ฑ๊ณผ ์šด์˜ ํŽธ์˜๋ฅผ ์œ„ํ•ด ๋™์ผํ•œ DLQ/Outbox ํŒจํ„ด ์ ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

#!/bin/bash
# ๋‹ค๋ฅธ ํ”„๋กœ๋“€์„œ๋“ค์˜ fallback ํŒจํ„ด ํ™•์ธ
rg -A 5 "public void.*Fallback" --type java

๋ชจ๋“  ํ”„๋กœ๋“€์„œ์— ์ผ๊ด€๋œ fallback ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์ ์šฉํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.java (1)

37-58: ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ๋งจํ‹ฑ ํ™•์ธ ํ•„์š”

๊ฐœ๋ณ„ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์‹คํŒจ ์‹œ์—๋„ finally ๋ธ”๋ก์—์„œ ์ „์ฒด ๋ฐฐ์น˜๋ฅผ acknowledge()ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” at-most-once ์‹œ๋งจํ‹ฑ์„ ์ œ๊ณตํ•˜๋ฉฐ, ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋Š” ์žฌ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

PR ์„ค๋ช…์—์„œ ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์„ ์œ„ํ•ด event_handled ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ํ–ˆ๋Š”๋ฐ, ์ด ์ปจ์Šˆ๋จธ์—์„œ๋Š” ํ•ด๋‹น ๋กœ์ง์ด ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”:

  1. ProductMetricsFacade ๋‚ด๋ถ€์—์„œ ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์ด ๊ตฌํ˜„๋˜์–ด ์žˆ๋Š”์ง€
  2. ๋ฉ”์‹œ์ง€ ์œ ์‹ค์„ ํ—ˆ์šฉํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ์ธ์ง€
#!/bin/bash
# ProductMetricsFacade์—์„œ ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ ํ™•์ธ
ast-grep --pattern $'class ProductMetricsFacade {
  $$$
  @InboxEvent
  $$$
}'

Based on learnings, @InboxEvent ์• ๋…ธํ…Œ์ด์…˜๊ณผ EventInboxAspect๋ฅผ ํ™œ์šฉํ•œ ์ค‘์•™ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํŒจํ„ด ์ ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsCommand.java (1)

7-46: ํƒ€์ž…๋ณ„ ์ปค๋งจ๋“œ ๋ถ„๋ฆฌ ๊ณ ๋ ค

ํ˜„์žฌ ๊ตฌ์กฐ๋Š” metricsType์— ๋”ฐ๋ผ ์ผ๋ถ€ ํ•„๋“œ๋งŒ ์‚ฌ์šฉ๋˜๊ณ  ๋‚˜๋จธ์ง€๋Š” null์ž…๋‹ˆ๋‹ค. ๊ธฐ๋Šฅ์ ์œผ๋กœ ๋ฌธ์ œ๋Š” ์—†์ง€๋งŒ, sealed interface์™€ ํƒ€์ž…๋ณ„ record๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ ๋ฉ”ํŠธ๋ฆญ ํƒ€์ž…์— ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ๋ช…์‹œ์ ์œผ๋กœ ๊ฐ–๊ฒŒ ๋˜์–ด ํƒ€์ž… ์•ˆ์ „์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„๋„ ๊ฐ„๊ฒฐํ•˜๊ณ  ์‹ค์šฉ์ ์ด๋ฏ€๋กœ, ํ–ฅํ›„ ๋ฉ”ํŠธ๋ฆญ ํƒ€์ž…์ด ๋Š˜์–ด๋‚  ๊ฒฝ์šฐ ๋ฆฌํŒฉํ† ๋ง์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/UserActionEventProducer.java (2)

21-27: Kafka ์ „์†ก ๊ฒฐ๊ณผ๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

kafkaTemplate.send()๋Š” ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•˜๋ฉฐ CompletableFuture๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ฝ”๋“œ๋Š” ์ „์†ก ๊ฒฐ๊ณผ๋ฅผ ๋ฌด์‹œํ•˜๋ฏ€๋กœ, ์ „์†ก ์‹คํŒจ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ @Retry๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹คํŒจ๋ฅผ ๊ฐ์ง€ํ•˜๋ ค๋ฉด ๊ฒฐ๊ณผ๋ฅผ ๋™๊ธฐ์ ์œผ๋กœ ๋Œ€๊ธฐํ•˜๊ฑฐ๋‚˜ ์ฝœ๋ฐฑ์„ ๋“ฑ๋กํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”Ž ์ „์†ก ๊ฒฐ๊ณผ๋ฅผ ๋™๊ธฐ์ ์œผ๋กœ ๋Œ€๊ธฐํ•˜๋Š” ๋ฐฉ๋ฒ• ์ œ์•ˆ
 @Retry(name = "kafkaProducer", fallbackMethod = "userActionFallback")
 public void sendUserActionEvent(Long userId, String actionType, String targetType, Long targetId) {
     UserActionDto event = UserActionDto.of(userId, actionType, targetType, targetId);
-    kafkaTemplate.send(userActionTopic, userId.toString(), event);
+    kafkaTemplate.send(userActionTopic, userId.toString(), event).get();
     log.info("์œ ์ € ํ–‰๋™ ์ด๋ฒคํŠธ ๋ฐœํ–‰: userId={}, actionType={}, targetType={}, targetId={}",
             userId, actionType, targetType, targetId);
 }

์ฐธ๊ณ : .get() ํ˜ธ์ถœ ์‹œ InterruptedException ๋ฐ ExecutionException์„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


29-31: Fallback ๋กœ๊ทธ์— ์ „์ฒด ์ปจํ…์ŠคํŠธ ํฌํ•จ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๋””๋ฒ„๊น…์„ ์œ„ํ•ด targetType๊ณผ targetId๋„ ๋กœ๊ทธ์— ํฌํ•จํ•˜๋ฉด ๋ฌธ์ œ ์ถ”์ ์ด ์šฉ์ดํ•ด์ง‘๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋กœ๊ทธ ๊ฐœ์„  ์ œ์•ˆ
 public void userActionFallback(Long userId, String actionType, String targetType, Long targetId, Throwable ex) {
-    log.error("์œ ์ € ํ–‰๋™ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์‹คํŒจ (์žฌ์‹œ๋„ ํ›„): userId={}, actionType={}", userId, actionType, ex);
+    log.error("์œ ์ € ํ–‰๋™ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์‹คํŒจ (์žฌ์‹œ๋„ ํ›„): userId={}, actionType={}, targetType={}, targetId={}",
+            userId, actionType, targetType, targetId, ex);
 }
apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DlqMessageService.java (1)

51-75: ID๋ฅผ ์ฐพ์ง€ ๋ชปํ•  ๊ฒฝ์šฐ ์กฐ์šฉํžˆ ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค.

markAsResolved, markAsAbandoned, incrementRetryCount ๋ฉ”์„œ๋“œ์—์„œ ID๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ์•„๋ฌด๋Ÿฐ ๋™์ž‘ ์—†์ด ์ข…๋ฃŒ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์˜๋„์ ์ธ ๋ฉฑ๋“ฑ์„ฑ ์„ค๊ณ„์ผ ์ˆ˜ ์žˆ์œผ๋‚˜, ํ˜ธ์ถœ์ž์˜ ๋ฒ„๊ทธ๋ฅผ ์ˆจ๊ธธ ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ตœ์†Œํ•œ ๋””๋ฒ„๊ทธ ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๊ฑฐ๋‚˜, ํ•„์š”์‹œ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”Ž ๋กœ๊น… ์ถ”๊ฐ€ ์˜ˆ์‹œ
 @Transactional
 public void markAsResolved(String id) {
-    dlqMessageRepository.findById(id).ifPresent(message -> {
+    dlqMessageRepository.findById(id).ifPresentOrElse(message -> {
         message.markAsResolved();
         dlqMessageRepository.save(message);
         log.info("DLQ ๋ฉ”์‹œ์ง€ ํ•ด๊ฒฐ ์™„๋ฃŒ: id={}", id);
-    });
+    }, () -> log.warn("DLQ ๋ฉ”์‹œ์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ: id={}", id));
 }
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java (1)

16-39: Magic string ๋Œ€์‹  Enum ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

likeType("LIKED", "UNLIKED")๊ณผ changedType("DECREASED", "RESTORED")์ด ๋ฌธ์ž์—ด๋กœ ์ฒ˜๋ฆฌ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํƒ€์ž… ์•ˆ์ „์„ฑ๊ณผ IDE ์ง€์›์„ ์œ„ํ•ด Enum์„ ์‚ฌ์šฉํ•˜๋ฉด ์˜คํƒ€๋กœ ์ธํ•œ ๋ฒ„๊ทธ๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Kafka ์ด๋ฒคํŠธ DTO์—์„œ ์ด๋ฏธ Enum์„ ์‚ฌ์šฉ ์ค‘์ด๋ผ๋ฉด ํ•ด๋‹น Enum์„ ์ง์ ‘ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ๋„ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DlqMessage.java (1)

77-85: ์ƒํƒœ ์ „ํ™˜ ์‹œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ˆ„๋ฝ

markAsResolved()์™€ markAsAbandoned() ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๊ฒ€์ฆํ•˜์ง€ ์•Š์•„, ABANDONED โ†’ RESOLVED ๋˜๋Š” RESOLVED โ†’ ABANDONED๋กœ ์ž˜๋ชป๋œ ์ƒํƒœ ์ „ํ™˜์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ƒํƒœ ๊ฒ€์ฆ ์ถ”๊ฐ€ ์ œ์•ˆ
 public void markAsResolved() {
+    if (this.status != DlqStatus.PENDING) {
+        throw new IllegalStateException("PENDING ์ƒํƒœ์—์„œ๋งŒ RESOLVED๋กœ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์ƒํƒœ: " + this.status);
+    }
     this.status = DlqStatus.RESOLVED;
     this.processedAt = LocalDateTime.now();
 }

 public void markAsAbandoned() {
+    if (this.status != DlqStatus.PENDING) {
+        throw new IllegalStateException("PENDING ์ƒํƒœ์—์„œ๋งŒ ABANDONED๋กœ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์ƒํƒœ: " + this.status);
+    }
     this.status = DlqStatus.ABANDONED;
     this.processedAt = LocalDateTime.now();
 }
apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/PaymentEventDto.java (1)

10-10: ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•ด String ๋Œ€์‹  enum ์‚ฌ์šฉ ๊ณ ๋ ค

paymentStatus๊ฐ€ String์œผ๋กœ ์ •์˜๋˜์–ด ์žˆ์–ด ์˜คํƒ€๋‚˜ ์ž˜๋ชป๋œ ๊ฐ’์ด ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. enum์„ ์‚ฌ์šฉํ•˜๋ฉด ์ปดํŒŒ์ผ ํƒ€์ž„์— ์˜ค๋ฅ˜๋ฅผ ์žก์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž enum ์ ์šฉ ์˜ˆ์‹œ
public enum PaymentStatus {
    SUCCESS, FAILED, PENDING
}

record ํ•„๋“œ๋ฅผ PaymentStatus paymentStatus๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ์ง๋ ฌํ™” ์‹œ @JsonFormat(shape = JsonFormat.Shape.STRING)์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/OrderEventDto.java (1)

42-44: null ๋Œ€์‹  ๋นˆ ๋ฆฌ์ŠคํŠธ ์‚ฌ์šฉ ๊ถŒ์žฅ

completed()์™€ failed() ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์—์„œ items๋ฅผ null๋กœ ์„ค์ •ํ•˜๋ฉด Consumer ์ธก์—์„œ NullPointerException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฉ์–ด์  ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์œ„ํ•ด Collections.emptyList()๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋นˆ ๋ฆฌ์ŠคํŠธ ์ ์šฉ ์˜ˆ์‹œ
 public static OrderEventDto completed(Long orderId, Long userId) {
     return new OrderEventDto(
             UUID.randomUUID().toString(),
             orderId,
             userId,
             "COMPLETED",
             null,
             null,
-            null,
+            List.of(),
             LocalDateTime.now()
     );
 }

 public static OrderEventDto failed(Long orderId, Long userId) {
     return new OrderEventDto(
             UUID.randomUUID().toString(),
             orderId,
             userId,
             "FAILED",
             null,
             null,
-            null,
+            List.of(),
             LocalDateTime.now()
     );
 }
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java (2)

27-28: ๋ฏธ์‚ฌ์šฉ ์ƒ์ˆ˜ ์กด์žฌ

PRODUCT_CACHE_KEY_PATTERN ์ƒ์ˆ˜๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์ง€๋งŒ Line 78์—์„œ ๋™์ผํ•œ ํŒจํ„ด์„ ์ธ๋ผ์ธ์œผ๋กœ ์ž‘์„ฑํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ƒ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ๋ถˆํ•„์š”ํ•œ ์ƒ์ˆ˜๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.

๐Ÿ”Ž ์ƒ์ˆ˜ ํ™œ์šฉ ์ œ์•ˆ
-    String detailKeyPattern = String.format("product:*:detail:%d", productId);
+    String detailKeyPattern = String.format(PRODUCT_CACHE_KEY_PATTERN, productId);

79-90: Redis KEYS ๋ช…๋ น ์‚ฌ์šฉ ์‹œ ์„ฑ๋Šฅ ์ฃผ์˜ ํ•„์š”

redisTemplate.keys() ๋ฉ”์„œ๋“œ๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ Redis KEYS ๋ช…๋ น์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์ด๋Š” O(N) ๋ณต์žก๋„๋กœ ๋Œ€๊ทœ๋ชจ ํ‚ค์ŠคํŽ˜์ด์Šค์—์„œ Redis๋ฅผ ๋ธ”๋กœํ‚นํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ, Line 86์—์„œ "product:*:list:*" ํŒจํ„ด์œผ๋กœ ๋ชจ๋“  ๋ชฉ๋ก ์บ์‹œ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๊ฒƒ์€ ๋‹จ์ผ ์ƒํ’ˆ ๋ณ€๊ฒฝ์— ๋น„ํ•ด ๊ณผ๋„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐœ์„  ๋ฐฉ์•ˆ:

  1. SCAN ๋ช…๋ น ์‚ฌ์šฉ (RedisTemplate์˜ scan() ๋ฉ”์„œ๋“œ)์œผ๋กœ ์ ์ง„์  ์กฐํšŒ
  2. ์ƒํ’ˆ๋ณ„ ์บ์‹œ ํ‚ค ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ Redis Set ํ™œ์šฉ
  3. ๋˜๋Š” ์บ์‹œ TTL์— ์˜์กดํ•˜์—ฌ ๋ช…์‹œ์  ๋ฌดํšจํ™” ์ตœ์†Œํ™”
// SCAN ์‚ฌ์šฉ ์˜ˆ์‹œ
ScanOptions scanOptions = ScanOptions.scanOptions()
    .match(detailKeyPattern)
    .count(100)
    .build();
try (Cursor<String> cursor = redisTemplate.scan(scanOptions)) {
    while (cursor.hasNext()) {
        redisTemplate.delete(cursor.next());
    }
}
apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/OrderEventListener.java (1)

35-36: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” orderEventProducer ํ•„๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

orderEventProducer๊ฐ€ ์ฃผ์ž…๋˜์—ˆ์ง€๋งŒ ์ด ํด๋ž˜์Šค ๋‚ด์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. handleKafkaEventPublish๋Š” ์ง์ ‘ Kafka ๋ฐœํ–‰ ๋Œ€์‹  outboxService๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์˜๋„์ ์œผ๋กœ outbox ํŒจํ„ด์œผ๋กœ ์ „ํ™˜ํ•œ ๊ฒƒ์ด๋ผ๋ฉด, ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜์„ธ์š”.

๐Ÿ”Ž ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ํ•„๋“œ ์ œ๊ฑฐ ์ œ์•ˆ
     private final OrderFacade orderFacade;
     private final OutboxService outboxService;
     private final DataPlatformSender dataPlatformSender;
     private final ApplicationEventPublisher eventPublisher;
-    private final OrderEventProducer orderEventProducer;
     private final StockChangedEventProducer stockChangedEventProducer;
apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/PaymentEventProducer.java (1)

22-26: ๋น„๋™๊ธฐ ์ „์†ก ํ›„ ์ฆ‰์‹œ ๋กœ๊น…ํ•˜๋ฉด ์˜คํ•ด์˜ ์†Œ์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

kafkaTemplate.send()๋Š” ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•˜์—ฌ CompletableFuture๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๋กœ๊น…์€ ์‹ค์ œ ์ „์†ก ์™„๋ฃŒ ์ „์— "์ด๋ฒคํŠธ ๋ฐœํ–‰" ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•˜๋ฏ€๋กœ, ๋ธŒ๋กœ์ปค ์‘๋‹ต ํ›„ ๋กœ๊น…ํ•˜๊ฑฐ๋‚˜ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๋ฅผ "์ด๋ฒคํŠธ ๋ฐœํ–‰ ์š”์ฒญ"์œผ๋กœ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ „์†ก ์™„๋ฃŒ ํ›„ ๋กœ๊น… ์˜ˆ์‹œ
     public void sendPaymentSuccessEvent(Long orderId, Long userId, String transactionId, Long amount) {
         PaymentEventDto event = PaymentEventDto.success(orderId, userId, transactionId, amount);
-        kafkaTemplate.send(paymentEventsTopic, orderId.toString(), event);
-        log.info("๊ฒฐ์ œ ์„ฑ๊ณต ์ด๋ฒคํŠธ ๋ฐœํ–‰: orderId={}, transactionId={}", orderId, transactionId);
+        kafkaTemplate.send(paymentEventsTopic, orderId.toString(), event)
+                .whenComplete((result, ex) -> {
+                    if (ex == null) {
+                        log.info("๊ฒฐ์ œ ์„ฑ๊ณต ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์™„๋ฃŒ: orderId={}, transactionId={}", orderId, transactionId);
+                    }
+                });
     }
๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 1cda434 and 2fc3189.

๐Ÿ“’ Files selected for processing (68)
  • apps/commerce-api/build.gradle.kts (2 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEvent.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEventRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxService.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/payment/event/PaymentSucceededEvent.java (2 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/LikeChangedDto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/OrderEventDto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/PaymentEventDto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/ProductViewedDto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/StockChangedDto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/UserActionDto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/LikeChangedEventProducer.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/OrderEventProducer.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/PaymentEventProducer.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/ProductViewedEventProducer.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/StockChangedEventProducer.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/UserActionEventProducer.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventJpaRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventRepositoryImpl.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/OrderEventListener.java (4 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/PaymentEventListener.java (4 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/ProductLikeEventListener.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/UserActionEventListener.java (2 hunks)
  • apps/commerce-api/src/main/resources/application.yml (2 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/application/auditlog/AuditLogCommand.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/application/auditlog/AuditLogFacade.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsType.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsCommand.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/auditlog/AuditLog.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/auditlog/AuditLogRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/auditlog/AuditLogService.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DlqMessage.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DlqMessageRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DlqMessageService.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandled.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandledDomainType.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandledRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandledService.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsId.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/auditlog/AuditLogJpaRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/auditlog/AuditLogRepositoryImpl.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DlqMessageJpaRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DlqMessageRepositoryImpl.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledJpaRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledRepositoryImpl.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/AuditLogConsumer.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java (0 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DlqConsumer.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/dto/ProductLikePayload.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/dto/ProductStockPayload.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/dto/ProductViewPayload.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/support/config/TimeConfig.java (1 hunks)
  • apps/commerce-streamer/src/main/resources/application.yml (1 hunks)
  • apps/commerce-streamer/src/test/java/com/loopers/CommerceStreamerContextTest.java (1 hunks)
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/AuditLogConsumerTest.java (1 hunks)
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/DlqConsumerTest.java (1 hunks)
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/ProductMetricsConsumerTest.java (1 hunks)
  • modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java (5 hunks)
  • modules/kafka/src/main/resources/kafka.yml (1 hunks)
๐Ÿ’ค Files with no reviewable changes (1)
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (11)
๐Ÿ““ 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-11-12T13:04:50.782Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 72
File: core/domain/src/main/java/com/loopers/core/domain/productlike/vo/ProductLikeId.java:5-7
Timestamp: 2025-11-12T13:04:50.782Z
Learning: In the com.loopers codebase, domain entity ID value objects (e.g., ProductLikeId, OrderItemId, ProductId, PaymentId, OrderId, BrandId) are system-generated identifiers and do not require pattern validation (regex, length checks). They are implemented as simple records with a String value and an empty() factory method returning null for unsaved entities. This differs from UserIdentifier, which is a user-supplied login ID that requires format validation. Domain IDs should not be flagged for missing validation logic in the create() method.
<!-- [add_learning]
UserIdentifier์™€ ๊ฐ™์€ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ID์™€ ProductLikeId, OrderItemId ๋“ฑ์˜ ๋„๋ฉ”์ธ ID๋Š” ๊ฒ€์ฆ ํŒจํ„ด์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. UserIdentifier๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•˜๋Š” ๋กœ๊ทธ์ธ ID๋กœ์„œ ์ •๊ทœ์‹, ๊ธธ์ด ๋“ฑ์˜ ํ˜•์‹ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜์ง€๋งŒ, ๋„๋ฉ”์ธ ID๋Š” ์‹œ์Šคํ…œ์—์„œ ์ƒ์„ฑํ•˜๋Š” ์‹๋ณ„์ž(UUID, DB ์ƒ์„ฑ ID)์ด๋ฏ€๋กœ ํŒจํ„ด ๊ฒ€์ฆ์ด ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋„๋ฉ”์ธ ID VO๋Š” ๋‹จ์ˆœํ•œ record์™€ empty() ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

Applied to files:

  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsId.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.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/auditlog/AuditLogJpaRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DlqMessageJpaRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DlqMessageRepository.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/ProductMetricsConsumer.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DlqConsumer.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/AuditLogConsumer.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-api/src/main/java/com/loopers/interfaces/api/listener/OrderEventListener.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/OrderEventProducer.java
  • modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.java
  • apps/commerce-api/src/main/resources/application.yml
  • apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxService.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/ProductViewedEventProducer.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/DlqConsumerTest.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/PaymentEventListener.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/PaymentEventProducer.java
๐Ÿ“š 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-11-09T10:49:10.381Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/order/OrderQueryService.kt:9-12
Timestamp: 2025-11-09T10:49:10.381Z
Learning: In this codebase, Service classes (classes whose name contains "Service") are allowed to use Spring's Service annotation, even when located in the domain layer. 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/domain/eventhandled/EventHandledService.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DlqMessageService.java
๐Ÿ“š 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:

  • modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java
  • apps/commerce-api/src/main/resources/application.yml
  • modules/kafka/src/main/resources/kafka.yml
  • apps/commerce-streamer/src/main/resources/application.yml
๐Ÿ“š 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/interfaces/api/listener/ProductLikeEventListener.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.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-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java
๐Ÿ“š Learning: 2025-12-07T12:45:45.556Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 52
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/payment/Payment.kt:160-181
Timestamp: 2025-12-07T12:45:45.556Z
Learning: In the Payment.confirmPayment method in com.loopers.domain.payment.Payment, the when-branch order is intentionally designed with business logic checks before timeout: (1) check SUCCESS/FAILED status first, (2) check for null match (business failure), (3) check timeout only as last resort for PENDING transactions. If matched transaction is PENDING and not timed out, no branch matches and payment stays IN_PROGRESS for scheduler retry. This design prioritizes business outcome determination over time-based fallback.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/PaymentEventListener.java
๐Ÿงฌ Code graph analysis (7)
apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DlqMessage.java (2)
pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt (1)
  • name (14-87)
pg-simulator/src/main/kotlin/com/loopers/support/error/ErrorType.kt (1)
  • status (5-11)
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventRepositoryImpl.java (1)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DlqMessageRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-44)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DlqMessageRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-44)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledRepositoryImpl.java (1)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DlqMessageRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-44)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/auditlog/AuditLogRepositoryImpl.java (1)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DlqMessageRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-44)
apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DlqMessageService.java (1)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DlqMessageRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-44)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DlqConsumer.java (1)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DlqMessageRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-44)
๐Ÿ”‡ Additional comments (45)
modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java (2)

38-41: DLQ ์ƒ์ˆ˜ ์ •์˜ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์žฌ์‹œ๋„ ๊ฐ„๊ฒฉ 1์ดˆ, ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„ ์„ค์ •์ด ๋ช…ํ™•ํ•˜๊ฒŒ ์ƒ์ˆ˜๋กœ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


87-109: ๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ์™€ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ ํ†ตํ•ฉ ํ™•์ธ ํ•„์š”

๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ(setBatchListener(true))์™€ DefaultErrorHandler ์กฐํ•ฉ ์‹œ, ๋ฐฐ์น˜ ๋‚ด ๋‹จ์ผ ๋ ˆ์ฝ”๋“œ ์‹คํŒจ ์‹œ ์ „์ฒด ๋ฐฐ์น˜๊ฐ€ ์žฌ์‹œ๋„๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์˜๋„๋œ ๋™์ž‘์ธ์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๋งŒ์•ฝ ๊ฐœ๋ณ„ ๋ ˆ์ฝ”๋“œ ๋‹จ์œ„ ์žฌ์‹œ๋„๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด, ์„œ๋น„์Šค ๋ ˆ์ด์–ด์—์„œ ๊ฐœ๋ณ„ ์ฒ˜๋ฆฌ ๋˜๋Š” FallbackBatchErrorHandler ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)

93-96: ๋ฉฑ๋“ฑ์„ฑ ๊ฐ€๋“œ ์ถ”๊ฐ€ ์Šน์ธ

์ฟ ํฐ ์ค‘๋ณต ์‚ฌ์šฉ์„ ๋ฐฉ์ง€ํ•˜๋Š” ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. cancelOrder(112-115์ค„)์™€ completeOrder(150-153์ค„) ๋ฉ”์„œ๋“œ์˜ ํŒจํ„ด๊ณผ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ž‘์„ฑ๋˜์—ˆ๊ณ , ๋‚™๊ด€์  ๋ฝ(92์ค„)๊ณผ ํ•จ๊ป˜ ์ด์ค‘ ๋ฐฉ์–ด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ์žฌ์ฒ˜๋ฆฌ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ์•ˆ์ „์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ์ข‹์€ ๋ฐฉ์–ด์  ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์‚ฌ๋ก€์ž…๋‹ˆ๋‹ค.

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

5-5: LGTM!

Kafka ๋ชจ๋“ˆ ์˜์กด์„ฑ๊ณผ ํ…Œ์ŠคํŠธ ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. testFixtures์™€ spring-kafka-test๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์ง€์›ํ•˜๋Š” ๊ตฌ์„ฑ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 26-28

apps/commerce-streamer/src/main/java/com/loopers/interfaces/dto/ProductViewPayload.java (1)

3-7: LGTM!

Kafka ๋ฉ”์‹œ์ง€ payload๋ฅผ ์œ„ํ•œ ๊ฐ„๊ฒฐํ•œ record ์ •์˜์ž…๋‹ˆ๋‹ค. eventId์™€ productId ํ•„๋“œ ๊ตฌ์„ฑ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

28-36: Kafka ์„ค์ • ๊ตฌ์„ฑ ํ™•์ธ๋จ

ํ† ํ”ฝ ์ด๋ฆ„๊ณผ consumer group ์„ค์ •์ด ์ ์ ˆํ•˜๊ฒŒ ์ •์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/application/auditlog/AuditLogCommand.java (1)

11-20: ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์—์„œ ๋Œ€๋ถ€๋ถ„์˜ ํ•„๋“œ๊ฐ€ null๋กœ ์„ค์ •๋จ

of() ๋ฉ”์„œ๋“œ๊ฐ€ eventId, userId, targetType, targetId๋ฅผ ๋ชจ๋‘ null๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ•„๋“œ๋“ค์ด downstream ์ฒ˜๋ฆฌ(์˜ˆ: AuditLogService, AuditLog.create())์—์„œ ํ•„์ˆ˜์ธ ๊ฒฝ์šฐ NullPointerException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ์‚ฌ์šฉ์ฒ˜์—์„œ null ํ—ˆ์šฉ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.

apps/commerce-streamer/src/main/java/com/loopers/domain/auditlog/AuditLog.java (1)

10-58: LGTM!

JPA ์—”ํ‹ฐํ‹ฐ ๊ตฌ์กฐ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. eventId์— unique ์ œ์•ฝ ์กฐ๊ฑด์ด ์žˆ์–ด ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€์— ํšจ๊ณผ์ ์ด๋ฉฐ, payload์— TEXT ํƒ€์ž…์„ ์‚ฌ์šฉํ•œ ๊ฒƒ๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/LikeChangedDto.java (1)

5-17: LGTM!

์ด๋ฒคํŠธ DTO ๊ตฌ์กฐ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. UUID.randomUUID()๋ฅผ ์‚ฌ์šฉํ•œ eventId ์ƒ์„ฑ์ด ์ ํ•ฉํ•˜๋ฉฐ, ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ํŒจํ„ด์ด ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEvent.java (2)

54-74: Outbox ํŒจํ„ด ๊ตฌํ˜„์ด ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์—ฌ ์ผ๊ด€๋œ ์ดˆ๊ธฐ ์ƒํƒœ(PENDING, retryCount=0)๋ฅผ ๋ณด์žฅํ•˜๊ณ , UUID ๊ธฐ๋ฐ˜ ID ์ƒ์„ฑ์œผ๋กœ ๋ถ„์‚ฐ ํ™˜๊ฒฝ์—์„œ์˜ ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ์œ„ํ•ด Clock์„ ์ฃผ์ž…๋ฐ›๋Š” ๋ฐฉ์‹๋„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์œผ๋‚˜, Outbox ์ด๋ฒคํŠธ์˜ ๊ฒฝ์šฐ ์ƒ์„ฑ ์‹œ์  ๊ธฐ๋ก์ด ์ฃผ ๋ชฉ์ ์ด๋ฏ€๋กœ ํ˜„์žฌ ๊ตฌํ˜„๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.


81-89: ์ƒํƒœ ์ „์ด ๋ฉ”์„œ๋“œ ๊ฒ€ํ† 

markAsFailed์—์„œ retryCount๋ฅผ ์ฆ๊ฐ€์‹œํ‚ค๊ณ , markForRetry์—์„œ๋Š” ์ƒํƒœ๋งŒ PENDING์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ markForRetry ํ˜ธ์ถœ ์‹œ lastError๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์˜๋„๋œ ๊ฒƒ์ธ์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋””๋ฒ„๊น… ๋ชฉ์ ์œผ๋กœ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด๋ผ๋ฉด ํ˜„์žฌ ๊ตฌํ˜„์ด ๋งž์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/auditlog/AuditLogRepository.java (1)

3-6: LGTM!

๋„๋ฉ”์ธ ๋ ˆ์ด์–ด์— ํฌํŠธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ•˜๊ณ , ์ธํ”„๋ผ์ŠคํŠธ๋Ÿญ์ฒ˜ ๋ ˆ์ด์–ด์—์„œ ๊ตฌํ˜„ํ•˜๋Š” ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด์„ ์ž˜ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/application/metrics/MetricsType.java (1)

3-7: LGTM!

๋ฉ”ํŠธ๋ฆญ ํƒ€์ž…์„ enum์œผ๋กœ ์ •์˜ํ•˜์—ฌ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandledDomainType.java (1)

3-6: LGTM!

์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋„๋ฉ”์ธ ํƒ€์ž…์„ enum์œผ๋กœ ์ •์˜ํ•˜์—ฌ ํƒ€์ž… ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ ์‹œ ๋„๋ฉ”์ธ๋ณ„ ๋ถ„๋ฅ˜์— ์ ํ•ฉํ•œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/CommerceStreamerContextTest.java (1)

6-13: LGTM!

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

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

46-53: LGTM!

Kafka ํ† ํ”ฝ ๋„ค์ด๋ฐ์ด ์ผ๊ด€๋˜๊ณ  ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ์œ ํ˜•๋ณ„๋กœ ํ† ํ”ฝ์ด ์ž˜ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์–ด ํ–ฅํ›„ ํ™•์žฅ ๋ฐ ๊ด€๋ฆฌ์— ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/auditlog/AuditLogJpaRepository.java (1)

1-7: LGTM!

ํ‘œ์ค€ Spring Data JPA ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค๋กœ, audit log ์˜์†ํ™”๋ฅผ ์œ„ํ•œ ๊น”๋”ํ•œ ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/ProductViewedDto.java (1)

5-14: LGTM!

Record ๊ธฐ๋ฐ˜ DTO์™€ ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ ๊ตฌํ˜„์ด ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค. UUID๋ฅผ ์ด์šฉํ•œ eventId ์ƒ์„ฑ์€ ๋ถ„์‚ฐ ํ™˜๊ฒฝ์—์„œ ์ด๋ฒคํŠธ ๊ณ ์œ ์„ฑ์„ ๋ณด์žฅํ•˜๊ธฐ์— ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/domain/payment/event/PaymentSucceededEvent.java (1)

15-15: LGTM!

transactionId ํ•„๋“œ ์ถ”๊ฐ€๊ฐ€ ๊ธฐ์กด ํŒจํ„ด๊ณผ ์ผ๊ด€๋˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ์ œ ์ด๋ฒคํŠธ์— ํŠธ๋žœ์žญ์…˜ ์‹๋ณ„์ž๋ฅผ ํฌํ•จ์‹œํ‚ค๋Š” ๊ฒƒ์€ ์ถ”์ ์„ฑ๊ณผ ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์— ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.

Also applies to: 26-26

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledJpaRepository.java (1)

6-9: LGTM!

๋ฉฑ๋“ฑ์„ฑ ๊ตฌํ˜„์„ ์œ„ํ•œ ๊ฐ„๊ฒฐํ•œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. existsByEventId ๋ฉ”์„œ๋“œ๋Š” Spring Data JPA ๋„ค์ด๋ฐ ๊ทœ์น™์„ ๋”ฐ๋ฅด๋ฉฐ, PR ๋ชฉํ‘œ์— ๋ช…์‹œ๋œ ๋Œ€๋กœ ์ค‘๋ณต ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandledService.java (1)

13-22: LGTM!

๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์„ ์œ„ํ•œ ์ž˜ ์„ค๊ณ„๋œ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค:

  • ์กฐํšŒ ๋ฉ”์„œ๋“œ์— readOnly = true ํŠธ๋žœ์žญ์…˜ ์ ์šฉ์œผ๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ์“ฐ๊ธฐ ๋ฉ”์„œ๋“œ๋Š” ๋ณ„๋„ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ถ„๋ฆฌ
  • ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•œ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ์œผ๋กœ ์ผ๊ด€์„ฑ ์œ ์ง€

ํ•™์Šต๋œ ๋ฐ”์™€ ๊ฐ™์ด, ์ด ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ๋Š” ๋„๋ฉ”์ธ ๊ณ„์ธต์˜ Service ํด๋ž˜์Šค์— @Service ์–ด๋…ธํ…Œ์ด์…˜ ์‚ฌ์šฉ์ด ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java (1)

6-11: LGTM!

๊น”๋”ํ•œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. Optional ๋ฐ˜ํ™˜ ํƒ€์ž… ์‚ฌ์šฉ๊ณผ ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ์ด ์ ์ ˆํ•˜๋ฉฐ, ํ•™์Šต๋œ ๋ฐ”์™€ ๊ฐ™์ด ์ด ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ๋Š” ๋„๋ฉ”์ธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์—์„œ Spring Data ํƒ€์ž… ์‚ฌ์šฉ์ด ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandledRepository.java (1)

1-8: LGTM!

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

apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEventRepository.java (1)

1-9: LGTM!

Outbox ํŒจํ„ด์„ ์ง€์›ํ•˜๋Š” ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋Œ€๊ธฐ ์ค‘์ธ ์ด๋ฒคํŠธ ์กฐํšŒ(findPendingEvents)์™€ ์žฌ์‹œ๋„ ๋Œ€์ƒ ์กฐํšŒ(findFailedEventsForRetry)๋ฅผ ์œ„ํ•œ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)

11-26: LGTM!

๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„์ฒด๊ฐ€ JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋กœ ๋‹จ์ˆœ ์œ„์ž„ํ•˜๋Š” ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ๋‹ค๋ฅธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„์ฒด(์˜ˆ: DlqMessageRepositoryImpl)์™€ ์ผ๊ด€๋œ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๊ณ  ์žˆ์–ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/UserActionDto.java (1)

5-21: LGTM!

์‚ฌ์šฉ์ž ์•ก์…˜ ์ด๋ฒคํŠธ๋ฅผ ์œ„ํ•œ DTO๊ฐ€ ๊ฐ„๊ฒฐํ•˜๊ณ  ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. of() ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์—์„œ UUID๋ฅผ ์ž๋™ ์ƒ์„ฑํ•˜์—ฌ ์ด๋ฒคํŠธ ID ์ค‘๋ณต์„ ๋ฐฉ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. Record ์‚ฌ์šฉ์œผ๋กœ ๋ถˆ๋ณ€์„ฑ๋„ ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/StockChangedDto.java (1)

11-18: LGTM!

UUID๋ฅผ ์‚ฌ์šฉํ•œ eventId ์ƒ์„ฑ ๋ฐฉ์‹์ด ์ ์ ˆํ•˜๋ฉฐ, ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandled.java (1)

10-28: LGTM!

์—”ํ‹ฐํ‹ฐ ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  JPA ๋งคํ•‘์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. Enum์„ STRING์œผ๋กœ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹๋„ ๊ฐ€๋…์„ฑ ์ธก๋ฉด์—์„œ ์ข‹์€ ์„ ํƒ์ž…๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/auditlog/AuditLogRepositoryImpl.java (1)

8-17: LGTM!

ํ‘œ์ค€ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„ ํŒจํ„ด์„ ์ž˜ ๋”ฐ๋ฅด๊ณ  ์žˆ์œผ๋ฉฐ, JpaRepository๋กœ์˜ ์œ„์ž„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsId.java (1)

13-32: LGTM!

๋ณตํ•ฉ ํ‚ค ๊ตฌํ˜„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. Serializable ๊ตฌํ˜„, @Embeddable ์• ๋…ธํ…Œ์ด์…˜, ๊ทธ๋ฆฌ๊ณ  @EqualsAndHashCode ์‚ฌ์šฉ์ด JPA ๋ณตํ•ฉ ํ‚ค ์š”๊ตฌ์‚ฌํ•ญ์„ ์ถฉ์กฑํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledRepositoryImpl.java (1)

8-23: LGTM!

๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„์ด ํ”„๋กœ์ ํŠธ์˜ ํ‘œ์ค€ ํŒจํ„ด์„ ์ž˜ ๋”ฐ๋ฅด๊ณ  ์žˆ์œผ๋ฉฐ, ๊น”๋”ํ•˜๊ฒŒ JpaRepository๋กœ ์œ„์ž„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DlqMessageRepository.java (1)

6-19: LGTM!

๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  DLQ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ์— ํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ๋“ค์ด ์ž˜ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DlqConsumer.java (1)

63-68: LGTM!

ํ† ํ”ฝ๋ช… ์ถ”์ถœ ๋กœ์ง์ด ๋ช…ํ™•ํ•˜๊ณ  ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. .DLT ์ ‘๋ฏธ์‚ฌ๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉ์‹์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/interfaces/DlqConsumerTest.java (3)

17-26: LGTM!

ํ…Œ์ŠคํŠธ ์„ค์ •์ด ๊น”๋”ํ•˜๊ณ , mock ์‚ฌ์šฉ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. makeRecord ํ—ฌํผ ๋ฉ”์„œ๋“œ๋กœ ์ค‘๋ณต์„ ์ค„์ธ ๊ฒƒ๋„ ์ข‹์Šต๋‹ˆ๋‹ค.


149-175: ํ…Œ์ŠคํŠธ๊ฐ€ ํ˜„์žฌ ๊ตฌํ˜„์„ ์ •ํ™•ํžˆ ๊ฒ€์ฆํ•˜์ง€๋งŒ, DlqConsumer ์ˆ˜์ • ์‹œ ์—…๋ฐ์ดํŠธ ํ•„์š”

์ด ํ…Œ์ŠคํŠธ๋Š” ํ˜„์žฌ DlqConsumer์˜ ๋™์ž‘(์ €์žฅ ์‹คํŒจ ์‹œ์—๋„ acknowledge ํ˜ธ์ถœ)์„ ์ •ํ™•ํžˆ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ DlqConsumer.java์—์„œ ์ œ๊ธฐํ•œ ํฌ๋ฆฌํ‹ฐ์ปฌ ์ด์Šˆ(์ €์žฅ ์‹คํŒจ ์‹œ ๋ฌด์กฐ๊ฑด acknowledge)๊ฐ€ ์ˆ˜์ •๋˜๋ฉด, ์ด ํ…Œ์ŠคํŠธ๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


28-142: LGTM!

ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ๋งค์šฐ ํฌ๊ด„์ ์ž…๋‹ˆ๋‹ค. ๋‹จ์ผ ๋ฉ”์‹œ์ง€, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ, ํŒŒ๋ผ๋ฏธํ„ฐ ์ „๋‹ฌ, ํ† ํ”ฝ ์ถ”์ถœ ๋“ฑ ๋ชจ๋“  ์ฃผ์š” ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ž˜ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ArgumentCaptor๋ฅผ ํ™œ์šฉํ•œ ๊ฒ€์ฆ ๋ฐฉ์‹๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 194-226

apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/PaymentEventListener.java (1)

141-162: LGTM - ์‹คํŒจ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋กœ์ง

๊ฒฐ์ œ ์‹คํŒจ ์ด๋ฒคํŠธ๋ฅผ Kafka๋กœ ๋ฐœํ–‰ํ•˜๋Š” ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ์•ž์„œ ์ฝ”๋ฉ˜ํŠธํ•œ ๋ณต๊ตฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ฐ•ํ™” ์‚ฌํ•ญ์ด ์ด ๋ฉ”์„œ๋“œ์—๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (1)

28-55: LGTM!

ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์™€ ๋ฎคํ…Œ์ดํ„ฐ ๋ฉ”์„œ๋“œ๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ธํƒ€ ๊ธฐ๋ฐ˜ ๋ฉ”ํŠธ๋ฆญ ์ถ”์  ์„ค๊ณ„๋กœ, ์ฆ๊ฐ ์—ฐ์‚ฐ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/UserActionEventListener.java (1)

55-73: LGTM!

publishKafkaEvent ๋ฉ”์„œ๋“œ์˜ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. PRODUCT_VIEW ์ด๋ฒคํŠธ๋ฅผ ๋ฉ”ํŠธ๋ฆญ์šฉ๊ณผ ๊ฐ์‚ฌ ๋กœ๊ทธ์šฉ ํ† ํ”ฝ์— ๊ฐ๊ฐ ๋ฐœํ–‰ํ•˜๋Š” ์„ค๊ณ„๋„ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DlqMessageRepositoryImpl.java (1)

15-43: ์œ„์ž„ ๊ตฌํ˜„ LGTM!

JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋กœ์˜ ์œ„์ž„ ํŒจํ„ด์ด ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. findPendingMessagesForRetry์—์„œ ์žฌ์‹œ๋„ ๋Œ€์ƒ ๋ฉ”์‹œ์ง€๋ฅผ ์กฐํšŒํ•˜๋Š” ๋กœ์ง๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

10-29: LGTM!

Repository ๊ตฌํ˜„์ด ๊น”๋”ํ•˜๊ณ , JPA ๋ ˆํฌ์ง€ํ† ๋ฆฌ์— ๋Œ€ํ•œ ์œ„์ž„ ํŒจํ„ด์ด DlqMessageRepositoryImpl๊ณผ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java (1)

48-51: LGTM!

getOrCreateMetrics ํ—ฌํผ ๋ฉ”์„œ๋“œ๊ฐ€ ๊น”๋”ํ•˜๊ฒŒ ์ถ”์ถœ๋˜์—ˆ๊ณ , ๋ชจ๋“  ํ˜ธ์ถœ์ž๊ฐ€ ์ˆ˜์ • ํ›„ save()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/OrderEventProducer.java (1)

50-65: Fallback์—์„œ ์ด๋ฒคํŠธ ์ง€์†์„ฑ ์ฒ˜๋ฆฌ ๊ณ ๋ ค

Fallback ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜„์žฌ ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์žฌ์‹œ๋„ ์‹คํŒจ ์‹œ ์ด๋ฒคํŠธ๊ฐ€ ์œ ์‹ค๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, Outbox ํŒจํ„ด๊ณผ ์—ฐ๊ณ„ํ•˜๊ฑฐ๋‚˜ ๋ณ„๋„ ์ €์žฅ์†Œ์— ์‹คํŒจ ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

PR ์„ค๋ช…์— Outbox ํŒจํ„ด์ด ์–ธ๊ธ‰๋˜์–ด ์žˆ๋Š”๋ฐ, ์ด Producer๊ฐ€ Outbox๋ฅผ ํ†ตํ•ด ํ˜ธ์ถœ๋˜๋Š”์ง€, ๋˜๋Š” ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„๋ฅผ ์œ„ํ•œ ๋ณ„๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ์žˆ๋Š”์ง€ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/OrderEventListener.java (1)

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

BEFORE_COMMIT ๋‹จ๊ณ„์—์„œ outbox์— ์ €์žฅํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. orderId๋ฅผ partition key๋กœ ์‚ฌ์šฉํ•˜์—ฌ ๋™์ผ ์ฃผ๋ฌธ์˜ ์ด๋ฒคํŠธ ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜๋Š” ์„ค๊ณ„๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/PaymentEventProducer.java (1)

14-19: ์ „๋ฐ˜์ ์ธ ๊ตฌ์กฐ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

@RequiredArgsConstructor๋ฅผ ํ†ตํ•œ ์˜์กด์„ฑ ์ฃผ์ž…๊ณผ @Value๋ฅผ ํ†ตํ•œ ํ† ํ”ฝ ์„ค์ • ๋ถ„๋ฆฌ๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. orderId๋ฅผ partition key๋กœ ์‚ฌ์šฉํ•˜์—ฌ ๋™์ผ ์ฃผ๋ฌธ์˜ ๊ฒฐ์ œ ์ด๋ฒคํŠธ ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜๋Š” ์„ค๊ณ„๋„ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

@looper88888
Copy link
Contributor

โœ… Checklist

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

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

@coderabbitai ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ™•์ธํ•ด

@coderabbitai
Copy link

coderabbitai bot commented Dec 20, 2025

Tip

For best results, initiate chat on the files or code changes.

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Check domain event DTOs structure and design
echo "=== 1. Event DTOs (Domain Event Design) ==="
fd -e java -p "EventDto" apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/dto/ --exec cat {}

echo -e "\n=== 2. Producer Partition Key Usage ==="
# Check how producers use partition keys
rg -A 5 "kafkaTemplate.send" apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/

echo -e "\n=== 3. Retry and Fallback Configuration ==="
# Check retry annotations and fallback methods
rg "@Retry|fallback" apps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/

echo -e "\n=== 4. EventHandled Implementation ==="
# Check event_handled usage in consumers
cat apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandled.java

echo -e "\n=== 5. EventHandled Service Usage ==="
rg "eventHandledService|EventHandledService" apps/commerce-streamer/src/main/java/com/loopers/application/

echo -e "\n=== 6. Cache Invalidation on Stock Change ==="
# Check cache invalidation in ProductMetricsFacade
cat apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java | grep -A 30 "processStockMetrics\|invalidateProductCache"

echo -e "\n=== 7. Idempotency Tests ==="
# Look for duplicate message tests
rg -l "duplicate|idempotent|event.*handled" apps/commerce-streamer/src/test/

Length of output: 19677


๐Ÿ Script executed:

#!/bin/bash
# Check Outbox pattern for publish failure handling
echo "=== Outbox Pattern for Failure Handling ==="
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.java | head -80

echo -e "\n=== Resilience4j Retry Configuration ==="
cat apps/commerce-api/src/main/resources/application.yml | grep -A 10 "kafkaProducer:"

Length of output: 2612


๐Ÿ Script executed:

#!/bin/bash
# Check test files for duplicate message/idempotency testing
echo "=== Consumer Test - Duplicate Message Handling ==="
cat apps/commerce-streamer/src/test/java/com/loopers/interfaces/ProductMetricsConsumerTest.java | grep -B 5 -A 20 "batch\|duplicate"

echo -e "\n=== Facade Idempotency Check in Tests ==="
rg "isEventHandled|processAuditLog|processLikeMetrics|processStockMetrics" apps/commerce-streamer/src/test/ -A 5

echo -e "\n=== Domain Event Listeners (Order events) ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/OrderEventListener.java | grep -B 3 -A 10 "@EventListener\|@TransactionalEventListener"

Length of output: 9375


@looper88888 ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค! ๊ฐ ํ•ญ๋ชฉ๋ณ„๋กœ ๊ตฌํ˜„ ์ƒํƒœ๋ฅผ ์ •๋ฆฌํ–ˆ์–ด์š”.

๐ŸŽพ Producer ์ฒดํฌ๋ฆฌ์ŠคํŠธ (4/4 ์™„๋ฃŒ)

โœ… ๋„๋ฉ”์ธ(์• ํ”Œ๋ฆฌ์ผ€์ด์…˜) ์ด๋ฒคํŠธ ์„ค๊ณ„

  • OrderEventDto, PaymentEventDto, StockChangedDto, LikeChangedDto, ProductViewedDto, UserActionDto ๋“ฑ ์ž˜ ์„ค๊ณ„๋จ
  • ๊ฐ DTO์— factory ๋ฉ”์„œ๋“œ (.created(), .success() ๋“ฑ) ์ œ๊ณต

โœ… Producer ์•ฑ์—์„œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰

  • OrderEventProducer: ์ฃผ๋ฌธ ์ƒ์„ฑ/์™„๋ฃŒ/์‹คํŒจ ์ด๋ฒคํŠธ
  • PaymentEventProducer: ๊ฒฐ์ œ ์„ฑ๊ณต/์‹คํŒจ/๋Œ€๊ธฐ ์ด๋ฒคํŠธ
  • StockChangedEventProducer: ์žฌ๊ณ  ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ
  • LikeChangedEventProducer: ์ข‹์•„์š” ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ
  • ProductViewedEventProducer: ์ƒํ’ˆ ์กฐํšŒ ์ด๋ฒคํŠธ
  • UserActionEventProducer: ์œ ์ € ํ–‰๋™ ์ด๋ฒคํŠธ

โœ… PartitionKey ๊ธฐ๋ฐ˜์˜ ์ด๋ฒคํŠธ ์ˆœ์„œ ๋ณด์žฅ

// ๋ชจ๋“  Producer์—์„œ ์ผ๊ด€๋œ ํ‚ค ์‚ฌ์šฉ
kafkaTemplate.send(topic, orderId.toString(), event);      // Order/Payment
kafkaTemplate.send(topic, productId.toString(), event);    // Product
kafkaTemplate.send(topic, userId.toString(), event);       // User

โœ… ๋ฉ”์„ธ์ง€ ๋ฐœํ–‰์ด ์‹คํŒจํ–ˆ์„ ๊ฒฝ์šฐ์— ๋Œ€ํ•ด ๊ณ ๋ฏผํ•ด๋ณด๊ธฐ

  • Resilience4j Retry: ๋ชจ๋“  Producer ๋ฉ”์„œ๋“œ์— @Retry(name = "kafkaProducer") ์ ์šฉ (3ํšŒ ์žฌ์‹œ๋„, 1์ดˆ ๋Œ€๊ธฐ)
  • Fallback ๋ฉ”์„œ๋“œ: ์žฌ์‹œ๋„ ์‹คํŒจ ์‹œ ๋กœ๊น… ๋ฐ ์—๋Ÿฌ ํ•ธ๋“ค๋ง
  • Outbox ํŒจํ„ด: OutboxEventPublisher๊ฐ€ 1์ดˆ๋งˆ๋‹ค ์‹คํŒจํ•œ ์ด๋ฒคํŠธ๋ฅผ ์žฌ๋ฐœํ–‰ (์ตœ๋Œ€ 5ํšŒ ์žฌ์‹œ๋„)
  • DLQ ์ฒ˜๋ฆฌ: DeadLetterPublishingRecoverer๋กœ ์ตœ์ข… ์‹คํŒจ ๋ฉ”์‹œ์ง€๋ฅผ .DLT ํ† ํ”ฝ์œผ๋กœ ์ „์†ก

โšพ Consumer ์ฒดํฌ๋ฆฌ์ŠคํŠธ (4/4 ์™„๋ฃŒ)

โœ… Consumer ๊ฐ€ Metrics ์ง‘๊ณ„ ์ฒ˜๋ฆฌ

  • ProductMetricsConsumer โ†’ ProductMetricsFacade โ†’ ProductMetricsService
  • Like/Stock/View ๋ฉ”ํŠธ๋ฆญ์„ product_metrics ํ…Œ์ด๋ธ”์— ์ง‘๊ณ„
  • ๊ฐ ๋ฉ”ํŠธ๋ฆญ๋ณ„๋กœ delta ํ•„๋“œ (likesDelta, salesDelta, viewsDelta) ์—…๋ฐ์ดํŠธ

โœ… event_handled ํ…Œ์ด๋ธ”์„ ํ†ตํ•œ ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ ๊ตฌํ˜„

// ProductMetricsFacade, AuditLogFacade ๋ชจ๋‘ ๋™์ผํ•œ ํŒจํ„ด ์ ์šฉ
if (eventHandledService.isEventHandled(command.eventId())) {
    return;  // ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ์ด๋ฒคํŠธ๋Š” ์Šคํ‚ต
}
// ... ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ ...
eventHandledService.saveEventHandled(command.eventId(), DOMAIN_TYPE, eventType);

โœ… ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์ƒํ’ˆ ์บ์‹œ ๊ฐฑ์‹ 

public void processStockMetrics(ProductMetricsCommand command) {
    // ... ๋ฉ”ํŠธ๋ฆญ ์ฒ˜๋ฆฌ ...
    invalidateProductCache(command.productId());  // ์บ์‹œ ๋ฌดํšจํ™”
}

private void invalidateProductCache(Long productId) {
    // ์ƒํ’ˆ ์ƒ์„ธ ์บ์‹œ ์‚ญ์ œ: product:*:detail:{productId}
    // ์ƒํ’ˆ ๋ชฉ๋ก ์บ์‹œ ์‚ญ์ œ: product:*:list:*
}

โš ๏ธ ์ค‘๋ณต ๋ฉ”์„ธ์ง€ ์žฌ์ „์†ก ํ…Œ์ŠคํŠธ โ†’ ์ตœ์ข… ๊ฒฐ๊ณผ๊ฐ€ ํ•œ ๋ฒˆ๋งŒ ๋ฐ˜์˜๋˜๋Š”์ง€ ํ™•์ธ

  • ํ˜„์žฌ ์ƒํƒœ: Unit ํ…Œ์ŠคํŠธ๊ฐ€ ์กด์žฌํ•˜์ง€๋งŒ ์‹ค์ œ ์ค‘๋ณต ๋ฉ”์‹œ์ง€ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š์Œ
  • ProductMetricsConsumerTest๋Š” Consumer โ†’ Facade ํ˜ธ์ถœ๋งŒ ๊ฒ€์ฆ
  • ๊ถŒ์žฅ์‚ฌํ•ญ: Integration ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ ํ•„์š”
// ์ถ”์ฒœํ•˜๋Š” ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค
@Test
void ๋™์ผํ•œ_eventId_๋‘๋ฒˆ_์ „์†ก์‹œ_ํ•œ๋ฒˆ๋งŒ_์ฒ˜๋ฆฌ๋จ() {
    String sameEventId = "evt-duplicate-001";
    
    // ์ฒซ ๋ฒˆ์งธ ์ „์†ก
    consumer.listen(record(sameEventId), ack);
    
    // ๋‘ ๋ฒˆ์งธ ์ „์†ก (์ค‘๋ณต)
    consumer.listen(record(sameEventId), ack);
    
    // ๊ฒ€์ฆ: event_handled ํ…Œ์ด๋ธ”์— 1๊ฐœ๋งŒ ์กด์žฌ
    // ๊ฒ€์ฆ: metrics ๊ฐ’์ด 1๋ฒˆ๋งŒ ์ฆ๊ฐ€
}

๐Ÿ“ ์š”์•ฝ

์™„๋ฃŒ๋œ ํ•ญ๋ชฉ: 8๊ฐœ ์ค‘ 8๊ฐœ โœ…
์ฃผ์˜ ์‚ฌํ•ญ: ์ค‘๋ณต ๋ฉ”์‹œ์ง€ ํ…Œ์ŠคํŠธ๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋กœ ๋ณด์™„ํ•˜๋ฉด ๋” ์ข‹์Šต๋‹ˆ๋‹ค.

์ „๋ฐ˜์ ์œผ๋กœ Kafka ํ†ตํ•ฉ์ด ๋งค์šฐ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค! ํŠนํžˆ Outbox ํŒจํ„ด, DLQ ์ฒ˜๋ฆฌ, EventHandled ๊ธฐ๋ฐ˜ ๋ฉฑ๋“ฑ์„ฑ ๊ตฌํ˜„์ด ์ธ์ƒ์ ์ด๋„ค์š” ๐Ÿ‘


๐Ÿง  Learnings used
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.

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.

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.

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.

@rnqhstmd rnqhstmd merged commit e7bf613 into Loopers-dev-lab:rnqhstmd Dec 24, 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.

2 participants