이벤트 기반 아키텍처(Event-Driven Architecture)
6분 읽기
이벤트 기반 아키텍처(Event-Driven Architecture)
이벤트 기반 아키텍처(EDA)는 이벤트를 중심으로 시스템 간의 상호작용을 처리하도록 설계된 소프트웨어 아키텍처 패턴으로, 주로 대규모 시스템의 구성요소(components) 간 또는 서로 다른 시스템 간의 통신을 처리하는 데 사용된다.
이벤트 기반 아키텍처의 비동기 통신
이벤트 발행 → 캡처 → 처리 → * 저장(event sourcing)
전통적인 Request/Response 패턴은 클라이언트가 서버로 직접 요청을 보내면 서버가 이에 대한 응답을 반환하는 방식으로 동작한다. 이러한 동기적(synchronous) 방식은 즉각적인 응답이 필요하거나, 상태를 동기적으로 확인해야 하는 경우(예: 사용자 인증 요청, 파일 업로드 등)에 적합하다.
그러나, 과정에서 유저가 굳이 즉각적인 정보를 돌려받을 필요가 없더라도 응답이 완료될 때까지 처리 흐름이 멈추고, 불필요한 대기가 발생한다. 더불어, 서비스 간 강한 결합(tight coupling)으로 인해 한 서비스의 장애나 응답 지연이 전체 시스템에 영향을 미칠 수 있으며, 응답성과 확장성 측면에서 한계를 가질 수 있다.
반면, 이벤트 기반 아키텍처는 상태 변화가 발생하면 서버로 직접 요청을 보내는 대신 이벤트를 발행하고, 이를 수신한 서비스가 이벤트를 소비하는 독립적인 방식으로 동작한다. 이러한 비동기적(asynchronous) 방식은 실시간 데이터 처리에 적합하며, 전반적인 시스템 응답성을 높일 수 있다. 또한, 시스템 구성요소 간의 의존도를 낮추고, 느슨한 결합(loose coupling)을 유지하기 때문에 서비스 간에 발생하는 장애나 응답 지연이 다른 서비스에 영향을 미치지 않아 확장성 및 부하 분산 처리에 유리하다.
이벤트(Event)
시스템 내에서 발생하는 상태 변화를 나타내는 데이터 단위로, 변경되지 않는 불변한 기록(immutable record)이다.
이벤트는 무엇이 변화했는지에 대한 정보를 제공할 뿐, 어떻게 반응해야 할지에 대해서는 명시하지 않는다.
이벤트의 표현
이벤트는 일반적으로 JSON, XML 등의 구조화된 형식으로 표현되며, 고유 식별자, 유형, 타임스탬프, * 데이터(payload)와 같은 요소를 포함한다. 이벤트의 정의는 생성하는 서비스와 이벤트 유형에 따라 달라질 수 있다.
{
"eventId": "995eb27e-d458-4557-b40a-1c25e350abd3",
"eventType": "OrderPlaced",
"timestamp": "2025-01-21T10:00:00Z",
"data": {
"orderId": "ORD12345",
"userId": "CUST98765",
"amount": 999
}
}-
그럼, 이벤트는 어디로부터 발생할까?
- User-Driven: 직접적인 사용자 행동에 의해 트리거된 이벤트
- System-Driven: 데이터 업데이트, 결제 처리 완료 또는 예약된 작업과 같은 내부 시스템 프로세스에 의해 트리거된 이벤트
핵심 구성요소(Key Components)
추상화 레벨에서 정의한 핵심 구성요소는 다음과 같다.
각 구성요소는 서로의 내부 상태나 동작을 알 필요 없이, 자신에게 주어진 역할에만 집중한다.
-
이벤트 생성자(Event Producers)
이벤트를 발행하는 역할 -
이벤트 소비자(Event Consumers)
발행된 이벤트를 수신하여 처리하는 역할 -
이벤트 라우터(Event Routers)
발행된 이벤트를 적절한 소비자에게 전달하는 역할
토폴로지(Topology)
시스템의 전반적인 구조와 구성 요소들이 어떻게 연결되고 상호작용할 것인가?
- 브로커 토폴로지(Broker Topology)
- 중개자 토폴로지(Mediator Topology)
브로커 토폴로지(Broker Topology)
독립적이고 분산적으로
브로커 토폴로지는 중앙에서 이벤트를 조정하지 않고, 경량 메시지 브로커(예: Kafka, RabbitMQ 등)를 통해 이벤트를 분산적으로 중개한다. 이벤트 처리 흐름이 단순하거나 중앙에서 이벤트를 조정할 필요가 없는 경우 적합하며, 각 소비자가 독립적으로 작동하므로 분산 시스템에서 여러 서비스가 독립적으로 작동해야 하는 상황에서 유용하다.
구성요소
- 이벤트(Event)
- 이벤트 브로커(Event Broker)
- 이벤트 채널(Event Channel): 메시지 큐, 메시지 토픽, 또는 둘의 조합
- 이벤트 프로세서(Event Processor)
브로커 토폴로지에서 모든 이벤트 프로세서는 고도로 분리되어 있으며, 각 프로세서는 브로커를 통해 전달된 이벤트만 처리하고 서로 독립적으로 움직인다. '릴레이 경주' 같다고 생각하면 이해가 빠르다. 릴레이 경주는 주자가 바통을 들고 일정 거리를 달리고, 마지막 주자가 결승선을 통과할 때까지 다음 주자에게 바통을 넘겨준다. 이벤트 프로세서는 이벤트 전달 후 더 이상 그 이벤트 처리에는 관여하지 않고, 다른 시작 이벤트 또는 처리 이벤트에 반응할 준비를 한다. 또한 각 이벤트 프로세서는 이벤트 처리 도중 가변적인 부하나 백업 조건을 처리하기 위해 서로 독립적으로 확장할 수 있다.
브로커 토폴로지의 장단점
-
장점
- 비동기적으로 처리되어 시스템 부하가 분산되고, 응답성 및 성능이 향상된다.
- 각 이벤트 소비자가 독립적으로 이벤트를 처리하므로, 시스템을 확장이 용이하다.
- 각 서비스나 프로세서가 디커플링되어 독립적으로 작동하므로, 장애의 전파를 방지할 수 있다.
-
단점
- 이벤트와 연관된 전체 워크플로우를 제어할 수가 없다.
- 트랜잭션 관리가 어렵고, 에러 처리가 복잡해질 수 있다.
- 장애가 발생했을 때 상태를 추적하거나 복구하기가 복잡하다.
중재자 토폴로지(Mediator Topology)
중앙 집중적이고 일관적으로
중재자 토폴로지에서 이벤트는 시작 이벤트 큐를 거쳐 중재자(mediator) 역할을 하는 컴포넌트로 전달된다. 중개자는 단순히 이벤트를 전달하는 것을 넘어 이벤트를 필터링하거나 변환하는 역할도 수행할 수 있다. 이벤트 처리 흐름을 중앙에서 제어하고, 여러 이벤트 프로세서가 순차적으로 또는 조건적으로 실행되어야 하는 경우에 적합하다. 또한, 여러 단계를 거치는 오케스트레이션이 필요한 상황에서 유용하게 활용될 수 있다.
구성요소
- 이벤트(Event)
- 이벤트 큐(Event Queue)
- 이벤트 중재자(Event Mediator)
- 이벤트 채널(Event Channel)
- 이벤트 프로세서(Event Processor)
중재자 토폴로지 구현체에는 대부분 특정 도메인이나 이벤트 그룹과 연관된 중재자가 여럿 존재하므로 토폴로지의 단일 장애점(SPOF)을 줄이고 전체 처리량과 성능을 높일 수 있다.
중재자 토폴로지의 장단점
-
장점
- 이벤트 처리 워크플로우를 중앙에서 제어할 수 있다.
- 오류를 일관되게 관리하고, 재시도 또는 롤백 로직을 적용할 수 있다.
- 새로운 서비스나 컴포넌트(이벤트 프로세서)를 중재자에 연결하기만 하면 되므로, 시스템 확장이 간편하다.
-
단점
- 중재자가 모든 이벤트를 처리하고 조정하므로, 단일 지점에 부하가 집중되어 병목 지점이 발생할 수 있다.
- 이벤트 프로세서들이 중재자와 더 강하게 커플링되어 중재자 자체의 성능 문제가 시스템에 영향을 미칠 수 있다.
- 복잡한 워크플로우나 동적인 이벤트 처리를 선언적으로 모델링하기 매우 어렵다.
이벤트 기반 아키텍처 패턴
이벤트를 어떻게 처리할 것인가?
- 이벤트 알림(Event Notification)
- 이벤트 기반 상태 전송(Event-Carried State Transfer)
- 이벤트 소싱(Event Sourcing)
- CQRS(Command Query Responsibility Segregation)
이벤트 알림(Event Notification)
이벤트 발생 자체를 알리는 방식으로
이벤트 알림 패턴은 단순히 이벤트가 발생했다는 사실을 알리기만 하며, 이벤트에 상태 정보는 포함되지 않고, 최소한의 식별자만 포함한다.
정보가 필요하다면 수신자는 별도 API를 통한 추가적인 조회가 필요하다.
-
언제 Event Notification을 사용할까?
- 데이터가 자주 변경되며 데이터 일관성(consistency)이 중요한 경우
- 이벤트 메시지 크기를 작게 유지, 네트워크 부하를 줄이고 싶은 경우
// Use Case: Stock price updates.
{
"eventType": "StockPriceUpdated",
"timestamp": "2025-02-22T10:30:00Z",
"stockSymbol": "AAPL"
}이벤트 기반 상태 전송(Event-Carried State Transfer, ECST)
이벤트에 상태 데이터를 포함하는 방식으로
이벤트 기반 상태 전송 패턴은 이벤트 알림 패턴에서 발전된 방식으로, 이벤트 메시지 자체에 상태 데이터를 명시적으로 포함시켜, 이벤트 수신자가 추가적인 쿼리 없이 이벤트만으로 필요한 데이터를 바로 얻을 수 있도록 한다.
이 방식은 수신자가 이벤트를 받고 바로 처리할 수 있어 응답 지연(latency)이 줄어들며, 특정 서비스가 다운되더라도 이벤트 로그를 기반으로 재처리(replay)할 수 있다는 장점이 있지만, 이벤트가 발생할 때의 상태가 포함되기 때문에 데이터의 정합성을 보장하기 어려울 수 있다.
-
언제 ECST를 사용할까?
- 이벤트 소비자가 이벤트 자체만으로 필요한 정보를 얻어야 할 때
- 이벤트 발생 시점에서 상태 정보가 중요하고 이후 변경될 가능성이 낮을 때
- 분산 환경에서 서비스 간 의존성을 줄이고 독립성을 높이고 싶을 때
// Use Case: Order creation in e-commerce.
{
"eventType": "OrderCreated",
"timestamp": "2025-02-22T12:00:00Z",
"orderId": "ORD123456",
"items": [
{
"productId": "MWW83KH/A",
"name": "airpods-max",
"quantity": 1,
"price": 769000
}
],
"totalAmount": 814000,
"paymentStatus": "Pending"
}이벤트 소싱(Event Sourcing)
상태 변화를 변경 불가능한 이벤트로 저장하는 방식으로
이벤트 소싱 패턴은 시스템 상태를 직접 저장하는 대신 시스템의 상태 변경을 유발하는 모든 변경 내역(이벤트 로그)을 저장한다. 이를 기반으로 특정 시점의 상태나 특정 이벤트가 시스템에 미친 영향을 추적할 수 있으며, 시스템의 상태를 재구성할 수 있다.
하지만, 특정 시점의 상태를 복원하려면 모든 이벤트를 순차적으로 적용해야 하므로, 이벤트가 많아질수록 성능 저하가 발생할 수 있다. 또한, 모든 변경 내역을 저장하기 때문에 저장소 사용량과 관리 비용이 높아질 수 있다.
CRUD vs Event Sourcing
- Classic CRUD:
Create,Read,Update, andDelete- Event Sourcing:
CreateandReadOnly
-
스냅샷(Snapshot)
스냅샷은 특정 시점의 시스템 상태를 저장하는 기법으로, 이벤스 소싱에서는 모든 변경 내역을 이벤트 로그로 저장하기 때문에, 이벤트가 많아질수록 특정 시점의 상태를 복원하는데 시간이 오래 걸려 이로 인해 성능 저하와 저장소 사용량 증가 이슈가 발생할 수 있다. 이를 해결하기 위해 주기적으로 스냅샷을 저장하여 처리 속도를 개선한다.
-
언제 이벤트 소싱을 사용할까?
- 변경 이력을 추적해야 하는 시스템 (ex: 금융 거래, 의료 기록, 회계 시스템)
- 특정 시점의 애플리케이션 상태로 재구성이 필요한 경우
CQRS(Command Query Responsibility Segregation)
명령(Command)와 조회(Query)를 분리하는 방식으로
CQRS 패턴은 쓰기를 위한 데이터 모델(Write Model)과 읽기를 위한 데이터 모델(Read Model)을 별도로 분리한다.
Commands vs Queries
- Commands: 데이터의 변경을 일으키는 작업 (ex:
INSERT,UPDATE,DELETE)- Queries: 데이터를 단순 조회하는 작업 (ex:
SELECT)
이 방식은 읽기 작업과 쓰기 작업의 책임을 명확히 분리하여, 각 작업 간의 충돌 없이 성능을 최적화할 수 있으며, 개별적으로 트래픽을 확장할 수 있어 대규모의 시스템에 유리하다. 그러나 단순 CRUD보다 시스템의 복잡성이 증가하므로, 데이터 모델이 단순하거나, 읽기와 쓰기의 부하가 균형을 이루는 경우 오히려 불필요한 복잡성을 초래할 수 있다.
-
CQRS 패턴 적용 예시
- 단일 DB에서 Command Model과 Query Model을 별도의 계층으로 분리하는 방식
- 별도의 DB로 Command, Query를 분리하고, Broker를 통해 데이터 동기화를 처리하는 방식
- CQRS에 이벤트 소싱을 적용하여, 이벤트 로그를 중심으로 상태 변경을 관리하고, CQRS의 Command 모델이 이벤트를 저장하는 방식 → 데이터 일관성 문제를 해결
-
언제 CQRS를 사용할까?
- 일반적으로 읽기(Read) 요청이 쓰기(Write)보다 훨씬 많을 경우
- 마이크로서비스 아키텍처나 대규모 트래픽이 발생하는 시스템
- 이벤트 소싱과 함께 사용하여 데이터 변경 내역을 추적할 때
관련 글
3분 읽기
안정적인 API를 위한 HTTP 멱등성 이해하기
HTTP 메서드별 안전함(Safe)과 멱등성(Idempotency)의 차이를 정리하고, PATCH·POST에서 멱등키를 활용해 중복 요청을 방지하는 방법을 소개합니다.
4분 읽기
Stateful vs Stateless
Stateful과 Stateless 아키텍처의 개념과 각각의 장단점을 정리하고, 모든 것을 무상태로 설계할 수 없는 이유를 살펴봅니다.
4분 읽기
왜 리액트는 단방향 데이터 흐름을 채택했을까?
양방향·단방향 데이터 바인딩의 차이를 MVC 아키텍처의 한계와 함께 살펴보며, React가 단방향 데이터 흐름을 채택한 배경과 이유를 알아봅니다.