EDA2026년 5월 26일21분 읽기

이벤트 드리븐 아키텍처 완벽 정리: 인프로세스부터 Kafka까지

동기 호출의 한계, EDA의 세 조건, Spring @EventListener, Kafka·RabbitMQ 구분, 멱등성, 코레오그래피 vs 오케스트레이션, 결과적 일관성, 실전 함정까지 한 번에 정리했다.

#EDA#EventDriven#Kafka#RabbitMQ#Spring#Architecture#MSA

이벤트 드리븐 아키텍처

EDA는 시스템 구성 요소 간 결합도를 낮추는 아키텍처 스타일이며, 그 대가로 시간적 결합과 일관성을 포기한다. 적용 여부는 항상 워크로드 의존적이며, 인프로세스부터 분산까지 동일한 원칙을 다른 비용으로 구현할 수 있다.


들어가며

"이벤트 드리븐"이라는 단어는 어디에나 등장한다. MSA 글에는 거의 빠지지 않고, Spring 강의에서도 자주 나오고, 시스템 디자인 인터뷰에서는 단골 주제다. 그런데 막상 정의를 물으면 답이 갈린다.

  • "Kafka 쓰면 EDA지?"
  • "메시지 큐를 쓰는 시스템 아닌가?"
  • "비동기 처리하면 EDA지?"

전부 정답이 아니다. EDA의 핵심은 도구도, 비동기성도 아니다. **"발신자가 수신자를 모르는 상태에서 과거형 도메인 사실을 발행하는 것"**이다. 이 정의를 잡아두면 인프로세스 Spring 이벤트도, Kafka도, DDD 도메인 이벤트도 같은 원리로 보인다.

이 글은 동기 호출의 한계라는 배경에서 출발해, EDA의 본질·구현 모델(인프로세스/분산)·메시지 브로커 분류·전달 보장·흐름 제어 패턴·일관성 모델·실전 함정까지 한 번에 정리한다.


목차

  • 1장. 배경: 동기 호출의 한계
  • 2장. 이벤트 드리븐의 본질
  • 3장. 구현 모델: 인프로세스와 분산
  • 4장. 메시지 브로커 분류
  • 5장. 전달 보장과 멱등성
  • 6장. 이벤트와 명령의 의미론
  • 7장. 흐름 제어 패턴
  • 8장. 결과적 일관성
  • 9장. 실전 함정과 대응
  • 10장. 결론
  • 부록 A. 자주 묻는 질문
  • 부록 B. 이벤트 루프와의 구분
  • 부록 C. 권장 학습 자료
  • 부록 D. 동일 원리를 공유하는 다른 패턴들

1장. 배경: 동기 호출의 한계

1.1 분산 시스템의 기본 통신 모델

마이크로서비스 환경에서 서비스 간 통신은 크게 두 모델로 나뉜다.

  • 동기 호출: REST API, gRPC. 호출 즉시 응답을 기다림
  • 비동기 메시지: 메시지 브로커를 통한 이벤트 발행/구독

1.2 직렬 동기 호출의 비용

주문 처리에 결제·재고·알림·적립금·배송 5개 서비스가 직렬 호출되는 경우를 가정해보자.

1.2.1 응답 시간의 합산

각 서비스의 응답 시간이 직렬로 누적되므로, 사용자가 체감하는 응답 시간은 모든 호출의 합이 된다.

1.2.2 가용성의 곱셈

각 서비스의 가용성이 99%일 때, 5개를 직렬로 호출하는 시스템의 전체 가용성은 다음과 같다.

text
0.99^5 ≈ 0.951 (95.1%)

가용성은 더해지지 않고 곱해진다. 의존 서비스가 늘어날수록 전체 가용성은 빠르게 감소한다.

1.2.3 장애 전파

호출 체인 중 하나가 지연되거나 실패하면 전체 요청이 실패한다. 회복성을 보장하려면 타임아웃·재시도·서킷 브레이커가 모든 호출 지점에 필요해진다.

1.3 결합도 문제

직렬 동기 호출은 본질적인 결합도를 강제한다.

  • 주문 서비스는 결제·재고·알림 서비스의 주소, API 스펙, 응답 형식을 모두 알아야 함
  • 알림 채널 추가(SMS, 카카오톡 등) 시 주문 서비스 코드 수정이 필요함
  • 분산 트랜잭션이 필요할 경우 2-Phase Commit 등의 무거운 메커니즘이 요구됨

1.4 EDA의 등장 동기

위 한계들을 완화하기 위해 등장한 것이 이벤트 드리븐 아키텍처다. 발신자는 "사실"을 발행하고, 그 사실에 관심 있는 수신자가 알아서 처리하는 구조로 결합도를 분리한다.


2장. 이벤트 드리븐의 본질

2.1 정의

이벤트 드리븐 아키텍처는 다음 세 조건을 모두 만족하는 아키텍처 스타일이다.

  1. 구성 요소 간 통신이 비동기 메시지로 이루어진다
  2. 메시지는 **과거에 일어난 도메인 사실(event)**이다
  3. 발신자는 수신자가 누구인지, 몇 명인지 알지 않는다

세 번째 조건이 가장 본질적이다. 메시지 브로커를 사용하더라도 발신자가 특정 수신자에게 작업을 요청하는 형태라면 그것은 비동기 RPC일 뿐 이벤트 드리븐이 아니다.

2.2 결합도와 책임의 역전

2.2.1 통신 모델 비교

항목REST 동기 호출이벤트 발행
발신자가 알아야 하는 정보수신자의 주소, API 스펙이벤트 스키마만
새 수신자 추가 비용발신자 코드 수정 필요수신자만 새로 추가
발신자–수신자 시간적 결합동시에 살아있어야 함분리 가능 (브로커가 버퍼링)

2.2.2 책임의 이전

동기 호출에서는 발신자가 "누구에게 무엇을 시킬지" 결정한다. 이벤트 드리븐에서는 발신자가 "무엇이 일어났는지"만 알리고, 그에 대응할 책임은 수신자에게 있다.

2.3 책임의 위치와 제어의 역전

EDA의 본질은 제어의 역전(Inversion of Control), 다른 이름으로 **헐리우드 원칙(Hollywood Principle)**으로 표현된다.

"Don't call us, we'll call you."

발행자가 수신자에게 "이걸 해라"라고 지시하는 것이 아니라, 수신자가 "나는 이런 일이 일어나면 반응하겠다"고 자기 책임 영역을 선언한다. 이 책임의 이전이 결합도가 풀리는 근본 원인이다.

이 원리는 EDA에만 국한되지 않는다. 옵저버 패턴, React useEffect, DOM addEventListener, Kubernetes 컨트롤러 등 다양한 영역에서 동일한 구조가 발견된다 (부록 D 참조).

2.4 트레이드오프

EDA는 결합도를 낮추는 대신 다음 비용을 새로 발생시킨다.

항목비용
즉시성비동기 처리로 인한 지연 발생
일관성결과적 일관성 수용 필요
디버깅흐름이 분산되어 추적이 어려움
운영 복잡도메시지 브로커 운영·모니터링 부담
도구 의존분산 트레이싱·스키마 레지스트리 등 필수 인프라

EDA는 결합도 감소가 위 비용을 상쇄할 만큼 가치 있는 경우에만 적합하다. 모든 시스템에 적용해야 하는 일반 해법이 아니다.

2.5 EDA와 아키텍처 스타일의 관계

EDA는 모놀리식·마이크로서비스(MSA) 등 아키텍처 스타일과 직교(orthogonal) 관계다. 한쪽이 다른 쪽을 강제하지 않는다.

2.5.1 4분면 매트릭스

동기/명령 통신이벤트 통신
모놀리식전통 Spring MVC, 모듈 간 직접 메서드 호출Spring @EventListener, DDD 도메인 이벤트
MSAREST/gRPC만 사용하는 마이크로서비스 (분산 모놀리식 위험)Kafka·RabbitMQ 기반 마이크로서비스 (전형적 EDA + MSA)

네 셀이 모두 독립적으로 존재 가능하며, 각각의 장단점이 있다.

2.5.2 MSA가 EDA를 (사실상) 요구하는 이유

MSA만 도입하고 EDA 없이 REST 동기 호출만 사용하면, 결합도는 그대로 유지된 채 네트워크 복잡도만 추가되는 "분산된 모놀리식(distributed monolith)" 안티패턴에 빠진다. MSA의 가장 큰 골칫거리(서비스 간 결합도)를 EDA가 해결해주는 구조이므로, 두 패턴이 자주 함께 등장한다.

다만 인과 관계는 일방향이다.

  • MSA → EDA: 거의 강제적. MSA는 EDA 없이는 그 가치를 실현하기 어려움
  • EDA → MSA: 강제 아님. 모놀리식 내부에서도 EDA는 완벽하게 유효함 (3장 참조)

3장. 구현 모델: 인프로세스와 분산

EDA의 세 조건(2.1절)을 만족하는 구현은 두 축으로 나뉜다. 동일한 원칙을 구현 비용과 도구만 달리하여 적용한 결과다.

3.1 두 가지 구현 축

대상 범위대표 도구주요 비용
인프로세스같은 프로세스/JVM 내부Spring ApplicationEventPublisher, Guava EventBus낮음 (라이브러리 수준)
분산서비스 간, 네트워크 경계 넘음Kafka, RabbitMQ, SQS 등 메시지 브로커높음 (브로커 운영, 직렬화, 네트워크)

두 구현 모두 "발신자가 수신자를 모름 + 과거형 도메인 사실 전달"이라는 본질을 공유한다. 차이는 도구일 뿐이다.

3.2 인프로세스 이벤트: Spring 사례

Spring은 초기부터 ApplicationEventPublisher / @EventListener로 인프로세스 EDA를 지원해왔다. DDD의 도메인 이벤트(Domain Event) 패턴이 일반적으로 이 형태로 구현된다.

3.2.1 직접 호출 (Before)

java
@Service
public class UserService {
    @Autowired private MailService mailService;
    @Autowired private PointService pointService;
    @Autowired private StatisticsService statisticsService;
    @Autowired private CouponService couponService;
    @Autowired private AnalyticsService analyticsService;
 
    @Transactional
    public User signup(SignupRequest req) {
        User user = userRepository.save(new User(req));
        mailService.sendWelcomeMail(user);
        pointService.giveSignupBonus(user);
        statisticsService.recordSignup(user);
        couponService.issueWelcomeCoupon(user);
        analyticsService.trackSignup(user);
        return user;
    }
}

3.2.2 이벤트 발행 (After)

java
@Service
public class UserService {
    @Autowired private ApplicationEventPublisher publisher;
 
    @Transactional
    public User signup(SignupRequest req) {
        User user = userRepository.save(new User(req));
        publisher.publishEvent(new UserSignedUpEvent(user));
        return user;
    }
}
 
@Component
public class WelcomeMailListener {
    @EventListener
    public void on(UserSignedUpEvent e) { /* 메일 발송 */ }
}
 
@Component
public class PointListener {
    @EventListener
    public void on(UserSignedUpEvent e) { /* 포인트 지급 */ }
}

두 코드 모두 인프로세스에서 동작하며 메시지 브로커를 사용하지 않는다. 그럼에도 EDA의 세 조건을 모두 만족한다.

3.3 인프로세스 이벤트의 이점

3.3.1 단일 책임 원칙 (SRP)

UserService가 "사용자 생성"이라는 핵심 책임만 가진다. 알림·통계·쿠폰 등 부수 효과가 분리되어 클래스가 비대해지지 않는다.

3.3.2 개방-폐쇄 원칙 (OCP): 변경 비용 감소

새로운 부수 기능(예: 카카오톡 알림) 추가 시 새 리스너만 추가하면 된다. 핵심 비즈니스 로직 파일은 수정되지 않는다. 핵심 로직의 변경 빈도를 낮추는 것이 장기적으로 가장 큰 가치다.

3.3.3 트랜잭션 경계 제어

@TransactionalEventListener로 트랜잭션 생명주기에 맞춘 실행 시점 제어가 가능하다 (3.4절 상세).

3.3.4 비동기 처리의 용이성

java
@Component
public class AnalyticsListener {
    @Async
    @EventListener
    public void on(UserSignedUpEvent e) {
        analyticsClient.track(e);
    }
}

@Async만 추가하면 별도 스레드 풀에서 실행된다. 회원가입 응답 시간이 외부 시스템 호출 시간의 영향을 받지 않는다.

3.3.5 테스트 용이성

UserService 단위 테스트에서 Mock 해야 할 의존성이 줄어든다. 핵심 로직과 부수 효과의 테스트를 독립적으로 작성할 수 있다.

3.3.6 순환 의존성 회피

A → B → C → A 형태의 순환 의존성을 한 지점에서 이벤트로 끊어 해소할 수 있다. Spring 3.0부터 순환 의존성을 기본적으로 차단하는 흐름과 부합한다.

3.3.7 횡단 관심사 분리

감사 로그, 캐시 무효화, 이벤트 소싱 같은 횡단 관심사를 도메인 코드에 침투시키지 않고 별도 리스너로 처리할 수 있다.

3.4 @TransactionalEventListener: 트랜잭션 경계 제어

인프로세스 EDA에서 가장 실용적인 기능 중 하나다. 트랜잭션 생명주기의 어느 시점에 리스너를 실행할지 명시적으로 제어한다.

java
@Component
public class WelcomeMailListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void on(UserSignedUpEvent e) {
        mailService.send(...);
    }
}

3.4.1 실행 시점 옵션

phase실행 시점주 용도
BEFORE_COMMIT커밋 직전같은 트랜잭션에 포함되어야 하는 DB 작업
AFTER_COMMIT (기본)커밋 후외부 호출 (메일, 외부 API) — 실전에서 가장 흔히 사용
AFTER_ROLLBACK롤백 후실패 알림, 보상 작업
AFTER_COMPLETION커밋/롤백 무관, 종료 후정리 작업

직접 호출 방식으로는 TransactionSynchronizationManager를 수동 호출하는 보일러플레이트가 필요하다.

3.5 인프로세스 EDA의 함정

3.5.1 흐름 추적의 어려움

IDE의 호출 추적(Call Hierarchy)만으로 흐름을 파악할 수 없다. 어떤 리스너가 등록되어 있는지 별도로 검색해야 하며, 동적 등록된 리스너는 정적 분석으로 찾기 어렵다.

3.5.2 동기 실행 시 효과의 제한

@TransactionalEventListener@Async 없이 단순 @EventListener만 사용하면, 발행자 스레드가 리스너 메서드를 직접 호출한다. 이 경우 결합도 분리 외의 효과(트랜잭션 분리, 비동기 처리)는 발생하지 않는다.

3.5.3 트랜잭션 컨텍스트 전파 문제

@Async @EventListener에서 DB 작업을 수행하면 부모 트랜잭션과 독립된 트랜잭션이 시작된다. 발행 시점의 데이터 일관성을 가정하면 안 된다.

3.5.4 이벤트 폭주 위험

리스너 안에서 또 다른 이벤트를 발행하는 연쇄가 통제되지 않으면 디버깅이 어려워진다. 이벤트 발행 깊이 제한·전체 이벤트 흐름 그래프 관리 등의 규칙이 필요하다.

3.5.5 순서 보장의 부재

동일 이벤트에 여러 리스너가 등록된 경우 실행 순서가 보장되지 않는다. @Order 어노테이션으로 명시 가능하지만, 순서에 의존하는 시점에 결합도가 부분적으로 복원된다.

3.6 인프로세스에서 분산으로: 마이그레이션 사다리

인프로세스 EDA의 장기적 가치는 분산 시스템으로의 진화 경로를 사전에 확보한다는 점이다.

3.6.1 진화 단계

text
[모놀리식, 직접 호출]
       ↓ 도메인 이벤트 도입
[모듈러 모놀리식, 인프로세스 이벤트]
       ↓ 결합도 낮은 모듈 분리
[하이브리드: 일부는 인프로세스, 일부는 분산]
       ↓ 전면 분리
[마이크로서비스, 분산 이벤트(Kafka 등)]

3.6.2 마이그레이션 시 변경 지점

java
// Before (인프로세스)
publisher.publishEvent(new UserSignedUpEvent(user));
 
// After (분산)
kafkaTemplate.send("user.signed-up", new UserSignedUpEvent(user));

이벤트 클래스 자체는 그대로 유지된다. 직렬화·역직렬화 인프라만 추가되고, 리스너는 Kafka 컨슈머로 옮겨가서 그대로 동작한다. 이벤트 클래스가 사실상 미래의 메시지 스키마 역할을 미리 수행한다.

이 전략은 Sam Newman의 Monolith to Microservices에서 이벤트 우선 모듈러 모놀리식(Event-first Modular Monolith) 패턴으로 다뤄진다.


4장. 메시지 브로커 분류

분산 EDA를 구현할 때 사용되는 메시지 브로커는 구현과 사용 모델에 따라 세 가지 진영으로 분류된다.

4.1 큐 모델 (Queue)

대표: RabbitMQ, AWS SQS, ActiveMQ

  • 메시지가 컨슈머에게 전달되어 ACK가 발생하면 삭제된다
  • 일반적으로 한 메시지 = 한 컨슈머 처리
  • 작업 분산(work distribution)에 적합
  • 비유: "택배 보관함" — 가져가면 비워짐

4.2 로그 모델 (Log)

대표: Apache Kafka, Apache Pulsar, Amazon Kinesis, Redis Streams

  • 메시지가 로그에 영속적으로 저장되며, 보존 기간 또는 용량 한도까지 유지된다
  • 여러 컨슈머 그룹이 각자 자기 위치(offset)를 기억하며 독립적으로 읽는다
  • 동일 이벤트를 N개의 팀이 각자 처리할 수 있다
  • 비유: "유튜브 영상" — 누가 시청해도 영상 자체는 남아 있음

4.3 펍/섭 모델 (Pub/Sub)

대표: Redis Pub/Sub, Google Cloud Pub/Sub, AWS SNS

  • 구독자만 메시지를 수신한다
  • Redis Pub/Sub는 영속성이 없어 컨슈머가 다운된 사이의 메시지는 손실된다
  • Google Cloud Pub/Sub은 영속성을 제공한다

4.4 Kafka와 RabbitMQ 비교

같은 카테고리로 묶이는 경우가 많으나, 두 시스템은 다른 카테고리에 속한다.

항목Kafka (로그)RabbitMQ (큐)
메시지 수명보존 기간 동안 유지컨슈머 ACK 시 삭제
컨슈머 모델컨슈머가 offset 관리, pull 방식브로커가 push, ACK 기반
라우팅토픽 + 파티션exchange + binding (복잡)
순서 보장파티션 단위로 강함큐 단위로 부분적
재처리offset을 되돌리면 재처리 가능어려움 (이미 ACK된 메시지는 소실)
처리량매우 높음 (수십만 msg/s)중간 (수만 msg/s)
적합 워크로드이벤트 스트림, 로그 수집작업 큐, 비동기 RPC

4.5 선택 기준

브로커 선택의 핵심 기준은 "한 이벤트를 몇 명이, 언제까지 봐야 하는가"다.

  • 한 명이 처리하고 종료 → RabbitMQ, SQS 등 큐 모델
  • 여러 컨슈머 그룹이 각자 보고, 일정 기간 후에도 재처리가 필요 → Kafka 등 로그 모델
  • 단순 알림성·휘발성 통신 → Pub/Sub 모델

5장. 전달 보장과 멱등성

5.1 전달 보장의 세 수준

수준의미특성
at-most-once최대 한 번중복 없음, 손실 가능
at-least-once최소 한 번손실 없음, 중복 가능
exactly-once정확히 한 번이상적이나 실현 어려움

실전에서 가장 흔히 사용되는 수준은 at-least-once다.

5.2 exactly-once의 한계

Kafka는 exactly-once semantics(EOS)를 지원한다고 명시하지만, 다음 제약이 있다.

  • Kafka 내부의 producer–broker–consumer 사이클에서만 보장된다
  • 컨슈머가 외부 시스템(데이터베이스, 외부 API)에 부수효과를 일으키는 순간 보장이 끝난다
  • 즉, "메시지를 한 번만 받는" 것은 가능하더라도, "외부에 한 번만 영향을 주는" 것은 별도 설계가 필요하다

따라서 실전 표준은 at-least-once 전달 + 멱등 컨슈머 조합이다.

5.3 멱등성 패턴

5.3.1 이벤트 ID 기반 중복 제거

처리한 이벤트 ID를 별도 테이블에 기록하고, 동일 ID 재수신 시 무시한다.

sql
INSERT INTO processed_events (event_id, processed_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING;

5.3.2 상태 머신 기반

도메인 객체의 상태 전이를 제한하여, 같은 전이를 두 번 시도하면 자동으로 무시되게 한다.

text
결제 대기 → 결제 완료     (허용)
결제 완료 → 결제 완료     (무시 또는 오류)

5.3.3 자연 멱등 연산

연산 자체가 멱등성을 갖도록 설계한다.

  • 멱등: SET balance = 1000 (몇 번 실행해도 결과 동일)
  • 비멱등: INCREMENT balance BY 100

5.4 핵심 원칙

이벤트 컨슈머 설계 시 첫 번째로 검토할 사항은 "이 처리가 멱등한가"다. 멱등하지 않다면, 어떤 메커니즘으로 멱등하게 만들지를 명시적으로 설계해야 한다.


6장. 이벤트와 명령의 의미론

6.1 정의의 차이

구분명령 (Command)이벤트 (Event)
시제명령형 (ProcessPayment)과거형 (PaymentProcessed)
의도"이것을 수행하라""이런 일이 일어났다"
수신자 거부 가능 여부가능 (검증 실패 시)불가능 (이미 일어난 일)
일반적 수신자 수1명 (발신자가 누구인지 알고 보냄)N명 (발신자는 누가 듣는지 모름)
결합도발신자가 수신자를 인지발신자가 수신자를 모름

6.2 명명 규약

이벤트는 과거형 + 도메인 사실 형태로 명명한다.

  • 권장: OrderPlaced, PaymentApproved, StockReserved
  • 비권장: SendNotification, UpdateInventory, ProcessPayment

비권장 사례의 문제는 이벤트가 아닌 명령을 메시지 브로커에 실어 보내는 형태로, 비동기 RPC와 다르지 않다. 발신자가 여전히 수신자의 책임을 인지하고 있기 때문이다.

6.3 혼용 패턴

명령과 이벤트는 상호 배타적이지 않고, 의도에 따라 선택적으로 사용된다.

  • 외부 요청에 의해 사용자 의도를 표현하는 동작 → 명령 (SubmitOrder)
  • 그 결과 발생한 도메인 사실 → 이벤트 (OrderPlaced, OrderRejected)

일반적인 흐름은 이렇다.

text
[클라이언트] ─명령(SubmitOrder)─> [주문 서비스]
                                      │
                                      ├─ 검증 후 OrderPlaced 이벤트 발행
                                      │
                                      ▼
                                  [브로커] ─> 결제, 재고, 알림 컨슈머

7장. 흐름 제어 패턴

여러 이벤트로 구성된 비즈니스 흐름을 조정하는 방식은 두 가지로 나뉜다.

7.1 코레오그래피 (Choreography)

각 서비스가 이벤트를 듣고 독립적으로 행동하며, 중앙 제어자가 없다.

text
OrderPlaced
  ├→ 결제 서비스: 결제 처리, PaymentApproved 발행
  ├→ 재고 서비스: 재고 예약, StockReserved 발행
  └→ 알림 서비스: 이메일 발송

7.2 오케스트레이션 (Orchestration)

중앙 코디네이터(오케스트레이터)가 흐름을 제어한다. 보통 상태 머신 형태로 구현되며, 사가(Saga) 패턴의 한 형태다.

text
오케스트레이터:
  1. 결제 서비스에 결제 명령 전송 → 응답 대기
  2. 결제 성공 시 재고 서비스에 예약 명령 전송 → 응답 대기
  3. 재고 예약 성공 시 알림 서비스에 발송 명령 전송
  실패 시: 보상 트랜잭션 수행 (결제 환불, 재고 복구)

7.3 두 패턴 비교

코레오그래피오케스트레이션
결합도매우 낮음오케스트레이터가 흐름 인지 (중간)
가시성낮음 (흐름이 분산)높음 (중앙에서 추적 가능)
변경 용이성새 컨슈머 추가가 쉬움흐름 변경 시 오케스트레이터 수정 필요
디버깅어려움 (분산 트레이싱 필수)비교적 용이
적합 상황단순 파이프라인, 알림성 처리복잡한 비즈니스 트랜잭션

7.4 선택 기준

  • 단순 파이프라인(예: 가입 후 환영 메일·쿠폰 발급·통계 집계) → 코레오그래피
  • 복잡한 비즈니스 트랜잭션(예: 결제·재고·배송이 얽힌 주문 처리) → 오케스트레이션

코레오그래피가 결합도 면에서 우월하지만, 흐름이 복잡해지면 전체 흐름을 단일 지점에서 파악하기 어려워진다. 보상 트랜잭션이 필요한 결제·환불 등 핵심 트랜잭션은 오케스트레이션이 권장된다.


8장. 결과적 일관성

8.1 정의

이벤트 드리븐 시스템에서는 어떤 도메인 변경 사항이 시스템 전체에 반영되기까지 시간차가 발생한다. 이 시간차 동안 시스템의 각 부분이 서로 다른 상태를 보일 수 있으며, 최종적으로는 동일한 상태로 수렴한다. 이를 **결과적 일관성(Eventual Consistency)**이라 한다.

ACID 트랜잭션이 보장하는 **즉시 일관성(Strong Consistency)**과 대비된다.

8.2 예시

text
1. 사용자가 주문 → 주문 서비스가 OrderPlaced 이벤트 발행
2. 마이페이지 서비스가 해당 이벤트를 수신하여 자기 DB 업데이트
3. 위 두 단계 사이 수 밀리초~수 초의 지연 존재
4. 사용자가 즉시 마이페이지에 접속하면 주문 내역이 보이지 않을 수 있음
5. 수 초 후 새로고침 시 정상 표시

8.3 비즈니스 관점

결과적 일관성은 버그가 아닌 의도된 트레이드오프다.

  • 강한 일관성을 요구하면 분산 트랜잭션(2PC 등)이 필요하며, 처리 속도와 확장성이 크게 저하된다
  • 결과적 일관성은 빠르고 확장 가능하지만, 짧은 시간 동안 시스템 부분 간 상태 불일치를 허용해야 한다
  • 비즈니스적으로 수용 가능한지가 적용 여부의 판단 기준이다

수용 불가능한 사례: 은행 계좌 이체 — 보낸 사람의 잔액이 줄었는데 받은 사람의 잔액이 즉시 늘지 않으면 문제

수용 가능한 사례: 쇼핑몰 주문 내역 표시 지연, 추천 시스템 업데이트 지연

8.4 UX 보완 패턴

8.4.1 Read-your-writes 일관성

쓰기 직후 일정 시간 동안 해당 사용자에게만 강한 일관성을 보장한다. 캐시 무효화나 세션 단위 라우팅으로 구현한다.

8.4.2 낙관적 UI (Optimistic UI)

클라이언트가 서버 응답을 기다리지 않고 작업이 성공한 것처럼 화면을 갱신한다. 실제 결과가 도착하면 필요시 보정한다.


9장. 실전 함정과 대응

9.1 이벤트 폭주 (Event Storm)

9.1.1 문제

하나의 이벤트가 N개의 후속 이벤트를 발생시키고, 그것이 다시 M개의 이벤트를 발생시키는 식의 연쇄로 시스템 부하가 지수적으로 증가한다.

9.1.2 대응

  • 이벤트의 책임 범위를 작게 유지: "도메인 사실 1개 = 이벤트 1개"
  • 이벤트 발행 빈도 모니터링 및 알람 설정
  • 필요시 이벤트 집계(aggregation) 또는 디바운싱(debouncing) 적용

9.2 순서 보장

9.2.1 문제

같은 엔티티에 대한 이벤트가 순서대로 수신되지 않으면 잘못된 상태가 형성될 수 있다. 예: OrderPlaced 다음 OrderCancelled인데 컨슈머가 역순으로 수신한 경우.

9.2.2 대응

  • 파티션 키로 같은 엔티티의 이벤트는 같은 파티션에 배치 (Kafka의 경우)
  • 이벤트에 시퀀스 번호 또는 버전 번호 포함, 컨슈머가 검증
  • 멱등성과 상태 머신을 결합하여 순서와 무관하게 처리 가능하도록 설계

9.3 스키마 진화

9.3.1 문제

이벤트 스키마는 시간이 지나면서 변경되지만, 보존된 과거 이벤트와 다양한 컨슈머 버전이 공존하는 환경에서는 호환성 관리가 어렵다.

9.3.2 대응

  • 후방 호환(backward compatible) 변경만 허용
    • 허용: 새 필드 추가 (기본값 포함)
    • 금지: 필드 제거, 타입 변경, 의미 변경
  • 스키마 레지스트리 도입 (Confluent Schema Registry, Apicurio 등)
  • 메이저 변경이 불가피하면 새 이벤트 타입(OrderPlaced.v2)으로 분리

9.4 분산 디버깅

9.4.1 문제

한 사용자 요청이 다수 서비스를 거치며 처리되므로, 장애나 오작동의 원인을 추적하려면 모든 서비스의 로그를 종합해야 한다.

9.4.2 대응

  • 분산 트레이싱 도입 (Jaeger, Zipkin, OpenTelemetry, Datadog APM 등)
  • 이벤트에 trace ID 전파 (W3C Trace Context 등 표준 사용)
  • 데드 레터 큐(DLQ)로 실패한 이벤트를 격리하여 별도 분석

9.5 트랜잭션 대체 시도라는 오해

9.5.1 문제

EDA를 분산 트랜잭션의 대체 수단으로 인식하면 잘못된 설계로 이어진다.

9.5.2 사실

EDA는 트랜잭션을 포기하고 결과적 일관성을 받아들이는 모델이다. 진정한 분산 트랜잭션이 필요한 경우 사가 패턴과 보상 트랜잭션을 명시적으로 설계해야 하며, 일부 시나리오에서는 강한 일관성을 위해 동기 모델을 유지하는 편이 적절하다.


10장. 결론

본 글은 이벤트 드리븐 아키텍처를 동기 호출의 한계로부터 출발하여 본질·구현 모델·구성 요소·흐름 패턴·일관성 모델·실전 함정까지 체계적으로 정리했다.

10.1 본질의 위치

EDA의 핵심은 "메시지 브로커를 사용하는가"가 아니라 다음 세 조건의 동시 충족이다.

  1. 비동기 메시지를 통한 통신
  2. 메시지가 과거 도메인 사실(이벤트)
  3. 발신자–수신자 간 인지적 분리

이 본질은 인프로세스 구현(Spring @EventListener)에서도, 분산 구현(Kafka)에서도 동일하게 성립한다.

10.2 아키텍처와의 직교성

EDA는 모놀리식·MSA 등 아키텍처 스타일과 독립적이다. MSA는 EDA를 사실상 요구하지만, EDA는 MSA를 요구하지 않는다. 모놀리식 내부의 도메인 이벤트는 EDA의 완전한 형태다.

10.3 트레이드오프의 인식

EDA는 결합도를 낮추는 대가로 즉시성·일관성·디버깅 용이성·운영 단순성을 포기하는 모델이다. 적용 여부는 워크로드와 조직 구조에 따라 결정되어야 하며, 모든 시스템에 일률적으로 적용할 일반 해법이 아니다.

10.4 실전 표준

  • 구현 모델: 작은 시스템은 인프로세스, 도메인 경계가 명확해지면 분산으로 진화
  • 전달 보장: at-least-once + 멱등 컨슈머
  • 명명 규약: 과거형 도메인 사실
  • 흐름 제어: 단순 파이프라인은 코레오그래피, 복잡 트랜잭션은 오케스트레이션
  • 일관성 모델: 결과적 일관성 수용 + UX 보완 패턴

10.5 한 문장 요약

EDA는 시스템 구성 요소 간 결합도를 낮추는 아키텍처 스타일이며, 그 대가로 시간적 결합과 일관성을 포기한다. 적용 여부는 항상 워크로드 의존적이며, 인프로세스부터 분산까지 동일한 원칙을 다른 비용으로 구현할 수 있다.


부록 A. 자주 묻는 질문

Q1. 이벤트 드리븐과 메시지 큐는 같은 개념인가요? A. 다르다. 메시지 큐는 구현 도구이며, 이벤트 드리븐은 아키텍처 스타일이다. 메시지 큐를 사용해 비동기 RPC만 수행해도 그것은 이벤트 드리븐이 아니다.

Q2. Kafka를 사용하면 이벤트 드리븐인가요? A. 아니다. Kafka로 명령(ProcessPayment 등)을 전달하는 형태는 비동기 RPC다. 이벤트 드리븐의 본질은 과거형 도메인 사실의 발행과 발신자–수신자의 인지적 분리이지, 특정 도구의 사용 여부가 아니다.

Q3. 모놀리식 시스템에서도 EDA가 가능한가요? A. 가능하다. Spring ApplicationEventPublisher / @EventListener로 인프로세스 이벤트 버스를 구현하면 EDA의 세 조건을 모두 만족한다. 메시지 브로커는 EDA의 필요조건이 아니다.

Q4. 메시지 브로커 없이 EDA를 도입하는 것의 가치는? A. 결합도 분리, 단일 책임 원칙 준수, 트랜잭션 경계 제어, 비동기 처리, 테스트 용이성, 횡단 관심사 분리, 그리고 향후 MSA 전환 시의 마이그레이션 비용 감소가 주요 가치다.

Q5. EDA는 MSA에서만 의미가 있나요? A. 아니다. EDA와 MSA는 직교 개념이다. MSA는 EDA를 사실상 요구하지만, 반대는 성립하지 않는다.

Q6. exactly-once 전달이 가능한가요? A. 메시지 브로커 내부 사이클에서는 가능하나, 외부 시스템에 부수효과가 발생하는 순간 보장이 끝난다. 실전에서는 at-least-once + 멱등 컨슈머가 표준이다.

Q7. 결과적 일관성을 사용자가 어떻게 인지하나요? A. 짧은 시간 동안 시스템의 일부가 다른 상태를 보인다. 비즈니스적 수용 가능 여부가 적용 기준이며, UX 패턴(낙관적 UI, 새로고침 유도, read-your-writes 캐시)으로 보완할 수 있다.

Q8. 코레오그래피와 오케스트레이션 중 무엇을 선택해야 하나요? A. 워크로드에 따라 다르다. 단순 파이프라인은 코레오그래피가, 보상 트랜잭션이 필요한 복잡 비즈니스 흐름은 오케스트레이션(사가 패턴)이 적합하다.


부록 B. 이벤트 루프와의 구분

"이벤트"라는 단어를 공유하지만 서로 다른 층위의 개념이다.

항목이벤트 루프이벤트 드리븐 아키텍처
다루는 범위단일 프로세스 내부시스템 구성 요소 간 통신
핵심 문제단일 스레드로 다수 I/O를 효율적으로 처리구성 요소 간 결합도와 확장성
핵심 메커니즘epoll/kqueue + 콜백 디스패치이벤트 발행/구독 (인프로세스 또는 분산)
대표 사례Node.js, Nginx, Redis 내부Spring @EventListener, Kafka 기반 시스템
관련 글이벤트 루프 아키텍처 — Node.js를 중심으로본 글

부록 C. 권장 학습 자료

  • 도서: Martin Kleppmann, Designing Data-Intensive Applications — 특히 11장(스트림 처리)
  • 도서: Sam Newman, Monolith to Microservices — 이벤트 우선 분리 전략
  • 도서: Vaughn Vernon, Implementing Domain-Driven Design — 도메인 이벤트 패턴
  • 블로그: Confluent 공식 블로그의 Event-Driven Architecture 시리즈
  • 실습: 로컬 환경에서 Kafka + 토픽 2개 + 컨슈머 그룹 2개를 구성하고 offset 변경 실험
  • 실습: Spring Boot에서 ApplicationEventPublisher + @TransactionalEventListener 조합 구현
  • 방법론: Event Storming 워크샵 — 도메인 이벤트 발굴 기법
  • 사양: AsyncAPI Specification — 이벤트 기반 시스템 문서화 표준

부록 D. 동일 원리를 공유하는 다른 패턴들

EDA의 핵심 원리인 **제어의 역전(Inversion of Control)**과 발행자–수신자 분리는 다양한 영역에서 동일한 구조로 나타난다.

패턴영역발행구독
Observer 패턴 (GoF)객체 지향 설계subject.notify()observer.update() 등록
Spring Application EventJVM 인프로세스publisher.publishEvent()@EventListener
DOM Event브라우저element.dispatchEvent()addEventListener()
React useEffectUI 상태 변화상태 변경의존성 배열로 반응 등록
데이터베이스 트리거RDBMSINSERT/UPDATE/DELETECREATE TRIGGER
Kubernetes 컨트롤러컨테이너 오케스트레이션리소스 상태 변경Reconcile 루프
메시지 브로커 (Kafka 등)분산 시스템producer.send()컨슈머 그룹 구독

공통 구조는 이렇게 요약된다.

"무언가 일어났음을 알린다 → 관심 있는 측이 자기 책임으로 반응한다."

이 원리를 한 영역에서 익히면 다른 영역으로 자연스럽게 전이된다. EDA는 이 원리를 시스템 아키텍처 수준에 적용한 형태다.


마치며

EDA를 요약하면 이렇다. "발신자가 수신자를 모르게 만들어, 시간이 지나도 코드가 부드럽게 진화하도록 하는 패턴."

처음 EDA를 만나면 도구 — Kafka, RabbitMQ, SQS — 가 먼저 눈에 들어온다. 그런데 도구를 쓴다고 EDA가 되지 않는다. 발신자가 여전히 "결제 서비스가 이걸 처리하면 좋겠는데"라고 생각하면서 Kafka 토픽에 명령을 던지면, 그건 비동기 RPC일 뿐이다.

반대로 메시지 브로커 없이 Spring @EventListener만 써도, "주문이 일어났다"는 사실만 발행하고 누가 듣든 신경 쓰지 않으면 그건 완전한 EDA다. 도구가 아니라 책임의 위치가 본질이다.

이 글을 처음부터 끝까지 보면 양이 많지만, 한번 잡아두면 새로운 기술을 만났을 때 — Redis Streams든, Temporal이든, Kubernetes 컨트롤러든 — "아, 이것도 발행/구독 패턴이구나" 하고 빠르게 위치를 잡을 수 있다. 부록 D의 표가 그 지도다.

#EDA#EventDriven#Kafka#RabbitMQ#Spring#Architecture#MSA

황호민

Backend Engineer · Java/Kotlin · Spring Boot · Next.js