-
Notifications
You must be signed in to change notification settings - Fork 34
[volume - 8] Decoupling with Kafka #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[volume - 8] Decoupling with Kafka #193
Conversation
- event_outbox: Producer Outbox ํจํด (At Least Once ๋ณด์ฅ) - event_inbox: Consumer Inbox ํจํด (๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ) - dead_letter_queue: ์คํจ ๋ฉ์์ง ์ฒ๋ฆฌ - product_metrics: ์ค์๊ฐ ์ง๊ณ ํ ์ด๋ธ (์ข์์/์กฐํ์/์ฃผ๋ฌธ์/ํ๋งค๊ธ์ก)
- KRaft ๋ชจ๋ ํ์ฑํ (Zookeeper ์ ๊ฑฐ) - Producer: At Least Once ์ค์ (acks=all, idempotence=true) - Consumer: ๋ฐฐ์น ์ฒ๋ฆฌ ๋ฐ ์๋ ์คํ์ ์ปค๋ฐ - ํ ์คํธ ํ๊ฒฝ: auto.offset.reset=earliest ์ค์ - Outbox Publisher ์ค์ผ์ค๋ฌ ํ์ฑํ
- OutboxEventService: ์ด๋ฒคํธ๋ฅผ Outbox ํ ์ด๋ธ์ ์ ์ฅ - OutboxEventPublisher: ์ค์ผ์ค๋ฌ๋ก ๋ฏธ๋ฐํ ์ด๋ฒคํธ ์กฐํ ๋ฐ Kafka ๋ฐํ - At Least Once ๋ณด์ฅ: ํธ๋์ญ์ ์ปค๋ฐ ํ ์ด๋ฒคํธ ๋ฐํ - ์คํจ ์ ์ฌ์๋: ์ค์ผ์ค๋ฌ๊ฐ ์ฃผ๊ธฐ์ ์ผ๋ก ๋ฏธ๋ฐํ ์ด๋ฒคํธ ์ฌ๋ฐํ
Consumer ๊ตฌํ: - CatalogEventConsumer: ์ข์์/์กฐํ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ - OrderEventConsumer: ์ฃผ๋ฌธ/๊ฒฐ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ - Inbox ํจํด์ผ๋ก ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ (์ค๋ณต ๋ฉ์์ง ๋ฐฉ์ง) - ๋ฐฐ์น ์ฒ๋ฆฌ๋ก ์ฑ๋ฅ ์ต์ ํ - DLQ(Dead Letter Queue)๋ก ์คํจ ๋ฉ์์ง ์ฒ๋ฆฌ ProductMetrics ์ง๊ณ: - ์ข์์ ์, ์กฐํ์, ์ฃผ๋ฌธ ์, ํ๋งค ๊ธ์ก ์ค์๊ฐ ์ง๊ณ - ๋ณ๋ ํ ์ด๋ธ๋ก ์กฐํ ์ฑ๋ฅ ์ต์ ํ
- OrderFacade: ์ฌ๊ณ ์ฐจ๊ฐ ํ ์ฌ๊ณ ๊ฐ 0์ด ๋๋ฉด ์บ์ ์ญ์ - ProductCacheService: Spring Cache ์ญ์ ๋ฉ์๋ ์ถ๊ฐ - ๋ค์ ์กฐํ ์ DB์์ ์ต์ ๋ฐ์ดํฐ(์ฌ๊ณ 0)๋ฅผ ๊ฐ์ ธ์ ์บ์ ๊ฐฑ์
ํ ์คํธ ์์ฑ: - CatalogEventConsumerTest: ์ข์์/์กฐํ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๊ฒ์ฆ (6๊ฐ) - OrderEventConsumerTest: ์ฃผ๋ฌธ/๊ฒฐ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๊ฒ์ฆ (7๊ฐ) - EventInboxServiceTest: ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ ๊ฒ์ฆ - ProductMetricsServiceTest: ์ง๊ณ ๋ก์ง ๊ฒ์ฆ - ConsumerSmokeTest: Bean ์์ฑ ๋ฐ Context ๋ก๋ฉ ๊ฒ์ฆ ํ ์คํธ ์ ๋ต: - Consumer ๋ก์ง ๋จ์ ํ ์คํธ (Mock ์ฌ์ฉ) - ์ค์ Kafka ์์ด ๋น์ฆ๋์ค ๋ก์ง๋ง ๊ฒ์ฆ - ๋ฌธ์๋ก์์ ํ ์คํธ: ๋ช ํํ ์ค๋ช ๊ณผ ์๋๋ฆฌ์ค
- IntelliJ Optimize Imports ์ ์ฉ - ์ฌ์ฉํ์ง ์๋ import ์ ๊ฑฐ - import ์์ ์ ๋ฆฌ (static import ์ฐ์ )
- Retry์ Circuit Breaker ์ํธ์์ฉ์ ๊ณ ๋ คํ ๊ฒ์ฆ ์์ - times(6)์์ atLeast(6)์ผ๋ก ๋ณ๊ฒฝํ์ฌ ์ฌ์๋ ๋ก์ง ๋ฐ์ - Mock ์ค์ ๋๋ฝ์ผ๋ก ์ธํ NullPointerException ํด๊ฒฐ
Embedded Kafka๋ฅผ ํ์ฉํ 4๊ฐ์ง ํ์ต ํ ์คํธ ๊ตฌํ: - Experiment1: Auto Commit vs Manual Commit ๋์ ๋น๊ต - Experiment2: auto.offset.reset (earliest/latest) ํ์ต - Experiment3: max.poll.interval.ms ํ์์์ ์ฒดํ - Experiment4: max.poll.records ๋ฐฐ์น ํฌ๊ธฐ ํธ๋ ์ด๋์คํ ํน์ง: - @embeddedkafka ์ฌ์ฉ์ผ๋ก Docker ๋ถํ์ - IntelliJ์์ ๋ฐ๋ก ์คํ ๊ฐ๋ฅ - ๊ฐ ํ ์คํธ๋ ๋ ๋ฆฝ์ ์ธ ํ ํฝ ์ฌ์ฉ
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the ์ํฌ์ค๋ฃจ์ด ๋ณ๊ฒฝ์ Kafka ๊ธฐ๋ฐ ์ด๋ฒคํธ ๋ฐํ ๋ฐ ์๋น ์ธํ๋ผ๋ฅผ ๋์ ํฉ๋๋ค. ์๊ฑฐ๋ API์๋ ์์๋ฐ์ค ์ด๋ฒคํธ๋ฅผ Kafka ์ฃผ์ ๋ก ๋ฐํํ๋ EventKafkaProducer๊ฐ ์ถ๊ฐ๋๊ณ , ์๊ฑฐ๋ ์คํธ๋ฆฌ๋จธ ๋ชจ๋์๋ ์นดํ๋ก๊ทธ ๋ฐ ์ฃผ๋ฌธ ์ด๋ฒคํธ๋ฅผ ์๋นํ๊ณ ์ฒ๋ฆฌํ๋ ์ปจ์๋จธ๊ฐ ์ถ๊ฐ๋ฉ๋๋ค. ๋ฉฑ๋ฑ์ฑ ํ์ธ, ์ํ ๋ฉํธ๋ฆญ ์์ง, ์คํจํ ๋ฉ์์ง ์ฒ๋ฆฌ๋ฅผ ์ํ ์ง์ ์๋น์ค๊ฐ ๋์ ๋์์ต๋๋ค. ๋ณ๊ฒฝ์ฌํญ
์ํ์ค ๋ค์ด์ด๊ทธ๋จsequenceDiagram
participant API as Commerce API
participant Kafka as Kafka Broker
participant Consumer as Event Consumer
participant Inbox as Event Inbox
participant Metrics as Metrics Service
participant DLQ as Dead Letter Queue
rect rgb(200, 220, 255)
Note over API,Kafka: 1. Outbox ๋ฐํ ํ๋ฆ
API->>Kafka: publish(EventOutbox)
Kafka-->>API: SendResult with offset
API->>API: Mark Outbox as Published
end
rect rgb(200, 255, 220)
Note over Consumer,DLQ: 2. ์ด๋ฒคํธ ์๋น ํ๋ฆ
Kafka->>Consumer: Poll batch of records
Consumer->>Inbox: Check isDuplicate(eventId)
alt ๋ฉฑ๋ฑ์ฑ ํ์ธ ํต๊ณผ
Inbox-->>Consumer: false (new event)
Consumer->>Inbox: save(eventId, ...)
Inbox-->>Consumer: saved
alt ์ด๋ฒคํธ ํ์
๋ณ ์ฒ๋ฆฌ
Consumer->>Metrics: incrementLikeCount/View/Order
Metrics-->>Consumer: metrics updated
end
else ์ค๋ณต ๊ฐ์ง
Inbox-->>Consumer: true (duplicate)
Consumer->>Consumer: skip processing
end
end
rect rgb(255, 220, 220)
Note over Consumer,DLQ: 3. ์คํจ ์ฒ๋ฆฌ
Consumer->>Consumer: Exception occurs
Consumer->>DLQ: save(topic, eventId, payload, error)
DLQ-->>Consumer: saved to DLQ
end
Consumer->>Kafka: commitAsync() for batch offset
Kafka-->>Consumer: offset committed
์์ ์ฝ๋ ๋ฆฌ๋ทฐ ๋ ธ๋ ฅ๐ฏ 4 (๋ณต์กํจ) | โฑ๏ธ ~75๋ถ ์ถ๊ฐ ๊ฒํ ์์ฒญ ์ฌํญ:
๊ด๋ จ PR ๋ชฉ๋ก
์
Pre-merge checks and finishing touchesโ Failed checks (1 warning)
โ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 15
Caution
Some comments are outside the diff and canโt be posted inline due to platform limitations.
โ ๏ธ Outside diff range comments (2)
modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt (1)
54-80: ์ํ ๋ณ๊ฒฝ ์updatedAtํ๋๋ฅผ ์ ๋ฐ์ดํธํด์ผ ํฉ๋๋ค.
updatedAtํ๋๊ฐ ์ด๊ธฐํ ํ ์ํ ๋ณ๊ฒฝ ๋ฉ์๋(approve(),invalidCard(),limitExceeded())์์ ์ ๋ฐ์ดํธ๋์ง ์์ต๋๋ค. ์ด๋ก ์ธํดupdated_at์ปฌ๋ผ์ด ํญ์ ์์ฑ ์์ ์ ํ์์คํฌํ๋ฅผ ์ ์งํ๊ฒ ๋์ด ์ค์ ์์ ์๊ฐ์ ์ถ์ ํ ์ ์์ต๋๋ค.๊ฐ ์ํ ๋ณ๊ฒฝ ๋ฉ์๋์์
updatedAt์ ์ ๋ฐ์ดํธํ์ธ์.private set์internal set์ผ๋ก ๋ณ๊ฒฝํ๊ฑฐ๋,@PreUpdate๋ผ์ดํ์ฌ์ดํด ์ฝ๋ฐฑ์ ์ถ๊ฐํ๋ ๋ฐฉ์์ด ์์ต๋๋ค:fun approve() { if (status != TransactionStatus.PENDING) { throw CoreException(ErrorType.INTERNAL_ERROR, "๊ฒฐ์ ์น์ธ์ ๋๊ธฐ์ํ์์๋ง ๊ฐ๋ฅํฉ๋๋ค.") } status = TransactionStatus.SUCCESS reason = "์ ์ ์น์ธ๋์์ต๋๋ค." + updatedAt = LocalDateTime.now() } fun invalidCard() { if (status != TransactionStatus.PENDING) { throw CoreException(ErrorType.INTERNAL_ERROR, "๊ฒฐ์ ์ฒ๋ฆฌ๋ ๋๊ธฐ์ํ์์๋ง ๊ฐ๋ฅํฉ๋๋ค.") } status = TransactionStatus.FAILED reason = "์๋ชป๋ ์นด๋์ ๋๋ค. ๋ค๋ฅธ ์นด๋๋ฅผ ์ ํํด์ฃผ์ธ์." + updatedAt = LocalDateTime.now() } fun limitExceeded() { if (status != TransactionStatus.PENDING) { throw CoreException(ErrorType.INTERNAL_ERROR, "ํ๋์ด๊ณผ ์ฒ๋ฆฌ๋ ๋๊ธฐ์ํ์์๋ง ๊ฐ๋ฅํฉ๋๋ค.") } status = TransactionStatus.FAILED reason = "ํ๋์ด๊ณผ์ ๋๋ค. ๋ค๋ฅธ ์นด๋๋ฅผ ์ ํํด์ฃผ์ธ์." + updatedAt = LocalDateTime.now() }apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.java (1)
125-158: ์ฌ์๋ ๋์๊ณผ ์ํ ์ ํ ํ์ด๋ฐ์ ๊ณ ๋ คํ ํ ์คํธ ์์ ์ฑ ๊ฐ์ ์ด ํ์ํฉ๋๋ค.ํ์ฌ ๊ตฌํ์์ ๋ค์ ์ฌํญ๋ค์ด ํ ์คํธ ์์ ์ฑ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค:
145๋ฒ ๋ผ์ธ์ ์๊ฒฉํ ์ํ ๋จ์ธ: ์ฌ์๋์ ์ํท ๋ธ๋ ์ด์ปค์ ๋น๋๊ธฐ ์ํ ์ ๋ฐ์ดํธ๋ก ์ธํด ๋ฃจํ ์งํ ์ฆ์
OPEN์ํ๊ฐ ์๋ ์ ์์ต๋๋ค. 81-120๋ฒ ๋ผ์ธ์ ๋ค๋ฅธ ํ ์คํธ๋isIn(CircuitBreaker.State.OPEN, CircuitBreaker.State.HALF_OPEN)๋ฅผ ์ฌ์ฉํ์ฌ ๋ ๊ด๋ํ ๊ฒ์ฆ์ ์ํํฉ๋๋ค.์ฌ์๋๋ก ์ธํ ์ค์ ํธ์ถ ํ์ ๋ถ์ผ์น: 82-84๋ฒ ๋ผ์ธ์ ์ฃผ์์ ๋ฐ๋ฅด๋ฉด ์ฌ์๋๊ฐ 2๋ฒ ๋ฐ์ํ๋ฏ๋ก, 6๋ฒ ๋ฃจํ๋ ์ค์ ๋ก ์ฝ 18๋ฒ(6 ์ด๊ธฐ + 12 ์ฌ์๋)์ pgClient ํธ์ถ์ ๋ฐ์์ํต๋๋ค. ์ฌ๋ผ์ด๋ฉ ์๋์ฐ๊ฐ 10์ด๋ฏ๋ก ๋ง์ง๋ง 10๊ฐ ํธ์ถ๋ง ์คํจ์จ ๊ณ์ฐ์ ํฌํจ๋๋๋ฐ, 129-130๋ฒ ๋ผ์ธ์ ์ฃผ์("์ฌ๋ผ์ด๋ฉ ์๋์ฐ 10๊ฐ ์ค 6๊ฐ ์ด์ ์คํจ")์ ์ด๋ฅผ ๋ช ํํ ๋ฐ์ํ์ง ๋ชปํฉ๋๋ค.
๐ ๊ฐ์ ์ ์
์ต์ 1: ๋ ๊ด๋ํ ์ํ ๊ฒ์ฆ ์ฌ์ฉ
- // Circuit์ด OPEN ์ํ์ธ์ง ํ์ธ - assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN); + // Circuit์ด OPEN ์ํ์ธ์ง ํ์ธ (์ฌ์๋ ํ์ด๋ฐ์ผ๋ก ์ธํด HALF_OPEN์ผ ์๋ ์์) + assertThat(circuitBreaker.getState()).isIn(CircuitBreaker.State.OPEN, CircuitBreaker.State.HALF_OPEN);์ต์ 2: ๋ฉํธ๋ฆญ ๊ธฐ๋ฐ ๊ฒ์ฆ ์ถ๊ฐ
// Circuit์ด OPEN ์ํ์ธ์ง ํ์ธ assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN); + + // ์คํจ ํ์์ ์คํจ์จ๋ก ์ถ๊ฐ ๊ฒ์ฆ + var metrics = circuitBreaker.getMetrics(); + assertThat(metrics.getNumberOfFailedCalls()).isGreaterThanOrEqualTo(5); + assertThat(metrics.getFailureRate()).isGreaterThanOrEqualTo(50.0f);์ต์ 3: ์ฃผ์ ๋ช ํํ
- // ์ฌ๋ผ์ด๋ฉ ์๋์ฐ 10๊ฐ ์ค 6๊ฐ ์ด์ ์คํจ ์์ผ์ Circuit OPEN (์คํจ์จ 50% ์ด๊ณผ) + // 6๋ฒ ์์ฒญ ์๋ (๊ฐ ์์ฒญ์ ์ฌ์๋ ํฌํจ ์ ์ต๋ 3๋ฒ ํธ์ถ) + // ์ฌ๋ผ์ด๋ฉ ์๋์ฐ(10)์ ์ถฉ๋ถํ ์คํจ๋ฅผ ๋ฐ์์์ผ Circuit OPEN (์คํจ์จ 50% ์ด๊ณผ)
๐งน Nitpick comments (18)
modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt (1)
6-6: wildcard import ๋์ ๋ช ์์ import ์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.
java.util.*wildcard import๋ ์ฝ๋์์ ์ค์ ๋กUUIDํด๋์ค๋ง ์ฌ์ฉํ๊ณ ์์ด ๋ถํ์ํฉ๋๋ค. Wildcard import๋ ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๋ฅผ ์ผ๊ธฐํ ์ ์์ต๋๋ค:
- ์ด๋ค ํด๋์ค๊ฐ ์ฌ์ฉ๋๋์ง ๋ช ํํ์ง ์์ ๊ฐ๋ ์ฑ ์ ํ
- ์ฌ๋ฌ ํจํค์ง์ ๋์ผํ ์ด๋ฆ์ ํด๋์ค๊ฐ ์์ ๊ฒฝ์ฐ naming conflict ๊ฐ๋ฅ์ฑ
- ์์กด์ฑ ์ถ์ ์ด๋ ค์
๋ํ, ์ด ๋ณ๊ฒฝ์ PR์ ์ฃผ์ ๋ชฉ์ (Kafka ํตํฉ)๊ณผ ๋ฌด๊ดํด ๋ณด์ ๋๋ค.
๐ ๋ช ์์ import๋ก ๋๋๋ฆฌ๋ diff:
-import java.util.* +import java.util.UUIDmodules/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt (1)
8-8: ์์ผ๋์นด๋ ์ํฌํธ ๋์ ๋ช ์์ ์ํฌํธ ์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.์ด ํ์ผ์์๋
TimeZone๋ง ์ฌ์ฉํ๊ณ ์์ผ๋ฏ๋ก,import java.util.*๋์import java.util.TimeZone์ผ๋ก ๋ช ์์ ์ํฌํธ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ์์ผ๋์นด๋ ์ํฌํธ๋ ์ฝ๋์ ๋ช ํ์ฑ์ ๋จ์ด๋จ๋ฆฌ๊ณ ๋ค์ด๋ฐ ์ถฉ๋ ๊ฐ๋ฅ์ฑ์ ๋์ผ ์ ์์ต๋๋ค.๐ ์ ์ํ๋ ์์ ์ฌํญ
-import java.util.* +import java.util.TimeZonemodules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.java (1)
209-237: ํฌํผ ๋ฉ์๋ ์ค๋ณต์ ๊ณ ๋ คํด๋ณผ ์ ์์ต๋๋ค.
produceMessages์createConsumerProps๋ฉ์๋๊ฐ Experiment1~4 ํ ์คํธ ํด๋์ค์์ ๋์ผํ๊ฒ ์ค๋ณต๋ฉ๋๋ค. ํ์ต์ฉ ํ ์คํธ์ด๋ฏ๋ก ๊ฐ ํ์ผ์ด ๋ ๋ฆฝ์ ์ผ๋ก ์ดํด ๊ฐ๋ฅํ ์ ์ ์ฅ์ ์ด์ง๋ง, ๊ณตํต ๋ฒ ์ด์ค ํด๋์ค๋ ์ ํธ๋ฆฌํฐ ํด๋์ค๋ก ์ถ์ถํ๋ฉด ์ ์ง๋ณด์๊ฐ ์ฉ์ดํด์ง๋๋ค.docker/init-db.sql (2)
10-24: Inbox ํ ์ด๋ธ ๋ฐ์ดํฐ ์ฆ๊ฐ ๋ฐ retention ์ ์ฑ ๊ฒํ ๊ถ์ฅ
event_inboxํ ์ด๋ธ์ ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ์ ์ํด ์ฒ๋ฆฌ๋ ๋ชจ๋ ์ด๋ฒคํธ๋ฅผ ์ ์ฅํฉ๋๋ค. ์๊ฐ์ด ์ง๋จ์ ๋ฐ๋ผ ํ ์ด๋ธ์ด ์ง์์ ์ผ๋ก ์ฆ๊ฐํ๋ฏ๋ก, ์ค๋๋ ๋ ์ฝ๋๋ฅผ ์ ๋ฆฌํ๋ retention ์ ์ฑ ์ด ํ์ํฉ๋๋ค.๊ณ ๋ ค ์ฌํญ:
processed_at๋๋created_at๊ธฐ์ค์ผ๋ก ์ผ์ ๊ธฐ๊ฐ(์: 7์ผ, 30์ผ) ์ดํ ๋ ์ฝ๋ ์ญ์ deleted_at์ปฌ๋ผ์ด ์์ง๋ง soft delete ์ฌ์ฉ ์ฌ๋ถ๊ฐ ๋ช ํํ์ง ์์- ๋ฐฐ์น ์์ ์ผ๋ก ์ฃผ๊ธฐ์ ์ ๋ฆฌ ๋๋ ํํฐ์ ๋ ์ ๋ต ๊ณ ๋ ค
PR ์์ฝ์ Inbox์ Outbox์ retention ๊ธฐ๊ฐ์ด ๋ค๋ฅด๋ค๋ ์ค๊ณ ๋ ผ์๊ฐ ์์๋ค๊ณ ๋ช ์๋์ด ์์ผ๋ฏ๋ก, ํด๋น ์ ์ฑ ์ด ๊ตฌํ๋์๋์ง ํ์ธํ์๊ธฐ ๋ฐ๋๋๋ค.
#!/bin/bash # Description: Inbox ์ ๋ฆฌ ์ค์ผ์ค๋ฌ ๋๋ ๋ฐฐ์น ์์ ์กด์ฌ ์ฌ๋ถ ํ์ธ echo "=== Inbox cleanup/retention ๊ด๋ จ ์ฝ๋ ๊ฒ์ ===" rg -n "deleteOldInbox|cleanupInbox|retentionInbox" --type=java -C 3 echo -e "\n=== ์ค์ผ์ค๋ฌ ์์ ๊ฒ์ ===" rg -n "@Scheduled.*Inbox" --type=java -C 3
44-60: DLQ ํ ์ด๋ธ ์ค๊ณ ๊ฒํ ๋ฐ ๋ฐ์ดํฐ ์ฆ๊ฐ ๋๋น ๊ถ์ฅ
dead_letter_queueํ ์ด๋ธ ์ค๊ณ์์ ๊ณ ๋ คํ ์ฌํญ:
์ปฌ๋ผ ํ์ ํฌ๊ธฐ ์ ํ:
payload TEXT: ์ต๋ 64KB๊น์ง๋ง ์ ์ฅ ๊ฐ๋ฅerror_message TEXT: ๊ธด ์คํ ํธ๋ ์ด์ค๊ฐ ์๋ฆด ์ ์์- ๋์ฉ๋ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํ๋ ๊ฒฝ์ฐ
MEDIUMTEXT(16MB) ๋๋LONGTEXT(4GB) ๊ณ ๋ ค ํ์๋ฐ์ดํฐ ์ฆ๊ฐ ๊ด๋ฆฌ:
- DLQ ๋ ์ฝ๋๋ ์ง์์ ์ผ๋ก ์ฆ๊ฐํ๋ฏ๋ก retention ์ ์ฑ ํ์
retry_count์ปฌ๋ผ์ด ์์ง๋ง, ์ฌ์๋ ํ ์๋ ์ ๋ฆฌ ๋ก์ง์ด ์๋์ง ๋ถ๋ช ํ๐ ๋์ฉ๋ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํ๋ ๊ฒฝ์ฐ ์ ์ฉ ๊ฐ๋ฅํ diff:
CREATE TABLE IF NOT EXISTS dead_letter_queue ( id BIGINT AUTO_INCREMENT PRIMARY KEY, original_topic VARCHAR(100) NOT NULL COMMENT '์๋ณธ Topic', partition_key VARCHAR(100) COMMENT 'Partition Key', event_id VARCHAR(50) COMMENT '์ด๋ฒคํธ ID', - payload TEXT NOT NULL COMMENT '์๋ณธ ๋ฉ์์ง', - error_message TEXT COMMENT '์๋ฌ ๋ฉ์์ง', + payload MEDIUMTEXT NOT NULL COMMENT '์๋ณธ ๋ฉ์์ง', + error_message MEDIUMTEXT COMMENT '์๋ฌ ๋ฉ์์ง', retry_count INT NOT NULL DEFAULT 0 COMMENT '์ฌ์๋ ํ์',apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java (1)
27-35: ์ค๋ณต ์ฒดํฌ ๋ก์ง์ด ์ ์ ํ๋ฉฐ, ์ ํ์ ์ผ๋ก ์ฝ๊ธฐ ์ ์ฉ ํธ๋์ญ์ ์ถ๊ฐ ๊ณ ๋ ค ๊ฐ๋ฅ
isDuplicate()๋ฉ์๋์ ๊ตฌํ์ด ๋ช ํํ๊ณ ์ ์ ํฉ๋๋ค. ๋จ์ ์กด์ฌ ์ฌ๋ถ๋ฅผ ํ์ธํ๋ ์ฝ๊ธฐ ์ ์ฉ ์์ ์ด๋ฏ๋ก ํ์ฌ ๊ตฌํ๋ ๋ฌธ์ ์์ง๋ง, ์ ํ์ ์ผ๋ก@Transactional(readOnly = true)๋ฅผ ์ถ๊ฐํ์ฌ ๋ช ์์ ์ผ๋ก ์ฝ๊ธฐ ์ ์ฉ์์ ํ์ํ ์ ์์ต๋๋ค.๐ ์ฝ๊ธฐ ์ ์ฉ ํธ๋์ญ์ ์ถ๊ฐ (์ ํ ์ฌํญ):
+ @Transactional(readOnly = true) public boolean isDuplicate(String eventId) { boolean exists = eventInboxRepository.existsByEventId(eventId);apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java (1)
14-28: ํ ์คํธ ์ค์ ์ด ์ ์ ํ๋ฉฐ, BeforeEach ์ ๋ฆฌ ๋ก์ง ๋จ์ํ ๊ณ ๋ ค ๊ฐ๋ฅํตํฉ ํ ์คํธ ๊ตฌ์ฑ์ด ์ ์ ํฉ๋๋ค.
@Transactional์ด๋ ธํ ์ด์ ์ผ๋ก ๊ฐ ํ ์คํธ ํ ์๋ ๋กค๋ฐฑ๋๋ฏ๋ก,BeforeEach์deleteAll()ํธ์ถ์ ์ฒซ ํ ์คํธ ์คํ ์ ๋ฐ์ดํฐ ์ ๋ฆฌ ๋ชฉ์ ์ผ๋ก๋ง ํ์ํฉ๋๋ค.ํ์ฌ ๊ตฌํ๋ ๋ฌธ์ ์์ง๋ง, ๋ช ์์ ์ผ๋ก ์ฒซ ์คํ๋ง์ ์ํ ๊ฒ์์ ๋ํ๋ด๋ ค๋ฉด
@BeforeAll(static) ์ฌ์ฉ์ ๊ณ ๋ คํ ์ ์์ต๋๋ค. ๋ค๋ง@Transactional๊ณผ์ ์กฐํฉ ์ ์ฃผ์๊ฐ ํ์ํ๋ฏ๋ก, ํ์ฌ ๋ฐฉ์์ด ๋ ์์ ํ ์ ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
97-102: ํธ๋์ญ์ ์ปค๋ฐ ํ ์บ์ ๋ฌดํจํ ํ์ ๊ถ์ฅ์ฌ๊ณ ์์ง ์ ์บ์ ๋ฌดํจํ ๋ก์ง์์ ์ค์ํ ์ด์:
ํธ๋์ญ์ ํ์ด๋ฐ ์ด์ (์ค์):
- ํ์ฌ ์บ์ ๋ฌดํจํ๊ฐ ํธ๋์ญ์ ์ปค๋ฐ ์ ์ ์คํ๋จ
- ์ดํ ์ฟ ํฐ ํ ์ธ, ํฌ์ธํธ ์ฐจ๊ฐ, ์ฃผ๋ฌธ ์ ์ฅ ์ค ์์ธ ๋ฐ์ ์ ํธ๋์ญ์ ๋กค๋ฐฑ๋์ง๋ง ์บ์๋ ์ด๋ฏธ ๋ฌดํจํ๋จ
- ๊ฒฐ๊ณผ: DB์ ์ฌ๊ณ ๋ ์์๋ณต๊ตฌ๋๋ ์บ์๋ง ๋ฌดํจํ ์ํ๋ก ๋จ์ ๋ถ์ผ์น ๋ฐ์
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)์ฌ์ฉํ์ฌ ํธ๋์ญ์ ์ฑ๊ณต ํ์๋ง ์บ์ ๋ฌดํจํ์กฐ๊ฑด ๊ฒํ :
- ํ์ฌ
getStock() == 0์กฐ๊ฑด์ ์ ํํจ (deductStock()์ด ์์ ๋ฐฉ์ง)- ๋ฐฉ์ด์ ์ฝ๋ฉ์ ์ํ๋ค๋ฉด
<= 0์ผ๋ก ๋ณ๊ฒฝ ๊ฐ๋ฅapps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java (1)
41-42: ํ ์คํธ์ ๋๋ฒ๊น ํธ์์ฑ์ ๊ฐ์ ํ ์ ์์ต๋๋ค.
orElseThrow()์ ๋ช ํํ ์๋ฌ ๋ฉ์์ง๋ฅผ ์ถ๊ฐํ๋ฉด ํ ์คํธ ์คํจ ์ ์์ธ ํ์ ์ด ๋ ์ฌ์์ง๋๋ค.๐ ๊ฐ์ ์ ์
- ProductMetrics metrics = productMetricsRepository.findByProductId(productId) - .orElseThrow(); + ProductMetrics metrics = productMetricsRepository.findByProductId(productId) + .orElseThrow(() -> new AssertionError("ProductMetrics not found for productId: " + productId));๊ฐ ํ ์คํธ ๋ฉ์๋์ ๋์ผํ๊ฒ ์ ์ฉํ ์ ์์ต๋๋ค.
Also applies to: 57-58, 72-73, 88-89
apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java (2)
53-57: ์์ธ ์ฒ๋ฆฌ ํจํด์ ๊ฐ์ ํ ์ ์์ต๋๋ค.
exceptionally๋ธ๋ก ๋ด์์RuntimeException์ ๋์ง๋ ํ์ฌ ํจํด์ ๋์ํ์ง๋ง ํผ๋์ค๋ฌ์ธ ์ ์์ต๋๋ค.exceptionallyํธ๋ค๋ฌ๋ ๋ณต๊ตฌ๋ ๊ฐ์ ๋ฐํํ๊ฑฐ๋, ์์ธ๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ์ ํํ๋๋ก ์ค๊ณ๋์์ต๋๋ค.๐ ๋ ๋ช ํํ ํจํด์ผ๋ก ๊ฐ์
์ต์ 1 (๊ถ์ฅ): exceptionally ๋ธ๋ก ์ ๊ฑฐ
์๋ฌ ๋ก๊น ๋ง ์ ์งํ๊ณ ์์ธ๋ ์์ฐ์ค๋ฝ๊ฒ ์ ํ๋๋๋ก ํฉ๋๋ค:
- return kafkaTemplate.send(topic, partitionKey, outbox.getPayload()) + CompletableFuture<SendResult<Object, Object>> future = + kafkaTemplate.send(topic, partitionKey, outbox.getPayload()) .thenApply(result -> { log.info("Kafka ๋ฐํ ์ฑ๊ณต - topic: {}, partition: {}, offset: {}, eventId: {}", topic, result.getRecordMetadata().partition(), result.getRecordMetadata().offset(), outbox.getId()); return result; - }) - .exceptionally(ex -> { - log.error("Kafka ๋ฐํ ์คํจ - topic: {}, key: {}, eventId: {}, error: {}", - topic, partitionKey, outbox.getId(), ex.getMessage(), ex); - throw new RuntimeException("Kafka ๋ฐํ ์คํจ", ex); }); + + // ์๋ฌ ๋ก๊น ์ OutboxEventService์์ ์ฒ๋ฆฌ + return future;์ต์ 2: whenComplete ์ฌ์ฉ
์ฑ๊ณต/์คํจ ๋ชจ๋ ๋ก๊น ํ๋ ๊ฒฐ๊ณผ๋ ๊ทธ๋๋ก ์ ํ:
return kafkaTemplate.send(topic, partitionKey, outbox.getPayload()) - .thenApply(result -> { - log.info("Kafka ๋ฐํ ์ฑ๊ณต - topic: {}, partition: {}, offset: {}, eventId: {}", - topic, - result.getRecordMetadata().partition(), - result.getRecordMetadata().offset(), - outbox.getId()); - return result; - }) - .exceptionally(ex -> { + .whenComplete((result, ex) -> { + if (ex != null) { - log.error("Kafka ๋ฐํ ์คํจ - topic: {}, key: {}, eventId: {}, error: {}", - topic, partitionKey, outbox.getId(), ex.getMessage(), ex); - throw new RuntimeException("Kafka ๋ฐํ ์คํจ", ex); + log.error("Kafka ๋ฐํ ์คํจ - topic: {}, key: {}, eventId: {}, error: {}", + topic, partitionKey, outbox.getId(), ex.getMessage(), ex); + } else { + log.info("Kafka ๋ฐํ ์ฑ๊ณต - topic: {}, partition: {}, offset: {}, eventId: {}", + topic, + result.getRecordMetadata().partition(), + result.getRecordMetadata().offset(), + outbox.getId()); + } });ํ์ฌ ์ฝ๋๋ ๋์ํ์ง๋ง, ์ ํจํด๋ค์ด CompletableFuture์ ์๋๋ ์ฌ์ฉ๋ฒ์ ๋ ๋ถํฉํฉ๋๋ค.
23-23: ํ์ ์์ ์ฑ์ ๊ฐ์ ํ ์ ์์ต๋๋ค.
KafkaTemplate<Object, Object>๋ ๋งค์ฐ ์ผ๋ฐ์ ์ด์ง๋ง, ์ค์ ๋ก๋Stringํ์ ์ ํํฐ์ ํค์ ํ์ด๋ก๋๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ๋ ๊ตฌ์ฒด์ ์ธ ํ์ ์ ์ฌ์ฉํ๋ฉด ์ปดํ์ผ ํ์์ ํ์ ์์ ์ฑ์ ํ๋ณดํ ์ ์์ต๋๋ค.๐ ํ์ ์์ ์ฑ ๊ฐ์ ์ ์
- private final KafkaTemplate<Object, Object> kafkaTemplate; + private final KafkaTemplate<String, String> kafkaTemplate; /** * Outbox ์ด๋ฒคํธ๋ฅผ Kafka๋ก ๋ฐํ * * @param outbox Outbox ์ด๋ฒคํธ * @return CompletableFuture<SendResult> */ - public CompletableFuture<SendResult<Object, Object>> publish(EventOutbox outbox) { + public CompletableFuture<SendResult<String, String>> publish(EventOutbox outbox) {์ด ๋ณ๊ฒฝ์ KafkaTemplate ์ค์ ์์๋ ํ์ ์ ๋ช ์ํด์ผ ํ ์ ์์ต๋๋ค.
Also applies to: 37-44
apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (1)
32-33: ๋ฉ์๋ ํ๋ผ๋ฏธํฐ๋ฅผ ๊ฐ์ ํ ์ ์์ต๋๋ค.6๊ฐ์ ํ๋ผ๋ฏธํฐ๋ ์ธ์ง์ ๋ถ๋ด์ด ํฌ๊ณ ํ๋ผ๋ฏธํฐ ์์๋ฅผ ์ค์ํ๊ธฐ ์ฝ์ต๋๋ค. ํ๋ผ๋ฏธํฐ ๊ฐ์ฒด๋ฅผ ๋์ ํ๋ฉด ๊ฐ๋ ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ฉ๋๋ค.
๐ ํ๋ผ๋ฏธํฐ ๊ฐ์ฒด ๋์ ์ ์
1๋จ๊ณ: ํ๋ผ๋ฏธํฐ ๊ฐ์ฒด ์์ฑ
@Getter @Builder public class DLQSaveRequest { private final String originalTopic; private final String partitionKey; private final String eventId; private final String payload; private final String errorMessage; private final int retryCount; }2๋จ๊ณ: ์๋น์ค ๋ฉ์๋ ์๊ทธ๋์ฒ ๊ฐ์
@Transactional - public void save(String originalTopic, String partitionKey, String eventId, - String payload, String errorMessage, int retryCount) { + public void save(DLQSaveRequest request) { DeadLetterQueue dlq = DeadLetterQueue.builder() - .originalTopic(originalTopic) - .partitionKey(partitionKey) - .eventId(eventId) - .payload(payload) - .errorMessage(errorMessage) - .retryCount(retryCount) + .originalTopic(request.getOriginalTopic()) + .partitionKey(request.getPartitionKey()) + .eventId(request.getEventId()) + .payload(request.getPayload()) + .errorMessage(request.getErrorMessage()) + .retryCount(request.getRetryCount()) .build(); deadLetterQueueRepository.save(dlq); log.error("โ ๏ธ DLQ์ ๋ฉ์์ง ์ ์ฅ - topic: {}, eventId: {}, retryCount: {}, error: {}", - originalTopic, eventId, retryCount, errorMessage); + request.getOriginalTopic(), request.getEventId(), + request.getRetryCount(), request.getErrorMessage()); }3๋จ๊ณ: Consumer ํธ์ถ๋ถ ์์ ์์
- deadLetterQueueService.save( - record.topic(), - record.key() != null ? record.key().toString() : null, - eventId, - payload, - error.getMessage(), - retryCount - ); + deadLetterQueueService.save( + DLQSaveRequest.builder() + .originalTopic(record.topic()) + .partitionKey(record.key() != null ? record.key().toString() : null) + .eventId(eventId) + .payload(payload) + .errorMessage(error.getMessage()) + .retryCount(retryCount) + .build() + );์ด ๋ณ๊ฒฝ์ ํ๋ผ๋ฏธํฐ ์์ ์ค์๋ฅผ ๋ฐฉ์งํ๊ณ , ๋น๋ ํจํด์ผ๋ก ๋ช ํํ ์๋ ์ ๋ฌ์ด ๊ฐ๋ฅํฉ๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (1)
95-99: ์ข์์ ์ ๊ฐ์ ์ ์์ ๋ฐฉ์ง ๋ก์ง์ ํ์ธํ์ธ์.ํ์ฌ
likeCount > 0์ฒดํฌ๋ก ์์๋ฅผ ๋ฐฉ์งํ๊ณ ์์ผ๋ฉฐ, 0์ผ ๋๋ ๋ฌด์๋ฉ๋๋ค. ์ด๋ ์์ ํ ๋ฐฉ์ด ๋ก์ง์ด์ง๋ง, ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ์ ๋ฐ๋ผ ๋ก๊น ์ด๋ ์์ธ ์ฒ๋ฆฌ๊ฐ ํ์ํ ์ ์์ต๋๋ค. ์ข์์๊ฐ ์๋๋ฐ ์ญ์ ์ด๋ฒคํธ๊ฐ ์ค๋ ๊ฒ์ ๋น์ ์ ์ํฉ์ผ ์ ์์ผ๋ฏ๋ก, ๋ชจ๋ํฐ๋ง ๊ด์ ์์ ๊ฒฝ๊ณ ๋ก๊ทธ๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.๐ ์ ํ์ ๊ฐ์ : ๊ฒฝ๊ณ ๋ก๊ทธ ์ถ๊ฐ
public void decrementLikeCount() { if (this.likeCount > 0) { this.likeCount--; + } else { + log.warn("์ข์์ ์๊ฐ ์ด๋ฏธ 0์ธ ์ํ์์ ๊ฐ์ ์๋ - productId: {}", this.productId); } }apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java (1)
46-52: ์์ธ ์ฒ๋ฆฌ ํ ์คํธ ์ถ๊ฐ ์ DeadLetterQueueService Mock ๊ฐ์ฒด ๊ถ์ฅํ์ฌ ํ ์คํธ๋ ์ ์ ํ๋ก์ฐ๋ง ๊ฒ์ฆํ๊ณ ์์ผ๋ฏ๋ก null ์ฃผ์ ์ด ๋ฌธ์ ๊ฐ ๋์ง ์์ต๋๋ค. ๊ทธ๋ฌ๋ ๊ตฌํ๋ถ์์ processEvent() ์์ธ ๋ฐ์ ์ sendToDLQ()๋ฅผ ํตํด deadLetterQueueService.save()๋ฅผ ํธ์ถํ๋ฏ๋ก, ํฅํ ์์ธ ์ฒ๋ฆฌ ํ ์คํธ ์ผ์ด์ค๋ฅผ ์ถ๊ฐํ ๋๋ DeadLetterQueueService๋ฅผ Mock ๊ฐ์ฒด๋ก ๋์ฒดํ์ฌ NPE๋ฅผ ๋ฐฉ์งํ๋ ๊ฒ์ด ํ์ํฉ๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java (2)
46-47: ํ๋ ์ด๊ธฐํ ์ค๋ณต์ ์ ๊ฑฐํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.
retryCountํ๋์= 0์ผ๋ก ์ด๊ธฐํํ๋ ๊ฒ์ ๋น๋(Line 61)์์ ์ด๋ฏธ null ์ฒดํฌ๋ฅผ ํตํด ๊ธฐ๋ณธ๊ฐ์ ์ค์ ํ๊ณ ์์ด ์ค๋ณต์ ๋๋ค. JPA๋ protected ์์ฑ์๋ฅผ ํตํด ์ํฐํฐ๋ฅผ ์์ฑํ๋ฏ๋ก ํ๋ ์ด๊ธฐํ๊ฐ ์ ์ฉ๋์ง๋ง, ๋น๋ ํจํด์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ๋น๋ ๋ด๋ถ์ ๊ธฐ๋ณธ๊ฐ ์ค์ ๋ง์ผ๋ก ์ถฉ๋ถํฉ๋๋ค.๐ ํ๋ ์ด๊ธฐํ๋ฅผ ์ ๊ฑฐํ๋ diff ์ ์ฉ
- @Column(name = "retry_count", nullable = false) - private Integer retryCount = 0; + @Column(name = "retry_count", nullable = false) + private Integer retryCount;
1-64: MAX_RETRY ์์๊ฐ ์ ์๋์์ง๋ง ์ฌ์ฉ๋์ง ์์ต๋๋ค.
CatalogEventConsumer์OrderEventConsumer์์MAX_RETRY = 3์์๊ฐ ์ ์๋์ด ์์ง๋ง, DLQ์ ์ ์ฅํ ๋ ํญ์retryCount = 0์ผ๋ก ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. ์ฌ์๋ ๋ก์ง์ ๊ตฌํํ ๊ณํ์ด๋ผ๋ฉด ์ด ์์๋ฅผ ํ์ฉํ๋๋ก ๊ฐ์ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (1)
1-194: CatalogEventConsumer์ ์ฝ๋ ์ค๋ณต์ด ์ฌ๊ฐํฉ๋๋ค.
OrderEventConsumer์CatalogEventConsumer๊ฐ ๊ฑฐ์ ๋์ผํ ํจํด์ ๊ณต์ ํฉ๋๋ค:
- ๋ฐฐ์น ์ฒ๋ฆฌ ๋ฐ ์๋ ์ปค๋ฐ ๋ก์ง (Lines 39-72)
- DLQ ์ ์ก ๋ก์ง (Lines 163-180)
- EventId ์ถ์ถ ๋ก์ง (Lines 185-193)
๊ถ์ฅ ํด๊ฒฐ์ฑ :
์ถ์ ๋ฒ ์ด์ค ํด๋์ค ๋๋ ๊ณตํต ์ ํธ๋ฆฌํฐ ํด๋์ค๋ฅผ ๋์ ํ์ฌ ์ค๋ณต์ ์ ๊ฑฐํ๊ณ ์ ์ง๋ณด์์ฑ์ ํฅ์์ํค์ธ์.๐ ์ถ์ ๋ฒ ์ด์ค ํด๋์ค ์์
@Slf4j @RequiredArgsConstructor public abstract class AbstractBatchEventConsumer { protected final EventInboxService eventInboxService; protected final DeadLetterQueueService deadLetterQueueService; protected final ObjectMapper objectMapper; protected void processBatch( List<ConsumerRecord<Object, Object>> records, Acknowledgment acknowledgment ) { log.info("๐ฆ ์ด๋ฒคํธ ์์ - count: {}", records.size()); int successCount = 0; int skipCount = 0; int failCount = 0; for (ConsumerRecord<Object, Object> record : records) { try { boolean processed = processEvent(record); if (processed) { successCount++; } else { skipCount++; } } catch (Exception e) { failCount++; log.error("โ ์ด๋ฒคํธ ์ฒ๋ฆฌ ์คํจ - partition: {}, offset: {}, key: {}, error: {}", record.partition(), record.offset(), record.key(), e.getMessage(), e); sendToDLQ(record, e, 0); } } acknowledgment.acknowledge(); log.info("โ ์ด๋ฒคํธ ์ฒ๋ฆฌ ์๋ฃ - success: {}, skip: {}, fail: {}, total: {}", successCount, skipCount, failCount, records.size()); } protected abstract boolean processEvent(ConsumerRecord<Object, Object> record) throws Exception; // sendToDLQ, extractEventId ๋ฑ ๊ณตํต ๋ฉ์๋... }๊ทธ๋ฐ ๋ค์ ๊ฐ Consumer๊ฐ ์ด๋ฅผ ์์:
@Component public class OrderEventConsumer extends AbstractBatchEventConsumer { private final ProductMetricsService productMetricsService; @KafkaListener(...) public void consumeOrderEvents( List<ConsumerRecord<Object, Object>> records, Acknowledgment acknowledgment ) { processBatch(records, acknowledgment); } @Override @Transactional protected boolean processEvent(ConsumerRecord<Object, Object> record) throws Exception { // Order ํนํ ๋ก์ง๋ง ๊ตฌํ } }apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1)
33-33: MAX_RETRY ์์๊ฐ ์ ์๋์์ง๋ง ์ฌ์ฉ๋์ง ์์ต๋๋ค.Line 33์
MAX_RETRY = 3์ด ์ ์๋์ด ์์ง๋ง, ์ค์ ๋ก๋ Line 64์์retryCount = 0์ผ๋ก ํ๋์ฝ๋ฉ๋์ด ์ฌ์ฉ๋์ง ์์ต๋๋ค.OrderEventConsumer์๋ ๋์ผํ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.์ฌ์๋ ๋ก์ง์ ๊ตฌํํ ๊ณํ์ด ์๋ค๋ฉด ์ด ์์๋ฅผ ์ ๊ฑฐํ๊ฑฐ๋, ์ฌ์๋๋ฅผ ๊ตฌํํ๋ค๋ฉด ์ด ๊ฐ์ ํ์ฉํ๋๋ก ๊ฐ์ ํ์ธ์.
๐ Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (69)
apps/commerce-api/build.gradle.kts(1 hunks)apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/dataplatform/DataPlatformEventHandler.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java(3 hunks)apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java(2 hunks)apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java(3 hunks)apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java(2 hunks)apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java(0 hunks)apps/commerce-api/src/main/java/com/loopers/config/CacheConfig.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClientConfig.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgPaymentRequest.java(0 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentApi.java(0 hunks)apps/commerce-api/src/main/resources/application.yml(2 hunks)apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.java(3 hunks)apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceFallbackTest.java(0 hunks)apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceRetryTest.java(0 hunks)apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/support/error/CoreExceptionTest.java(1 hunks)apps/commerce-streamer/build.gradle.kts(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueueRepository.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInboxRepository.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/ProductMetricsRepository.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueJpaRepository.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxJpaRepository.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.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/CatalogEventConsumer.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java(1 hunks)apps/commerce-streamer/src/main/resources/application.yml(2 hunks)apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java(1 hunks)apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java(1 hunks)apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java(1 hunks)apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.java(1 hunks)apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.java(1 hunks)docker/init-db.sql(1 hunks)modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java(1 hunks)modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java(1 hunks)modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java(1 hunks)modules/kafka/src/main/resources/kafka.yml(2 hunks)modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment1_AutoCommitTest.java(1 hunks)modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.java(1 hunks)modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment3_PollIntervalTest.java(1 hunks)modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java(1 hunks)modules/kafka/src/test/java/com/loopers/kafka/learning/README.md(1 hunks)modules/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt(1 hunks)modules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt(1 hunks)modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt(1 hunks)modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt(1 hunks)modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt(0 hunks)modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt(1 hunks)modules/redis/src/main/java/com/loopers/config/redis/RedisConfig.java(1 hunks)modules/redis/src/main/java/com/loopers/config/redis/RedisProperties.java(1 hunks)performance-test/k6/product-load-test-fixed.js(1 hunks)performance-test/k6/product-load-test.js(1 hunks)
๐ค Files with no reviewable changes (6)
- apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgPaymentRequest.java
- apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
- apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceRetryTest.java
- apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentApi.java
- modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt
- apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceFallbackTest.java
๐งฐ Additional context used
๐ง Learnings (10)
๐ 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-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/DemoKafkaConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.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/inbox/EventInboxJpaRepository.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueJpaRepository.java
๐ Learning: 2025-12-16T09:44:15.945Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 58
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/payment/PaymentService.kt:157-183
Timestamp: 2025-12-16T09:44:15.945Z
Learning: In PaymentService.requestPgPayment (apps/commerce-api/src/main/kotlin/com/loopers/domain/payment/PaymentService.kt), PG payment requests are idempotent using paymentId as the idempotency key, so Retryable covering the entire method including the pgClient.requestPayment call is safe and will not cause duplicate charges even if retries occur due to ObjectOptimisticLockingFailureException.
Applied to files:
modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.ktapps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.javamodules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.ktmodules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt
๐ Learning: 2025-12-18T13:24:54.339Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:54.339Z
Learning: In this codebase, Kafka consumers delegate error handling and event tracking to the service layer via EventInboxAspect. Service methods annotated with InboxEvent are intercepted by the aspect, which handles failures by saving failed EventInbox entries and logging errors. This centralized approach avoids duplicating error handling logic across multiple consumers.
Applied to files:
apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.javaapps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.javaapps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.javaapps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.javaapps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java
๐ Learning: 2025-12-07T15:47:18.277Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 52
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/payment/PgFeignConfig.kt:0-0
Timestamp: 2025-12-07T15:47:18.277Z
Learning: In Spring Cloud OpenFeign, configuration classes specified in FeignClient(configuration = ...) do NOT need the Configuration annotation. Spring Cloud OpenFeign creates a separate ApplicationContext for each named client and directly instantiates Bean methods from the configuration class. Adding Configuration would cause the class to be registered globally via ComponentScan, making the configuration apply to ALL Feign clients instead of just the specific client, which is usually undesirable.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClientConfig.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.javamodules/kafka/src/main/resources/kafka.yml
๐ 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-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product ์ํฐํฐ (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)๋ ์ ์ฆ์ผ์ด์ค๋ณ๋ก ์๋์ ์ผ๋ก ๋ค๋ฅธ ๋ฝ ์ ๋ต์ ์ฌ์ฉํ๋ค: ์ข์์ ๊ธฐ๋ฅ์๋ ๋น๊ด์ ๋ฝ(findByIdForUpdate)์, ์ฌ๊ณ ์ฐจ๊ฐ์๋ ๋๊ด์ ๋ฝ(Version + ์ฌ์๋)์ ์ฌ์ฉํ๋ค.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.javaapps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.javaapps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.javaapps/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-api/src/main/java/com/loopers/application/product/ProductCacheService.java
๐งฌ Code graph analysis (10)
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.java (3)
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment1_AutoCommitTest.java (1)
DisplayName(31-226)modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment3_PollIntervalTest.java (1)
DisplayName(32-241)modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java (1)
DisplayName(30-357)
apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java (1)
apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java (1)
Getter(18-55)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java (1)
ExampleV1Dto(5-15)
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java (4)
apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (1)
Slf4j(14-49)apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java (1)
Slf4j(14-59)apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1)
Slf4j(23-167)apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (1)
Slf4j(24-194)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (4)
apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (1)
Slf4j(14-49)apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java (1)
Slf4j(14-59)apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java (1)
Slf4j(15-90)apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1)
Slf4j(23-167)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java (2)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java (1)
Repository(11-21)apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
Repository(12-32)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (2)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java (1)
Repository(11-21)apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java (1)
Repository(11-31)
apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (2)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1)
Slf4j(23-167)apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (1)
Slf4j(24-194)
apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java (2)
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java (1)
Slf4j(17-87)apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java (1)
Slf4j(20-126)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java (2)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java (1)
Repository(11-31)apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
Repository(12-32)
๐ Additional comments (70)
modules/redis/src/main/java/com/loopers/config/redis/RedisConfig.java (1)
5-6: ํ์ํ import๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ ๋ฆฌํ์ต๋๋ค.ํ์ผ์์ ์ฌ์ฉ๋๋
List(35, 47์ค)์Consumer(75์ค)๋ฅผ ํ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ import ์น์ ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ถ๊ฐํ์ต๋๋ค. Import ๊ตฌ์ฑ๋ ๊ด๋ก๋ฅผ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java (1)
7-8: โ Import ์ ๋ฆฌ ์ ๋์์ต๋๋ค.
java.util.ArrayList์java.util.Listimport๊ฐ ์ฌ๋ฐ๋ฅธ ์์น๋ก ์ ๋ ฌ๋์์ต๋๋ค. ์ฝ๋์์ ์ค์ ๋ก ์ฌ์ฉ๋๋ import์ด๋ฉฐ (line 19), ํ์ค Java import ์์ ๊ท์น์ ๋ฐ๋ฆ ๋๋ค. ์ค๋ณต ์ ๊ฑฐ ๋ฐ ์กฐ์งํ๊ฐ ๊น๋ํ๊ฒ ์ฒ๋ฆฌ๋์์ต๋๋ค.performance-test/k6/product-load-test-fixed.js (1)
2-3: ์คํ์ผ ์ ๋ฆฌ๋ง ์ ์ฉ๋จ โ ๊ธฐ๋ฅ ๋ณ๊ฒฝ ์์import ๋ฌธ์ ๊ณต๋ฐฑ๋ง ์ ๊ฑฐ๋ ํฌ๋งทํ ๋ณ๊ฒฝ์ ๋๋ค. ๊ตฌ๋ฌธ, ๊ธฐ๋ฅ, ๋๋ ๋ฐํ์ ๋์์ ์ํฅ์ ๋ฏธ์น์ง ์์ต๋๋ค.
๋ค๋ง ์ด ํ์ผ์ Kafka ์ด๋ฒคํธ ํ์ดํ๋ผ์ธ PR์ ๋ฉ์ธ ๋ชฉํ(์์ฐ์, ์๋น์, ํธ๋์ญ์ ์์๋ฐ์ค, ์์ ํจ ํจํด ๋ฑ)์ ๋ณ๊ฐ์ ๋๋ค. ๋ก๋ ํ ์คํธ ๊ฐ์ ๊ฐ ๊ด๋ จ ์๋ค๋ฉด ๊ฐ์ฃผ๋ก ๋ช ์ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1)
4-4: LGTM! ์ค๋ณต import ์ ๊ฑฐ๊ฐ ์ ์ ํฉ๋๋ค.์ค๋ณต๋ TimeZone import๋ฅผ ํ๋๋ก ํตํฉํ ๊ฒ์ ์ฝ๋ ํ์ง์ ๊ฐ์ ํ๋ ์ข์ ์ ๋ฆฌ ์์ ์ ๋๋ค.
performance-test/k6/product-load-test.js (1)
15-16: LGTM! ํฌ๋งทํ ๋ณ๊ฒฝ์ด ์ ์์ ์ผ๋ก ์ ์ฉ๋์์ต๋๋ค.import ๋ฌธ์์ ์ค๊ดํธ ๋ด๋ถ ๊ณต๋ฐฑ์ด ์ ๊ฑฐ๋์์ผ๋ฉฐ, ๋ฌธ๋ฒ์ ์ผ๋ก ์ ํํ๊ณ ๊ธฐ๋ฅ์ ์ํฅ์ ๋ฏธ์น์ง ์์ต๋๋ค.
modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt (1)
5-8: PR ๋ชฉํ์ ๋ฌด๊ดํ ๋ณ๊ฒฝ์ฌํญ์ ๋๋ค.์ด ๋ณ๊ฒฝ์ฌํญ์ import ๊ตฌ๋ฌธ ์ฌ์ ๋ ฌ๋ง ํฌํจํ๊ณ ์์ผ๋ฉฐ, Kafka ์ด๋ฒคํธ ํ์ดํ๋ผ์ธ ๊ตฌํ์ด๋ผ๋ PR์ ์ฃผ์ ๋ชฉํ์ ์ง์ ์ ์ธ ์ฐ๊ด์ฑ์ด ์์ต๋๋ค. ๋์์๋ ์ํฅ์ด ์์ผ๋ฏ๋ก ๋ฌธ์ ๋ ์์ผ๋, ํฅํ PR ๋ฒ์๋ฅผ ๋ช ํํ ํ๊ธฐ ์ํด ๊ด๋ จ ์๋ ํฌ๋งทํ ๋ณ๊ฒฝ์ ๋ณ๋๋ก ๋ถ๋ฆฌํ๋ ๊ฒ์ ๊ณ ๋ คํด๋ณด์ธ์.
modules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt (1)
3-3: LGTM! ์ํฌํธ ์ ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.๋์ผํ ๋๋ฉ์ธ ํจํค์ง(
com.loopers.domain.payment)์์ ์ฌ๋ฌ ํด๋์ค๋ฅผ ์ํฌํธํ๋ ๊ฒฝ์ฐ ์์ผ๋์นด๋ ์ํฌํธ๋ก ๋ณ๊ฒฝํ๋ ๊ฒ์ Kotlin์ ์ผ๋ฐ์ ์ธ ๊ด๋ก๋ฅผ ๋ฐ๋ฆ ๋๋ค. ๋ค๋ฅธ ์ํฌํธ๋ค๊ณผ์ ๋ค์ด๋ฐ ์ถฉ๋ ์ํ๋ ์์ผ๋ฉฐ, ์ฝ๋์ ๋์์๋ ์ํฅ์ด ์์ต๋๋ค.apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.java (1)
7-7: ์ฌ์๋ ๋์์ ๊ณ ๋ คํ ์ ์ ํ import ๋ณ๊ฒฝ์ ๋๋ค.
times์์atLeast๋ก ๋ณ๊ฒฝํ ๊ฒ์ PaymentService์ ์ฌ์๋ ๋ฉ์ปค๋์ฆ์ ๊ณ ๋ คํ ๋ ์ ํํ ํธ์ถ ํ์๋ฅผ ์์ธกํ๊ธฐ ์ด๋ ต๊ธฐ ๋๋ฌธ์ ์ ์ ํฉ๋๋ค. ์ด๋ 158๋ฒ ๋ผ์ธ์ ๊ฒ์ฆ ๋ก์ง ๋ณ๊ฒฝ๊ณผ ์ผ์นํฉ๋๋ค.modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java (2)
4-5: ๋ช ์์ import๋ก ๊ฐ์ ํ์ฌ ์ข์ต๋๋ค.์์ผ๋์นด๋ import๋ฅผ ๋ช ์์ ์ธ import๋ก ๋ณ๊ฒฝํ ๊ฒ์ ์ฝ๋ ๊ฐ๋ ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ํฅ์์ํค๋ ์ข์ ๊ดํ์ ๋๋ค.
Also applies to: 13-17
28-28: MAX_POLL_RECORDS ๊ฐ์ด PR ์ค๋ช ๊ณผ ๋ถ์ผ์นํ๋์ง ํ์ธ ํ์ํฉ๋๋ค.์ฝ๋์ Line 28์์
MAX_POLLING_SIZE๊ฐ 3000์ผ๋ก ์ค์ ๋์ด ์๊ณ , ์ด ๊ฐ์ด Line 63์์MAX_POLL_RECORDS_CONFIG๋ก ์ ์ฉ๋๊ณ ์์ต๋๋ค. PR ์ค๋ช ์์max.poll.records=100์ผ๋ก ๋ช ์๋ ๊ฒฝ์ฐ, ์ค์ ๋ ๊ฐ์ด ์๋ํ ๊ฐ๊ณผ ์ผ์นํ๋์ง ํ์ธํด ์ฃผ์ธ์. 3000์ ๋ฐฐ์น ํฌ๊ธฐ์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ์ํฅ์ ๋ฏธ์นฉ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java (1)
4-4: Import ์ ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.Import ์์๋ฅผ ์ ๋ฆฌํ ๋ณ๊ฒฝ์ฌํญ์ผ๋ก, ์ฝ๋ ๊ตฌ์กฐ์๋ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค.
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.java (4)
1-44: ํ์ต ํ ์คํธ ๊ตฌ์กฐ๊ฐ ์ ๋์ด ์์ต๋๋ค.
auto.offset.reset์ค์ ์ ๋์ ๋ฐฉ์์ ๋ช ํํ๊ฒ ๋ณด์ฌ์ฃผ๋ ์ธ ๊ฐ์ง ์๋๋ฆฌ์ค(earliest, latest, ๊ธฐ์กด ์คํ์ ์กด์ฌ)๊ฐ ์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค. EmbeddedKafka ์ค์ ๊ณผ ์์ ์ ์๊ฐ ์ ์ ํฉ๋๋ค.
45-89: LGTM!
earliest์ค์ ์ ๋์์ ๋ช ํํ๊ฒ ๋ณด์ฌ์ฃผ๋ ํ ์คํธ์ ๋๋ค. Producer๊ฐ ๋จผ์ ๋ฉ์์ง๋ฅผ ์ ์กํ๊ณ , Consumer๊ฐ ๋์ค์ ์์ํด๋ ๊ณผ๊ฑฐ ๋ฉ์์ง๋ฅผ ๋ชจ๋ ์ฝ๋ ์๋๋ฆฌ์ค๊ฐ ์ ๊ตฌํ๋์ด ์์ต๋๋ค.
91-145: LGTM!
latest์ค์ ํ ์คํธ๊ฐ ์ ๊ตฌํ๋์ด ์์ต๋๋ค. Consumer ์์ ์ ๋ฉ์์ง๋ ๋ฌด์ํ๊ณ , ์์ ํ ๋ฉ์์ง๋ง ์ฝ๋ ๋์์ ๋ ๋ฒ์ poll๋ก ๋ช ํํ๊ฒ ๋ณด์ฌ์ค๋๋ค.
147-207: LGTM!๊ธฐ์กด ์คํ์ ์ด ์์ ๋
auto.offset.reset์ค์ ์ด ๋ฌด์๋๋ ๋์์ ์ ๋ณด์ฌ์ค๋๋ค. ์ฒซ ์คํ์์ ์ปค๋ฐ ํ ์ฌ์คํ ์ ๋ง์ง๋ง ์ปค๋ฐ ์์น๋ถํฐ ์ฝ๋ ์๋๋ฆฌ์ค๊ฐ ๋ช ํํฉ๋๋ค.modules/kafka/src/test/java/com/loopers/kafka/learning/README.md (1)
1-36: ๋ฌธ์ ๊ตฌ์กฐ๊ฐ ์ ์ ๋ฆฌ๋์ด ์์ต๋๋ค.Embedded Kafka ์ฌ์ฉ์ผ๋ก Docker ๋ถํ์, ์๋ ํ ํฝ ์์ฑ ๋ฑ ์ฌ์ ์ค๋น ์ฌํญ์ด ๋ช ํํ๊ฒ ์ค๋ช ๋์ด ์์ต๋๋ค.
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java (2)
1-44: ๋ฐฐ์น ํฌ๊ธฐ ํ์ต ํ ์คํธ๊ฐ ์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
max.poll.records์ค์ ์ ํธ๋ ์ด๋์คํ(์ฒ๋ฆฌ๋ vs ์์ ์ฑ, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ, ์ฌ์ฒ๋ฆฌ ๋ฒ์)๋ฅผ ๋ค์ํ ์๋๋ฆฌ์ค๋ก ๋ณด์ฌ์ฃผ๋ ๊ตฌ์กฐ๊ฐ ์ข์ต๋๋ค.
185-253: compareBatchSizes ํ ์คํธ๊ฐ ์ ์ฉํฉ๋๋ค.์ฌ๋ฌ ๋ฐฐ์น ํฌ๊ธฐ(10, 50, 100, 200)๋ฅผ ์์ฐจ์ ์ผ๋ก ๋น๊ตํ์ฌ trade-off๋ฅผ ์ง์ ์ฒดํํ ์ ์๋ ์ข์ ํ์ต ํ ์คํธ์ ๋๋ค.
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment3_PollIntervalTest.java (3)
1-46: LGTM!
max.poll.interval.msํ์ต ํ ์คํธ๊ฐ ์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค. ํ์์์ ๋ฐ์ ์ ๋ฆฌ๋ฐธ๋ฐ์ฑ, ์ ์ ์ฒ๋ฆฌ, ๋ฐฐ์น ํฌ๊ธฐ ์กฐ์ ์ ํตํ ํ์์์ ๋ฐฉ์ง ๋ฑ ํต์ฌ ์๋๋ฆฌ์ค๋ฅผ ๋ค๋ฃจ๊ณ ์์ต๋๋ค.
47-106: ํ์์์ ์๋๋ฆฌ์ค๊ฐ ์ ๊ตฌํ๋์ด ์์ต๋๋ค.40์ด ์ฒ๋ฆฌ ์๊ฐ vs 30์ด ํ์์์ ์ค์ ์ผ๋ก ๋ฆฌ๋ฐธ๋ฐ์ฑ ๋ฐ์์ ์ง์ ์ฒดํํ ์ ์์ต๋๋ค. ํ ์คํธ ์์ ์๊ฐ(์ฝ 2๋ถ)์ ๋ํ ๊ฒฝ๊ณ ๋ ์ ์ ํ ํฌํจ๋์ด ์์ต๋๋ค.
159-210: LGTM!๋ฐฐ์น ํฌ๊ธฐ ์กฐ์ (10๊ฐ)์ผ๋ก ํ์์์์ ๋ฐฉ์งํ๋ ์๋๋ฆฌ์ค๊ฐ ์ ๊ตฌํ๋์ด ์์ต๋๋ค. "๋ฐฐ์น ํฌ๊ธฐ ร ๋ฉ์์ง ์ฒ๋ฆฌ ์๊ฐ < max.poll.interval.ms" ๊ณต์์ ์ค์ ๋ก ๋ณด์ฌ์ฃผ๋ ์ข์ ์์ ์ ๋๋ค.
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment1_AutoCommitTest.java (3)
1-45: Auto Commit vs Manual Commit ํ์ต ํ ์คํธ๊ฐ ์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.์ปค๋ฐ ํ์ด๋ฐ์ด ๋ฉ์์ง ์ฒ๋ฆฌ์ ๋ฏธ์น๋ ์ํฅ์ ๋ช ํํ๊ฒ ๋ณด์ฌ์ฃผ๋ ๋ ๊ฐ์ง ์๋๋ฆฌ์ค๊ฐ ์ ์ค๋น๋์ด ์์ต๋๋ค.
46-119: Auto Commit ๋ฉ์์ง ์ ์ค ์๋๋ฆฌ์ค๊ฐ ์ ๊ตฌํ๋์ด ์์ต๋๋ค.4์ด ๋๊ธฐ๋ก 3์ด auto.commit.interval์ ์ด๊ณผํ์ฌ ์๋ ์ปค๋ฐ์ด ๋ฐ์ํ๊ณ , ์ฒ๋ฆฌ ์คํจํ ๋ฉ์์ง(5~9)๊ฐ ์ ์ค๋๋ ์ํฉ์ ๋ช ํํ๊ฒ ๋ณด์ฌ์ค๋๋ค.
121-195: Manual Commit ํ ์คํธ๊ฐ At-Least-Once ๋ณด์ฅ์ ์ ๋ณด์ฌ์ค๋๋ค.์ปค๋ฐํ์ง ์๊ณ ์ข ๋ฃ ์ ์ฌ์์ ํ ๋ชจ๋ ๋ฉ์์ง๋ฅผ ๋ค์ ์ฝ์ ์ ์์์ ๋ช ํํ๊ฒ ๋ณด์ฌ์ฃผ๋ฉฐ, ๋ฉฑ๋ฑ์ฑ์ ํ์์ฑ๋ ์ธ๊ธํ๊ณ ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java (1)
5-5: ์ํฌํธ ์ถ๊ฐ ํ์ธ ์๋ฃ
Optional์ํฌํธ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ถ๊ฐ๋์์ผ๋ฉฐ, ๋ฉ์๋์ ๋ฐํ ํ์ ์ ํ์ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (1)
8-11: ์ํฌํธ ์ ๋ฆฌ ํ์ธ ์๋ฃ์ค๋ณต๋ ์ํฌํธ๋ฅผ ์ ๊ฑฐํ๊ณ ๋ช ์์ ์ผ๋ก ์ ์ธํ์ฌ ๊ฐ๋ ์ฑ์ด ๊ฐ์ ๋์์ต๋๋ค.
apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java (1)
3-5: ํ ์คํธ ์ํฌํธ ์ ๋ฆฌ ํ์ธ ์๋ฃํ ์คํธ์ ํ์ํ static ์ํฌํธ์
Function์ํฌํธ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ถ๊ฐ๋์์ต๋๋ค.Also applies to: 11-11
apps/commerce-api/src/test/java/com/loopers/support/error/CoreExceptionTest.java (1)
3-4: Static ์ํฌํธ ์ ๋ฆฌ ํ์ธ ์๋ฃ์ค๋ณต๋ static ์ํฌํธ๊ฐ ์ ๊ฑฐ๋๊ณ ๊น๋ํ๊ฒ ์ ๋ฆฌ๋์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/dataplatform/DataPlatformEventHandler.java (1)
8-9: ์ํฌํธ ์ฌ์ ๋ ฌ ํ์ธ ์๋ฃ์ํฌํธ๊ฐ ํ์ผ ์๋จ์ผ๋ก ์ด๋๋์ด ๊ฐ๋ ์ฑ์ด ๊ฐ์ ๋์์ผ๋ฉฐ, ์ค๋ณต์ด ์ ๊ฑฐ๋์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClientConfig.java (1)
7-7: ์ํฌํธ ์ฌ์ ๋ ฌ ํ์ธ ์๋ฃ์ํฌํธ ์์๊ฐ ์กฐ์ ๋์์ผ๋ฉฐ, Feign ํด๋ผ์ด์ธํธ ์ค์ ํด๋์ค๋
@Configuration์์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java (1)
4-4: ์ํฌํธ ์์น ๋ณ๊ฒฝ ํ์ธ ์๋ฃ
TimeZone์ํฌํธ๊ฐ ์ ์ ํ ์์น๋ก ์ด๋๋์์ผ๋ฉฐ, ์ ํ๋ฆฌ์ผ์ด์ ์ด๊ธฐํ ๋ก์ง์ ๋ณ๊ฒฝ๋์ง ์์์ต๋๋ค.modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java (1)
11-11: Lombok ์ํฌํธ ์์น ๋ณ๊ฒฝ ํ์ธ ์๋ฃ
Getter์ํฌํธ๊ฐ ์ฌ์ ๋ ฌ๋์์ผ๋ฉฐ,BaseEntity์ ๊ธฐ๋ฅ์๋ ๋ณ๊ฒฝ์ด ์์ต๋๋ค.apps/commerce-streamer/src/main/resources/application.yml (2)
17-17: ์ ํ๋ฆฌ์ผ์ด์ ์ด๋ฆ ์์ ์ด ์ฌ๋ฐ๋ฆ ๋๋ค.
commerce-api์์commerce-streamer๋ก ๋ณ๊ฒฝํ์ฌ ๋ชจ๋๋ช ๊ณผ ์ผ์นํ๋๋ก ์์ ๋์์ต๋๋ค.
28-32: Kafka ํ ํฝ ์ค์ ์ด ์ ์ ํฉ๋๋ค.์นดํ๋ก๊ทธ ์ด๋ฒคํธ์ ์ฃผ๋ฌธ ์ด๋ฒคํธ๋ฅผ ์ํ ํ ํฝ ๋งคํ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์์ผ๋ฉฐ, EventKafkaProducer์ ํ ํฝ ํด๊ฒฐ ๋ก์ง๊ณผ ์ผ์นํฉ๋๋ค.
apps/commerce-api/build.gradle.kts (1)
5-5: Kafka ๋ชจ๋ ์์กด์ฑ ์ถ๊ฐ๊ฐ ์ ์ ํฉ๋๋ค.Kafka ํตํฉ์ ์ํด ํ์ํ ์์กด์ฑ์ด๋ฉฐ, ๋ค๋ฅธ ๋ชจ๋ ์์กด์ฑ๋ค๊ณผ ์ผ๊ด๋๊ฒ ๊ตฌ์ฑ๋์์ต๋๋ค.
apps/commerce-api/src/main/resources/application.yml (3)
24-24: Kafka ์ค์ import๊ฐ ์ ์ ํฉ๋๋ค.Kafka ํตํฉ์ ์ํด ํ์ํ ์ค์ ํ์ผ์ด ์ฌ๋ฐ๋ฅด๊ฒ import๋์์ต๋๋ค.
30-34: Kafka ํ ํฝ ๋งคํ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์์ต๋๋ค.
catalog-events์order-eventsํ ํฝ์ด ์ ์๋์์ผ๋ฉฐ, EventKafkaProducer์ aggregate type ๊ธฐ๋ฐ ํ ํฝ ํด๊ฒฐ ๋ก์ง๊ณผ ์ผ์นํฉ๋๋ค.
47-50: ํ ์คํธ ํ๊ฒฝ์์ Outbox Publisher ๋นํ์ฑํ๊ฐ ์ ์ ํฉ๋๋ค.๋ก์ปฌ ๋ฐ ํ ์คํธ ํ๊ฒฝ์์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค์ผ์ค๋ฌ์ ์ํ ์ด๋ฒคํธ ๋ฐํ์ ๋ฐฉ์งํ์ฌ ํ ์คํธ ๊ฒฉ๋ฆฌ์ฑ์ ๋ณด์ฅํ๋ ์ค์ ์ ๋๋ค. ์ด๋
@ConditionalOnProperty๋ก ๊ตฌํ๋ OutboxEventPublisher์ ์ผ์นํฉ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueueRepository.java (1)
1-12: DLQ ๋ฆฌํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ค๊ณ๋์์ต๋๋ค.๋จ์ผ ์ฑ ์ ์์น์ ๋ฐ๋ฅด๋ ๊ฐ๊ฒฐํ ์ธํฐํ์ด์ค๋ก, DeadLetterQueue ์ํฐํฐ์ ์์์ฑ์ ๋ด๋นํฉ๋๋ค. DLQ ์๋น์ค์์ ์คํจํ ๋ฉ์์ง๋ฅผ ์ ์ฅํ๋ ๋ฐ ์ ์ ํ๊ฒ ์ฌ์ฉ๋ฉ๋๋ค.
docker/init-db.sql (1)
27-41: ์ง๊ณ ํ ์ด๋ธ ์ค๊ณ๊ฐ ์ ์ ํจ
product_metricsํ ์ด๋ธ ์ค๊ณ๊ฐ ์ ๋์ด ์์ต๋๋ค:
version์ปฌ๋ผ์ ํตํ ๋๊ด์ ๋ฝ ๊ตฌํ์ผ๋ก ๋์์ฑ ์ ์ด- DESC ์ธ๋ฑ์ค๋ฅผ ํตํด ์ธ๊ธฐ ์ํ ์กฐํ ์ต์ ํ (์ข์์/์กฐํ/์ฃผ๋ฌธ ์ ๊ธฐ์ค)
- ์ ์ ํ ๋ฐ์ดํฐ ํ์ ์ ํ (INT, DECIMAL(15,2))
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java (1)
1-13: LGTM!JPA Repository ์ธํฐํ์ด์ค ๊ตฌํ์ด ์ ์ ํ๋ฉฐ, Spring Data JPA ์ปจ๋ฒค์ ์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค. ์ฝ๋๋ฒ ์ด์ค์ ๋ค๋ฅธ ๋ฆฌํฌ์งํ ๋ฆฌ ํจํด๊ณผ๋ ์ผ๊ด์ฑ์ด ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
6-6: ์บ์ ์๋น์ค ์์กด์ฑ ์ถ๊ฐ๊ฐ ์ ์ ํจ์ฌ๊ณ ์์ง ์ ์บ์ ๋ฌดํจํ๋ฅผ ์ํด
ProductCacheService์์กด์ฑ์ ์ฃผ์ ํ ๊ฒ์ด ์ ์ ํฉ๋๋ค.Also applies to: 42-42
apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java (1)
45-58: LGTM!Inbox ์ ์ฅ ๋ก์ง์ด ๋ช ํํ๊ณ ํธ๋์ญ์ ์ฒ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค. Builder ํจํด์ ์ฌ์ฉํ์ฌ ๊ฐ๋ ์ฑ์ด ์ข์ผ๋ฉฐ, ์ ์ ํ ๋ก๊น ๋ ํฌํจ๋์ด ์์ต๋๋ค.
ํ์ต ๋ด์ฉ(Based on learnings): EventInboxAspect๊ฐ ์คํจ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํ๋ฏ๋ก, ์ด ์๋น์ค๋ ์ฑ๊ณต ์ผ์ด์ค์๋ง ์ง์คํ๋ ๊ฒ์ด ์ ์ ํฉ๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java (1)
1-21: LGTM!DLQ Repository ๊ตฌํ์ด ์ฝ๋๋ฒ ์ด์ค์ ๋ค๋ฅธ Repository ๊ตฌํ์ฒด(EventInboxRepositoryImpl, ProductMetricsRepositoryImpl)์ ์ผ๊ด๋ ํจํด์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค. JPA Repository๋ฅผ ๊ฐ์ธ๋ ๊ตฌ์กฐ๊ฐ ๋ช ํํ๊ณ ์ ์ ํฉ๋๋ค.
apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java (1)
30-69: LGTM!์ธ ๊ฐ์ง ํ ์คํธ ์ผ์ด์ค๊ฐ EventInboxService์ ํต์ฌ ๊ธฐ๋ฅ์ ์ ๊ฒ์ฆํ๊ณ ์์ต๋๋ค:
- ์ค๋ณต ์ด๋ฒคํธ ๊ฐ์ง
- ์ ๊ท ์ด๋ฒคํธ ํ์ธ
- Inbox ์ ์ฅ ๋ฐ ๊ฒ์ฆ
ํ ์คํธ ๋ก์ง์ด ๋ช ํํ๊ณ , Given-When-Then ํจํด์ ์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInboxRepository.java (1)
1-22: LGTM!Repository ์ธํฐํ์ด์ค๊ฐ ๋ช ํํ๊ณ ๊ฐ๊ฒฐํฉ๋๋ค. ๋ฉฑ๋ฑ์ฑ ์ฒดํฌ๋ฅผ ์ํ
existsByEventId(), ์ ์ฅ์ ์ํsave(), ํ ์คํธ๋ฅผ ์ํdeleteAll()๋ฉ์๋๊ฐ ์ ์ ํ๊ฒ ์ ์๋์ด ์์ต๋๋ค.์ฝ๋๋ฒ ์ด์ค์ ๋ค๋ฅธ Repository ์ธํฐํ์ด์ค๋ค๊ณผ ์ผ๊ด๋ ํจํด์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.java (1)
23-50: ์ค๋ชจํฌ ํ ์คํธ์ Consumer ํตํฉ ํ ์คํธ๊ฐ ์ ์ ํ๊ฒ ๊ณ์ธตํ๋์ด ์์ต๋๋คBean ์์ฑ๊ณผ Context ๋ก๋ฉ์ ๊ฒ์ฆํ๋ ์ค๋ชจํฌ ํ ์คํธ๊ฐ ์ ์์ฑ๋์์ต๋๋ค. ์ค์ Consumer ๋์ ๊ฒ์ฆ์ CatalogEventConsumerTest ๋ฐ OrderEventConsumerTest์์ ๋จ์ ํ ์คํธ๋ก ๊ตฌํ๋์ด ์์ผ๋ฉฐ, modules/kafka ํ์์๋ @embeddedkafka๋ฅผ ์ฌ์ฉํ 4๊ฐ์ ํ์ต ํ ์คํธ(Experiment1~4)๊ฐ ์กด์ฌํ์ฌ ํ ์คํธ ๊ณ์ธตํ๊ฐ ๋ช ํํฉ๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxJpaRepository.java (1)
1-12: ๊ตฌํ์ด ๊น๋ํฉ๋๋ค.Spring Data JPA์ ํ์ค ๋ช ๋ช ๊ท์น์ ๋ฐ๋ฅด๋ฉฐ, Inbox ํจํด์ ์ค๋ณต ์ฒดํฌ๋ฅผ ์ํ
existsByEventId๋ฉ์๋๊ฐ ์ ์ ํ๊ฒ ์ ์๋์ด ์์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueJpaRepository.java (1)
1-10: DLQ ์ ์ฅ์ ๊ตฌํ์ด ์ ์ ํฉ๋๋ค.ํ์ฌ ์๊ตฌ์ฌํญ์ ํ์ํ ๊ธฐ๋ณธ CRUD ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ฉฐ, ํฅํ ํ์ ์ ์ปค์คํ ์ฟผ๋ฆฌ ๋ฉ์๋๋ฅผ ์ถ๊ฐํ ์ ์๋ ํ์ฅ ๊ฐ๋ฅํ ๊ตฌ์กฐ์ ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java (1)
8-8: Feature toggle ์ถ๊ฐ๊ฐ ์ ์ ํฉ๋๋ค.
@ConditionalOnProperty๋ฅผ ํตํด Outbox Publisher๋ฅผ ์ ํ์ ์ผ๋ก ํ์ฑํํ ์ ์์ด, ํ ์คํธ ํ๊ฒฝ์ด๋ ํน์ ํ๋กํ์ผ์์ ์ค์ผ์ค๋ฌ๋ฅผ ๋นํ์ฑํํ ์ ์์ต๋๋ค.matchIfMissing=true์ค์ ์ผ๋ก ๊ธฐ๋ณธ๊ฐ์ด ํ์ฑํ๋์ด ์์ด ์์ ํฉ๋๋ค.Also applies to: 20-20
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java (1)
1-24: ๋๋ฉ์ธ ์ ์ฅ์ ์ธํฐํ์ด์ค ์ค๊ณ๊ฐ ์ฐ์ํฉ๋๋ค.DIP(Dependency Inversion Principle)๋ฅผ ์ค์ํ๋ฉฐ, ๋ฉ์๋ ์๊ทธ๋์ฒ๊ฐ ๋ช ํํ๊ณ ๋ฌธ์ํ๋ ์ ๋์ด ์์ต๋๋ค.
deleteAll()๋ฉ์๋์ ํ ์คํธ์ฉ์์ ๋ช ์ํ ์ฃผ์๋ ์ข์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
1-32: ๋ฆฌํฌ์งํ ๋ฆฌ ๊ตฌํ์ด ์ผ๊ด์ฑ ์์ต๋๋ค.์ฝ๋๋ฒ ์ด์ค์ ๋ค๋ฅธ ๋ฆฌํฌ์งํ ๋ฆฌ ๊ตฌํ์ฒด๋ค(
EventInboxRepositoryImpl,DeadLetterQueueRepositoryImpl)๊ณผ ๋์ผํ ํจํด์ ๋ฐ๋ฅด๊ณ ์์ด ์ ์ง๋ณด์์ฑ์ด ๋์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java (1)
63-69: ํ ํฝ ๋ผ์ฐํ ๋ก์ง์ด ์ ์ ํฉ๋๋ค.
toUpperCase()๋ฅผ ์ฌ์ฉํ ๋์๋ฌธ์ ๋ฌด๊ด ๋งค์นญ๊ณผ ๋ช ํํ switch ํํ์, ๊ทธ๋ฆฌ๊ณ ์ ์ ์๋ ํ์ ์ ๋ํ ์์ธ ์ฒ๋ฆฌ๊ฐ ์ ๋์ด ์์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (1)
31-31: ํธ๋์ญ์ ๊ณผ ๋ก๊น ์ฒ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.
@Transactional์ด๋ ธํ ์ด์ ์ผ๋ก DLQ ์ ์ฅ์ ์์์ฑ์ ๋ณด์ฅํ๊ณ , ์๋ฌ ๋ ๋ฒจ ๋ก๊ทธ์ ํ์ํ ๋ชจ๋ ์ ๋ณด(ํ ํฝ, ์ด๋ฒคํธID, ์ฌ์๋ ํ์, ์๋ฌ ๋ฉ์์ง)๊ฐ ํฌํจ๋์ด ์์ด ์ด์ ์ค ๋ฌธ์ ์ถ์ ์ด ์ฉ์ดํฉ๋๋ค.Also applies to: 44-47
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java (2)
59-77: ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๊ฐ ์ฐ์ํฉ๋๋ค.์ข์์ ์์ฑ ์ด๋ฒคํธ ์ฒ๋ฆฌ์ ์ ์ ํ๋ก์ฐ๋ฅผ ์ ๊ฒ์ฆํ๊ณ ์์ต๋๋ค. Inbox ์ ์ฅ๊ณผ ๋ฉํธ๋ฆญ ์ฆ๊ฐ๋ฅผ ๋ชจ๋ ํ์ธํ๊ณ ์์ด ๋น์ฆ๋์ค ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๋์ํ๋์ง ๊ฒ์ฆํฉ๋๋ค.
198-210: ํฌํผ ๋ฉ์๋๊ฐ ์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.ํ ์คํธ ๋ฐ์ดํฐ ์์ฑ ๋ก์ง์ ๋ณ๋ ๋ฉ์๋๋ก ๋ถ๋ฆฌํ์ฌ ๊ฐ๋ ์ฑ๊ณผ ์ฌ์ฌ์ฉ์ฑ์ ๋์์ต๋๋ค. ObjectMapper๋ฅผ ์ฌ์ฉํ ์ง๋ ฌํ๋ ์ค์ ๋ฐํ์ ๋์๊ณผ ์ผ์นํ์ฌ ์ ์ ํฉ๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java (1)
11-30: ํ์ค์ ์ธ Repository ๊ตฌํ์ ๋๋ค.JPA Repository๋ก์ ๋จ์ ์์ ํจํด์ ์ฌ์ฉํ๊ณ ์์ผ๋ฉฐ, ๋ค๋ฅธ Repository ๊ตฌํ์ฒด๋ค(DeadLetterQueueRepositoryImpl, ProductMetricsRepositoryImpl)๊ณผ ์ผ๊ด๋ ๊ตฌ์กฐ๋ฅผ ์ ์งํ๊ณ ์์ต๋๋ค. Repository ๋ ์ด์ด์ ๋น์ฆ๋์ค ๋ก์ง์ด ์์ด ์ ์ ํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java (2)
122-135: ์ฌ๊ณ ์์ง ์ ์บ์ ๋ฌดํจํ ๋ก์ง์ด ์ ์ ํฉ๋๋ค.์ฌ๊ณ ๊ฐ 0์ด ๋์์ ๋ ํด๋น ์ํ์ ์บ์๋ฅผ ๋ฌดํจํํ์ฌ ๋ค์ ์กฐํ ์ ์ต์ ๋ฐ์ดํฐ๋ฅผ DB์์ ๊ฐ์ ธ์ค๋๋ก ๋ณด์ฅํฉ๋๋ค. OrderFacade์์ ํตํฉ๋ ์ ๋์ด ์์ผ๋ฉฐ, ๋ก๊น ๋ ์ ์ ํ ์ถ๊ฐ๋์์ต๋๋ค.
137-144: ์ ์ฒด ์บ์ ์ญ์ ๋ฉ์๋๋ ์ ์คํ๊ฒ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
allEntries = true์ต์ ์ ์บ์์ ๋ชจ๋ ํญ๋ชฉ์ ์ ๊ฑฐํ๋ฏ๋ก, ์บ์์ ๋ค๋ฅธ ๊ด๋ จ ํญ๋ชฉ์ด ํฌํจ๋์ด ์์ผ๋ฉด ์ฑ๋ฅ ์ ํ๋ฅผ ์ด๋ํ ์ ์์ต๋๋ค. ๋๋ ์ ๋ฐ์ดํธ ๋ฑ ํน์ํ ๊ฒฝ์ฐ์๋ง ์ฌ์ฉํ๋๋ก ์ ํํ๋ ๊ฒ์ด ์ข์ต๋๋ค.apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.java (3)
92-121: String payload ์ฒ๋ฆฌ ํ ์คํธ๊ฐ ์ค์ํ ์ฃ์ง ์ผ์ด์ค๋ฅผ ์ปค๋ฒํฉ๋๋ค.payload๊ฐ Map์ด ์๋ JSON String์ผ๋ก ์ ๋ฌ๋๋ ๊ฒฝ์ฐ๋ฅผ ํ ์คํธํ๊ณ ์์ต๋๋ค. ์ด๋ ์ค์ Kafka ๋ฉ์์ง์์ ๋ฐ์ํ ์ ์๋ ์ง๋ ฌํ ๋ฐฉ์ ์ฐจ์ด๋ฅผ ๋ฐ์ํ๋ฉฐ, Consumer์ ๊ฒฌ๊ณ ์ฑ์ ๊ฒ์ฆํ๋ ์ข์ ํ ์คํธ์ ๋๋ค.
148-166: ๊ฒฐ์ ์ฑ๊ณต ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก์ง์ ํฅํ ํ์ฅ ๊ฐ๋ฅ์ฑ์ ๊ณ ๋ คํ์ธ์.ํ์ฌ๋ Inbox์๋ง ์ ์ฅํ๊ณ ์ถ๊ฐ ๋น์ฆ๋์ค ๋ก์ง์ด ์๋ค๊ณ ๋ช ์๋์ด ์์ต๋๋ค. ํฅํ ๊ฒฐ์ ์ฑ๊ณต ์ ์ถ๊ฐ ์ฒ๋ฆฌ๊ฐ ํ์ํ ๊ฒฝ์ฐ(์: ํฌ์ธํธ ์ ๋ฆฝ, ์๋ฆผ ๋ฐ์ก ๋ฑ) ์ด ๋ถ๋ถ์ ํ์ฅํ ์ ์๋๋ก ์ค๋น๋์ด ์์ต๋๋ค.
174-218: ์์ธ ์ํฉ ์ฒ๋ฆฌ ํ ์คํธ๊ฐ ๊ฒฌ๊ณ ํฉ๋๋ค.payload ๋๋ฝ ๋ฐ ํ์ ํ๋ ๋๋ฝ ์๋๋ฆฌ์ค๋ฅผ ๋ชจ๋ ํ ์คํธํ๊ณ ์์ผ๋ฉฐ, ๊ฐ ๊ฒฝ์ฐ์ ์ ์ ํ ๋์(Inbox ์ ์ฅ์ ํ๋ ๋ฉํธ๋ฆญ ์ ๋ฐ์ดํธ๋ ํ์ง ์์)์ ๊ฒ์ฆํฉ๋๋ค. ์ด๋ ๋ถ๋ถ ์คํจ ์์๋ ์์คํ ์ด ์์ ์ ์ผ๋ก ๋์ํ๋๋ก ๋ณด์ฅํฉ๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (3)
25-33: ์ธ๋ฑ์ค ์ ๋ต์ด ์ ์ ํฉ๋๋ค.๋ฉํธ๋ฆญ ์ปฌ๋ผ์ DESC ์ธ๋ฑ์ค๋ฅผ ์์ฑํ์ฌ ์ธ๊ธฐ ์ํ ์กฐํ ์ฟผ๋ฆฌ(์ข์์ ์/์กฐํ ์/์ฃผ๋ฌธ ์ ๊ธฐ์ค ์ ๋ ฌ)๋ฅผ ํจ์จ์ ์ผ๋ก ์ง์ํฉ๋๋ค. updated_at ์ธ๋ฑ์ค๋ ์ต๊ทผ ์ ๋ฐ์ดํธ๋ ๋ฉํธ๋ฆญ ์กฐํ์ ์ ์ฉํฉ๋๋ค.
50-51: BigDecimal precision ์ค์ ์ด ์ ์ ํฉ๋๋ค.
precision = 15, scale = 2๋ก ์ค์ ํ์ฌ ์ต๋ 9,999,999,999,999.99๊น์ง ํํ ๊ฐ๋ฅํ๋ฉฐ, ์ด๋ ๋๋ถ๋ถ์ e-commerce ํ๋งค ๊ธ์ก์ ์ถฉ๋ถํ ์ปค๋ฒํฉ๋๋ค.
111-114: ์ฃผ๋ฌธ ์ ๋ฐ ํ๋งค ๊ธ์ก ์ฆ๊ฐ ๋ก์ง์ด ์ ํํฉ๋๋ค.์ฃผ๋ฌธ ์๋๊ณผ ํ๋งค ๊ธ์ก์ ๋์์ ์ ๋ฐ์ดํธํ๋ฉฐ, BigDecimal์ add ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ ๋ฐ๋๋ฅผ ์ ์งํฉ๋๋ค. ๋๊ด์ ๋ฝ(@Version)๊ณผ ํจ๊ป ๋์์ฑ ์ด์๋ฅผ ์ ์ ํ ์ฒ๋ฆฌํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java (1)
64-64: ํธ๋์ญ์ ์ ํ ์ค์ ์ด ์ ์ ํฉ๋๋ค.
REQUIRES_NEW๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฒคํธ ๋ฐํ ๋ก์ง์ ๋ณ๋ ํธ๋์ญ์ ์์ ์คํํฉ๋๋ค. ์ด๋ ๋น์ฆ๋์ค ํธ๋์ญ์ (saveEvent)๊ณผ ๋ฐํ ํธ๋์ญ์ ์ ๋ถ๋ฆฌํ์ฌ, ๋ฐํ ์คํจ๊ฐ ์๋ณธ ํธ๋์ญ์ ์ ์ํฅ์ ์ฃผ์ง ์๋๋ก ๋ณด์ฅํฉ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java (2)
20-27: ์ธ๋ฑ์ค ์ค๊ณ๊ฐ idempotency์ ์กฐํ ํจํด์ ์ ์ง์ํฉ๋๋ค.
idx_event_id(unique): ์ค๋ณต ์ด๋ฒคํธ ๋ฐฉ์ง๋ฅผ ์ํ ํต์ฌ ์ธ๋ฑ์คidx_aggregate: ํน์ aggregate(์: ์ฃผ๋ฌธ 123๋ฒ)์ ๋ํ ๋ชจ๋ ์ด๋ฒคํธ ์กฐํ์ ์ ์ฉidx_processed_at: ์ค๋๋ ์ด๋ฒคํธ ์ ๋ฆฌ/์์นด์ด๋น ์ฟผ๋ฆฌ์ ํ์ฉ
31-32: eventId ๊ธธ์ด๋ ํ์ฌ ์ค์ ์ด ์ ์ ํฉ๋๋ค.EventOutbox๋ IDENTITY ์ ๋ต์ผ๋ก ์์ฑ๋ Long ํ์ ์ ์๋ ์ฆ๊ฐ ID๋ฅผ ์ฌ์ฉํ๋ฏ๋ก, ๋ฌธ์์ด๋ก ๋ณํ ์ ์ต๋ 19์์ ๋๋ค. ํ์ฌ length=50์ ์ด๋ฏธ ์ถฉ๋ถํ ์ฌ์ ๋ฅผ ์ ๊ณตํ๊ณ ์์ผ๋ฉฐ, ํฅํ ID ํ์ ๋ณ๊ฒฝ ์์๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ์กฐ์ ์ด ํ์ ์์ต๋๋ค.
Likely an incorrect or invalid review comment.
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java (2)
27-34: ์๋น์ค ๋ฉ์๋ ๊ตฌ์กฐ๊ฐ ์ผ๊ด์ ์ด๊ณ ๋ช ํํฉ๋๋ค.๋ชจ๋ ๋ฉํธ๋ฆญ ์ ๋ฐ์ดํธ ๋ฉ์๋๊ฐ ๋์ผํ ํจํด์ ๋ฐ๋ฆ ๋๋ค: ์กฐํ/์์ฑ โ ์ ๋ฐ์ดํธ โ ์ ์ฅ โ ๋ก๊น . @transactional ์ ์ฉ์ผ๋ก ์์์ฑ๋ ๋ณด์ฅ๋ฉ๋๋ค.
71-79: ์ฃผ๋ฌธ ์ ์ฆ๊ฐ ๋ก์ง์ด ์ ํํฉ๋๋ค.์๋(quantity)๊ณผ ๊ธ์ก(amount)์ ๋ชจ๋ ์ ๋ฌํ์ฌ ์ฃผ๋ฌธ ์์ ํ๋งค ๊ธ์ก์ ๋์์ ์ ๋ฐ์ดํธํฉ๋๋ค. ๋ก๊น ์๋ ๋ ๊ฐ์ด ๋ชจ๋ ํฌํจ๋์ด ์์ด ๋ชจ๋ํฐ๋ง์ ์ ์ฉํฉ๋๋ค.
| kafkaProducer.publish(outbox) | ||
| .thenAccept(result -> { | ||
| // ๋ฐํ ์ฑ๊ณต ์ Outbox ์ํ ์ ๋ฐ์ดํธ | ||
| outbox.markAsPublished(); | ||
| outboxRepository.save(outbox); | ||
|
|
||
| log.info("Outbox โ Kafka ๋ฐํ ์๋ฃ - outboxId: {}, eventType: {}, offset: {}", | ||
| outbox.getId(), | ||
| outbox.getEventType(), | ||
| result.getRecordMetadata().offset()); | ||
| }) | ||
| .exceptionally(ex -> { | ||
| // ๋ฐํ ์คํจ ์ Outbox ์ํ ์ ๋ฐ์ดํธ | ||
| outbox.markAsFailed(ex.getMessage()); | ||
| outboxRepository.save(outbox); | ||
|
|
||
| log.error("Outbox โ Kafka ๋ฐํ ์คํจ - outboxId: {}, error: {}", | ||
| outbox.getId(), ex.getMessage(), ex); | ||
|
|
||
| return null; | ||
| }) | ||
| .join(); // ๋๊ธฐ ๋๊ธฐ (ํธ๋์ญ์ ๋ด์์ ์๋ฃ ๋ณด์ฅ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
find . -type f -name "OutboxEventService.java" -o -name "*EventKafkaProducer*" 2>/dev/null | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 264
๐ Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java | sed -n '60,105p'Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2150
๐ Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java | head -80Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3056
Kafka ๋ฐํ ๋ก์ง๊ณผ ์์ธ ์ฒ๋ฆฌ์ ์ฃผ์๊ฐ ํ์ํฉ๋๋ค.
join()์ด ํธ๋์ญ์
๊ฒฝ๊ณ ๋ด ๋๊ธฐ ์๋ฃ๋ฅผ ๋ณด์ฅํ๋ ๊ฒ์ ์ฌ๋ฐ๋ฅด์ง๋ง, ์์ธ ์ฒ๋ฆฌ ๊ตฌ์กฐ์ ์ค๋ณต ์ํ์ด ์์ต๋๋ค. EventKafkaProducer์ exceptionally() ๋ธ๋ก(line 56)์์ RuntimeException์ ๋์ง๊ณ , OutboxEventService์ exceptionally() ๋ธ๋ก(lines 79-88)์์ ์ด๋ฅผ ์ฒ๋ฆฌํ๋ฉฐ markAsFailed()์ save()๋ฅผ ํธ์ถํฉ๋๋ค. ๊ทธ๋ฐ๋ฐ ์ด save() ํธ์ถ์ด ์คํจํ๋ฉด ์์ธ๊ฐ ์ ํ๋์ด join()์ด CompletionException์ ๋์ง๊ณ , ์ธ๋ถ catch ๋ธ๋ก(lines 91-99)์ด ๋ค์ markAsFailed()์ save()๋ฅผ ์๋ํ๊ฒ ๋ฉ๋๋ค. ์ด๋ ์ค๋ณต ์ํ ์
๋ฐ์ดํธ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฝ ์กฐ๊ฑด ์๋ฐ์ ์ด๋ํ ์ ์์ต๋๋ค.
exceptionally() ๋ธ๋ก์์ ๋ฐ์ํ๋ ์์ธ๋ฅผ ์ธ๋ถ catch์์ ๋ณ๋ ์ฒ๋ฆฌํ์ง ์๋๋ก ๋ถ๋ฆฌํ๊ฑฐ๋, ์ค๋ณต ํธ์ถ์ ๋ฐฉ์งํ๋ ํ๋๊ทธ๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
๐ค Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java
around lines 68 to 89, avoid double-updating Outbox status when
kafkaProducer.exceptionally already marks/saves failures: either stop rethrowing
from EventKafkaProducer.exceptionally (so it handles markAsFailed/save and
returns a completed result) or add a guard here before calling markAsFailed/save
(e.g., check outbox status/isFinalState and only update if not already
failed/published). Update the code so only one place performs markAsFailed/save
(remove the duplicate update or add the status check) and ensure kafka publish
exceptions are not rethrown into join() to prevent the outer catch from
repeating the save.
| // test - awaitility for async testing | ||
| testImplementation("org.awaitility:awaitility:4.2.0") | ||
| // test - kafka testcontainers | ||
| testImplementation("org.testcontainers:kafka") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ํ ์คํธ ์์กด์ฑ ์ถ๊ฐ๊ฐ ์ ์ ํ๋ฉฐ, awaitility ๋ฒ์ ์ ๊ทธ๋ ์ด๋๋ฅผ ๊ถ์ฅํฉ๋๋ค.
Kafka ํตํฉ ํ ์คํธ๋ฅผ ์ํ ์์กด์ฑ ์ถ๊ฐ๊ฐ PR ๋ชฉํ์ ์ ๋ถํฉํฉ๋๋ค:
- Awaitility: ๋น๋๊ธฐ Kafka ๋ฉ์์ง ์ฒ๋ฆฌ ๋๊ธฐ์ ํ์ฉ
- Testcontainers: ํตํฉ ํ ์คํธ์ฉ Kafka ์ปจํ ์ด๋ ๊ตฌ๋์ ํ์ฉ
testImplementation ์ค์ฝํ ์ฌ์ฉ๋ ์ฌ๋ฐ๋ฅด๋ฉฐ, testcontainers:kafka๊ฐ BOM์ ํตํด ๋ฒ์ ๊ด๋ฆฌ๋๋ ๊ฒ๋ ์ข์ ๊ดํ์
๋๋ค.
๋ค๋ง awaitility 4.3.0์ด 2025๋ 2์ 21์ผ์ ๋ฆด๋ฆฌ์ค๋์๊ณ ๋ ๋์ Kotlin ์๊ฐ ์ง์๊ณผ ์๋ก์ด untilAsserted ์ฌ์ฉ ๋ฐฉ์์ ์ ๊ณตํฉ๋๋ค. ํ์ฌ ์ฌ์ฉ ์ค์ธ 4.2.0์ 2022๋ 3์์ ๋ฒ์ ์ด๋ฏ๋ก, 4.3.0์ผ๋ก ์ ๊ทธ๋ ์ด๋ํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
๐ค Prompt for AI Agents
In apps/commerce-streamer/build.gradle.kts around lines 24 to 27, the Awaitility
test dependency is pinned to 4.2.0; update the version to 4.3.0 to pick up
improved Kotlin time support and untilAsserted APIs โ replace the
testImplementation("org.awaitility:awaitility:4.2.0") entry with
testImplementation("org.awaitility:awaitility:4.3.0") and run a quick test build
to ensure compatibility.
| private ProductMetrics getOrCreate(Long productId) { | ||
| return productMetricsRepository.findByProductId(productId) | ||
| .orElseGet(() -> ProductMetrics.builder() | ||
| .productId(productId) | ||
| .build()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getOrCreate ํจํด์ ๋์์ฑ ์ด์๋ฅผ ๊ณ ๋ คํ์ธ์.
findByProductId๊ฐ ๋น์ด์์ ๋ ์ ์ํฐํฐ๋ฅผ ์์ฑํ๋๋ฐ, ๋ ์ค๋ ๋๊ฐ ๋์์ ๊ฐ์ productId๋ก ์ฒ์ ๋ฉํธ๋ฆญ์ ์์ฑํ๋ ค ํ๋ฉด unique constraint ์๋ฐ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
ProductMetrics ์ํฐํฐ๊ฐ @Version์ ์ฌ์ฉํ ๋๊ด์ ๋ฝ์ ์ ์ฉํ๊ณ ์์ง๋ง, ์ต์ด ์์ฑ ์์ ์๋ ๋์์ด ๋์ง ์์ต๋๋ค. ์ค์ ํ๊ฒฝ์์ ๋์์ ๊ฐ์ ์ํ์ ์ฒซ ์ด๋ฒคํธ๊ฐ ์ฒ๋ฆฌ๋ ๊ฐ๋ฅ์ฑ์ด ๋ฎ๋ค๋ฉด ๋ฌธ์ ์์ง๋ง, ๋ฐ์ ์ ์ฌ์๋ ๋ก์ง์ด๋ ์์ธ ์ฒ๋ฆฌ๊ฐ ํ์ํฉ๋๋ค.
๐ ๋์ ๋ฐฉ์
- ์์ ๋ ์ด์ด(Consumer)์์ OptimisticLockException์ ์ก์ ์ฌ์๋
- ProductMetrics๋ฅผ ์ํ ์์ฑ ์์ ์ ๋ฏธ๋ฆฌ ์ด๊ธฐํ
- ๋๋ save ์คํจ ์ ๋ค์ ์กฐํ ์๋ํ๋ ๋ก์ง ์ถ๊ฐ
ํ์ฌ Kafka consumer์ ์ฌ์๋ ๋ฉ์ปค๋์ฆ์ด ์ด๋ฅผ ์ฒ๋ฆฌํ ์ ์๋์ง ํ์ธํ์ธ์.
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java
around lines 84-89: the current getOrCreate() can produce a unique-constraint
race when two threads create the same productId concurrently; change the flow so
creation is attempted but save failures due to duplicate key/constraint (e.g.,
DataIntegrityViolationException, ConstraintViolationException) are caught and
handled by re-querying the repository and returning the existing record (or
retrying a bounded number of times); alternatively ensure the caller (Kafka
consumer) catches OptimisticLockException/DataIntegrityViolationException and
retries โ implement the catch-and-requery pattern in this service: try to save
the new ProductMetrics, on constraint exception fetch findByProductId() again
and return that result (or bubble up after N retries).
| for (ConsumerRecord<Object, Object> record : records) { | ||
| try { | ||
| boolean processed = processEvent(record); | ||
| if (processed) { | ||
| successCount++; | ||
| } else { | ||
| skipCount++; // ์ค๋ณต ์ด๋ฒคํธ | ||
| } | ||
| } catch (Exception e) { | ||
| failCount++; | ||
| log.error("โ ์ด๋ฒคํธ ์ฒ๋ฆฌ ์คํจ - partition: {}, offset: {}, key: {}, error: {}", | ||
| record.partition(), record.offset(), record.key(), e.getMessage(), e); | ||
|
|
||
| // DLQ์ ์ ์ก | ||
| sendToDLQ(record, e, 0); | ||
| } | ||
| } | ||
|
|
||
| // Offset ์ปค๋ฐ (๋ฐฐ์น ๋จ์) | ||
| acknowledgment.acknowledge(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OrderEventConsumer์ ๋์ผํ ๋ฐฐ์น ์ปค๋ฐ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
์ด ํ์ผ๋ OrderEventConsumer์ ๋์ผํ critical ์ด์๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค. ๋ฐฐ์น ๋ด ์ผ๋ถ ๋ ์ฝ๋๊ฐ ์คํจํ์ฌ DLQ๋ก ์ ์ก๋๋๋ผ๋(Line 64), Line 69์์ ์ ์ฒด ๋ฐฐ์น์ ์คํ์
์ ์ปค๋ฐํ์ฌ ์คํจํ ๋ฉ์์ง๊ฐ ์ฌ์ฒ๋ฆฌ๋์ง ์์ต๋๋ค.
ํด๊ฒฐ์ฑ
:
OrderEventConsumer์ ์ ์๋ ๊ฒ๊ณผ ๋์ผํ ์์ ์ด ํ์ํฉ๋๋ค. ์คํจํ ๋ ์ฝ๋๊ฐ ์์ผ๋ฉด ์คํ์
์ ์ปค๋ฐํ์ง ์๊ฑฐ๋ ์์ธ๋ฅผ ๋์ ธ ์ฌ์ฒ๋ฆฌ๋ฅผ ํธ๋ฆฌ๊ฑฐํด์ผ ํฉ๋๋ค.
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java
around lines 50 to 69, the code always calls acknowledgment.acknowledge() after
processing the batch which commits offsets even when some records failed and
were sent to DLQ; change the flow so that if any record failed (failCount > 0)
you do NOT acknowledge the batch and instead throw a RuntimeException (or
propagate the caught exception) after sending to DLQ to force a retry, otherwise
acknowledge only when failCount == 0; ensure DLQ send remains inside the catch
but move/keep the ack conditional so offsets are committed only when the entire
batch succeeded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
์๊ฑฐ ์ค์ ๋ก ๋ฌธ์ ์ ๋๋ค. ํ์ธ๋ถํ๋๋ ค์
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@looper88888, ๋ค, ํ์ธํ์ต๋๋ค. CodeRabbit์ด ์ง์ ํ ๋ฌธ์ ๊ฐ ์ค์ ๋ก ์กด์ฌํฉ๋๋ค.
๐ด ๋ฌธ์ ์ํฉ
ํ์ฌ CatalogEventConsumer์ Line 50-69 ์ฝ๋๋ฅผ ๋ณด๋ฉด:
for (ConsumerRecord<Object, Object> record : records) {
try {
boolean processed = processEvent(record);
if (processed) {
successCount++;
} else {
skipCount++;
}
} catch (Exception e) {
failCount++;
// DLQ์ ์ ์ก
sendToDLQ(record, e, 0);
}
}
// โ ๏ธ ๋ฌธ์ : ์คํจ ์ฌ๋ถ์ ๊ด๊ณ์์ด ํญ์ ์ปค๋ฐ
acknowledgment.acknowledge();๋ฌธ์ ์ :
- ๋ฐฐ์น ๋ด ์ผ๋ถ ๋ ์ฝ๋๊ฐ ์ฒ๋ฆฌ ์คํจํ๋ฉด
failCount๊ฐ ์ฆ๊ฐํ๊ณ DLQ๋ก ์ ์ก๋ฉ๋๋ค - ํ์ง๋ง Line 69์์ ๋ฌด์กฐ๊ฑด
acknowledgment.acknowledge()๋ฅผ ํธ์ถํฉ๋๋ค - ์ด๋ ์คํจํ ๋ ์ฝ๋์ ์คํ์ ๋ ์ปค๋ฐ๋์ด ์ฌ์ฒ๋ฆฌ ๊ธฐํ๋ฅผ ์๊ฒ ๋ฉ๋๋ค
- ๊ฒฐ๊ณผ์ ์ผ๋ก ์คํจํ ๋ฉ์์ง๋ DLQ์๋ง ์ ์ฅ๋๊ณ , ์๋ณธ ํ ํฝ์์๋ ์๊ตฌ์ ์ผ๋ก ์๋น๋ ๊ฒ์ผ๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค
๐ก ํด๊ฒฐ ๋ฐฉ์
์คํจํ ๋ ์ฝ๋๊ฐ ์์ ๊ฒฝ์ฐ ์คํ์ ์ ์ปค๋ฐํ์ง ์์์ผ ํฉ๋๋ค:
// Offset ์ปค๋ฐ (๋ฐฐ์น ๋จ์)
if (failCount > 0) {
log.warn("โ ๏ธ ์คํจํ ์ด๋ฒคํธ๊ฐ ์์ด Offset์ ์ปค๋ฐํ์ง ์์ต๋๋ค - fail: {}, success: {}, skip: {}",
failCount, successCount, skipCount);
throw new RuntimeException(
String.format("๋ฐฐ์น ์ฒ๋ฆฌ ์ค %d๊ฐ ์คํจ, ์ฌ์ฒ๋ฆฌ๋ฅผ ์ํด Offset ์ปค๋ฐ ๊ฑฐ๋ถ", failCount)
);
}
acknowledgment.acknowledge();
log.info("โ
Catalog ๋ฐฐ์น ์ฒ๋ฆฌ ์๋ฃ - success: {}, skip: {}, fail: {}",
successCount, skipCount, failCount);์ด๋ ๊ฒ ํ๋ฉด:
- ์คํจํ ๋ ์ฝ๋๊ฐ ์์ผ๋ฉด ์์ธ๋ฅผ ๋์ ธ Kafka๊ฐ ํด๋น ๋ฐฐ์น๋ฅผ ์ฌ์ฒ๋ฆฌํ๋๋ก ํฉ๋๋ค
- ์คํจํ ๋ ์ฝ๋๋ DLQ์๋ ์ ์ฅ๋๊ณ , ๋์์ ์ฌ์ฒ๋ฆฌ ๊ธฐํ๋ ์ป์ต๋๋ค
- ๋ชจ๋ ๋ ์ฝ๋๊ฐ ์ฑ๊ณตํ์ ๋๋ง ์คํ์ ์ ์ปค๋ฐํฉ๋๋ค
๋์ผํ ๋ฌธ์ ๊ฐ OrderEventConsumer์๋ ์กด์ฌํ๋ฏ๋ก ํจ๊ป ์์ ์ด ํ์ํฉ๋๋ค.
๐ง Learnings used
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:54.339Z
Learning: In this codebase, Kafka consumers delegate error handling and event tracking to the service layer via EventInboxAspect. Service methods annotated with InboxEvent are intercepted by the aspect, which handles failures by saving failed EventInbox entries and logging errors. This centralized approach avoids duplicating error handling logic across multiple consumers.
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: docs/week8/round8-detailed-design.md:151-178
Timestamp: 2025-12-19T20:59:50.009Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository's Kafka event pipeline, only 5 domain events are intentionally published to Kafka via CloudEventEnvelopeFactory: OrderPaidEventV1, LikeCreatedEventV1, LikeCanceledEventV1, ProductViewedEventV1, and StockDepletedEventV1. Other domain events (OrderCreatedEventV1, OrderCanceledEventV1, PaymentCreatedEventV1, PaymentPaidEventV1, PaymentFailedEventV1) are internal-only and intentionally not mapped in resolveMetadata(), which correctly returns null for them to exclude them from Outbox publication.
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.
| eventInboxService.save(eventId, aggregateType, aggregateId, eventType); | ||
|
|
||
| // 3. ๋น์ฆ๋์ค ๋ก์ง ์ฒ๋ฆฌ | ||
| Long productId = Long.parseLong(aggregateId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Long.parseLong์ ์์ธ ์ฒ๋ฆฌ๊ฐ ์์ต๋๋ค.
aggregateId๋ฅผ Long.parseLong์ผ๋ก ํ์ฑํ ๋(Line 108) NumberFormatException์ด ๋ฐ์ํ ์ ์์ง๋ง ์์ธ ์ฒ๋ฆฌ๊ฐ ์์ต๋๋ค.
๋ฌธ์ ์ :
- ์๋ชป๋ ํ์์ aggregateId๋ก ์ธํด ์ ์ฒด ํธ๋์ญ์ ์ด ์คํจ
- ์๋ฌ ๋ฉ์์ง๊ฐ ํ์ฑ ์คํจ ์์ธ์ ๋ช ํํ ๋ํ๋ด์ง ๋ชปํจ
๊ถ์ฅ ํด๊ฒฐ์ฑ
:
์ซ์ ํ์ฑ ์ ์ ์ ํจ์ฑ ๊ฒ์ฆ์ ์ถ๊ฐํ๊ฑฐ๋ try-catch๋ก ๋ ๋ช
ํํ ์๋ฌ ๋ฉ์์ง๋ฅผ ์ ๊ณตํ์ธ์.
๐ ์ซ์ ํ์ฑ ์์ธ ์ฒ๋ฆฌ ์ถ๊ฐ
// 3. ๋น์ฆ๋์ค ๋ก์ง ์ฒ๋ฆฌ
- Long productId = Long.parseLong(aggregateId);
+ Long productId;
+ try {
+ productId = Long.parseLong(aggregateId);
+ } catch (NumberFormatException e) {
+ log.error("โ ๏ธ aggregateId ํ์ฑ ์คํจ - aggregateId: {}", aggregateId, e);
+ throw new IllegalArgumentException("์๋ชป๋ aggregateId ํ์: " + aggregateId, e);
+ }
switch (eventType) {๐ 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.
| Long productId = Long.parseLong(aggregateId); | |
| // 3. ๋น์ฆ๋์ค ๋ก์ง ์ฒ๋ฆฌ | |
| Long productId; | |
| try { | |
| productId = Long.parseLong(aggregateId); | |
| } catch (NumberFormatException e) { | |
| log.error("โ ๏ธ aggregateId ํ์ฑ ์คํจ - aggregateId: {}", aggregateId, e); | |
| throw new IllegalArgumentException("์๋ชป๋ aggregateId ํ์: " + aggregateId, e); | |
| } | |
| switch (eventType) { |
๐ค Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java
around line 108, Long.parseLong(aggregateId) can throw NumberFormatException and
currently lacks handling; wrap the parse in a try-catch (or pre-validate with a
regex/NumberUtils) to catch invalid format, log a clear, contextual error
including the aggregateId and event metadata, and either skip/acknowledge the
bad event or rethrow a domain-specific exception with a descriptive message so
the transaction doesn't fail silently due to an unhandled parsing error.
| properties: | ||
| auto: | ||
| offset.reset: earliest # ํ ์คํธ ํ๊ฒฝ์์๋ earliest๋ถํฐ ์ฝ๊ธฐ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
head -50 modules/kafka/src/main/resources/kafka.ymlRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1650
ํ ์คํธ ํ๊ฒฝ์ offset ์ค์ ์ ๋ธ๋ํท ํ๊ธฐ๋ฒ์ผ๋ก ์์ ํด์ฃผ์ธ์.
ํ์ฌ YAML ์ค์ ์์ properties ๋งต ๋ด๋ถ์ Kafka ์์ฑ์ด ์ค์ฒฉ๋ ๊ตฌ์กฐ๋ก ์์ฑ๋์ด ์์ต๋๋ค. Spring Boot ๊ณต์ ๋ฌธ์์ ๋ฐ๋ฅด๋ฉด, properties ๋งต ๋ด๋ถ์ Kafka ํด๋ผ์ด์ธํธ ์์ฑ์ ๋ธ๋ํท ํ๊ธฐ๋ฒ์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
ํ์ฌ ๊ตฌ์กฐ:
properties:
auto:
offset.reset: earliest์์ ํ์:
properties:
"[auto.offset.reset]": earliestYAML ํ์ผ์์๋ ๋ธ๋ํท์ด ํค๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ํ์ฑํ๊ธฐ ์ํด ๋ฐ์ดํ๋ก ๊ฐ์ธ์ ธ ์์ด์ผ ํฉ๋๋ค. ์ด๋ lines 5-14์ main profile์์๋ ๋์ผํ๊ฒ ์ ์ฉ๋ฉ๋๋ค.
๐ค Prompt for AI Agents
In modules/kafka/src/main/resources/kafka.yml around lines 38 to 40 (and apply
same change to the main profile at lines 5 to 14), the Kafka client property is
written as a nested map (auto: offset.reset: earliest); replace it with bracket
notation and quoted key so the properties map parses correctly by Spring Boot:
change the nested key to "[auto.offset.reset]" and set its value to earliest;
apply the identical quoted bracket-key change in the main profile section as
well.
| log.info("- ๋ฐฐ์น ํฌ๊ธฐ: 500๊ฐ"); | ||
| log.info("- poll ํ์: {}ํ", pollCount); | ||
| log.info("- ์ด ์๊ฐ: {}ms", totalTime); | ||
| log.info("- ์ด๋น ์ฒ๋ฆฌ๋: {:.2f}๊ฐ/์ด", (totalProcessed * 1000.0) / totalTime); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SLF4J ๋ก๊ทธ ํฌ๋งท ์ค๋ฅ
{:.2f}๋ Python ํฌ๋งท ๋ฌธ๋ฒ์ด๋ฉฐ, SLF4J์์๋ ์ง์๋์ง ์์ต๋๋ค. SLF4J๋ ๋จ์ํ {}๋ฅผ ์ฌ์ฉํ๋ฉฐ, ์์์ ํฌ๋งทํ
์ ๋ณ๋๋ก ์ฒ๋ฆฌํด์ผ ํฉ๋๋ค.
๐ ์์ ๋ฐฉ์:
- log.info("- ์ด๋น ์ฒ๋ฆฌ๋: {:.2f}๊ฐ/์ด", (totalProcessed * 1000.0) / totalTime);
+ log.info("- ์ด๋น ์ฒ๋ฆฌ๋: {}๊ฐ/์ด", String.format("%.2f", (totalProcessed * 1000.0) / totalTime));๋๋ Lines 175, 241, 242์๋ ๋์ผํ ์์ ์ด ํ์ํฉ๋๋ค:
- log.info("- ์ด๋น ์ฒ๋ฆฌ๋: {:.2f}๊ฐ/์ด", (totalProcessed * 1000.0) / totalTime);
+ log.info("- ์ด๋น ์ฒ๋ฆฌ๋: {}๊ฐ/์ด", String.format("%.2f", (totalProcessed * 1000.0) / totalTime));
- log.info(" - poll๋น ํ๊ท : {:.2f}ms", avgTimePerPoll);
+ log.info(" - poll๋น ํ๊ท : {}ms", String.format("%.2f", avgTimePerPoll));
- log.info(" - ์ด๋น ์ฒ๋ฆฌ๋: {:.2f}๊ฐ/์ด\n", (totalProcessed * 1000.0) / totalTime);
+ log.info(" - ์ด๋น ์ฒ๋ฆฌ๋: {}๊ฐ/์ด\n", String.format("%.2f", (totalProcessed * 1000.0) / totalTime));๐ 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.
| log.info("- ์ด๋น ์ฒ๋ฆฌ๋: {:.2f}๊ฐ/์ด", (totalProcessed * 1000.0) / totalTime); | |
| log.info("- ์ด๋น ์ฒ๋ฆฌ๋: {}๊ฐ/์ด", String.format("%.2f", (totalProcessed * 1000.0) / totalTime)); |
๐ค Prompt for AI Agents
In
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java
around line 105 (and similarly lines 175, 241, 242), the SLF4J log call
incorrectly uses Python-style "{:.2f}" formatting; replace it by formatting the
numeric value to the desired precision before passing it to log (e.g., use
String.format("%.2f", value) or BigDecimal#setScale) and then call
log.info("...: {}๊ฐ/์ด", formattedValue) so SLF4J receives a "{}" placeholder with
a pre-formatted string.
| int processedCount = 0; | ||
| records.forEach(record -> { | ||
| log.info("์ฒ๋ฆฌ: offset={}, value={}", record.offset(), record.value()); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
์ฌ์ฉ๋์ง ์๋ ๋ณ์
processedCount ๋ณ์๊ฐ ์ ์ธ๋์์ง๋ง ์ฌ์ฉ๋์ง ์์ต๋๋ค. ์ ๊ฑฐํ๊ฑฐ๋ ์ค์ ์นด์ดํ
๋ก์ง์ ์ถ๊ฐํด ์ฃผ์ธ์.
๐ ์์ ๋ฐฉ์:
- int processedCount = 0;
records.forEach(record -> {
log.info("์ฒ๋ฆฌ: offset={}, value={}", record.offset(), record.value());
});๐ Committable suggestion
โผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| int processedCount = 0; | |
| records.forEach(record -> { | |
| log.info("์ฒ๋ฆฌ: offset={}, value={}", record.offset(), record.value()); | |
| }); | |
| records.forEach(record -> { | |
| log.info("์ฒ๋ฆฌ: offset={}, value={}", record.offset(), record.value()); | |
| }); |
๐ค Prompt for AI Agents
In
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java
around lines 284 to 287 the local variable processedCount is declared but never
used; either remove the unused declaration or implement counting by incrementing
processedCount inside the forEach (or replace forEach with an indexed loop) and
assert or log the final count as appropriate for the test; pick one approach and
apply it consistently so there are no unused variables.
| ### 3. Consumer Group ๊ด๋ฆฌ | ||
|
|
||
| ```bash | ||
| # Consumer Group ๋ชฉ๋ก | ||
| kafka-consumer-groups.sh --bootstrap-server localhost:19092 --list | ||
|
|
||
| # ์คํ์ ํ์ธ | ||
| kafka-consumer-groups.sh --bootstrap-server localhost:19092 \ | ||
| --group learning-group \ | ||
| --describe | ||
|
|
||
| # Group ์ญ์ (์ฌ์คํ ์) | ||
| kafka-consumer-groups.sh --bootstrap-server localhost:19092 \ | ||
| --group learning-group \ | ||
| --delete | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ํฌํธ ๋ฒํธ ๋ถ์ผ์น ํ์ธ ํ์
Consumer Group ๊ด๋ฆฌ ์น์
์ Kafka CLI ๋ช
๋ น์ด๋ localhost:19092๋ฅผ ์ฌ์ฉํ์ง๋ง, Embedded Kafka ํ
์คํธ๋ localhost:9092๋ฅผ ์ฌ์ฉํฉ๋๋ค. ํผ๋์ ๋ฐฉ์งํ๊ธฐ ์ํด ํฌํธ๋ฅผ ํต์ผํ๊ฑฐ๋, ๋ ํ๊ฒฝ(Embedded vs Docker)์ ๋ํ ์ค๋ช
์ ์ถ๊ฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๐ค Prompt for AI Agents
In modules/kafka/src/test/java/com/loopers/kafka/learning/README.md around lines
166 to 181, the Kafka CLI examples use port 19092 while the Embedded Kafka tests
use 9092; either make the ports consistent or clarify environments. Update the
CLI examples to use the same port as the tests (change 19092 to 9092) if the
README is intended for the Embedded Kafka setup, or add a one-line note above
this section explaining that 19092 is for the Docker/standalone broker and 9092
is for Embedded Kafka tests so readers know which port to use for each
environment.
| ## ๐ ์ฐธ๊ณ ๋ฌธ์ | ||
|
|
||
| - `.claude/round-8/kafka-configuration-guide.md` - ์์ธ ์ค์ ๊ฐ์ด๋ | ||
| - `.claude/round-8/exactly-once-semantics.md` - Exactly-Once ๊ฐ๋ | ||
| - `.claude/round-8/inbox-pattern-analysis.md` - Inbox ํจํด ๋ถ์ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
#!/bin/bash
# Check if referenced documentation files exist
for file in ".claude/round-8/kafka-configuration-guide.md" \
".claude/round-8/exactly-once-semantics.md" \
".claude/round-8/inbox-pattern-analysis.md"; do
if [ -f "$file" ]; then
echo "โ
์กด์ฌ: $file"
else
echo "โ ์์: $file"
fi
doneRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 234
์ ์ฅ์์์ ์ฐธ๊ณ ๋ฌธ์ ํ์ผ๋ค์ด ๋๋ฝ๋จ
.claude/round-8/ ๋๋ ํ ๋ฆฌ์ ์ธ ๋ฌธ์ ํ์ผ์ด ๋ชจ๋ ์ปค๋ฐ๋์ด ์์ง ์์ต๋๋ค:
kafka-configuration-guide.md- ์์exactly-once-semantics.md- ์์inbox-pattern-analysis.md- ์์
์ด ํ์ผ๋ค์ ์ ์ฅ์์ ์ถ๊ฐํ๊ฑฐ๋ README์์ ์ฐธ์กฐ๋ฅผ ์ ๊ฑฐํด์ผ ํฉ๋๋ค.
๐ค Prompt for AI Agents
In modules/kafka/src/test/java/com/loopers/kafka/learning/README.md around lines
212 to 216, the README references three files under .claude/round-8 that are not
present in the repo; either add the missing files to the repository at
.claude/round-8/{kafka-configuration-guide.md, exactly-once-semantics.md,
inbox-pattern-analysis.md} with appropriate content and commit them, or remove
or replace the three bullet references in the README to avoid broken
documentation links; ensure commits include correct paths and update any CI or
packaging rules if those files need to be included.
|
โ Checklist ๐พ Producer โพ Consumer @coderabbitai ์ฒดํฌ๋ฆฌ์คํธ ํ์ธํด |
|
Tip For best results, initiate chat on the files or code changes. @looper88888, ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ํ์ธํ์ต๋๋ค! ๊ฐ ํญ๋ชฉ๋ณ๋ก ๊ฒ์ฆ ๊ฒฐ๊ณผ๋ฅผ ๊ณต์ ๋๋ฆฝ๋๋ค: ๐พ Producerโ ๋๋ฉ์ธ(์ ํ๋ฆฌ์ผ์ด์ ) ์ด๋ฒคํธ ์ค๊ณ
โ Producer ์ฑ์์ ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐํ
โ PartitionKey ๊ธฐ๋ฐ์ ์ด๋ฒคํธ ์์ ๋ณด์ฅ
โ ๋ฉ์์ง ๋ฐํ ์คํจ ์ ๋์
โพ Consumerโ Consumer๊ฐ Metrics ์ง๊ณ ์ฒ๋ฆฌ
โ event_handled ํ ์ด๋ธ์ ํตํ ๋ฉฑ๋ฑ ์ฒ๋ฆฌ ๊ตฌํ
โ ์ฌ๊ณ ์์ง ์ ์ํ ์บ์ ๊ฐฑ์
โ ์ค๋ณต ๋ฉ์์ง ์ฌ์ ์ก ํ ์คํธ
์ถ๊ฐ ๊ฒ์ฆ ์ฌํญโ Dead Letter Queue (DLQ) ๊ตฌํ
โ Batch ์ฒ๋ฆฌ ๋ฐ Manual Commit
โ Learning Tests
๋ชจ๋ ์ฒดํฌ๋ฆฌ์คํธ ํญ๋ชฉ์ด ์๋ฃ๋์์ต๋๋ค! ๐ Kafka ๊ธฐ๋ฐ ์ด๋ฒคํธ ํ์ดํ๋ผ์ธ์ด ๊ฒฌ๊ณ ํ๊ฒ ๊ตฌํ๋์ด ์๊ณ , Outbox/Inbox ํจํด, DLQ, ๋ฉฑ๋ฑ์ฑ ์ฒ๋ฆฌ, ๋ฐฐ์น ์ฒ๋ฆฌ ๋ฑ ํ๋ก๋์ ๋ ๋ฒจ์ ๊ณ ๋ ค์ฌํญ๋ค์ด ์ ๋ฐ์๋์ด ์์ต๋๋ค. ๐ง Learnings used |
README ํ์ผ์์ ํ๋ก์ ํธ ๋ด๋ถ ์ฐธ์กฐ๋ฅผ ์ผ๋ฐ์ ์ธ ์ค๋ช ์ผ๋ก ์์ - performance-test/README.md - modules/kafka/learning/README.md
๐ Summary
์์ฝ
Kafka ๊ธฐ๋ฐ ์ด๋ฒคํธ ํ์ดํ๋ผ์ธ ๊ตฌํ
๊ตฌํ ์ฌํญ
1. Producer
2. Consumer
max.poll.records=100์ผ๋ก ๋ฐฐ์น ๋จ์ ์ฒ๋ฆฌ3. ์บ์ ๋ฌดํจํ ์ ๋ต
OrderFacade์์product.getStock() == 0์ผ ๋ ๋ฌดํจํ)4. Embbed Kafka๋ฅผ ํตํ ์นดํ์นด ํ์ต ํ ์คํธ
๊ฐ๋จํ ์ค์ ํ ์คํธ๋ผ๊ณ ํ๋จํ๊ธฐ ๋๋ฌธ์ ์ด ๋ฐฉ์์ ์ ํํจ
๐ฌ Review Points
Q.
์ ์ด๋ฒคํธ ํธ๋ค๋ง ํ ์ด๋ธ๊ณผ ๋ก๊ทธ ํ ์ด๋ธ์ ๋ถ๋ฆฌํ๋ ๊ฑธ๊น?์ ๋ํ ์ ์ ์๊ฐ์ ๋๋ค.์ ๋
ํธ๋ค๋ง ํ ์ด๋ธ๊ณผ๋ก๊ทธ ํ ์ด๋ธ์ ๊ฐ๊ฐinbox ํ ์ด๋ธ๊ณผoutbox ํ ์ด๋ธ๋ก ์๊ฐํ์ต๋๋ค.๋ค์์ ์ ๊ฐ ์๊ฐํ๋ ๋ถ๋ฆฌํ๋ ์ด์ ์ ๋๋ค.
1. ๋ ํ ์ด๋ธ์ ์ฌ์ฉํ๋ ๋ชฉ์ ์ด ๋ค๋ฆ ๋๋ค.
inbox๋ ์ด ์ด๋ฒคํธ๋ฅผ ์ด๋ฏธ ์ฒ๋ฆฌํ๋์ง์ ๋ํ ํ๋จ์ ํฉ๋๋ค.๋ฐ๋ผ์ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ง์ ์์ค๋ณต๋ ์ด๋ฒคํธ์ธ๊ฐ๋ฅผ ์ดํด๋ณด๊ณ ์ค๋ณต ๋ฐฉ์ง ๋ชฉ์ ์ด ๋๋๋ฉด ์ญ์ ํ๋ ๋ฐ์ดํฐ ์ ๋๋ค. (ex. 10์ผ์ด ์ง๋๋ฉด ์ญ์ )
outbox๋ ์ด ์ด๋ฒคํธ๋ฅผ MQ(์ฌ๊ธฐ์ Kafka)์ ๋ฐํํ๋์ง์ ๋ํ ๊ธฐ๋ก์ ๋๋ค.๋ฐ๋ผ์ ์ด๋ฒคํธ ๋ฐํ์ ๋ํด ๋ฐํํ๋์ง, ์ธ์ , ์ด๋ค ๋ด์ฉ์ผ๋ก ๋ฐํํ๋์ง์ ๋ํ ๋ชฉ์ ์ ๊ฐ์ง๋๋ค.
2. ๋ฐ์ดํฐ ๋ณด์กด ๊ธฐ๊ฐ์ ์ฐจ์ด
inbox์ ๋ชฉ์ ์ ์ค๋ณต๋ง ๋ฐฉ์งํ๋ฉด ๋๋ฏ๋ก ์ต๊ทผ ์ด๋ฒคํธ๋ง ํ์ํฉ๋๋ค.ํ์ง๋ง,
outbox๋ ์ด๋ฒคํธ์ ๋ํด ๊ฐ์ฌ ๋ก๊ทธ ์ญํ ๋ ๊ฒธํ๋ฏ๋ก ์ฅ๊ธฐ ๋ณด๊ด์ด ํ์ํ๋ค๊ณ ์๊ฐํฉ๋๋ค.Q. ์ด๋ฒ ๊ตฌํ์์ Exactly-Once๋ฅผ ๋ณด์ฅํ๊ธฐ ์ํด ์ ์ฉํ ๋ฐฉ์์ ๋๋ค.
์ผ๋จ Exactly-Once๋ฅผ ๋ณด์ฅํ๋ ค๋ฉด ์ด๋ป๊ฒ ํ ์ ์์์ง์ ๋ํ ๋ด์ฉ์ ๋๋ค. (Kafka๋ฅผ ์ฌ์ฉํ๋ ์ํฉ)
1. Kafka Transactions (Kafka Native Exactly-Once)
2. ๋น์ฆ๋์ค ๋ ๋ฒจ์์ Idempotency ๊ตฌํ
3. Upsert ๋ฐฉ์
4. Inbox ํจํด ์ฌ์ฉ
ํ์ฌ ์ ํํ ๋ฐฉ์
์ ๋
Inbox ํจํด์ ์ ํํ์ต๋๋ค. ์ด์ ๋ก๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.Kafka ๋ฐฉ์์ ๋ฌธ์ ์
์ธ๋ถ DB์ Kafka๋ฅผ ํ๋์ ํธ๋์ญ์ ์ผ๋ก ์ด๋ป๊ฒ ๋ฌถ์ ์ ์์์ง์ ๋ํด ํด๊ฒฐ๋์ง ์์์ต๋๋ค..
๋น์ฆ๋์ค ๋ ๋ฒจ์์ Idempotencey์ ๋ฌธ์ ์
๋น์ฆ๋์ค ํค๊ฐ ์๋ ๊ฒฝ์ฐ ์ ์ฉํ ์ ์๋ค๊ณ ํ๋จํ์ต๋๋ค.
Upsert์ ๋ฌธ์ ์
์ค๋ณต ์ด๋ฒคํธ ์ ๊ฐ์ด 2๋ฐฐ๋ก ์ฆ๊ฐ(ex. ์กฐํ์)ํ ์ ์๋ค๊ณ ํ๋จํ์ต๋๋ค. (๋ฉฑ๋ฑ์ฑ์ด ๋ณด์ฅ๋์ง ์์)
๋ฐ๋ผ์
Inboxํ ์ด๋ธ๋ก ๊ตฌํํ์ต๋๋ค.์ฌ์ค ์ฒ์์๋
Inbox๊น์ง ํ์ํ ๊น ๋ผ๋ ๋ง์์ผ๋ก ๊ตฌํ์ ์์ ํ์ต๋๋ค.ํ์ง๋ง, ๋ฃจํผ์ค๋ฅผ ์งํํ๋ ๊ณผ์ ๋ด์์๋ ์ด๋ฒ ์ฃผ์๋ ์ด๋ค ๊ฒ์ ๋ฐฐ์ฐ๊ณ , ์ ์ฉํ ์ ์์๊น์ ๋ํด ๊ณ ๋ฏผํด๋ณด๋ค๊ฐ
์ต๋ํ ๊ณ ๋ฏผ์ ๋ง์ด ๋ด์ ์ ์๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ ํํ์ต๋๋ค.
Inbox ํ ์ด๋ธ์ ์ฃผ๊ธฐ์ ์ผ๋ก ์ญ์ ๋ฅผ ํด์ผ ๋๋๋ฐ ํธ๋ํฝ์ด ์์ฒญ ๋ชฐ๋ ค Inbox ํ ์ด๋ธ์ด ์ญ์ ๋ ๋ downtime์ด ๋ฐ์ํ๋ฉด ์ด๋กํ์ง? ๋ฑ ์ถ๊ฐ์ ์ธ ๊ณ ๋ฏผ์ ํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ ํํ์ต๋๋ค.. ์ ํ ๊ฑด์ง๋ ๋ชจ๋ฅด๊ฒ ์ต๋๋ค..!
โ Checklist
๐พ Producer
โพ Consumer
event_handledํ ์ด๋ธ์ ํตํ ๋ฉฑ๋ฑ ์ฒ๋ฆฌ ๊ตฌํSummary by CodeRabbit
๋ฆด๋ฆฌ์ค ๋ ธํธ
์ ๊ธฐ๋ฅ
๋ฆฌํฉํ ๋ง
โ๏ธ Tip: You can customize this high-level summary in your review settings.