현대의 복잡한 분산 시스템에서 '누가 누구에게 직접 요청을 보내느냐'보다, '어떤 일이 발생했고, 그에 어떻게 반응할 것인가'가 더 중요해지고 있습니다. 이러한 흐름에서 중심에 있는 것이 바로 'Event-Driven Architecture (EDA)'입니다.
이번 글에는 Event-Driven Architecture의 개념과 구성 요소, 장단점 그리고 Martin Fowler가 정리하는 4가지 Event-Driven 패턴을 정리하겠습니다.
📌 Event-Driven Architecture란?
Event-Driven Atchitecture(EDA)는 시스템 내에서 상태 변화나 행위가 발생할 때 이를 이벤트(Event)로 정의하고, 해당 이벤트를 기반으로 다른 서비스나 컴포넌트들이 반응하도록 구성하는 소프트웨어 아키텍처입니다.
🧱 EDA 구성 요소
이벤트는 다음과 같은 구조로 흐릅니다:
[Event Producer] ---> [Event Broker] ---> [Event Consumer]
1. Producer
- 이벤트를 발생시키는 주체
- Producer는 자신이 이벤트를 누구에게 보내는지는 알지 못하며, 단지 메시지 브로커로 이벤트를 발행(Publish)할 뿐입니다.
2. Broker
- 이벤트를 전달하는 중개자
- Event Broker는 큐(Queue) 역할을 하기에, Message Queue라고도 합니다.
- Producer가 발행한 이벤트를 받아서, 해당 이벤트를 구독한 Consumer에게 전달하는 역할을 합니다.
- 대표적인 브로커 기술: Kafka, Pulsar 등
💡 Message Broker와 Event Broker는 다른 말인가?
Event Broker는 Message Broker보다 한 단계 확장된 개념입니다. 단순히 메시지를 1회 소비하는 구조가 아닌, 이벤트를 로그 형태로 저장하고, 메시지마다 고유한 인덱스(offset)를 부여합니다. 이 offset 덕분에 각 Consumer는 자신의 위치를 기억하고, 이벤트를 다시 읽거나(재처리), 순서대로 처리하거나, 여러 Consumer가 각각 읽는 것도 가능해집니다.
대표적으로 Kafka, Pulsar는 Event Broker에 해당하며, EDA에서는 주로 Kafka를 사용해 이벤트 스트림을 안정적으로 처리합니다. 반면 RabbitMQ 같은 전통적인 Message Broker는 이러한 기능이 기본적으로 제공되지 않습니다.
하지만 일반적으로 Message Broker와 Event Broker를 엄밀히 구분하지 않고 혼용하는 경우가 많습니다. Kafka 같은 경우도 Event Broker로 사용되지만, 메시지를 전달하는 역할도 수행할 수 있기 때문에 Message Broker로 분류되기도 합니다.
따라서 용어보다는 어떤 아키텍처에 어떻게 활용하느냐에 따라 그 시스템의 역할을 이해하는 것이 중요합니다.
3. Consumer
- 이벤트를 구독(Subscribe)하고, 이를 처리하는 주체 (ex. 결제, 알림)
- 각 소비자(Consumer)는 자신이 관심 있는 이벤트만 구독하여, 해당 이벤트가 도착했을 대 정의된 로직을 실행합니다.
이 구조는 시스템 간 결합도를 낮추고, 비동기적 흐름을 가능하게 하며, 트래픽 증가에 유연하게 대응할 수 있는 기반을 마련해줍니다.
✅ EDA의 장점
1. 비동기 처리로 병목 제거
- 서비스 간 통신을 동기 호출이 아닌 비동기 메시지로 처리함으로써, 응답 지연으로 인한 병목현상을 줄일 수 있습니다. 이는 실시간 처리에 유리합니다.
2. 서비스 간 느슨한 결합 (Loose Coupling)
- Producer는 이벤트만 발행하고, 어떤 Consumer가 이를 처리하는지 모릅니다.
- 이로 인해 서비스 독립성이 보장되고, 신규 기능 추가나 유지보수가 쉬워집니다.
3. 확장성과 유연한 배포
- Consumer 단위를 독립적으로 확장하거나 배포할 수 있어, 특정 기능에만 리소스를 집중할 수 있습니다.
- 높은 부하가 예상되는 이벤트만 따로 처리 성능을 확장 가능합니다.
4. 장애 격리 및 복원력 향상
- Consumer가 일시적으로 다운되어도 메시지는 큐에 남아 처리 대기 → 전체 시스템은 다운되지 않음.
- 복원력(Resilience) 있는 시스템 구성에 적합합니다.
💡 EDA의 특징은 MSA(Microservices Architecture)와 잘 어울립니다.
MSA는 시스템을 기능 단위로 작게 나누고, 각 서비스를 독립적으로 운영하는 방식입니다. 이런 구조에서는 서비스 간 통신이 많아질 수 밖에 없고, 그로 인한 복잡성, 장애 전파, 배포 간섭 등의 문제가 자주 발생합니다.
EDA는 앞서 언급한 장점들인 비동기 처리, 느슨한 결합, 독립적 확장, 장애 격리 등을 통해 MSA의 구조적 약점을 보완하며, 전체 시스템의 유연성과 안정성을 높이는 데 큰 역할을 합니다.
⚠️ EDA의 단점
1. 복잡한 에러 처리
- 비동기 구조에서는 에러가 발생해도 즉시 인지하거나 복구하기 어렵습니다.
- Dead Letter Queue 정책(처리 되지 않은 이벤트 모아놓음) 등 별도 처리 전략이 필요합니다.
2. 이벤트 순서 및 정합성 문제
- Kafka나 RabbitMQ는 특정 파티션 내 순서는 보장하지만, 여러 Consumer 간 이벤트 처리 순서를 제어하는 것이 어렵습니다.
- 데이터 불일치(Consistency Issue) 가능성 존재
3. 분산 추적 및 모니터링의 어려움
- 직접적인 호출 관계가 없다 보니, 전체 트랜잭션 흐름 파악이 어렵습니다.
- 분산 트레이싱 시스템(예: Jaeger, OpenTelemetry)이 필수적
4. 운영 복잡성
- 메시지 브로커 운영, 메시지 포맷 관리, 중복 수신/재처리 방지 등 인프라와 운영 비용이 증가합니다.
- 특히 데이터 모델이 자주 바뀌는 조직에서는 이벤트 버전 관리가 큰 과제가 합니다.
📌 이벤트를 다루는 방식 (by. Martin Fowler)
Martin Fowler가 강조한 핵심은 다음과 같습니다.
"Event-Driven 이라는 말 하나로는 충분하지 않다.
이벤트가 단순 알림인지, 상태 전달자인지, 혹은 전체 상태를 표현하는 이력인지 명확히 구분해야 한다."
Martin Fowler는 'Event-Driven Architecture'라는 용어가 다양한 문맥에서 혼용되는 문제를 지적하며, 이를 4가지 방식으로 정리했습니다. 각각의 접근은 이벤트가 어떤 역할을 하는지, 그리고 어떤 구조적 책임을 가지는지에 따라 구분됩니다.
분류 | 이벤트 내용 | 소비자 역할 | 특징 요약 |
1. Event Notification | 알림만 전달 | 직접 데이터 조회 | 간단하지만 의존도 존재 |
2. Event-Carried State Transfer | 데이터 상태 포함 | 조회 없이 자체 처리 가능 | 효율적이나 데이터 중복 가능성 있음 |
3. Event Sourcing | 이벤트 로그 자체가 원천 | 이벤트 재생으로 상태 계산 | 완전 추적 가능, 복잡함 |
4. CQRS | 명령/조회 분리 구조 | 쓰기→이벤트→읽기모델 갱신 | 대규모 시스템에서 유리함 |
1. Event Notification (이벤트 알림)
- 단순히 '무언가 발생했다'는 최소한 사실만 전달합니다.
- 예시: '주문 완료'라는 이벤트가 발행되지만, 실제 주문 정보는 포함되지 않습니다. 이런 경우 소비자(Consumer)는 별도로 데이터 조회(API 호출 등)해서 필요한 상태를 알아내야 합니다.
- 이벤트가 작고 간단하며, 시스템 간 결합도가 낮습니다. 대신 Consumer가 데이터를 다시 조회해야 해서 시스템 간 통신이 증가할 수 있습니다.
2. Event-Carried State Transfer (상태를 포함한 이벤트 전달)
- 이벤트 안에 변경된 상태 값 자체를 포함하여 전송합니다.
- 예시: '주문 완료'라는 이벤트에 주문 상세 정보(payload)가 함께 포함됩니다. 소비자는 이벤트만으로 필요한 처리를 자체적으로 완료 가능합니다.
- Consumer가 추가 조회 없이 독립적으로 처리가 가능하여, 느슨한 결합을 유지하면서도 실용적입니다. 대신 데이터 중복 발생 가능성이 있고, 데이터 정합성과 버전 관리가 어려울 수 있습니다.
3. Event Sourcing (이벤트 소싱)
- 애플리케이션의 최종 상태를 저장하는 대신, 상태 변화를 일으킨 모든 이벤트 자체의 로그를 저장하는 방식입니다.
- 예시: 계좌에서 입금과 출금을 할 때, '입금', '출금'만 저장하고, 현재 잔액은 이벤트 재생으로 계산합니다.
- 변경 이력을 완벽히 추적이 가능하여 감사 및 재처리가 용이합니다. 또한 Rollback, Replay가 가능합니다. 하지만 조회 시 이벤트 재생이 필요하기 때문에 성능이 안 좋아질 수 있고, 쿼리 로직이 복잡하다는 단점이 있습니다.
4. Command Query Responsibility Segregation (CQRS)
- 읽기(조회)와 쓰기(명령)를 서로 다른 모델로 분리하는 방식입니다. 이벤트는 쓰기 모델에서 발생하고, 이를 바탕으로 읽기 모델을 업데이트합니다.
- Read DB를 따로 구성하여 읽기 성능을 최적화할 수 있습니다. 이벤트 소싱과 함께 사용 시, 강력한 추적 및 확장 구조 장점이 있습니다. 대신 시스템 설계와 운영이 복잡해지고, 정합성 유지를 위한 eventual consistency 구조가 필요합니다.