항해 9주차 WIL – 도메인 이벤트 기반 Outbox + Kafka 비동기 아키텍처

JUNYOUNG·2025년 6월 3일

항해 플러스 백엔드

목록 보기
14/14
post-thumbnail

이번 주 목표


이번 주의 핵심 목표는 도메인 이벤트를 기점으로 Outbox + Kafka 기반의 비동기 전파 아키텍처를 직접 설계하고,

그 흐름을 통해 도메인 간의 협업을 결합도 낮은 구조로 구현해보는 것이었다.

이를 통해 다음과 같은 설계를 실험하고 검증했다:

  • 쿠폰 발급, 주문 확정, 외부 전송 등의 도메인 흐름을 상태 기반 이벤트로 분리
  • 도메인에서 이벤트 등록 → ApplicationEventPublisher 를 통한 발행
  • @TransactionalEventListener(BEFORE_COMMIT or AFTER_COMMIT) 기반 이벤트 감지
  • Outbox DB 저장 → Kafka로 전송하는 이벤트 릴레이 구조 구현
  • Kafka Consumer → 후속 도메인 처리까지 연결
  • Awaitility 기반 통합 테스트로 전체 흐름 검증

문제 정의

1. 동기 트랜잭션 내에서 모든 처리를 해결하려다 보니 책임이 모호해짐

예를 들어, 쿠폰 발급 과정에서 수량 확인 → 발급 처리 → 메시지 전송까지 한 메서드에 몰려있었고,

메시지 발행 실패 시 전체 트랜잭션이 롤백되는 구조는 장애 복원력 측면에서도 한계가 있었다.

2. 이벤트는 도메인에서 발생하지만, 전송/처리는 애플리케이션 레이어의 역할

초기에는 컨트롤러나 서비스 단에서 Kafka 메시지를 직접 발행하려 했으나,

"도메인의 책임은 상태 전이",

  • *"메시지 전파는 후속 처리"**라는 역할 분리가 필요하다는 결론에 도달했다.

해결 전략 및 설계 방식

도메인 → ApplicationEvent → Outbox → Kafka 구조

도메인 객체는 상태 변화 시 registerEvent()를 통해 도메인 이벤트를 등록하고,

Application Layer에서 publishEvent()로 발행한다.

coupon.validateUsable(clock, userId); // → 내부적으로 도메인 이벤트 등록
coupon.getDomainEvents().forEach(eventPublisher::publishEvent);
coupon.clearEvents();

전체 흐름 예시

쿠폰 발급 요청 흐름

[Coupon 도메인]
 - validateUsable() 내부에서 CouponIssueRequestedEvent 등록

[Application Layer]
 - @TransactionalEventListener(BEFORE_COMMIT)
   → OutboxService.saveEvent() 호출 → OutboxMessage 저장

[Infrastructure Layer]
 - ScheduledOutboxRelayRunner (1초마다 실행)
   → 저장된 메시지를 Kafka 로 전송 → 성공 시 lastProcessedId 갱신

[Kafka Consumer]
 - coupon.issue.requested 수신 → 발급 처리 실행

결제 성공 → 주문 상태 변경 → 외부 전송 흐름

[Payment 도메인]
 - createSuccess() 시 OrderConfirmedEvent 등록

[OrderConfirmedEventHandler]
 - AFTER_COMMIT + @Async로 수신 → 주문 상태 CONFIRMED 전이

[OrderAggregate]
 - markConfirmed() → ProductSalesRankRecordedEvent, OrderExportRequestedEvent 등록

[OrderExportEventHandler]
 - AFTER_COMMIT + @Async → Kafka 전송

[Kafka Consumer]
 - order-export 수신 → 외부 연동 처리

테스트 전략

통합 테스트 검증 포인트

테스트 시나리오검증 내용
쿠폰 발급 요청도메인 이벤트 등록 → Outbox 저장 → Kafka 전파 → 발급 처리
주문 생성 → 재고 감소 실패Kafka 메시지 전송 실패 시 OrderCanceled 이벤트 발행
결제 성공주문 상태 전이 + 후속 이벤트 발행 및 Kafka 전송
주문 CONFIRMED상품 랭킹 등록 + 외부 전송 이벤트 발행

Awaitility를 활용하여 비동기 리스너가 동작 완료될 때까지 기다리며, 로그 기반으로 실제 흐름 검증 완료.


인사이트

도메인 이벤트는 도메인 상태 전이의 일부로만 발생해야 한다

  • 단순 메시지 전송을 위한 이벤트는 Application 레벨에서만 처리하고,
  • 도메인 이벤트는 도메인의 상태 변화가 명확히 발생한 경우에만 사용해야 구조가 명확해졌다.

Kafka 도입의 기준점은 ‘타 도메인으로의 이벤트 전파 필요성’

  • 쿠폰 발급 같은 단일 도메인 처리라면 Redis/Stream 으로도 충분하지만,
  • 주문 → 랭킹 → 외부 전송처럼 도메인이 여러 개로 분리되고, 처리를 병렬화하거나 장애 복원성을 확보하고자 할 때 Kafka의 도입 가치가 명확해졌다.

Outbox 패턴은 메시지 신뢰성과 장애 대응의 기초

  • 메시지 저장 → 별도 릴레이 스케줄러 → Kafka 전송 구조를 구현하면서, 트랜잭션 일관성과 메시지 재처리 구조를 어느 정도 확보했다.
  • 다만 실패 메시지 저장, Retry 큐, DLT(DLQ)까지는 아직 보완이 필요하다.

요약 정리

흐름이벤트리스너/처리 방식목적
쿠폰 발급 요청CouponIssueRequestedEventBEFORE_COMMIT → Outbox 저장Kafka로 전파 후 발급 처리
결제 성공OrderConfirmedEventAFTER_COMMIT → 주문 CONFIRMED후속 이벤트 트리거
주문 CONFIRMEDProductSalesRankRecordedEvent, OrderExportRequestedEventAFTER_COMMIT + @Async상품 랭킹, 외부 전송
메시지 전파OutboxMessageScheduled Relay → Kafka트랜잭션과 전파 분리

회고

성과

  • Outbox 기반의 Kafka 연동 구조를 도메인 이벤트로 연결
  • 도메인 책임(상태 전이) ↔ 후속 전파(메시징) 역할 분리
  • Kafka, ApplicationEvent, Scheduler, Consumer의 흐름을 하나의 시나리오로 통합

아쉬움

  • 단일 애플리케이션 구조에서 Kafka를 붙이다 보니 약간의 과도함
profile
Onward, Always Upward - 기록은 성장의 증거

0개의 댓글