-
Notifications
You must be signed in to change notification settings - Fork 34
[volume - 8] Decoupling with Kafka #191
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
|
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 Walkthrough์ด PR์ transactional outbox ํจํด์ ๊ตฌํํ์ฌ ๋๋ฉ์ธ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ ํ ๋ณ๋ ํ๋ก์ธ์ค์์ Kafka๋ก ๋ฐํํ๋ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ๋ฅผ ๋์ ํฉ๋๋ค. commerce-api์์ ProductEvent, LikeEvent, OrderEvent๋ฅผ ๋ฐํํ๊ณ , OutboxEventPublisher๊ฐ ์ฃผ๊ธฐ์ ์ผ๋ก pending ์ด๋ฒคํธ๋ฅผ Kafka์ ๋ฐฐํฌํ๋ฉฐ, commerce-streamer์ ProductMetricsConsumer๊ฐ ์ด๋ฒคํธ๋ฅผ ์๋นํ์ฌ ์ํ ๋ฉํธ๋ฆญ์ ์ ๋ฐ์ดํธํฉ๋๋ค. Changes
Sequence DiagramsequenceDiagram
participant CatalogFacade
participant ProductEventPublisher
participant OutboxBridgeEventListener
participant OutboxEventService
participant OutboxEventRepository
participant Database
participant OutboxEventPublisher_Infra
participant Kafka
participant ProductMetricsConsumer
participant ProductMetricsService
CatalogFacade->>ProductEventPublisher: publish(ProductViewed)
ProductEventPublisher->>OutboxBridgeEventListener: publishEvent(ProductEvent)
OutboxBridgeEventListener->>OutboxEventService: saveEvent(eventType, aggregateId, ...)
OutboxEventService->>OutboxEventRepository: save(OutboxEvent)
OutboxEventRepository->>Database: INSERT outbox_event (status=PENDING)
rect rgb(200, 220, 240)
Note over OutboxEventPublisher_Infra: 1์ด ์ฃผ๊ธฐ ์ค์ผ์ค
OutboxEventPublisher_Infra->>OutboxEventRepository: findPendingEvents(100)
OutboxEventRepository->>Database: SELECT * WHERE status=PENDING
Database-->>OutboxEventPublisher_Infra: List<OutboxEvent>
end
loop ๊ฐ Pending ์ด๋ฒคํธ
OutboxEventPublisher_Infra->>Kafka: send(topic, key, payload, headers)
alt ๋ฐํ ์ฑ๊ณต
OutboxEventPublisher_Infra->>OutboxEventRepository: save(status=PUBLISHED, publishedAt)
else ๋ฐํ ์คํจ
OutboxEventPublisher_Infra->>OutboxEventRepository: save(status=FAILED)
end
end
ProductMetricsConsumer->>Kafka: poll(like-events, order-events, product-events)
Kafka-->>ProductMetricsConsumer: ConsumerRecord<String, Object>
loop ๋ฐฐ์น ์ฒ๋ฆฌ
rect rgb(240, 220, 200)
Note over ProductMetricsConsumer: ๋ฉฑ๋ฑ์ฑ ํ์ธ
ProductMetricsConsumer->>ProductMetricsService: eventHandledService.isAlreadyHandled(eventId)
end
alt ๋ฏธ์ฒ๋ฆฌ ์ด๋ฒคํธ
ProductMetricsConsumer->>ProductMetricsService: incrementViewCount(productId, version)
ProductMetricsService->>Database: SELECT * FOR UPDATE WHERE productId (PESSIMISTIC_WRITE)
ProductMetricsService->>Database: UPDATE product_metrics SET viewCount++, version++
ProductMetricsConsumer->>ProductMetricsService: markAsHandled(eventId)
else ์ค๋ณต ์ด๋ฒคํธ
Note over ProductMetricsConsumer: ์คํต (DataIntegrityViolationException)
end
end
ProductMetricsConsumer->>Kafka: acknowledge(batch)
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~60 minutes ์ถ๊ฐ ์ฃผ์๊ฐ ํ์ํ ์์ญ:
Possibly related PRs
Suggested labels
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 |
|
@coderabbitai review |
โ Actions performedReview triggered.
|
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: 7
๐งน Nitpick comments (16)
apps/commerce-streamer/src/main/java/com/loopers/domain/event/ProductEvent.java (1)
21-26: DTO ๊ฒ์ฆ ๋ก์ง ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํ์ธ์.commerce-api์
ProductEvent.ProductViewed๋ ์์ฑ์์์productId != null๊ฒ์ฆ์ ์ํํ์ง๋ง, ์ด DTO์๋ ๊ฒ์ฆ ๋ก์ง์ด ์์ต๋๋ค. Kafka์์ ์๋ชป๋ ๋ฐ์ดํฐ๋ฅผ ์์ ํ ๊ฒฝ์ฐ null ๊ฐ์ด ์ ํ๋์ด ์์์น ๋ชปํ ๋์์ด ๋ฐ์ํ ์ ์์ต๋๋ค.๐ ๋ค์ ๊ฒ์ฆ ๋ก์ง ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํ์ธ์:
public record ProductViewed( Long productId, Long userId, LocalDateTime occurredAt ) { + public ProductViewed { + if (productId == null) { + throw new IllegalArgumentException("productId๋ ํ์์ ๋๋ค."); + } + } }์ฐธ๊ณ : Line 10์ ์ฃผ์์์ ์ธ๊ธํ ๋๋ก, ํฅํ ๊ณต์ ๋ชจ๋๋ก ๋ถ๋ฆฌํ๋ฉด ์ด๋ฌํ ์ค๋ณต๊ณผ ๋ถ์ผ์น๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java (1)
122-124: ์บ์ ํํธ ์ ํธ๋์ญ์ ์ค๋ฒํค๋๋ฅผ ๊ณ ๋ คํ์ธ์.์บ์์์ ์ํ ์ ๋ณด๋ฅผ ์กฐํํ๋ ๊ฒฝ์ฐ์๋
@Transactional๋ฉ์๋ ๋ด์์ ์คํ๋๋ฏ๋ก DB ์ปค๋ฅ์ ์ ํ๋ํ๊ฒ ๋ฉ๋๋ค. ์บ์ ํํธ์จ์ด ๋์ ๊ฒฝ์ฐ ๋ถํ์ํ ํธ๋์ญ์ ์ค๋ฒํค๋๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.์ฑ๋ฅ์ด ์ค์ํ ๊ฒฝ์ฐ, ๋ค์๊ณผ ๊ฐ์ ์ต์ ํ๋ฅผ ๊ณ ๋ คํ ์ ์์ต๋๋ค:
- ์ด๋ฒคํธ ๋ฐํ์ ๋ณ๋์ ๋น๋๊ธฐ ๋ฉ์๋๋ก ๋ถ๋ฆฌ
- ์บ์ ํํธ ๊ฒฝ๋ก์์๋
@Transactional(propagation = Propagation.NOT_SUPPORTED)์ฌ์ฉ๋ค๋ง, ํ์ฌ ๊ตฌ์กฐ๊ฐ outbox ํจํด์ ์ผ๊ด์ฑ์ ๋ณด์ฅํ๋ฏ๋ก, ์ค์ ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ํ์ธ๋๊ธฐ ์ ๊น์ง๋ ํ์ฌ ๊ตฌํ์ ์ ์งํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventPublisherIntegrationTest.java (1)
37-42: TODO ์ฃผ์: ํตํฉ ํ ์คํธ ๊ตฌํ์ด ํ์ํฉ๋๋ค.ํตํฉ ํ ์คํธ์ ๋ผ๋๋ ์ ๊ตฌ์ฑ๋์ด ์์ง๋ง, ์ค์ ๊ฒ์ฆ ๋ก์ง์ด ๊ตฌํ๋์ง ์์์ต๋๋ค. Outbox ํจํด์ ํต์ฌ ๊ธฐ๋ฅ์ธ ์ด๋ฒคํธ ์ ์ฅ ๋ฐ Kafka ๋ฐํ์ ๊ฒ์ฆํ๋ ํ ์คํธ๋ฅผ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
ํ ์คํธ ๊ตฌํ์ ๋์๋๋ฆด๊น์? ๋ค์๊ณผ ๊ฐ์ ์๋๋ฆฌ์ค๋ฅผ ํฌํจํ ์ ์์ต๋๋ค:
- OutboxEvent ์ ์ฅ ํ PENDING ์ํ ํ์ธ
- OutboxEventPublisher ์คํ ํ Kafka ๋ฉ์์ง ์์ ํ์ธ
- ๋ฐํ ํ PUBLISHED ์ํ๋ก ๋ณ๊ฒฝ ํ์ธ
๋๋ ์ด ์์ ์ ์ถ์ ํ๊ธฐ ์ํ ์ด์๋ฅผ ์์ฑํด ๋๋ฆด๊น์?
apps/commerce-api/src/test/java/com/loopers/domain/outbox/OutboxEventTest.java (1)
51-75: Thread.sleep ์ฌ์ฉ์ ๊ฐ์ ํ ์ ์์ต๋๋ค.Line 66์
Thread.sleep(1)์ ํ์์คํฌํ ์ฐจ์ด๋ฅผ ๋ณด์ฅํ๊ธฐ ์ํด ์ฌ์ฉ๋์์ง๋ง, ํ ์คํธ๋ฅผ ๋๋ฆฌ๊ฒ ๋ง๋ค๊ณ ๋ถ์์ ํ ์ ์์ต๋๋ค.๐ ๋ค์ ๋ฐฉ๋ฒ๋ค์ ๊ณ ๋ คํด๋ณด์ธ์:
๋ฐฉ๋ฒ 1: ๋จ์ํ publishedAt์ด null์ด ์๋์ง๋ง ํ์ธ
- LocalDateTime beforePublish = outboxEvent.getCreatedAt(); - Thread.sleep(1); // ์๊ฐ ์ฐจ์ด๋ฅผ ๋ณด์ฅํ๊ธฐ ์ํ ์์ ์ง์ฐ - // act outboxEvent.markAsPublished(); // assert assertThat(outboxEvent.getStatus()).isEqualTo(OutboxEvent.OutboxStatus.PUBLISHED); assertThat(outboxEvent.getPublishedAt()).isNotNull(); - assertThat(outboxEvent.getPublishedAt()).isAfter(beforePublish); + assertThat(outboxEvent.getPublishedAt()).isAfterOrEqualTo(outboxEvent.getCreatedAt());๋ฐฉ๋ฒ 2: Clock์ ์ฃผ์ ๋ฐ๋๋ก OutboxEvent ๋ฆฌํฉํ ๋ง (๋ ๋ณต์กํ์ง๋ง ํ ์คํธํ๊ธฐ ์ข์)
apps/commerce-api/src/test/java/com/loopers/application/outbox/OutboxEventServiceTest.java (1)
132-147: ํ ์คํธ ์ ํ์ฑ ๊ฐ์ ์ ์: ์ค์ ์์ธ ํ์ ์ฌ์ฉ
ObjectMapper.writeValueAsString()์JsonProcessingException(checked exception)์ ๋ฐ์์ํต๋๋ค. ์ค์ ๋์์ ๋ ์ ํํ ํ ์คํธํ๋ ค๋ฉดRuntimeException๋์JsonProcessingException์ throwํ๋๋ก ์์ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์๋ ์์ ์ฌํญ:
@DisplayName("JSON ์ง๋ ฌํ ์คํจ ์ ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค.") @Test void throwsException_whenJsonSerializationFails() throws Exception { // arrange LikeEvent.LikeAdded event = new LikeEvent.LikeAdded(100L, 1L, LocalDateTime.now()); when(objectMapper.writeValueAsString(event)) - .thenThrow(new RuntimeException("JSON ์ง๋ ฌํ ์คํจ")); + .thenThrow(new com.fasterxml.jackson.core.JsonProcessingException("JSON ์ง๋ ฌํ ์คํจ") {}); // act & assert assertThatThrownBy(() -> outboxEventService.saveEvent("LikeAdded", "1", "Product", event, "like-events", "1") ).isInstanceOf(RuntimeException.class) .hasMessageContaining("Outbox ์ด๋ฒคํธ ์ ์ฅ ์คํจ"); verify(outboxEventRepository, never()).save(any()); }apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandled.java (1)
56-61: ์ ํ์ ๊ฐ์ : ์์ฑ์ ํ๋ผ๋ฏธํฐ null ๊ฒ์ฆํ์ ํ๋(
eventId,eventType,topic)์ ๋ํ null ๊ฒ์ฆ์ ์์ฑ์์ ์ถ๊ฐํ๋ฉด DB ์ ์ฝ์กฐ๊ฑด ์๋ฐ ์ ์ ๋น ๋ฅด๊ฒ ์คํจํ ์ ์์ต๋๋ค. ํ์ฌ ๊ตฌํ๋ DB ๋ ๋ฒจ์์ ๋ณดํธ๋๋ฏ๋ก ์ ํ์ฌํญ์ ๋๋ค.๐ ์ ์๋ ์์ ์ฌํญ:
public EventHandled(String eventId, String eventType, String topic) { + if (eventId == null || eventId.isBlank()) { + throw new IllegalArgumentException("eventId๋ ํ์์ ๋๋ค."); + } + if (eventType == null || eventType.isBlank()) { + throw new IllegalArgumentException("eventType์ ํ์์ ๋๋ค."); + } + if (topic == null || topic.isBlank()) { + throw new IllegalArgumentException("topic์ ํ์์ ๋๋ค."); + } this.eventId = eventId; this.eventType = eventType; this.topic = topic; this.handledAt = LocalDateTime.now(); }apps/commerce-api/src/main/java/com/loopers/domain/product/ProductEvent.java (1)
5-14: ๋ฌธ์ํ ๊ฐ์ ์ ์: ์ค๋ณต ๊ตฌ์กฐ์ ๋ํ ์ฃผ์์ฌํญ ์ถ๊ฐ
commerce-streamer์ProductEvent์ ๋์ผํ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค.LikeEvent์์์ฒ๋ผ ํฅํ ๊ณต์ ๋ชจ๋๋ก ๋ถ๋ฆฌ๋ฅผ ๊ณ ๋ คํ๋ค๋ ์ฃผ์์ ์ถ๊ฐํ๋ฉด ์ผ๊ด์ฑ์ด ํฅ์๋ฉ๋๋ค.๐ JavaDoc ๋ณด์ ์์:
/** * ์ํ ๋๋ฉ์ธ ์ด๋ฒคํธ. * <p> * ์ํ ๋๋ฉ์ธ์ ์ค์ํ ์ํ ๋ณํ๋ฅผ ๋ํ๋ด๋ ์ด๋ฒคํธ๋ค์ ๋๋ค. * </p> + * <p> + * <b>์ฃผ์:</b> ์ด ํด๋์ค๋ commerce-streamer์ ProductEvent์ ๋์ผํ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๋๋ค. + * ํฅํ ๊ณต์ ๋ชจ๋๋ก ๋ถ๋ฆฌํ๋ ๊ฒ์ ๊ณ ๋ คํด์ผ ํฉ๋๋ค. + * </p> * * @author Loopers * @version 1.0 */apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java (1)
76-80: ์์ธ ์ฒ๋ฆฌ ๊ฐ์ ์ ๊ณ ๋ คํด ์ฃผ์ธ์.
Exception์ ํฌ๊ด์ ์ผ๋ก catchํ๋ ๋์JsonProcessingException๊ณผ ๊ฐ์ ํน์ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ฉด ๋๋ฒ๊น ์ด ๋ ์ฉ์ดํฉ๋๋ค.๐ ๊ฐ์ ์ ์:
- } catch (Exception e) { + } catch (JsonProcessingException e) { log.error("Outbox ์ด๋ฒคํธ ์ ์ฅ ์คํจ: eventType={}, aggregateId={}", eventType, aggregateId, e); - throw new RuntimeException("Outbox ์ด๋ฒคํธ ์ ์ฅ ์คํจ", e); + throw new RuntimeException("Outbox ์ด๋ฒคํธ JSON ์ง๋ ฌํ ์คํจ", e); }apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventJpaRepository.java (1)
21-25: ๋ค์ดํฐ๋ธ ์ฟผ๋ฆฌ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด์์ฑ ๊ณ ๋ ค.
LIMIT์ ์ MySQL/PostgreSQL์์ ๋์ํ์ง๋ง, Oracle์ด๋ SQL Server์์๋ ๋ค๋ฅธ ๊ตฌ๋ฌธ์ด ํ์ํฉ๋๋ค. ๋ค์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ง์ํด์ผ ํ๋ค๋ฉด JPQL๊ณผ Pageable์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๋ํ, ์ฌ๋ฌ ์ธ์คํด์ค๊ฐ ์ค์ผ์ค๋ฌ๋ฅผ ์คํํ๋ ๊ฒฝ์ฐ ์ค๋ณต ๋ฐํ์ ๋ฐฉ์งํ๊ธฐ ์ํด ๋น๊ด์ ๋ฝ(
FOR UPDATE SKIP LOCKED)์ ๊ณ ๋ คํด ์ฃผ์ธ์.๐ ๊ฐ์ ์ ์:
- @Query(value = "SELECT * FROM outbox_event e " + - "WHERE e.status = 'PENDING' " + - "ORDER BY e.created_at ASC " + - "LIMIT :limit", nativeQuery = true) - List<OutboxEvent> findPendingEvents(@Param("limit") int limit); + @Query(value = "SELECT * FROM outbox_event e " + + "WHERE e.status = 'PENDING' " + + "ORDER BY e.created_at ASC " + + "LIMIT :limit " + + "FOR UPDATE SKIP LOCKED", nativeQuery = true) + List<OutboxEvent> findPendingEvents(@Param("limit") int limit);apps/commerce-streamer/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.java (1)
40-41: Thread.sleep ๋์ ๊ณ ๋ ค.
Thread.sleep(1)์ ํ์์คํฌํ ์ฐจ์ด๋ฅผ ๋ณด์ฅํ๊ธฐ ์ํ ๊ฒ์ด์ง๋ง, ํ ์คํธ ์์ ์ฑ์ ์ํดjava.time.Clock์ ์ฃผ์ ๋ฐ์ ํ ์คํธ์์ ์๊ฐ์ ์ ์ดํ๋ ๋ฐฉ์์ ๊ณ ๋ คํด ๋ณผ ์ ์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEventRepository.java (1)
37-38: findById ๋ฐํ ํ์ ์ Optional ์ฌ์ฉ์ ๊ณ ๋ คํด ์ฃผ์ธ์.ID๋ก ์กฐํ ์ ๊ฒฐ๊ณผ๊ฐ ์์ ์ ์์ผ๋ฏ๋ก
Optional<OutboxEvent>๋ฅผ ๋ฐํํ๋ฉด ํธ์ถ์๊ฐ null ์ฒดํฌ๋ฅผ ๊ฐ์ ํ๊ฒ ๋์ด NPE๋ฅผ ๋ฐฉ์งํ ์ ์์ต๋๋ค.๐ ๊ฐ์ ์ ์:
- OutboxEvent findById(Long id); + Optional<OutboxEvent> findById(Long id);import ์ถ๊ฐ:
import java.util.Optional;apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventPublisherTest.java (1)
52-52: ๋ฐฐ์น ํฌ๊ธฐ ์์ ์ถ์ถ์ ๊ณ ๋ คํด ์ฃผ์ธ์.
100์ด ์ฌ๋ฌ ํ ์คํธ์์ ๋ฐ๋ณต ์ฌ์ฉ๋๊ณ ์์ต๋๋ค. ์์๋ก ์ถ์ถํ๋ฉด ์ค์ Publisher์ ๋ฐฐ์น ํฌ๊ธฐ๊ฐ ๋ณ๊ฒฝ๋ ๋ ํ ์คํธ ์ ์ง๋ณด์๊ฐ ์ฌ์์ง๋๋ค.๐ ๊ฐ์ ์ ์:
private static final int BATCH_SIZE = 100; // ์ฌ์ฉ ์์ when(outboxEventRepository.findPendingEvents(BATCH_SIZE)).thenReturn(pendingEvents);apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxBridgeEventListener.java (1)
53-140: ํ ํฝ ์ด๋ฆ์ ์์๋ก ์ถ์ถํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์
"like-events","order-events","product-events"๋ฑ ํ ํฝ ์ด๋ฆ์ด ์ฌ๋ฌ ๊ณณ์์ ์ฌ์ฉ๋ฉ๋๋ค. ์คํ ๋ฐฉ์ง์ ์ ์ง๋ณด์๋ฅผ ์ํด ์์๋ ์ค์ ์ผ๋ก ๊ด๋ฆฌํ๋ฉด ์ข์ต๋๋ค.๐ ํ ํฝ ์์ ์ถ์ถ ์์:
public final class KafkaTopics { public static final String LIKE_EVENTS = "like-events"; public static final String ORDER_EVENTS = "order-events"; public static final String PRODUCT_EVENTS = "product-events"; private KafkaTopics() {} }modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.java (1)
35-51: ํ ์คํธ ์ ํธ๋ฆฌํฐ๊ฐ ํ๋ก๋์ ์์ ์ค์๋ก ํ์ฑํ๋ ์ํ์ด ์์ต๋๋ค.
@Component์ด๋ ธํ ์ด์ ์ ์ปดํฌ๋ํธ ์ค์บ ์ ์ด๋ค ์ปจํ ์คํธ์์๋ ์ด ๋น์ ๋ฑ๋กํ ์ ์์ต๋๋ค. testFixtures ์์ค ์ธํธ๋ ์ผ๋ฐ์ ์ผ๋ก ํ๋ก๋์ ์ ํฌํจ๋์ง ์์ง๋ง, ๋ช ์์ ์ผ๋ก ํ ์คํธ ์ ์ฉ์์ ํ์ํ๋ ๊ฒ์ด ๋ ์์ ํฉ๋๋ค.๐ ํ ์คํธ ํ๋กํ์ผ ์ ํ์ ์ถ๊ฐํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์:
+import org.springframework.context.annotation.Profile; + +@Profile("test") @Component public class KafkaCleanUp {๋๋ ์ด ํด๋์ค๋ฅผ
@Configurationํด๋์ค์์ ๋ช ์์ ์ผ๋ก@Bean์ผ๋ก ๋ฑ๋กํ์ฌ ์ ์ด๋ฅผ ๊ฐํํ ์ ์์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.java (1)
232-240: ํ์ฑ ๋ฉ์๋์ ์ผ๊ด์ฑ์ ๊ฐ์ ํ์ธ์.
parseLikeEvent๋instanceof์ฒดํฌ ์์ด ๋ฐ๋ก JSON ํ์ฑ์ ์๋ํ๋ ๋ฐ๋ฉด,parseOrderCreatedEvent์parseProductViewedEvent๋instanceof์ฒดํฌ๋ฅผ ๋จผ์ ์ํํฉ๋๋ค. ์ผ๊ด์ฑ์ ์ํดparseLikeEvent๋ ๋์ผํ ํจํด์ ๋ฐ๋ฅด๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ผ๊ด๋ ํ์ฑ ํจํด ์ ์ฉ:
private LikeEvent.LikeAdded parseLikeEvent(Object value) { try { + if (value instanceof LikeEvent.LikeAdded) { + return (LikeEvent.LikeAdded) value; + } + // JSON ๋ฌธ์์ด์ธ ๊ฒฝ์ฐ ํ์ฑ String json = value instanceof String ? (String) value : objectMapper.writeValueAsString(value); return objectMapper.readValue(json, LikeEvent.LikeAdded.class); } catch (Exception e) { throw new RuntimeException("LikeEvent ํ์ฑ ์คํจ", e); } }modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java (1)
78-166: ํ ํฝ ์ค์ ์ด ๊ฐ๋ฐ/ํ ์คํธ ํ๊ฒฝ์ ์ ํฉํ๊ฒ ๊ตฌ์ฑ๋์์ต๋๋ค.๊ฐ ํ ํฝ์ ํํฐ์ ํค ์ ๋ต์ด ๋ช ํํ ๋ฌธ์ํ๋์ด ์์ผ๋ฉฐ, 3๊ฐ ํํฐ์ ์ ์ ์ ํ ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ฅผ ์ ๊ณตํฉ๋๋ค. ๋ค๋ง
replicas=1์ค์ ์ ๊ฐ๋ฐ/ํ ์คํธ ํ๊ฒฝ์๋ง ์ ํฉํ๋ฏ๋ก, ํ๋ก๋์ ํ๊ฒฝ์์๋ ์ต์ 3๊ฐ ์ด์์ ๋ ํ๋ฆฌ์นด๋ฅผ ์ฌ์ฉํ๋๋ก ์ค์ ์ ์ธ๋ถํํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ํ๋ก๋์ ํ๊ฒฝ์ ์ํ ์ค์ ์ธ๋ถํ ์ ์:
application.yml์ ํ ํฝ ์ค์ ์ ์ถ๊ฐํ์ฌ ํ๊ฒฝ๋ณ๋ก ๋ค๋ฅธ ๋ ํ๋ฆฌ์นด ์๋ฅผ ์ฌ์ฉํ๋๋ก ํ ์ ์์ต๋๋ค:
kafka: topic: partitions: 3 replicas: 3 # production min-insync-replicas: 2๊ทธ๋ฆฌ๊ณ
@Value๋๋@ConfigurationProperties๋ฅผ ํตํด ์ด ๊ฐ๋ค์ ์ฃผ์ ๋ฐ์ ์ฌ์ฉํฉ๋๋ค.
๐ Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (41)
apps/commerce-api/build.gradle.kts(2 hunks)apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java(4 hunks)apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxBridgeEventListener.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.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/product/ProductEvent.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/product/ProductEventPublisher.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/infrastructure/product/ProductEventPublisherImpl.java(1 hunks)apps/commerce-api/src/main/resources/application.yml(1 hunks)apps/commerce-api/src/test/java/com/loopers/application/outbox/OutboxBridgeEventListenerTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/application/outbox/OutboxEventServiceTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/outbox/OutboxEventTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventPublisherIntegrationTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventPublisherTest.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/application/eventhandled/EventHandledService.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/event/LikeEvent.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/domain/event/OrderEvent.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/domain/event/ProductEvent.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/EventHandledRepository.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/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/ProductMetricsConsumer.java(1 hunks)apps/commerce-streamer/src/test/java/com/loopers/application/eventhandled/EventHandledServiceTest.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/domain/metrics/ProductMetricsTest.java(1 hunks)apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ProductMetricsConsumerIntegrationTest.java(1 hunks)apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ProductMetricsConsumerTest.java(1 hunks)modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java(4 hunks)modules/kafka/src/main/resources/kafka.yml(2 hunks)modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java(1 hunks)modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.java(1 hunks)
๐งฐ Additional context used
๐ง Learnings (8)
๐ 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: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/application/outbox/OutboxEventService.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ProductMetricsConsumerTest.javaapps/commerce-streamer/src/main/java/com/loopers/application/eventhandled/EventHandledService.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.javaapps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventPublisherIntegrationTest.java
๐ Learning: 2025-12-18T13:24:51.650Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:51.650Z
Learning: Adopt centralized error handling for Kafka consumers by using the EventInboxAspect to intercept methods annotated with InboxEvent. Ensure that service methods annotated with InboxEvent save failed EventInbox entries and log errors, avoiding duplicating error handling logic in individual consumers. Apply this pattern broadly to similar consumer/service layers within the commerce-streamer module.
Applied to files:
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.java
๐ Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEventRepository.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-streamer/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.javaapps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.javaapps/commerce-streamer/src/main/java/com/loopers/domain/event/ProductEvent.javaapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.javaapps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductEvent.javaapps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java
๐ 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/test/java/com/loopers/domain/metrics/ProductMetricsTest.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductEvent.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-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/catalog/CatalogFacade.java
๐งฌ Code graph analysis (10)
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ProductMetricsConsumerTest.java (2)
apps/commerce-streamer/src/main/java/com/loopers/domain/event/LikeEvent.java (1)
LikeEvent(16-37)apps/commerce-streamer/src/main/java/com/loopers/domain/event/OrderEvent.java (1)
OrderEvent(17-40)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.java (2)
apps/commerce-streamer/src/main/java/com/loopers/domain/event/LikeEvent.java (1)
LikeEvent(16-37)apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java (1)
Slf4j(28-154)
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.java (3)
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxBridgeEventListener.java (1)
Slf4j(39-141)apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java (1)
Slf4j(23-82)apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventRepositoryImpl.java (1)
Component(13-39)
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ProductMetricsConsumerIntegrationTest.java (1)
apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventPublisherIntegrationTest.java (1)
SpringBootTest(23-43)
apps/commerce-streamer/src/main/java/com/loopers/domain/event/ProductEvent.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductEvent.java (1)
ProductEvent(14-59)
apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventPublisherIntegrationTest.java (1)
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ProductMetricsConsumerIntegrationTest.java (1)
SpringBootTest(35-116)
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java (1)
apps/commerce-streamer/src/main/java/com/loopers/application/eventhandled/EventHandledService.java (1)
Slf4j(21-64)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductEvent.java (1)
apps/commerce-streamer/src/main/java/com/loopers/domain/event/ProductEvent.java (1)
ProductEvent(16-27)
apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductEvent.java (1)
ProductEvent(14-59)apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEventPublisherImpl.java (1)
Component(26-36)
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxBridgeEventListener.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductEvent.java (1)
ProductEvent(14-59)apps/commerce-streamer/src/main/java/com/loopers/domain/event/ProductEvent.java (1)
ProductEvent(16-27)
๐ Additional comments (58)
apps/commerce-api/src/main/resources/application.yml (1)
23-23: LGTM!Kafka ์ค์ ํ์ผ์ configuration import ๋ชฉ๋ก์ ์ถ๊ฐํ ๊ฒ์ ์ ์ ํฉ๋๋ค. ์ด๋ PR์ Kafka ํตํฉ ๋ชฉํ์ ์ผ์นํฉ๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java (1)
40-57: LGTM!Upsert ์ ๋์์ฑ ์ ์ด๋ฅผ ์ํ ๋น๊ด์ ๋ฝ ์ ๋ต์ด ์ ๋ฌธ์ํ๋์ด ์๊ณ , ๋ฉํธ๋ฆญ ์ง๊ณ ์ Lost Update๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํ ์ ์ ํ ์ ๊ทผ ๋ฐฉ์์ ๋๋ค. Lock ๋ฒ์๋ฅผ PK ๊ธฐ๋ฐ ์กฐํ๋ก ์ต์ํํ ๊ฒ๋ ์ข์ต๋๋ค.
Based on learnings, ์ด ํ๋ก์ ํธ๋ ์ ์ฆ์ผ์ด์ค๋ณ๋ก ์๋์ ์ผ๋ก ๋ค๋ฅธ ๋ฝ ์ ๋ต์ ์ฌ์ฉํ๊ณ ์์ผ๋ฉฐ, ๋ฉํธ๋ฆญ ์ง๊ณ์ ๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ๋ ๊ฒ์ ํฉ๋ฆฌ์ ์ธ ์ ํ์ ๋๋ค.
apps/commerce-api/build.gradle.kts (1)
5-5: LGTM!Kafka ๋ชจ๋๊ณผ ํ ์คํธ ํฝ์ค์ฒ ์์กด์ฑ์ ์ถ๊ฐํ์ฌ commerce-api์์ Kafka ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์๋๋ก ํ ๊ฒ์ ์ ์ ํฉ๋๋ค. ์ด๋ PR์ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ ๋์ ๋ชฉํ์ ์ผ์นํฉ๋๋ค.
Also applies to: 38-38
apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java (1)
117-125: LGTM!
@Transactional(readOnly = true)์ด๋ ธํ ์ด์ ์ถ๊ฐ๋ outbox ํจํด์ ์ํ ์ ์ ํ ์ ํ์ ๋๋ค. ์ด๋ ์ด๋ฒคํธ ๋ฐํ ์ outbox ์ํธ๋ฆฌ๊ฐ ํธ๋์ญ์ ์ปจํ ์คํธ ๋ด์์ ์ ์ฅ๋๋๋ก ๋ณด์ฅํฉ๋๋ค. ์บ์ ํํธ ์์๋ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ์ฌ ์กฐํ ์ ์ง๊ณ๋ฅผ ์ผ๊ด๋๊ฒ ์ฒ๋ฆฌํ๋ ๊ฒ๋ ์ข์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/product/ProductEventPublisher.java (1)
1-21: LGTM! ๋๋ฉ์ธ ๋ ์ด์ด์ ์ด๋ฒคํธ ๋ฐํ ์ธํฐํ์ด์ค๊ฐ ์ ์ค๊ณ๋์์ต๋๋ค.DIP ์์น์ ์ค์ํ์ฌ ๋๋ฉ์ธ ๋ ์ด์ด์์ ์ธํฐํ์ด์ค๋ฅผ ์ ์ํ๊ณ ๊ตฌํ์ ์ธํ๋ผ ๋ ์ด์ด์ ์์ํ๋ ๊ตฌ์กฐ๊ฐ ์ ์ ํฉ๋๋ค. ๋ฌธ์ํ๋ ๋ช ํํ๊ฒ ์์ฑ๋์์ต๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandledRepository.java (1)
1-40: LGTM! ์ด๋ฒคํธ ์ฒ๋ฆฌ ๊ธฐ๋ก์ ์ํ ์ ์ฅ์ ์ธํฐํ์ด์ค๊ฐ ์ ์ ์๋์์ต๋๋ค.์ด๋ฒคํธ ์ค๋ณต ์ฒ๋ฆฌ ๋ฐฉ์ง๋ฅผ ์ํ ๋ฉ์๋๋ค์ด ์ ์ ํ๊ฒ ์ ์๋์ด ์์ผ๋ฉฐ, DIP ์์น์ ์ค์ํ๊ณ ์์ต๋๋ค. ๋ฌธ์ํ๋ ๋ช ํํฉ๋๋ค.
apps/commerce-api/src/test/java/com/loopers/domain/outbox/OutboxEventTest.java (3)
15-49: LGTM! OutboxEvent ์์ฑ ๋ฐ ์ด๊ธฐ ์ํ ๊ฒ์ฆ์ด ์ ๊ตฌํ๋์์ต๋๋ค.ํ์ ํ๋์ ์ด๊ธฐ ์ํ(PENDING)๋ฅผ ํ์ธํ๋ ํ ์คํธ๊ฐ ๋ช ํํ๊ณ ํฌ๊ด์ ์ ๋๋ค.
77-97: LGTM! ์คํจ ์ํ ์ ํ ํ ์คํธ๊ฐ ์ ์ ํฉ๋๋ค.์ด๋ฒคํธ ์คํจ ์ฒ๋ฆฌ ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ฒ์ฆ๋๊ณ ์์ต๋๋ค.
99-123: LGTM! ์ํ ์ ํ ์๋๋ฆฌ์ค ํ ์คํธ๊ฐ ์ ๊ตฌํ๋์์ต๋๋ค.๋ฐํ ์๋ฃ ํ ์คํจ ์ํ๋ก ๋ณ๊ฒฝํ๋ ์ฃ์ง ์ผ์ด์ค๋ฅผ ๋ค๋ฃจ๊ณ ์์ผ๋ฉฐ, publishedAt์ด ์ ์ง๋๋์ง ํ์ธํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ProductMetricsConsumerTest.java (8)
29-48: LGTM! ํ ์คํธ ์ค์ ์ด ์ ๊ตฌ์ฑ๋์์ต๋๋ค.Mockito๋ฅผ ์ฌ์ฉํ ์์กด์ฑ ๋ชจํน์ด ์ ์ ํ๊ฒ ์ค์ ๋์ด ์์ต๋๋ค.
50-108: LGTM! ๊ฐ๋ณ ์ด๋ฒคํธ ์ฒ๋ฆฌ ํ ์คํธ๊ฐ ํฌ๊ด์ ์ ๋๋ค.LikeAdded์ LikeRemoved ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก์ง์ ๋ช ํํ๊ฒ ๊ฒ์ฆํ๊ณ ์์ผ๋ฉฐ, eventId ํค๋์ version ํค๋๋ฅผ ํฌํจํ ์์ ํ ์๋๋ฆฌ์ค๋ฅผ ๋ค๋ฃจ๊ณ ์์ต๋๋ค.
110-149: LGTM! OrderCreated ์ด๋ฒคํธ์ ๋ณต์กํ ์๋๋ฆฌ์ค๊ฐ ์ ํ ์คํธ๋์์ต๋๋ค.์ฌ๋ฌ ์ฃผ๋ฌธ ์์ดํ ์ ํฌํจํ OrderCreated ์ด๋ฒคํธ ์ฒ๋ฆฌ์ ๊ฐ ์ํ๋ณ ํ๋งค ์๋ ์ฆ๊ฐ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ฒ์ฆ๋๊ณ ์์ต๋๋ค.
151-189: LGTM! ๋ฐฐ์น ์ฒ๋ฆฌ ํ ์คํธ๊ฐ ์ ์ ํฉ๋๋ค.์ฌ๋ฌ ์ด๋ฒคํธ๋ฅผ ๋ฐฐ์น๋ก ์ฒ๋ฆฌํ๋ ์๋๋ฆฌ์ค๋ฅผ ๊ฒ์ฆํ๊ณ ์์ผ๋ฉฐ, acknowledgment๊ฐ ํ ๋ฒ๋ง ํธ์ถ๋๋์ง ํ์ธํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
191-263: LGTM! ์ค๋ฅ ๋ณต์๋ ฅ ํ ์คํธ๊ฐ ํ๋ฅญํฉ๋๋ค.๊ฐ๋ณ ์ด๋ฒคํธ ์ฒ๋ฆฌ ์คํจ ์์๋ ๋ฐฐ์น ์ฒ๋ฆฌ๋ฅผ ๊ณ์ํ๊ณ acknowledgment๋ฅผ ์ํํ๋ ๋ก์ง์ด ์ ๊ฒ์ฆ๋์ด ์์ต๋๋ค. ์ด๋ Kafka ์๋น์์ ์ค์ํ ํน์ฑ์ ๋๋ค.
Based on learnings, ์ด ์ฝ๋๋ฒ ์ด์ค์์ Kafka ์๋น์๋ ์๋ฌ ์ฒ๋ฆฌ์ ์ด๋ฒคํธ ์ถ์ ์ EventInboxAspect๋ฅผ ํตํด ์๋น์ค ๋ ์ด์ด์ ์์ํ๋ ๊ฒ์ผ๋ก ์๊ณ ์์ต๋๋ค. ์ด ํ ์คํธ๋ ์๋น์ ๋ ๋ฒจ์ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ ๊ฒ์ฆํ๊ณ ์์ต๋๋ค.
265-292: LGTM! ๋ฉฑ๋ฑ์ฑ ํ ์คํธ๊ฐ ์ ์ ํฉ๋๋ค.์ด๋ฏธ ์ฒ๋ฆฌ๋ ์ด๋ฒคํธ๋ฅผ ๊ฑด๋๋ฐ๋ ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ฒ์ฆ๋๊ณ ์์ผ๋ฉฐ, ์ค๋ณต ์ฒ๋ฆฌ ๋ฐฉ์ง ๋ฉ์ปค๋์ฆ์ด ์ ์๋ํจ์ ๋ณด์ฌ์ค๋๋ค.
294-314: LGTM! eventId ๋๋ฝ ์ฒ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.eventId๊ฐ ์๋ ๋ฉ์์ง๋ฅผ ๊ฑด๋๋ฐ๋ ๋ฐฉ์ด์ ํ๋ก๊ทธ๋๋ฐ์ด ์ ๊ตฌํ๋์ด ์์ต๋๋ค.
316-346: LGTM! ๋์์ฑ ์์ธ ์ฒ๋ฆฌ๊ฐ ํ๋ฅญํฉ๋๋ค.DataIntegrityViolationException์ด ๋ฐ์ํ๋ ๊ฒฝ์ ์กฐ๊ฑด(race condition) ์๋๋ฆฌ์ค๋ฅผ ๋ค๋ฃจ๊ณ ์์ต๋๋ค. ์ด๋ ์ฌ๋ฌ ์๋น์ ์ธ์คํด์ค๊ฐ ๋์ผํ ์ด๋ฒคํธ๋ฅผ ๊ฑฐ์ ๋์์ ์ฒ๋ฆฌํ๋ ค ํ ๋ ๋ฐ์ํ ์ ์๋ ์ํฉ์ ์ ๋ฐ์ํ๊ณ ์์ต๋๋ค.
apps/commerce-api/src/test/java/com/loopers/application/outbox/OutboxBridgeEventListenerTest.java (6)
19-29: LGTM! ํ ์คํธ ์ค์ ์ด ์ ์ ํฉ๋๋ค.Mockito๋ฅผ ์ฌ์ฉํ ์์กด์ฑ ๋ชจํน์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
31-73: LGTM! Like ์ด๋ฒคํธ ์ฒ๋ฆฌ ํ ์คํธ๊ฐ ํฌ๊ด์ ์ ๋๋ค.LikeAdded์ LikeRemoved ์ด๋ฒคํธ๋ฅผ Outbox์ ์ ์ฅํ๋ ๋ก์ง์ด ๋ช ํํ๊ฒ ๊ฒ์ฆ๋๊ณ ์์ผ๋ฉฐ, ์ฌ๋ฐ๋ฅธ ํ๋ผ๋ฏธํฐ๋ก OutboxEventService๊ฐ ํธ์ถ๋๋์ง ํ์ธํ๊ณ ์์ต๋๋ค.
75-100: LGTM! OrderCreated ์ด๋ฒคํธ ์ฒ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.์ฃผ๋ฌธ ์์ฑ ์ด๋ฒคํธ๋ฅผ Outbox์ ์ ์ฅํ๋ ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ฒ์ฆ๋๊ณ ์์ต๋๋ค.
102-124: LGTM! ProductViewed ์ด๋ฒคํธ ์ฒ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.์ํ ์กฐํ ์ด๋ฒคํธ๋ฅผ Outbox์ ์ ์ฅํ๋ ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ฒ์ฆ๋๊ณ ์์ต๋๋ค.
126-141: LGTM! ์๋ฌ ๊ฒฉ๋ฆฌ ํ ์คํธ๊ฐ ํ๋ฅญํฉ๋๋ค.Outbox ์ ์ฅ ์คํจ ์์๋ ์์ธ๋ฅผ ์ ํํ์ง ์๋ ๊ฒ์ transactional outbox ํจํด์ ์ค์ํ ํน์ฑ์ ๋๋ค. ์ฃผ ํธ๋์ญ์ ์ด ์ฑ๊ณตํ๋๋ผ๋ outbox ์ ์ฅ ์คํจ๋ก ์ธํด ์ ์ฒด ์์ ์ด ๋กค๋ฐฑ๋์ง ์๋๋ก ํ๋ ๋ฐฉ์ด์ ์ค๊ณ๊ฐ ์ ๊ฒ์ฆ๋์์ต๋๋ค.
143-162: LGTM! ์์ฐจ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ ํ ์คํธ๊ฐ ์ ์ ํฉ๋๋ค.์ฌ๋ฌ ์ด๋ฒคํธ๋ฅผ ์์ฐจ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ์๋๋ฆฌ์ค๊ฐ ์ ๊ฒ์ฆ๋๊ณ ์์ต๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledJpaRepository.java (1)
1-31: LGTM! JPA ์ ์ฅ์ ์ธํฐํ์ด์ค๊ฐ ์ ์ ์๋์์ต๋๋ค.Spring Data JPA์ derived query ๋ฉ์๋๋ฅผ ์ ์ ํ๊ฒ ํ์ฉํ๊ณ ์์ผ๋ฉฐ, ์ด๋ฒคํธ ์ค๋ณต ์ฒ๋ฆฌ ๋ฐฉ์ง๋ฅผ ์ํ ์กฐํ ๋ฉ์๋๋ค์ด ๋ช ํํ๊ฒ ์ ์๋์ด ์์ต๋๋ค.
apps/commerce-streamer/src/test/java/com/loopers/application/eventhandled/EventHandledServiceTest.java (4)
18-28: LGTM! ํ ์คํธ ์ค์ ์ด ์ ์ ํฉ๋๋ค.Mockito๋ฅผ ์ฌ์ฉํ ์์กด์ฑ ๋ชจํน์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
30-58: LGTM! ์ด๋ฒคํธ ์ฒ๋ฆฌ ํ์ธ ๋ก์ง์ด ์ ํ ์คํธ๋์์ต๋๋ค.์ฒ๋ฆฌ๋์ง ์์ ์ด๋ฒคํธ์ ์ด๋ฏธ ์ฒ๋ฆฌ๋ ์ด๋ฒคํธ์ ๋ํ ๋ ๊ฐ์ง ๊ฒฝ๋ก๋ฅผ ๋ชจ๋ ๊ฒ์ฆํ๊ณ ์์ผ๋ฉฐ, ์ ์ฅ์์์ ์ํธ์์ฉ์ด ์ฌ๋ฐ๋ฅด๊ฒ ํ์ธ๋๊ณ ์์ต๋๋ค.
60-76: LGTM! ์ฑ๊ณต์ ์ธ ์ด๋ฒคํธ ์ ์ฅ ํ ์คํธ๊ฐ ์ ์ ํฉ๋๋ค.์๋ก์ด ์ด๋ฒคํธ๋ฅผ ์ ์์ ์ผ๋ก ์ ์ฅํ๋ ๊ฒฝ๋ก๊ฐ ์ ๊ฒ์ฆ๋๊ณ ์์ต๋๋ค.
78-95: LGTM! ์ค๋ณต ์ด๋ฒคํธ ์ฒ๋ฆฌ ์์ธ ํ ์คํธ๊ฐ ํ๋ฅญํฉ๋๋ค.DataIntegrityViolationException์ ํตํด eventId์ UNIQUE ์ ์ฝ ์กฐ๊ฑด ์๋ฐ์ ๊ฒ์ฆํ๊ณ ์์ต๋๋ค. ์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ค์์ ๋ฉฑ๋ฑ์ฑ์ ๋ณด์ฅํ๋ ์ค์ํ ๋ฉ์ปค๋์ฆ์ ๋๋ค.
apps/commerce-api/src/test/java/com/loopers/application/outbox/OutboxEventServiceTest.java (1)
1-73: LGTM! ํ ์คํธ๊ฐ ์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.Outbox ์ด๋ฒคํธ ์ ์ฅ์ ํต์ฌ ์๋๋ฆฌ์ค๊ฐ ์ ๊ฒ์ฆ๋์์ต๋๋ค. ArgumentCaptor๋ฅผ ํ์ฉํ์ฌ ์ ์ฅ๋ ์ํฐํฐ์ ์์ฑ๋ค์ ์ธ๋ฐํ๊ฒ ๊ฒ์ฆํ๋ ๋ฐฉ์์ด ์ข์ต๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandled.java (1)
28-61: ์ ์ค๊ณ๋ ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ ์ํฐํฐ์ ๋๋ค.
eventId๋ฅผ PK๋ก ์ฌ์ฉํ์ฌ DB ๋ ๋ฒจ์์ ์ค๋ณต ์ฒ๋ฆฌ๋ฅผ ๋ฐฉ์งํ๋ ์ค๊ณ๊ฐ ์ ์ ํฉ๋๋ค.handled_at์ธ๋ฑ์ค๋ ์ด๋ฒคํธ ๋ณด์กด ์ ์ฑ ์ด๋ ์ ๋ฆฌ ์์ ์ ์ ์ฉํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/product/ProductEvent.java (1)
27-58: ์ ๊ตฌํ๋ ๋๋ฉ์ธ ์ด๋ฒคํธ์ ๋๋ค.
productId์ ๋ํ null ๊ฒ์ฆ๊ณผ ๋ ๊ฐ์ง ํฉํ ๋ฆฌ ๋ฉ์๋ ์ ๊ณต์ด ์ข์ต๋๋ค.userId๊ฐ nullable์ธ ๊ฒ๋ ๋น๋ก๊ทธ์ธ ์ฌ์ฉ์ ์ผ์ด์ค๋ฅผ ๊ณ ๋ คํ ์ ์ ํ ์ค๊ณ์ ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/domain/event/LikeEvent.java (1)
1-37: LGTM! DTO ๊ตฌ์กฐ๊ฐ ์ ์ ํฉ๋๋ค.Kafka ์ด๋ฒคํธ ํ์ฑ์ฉ DTO๋ก์ ์ ์ ํ ๊ตฌ์กฐ์ ๋๋ค. JavaDoc์์
commerce-api์LikeEvent์์ ์ค๋ณต ๋ฐ ํฅํ ๊ณต์ ๋ชจ๋ ๋ถ๋ฆฌ ๊ณ ๋ ค ์ฌํญ์ ๋ช ํํ ๋ฌธ์ํํ ์ ์ด ์ข์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
20-49: LGTM! ํ์ค์ ์ธ Repository ๊ตฌํ์ ๋๋ค.JPA ๋ฆฌํฌ์งํ ๋ฆฌ์ ์์ํ๋ ๊น๋ํ ๊ตฌํ์ ๋๋ค.
findByProductIdForUpdate๋ฉ์๋๋ฅผ ํตํด ๋น๊ด์ ๋ฝ์ ์ง์ํ๋ ๊ฒ์ด ๋ฉํธ๋ฆญ ์ ๋ฐ์ดํธ์ ๋์์ฑ ์ ์ด์ ์ ํฉํฉ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/domain/event/OrderEvent.java (1)
17-40: LGTM! ๊ตฌ์กฐ๊ฐ ๊น๋ํฉ๋๋ค.Kafka ์ด๋ฒคํธ ์ญ์ง๋ ฌํ๋ฅผ ์ํ record ๊ธฐ๋ฐ DTO ๊ตฌ์กฐ๊ฐ ์ ์ ํฉ๋๋ค. ์ฃผ์์์ ์ธ๊ธ๋ ๋๋ก commerce-api์ OrderEvent์ ์ค๋ณต๋๋ฏ๋ก ํฅํ ๊ณต์ ๋ชจ๋๋ก ๋ถ๋ฆฌํ๋ ๊ฒ์ด ์ข๊ฒ ์ต๋๋ค.
ํ ๊ฐ์ง ํ์ธ ์ฌํญ:
subtotal์Integer์ด๊ณusedPointAmount๋Long์ธ๋ฐ, ๊ธ์ก ๊ด๋ จ ํ๋์ ํ์ ์ด ์๋์ ์ผ๋ก ๋ค๋ฅธ ๊ฒ์ธ์ง ํ์ธํด ์ฃผ์ธ์.apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEventPublisherImpl.java (1)
26-36: LGTM! DIP ์์น์ ์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.๋จ์ผ ์ฑ ์ ์์น์ ๋ฐ๋ผ ApplicationEvent๋ง ๋ฐํํ๊ณ , Kafka ์ ์ก์ OutboxBridgeEventListener๊ฐ ์ฒ๋ฆฌํ๋ ๊ด์ฌ์ฌ ๋ถ๋ฆฌ๊ฐ ์ ์ ํฉ๋๋ค.
apps/commerce-streamer/src/test/java/com/loopers/domain/metrics/ProductMetricsTest.java (1)
10-155: LGTM! ๋๋ฉ์ธ ์ํฐํฐ์ ๋์์ ์ ๊ฒ์ฆํ๊ณ ์์ต๋๋ค.ProductMetrics์ ์ฃผ์ ๋์(์ข์์, ํ๋งค๋, ์กฐํ์ ์ฆ๊ฐ, ๋ฉฑ๋ฑ์ฑ, ๋ฒ์ ๊ด๋ฆฌ)์ ๋ํ ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๊ฐ ์ถฉ๋ถํฉ๋๋ค.
apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java (1)
21-217: LGTM! ์๋น์ค ๋ ์ด์ด์ ๋์์ ์ ๊ฒ์ฆํ๊ณ ์์ต๋๋ค.๋ฒ์ ๊ธฐ๋ฐ ์ ๋ฐ์ดํธ ๋ก์ง(์ค๋๋ ์ด๋ฒคํธ ์คํต, ์ต์ ์ด๋ฒคํธ ์ ์ฉ), ๋ฉํธ๋ฆญ ์์ฑ/์ ๋ฐ์ดํธ, ์ ํจํ์ง ์์ ์๋ ๋ฌด์ ๋ฑ ์ฃผ์ ์๋๋ฆฌ์ค๊ฐ ์ ํ ์คํธ๋์ด ์์ต๋๋ค.
apps/commerce-api/src/test/java/com/loopers/infrastructure/outbox/OutboxEventPublisherTest.java (1)
29-299: LGTM! OutboxEventPublisher์ ์ฃผ์ ๋์์ ์ ๊ฒ์ฆํ๊ณ ์์ต๋๋ค.๋ฐฐ์น ์ฒ๋ฆฌ, ๊ฐ๋ณ ์คํจ ์ ๊ณ์ ์งํ, ์ํ ์ ํ(PUBLISHED/FAILED), JSON ์ญ์ง๋ ฌํ ์คํจ ์ฒ๋ฆฌ, ํํฐ์ ํค ๊ฒ์ฆ ๋ฑ ์ค์ํ ์๋๋ฆฌ์ค๊ฐ ๋ชจ๋ ํ ์คํธ๋์ด ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.java (1)
87-116: publishEvent ๋ฉ์๋ ๊ตฌํ ๊ฒํKafka ํค๋์
eventId์version์ ํฌํจ์์ผ ๋ฉฑ๋ฑ์ฑ ์ฒ๋ฆฌ๋ฅผ ์ง์ํ๋ ์ค๊ณ๊ฐ ์ ์ ํฉ๋๋ค. ๋ค๋ง ์์์ ์ธ๊ธํ ๋น๋๊ธฐ ์ ์ก ์ด์๋ฅผ ํด๊ฒฐํ๋ฉด ์ ์ฒด์ ์ผ๋ก ์์ ์ ์ธ ๊ตฌํ์ด ๋ฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventRepositoryImpl.java (1)
13-39: JPA ๋ ํฌ์งํ ๋ฆฌ ์์ ๊ตฌํ LGTM๋๋ฉ์ธ ๋ ํฌ์งํ ๋ฆฌ ์ธํฐํ์ด์ค๋ฅผ JPA ๋ ํฌ์งํ ๋ฆฌ์ ๊น๋ํ๊ฒ ์์ํ๊ณ ์์ต๋๋ค.
findById์์ ์กฐํ ์คํจ ์ ์์ธ๋ฅผ ๋์ง๋ ๊ฒ์ ๋๋ฉ์ธ ๊ณ์ธต์์ Optional ๋์ ํ์ ๋ ๊ฐ์ ๊ธฐ๋ํ๋ ๊ฒฝ์ฐ์ ์ ํฉํ ์ค๊ณ์ ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java (1)
44-58: ๋ฒ์ ๊ธฐ๋ฐ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก์ง LGTM
shouldUpdate(eventVersion)์ฒดํฌ๋ฅผ ํตํด ์ด๋ฏธ ์ฒ๋ฆฌ๋ ์ค๋๋ ์ด๋ฒคํธ๋ฅผ ์คํตํ๋ ๋ฉฑ๋ฑ์ฑ ์ฒ๋ฆฌ๊ฐ ์ ๊ตฌํ๋์ด ์์ต๋๋ค. ๋น๊ด์ ๋ฝ๊ณผ ํจ๊ป ์ฌ์ฉํ์ฌ ๋์์ฑ ์ ์ด๋ ์ ์ ํฉ๋๋ค. ํ์ต๋ ๋ด์ฉ์ ๋ฐ๋ฅด๋ฉด, ์ด ์ฝ๋๋ฒ ์ด์ค์์๋ ์ ์ฆ์ผ์ด์ค๋ณ๋ก ์๋์ ์ผ๋ก ๋ค๋ฅธ ๋ฝ ์ ๋ต์ ์ฌ์ฉํฉ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java (1)
37-39: ๋น๊ด์ ๋ฝ ์ฟผ๋ฆฌ ๊ตฌํ LGTM
PESSIMISTIC_WRITE๋ฝ ๋ชจ๋์ ๋ช ์์ JPQL ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํfindByProductIdForUpdate๊ตฌํ์ด ์ ์ ํฉ๋๋ค. ํ์ต๋ ํจํด์ ๋ฐ๋ฅด๋ฉด ์ด ์ฝ๋๋ฒ ์ด์ค์์๋ ๋์์ฑ ์ ์ด๊ฐ ํ์ํ ๊ฒฝ์ฐ ๋น๊ด์ ๋ฝ์ ์ฌ์ฉํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxBridgeEventListener.java (1)
51-68: @async์ @TransactionalEventListener ํจํด์ ์์ธ ์ฒ๋ฆฌ ๊ณ ๋ ค
@Async์@TransactionalEventListener(AFTER_COMMIT)์กฐํฉ์ ์๋ณธ ํธ๋์ญ์ ์ปค๋ฐ ํ ๋น๋๊ธฐ๋ก Outbox ์ ์ฅ์ ์ํํ์ฌ ์๋ฌ ๊ฒฉ๋ฆฌ๋ฅผ ์ ๊ตฌํํ์ต๋๋ค. ์ ํ๋ฆฌ์ผ์ด์ ์ ์ด๋ฏธ@EnableAsync๊ฐ ๊ตฌ์ฑ๋์ด ์๊ณ Spring Boot๊ฐ ์๋์ผ๋ก TaskExecutor๋ฅผ ์ ๊ณตํ๋ฏ๋ก ์ถ๊ฐ ์ค์ ์ด ํ์ํ์ง ์์ต๋๋ค. ๋ค๋ง void ๋ฐํ ํ์ ์ ๋ฉ์๋์์๋ ๋ฐ์ํ ์์ธ๊ฐ ํธ์ถ์์๊ฒ ์ ํ๋์ง ์์ผ๋ฏ๋ก, ๋น๋๊ธฐ ์์ ์ค ์์ธ๊ฐ ๋ฐ์ํด๋ ๋ก๊น ๋ง ๋๊ณ ์๋ณธ ํธ๋์ญ์ ์๋ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/application/eventhandled/EventHandledService.java (1)
24-63: LGTM! ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ ํจํด์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.๋ฐ์ดํฐ๋ฒ ์ด์ค UNIQUE ์ ์ฝ์กฐ๊ฑด์ ํ์ฉํ ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ ๊ตฌํ์ด ์ ์ ํฉ๋๋ค.
isAlreadyHandled()์markAsHandled()์กฐํฉ ์ฌ์ฉ ์ ๋ฐ์ํ ์ ์๋ TOCTOU(Time-of-check to time-of-use) ์ด์๋ UNIQUE ์ ์ฝ์กฐ๊ฑด์ด ์ต์ข ์ ์ผ๋ก ๋ฐฉ์งํ๋ฏ๋ก,DataIntegrityViolationException์ ์ฌ๋ฐ์์์ผ ํธ์ถ์๊ฐ ๋์์ฑ ์ค๋ณต์ ์ฒ๋ฆฌํ๋๋ก ํ๋ ๋ฐฉ์์ด ์ฌ๋ฐ๋ฆ ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/infrastructure/eventhandled/EventHandledRepositoryImpl.java (1)
21-38: LGTM! ํ์ค ๋ฆฌํฌ์งํ ๋ฆฌ ์ด๋ํฐ ํจํด์ด ์ ์ ํ๊ฒ ๊ตฌํ๋์์ต๋๋ค.JPA ๋ฆฌํฌ์งํ ๋ฆฌ๋ก์ ๋จ์ ์์ ๊ตฌ์กฐ๊ฐ ๋ช ํํ๊ณ ๊น๋ํฉ๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (3)
58-65: LGTM! ์์ฑ์ ์ด๊ธฐํ๊ฐ ์ ์ ํฉ๋๋ค.๋ชจ๋ ์นด์ดํฐ์ ๋ฒ์ ์ 0์ผ๋ก ์ด๊ธฐํํ๊ณ ์์ฑ ์์ ์ ๊ธฐ๋กํ๋ ๋ก์ง์ด ๋ช ํํฉ๋๋ค.
79-85: ์ข์์ ๊ฐ์ ๋ก์ง์ด ์์ ํ๊ฒ ๊ตฌํ๋์์ต๋๋ค.์์ ๋ฐฉ์ง ๊ฐ๋๊ฐ ์ ์ ํ๋ฉฐ, ์กฐ๊ฑด ๋ถ์ถฉ์กฑ ์ ์กฐ์ฉํ ๋ฌด์ํ๋ ๋ฐฉ์์ ๋ฐฉ์ด์ ์ฝ๋ฉ์ผ๋ก ํ์ฉ๋ฉ๋๋ค.
119-126: ๋ฒ์ ๊ธฐ๋ฐ ์ด๋ฒคํธ ์์ ๋ณด์ฅ ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.
shouldUpdate๋ฉ์๋๋ null ์ด๋ฒคํธ ๋ฒ์ ์ ํ์ฉ(ํ์ ํธํ์ฑ)ํ๋ฉด์ ์ด๋ฒคํธ ๋ฒ์ ๋น๊ต๋ฅผ ํตํด ์ค๋๋ ์ด๋ฒคํธ๊ฐ ์ต์ ๋ฉํธ๋ฆญ์ ๋ฎ์ด์ฐ๋ ๊ฒ์ ๋ฐฉ์งํฉ๋๋ค. ์ด ๋ก์ง์ProductMetricsService์์ ๋น๊ด์ ๋ฝ(findByProductIdForUpdate)๊ณผ ํจ๊ป ์ฌ์ฉ๋์ด ๋์์ฑ ์ ์ด๊ฐ ๋ณด์ฅ๋ฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEvent.java (3)
22-68: LGTM! Outbox ์ํฐํฐ ๊ตฌ์กฐ๊ฐ ํธ๋์ญ์ ๋ ์์๋ฐ์ค ํจํด์ ์ ํฉํฉ๋๋ค.
eventId์ UNIQUE ์ ์ฝ์กฐ๊ฑด์ ์ค๋ณต ์ด๋ฒคํธ๋ฅผ ๋ฐฉ์งํ๊ณ ,(status, created_at)์ธ๋ฑ์ค๋ PENDING ์ด๋ฒคํธ์ ํจ์จ์ ์ธ ํด๋ง์ ์ง์ํฉ๋๋ค.
69-90: LGTM! ๋น๋ ํจํด ์์ฑ์๊ฐ ์ ์ ํ๊ฒ ๊ตฌํ๋์์ต๋๋ค.์ด๊ธฐ ์ํ๋ฅผ
PENDING์ผ๋ก ์ค์ ํ๊ณ ์์ฑ ์์ ์ ๊ธฐ๋กํ๋ ๋ก์ง์ด ๋ช ํํฉ๋๋ค.
92-105: ์ํ ์ ํ ๋ฉ์๋๊ฐ ๋จ์ํ๊ณ ๋ช ํํฉ๋๋ค.ํ์ฌ ์ํ๋ฅผ ๊ฒ์ฆํ์ง ์๊ณ ์ ํ์ ํ์ฉํ๋ ๋ฐฉ์์ ์์๋ฐ์ค ํจํด์์ ์ฌ์๋ ์๋๋ฆฌ์ค๋ฅผ ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌํ ์ ์๊ฒ ํฉ๋๋ค.
markAsPublished()๋ฅผ ์ฌ๋ฌ ๋ฒ ํธ์ถํด๋ ๋ฉฑ๋ฑ์ ์ด๋ฏ๋ก ๋ฌธ์ ์์ต๋๋ค.modules/kafka/src/testFixtures/java/com/loopers/utils/KafkaCleanUp.java (2)
95-123: LGTM! ํ ์คํธ ํ ํฝ ์ฌ์์ฑ ๋ก์ง์ด ์ ์ ํฉ๋๋ค.3๊ฐ ํํฐ์ , 1๊ฐ ๋ ํ๋ฆฌ์นด,
min.insync.replicas=1์ค์ ์ ํ ์คํธ ํ๊ฒฝ์ ์ ํฉํ๋ฉฐ, ํ ํฝ๋ณ ์๋ฌ ํธ๋ค๋ง์ ํตํด ๋ถ๋ถ ์คํจ๋ฅผ ํ์ฉํฉ๋๋ค.
187-193: LGTM! AdminClient ์์ฑ ๋ก์ง์ด ์ ์ ํฉ๋๋ค.
KafkaAdmin์ ์ค์ ์์ ๋ถํธ์คํธ๋ฉ ์๋ฒ๋ฅผ ๊ฐ์ ธ์ค๋ฉฐ,localhost:19092ํด๋ฐฑ์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ํฉ๋ฆฌ์ ์ ๋๋ค. ํ ์คํธ ํ๊ฒฝ์์๋KafkaTestContainersConfig๊ฐ ์ฌ๋ฐ๋ฅธ ๋์ ํฌํธ๋ฅผ ์ค์ ํ๋ฏ๋ก ํด๋ฐฑ์ด ์ฌ์ฉ๋ ๊ฐ๋ฅ์ฑ์ ๋ฎ์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.java (4)
164-224: LGTM! ์ฃผ๋ฌธ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.
OrderCreated์ด๋ฒคํธ์ ์ฌ๋ฌ ์ฃผ๋ฌธ ํญ๋ชฉ์ ์ํํ๋ฉฐ ๊ฐ ์ํ์ ํ๋งค๋์ ์ฆ๊ฐ์ํค๋ ๋ก์ง์ด ์ ํํ๋ฉฐ, ์๋ฌ ํธ๋ค๋ง ํจํด๋ ์ผ๊ด์ฑ ์๊ฒ ์ ์ฉ๋์์ต๋๋ค.
256-312: LGTM! ์ํ ์กฐํ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก์ง์ด ์ผ๊ด์ฑ ์๊ฒ ๊ตฌํ๋์์ต๋๋ค.๋ค๋ฅธ ๋ฆฌ์ค๋ ๋ฉ์๋๋ค๊ณผ ๋์ผํ ํจํด์ ๋ฐ๋ฅด๋ฉฐ,
ProductViewed์ด๋ฒคํธ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋จ๋ช ๋ฃํฉ๋๋ค.
360-387: LGTM! ํค๋ ์ถ์ถ ๋ก์ง์ด ์์ ํ๊ฒ ๊ตฌํ๋์์ต๋๋ค.
eventId์version์ถ์ถ ์ null ์ฒ๋ฆฌ์ ํ์ฑ ์๋ฌ ํธ๋ค๋ง์ด ์ ์ ํ๋ฉฐ, UTF-8 ์ธ์ฝ๋ฉ ์ฌ์ฉ๋ ํ์ค์ ๋ฐ๋ฆ ๋๋ค.
129-147: ์๋ฌ ํธ๋ค๋ง ํจํด์ด ์ ์ ํฉ๋๋ค.๊ฐ๋ณ ๋ ์ฝ๋ ์๋ฌ๋ฅผ ๋ก๊น ํ ๊ณ์ ์งํํ๊ณ , ๋ฐฐ์น ๋ ๋ฒจ ์๋ฌ๋ ์ฌ๋ฐ์์์ผ ์ปค๋ฐ์ ๋ฐฉ์งํ๋ ๋ฐฉ์์ด ํฉ๋ฆฌ์ ์ ๋๋ค.
DataIntegrityViolationException์ ๋์์ฑ ์ค๋ณต์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ฒ๋ ์ฌ๋ฐ๋ฆ ๋๋ค. ์ด ๊ตฌํ์ ์ ์ ํ At-Least-Once ์ ๋ฌ ๋ณด์ฅ์ ์ ๊ณตํฉ๋๋ค.modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java (1)
35-76: LGTM! ์ ๋ค๋ฆญ ํ์ ๋ณ๊ฒฝ์ผ๋ก ํ์ ์์ ์ฑ์ด ํฅ์๋์์ต๋๋ค.ํค ํ์ ์
Object์์String์ผ๋ก ๋ณ๊ฒฝํ์ฌ Kafka ๋ฉ์์ง์ ํํฐ์ ํค๊ฐ ํญ์ ๋ฌธ์์ด์์ ๋ช ์์ ์ผ๋ก ํํํ๋ฉฐ, ์ค์๋ก ๋ค๋ฅธ ํ์ ์ ํค๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๋ฐฉ์งํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java
Outdated
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.java
Show resolved
Hide resolved
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java
Show resolved
Hide resolved
.../commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.java
Show resolved
Hide resolved
...mer/src/test/java/com/loopers/interfaces/consumer/ProductMetricsConsumerIntegrationTest.java
Outdated
Show resolved
Hide resolved
modules/kafka/src/testFixtures/java/com/loopers/testcontainers/KafkaTestContainersConfig.java
Show resolved
Hide resolved
239a920 to
71bc388
Compare
c123b34 to
bf7b8e3
Compare
* chore:kafka producer ์ค์ * chore: kafka ํ ํฝ ์๋ ์์ฑ ์ค์ ์ถ๊ฐ * feat: kafka event publisher, comsumer ์ถ๊ฐ * test: ์ง๊ณ ๋๋ฉ์ธ ๋จ์ ํ ์คํธ ์ฝ๋ ์ถ๊ฐ * feat: ์ง๊ณ ๋๋ฉ์ธ domain ๋ ์ด์ด ๊ตฌํ * feat: ์ง๊ณ ๋๋ฉ์ธ infra ๋ ์ด์ด ๊ตฌํ * chore: kafka ํ ํฝ ์๋ ์์ฑ ์ค์ ์ถ๊ฐ * chore: kafka ๋น๋ ์์กด์ฑ ์ถ๊ฐ * test: ์ง๊ณ ํตํฉ ํ ์คํธ ์ถ๊ฐ * feat: ์ง๊ณ ์๋น์ค ๋ก์ง ๊ตฌํ * test: kafka consumer ํ ์คํธ ์ฝ๋ ์ถ๊ฐ * feat: kafka comsumer ๊ตฌํ * outbox ํจํด ์ ์ฉ์ํด ๊ธฐ์กด kafka ์ค์ ์ญ์ * test: outboxevent ๋จ์ ํ ์คํธ ์ถ๊ฐ * feat: outbox ๋๋ฉ์ธ ๊ตฌํ * feat: outbox infrastructure repository๊ตฌํ * metric ์คํ ์์ * refactor: consumer ๊ด๋ จ ๋ก์ง๋ค์ commerce-streamer ๋ชจ๋๋ก ์ด๋ * test: outbox ํ ์คํธ ์ฝ๋ ์ถ๊ฐ * test: outbox ๊ตฌํ * outbox event listener ๊ตฌํ * feat: ์ํ ์กฐํ ์ด๋ฒคํธ ์ถ๊ฐ * feat: ์ํ ์กฐํ์ ์ด๋ฒคํธ ๋ฐํ * chore: kafka ์ค์ ์์ * fix: outbox ์ฒ๋ฆฌ๋์ง ์๋ ์ค๋ฅ ์์ * chore: ํ ์คํธ ์ฝ๋ ์คํ์ kafka ์ฌ์ฉํ ์ ์๋๋ก test container ์ค์ ์ถ๊ฐ * test: offset.reset: latest ์ค์ ์ด ์ ๋๋ก ์ ์ฉ๋๋์ง ํ์ธํ๋ ํ ์คํธ ์ฝ๋ ์ถ๊ฐ * test: kafka ํํฐ์ ํค ์ค์ ์ ๋ํ ํ ์คํธ ์ฝ๋ ์ถ๊ฐ * chore: commerce-api ํ ์คํธ ํ๊ฒฝ์์ ์นดํ์นด ์ฌ์ฉํ๋๋ก ์คใน์ * test: event id ๊ธฐ์ค์ผ๋ก ํ ๋ฒ๋ง publish, consumeํ๋ ๊ฒ์ ๊ฒ์ฆํ๋ ํ ์คํธ ์ฝ๋ ์ถ๊ฐ * chore: ์ถฉ๋ ๋ฐ์ํ ํ ์คํธ ์ฝ๋ ์์ * feat: event id ๊ธฐ์ค 1ํ ์ฒ๋ฆฌ๋๋๋ก ๋ก์ง ๊ตฌํ * test: ๋ฒ์ ๊ธฐ์ค์ผ๋ก ์ต์ ์ด๋ฒคํธ๋ง ์ฒ๋ฆฌํ๋๋ก ํ ์คํธ ์ฝ๋ ์์ * feat: version ๊ธฐ์ค์ผ๋ก ์ต์ ์ด๋ฒคํธ๋ง ์ฒ๋ฆฌํ๋๋ก ํจ * test: ์ค๋ณต ๋ฉ์์ง ์ฌ์ ์ก ์ ํ ๋ฒ๋ง ์ฒ๋ฆฌ๋๋์ง ๊ฒ์ฆํ๋ ํ ์คํธ ์ฝ๋ ์ถ๊ฐ * feat: kafka ์ด๋ฒคํธ publish ํ ๋ ์ฝ๋ฐฑ ์ฌ์ฉํ์ฌ ์ด๋ฒคํธ ์ ์ค ๋ฐฉ์ง * feat: kafka๋ฉ์์ง ํค๋์ event type ์ถ๊ฐ * feat: ๋ฒ์ ์กฐํ์ ์ ์ฅ ์ฌ์ด์ ๊ฒฝ์ ์กฐ๊ฑด ๊ฐ๋ฅ์ฑ ํด๊ฒฐ * feat: ์ ๊ท ์ํ ๋ฑ๋ก์ event ๋ฐํ์์ ๋ฐ์ํ๋ ๊ฒฝํฉ ๋ฌธ์ ์์
bf7b8e3 to
e4403b1
Compare
|
โ Checklist ๐พ Producer โพ Consumer @coderabbitai ์ฒดํฌ๋ฆฌ์คํธ ํ์ธํด |
|
Tip For best results, initiate chat on the files or code changes. ๐งฉ Analysis chain๐ Script executed: Length of output: 18813 @looper88888, ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ๊ฒํ ํ์ต๋๋ค: ๐พ Producer ๊ตฌํ ์ํโ ๋๋ฉ์ธ(์ ํ๋ฆฌ์ผ์ด์ ) ์ด๋ฒคํธ ์ค๊ณ
โ Producer ์ฑ์์ ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐํ
โ PartitionKey ๊ธฐ๋ฐ์ ์ด๋ฒคํธ ์์ ๋ณด์ฅ
โ ๋ฉ์์ง ๋ฐํ ์คํจ ์ ์ฒ๋ฆฌ
โพ Consumer ๊ตฌํ ์ํโ Consumer๊ฐ Metrics ์ง๊ณ ์ฒ๋ฆฌ
โ event_handled ํ ์ด๋ธ์ ํตํ ๋ฉฑ๋ฑ ์ฒ๋ฆฌ ๊ตฌํ
โ ์ฌ๊ณ ์์ง ์ ์ํ ์บ์ ๊ฐฑ์
โ ์ค๋ณต ๋ฉ์์ง ์ฌ์ ์ก ํ ์คํธ
๐ ์ข ํฉ ์์ฝ์๋ฃ: 7๊ฐ ํญ๋ชฉ ์ฌ๊ณ ์์ง ์บ์ ๊ฐฑ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ ค๋ฉด:
๐ง Learnings used |
๐ Summary
API ์๋น์ค(
commerce-api)์ ๋ ๋ฆฝ๋ ์ง๊ณ ์๋น์ค(commerce-streamer)๋ฅผ ์ถ๊ฐํ๊ณ , ๋ ์๋น์ค ๊ฐ ๋์จํ ์ฐ๊ฒฐ์ Kafka ๊ธฐ๋ฐ ์ด๋ฒคํธ ํ์ดํ๋ผ์ธ์ผ๋ก ๊ตฌํํ์ต๋๋ค.์ฃผ์ ๊ตฌํ ๋ด์ฉ:
event_handledํ ์ด๋ธ(UUID ๊ธฐ๋ฐ eventId)๊ณผversionํ๋(aggregateId๋ณ ์์ฐจ์ ๋ฒ์ )๋ฅผ ํตํ ์ค๋ณต ์ฒ๋ฆฌ ๋ฐฉ์งoffset.reset: latest์ค์ ์ผ๋ก ๋ถํ์ํ ๊ณผ๊ฑฐ ๋ฉ์์ง ์ฒ๋ฆฌ ๋ฐฉ์ง๊ตฌํ๋ ์ด๋ฒคํธ:
like-events:LikeAdded,LikeRemovedโ ์ข์์ ์ ์ง๊ณorder-events:OrderCreatedโ ํ๋งค๋ ์ง๊ณproduct-events:ProductViewedโ ์กฐํ ์ ์ง๊ณ๐ฌ Review Points
1. Transactional Outbox Pattern ๊ตฌํ ๋ฐฉ์์ ์ ์ ์ฑ
๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ ์ํฉ:
์ธ๋ถ ์์คํ (Kafka)๊ณผ์ ํต์ ์ด ํ์ํ ์ํฉ์์, ๋๋ฉ์ธ ํธ๋์ญ์ ๊ณผ Kafka ๋ฐํ์ ๋์ผ ํธ๋์ญ์ ์ผ๋ก ๋ฌถ์ ์ ์๊ธฐ ๋๋ฌธ์ ์ด๋ฒคํธ ์ ์ค ๊ฐ๋ฅ์ฑ์ด ์์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ์ฃผ๋ฌธ ์์ฑ ํธ๋์ญ์ ์ด ์ฑ๊ณตํ์ง๋ง Kafka ๋ฐํ์ด ์คํจํ๋ฉด, ์ง๊ณ ์๋น์ค๋ ํด๋น ์ฃผ๋ฌธ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ง ๋ชปํ๊ฒ ๋ฉ๋๋ค. ๋ฐ๋๋ก Kafka ๋ฐํ์ ์ฑ๊ณตํ์ง๋ง ๋๋ฉ์ธ ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ฉด, ์ค์ ๋ก๋ ์ฃผ๋ฌธ์ด ์์ฑ๋์ง ์์๋๋ฐ ์ง๊ณ ์๋น์ค๋ ์ฃผ๋ฌธ ์ด๋ฒคํธ๋ฅผ ๋ฐ๊ฒ ๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค.
ํด๊ฒฐ ๋ฐฉ์:
์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด Transactional Outbox Pattern์ ์ ์ฉํ์ต๋๋ค. ๋๋ฉ์ธ ํธ๋์ญ์ ๊ณผ ๊ฐ์ ํธ๋์ญ์ ์์
OutboxEvent๋ฅผ DB์ ๋จผ์ ์ ์ฅํ๊ณ , ๋ณ๋ ์ค์ผ์ค๋ฌ๊ฐ ์ฃผ๊ธฐ์ ์ผ๋ก PENDING ์ํ์ ์ด๋ฒคํธ๋ฅผ ์ฝ์ด Kafka๋ก ๋ฐํํ๋ ๊ตฌ์กฐ์ ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ๋๋ฉ์ธ ํธ๋์ญ์ ์ด ์ฑ๊ณตํ๋ฉด Outbox์ ์ด๋ฒคํธ๊ฐ ์ ์ฅ๋๊ณ , ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ฉด Outbox ์ ์ฅ๋ ํจ๊ป ๋กค๋ฐฑ๋์ด ์ผ๊ด์ฑ์ด ๋ณด์ฅ๋ฉ๋๋ค.๊ตฌํ ์ธ๋ถ์ฌํญ:
ApplicationEvent โ OutboxEvent ๋ณํ:
OutboxBridgeEventListener๊ฐ@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)๋ก ์ค์ ๋์ด, ๋๋ฉ์ธ ํธ๋์ญ์ ์ด ์ปค๋ฐ๋ ํ์๋ง Outbox์ ์ ์ฅํฉ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ๋๋ฉ์ธ ๋ก์ง์ด ์คํจํ์ฌ ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ฉด Outbox ์ ์ฅ๋ ๋กค๋ฐฑ๋์ด ๋ถํ์ํ ์ด๋ฒคํธ๊ฐ ์ ์ฅ๋์ง ์์ต๋๋ค.์ค์ผ์ค๋ฌ ๊ธฐ๋ฐ ๋ฐํ:
OutboxEventPublisher๊ฐ 1์ด๋ง๋ค ์คํ๋์ด PENDING ์ํ์ ์ด๋ฒคํธ๋ฅผ ์ต๋ 100๊ฐ์ฉ ์ฝ์ด Kafka๋ก ๋ฐํํฉ๋๋ค. ๋ฐํ ์ฑ๊ณต ์PUBLISHED์ํ๋ก ๋ณ๊ฒฝํ๊ณ , ์คํจ ์FAILED์ํ๋ก ๋ณ๊ฒฝํ์ฌ ๋ค์ ์ค์ผ์ค์์ ์ฌ์๋ํ ์ ์๋๋ก ํ์ต๋๋ค.๊ด๋ จ ์ฝ๋:
๊ณ ๋ฏผํ ์ :
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)๋ฅผ ์ฌ์ฉํ ์ด์ ๋, ๋๋ฉ์ธ ํธ๋์ญ์ ์ด ์ฑ๊ณต์ ์ผ๋ก ์ปค๋ฐ๋ ํ์๋ง Outbox์ ์ ์ฅํ์ฌ ์ผ๊ด์ฑ์ ๋ณด์ฅํ๊ธฐ ์ํจ์ ๋๋ค. ๋ง์ฝAFTER_COMMIT์ด ์๋ ๋ค๋ฅธ ์์ ์ ์ ์ฅํ๋ฉด, ๋๋ฉ์ธ ๋ก์ง์ด ์คํจํ์ฌ ๋กค๋ฐฑ๋์๋๋ฐ๋ Outbox์ ์ด๋ฒคํธ๊ฐ ์ ์ฅ๋ ์ ์์ต๋๋ค. ํ์ง๋งAFTER_COMMIT์ ํธ๋์ญ์ ์ด ์์ ํ ์ปค๋ฐ๋ ํ์ ์คํ๋๋ฏ๋ก, Outbox ์ ์ฅ ์คํจ ์ ๋๋ฉ์ธ ํธ๋์ญ์ ์ ๋กค๋ฐฑํ ์ ์๋ค๋ ๋จ์ ์ด ์์ต๋๋ค. ์ด ๋ถ๋ถ์ ๋ํ ๊ฒํ ๊ฐ ํ์ํฉ๋๋ค.์ค์ผ์ค๋ฌ ์ฃผ๊ธฐ(1์ด)์ ๋ฐฐ์น ํฌ๊ธฐ(100)๋ ํ์ฌ ํธ๋ํฝ์ ๊ณ ๋ คํ์ฌ ์ค์ ํ์ง๋ง, ์ค์ ์ด์ ํ๊ฒฝ์์๋ ์ด๋ฒคํธ ๋ฐ์ ๋น๋์ Kafka ์ฒ๋ฆฌ ์๋๋ฅผ ๊ณ ๋ คํ์ฌ ์กฐ์ ์ด ํ์ํ ์ ์์ต๋๋ค. ๋๋ฌด ์งง์ ์ฃผ๊ธฐ(์: 100ms)๋ DB ๋ถํ๋ฅผ ์ฆ๊ฐ์ํฌ ์ ์๊ณ , ๋๋ฌด ๊ธด ์ฃผ๊ธฐ(์: 10์ด)๋ ์ด๋ฒคํธ ๋ฐํ ์ง์ฐ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
๊ฐ๋ณ ์ด๋ฒคํธ ๋ฐํ ์คํจ ์
FAILED์ํ๋ก ๋ณ๊ฒฝํ๊ณ ๊ณ์ ์งํํ๋๋ก ํ๋๋ฐ, ์ด๋ ๊ฒ ํ๋ฉด ์ผ๋ถ ์ด๋ฒคํธ๋ง ์คํจํด๋ ๋ค์ ์ค์ผ์ค์์ ์ฌ์๋ํ ์ ์์ต๋๋ค. ํ์ง๋งFAILED์ํ์ ์ด๋ฒคํธ๋ฅผ ๋ณ๋๋ก ๋ชจ๋ํฐ๋งํ๊ฑฐ๋, ์ฌ์๋ ํ์ ์ ํ์ ๋๋ ๋ฑ์ ์ถ๊ฐ ๋ก์ง์ด ํ์ํ ์ ์์ต๋๋ค.2. ๋ฉฑ๋ฑ์ฑ ์ฒ๋ฆฌ ์ ๋ต: event_handled ํ ์ด๋ธ๊ณผ version ํ๋์ ์กฐํฉ
๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ ์ํฉ:
Kafka๋ ๊ธฐ๋ณธ์ ์ผ๋ก At Least Once ๋ณด์ฅ์ ์ ๊ณตํ๋ฏ๋ก, ๋คํธ์ํฌ ์ค๋ฅ๋ Consumer ์ฌ์์ ๋ฑ์ ์ํฉ์์ ๋์ผํ ๋ฉ์์ง๊ฐ ์ฌ๋ฌ ๋ฒ ์ ๋ฌ๋ ์ ์์ต๋๋ค. ๋ํ Producer ์ธก์์๋
acks=all,enable.idempotence=true์ค์ ์ผ๋ก At Least Once๋ฅผ ๋ณด์ฅํ๋ฏ๋ก, ๋์ผํ ์ด๋ฒคํธ๊ฐ ์ค๋ณต ๋ฐํ๋ ์ ์์ต๋๋ค. ์ด๋ฌํ ์ค๋ณต ๋ฉ์์ง๋ฅผ ๊ทธ๋๋ก ์ฒ๋ฆฌํ๋ฉด ์ข์์ ์๋ ํ๋งค๋์ด ์ค๋ณต ์ง๊ณ๋์ด ์๋ชป๋ ๋ฉํธ๋ฆญ์ด ์์ฑ๋ฉ๋๋ค.ํด๊ฒฐ ๋ฐฉ์:
์ค๋ณต ์ฒ๋ฆฌ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ๋ ๊ฐ์ง ์ ๋ต์ ์กฐํฉํ์ต๋๋ค:
eventId์ ์ค๋ณต ์ฒ๋ฆฌ ๋ฐฉ์ง (๋์ผ ์ด๋ฒคํธ์ ์์ ์ค๋ณต ๋ฐฉ์ง)๊ตฌํ ์ธ๋ถ์ฌํญ:
1) event_handled ํ ์ด๋ธ์ ํตํ ์ค๋ณต ์ฒ๋ฆฌ ๋ฐฉ์ง:
eventId๋ฅผ ๋ถ์ฌํฉ๋๋ค.event_handledํ ์ด๋ธ์์ ํด๋นeventId๊ฐ ์ด๋ฏธ ์ฒ๋ฆฌ๋์๋์ง ํ์ธํฉ๋๋ค.event_handledํ ์ด๋ธ์ ๊ธฐ๋ก์ ์ ์ฅํฉ๋๋ค.event_handledํ ์ด๋ธ์event_id์ปฌ๋ผ์ UNIQUE ์ ์ฝ์กฐ๊ฑด์ ์ค์ ํ์ฌ, ๋์์ฑ ์ํฉ์์๋ ์ค๋ณต ์ฒ๋ฆฌ๋ฅผ ๋ฐฉ์งํฉ๋๋ค. ๋ง์ฝ ๋ ๊ฐ์ Consumer ์ธ์คํด์ค๊ฐ ๋์์ ๊ฐ์ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ ค๊ณ ํ๋ฉด, ํ๋๋ ์ฑ๊ณตํ๊ณ ๋ค๋ฅธ ํ๋๋ UNIQUE ์ ์ฝ์กฐ๊ฑด ์๋ฐ ์์ธ๊ฐ ๋ฐ์ํ์ฌ ์ค๋ณต ์ฒ๋ฆฌ๊ฐ ๋ฐฉ์ง๋ฉ๋๋ค.2) version ํ๋๋ฅผ ํตํ ์ค๋๋ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ฐฉ์ง:
OutboxEvent์aggregateId๋ณ๋ก ์์ฐจ์ ์ผ๋ก ์ฆ๊ฐํ๋versionํ๋๋ฅผ ๋ถ์ฌํฉ๋๋ค. ์๋ฅผ ๋ค์ด,productId=1์ ๋ํ ์ฒซ ๋ฒ์งธ ์ด๋ฒคํธ๋version=1, ๋ ๋ฒ์งธ ์ด๋ฒคํธ๋version=2๊ฐ ๋ฉ๋๋ค.version์ Kafka ๋ฉ์์ง ํค๋์ ํฌํจ๋์ด Consumer๋ก ์ ๋ฌ๋ฉ๋๋ค.ProductMetrics์ ํ์ฌversion๊ณผ ์ด๋ฒคํธ์version์ ๋น๊ตํฉ๋๋ค. ์ด๋ฒคํธ์version์ด ๋ฉํธ๋ฆญ์version๋ณด๋ค ํฌ๋ฉด ์ ๋ฐ์ดํธํ๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด ์คํตํฉ๋๋ค.๊ด๋ จ ์ฝ๋:
๊ณ ๋ฏผํ ์ :
event_handledํ ์ด๋ธ์ UNIQUE ์ ์ฝ์กฐ๊ฑด์ผ๋ก ๋์์ฑ ์ํฉ์์๋ ์ค๋ณต ์ฒ๋ฆฌ๋ฅผ ๋ฐฉ์งํ์ง๋ง, ์ด ํ ์ด๋ธ์ด ๊ณ์ ์ฆ๊ฐํ๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ์๊ฐ์ด ์ง๋๋ฉด์ ์ด ํ ์ด๋ธ์ ๋ฐ์ดํฐ๊ฐ ๋ฌดํ์ ์ฆ๊ฐํ๊ฒ ๋๋๋ฐ, ์ด๋ ์คํ ๋ฆฌ์ง ๋น์ฉ๊ณผ ์กฐํ ์ฑ๋ฅ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค. TTL(Time To Live)์ ์ค์ ํ์ฌ ์ผ์ ๊ธฐ๊ฐ์ด ์ง๋ ๋ ์ฝ๋๋ฅผ ์๋์ผ๋ก ์ญ์ ํ๊ฑฐ๋, ์์นด์ด๋น ์ ๋ต์ ์๋ฆฝํ์ฌ ์ค๋๋ ๋ฐ์ดํฐ๋ฅผ ๋ณ๋ ํ ์ด๋ธ๋ก ์ด๋์ํค๋ ๋ฑ์ ๋ฐฉ์์ด ํ์ํ ์ ์์ต๋๋ค.versionํ๋๋aggregateId๋ณ๋ก ์๋ ์ฆ๊ฐํ๋๋ก ๊ตฌํํ๋๋ฐ, ์ด ๋ฐฉ์์ ์ฅ์ ์ ๊ฐ๋จํ๊ณ ์์ฐจ์ ์ธ ๋ฒ์ ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ ๋๋ค. ํ์ง๋งupdatedAt๊ธฐ๋ฐ ๋ฐฉ์๊ณผ ๋น๊ตํ์ ๋,updatedAt์ ์๊ฐ ๊ธฐ๋ฐ์ด๋ฏ๋ก ๋คํธ์ํฌ ์ง์ฐ์ด๋ ์์คํ ์๊ฐ ๋ถ์ผ์น ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ๋ฐ๋ฉดversion์ ์์ฐจ์ ์ผ๋ก ์ฆ๊ฐํ๋ฏ๋ก ์ด๋ฌํ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ๋ค๋ง,aggregateId๋ณ๋ก ๋ณ๋์ ๋ฒ์ ์ ๊ด๋ฆฌํด์ผ ํ๋ฏ๋ก ๋ณต์ก๋๊ฐ ์ฆ๊ฐํฉ๋๋ค. ์ด ๋ฐฉ์์ด ์ ์ ํ์ง, ๋๋ ๋ค๋ฅธ ๋ฐฉ์(์:updatedAt๊ธฐ๋ฐ, ๋๋ ์ด๋ฒคํธ ๋ฐ์ ์์ ์ ํ์์คํฌํ ๊ธฐ๋ฐ)์ด ๋ ๋์์ง ๊ฒํ ๊ฐ ํ์ํฉ๋๋ค.event_handledํ ์ด๋ธ๊ณผversionํ๋๋ฅผ ๋ชจ๋ ์ฌ์ฉํ๋ ๊ฒ์ด ์ค๋ณต์ผ ์ ์๋ค๋ ์๋ฌธ๋ ์์ต๋๋ค. ํ์ง๋ง ๋ ๊ฐ์ง๋ ์๋ก ๋ค๋ฅธ ๋ชฉ์ ์ ๊ฐ์ง๊ณ ์์ต๋๋ค.event_handled๋ ๋์ผํ ์ด๋ฒคํธ์ ์์ ์ค๋ณต์ ๋ฐฉ์งํ๊ณ ,version์ ์์๊ฐ ๋ค๋ฐ๋ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ์ง ์๋๋ก ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๋คํธ์ํฌ ๋ฌธ์ ๋ก ์ธํดversion=3์ด๋ฒคํธ๊ฐ ๋จผ์ ๋์ฐฉํ๊ณversion=2์ด๋ฒคํธ๊ฐ ๋์ค์ ๋์ฐฉํ๋ ๊ฒฝ์ฐ,event_handled๋ก๋ ์ค๋ณต์ ๊ฐ์งํ ์ ์์ง๋งversion์ผ๋ก๋ ์ค๋๋ ์ด๋ฒคํธ์์ ๊ฐ์งํ ์ ์์ต๋๋ค.3. ํํฐ์ ํค ๊ธฐ๋ฐ ์์ ๋ณด์ฅ๊ณผ offset.reset: latest ์ค์ ์ ์กฐํฉ
๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ ์ํฉ:
Kafka๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํํฐ์ ๋ด์์๋ง ์์๋ฅผ ๋ณด์ฅํ๊ณ , ์๋ก ๋ค๋ฅธ ํํฐ์ ๊ฐ์ ์์๋ ๋ณด์ฅํ์ง ์์ต๋๋ค. ๋ํ Consumer๊ฐ ์ฌ์์๋๊ฑฐ๋ ์๋ก์ด Consumer Group์ด ์์๋ ๋, ๊ณผ๊ฑฐ์ ๋ชจ๋ ๋ฉ์์ง๋ฅผ ๋ค์ ์ฒ๋ฆฌํ๊ฒ ๋๋ฉด ์ด๋ฏธ ์ฒ๋ฆฌ๋ ์ค๋๋ ๋ฉ์์ง๋ฅผ ์ค๋ณต ์ฒ๋ฆฌํ๊ฑฐ๋, ํ ์คํธ ํ๊ฒฝ์์ ์ด์ ํ ์คํธ์ ๋ฉ์์ง๊ฐ ๋ค์ ํ ์คํธ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค.
ํด๊ฒฐ ๋ฐฉ์:
1) ํํฐ์ ํค ๊ธฐ๋ฐ ์์ ๋ณด์ฅ:
like-events์product-events๋productId๋ฅผ ํํฐ์ ํค๋ก ์ฌ์ฉํ์ฌ, ๊ฐ์ ์ํ์ ๋ํ ์ด๋ฒคํธ๋ ํญ์ ๊ฐ์ ํํฐ์ ์์ ์์๋๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค.order-events๋orderId๋ฅผ ํํฐ์ ํค๋ก ์ฌ์ฉํ์ฌ, ๊ฐ์ ์ฃผ๋ฌธ์ ๋ํ ์ด๋ฒคํธ๋ ํญ์ ๊ฐ์ ํํฐ์ ์์ ์์๋๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค.LikeAddedโLikeRemoved์์๋ก ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ์ ๋ Consumer๋ ๊ฐ์ ์์๋ก ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.2) offset.reset: latest ์ค์ :
offset.reset: latest๋ฅผ ์ค์ ํ์ต๋๋ค.๋ค๋ฅธ ์ต์ ๊ณผ์ ๋น๊ต:
offset.reset: latest์ค์ ์ ์๋ก์ด Consumer Group์ด ์์ํ ๋ ์ต์ ๋ฉ์์ง๋ถํฐ ์ฝ๊ธฐ ์์ํ๋ฏ๋ก,earliest๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ ๋ฐ์ํ๋ ๋ฌธ์ (์ด์ ํ ์คํธ์ ๋ฉ์์ง๊ฐ ๋ค์ ํ ์คํธ์ ์ํฅ์ ์ฃผ์ด ํ ์คํธ ๊ฒฉ๋ฆฌ ์คํจ)๋ฅผ ๋ฐฉ์งํ๊ณ ,manual๋ฐฉ์์ฒ๋ผ ๋ณต์กํ offset ๊ด๋ฆฌ ๋ก์ง ์์ด๋ ํ ์คํธ ๊ฒฉ๋ฆฌ๋ฅผ ๋ณด์ฅํ ์ ์์ต๋๋ค.latest์ค์ ์ ์๋ก์ด Consumer Group์ด ์์ํ ๋๋ง ์ ์ฉ๋๋ฏ๋ก, ๊ฐ์ Consumer Group์ ๊ณ์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ์ด๋ฏธ ์ปค๋ฐ๋ offset์ด ์์ผ๋ฉดlatest๋ ์ ์ฉ๋์ง ์๊ณ ๊ธฐ์กด offset๋ถํฐ ๊ณ์ ์ฝ๊ฒ ๋ฉ๋๋ค.KafkaCleanUp.resetAllTestTopics()๋ก ํ ํฝ์ ์ญ์ ํ๊ณ ์ฌ์์ฑํ์ฌ Consumer Group์ ์ด๊ธฐํํจ์ผ๋ก์จ, ๋งค ํ ์คํธ๋ง๋คoffset.reset: latest์ค์ ์ด ์ ์ฉ๋๋๋ก ํ๊ณ , ์ด์ ํ ์คํธ์ ๋ฉ์์ง๊ฐ ์์ ํ ์ ๊ฑฐ๋์ด ํ ์คํธ ๊ฐ ๊ฒฉ๋ฆฌ๋ฅผ ๋ณด์ฅํฉ๋๋ค.๊ตฌํ ์ธ๋ถ์ฌํญ:
ํํฐ์ ํค ์ค์ :
offset.reset: latest ์ค์ :
ํ ์คํธ ํ๊ฒฝ์์์ ํ ํฝ ๋ฐ Consumer Group ์ด๊ธฐํ:
ํ ์คํธ ํ๊ฒฝ์์์ ๊ฒฉ๋ฆฌ ๋ณด์ฅ:
offset.reset: latest๋ ์๋ก์ด Consumer Group์ด ์์ํ ๋๋ง ์ ์ฉ๋๋ฏ๋ก, ์ด๋ฏธ offset์ด ์ปค๋ฐ๋ Consumer Group์์๋ ์ ์ฉ๋์ง ์์ต๋๋ค. ๋ฐ๋ผ์ ํ ์คํธ ํ๊ฒฝ์์๋ ๊ฐ ํ ์คํธ ์คํ ์ ์KafkaCleanUp.resetAllTestTopics()์resetAllConsumerGroups()๋ฅผ ํธ์ถํ์ฌ ํ ํฝ๊ณผ Consumer Group์ ์ด๊ธฐํํ์ต๋๋ค.offset.reset: latest์ค์ ์ด ์ ์ฉ๋์ด, ์ด์ ํ ์คํธ์ ๋ฉ์์ง๊ฐ ๋ค์ ํ ์คํธ์ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค.${spring.application.name}-test-${random.uuid})ํ์ฌ, ๊ฐ ํ ์คํธ๋ง๋ค ๋ค๋ฅธ Consumer Group์ ์ฌ์ฉํ๋๋ก ํ์ต๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ์ด์ ํ ์คํธ์ offset์ด ๋ค์ ํ ์คํธ์ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค.๊ด๋ จ ์ฝ๋:
๊ณ ๋ฏผํ ์ :
offset.reset: latest๋ ์๋ก์ด Consumer Group์ด ์์ํ ๋๋ง ์ ์ฉ๋๋ฏ๋ก, ์ด๋ฏธ offset์ด ์ปค๋ฐ๋ Consumer Group์์๋ ์ ์ฉ๋์ง ์์ต๋๋ค. ๋ฐ๋ผ์ ํ ์คํธ ํ๊ฒฝ์์๋ ๊ฐ ํ ์คํธ ์คํ ์ ์KafkaCleanUp.resetAllTestTopics()์resetAllConsumerGroups()๋ฅผ ํธ์ถํ์ฌ ํ ํฝ๊ณผ Consumer Group์ ์ด๊ธฐํํ์ต๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ๋งค ํ ์คํธ๋ง๋คoffset.reset: latest์ค์ ์ด ์ ์ฉ๋์ด, ์ด์ ํ ์คํธ์ ๋ฉ์์ง๊ฐ ๋ค์ ํ ์คํธ์ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค. ํ์ง๋ง ์ด ๋ฐฉ์์ ํ ์คํธ ์คํ ์๊ฐ์ ์ฆ๊ฐ์ํฌ ์ ์๊ณ , ํ๋ก๋์ ํ๊ฒฝ์์๋ ์ฌ์ฉํ ์ ์์ต๋๋ค. ๋ค๋ฅธ ํ ์คํธ ๊ฒฉ๋ฆฌ ์ ๋ต(์: ๊ฐ ํ ์คํธ๋ง๋ค ๊ณ ์ ํ Consumer Group ID ์ฌ์ฉ, ๋๋ ํ ์คํธ์ฉ ๋ณ๋ ํ ํฝ ์ฌ์ฉ)์ด ๋ ๋์์ง ๊ฒํ ๊ฐ ํ์ํฉ๋๋ค.ํํฐ์ ํค๋ฅผ
productId๋๋orderId๋ก ์ค์ ํ๋๋ฐ, ์ด๋ก ์ธํ ํํฐ์ ๋ถ๊ท ํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ํน์ ์ํ์ ๋ํ ์ด๋ฒคํธ๊ฐ ๋งค์ฐ ๋ง์ผ๋ฉด ํด๋น ์ํ์ ํํฐ์ ์๋ง ๋ฉ์์ง๊ฐ ์ง์ค๋์ด ๋ค๋ฅธ ํํฐ์ ์ ๋น์ด์์ ์ ์์ต๋๋ค. ํ์ง๋ง ํ์ฌ ๊ตฌํ์์๋ ํํฐ์ ํค๋ฅผ ์ฌ์ฉํ์ฌ ์์๋ฅผ ๋ณด์ฅํ๋ ๊ฒ์ด ๋ ์ค์ํ๋ค๊ณ ํ๋จํ์ต๋๋ค. ๋ง์ฝ ํํฐ์ ๋ถ๊ท ํ์ด ์ฌ๊ฐํ ๋ฌธ์ ๊ฐ ๋๋ค๋ฉด, ํํฐ์ ํค๋ฅผ ํด์ ํจ์๋ก ๋ณํํ๊ฑฐ๋, ๋ณตํฉ ํค๋ฅผ ์ฌ์ฉํ๋ ๋ฑ์ ๋ฐฉ์์ ๊ณ ๋ คํ ์ ์์ต๋๋ค.ํํฐ์ ํค๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๋๋ค ํค๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊น์? ์๋ฅผ ๋ค์ด, ๊ฐ์ ์ํ์ ๋ํ
LikeAdded์LikeRemoved์ด๋ฒคํธ๊ฐ ์๋ก ๋ค๋ฅธ ํํฐ์ ์ ๋ฐํ๋๋ฉด, Consumer๊ฐLikeRemoved๋ฅผ ๋จผ์ ์ฒ๋ฆฌํ๊ณLikeAdded๋ฅผ ๋์ค์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ์ด ๊ฒฝ์ฐ ์ข์์ ์๊ฐ ์์๊ฐ ๋๊ฑฐ๋ ์๋ชป๋ ๋ฉํธ๋ฆญ์ด ์์ฑ๋ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ ํํฐ์ ํค๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ์ aggregate root์ ๋ํ ์ด๋ฒคํธ๋ ํญ์ ๊ฐ์ ํํฐ์ ์์ ์์๋๋ก ์ฒ๋ฆฌ๋๋๋ก ํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.๊ตฌํ ์ธ๋ถ์ฌํญ
1. ๋ด๋ถ ์ด๋ฒคํธ์ ์ธ๋ถ ์ด๋ฒคํธ์ ๊ตฌ๋ถ
๋ฐฐ๊ฒฝ:
๊ธฐ์กด์๋ JVM ๋ด์์ ์ฒ๋ฆฌ ๊ฐ๋ฅํ ๋์จํ ์ฐ๊ฒฐ์ ์ํด Spring Application Event๋ฅผ ์ฌ์ฉํ์ต๋๋ค. ์๋ฅผ ๋ค์ด, ์ฃผ๋ฌธ ์์ฑ ์ ์ฌ๊ณ ์ฐจ๊ฐ์ด๋ ํฌ์ธํธ ์ ๋ฆฝ ๋ฑ์ ๋ก์ง์ ๊ฐ์ ์ ํ๋ฆฌ์ผ์ด์ ๋ด์์ ์ฒ๋ฆฌ๋๋ฏ๋ก Application Event๋ก ์ถฉ๋ถํ์ต๋๋ค. ํ์ง๋ง ์ด๋ฒ์๋ ์ธ๋ถ ์์คํ (์ง๊ณ ์๋น์ค)๊ณผ์ ํต์ ์ด ํ์ํ๋ฏ๋ก, JVM์ ๋ฒ์ด๋ ๋คํธ์ํฌ๋ฅผ ํตํด ๋ฉ์์ง๋ฅผ ์ ๋ฌํ ์ ์๋ Kafka๋ฅผ ์ฌ์ฉํ๋๋ก ๊ตฌ์ฑํ์ต๋๋ค.
๋ด๋ถ ์ด๋ฒคํธ (Application Event):
ApplicationEventPublisher๋ฅผ ํตํด ๋ฐํ์ธ๋ถ ์ด๋ฒคํธ (Kafka Event):
๊ตฌ์กฐ:
๊ด๋ จ ์ฝ๋:
2. Producer ์ค์ : At Least Once ๋ณด์ฅ ๋ฐ ์ด๋ฒคํธ ์์ฑ ์คํจ ์ฒ๋ฆฌ
๋ฐฐ๊ฒฝ:
์ธ๋ถ ์์คํ ๊ณผ์ ํต์ ์ด๊ธฐ ๋๋ฌธ์ DB์์ ๋ฐ์ดํฐ๋ฅผ ์ง์ ์กฐํํ๊ฑฐ๋, ๊ธฐ์กด์ ์๋น์ค ๋ก์ง์ ์ฌ์ฉํ์ฌ ๊ฒ์ฆ์ฒ๋ฆฌํ๊ธฐ๋ ์ด๋ ค์ด ์ํฉ์ ๋๋ค. ๋ฐ๋ผ์ ์ด๋ฒคํธ publisher ์์ฒด์ ์ค์ ์ ํตํด ๋ฉ์์ง ์ ์ค์ ๋ฐฉ์งํ๊ณ , Consumer ์ธก์์ ๋ฉฑ๋ฑ ์ฒ๋ฆฌ๋ฅผ ํตํด ์ค๋ณต์ ๋ฐฉ์งํ๋ ๊ตฌ์กฐ๋ก ๊ตฌ์ฑํ์ต๋๋ค.
์ค์ ๋ด์ฉ:
acks=all: Producer๊ฐ ๋ฉ์์ง๋ฅผ ๋ฐํํ ๋, ๋ชจ๋ ๋ฆฌํ๋ฆฌ์นด์ ์ฐ๊ธฐ๊ฐ ์๋ฃ๋ ๋๊น์ง ๋๊ธฐํฉ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ๋ฆฌํ๋ฆฌ์นด ์ค ํ๋๊ฐ ์คํจํ๋๋ผ๋ ๋ค๋ฅธ ๋ฆฌํ๋ฆฌ์นด์ ๋ฉ์์ง๊ฐ ์ ์ฅ๋์ด ์ ์ค์ ๋ฐฉ์งํ ์ ์์ต๋๋ค.enable.idempotence=true: Producer๊ฐ ๋์ผํ ๋ฉ์์ง๋ฅผ ์ฌ๋ฌ ๋ฒ ๋ฐํํ๋๋ผ๋ Kafka ๋ธ๋ก์ปค๊ฐ ์ค๋ณต์ ์ ๊ฑฐํ์ฌ ํ ๋ฒ๋ง ์ ์ฅํ๋๋ก ํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋คํธ์ํฌ ์ค๋ฅ๋ก ์ธํ ์ฌ์๋ ์ ์ค๋ณต ๋ฉ์์ง๊ฐ ์ ์ฅ๋๋ ๊ฒ์ ๋ฐฉ์งํฉ๋๋ค.max.in.flight.requests.per.connection=5:idempotence=true์ผ ๋ ํ์ ์ค์ ์ ๋๋ค. ๋์์ ์ ์กํ ์ ์๋ ๋ฏธํ์ธ ์์ฒญ์ ์ต๋ ๊ฐ์๋ฅผ ์ ํํฉ๋๋ค.๊ด๋ จ ์ฝ๋:
๊ณ ๋ฏผํ ์ :
acks=all์ ๋ฉ์์ง ์ ์ค์ ๋ฐฉ์งํ์ง๋ง, ๋ชจ๋ ๋ฆฌํ๋ฆฌ์นด์ ์ฐ๊ธฐ๊ฐ ์๋ฃ๋ ๋๊น์ง ๋๊ธฐํ๋ฏ๋ก ์ง์ฐ ์๊ฐ์ด ์ฆ๊ฐํ ์ ์์ต๋๋ค. ํ์ง๋ง ๋ฉ์์ง ์ ์ค์ ๋ฐฉ์งํ๋ ๊ฒ์ด ๋ ์ค์ํ๋ค๊ณ ํ๋จํ์ฌacks=all์ ์ ํํ์ต๋๋ค.enable.idempotence=true๋ Producer ์ธก์์ ์ค๋ณต์ ์ ๊ฑฐํ์ง๋ง, ๋คํธ์ํฌ ์ค๋ฅ๋ Consumer ์ฌ์์ ๋ฑ์ ์ํฉ์์ ๋์ผํ ๋ฉ์์ง๊ฐ ์ฌ๋ฌ ๋ฒ ์ ๋ฌ๋ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ Consumer ์ธก์์๋ ๋ฉฑ๋ฑ ์ฒ๋ฆฌ๊ฐ ํ์ํฉ๋๋ค.3. Consumer ์ค์ : Manual Ack ์ฒ๋ฆฌ
๋ฐฐ๊ฒฝ:
Consumer๊ฐ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ณผ์ ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ๋ง์ฝ ์๋ ์ปค๋ฐ์ ์ฌ์ฉํ๋ฉด, ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ ์ offset์ด ์ปค๋ฐ๋์ด ์ด๋ฒคํธ๊ฐ ์ ์ค๋ ์ ์์ต๋๋ค. ๋ฐ๋๋ก ์ด๋ฒคํธ ์ฒ๋ฆฌ ์คํจ ์์๋ offset์ด ์ปค๋ฐ๋์ง ์๋๋ก ํ์ฌ ์ฌ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋๋ก ํด์ผ ํฉ๋๋ค.
์ค์ ๋ด์ฉ:
enable-auto-commit: false: ์๋ ์ปค๋ฐ์ ๋นํ์ฑํํ์ฌ ์๋ ์ปค๋ฐ์ ์ฌ์ฉํฉ๋๋ค.ack-mode: manual: ์๋ ์ปค๋ฐ ๋ชจ๋๋ฅผ ์ฌ์ฉํ์ฌ, ์ด๋ฒคํธ ์ฒ๋ฆฌ ์ฑ๊ณต ํ์๋งAcknowledgment.acknowledge()๋ฅผ ํธ์ถํ์ฌ offset์ ์ปค๋ฐํฉ๋๋ค.์ฒ๋ฆฌ ํ๋ฆ:
acknowledgment.acknowledge()๋ฅผ ํธ์ถํ์ฌ offset์ ์ปค๋ฐํฉ๋๋ค.acknowledgment.acknowledge()๋ฅผ ํธ์ถํ์ง ์์ offset์ด ์ปค๋ฐ๋์ง ์์ผ๋ฏ๋ก, Consumer๊ฐ ์ฌ์์๋๊ฑฐ๋ ๋ค์ poll ์ ๋์ผํ ๋ฉ์์ง๋ฅผ ๋ค์ ๋ฐ์ ์ฌ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.๊ด๋ จ ์ฝ๋:
๊ณ ๋ฏผํ ์ :
4. ๋ฉฑ๋ฑ์ฑ ์ฒ๋ฆฌ: event_handled ํ ์ด๋ธ
๋ฐฐ๊ฒฝ:
Kafka์ At Least Once ๋ณด์ฅ๊ณผ Manual Ack ์ฒ๋ฆฌ๋ก ์ธํด, ๋์ผํ ์ด๋ฒคํธ๊ฐ ์ฌ๋ฌ ๋ฒ ์ ๋ฌ๋ ์ ์์ต๋๋ค. ๋ํ Consumer๊ฐ ์ฌ๋ฌ ์ธ์คํด์ค๋ก ์คํ๋๋ ๊ฒฝ์ฐ, ๋์์ ๊ฐ์ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ ค๊ณ ํ ์ ์์ต๋๋ค. ์ด๋ฌํ ์ํฉ์์ ์ค๋ณต ์ฒ๋ฆฌ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด
event_handledํ ์ด๋ธ์ ์ฌ์ฉํฉ๋๋ค.๊ตฌํ ๋ฐฉ์:
eventId๋ฅผ ๋ถ์ฌํฉ๋๋ค.event_handledํ ์ด๋ธ์์ ํด๋นeventId๊ฐ ์ด๋ฏธ ์ฒ๋ฆฌ๋์๋์ง ํ์ธํฉ๋๋ค.event_handledํ ์ด๋ธ์ ๊ธฐ๋ก์ ์ ์ฅํฉ๋๋ค.event_handledํ ์ด๋ธ์event_id์ปฌ๋ผ์ UNIQUE ์ ์ฝ์กฐ๊ฑด์ ์ค์ ํ์ฌ, ๋์์ฑ ์ํฉ์์๋ ์ค๋ณต ์ฒ๋ฆฌ๋ฅผ ๋ฐฉ์งํฉ๋๋ค.๊ด๋ จ ์ฝ๋:
๊ณ ๋ฏผํ ์ :
event_handledํ ์ด๋ธ์ DB๋ก ๊ตฌํํ์ง๋ง, Redis๋ฅผ ์ฌ์ฉํ๋ ๊ฒ๋ ๊ณ ๋ คํ ์ ์์ต๋๋ค. Redis๋ TTL์ ์ฝ๊ฒ ์ค์ ํ ์ ์๊ณ ์กฐํ ์ฑ๋ฅ์ด ๋น ๋ฅด์ง๋ง, ์์์ฑ์ด ๋ณด์ฅ๋์ง ์์ต๋๋ค. DB๋ ์์์ฑ์ด ๋ณด์ฅ๋์ง๋ง TTL ์ค์ ์ด ๋ณต์กํ๊ณ ์กฐํ ์ฑ๋ฅ์ด ์๋์ ์ผ๋ก ๋๋ฆด ์ ์์ต๋๋ค. ํ์ฌ๋ DB๋ฅผ ์ ํํ๋๋ฐ, ์ด๋ ์์์ฑ์ด ์ค์ํ๊ณ TTL์ ๋ณ๋ ์์นด์ด๋น ์ ๋ต์ผ๋ก ํด๊ฒฐํ ์ ์๋ค๊ณ ํ๋จํ๊ธฐ ๋๋ฌธ์ ๋๋ค.event_handledํ ์ด๋ธ์ด ๊ณ์ ์ฆ๊ฐํ๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ์๊ฐ์ด ์ง๋๋ฉด์ ์ด ํ ์ด๋ธ์ ๋ฐ์ดํฐ๊ฐ ๋ฌดํ์ ์ฆ๊ฐํ๊ฒ ๋๋๋ฐ, ์ด๋ ์คํ ๋ฆฌ์ง ๋น์ฉ๊ณผ ์กฐํ ์ฑ๋ฅ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค. TTL์ ์ค์ ํ์ฌ ์ผ์ ๊ธฐ๊ฐ์ด ์ง๋ ๋ ์ฝ๋๋ฅผ ์๋์ผ๋ก ์ญ์ ํ๊ฑฐ๋, ์์นด์ด๋น ์ ๋ต์ ์๋ฆฝํ์ฌ ์ค๋๋ ๋ฐ์ดํฐ๋ฅผ ๋ณ๋ ํ ์ด๋ธ๋ก ์ด๋์ํค๋ ๋ฑ์ ๋ฐฉ์์ด ํ์ํ ์ ์์ต๋๋ค.5. ๋ฒ์ ๊ธฐ๋ฐ ์ต์ ์ด๋ฒคํธ๋ง ๋ฐ์
๋ฐฐ๊ฒฝ:
๋คํธ์ํฌ ์ง์ฐ์ด๋ ํํฐ์ ์์ ๋ฌธ์ ๋ก ์ธํด, ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ์์์ Consumer๊ฐ ๋ฐ๋ ์์๊ฐ ๋ค๋ฅผ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด,
version=3์ด๋ฒคํธ๊ฐ ๋จผ์ ๋์ฐฉํ๊ณversion=2์ด๋ฒคํธ๊ฐ ๋์ค์ ๋์ฐฉํ๋ ๊ฒฝ์ฐ,version=2์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ฉด ์ด๋ฏธversion=3์ผ๋ก ์ ๋ฐ์ดํธ๋ ๋ฉํธ๋ฆญ์ ๋ฎ์ด์ฐ๊ฒ ๋์ด ์๋ชป๋ ์ํ๊ฐ ๋ฉ๋๋ค.ํด๊ฒฐ ๋ฐฉ์:
OutboxEvent์aggregateId๋ณ๋ก ์์ฐจ์ ์ผ๋ก ์ฆ๊ฐํ๋versionํ๋๋ฅผ ๋ถ์ฌํฉ๋๋ค.version์ Kafka ๋ฉ์์ง ํค๋์ ํฌํจ๋์ด Consumer๋ก ์ ๋ฌ๋ฉ๋๋ค.ProductMetrics์ ํ์ฌversion๊ณผ ์ด๋ฒคํธ์version์ ๋น๊ตํฉ๋๋ค.version์ด ๋ฉํธ๋ฆญ์version๋ณด๋ค ํฌ๋ฉด ์ ๋ฐ์ดํธํ๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด ์คํตํฉ๋๋ค.๊ตฌํ ๋ฐฉ์:
OutboxEventService.saveEvent()์์aggregateId๋ณ ์ต์ ๋ฒ์ ์ ์กฐํํ ํ +1ํ์ฌ ์๋ก์ด ๋ฒ์ ์ ๋ถ์ฌํฉ๋๋ค.ProductMetrics์๋versionํ๋๋ฅผ ๋๊ณ , ์ ๋ฐ์ดํธํ ๋๋ง๋ค ์ฆ๊ฐ์ํต๋๋ค.ProductMetrics.shouldUpdate()๋ฉ์๋๋ก ์ด๋ฒคํธ ๋ฒ์ ๊ณผ ๋ฉํธ๋ฆญ ๋ฒ์ ์ ๋น๊ตํ์ฌ, ์ด๋ฒคํธ๊ฐ ์ต์ ์ธ ๊ฒฝ์ฐ์๋ง ์ ๋ฐ์ดํธํฉ๋๋ค.์ ์ฒด ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ:
ํต์ฌ ์ฝ๋ ์์น:
1) ๋ฒ์ ์์ฑ: OutboxEventService.saveEvent() (59-60์ค)
2) Kafka ํค๋์ ์ถ๊ฐ: OutboxEventPublisher.publishEvent() (99-102์ค)
3) Consumer์์ ์ถ์ถ: ProductMetricsConsumer.extractVersion() (374-387์ค)
4) ๋ฒ์ ๋น๊ต: ProductMetricsService์์ eventVersion๊ณผ metrics.version ๋น๊ตํ์ฌ ์ต์ ์ด๋ฒคํธ๋ง ๋ฐ์
์ด๋ ๊ฒ
aggregateId๋ณ๋ก ์์ฐจ์ ์ธ ๋ฒ์ ์ด ์์ฑ๋์ด Kafka ํค๋๋ก ์ ๋ฌ๋๊ณ , Consumer์์ ์ต์ ์ด๋ฒคํธ๋ง ๋ฐ์ํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.๊ด๋ จ ์ฝ๋:
๊ณ ๋ฏผํ ์ :
versionํ๋๋aggregateId๋ณ๋ก ์๋ ์ฆ๊ฐํ๋๋ก ๊ตฌํํ๋๋ฐ, ์ด ๋ฐฉ์์ ์ฅ์ ์ ๊ฐ๋จํ๊ณ ์์ฐจ์ ์ธ ๋ฒ์ ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ ๋๋ค. ํ์ง๋งupdatedAt๊ธฐ๋ฐ ๋ฐฉ์๊ณผ ๋น๊ตํ์ ๋,updatedAt์ ์๊ฐ ๊ธฐ๋ฐ์ด๋ฏ๋ก ๋คํธ์ํฌ ์ง์ฐ์ด๋ ์์คํ ์๊ฐ ๋ถ์ผ์น ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ๋ฐ๋ฉดversion์ ์์ฐจ์ ์ผ๋ก ์ฆ๊ฐํ๋ฏ๋ก ์ด๋ฌํ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ๋ค๋ง,aggregateId๋ณ๋ก ๋ณ๋์ ๋ฒ์ ์ ๊ด๋ฆฌํด์ผ ํ๋ฏ๋ก ๋ณต์ก๋๊ฐ ์ฆ๊ฐํฉ๋๋ค.versionํ๋๊ฐaggregateId๋ณ๋ก ๊ด๋ฆฌ๋๋ฏ๋ก, ๊ฐ์aggregateId์ ๋ํ ์ด๋ฒคํธ๋ ์์ฐจ์ ์ผ๋ก ์ฒ๋ฆฌ๋์ด์ผ ํฉ๋๋ค. ํ์ง๋ง ํํฐ์ ํค๋ฅผaggregateId๋ก ์ค์ ํ์ผ๋ฏ๋ก, ๊ฐ์aggregateId์ ๋ํ ์ด๋ฒคํธ๋ ๊ฐ์ ํํฐ์ ์์ ์์๋๋ก ์ฒ๋ฆฌ๋๋ฏ๋ก ์ด ๋ฌธ์ ๋ ํด๊ฒฐ๋ฉ๋๋ค.6. ํํฐ์ ํค ์ค์
๋ฐฐ๊ฒฝ:
Kafka๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํํฐ์ ๋ด์์๋ง ์์๋ฅผ ๋ณด์ฅํ๊ณ , ์๋ก ๋ค๋ฅธ ํํฐ์ ๊ฐ์ ์์๋ ๋ณด์ฅํ์ง ์์ต๋๋ค. ๋ฐ๋ผ์ ๊ฐ์ aggregate root์ ๋ํ ์ด๋ฒคํธ๋ ๊ฐ์ ํํฐ์ ์์ ์ฒ๋ฆฌ๋์ด์ผ ์์๊ฐ ๋ณด์ฅ๋ฉ๋๋ค.
7. ํผ๋ธ๋ฆฌ์ ์ ์ปจ์๋จธ๊ฐ ๋์ผ ์ด๋ฒคํธ๋ฅผ ํ๋จํ๋ ๊ธฐ์ค ์ค์
๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ ์ํฉ:
aggregate root id(์:
productId,orderId)๋ง์ผ๋ก๋ ํด๋น event๊ฐ ๋์ผํ ํญ๋ชฉ์ ์ง์ ํ๋์ง ๋ณด์ฅํ๊ธฐ ์ด๋ ต์ต๋๋ค. ์๋ฅผ ๋ค์ด, ๊ฐ์productId์ ๋ํด ์ฌ๋ฌ ๋ฒ ์ข์์๊ฐ ์ถ๊ฐ๋๋ฉด, ๊ฐ๊ฐ์ ์๋ก ๋ค๋ฅธ ์ด๋ฒคํธ์ด์ง๋งproductId๋ง์ผ๋ก๋ ๊ตฌ๋ถํ ์ ์์ต๋๋ค. ๋ํ ๋คํธ์ํฌ ์ค๋ฅ๋ Consumer ์ฌ์์์ผ๋ก ์ธํด ๋์ผํ ์ด๋ฒคํธ๊ฐ ์ฌ๋ฌ ๋ฒ ์ ๋ฌ๋ ์ ์๋๋ฐ,productId๋ง์ผ๋ก๋ ์ด๊ฒ์ด ์ค๋ณต์ธ์ง ์๋ก์ด ์ด๋ฒคํธ์ธ์ง ํ๋จํ ์ ์์ต๋๋ค.ํด๊ฒฐ ๋ฐฉ์:
์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ ๊ฐ์ง ์๋ณ์๋ฅผ ์กฐํฉํ์ฌ ์ฌ์ฉํฉ๋๋ค:
๊ตฌํ ์ธ๋ถ์ฌํญ:
1) eventId๋ฅผ ํตํ ๋์ผ ์ด๋ฒคํธ ํ๋จ:
OutboxEventService.saveEvent()์์ ๊ฐ ์ด๋ฒคํธ์ UUID ๊ธฐ๋ฐ์ ๊ณ ์ ํeventId๋ฅผ ๋ถ์ฌํฉ๋๋ค.eventId๋ Kafka ๋ฉ์์ง ํค๋์ ํฌํจ๋์ด Consumer๋ก ์ ๋ฌ๋ฉ๋๋ค.event_handledํ ์ด๋ธ์์ ํด๋นeventId๊ฐ ์ด๋ฏธ ์ฒ๋ฆฌ๋์๋์ง ํ์ธํฉ๋๋ค.event_handledํ ์ด๋ธ์ ๊ธฐ๋ก์ ์ ์ฅํฉ๋๋ค.2) version์ ํตํ ๋ถํ์ํ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ฐฉ์ง:
OutboxEventService.saveEvent()์์aggregateId๋ณ๋ก ์์ฐจ์ ์ผ๋ก ์ฆ๊ฐํ๋version์ ๋ถ์ฌํฉ๋๋ค.version์ Kafka ๋ฉ์์ง ํค๋์ ํฌํจ๋์ด Consumer๋ก ์ ๋ฌ๋ฉ๋๋ค.ProductMetrics์ ํ์ฌversion๊ณผ ์ด๋ฒคํธ์version์ ๋น๊ตํฉ๋๋ค.version์ด ๋ฉํธ๋ฆญ์version๋ณด๋ค ํฌ๋ฉด ์ ๋ฐ์ดํธํ๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด ์คํตํฉ๋๋ค.๊ด๋ จ ์ฝ๋:
๊ณ ๋ฏผํ ์ :
eventId์version์ ๋ชจ๋ ์ฌ์ฉํ๋ ๊ฒ์ด ์ค๋ณต์ผ ์ ์๋ค๋ ์๋ฌธ์ด ์์ต๋๋ค. ํ์ง๋ง ๋ ๊ฐ์ง๋ ์๋ก ๋ค๋ฅธ ๋ชฉ์ ์ ๊ฐ์ง๊ณ ์์ต๋๋ค.eventId๋ ๋์ผํ ์ด๋ฒคํธ์ ์์ ์ค๋ณต์ ๋ฐฉ์งํ๊ณ ,version์ ์์๊ฐ ๋ค๋ฐ๋ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ์ง ์๋๋ก ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๋คํธ์ํฌ ๋ฌธ์ ๋ก ์ธํดversion=3์ด๋ฒคํธ๊ฐ ๋จผ์ ๋์ฐฉํ๊ณversion=2์ด๋ฒคํธ๊ฐ ๋์ค์ ๋์ฐฉํ๋ ๊ฒฝ์ฐ,eventId๋ก๋ ์ค๋ณต์ ๊ฐ์งํ ์ ์์ง๋งversion์ผ๋ก๋ ์ค๋๋ ์ด๋ฒคํธ์์ ๊ฐ์งํ ์ ์์ต๋๋ค. ๋ฐ๋๋ก, ๋์ผํ ์ด๋ฒคํธ๊ฐ ๋คํธ์ํฌ ์ค๋ฅ๋ก ์ธํด ์ฌ๋ฌ ๋ฒ ์ ๋ฌ๋๋ ๊ฒฝ์ฐ,version์ผ๋ก๋ ์ค๋ณต์ ๊ฐ์งํ ์ ์์ง๋งeventId๋ก๋ ์ค๋ณต์ ๊ฐ์งํ ์ ์์ต๋๋ค.aggregateId๋ง์ผ๋ก๋ ๋์ผ ์ด๋ฒคํธ๋ฅผ ํ๋จํ ์ ์๋ ์ด์ ๋, ๊ฐ์aggregateId์ ๋ํด ์ฌ๋ฌ ๋ฒ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค. ์๋ฅผ ๋ค์ด, ๊ฐ์ ์ํ์ ๋ํด ์ข์์๊ฐ ์ฌ๋ฌ ๋ฒ ์ถ๊ฐ๋๋ฉด, ๊ฐ๊ฐ์ ์๋ก ๋ค๋ฅธ ์ด๋ฒคํธ์ด์ง๋งproductId๋ง์ผ๋ก๋ ๊ตฌ๋ถํ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ ๊ฐ ์ด๋ฒคํธ์ ๊ณ ์ ํeventId๋ฅผ ๋ถ์ฌํ์ฌ ๊ตฌ๋ถํด์ผ ํฉ๋๋ค.version์aggregateId๋ณ๋ก ๊ด๋ฆฌ๋๋ฏ๋ก, ๊ฐ์aggregateId์ ๋ํ ์ด๋ฒคํธ๋ ์์ฐจ์ ์ผ๋ก ์ฒ๋ฆฌ๋์ด์ผ ํฉ๋๋ค. ํ์ง๋ง ํํฐ์ ํค๋ฅผaggregateId๋ก ์ค์ ํ์ผ๋ฏ๋ก, ๊ฐ์aggregateId์ ๋ํ ์ด๋ฒคํธ๋ ๊ฐ์ ํํฐ์ ์์ ์์๋๋ก ์ฒ๋ฆฌ๋๋ฏ๋ก ์ด ๋ฌธ์ ๋ ํด๊ฒฐ๋ฉ๋๋ค.6. ํํฐ์ ํค ์ค์
๋ฐฐ๊ฒฝ:
Kafka๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํํฐ์ ๋ด์์๋ง ์์๋ฅผ ๋ณด์ฅํ๊ณ , ์๋ก ๋ค๋ฅธ ํํฐ์ ๊ฐ์ ์์๋ ๋ณด์ฅํ์ง ์์ต๋๋ค. ๋ฐ๋ผ์ ๊ฐ์ aggregate root์ ๋ํ ์ด๋ฒคํธ๋ ๊ฐ์ ํํฐ์ ์์ ์ฒ๋ฆฌ๋์ด์ผ ์์๊ฐ ๋ณด์ฅ๋ฉ๋๋ค.
์ค์ ๋ด์ฉ:
like-events,product-events:productId๋ฅผ ํํฐ์ ํค๋ก ์ฌ์ฉํ์ฌ, ๊ฐ์ ์ํ์ ๋ํ ์ด๋ฒคํธ๋ ํญ์ ๊ฐ์ ํํฐ์ ์์ ์์๋๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค.order-events:orderId๋ฅผ ํํฐ์ ํค๋ก ์ฌ์ฉํ์ฌ, ๊ฐ์ ์ฃผ๋ฌธ์ ๋ํ ์ด๋ฒคํธ๋ ํญ์ ๊ฐ์ ํํฐ์ ์์ ์์๋๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค.๊ด๋ จ ์ฝ๋:
๊ณ ๋ฏผํ ์ :
productId๋๋orderId๋ก ์ค์ ํ๋๋ฐ, ์ด๋ก ์ธํ ํํฐ์ ๋ถ๊ท ํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ํน์ ์ํ์ ๋ํ ์ด๋ฒคํธ๊ฐ ๋งค์ฐ ๋ง์ผ๋ฉด ํด๋น ์ํ์ ํํฐ์ ์๋ง ๋ฉ์์ง๊ฐ ์ง์ค๋์ด ๋ค๋ฅธ ํํฐ์ ์ ๋น์ด์์ ์ ์์ต๋๋ค. ํ์ง๋ง ํ์ฌ ๊ตฌํ์์๋ ํํฐ์ ํค๋ฅผ ์ฌ์ฉํ์ฌ ์์๋ฅผ ๋ณด์ฅํ๋ ๊ฒ์ด ๋ ์ค์ํ๋ค๊ณ ํ๋จํ์ต๋๋ค. ๋ง์ฝ ํํฐ์ ๋ถ๊ท ํ์ด ์ฌ๊ฐํ ๋ฌธ์ ๊ฐ ๋๋ค๋ฉด, ํํฐ์ ํค๋ฅผ ํด์ ํจ์๋ก ๋ณํํ๊ฑฐ๋, ๋ณตํฉ ํค๋ฅผ ์ฌ์ฉํ๋ ๋ฑ์ ๋ฐฉ์์ ๊ณ ๋ คํ ์ ์์ต๋๋ค.LikeAdded์LikeRemoved์ด๋ฒคํธ๊ฐ ์๋ก ๋ค๋ฅธ ํํฐ์ ์ ๋ฐํ๋๋ฉด, Consumer๊ฐLikeRemoved๋ฅผ ๋จผ์ ์ฒ๋ฆฌํ๊ณLikeAdded๋ฅผ ๋์ค์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ์ด ๊ฒฝ์ฐ ์ข์์ ์๊ฐ ์์๊ฐ ๋๊ฑฐ๋ ์๋ชป๋ ๋ฉํธ๋ฆญ์ด ์์ฑ๋ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ ํํฐ์ ํค๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ์ aggregate root์ ๋ํ ์ด๋ฒคํธ๋ ํญ์ ๊ฐ์ ํํฐ์ ์์ ์์๋๋ก ์ฒ๋ฆฌ๋๋๋ก ํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.โ Checklist
๋๋ฉ์ธ(์ ํ๋ฆฌ์ผ์ด์ ) ์ด๋ฒคํธ ์ค๊ณ
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeEvent.javaapps/commerce-api/src/main/java/com/loopers/domain/order/OrderEvent.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductEvent.javaLikeAdded,LikeRemoved,OrderCreated,ProductViewedProducer ์ฑ์์ ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐํ
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxBridgeEventListener.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.javalike-events,order-events,product-eventsPartitionKey ๊ธฐ๋ฐ์ ์ด๋ฒคํธ ์์ ๋ณด์ฅ
apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEvent.java(partitionKey ํ๋)apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.java(KafkaHeaders.KEY ์ค์ )like-events,product-eventsโproductId,order-eventsโorderIdAt Least Once ๋ณด์ฅ (acks=all, idempotence=true)
modules/kafka/src/main/resources/kafka.yml(19-21์ค)acks: all,enable.idempotence: true,max.in.flight.requests.per.connection: 5Transactional Outbox Pattern ๊ตฌํ
apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEvent.javaapps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.javaapps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxBridgeEventListener.java๋ฉ์์ง ๋ฐํ ์คํจ ์ฒ๋ฆฌ
apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.java(63-69์ค)FAILED์ํ๋ก ๋ณ๊ฒฝํ๊ณ ๋ค์ ์ค์ผ์ค์์ ์ฌ์๋โพ Consumer (7/7)
Consumer๊ฐ Metrics ์ง๊ณ ์ฒ๋ฆฌ
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.javaapps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.javaManual Ack ์ฒ๋ฆฌ
modules/kafka/src/main/resources/kafka.yml(27, 29์ค)apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.java(141, 217์ค)enable-auto-commit: false,ack-mode: manualevent_handledํ ์ด๋ธ ๊ธฐ๋ฐ ๋ฉฑ๋ฑ ์ฒ๋ฆฌapps/commerce-streamer/src/main/java/com/loopers/domain/eventhandled/EventHandled.javaapps/commerce-streamer/src/main/java/com/loopers/application/eventhandled/EventHandledService.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.java(91, 275์ค)version๊ธฐ์ค ์ต์ ์ด๋ฒคํธ๋ง ๋ฐ์apps/commerce-api/src/main/java/com/loopers/domain/outbox/OutboxEvent.java(56์ค, version ํ๋)apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java(59-60์ค, ๋ฒ์ ์์ฑ)apps/commerce-api/src/main/java/com/loopers/infrastructure/outbox/OutboxEventPublisher.java(99-102์ค, ํค๋์ ์ถ๊ฐ)apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/ProductMetricsConsumer.java(100, 284์ค, ํค๋์์ ์ถ์ถ)apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java(100, 126์ค, ๋ฒ์ ๋น๊ต)apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java(shouldUpdate ๋ฉ์๋)product_metricsํ ์ด๋ธ์ upsertapps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.javaapps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.javaapps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java์ค๋ณต ๋ฉ์์ง ์ฌ์ ์ก ํ ์คํธ
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ProductMetricsConsumerTest.java(352์ค,handlesDuplicateMessagesIdempotently())eventId๋ฅผ ๊ฐ์ง ๋ฉ์์ง๊ฐ ํ ๋ฒ๋ง ์ฒ๋ฆฌ๋๋์ง ๊ฒ์ฆ์ฌ๊ณ ์์ง ์ ์ํ ์บ์ ๊ฐฑ์
apps/commerce-api/src/main/java/com/loopers/application/product/ProductEventHandler.java(147-151์ค)apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java(evictProductDetailCache ๋ฉ์๋)stock == 0์ฒดํฌํ์ฌ ์บ์ ๋ฌดํจํ๐ References
Summary by CodeRabbit
๋ฆด๋ฆฌ์ค ๋ ธํธ
์๋ก์ด ๊ธฐ๋ฅ
ํ ์คํธ
โ๏ธ Tip: You can customize this high-level summary in your review settings.