-
Notifications
You must be signed in to change notification settings - Fork 34
[volume - 8] Decoupling with Kafka #196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
โฆryImpl ํด๋์ค ์ถ๊ฐ ๋ฐ ์ ์ฅ ๋ฉ์๋ ๊ตฌํ
โฆpositoryImpl ํด๋์ค ์ถ๊ฐ ๋ฐ DLQ ๋ฉ์์ง ์ฒ๋ฆฌ ๋ฉ์๋ ๊ตฌํ
โฆdledRepositoryImpl ํด๋์ค ์ถ๊ฐ ๋ฐ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ฉ์๋ ๊ตฌํ
|
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 ์์ฝ์ด PR์ Kafka ๊ธฐ๋ฐ ์ด๋ฒคํธ ์ฃผ๋ ์ํคํ ์ฒ๋ฅผ ๋์ ํ์ฌ ์ฃผ๋ฌธ, ๊ฒฐ์ , ์ํ ์ด๋ฒคํธ๋ฅผ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํฉ๋๋ค. Outbox ํจํด์ ์ ์ฉํ๊ณ ์๋ก์ด commerce-streamer ์ ํ๋ฆฌ์ผ์ด์ ์ ์ถ๊ฐํ์ฌ ๋ฉํธ๋ฆญ, ๊ฐ์ฌ ๋ก๊ทธ, DLQ ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํฉ๋๋ค. Walkthroughcommerce-api์ ์๋ก์ด commerce-streamer ์ ํ๋ฆฌ์ผ์ด์ ์ Kafka ๊ธฐ๋ฐ ์ด๋ฒคํธ ์์คํ ์ ๋์ ํฉ๋๋ค. Outbox ํจํด์ผ๋ก ์ด๋ฒคํธ ์ง์์ฑ์ ๋ณด์ฅํ๊ณ , ์ฌ๋ฌ Kafka ํ๋ก๋์๋ก ์ฃผ๋ฌธยท๊ฒฐ์ ยท์ํ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๋ฉฐ, ์คํธ๋ฆฌ๋จธ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ฉํธ๋ฆญ, ๊ฐ์ฌ ๋ก๊ทธ, DLQ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค. Changes
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
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
Estimated code review effort๐ฏ 5 (Critical) | โฑ๏ธ ~120+ ๋ถ ์ถ๊ฐ ๊ฒํ ํ์ ์ฌํญ:
Possibly related PRs
Poem
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: 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: trueapps/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์ ๋์ ธ ํธ๋์ญ์ ์ ๋กค๋ฐฑ์ํต๋๋ค. ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ์ ์ ํ์ง๋ง, ๋ค์ ์ฌํญ๋ค์ ๊ณ ๋ คํด๋ณด์ธ์:
- ์ง๋ ฌํ ์คํจ๊ฐ ํธ๋์ญ์ ๋กค๋ฐฑ์ ์ ๋ฐํด์ผ ํ๋์ง ํ์ธ
- ๋ ๊ตฌ์ฒด์ ์ธ ์ปค์คํ ์์ธ ํ์ ์ฌ์ฉ ๊ณ ๋ ค (์:
EventSerializationException)- ์ง๋ ฌํ ์คํจํ ์ด๋ฒคํธ๋ฅผ ๋ณ๋๋ก ์ถ์ ํ ํ์์ฑ ๊ฒํ
ํ์ฌ ๊ตฌํ์ ๋์ํ์ง๋ง, ์ด์ ์ค ์ง๋ ฌํ ์คํจ๊ฐ ๋ฐ์ํ๋ฉด ์๋ณธ ํธ๋์ญ์ ๋ ์คํจํ๊ฒ ๋ฉ๋๋ค.
๐ ์ปค์คํ ์์ธ๋ฅผ ์ฌ์ฉํ ๊ฐ์ ์์
๋๋ฉ์ธ ๋ ์ด์ด์ ์ปค์คํ ์์ธ ์ถ๊ฐ:
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 ํจํด์ ์ธ๊ธํ์ จ๋๋ฐ, ๋ค์ ์ค ํ๋๋ฅผ ๊ณ ๋ คํด์ฃผ์ธ์:
- Outbox ํ ์ด๋ธ ํ์ฉ: ์ด๋ฒคํธ๋ฅผ DB์ ๋จผ์ ์ ์ฅํ๊ณ ๋ณ๋ ํ๋ก์ธ์ค๋ก ๋ฐํ
- DLT (Dead Letter Topic): ํ๋ก๋์ fallback์์ ์คํจ ์ด๋ฒคํธ๋ฅผ DLT์ ์ ์ฅ
- ๋ณด์ ํธ๋์ญ์ ํธ๋ฆฌ๊ฑฐ: ๋ฐํ ์คํจ ์ ์๋ฆผ/๋ชจ๋ํฐ๋ง ์์คํ ํธ์ถ
ํ์ฌ ๊ตฌ์กฐ์์๋ ํ๋ก๋์์ fallback ๋ฉ์๋์์ DLT๋ outbox์ ์ ์ฅํ๋ ๋ก์ง์ ์ถ๊ฐํ๋ ๊ฒ์ด ๊ฐ์ฅ ํ์ค์ ์ ๋๋ค.
apps/commerce-streamer/src/test/java/com/loopers/interfaces/ProductMetricsConsumerTest.java (1)
1-141: ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง ๊ฐ์ ๊ถ์ฅํ์ฌ ์ ์ ์ผ์ด์ค๋ง ํ ์คํธ๋๊ณ ์์ต๋๋ค. PR ์ฒดํฌ๋ฆฌ์คํธ์ ์ธ๊ธ๋ "์ค๋ณต ๋ฉ์์ง ์ฌ์ ์ก ํ ์คํธ"์ ๋ค์ ์๋๋ฆฌ์ค ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํด์ฃผ์ธ์:
- ํ์ฑ ์คํจ: ์๋ชป๋ JSON ํ์
- Null ํ ํฝ: Line 42-44์ ๋ฐฉ์ด ๋ก์ง ๊ฒ์ฆ
- Facade ์์ธ:
processLikeMetrics()์คํจ ์์๋acknowledge()ํธ์ถ ํ์ธ- ๋ฉฑ๋ฑ์ฑ: ๋์ผํ
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 ๋ฉ์๋์์๋ ๋ก๊น ๋ง ์ํํฉ๋๋ค. ์ฌ๊ณ ๋ณ๊ฒฝ์ ๋น์ฆ๋์ค ํฌ๋ฆฌํฐ์ปฌํ ์ด๋ฒคํธ์ด๋ฏ๋ก, ์ต์ข ์คํจ ์ ๋ณต๊ตฌ ๋ฉ์ปค๋์ฆ์ ๊ณ ๋ คํด์ฃผ์ธ์:
- DLT (Dead Letter Topic) ์ ์ด๋ฒคํธ ์ ์ฅ
- Outbox ํ ์ด๋ธ ์ ์คํจ ์ด๋ฒคํธ ๊ธฐ๋ก ํ ๋ณ๋ ๋ฐฐ์น ์ฌ์ฒ๋ฆฌ
- ์๋ฆผ ์์คํ ์ฐ๋
๐ 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ํ ์ด๋ธ์ ์ฌ์ฉํ๋ค๊ณ ํ๋๋ฐ, ์ด ์ปจ์๋จธ์์๋ ํด๋น ๋ก์ง์ด ๋ณด์ด์ง ์์ต๋๋ค. ๋ค์์ ํ์ธํด์ฃผ์ธ์:
ProductMetricsFacade๋ด๋ถ์์ ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ์ด ๊ตฌํ๋์ด ์๋์ง- ๋ฉ์์ง ์ ์ค์ ํ์ฉํ๋ ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ์ธ์ง
#!/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()๋ฉ์๋๋ ๋ด๋ถ์ ์ผ๋ก RedisKEYS๋ช ๋ น์ ์ฌ์ฉํ๋ฉฐ, ์ด๋ O(N) ๋ณต์ก๋๋ก ๋๊ท๋ชจ ํค์คํ์ด์ค์์ Redis๋ฅผ ๋ธ๋กํนํ ์ ์์ต๋๋ค. ํ๋ก๋์ ํ๊ฒฝ์์ ์ฃผ์๊ฐ ํ์ํฉ๋๋ค.๋ํ, Line 86์์
"product:*:list:*"ํจํด์ผ๋ก ๋ชจ๋ ๋ชฉ๋ก ์บ์๋ฅผ ์ญ์ ํ๋ ๊ฒ์ ๋จ์ผ ์ํ ๋ณ๊ฒฝ์ ๋นํด ๊ณผ๋ํ ์ ์์ต๋๋ค.๊ฐ์ ๋ฐฉ์:
SCAN๋ช ๋ น ์ฌ์ฉ (RedisTemplate์scan()๋ฉ์๋)์ผ๋ก ์ ์ง์ ์กฐํ- ์ํ๋ณ ์บ์ ํค ๊ด๋ฆฌ๋ฅผ ์ํ Redis Set ํ์ฉ
- ๋๋ ์บ์ 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
๐ 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.javaapps/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.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DlqMessageJpaRepository.javaapps/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.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DlqConsumer.javaapps/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.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/OrderEventProducer.javamodules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.javaapps/commerce-api/src/main/resources/application.ymlapps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxService.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/ProductViewedEventProducer.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/DlqConsumerTest.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/PaymentEventListener.javaapps/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.javaapps/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.javaapps/commerce-api/src/main/resources/application.ymlmodules/kafka/src/main/resources/kafka.ymlapps/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.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-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๋ก ์ฌ์ฉํ์ฌ ๋์ผ ์ฃผ๋ฌธ์ ๊ฒฐ์ ์ด๋ฒคํธ ์์๋ฅผ ๋ณด์ฅํ๋ ์ค๊ณ๋ ์ ์ ํฉ๋๋ค.
...mmerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/PaymentEventProducer.java
Show resolved
Hide resolved
...mmerce-api/src/main/java/com/loopers/infrastructure/kafka/producer/PaymentEventProducer.java
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.java
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/OrderEventListener.java
Outdated
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/interfaces/api/listener/OrderEventListener.java
Outdated
Show resolved
Hide resolved
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DlqConsumer.java
Outdated
Show resolved
Hide resolved
.../commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.java
Show resolved
Hide resolved
|
โ Checklist ๐พ Producer โพ Consumer @coderabbitai ์ฒดํฌ๋ฆฌ์คํธ ํ์ธํด |
|
Tip For best results, initiate chat on the files or code changes. ๐งฉ Analysis chain๐ Script executed: Length of output: 19677 ๐ Script executed: Length of output: 2612 ๐ Script executed: Length of output: 9375 @looper88888 ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ํ์ธํ์ต๋๋ค! ๊ฐ ํญ๋ชฉ๋ณ๋ก ๊ตฌํ ์ํ๋ฅผ ์ ๋ฆฌํ์ด์. ๐พ Producer ์ฒดํฌ๋ฆฌ์คํธ (4/4 ์๋ฃ)โ ๋๋ฉ์ธ(์ ํ๋ฆฌ์ผ์ด์ ) ์ด๋ฒคํธ ์ค๊ณ
โ Producer ์ฑ์์ ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐํ
โ PartitionKey ๊ธฐ๋ฐ์ ์ด๋ฒคํธ ์์ ๋ณด์ฅ // ๋ชจ๋ Producer์์ ์ผ๊ด๋ ํค ์ฌ์ฉ
kafkaTemplate.send(topic, orderId.toString(), event); // Order/Payment
kafkaTemplate.send(topic, productId.toString(), event); // Product
kafkaTemplate.send(topic, userId.toString(), event); // Userโ ๋ฉ์ธ์ง ๋ฐํ์ด ์คํจํ์ ๊ฒฝ์ฐ์ ๋ํด ๊ณ ๋ฏผํด๋ณด๊ธฐ
โพ Consumer ์ฒดํฌ๋ฆฌ์คํธ (4/4 ์๋ฃ)โ Consumer ๊ฐ Metrics ์ง๊ณ ์ฒ๋ฆฌ
โ 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:*
}
// ์ถ์ฒํ๋ ํ
์คํธ ์๋๋ฆฌ์ค
@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 |
๐ Summary
๐ฌ Review Points
โ Checklist
๐พ Producer
โพ Consumer
event_handledํ ์ด๋ธ์ ํตํ ๋ฉฑ๋ฑ ์ฒ๋ฆฌ ๊ตฌํSummary by CodeRabbit
๋ฆด๋ฆฌ์ค ๋ ธํธ
์ ๊ธฐ๋ฅ
๋ฒ๊ทธ ์์
โ๏ธ Tip: You can customize this high-level summary in your review settings.