이벤트 기반 아키텍처(EDA) 마스터하기: 유연하고 확장 가능한 시스템의 핵심

안녕하세요! 10년 경력의 소프트웨어 엔지니어이자 기술 교육자입니다. 오늘 우리가 함께 탐구할 주제는 바로 '이벤트 기반 아키텍처(Event-Driven Architecture, EDA)'입니다. 현대 소프트웨어 시스템, 특히 마이크로서비스 환경이나 분산 시스템을 설계할 때 EDA는 선택이 아닌 필수가 되어가고 있습니다. 복잡한 시스템을 더 유연하고, 확장 가능하며, 빠르게 변화에 대응할 수 있도록 만드는 마법 같은 설계 방식이죠.
1. 개념 소개: 정의, 탄생 배경, 왜 중요한지

정의
이벤트 기반 아키텍처(EDA)는 시스템의 구성 요소들이 '이벤트'라는 것을 발행(publish)하고 구독(subscribe)함으로써 서로 통신하는 아키텍처 스타일입니다. 여기서 '이벤트'란 시스템 내에서 발생한 어떤 중요한 '사실'이나 '변경 사항'을 의미합니다. 예를 들어, "주문이 생성되었다", "사용자 비밀번호가 변경되었다", "재고가 부족하다"와 같은 것들이 이벤트가 될 수 있습니다.
탄생 배경
전통적인 요청-응답(Request-Response) 방식은 클라이언트가 서버에 요청을 보내고 응답을 기다리는 동기식 통신 방식입니다. 이는 간단한 시스템에서는 잘 작동하지만, 시스템이 복잡해지고 규모가 커질수록 여러 가지 문제에 직면합니다.
- 강한 결합(Tight Coupling): 특정 서비스가 다른 서비스의 응답을 기다려야 하므로, 한 서비스의 장애가 다른 서비스로 전파될 위험이 큽니다.
- 확장성 문제: 서비스가 많아질수록 종속성이 복잡해지고, 특정 서비스의 부하가 전체 시스템 성능에 영향을 미칩니다.
- 반응성 저하: 여러 작업을 순차적으로 처리해야 하므로, 최종 응답까지 시간이 오래 걸릴 수 있습니다.
이러한 문제들을 해결하기 위해 시스템 구성 요소 간의 의존성을 줄이고, 비동기적으로 작업을 처리하여 유연성과 확장성을 높일 필요성이 대두되었고, 그 결과 EDA가 강력한 대안으로 떠올랐습니다.
왜 중요한가?
EDA는 현대의 마이크로서비스 아키텍처, 클라우드 네이티브 애플리케이션, 그리고 실시간 데이터 처리 시스템에서 특히 중요합니다.
- 느슨한 결합(Loose Coupling): 이벤트 발행자는 구독자가 누구인지, 몇 명인지 알 필요가 없습니다. 그저 이벤트만 발행할 뿐입니다. 구독자 역시 이벤트 발행자에 대해 직접적으로 알 필요 없이, 특정 이벤트에만 관심이 있습니다. 이는 서비스 간의 의존성을 획기적으로 줄여줍니다.
- 높은 확장성(High Scalability): 각 서비스는 독립적으로 확장될 수 있습니다. 특정 이벤트 처리량이 많아지면 해당 이벤트를 구독하는 서비스의 인스턴스만 늘리면 됩니다.
- 뛰어난 반응성(High Responsiveness): 요청을 보낸 후 응답을 기다릴 필요 없이, 즉시 다음 작업을 수행하거나 사용자에게 즉각적인 피드백을 줄 수 있습니다. 이벤트 처리 자체는 백그라운드에서 비동기적으로 이루어집니다.
- 내결함성(Fault Tolerance): 한 서비스에 장애가 발생하더라도 다른 서비스에 직접적인 영향을 주지 않습니다. 이벤트 브로커가 이벤트를 저장하고 있다가 서비스가 복구되면 다시 전달할 수 있기 때문입니다.
- 유연성 및 변경 용이성: 새로운 기능을 추가하거나 기존 기능을 변경할 때, 관련된 서비스만 수정하고 새로운 이벤트를 구독하는 서비스를 추가하면 됩니다. 전체 시스템에 미치는 영향이 최소화됩니다.
2. 핵심 원리 설명 (비유와 다이어그램 활용)

EDA의 핵심 구성 요소는 크게 세 가지입니다:
- 이벤트 발행자 (Event Producer/Publisher): 시스템 내에서 특정 사건이 발생하면 해당 사건에 대한 이벤트 메시지를 생성하고 발행합니다.
- 이벤트 브로커 (Event Broker/Bus): 발행자로부터 이벤트를 받아들이고, 이 이벤트에 관심 있는 모든 구독자에게 이벤트를 전달하는 중간 매개체입니다. 메시지 큐(Message Queue)나 스트리밍 플랫폼(Data Streaming Platform)이 이 역할을 수행합니다.
- 이벤트 구독자 (Event Consumer/Subscriber): 특정 이벤트에 관심이 있으며, 이벤트 브로커로부터 해당 이벤트를 수신하여 필요한 작업을 수행합니다.
신문사 비유
EDA의 원리를 이해하기 위해 '신문사' 비유를 들어봅시다.
- 이벤트 발행자 (기자): 기자는 세상에서 벌어지는 다양한 사건(이벤트)들을 취재하고 기사(이벤트 메시지)를 작성합니다. 기자는 누가 이 기사를 읽을지, 몇 명이 읽을지 알 필요가 없습니다. 그저 사실을 기록하고 신문사에 넘길 뿐입니다.
- 이벤트 브로커 (신문사): 신문사는 기자들이 보낸 기사들을 편집하고 인쇄하여 독자들에게 배포합니다. 독자들이 원하는 종류의 기사를 배달해주는 역할을 합니다. 독자가 잠시 신문을 받지 못하더라도, 나중에 다시 받아볼 수 있도록 보관할 수도 있습니다.
- 이벤트 구독자 (독자): 독자들은 신문사에 '구독'을 신청하여 매일 신문을 받아봅니다. 독자들은 기자가 누구인지, 기사가 어떻게 쓰였는지 자세히 알 필요 없이, 신문사에 의해 전달된 기사 내용에만 관심을 가집니다. 어떤 독자는 정치 기사만 보고, 어떤 독자는 스포츠 기사만 볼 수도 있습니다.
이 비유에서 기자가 사건을 '발행'하면, 신문사(브로커)가 이를 '중개'하여, 관심 있는 독자(구독자)에게 '전달'되는 과정이 EDA의 핵심 원리와 같습니다.
다이어그램
graph TD
A[서비스 A (주문 생성)] -->|OrderCreated 이벤트 발행| B(이벤트 브로커);
B -->|OrderCreated 이벤트 전달| C[서비스 C (재고 감소)];
B -->|OrderCreated 이벤트 전달| D[서비스 D (결제 처리)];
B -->|OrderCreated 이벤트 전달| E[서비스 E (배송 준비)];
F[서비스 F (재고 부족)] -->|StockLow 이벤트 발행| B;
B -->|StockLow 이벤트 전달| G[서비스 G (알림 발송)];
B -->|StockLow 이벤트 전달| H[서비스 H (자동 재주문)];
위 다이어그램에서, '서비스 A'가 주문 생성 이벤트를 발행하면, '이벤트 브로커'가 이 이벤트를 받아 '서비스 C (재고 감소)', '서비스 D (결제 처리)', '서비스 E (배송 준비)' 등 관심 있는 모든 구독자에게 비동기적으로 전달합니다. 각 서비스는 독립적으로 자신의 역할을 수행하며, 서로 직접적인 통신 없이도 유기적으로 연결됩니다.
3. 코드 예제 2개 (Python)
여기서는 실제 메시지 큐 라이브러리 대신, 개념을 명확히 하기 위해 간단한 EventBus 클래스를 사용하여 EDA의 작동 방식을 설명합니다.
예제 1: 기본적인 이벤트 발행 및 구독
이 예제는 EventBus를 통해 이벤트를 발행하고, 여러 구독자가 이벤트를 수신하여 처리하는 과정을 보여줍니다.
import collections
import time
import threading
# 1. EventBus 클래스 정의
class EventBus:
def __init__(self):
# 각 이벤트 타입별로 구독자 리스트를 저장
self.subscribers = collections.defaultdict(list)
# 이벤트를 발행할 큐 (실제 메시지 큐의 역할을 간접적으로 흉내)
self.event_queue = collections.deque()
self.running = True
self.processor_thread = threading.Thread(target=self._process_events)
self.processor_thread.daemon = True # 메인 스레드 종료시 함께 종료
def subscribe(self, event_type, handler):
"""특정 이벤트 타입에 핸들러(구독자)를 등록합니다."""
self.subscribers[event_type].append(handler)
print(f"'{handler.__name__}'가 '{event_type}' 이벤트 구독 완료.")
def publish(self, event_type, data):
"""이벤트를 발행하여 큐에 추가합니다."""
event = {'type': event_type, 'data': data}
self.event_queue.append(event)
print(f"이벤트 발행: '{event_type}' - {data}")
def _process_events(self):
"""큐에 있는 이벤트를 비동기적으로 처리합니다."""
while self.running:
if self.event_queue:
event = self.event_queue.popleft()
event_type = event['type']
event_data = event['data']
# 해당 이벤트 타입의 모든 구독자에게 전달
for handler in self.subscribers.get(event_type, []):
try:
# 구독자 핸들러를 별도의 스레드에서 실행하여 비동기 처리 흉내
# 실제 시스템에서는 메시지 큐 컨슈머가 이 역할을 수행
threading.Thread(target=handler, args=(event_data,)).start()
except Exception as e:
print(f"이벤트 처리 중 오류 발생 (핸들러: {handler.__name__}): {e}")
else:
time.sleep(0.1) # 큐가 비어있으면 잠시 대기
def start(self):
"""이벤트 처리 스레드를 시작합니다."""
self.processor_thread.start()
print("이벤트 버스 시작.")
def stop(self):
"""이벤트 처리 스레드를 중지합니다."""
self.running = False
self.processor_thread.join()
print("이벤트 버스 중지.")
# 2. 구독자(Consumer) 핸들러 함수 정의
def log_event_handler(event_data):
"""이벤트 발생 시 로그를 기록하는 핸들러."""
time.sleep(0.5) # 처리 지연을 시뮬레이션
print(f"[Log Service] 이벤트 수신: {event_data} - 로그 기록 완료.")
def notification_service_handler(event_data):
"""이벤트 발생 시 사용자에게 알림을 보내는 핸들러."""
time.sleep(1) # 처리 지연을 시뮬레이션
print(f"[Notification Service] 이벤트 수신: {event_data} - 사용자에게 알림 전송 완료.")
# 3. 메인 로직
if __name__ == "__main__":
event_bus = EventBus()
event_bus.start()
# 구독자 등록
event_bus.subscribe("UserRegistered", log_event_handler)
event_bus.subscribe("UserRegistered", notification_service_handler)
event_bus.subscribe("ProductAdded", log_event_handler)
print("\n--- 이벤트 발행 시작 ---\n")
# 이벤트 발행 (UserRegistered)
event_bus.publish("UserRegistered", {"user_id": 101, "username": "alice"})
time.sleep(0.1) # 이벤트 발행 간격
# 이벤트 발행 (ProductAdded)
event_bus.publish("ProductAdded", {"product_id": 201, "name": "Laptop", "price": 1200})
time.sleep(0.1)
# 또 다른 UserRegistered 이벤트 발행
event_bus.publish("UserRegistered", {"user_id": 102, "username": "bob"})
print("\n--- 모든 이벤트 발행 요청 완료. 백그라운드 처리 대기 중 ---\n")
# 모든 백그라운드 스레드가 작업을 완료할 시간을 줍니다.
time.sleep(3)
event_bus.stop()
설명:
EventBus는 이벤트를 발행하고 구독하는 핵심 역할을 합니다._process_events스레드가 백그라운드에서event_queue에 쌓인 이벤트를 꺼내 해당 구독자들에게 전달합니다.log_event_handler와notification_service_handler는UserRegistered이벤트를 구독하는 두 개의 독립적인 서비스(핸들러)입니다.UserRegistered이벤트가 발행되면, 두 핸들러가 동시에 비동기적으로 이 이벤트를 처리합니다. 이때 발행자는 구독자가 누구인지 전혀 알 필요가 없습니다.
예제 2: 주문 처리 시스템 (더 실용적인 시나리오)
이 예제는 전자상거래 주문 처리 과정을 EDA 방식으로 구현한 시뮬레이션입니다.
import collections
import time
import threading
class EventBus:
# 예제 1과 동일한 EventBus 구현 사용
def __init__(self):
self.subscribers = collections.defaultdict(list)
self.event_queue = collections.deque()
self.running = True
self.processor_thread = threading.Thread(target=self._process_events)
self.processor_thread.daemon = True
def subscribe(self, event_type, handler):
self.subscribers[event_type].append(handler)
print(f"'{handler.__name__}'가 '{event_type}' 이벤트 구독 완료.")
def publish(self, event_type, data):
event = {'type': event_type, 'data': data}
self.event_queue.append(event)
print(f"이벤트 발행: '{event_type}' - {data}")
def _process_events(self):
while self.running:
if self.event_queue:
event = self.event_queue.popleft()
event_type = event['type']
event_data = event['data']
for handler in self.subscribers.get(event_type, []):
try:
threading.Thread(target=handler, args=(event_data,)).start()
except Exception as e:
print(f"이벤트 처리 중 오류 발생 (핸들러: {handler.__name__}): {e}")
else:
time.sleep(0.1)
def start(self):
self.processor_thread.start()
print("이벤트 버스 시작.")
def stop(self):
self.running = False
self.processor_thread.join()
print("이벤트 버스 중지.")
# ----------------------------------------------------------------------
# 주문 서비스 (이벤트 발행자)
def order_service(event_bus, order_details):
print(f"\n[Order Service] 주문 접수: {order_details['order_id']}")
# 주문 생성 로직...
time.sleep(0.2)
print(f"[Order Service] 주문 '{order_details['order_id']}' 생성 완료.")
event_bus.publish("OrderCreated", order_details) # OrderCreated 이벤트 발행
# ----------------------------------------------------------------------
# 구독자 서비스들
def inventory_service_handler(order_data):
"""재고 서비스: 주문이 생성되면 재고를 감소시킵니다."""
time.sleep(0.8)
print(f"[Inventory Service] 주문 '{order_data['order_id']}'에 대한 재고 감소 처리 완료.")
# 재고 감소 로직...
# 재고가 부족해지면 StockLow 이벤트를 발행할 수도 있습니다.
def payment_service_handler(order_data):
"""결제 서비스: 주문이 생성되면 결제를 처리합니다."""
time.sleep(1.2)
print(f"[Payment Service] 주문 '{order_data['order_id']}'에 대한 결제 처리 완료.")
# 결제 로직...
event_bus.publish("PaymentProcessed", {"order_id": order_data['order_id'], "amount": order_data['amount']})
def shipping_service_handler(order_data):
"""배송 서비스: 주문이 생성되면 배송 준비를 시작합니다."""
time.sleep(0.5)
print(f"[Shipping Service] 주문 '{order_data['order_id']}'에 대한 배송 준비 시작.")
# 배송 준비 로직...
def email_notification_service_handler(event_data):
"""이메일 알림 서비스: 결제가 완료되면 고객에게 이메일을 보냅니다."""
time.sleep(0.7)
print(f"[Email Notification Service] 주문 '{event_data['order_id']}' 결제 완료 이메일 전송.")
# ----------------------------------------------------------------------
# 메인 로직
if __name__ == "__main__":
event_bus = EventBus()
event_bus.start()
# 구독자 등록
event_bus.subscribe("OrderCreated", inventory_service_handler)
event_bus.subscribe("OrderCreated", payment_service_handler)
event_bus.subscribe("OrderCreated", shipping_service_handler)
event_bus.subscribe("PaymentProcessed", email_notification_service_handler)
# 주문 생성 (이벤트 발행)
order_service(event_bus, {"order_id": "ORD-001", "user_id": 1, "items": ["itemA", "itemB"], "amount": 150.00})
time.sleep(0.2)
order_service(event_bus, {"order_id": "ORD-002", "user_id": 2, "items": ["itemC"], "amount": 75.50})
print("\n--- 모든 주문 요청 완료. 백그라운드 처리 대기 중 ---\n")
time.sleep(5) # 모든 이벤트가 처리될 충분한 시간 대기
event_bus.stop()
설명:
order_service는 주문을 생성하고OrderCreated이벤트를 발행하는 역할을 합니다.inventory_service_handler,payment_service_handler,shipping_service_handler는OrderCreated이벤트를 구독하여 각자의 작업을 독립적으로 수행합니다.payment_service_handler는 결제 처리 후 다시PaymentProcessed이벤트를 발행하고,email_notification_service_handler가 이 이벤트를 구독하여 고객에게 알림을 보냅니다.- 이 모든 과정은 비동기적으로 진행되어, 주문 서비스는 주문을 접수하고 이벤트를 발행한 후 바로 다음 작업을 수행할 수 있습니다.
4. 실무 적용 사례
EDA는 다양한 실무 영역에서 그 진가를 발휘합니다.
- 전자상거래: 주문이 생성되면(OrderCreated 이벤트), 재고 시스템은 재고를 업데이트하고, 결제 시스템은 결제를 처리하며, 배송 시스템은 배송 준비를 시작하고, 고객에게는 주문 확인 이메일이 발송됩니다. 이 모든 과정이 비동기적으로 동시에 진행될 수 있습니다.
- IoT (사물 인터넷): 센서에서 데이터가 감지되면(SensorDataReceived 이벤트), 데이터 분석 시스템은 이를 분석하고, 알림 시스템은 특정 임계치를 넘었을 때 경고를 보내고, 제어 시스템은 필요한 조치를 취합니다.
- 금융 시스템: 주식 거래가 체결되면(TradeExecuted 이벤트), 포트폴리오 시스템은 고객의 자산을 업데이트하고, 리스크 관리 시스템은 잠재적 위험을 평가하며, 정산 시스템은 거래를 확정합니다.
- 사용자 활동 추적 및 분석: 사용자가 웹사이트에서 특정 행동을 하면(UserClicked, ItemViewed 이벤트), 이를 이벤트 스트림으로 보내 실시간으로 사용자 행동을 분석하고, 개인화된 추천을 제공하며, 마케팅 캠페인을 트리거할 수 있습니다.
- 마이크로서비스 간 통신: 각 마이크로서비스가 서로 직접 호출하는 대신 이벤트를 통해 통신함으로써, 서비스 간의 강한 결합을 제거하고 독립적인 배포 및 확장을 가능하게 합니다.
5. 자주 하는 실수와 해결법
실수 1: 이벤트를 명령(Command)으로 오해하기
- 문제: 이벤트는 '무엇이 발생했다'는 과거형의 사실을 나타내야 하지만, '무엇을 해라'라는 명령형으로 사용하는 경우가 있습니다. 예를 들어,
UpdateUserAddress(명령) 대신UserAddressUpdated(이벤트)가 되어야 합니다. 명령은 특정 수신자에게 특정 작업을 요청하는 것이고, 이벤트는 시스템 전체에 알리는 사실입니다. - 해결법: 이벤트는 항상 과거 시제로 명명하고, 불변(immutable)의 사실로 정의합니다. 이벤트 이름이 동사로 시작한다면 명령일 가능성이 높으므로 주의 깊게 살펴보세요. 이벤트는 발생한 사실에 대한 정보를 담고 있어야 하며, 특정 액션을 강요해서는 안 됩니다.
실수 2: 이벤트 브로커에 과도한 로직 의존
- 문제: 이벤트 브로커(예: 메시지 큐)를 너무 복잡하게 설정하거나, 브로커 내에 비즈니스 로직을 구현하려는 경향이 있습니다. 예를 들어, 브로커 자체에서 메시지를 필터링하거나, 여러 이벤트를 조합하여 새로운 이벤트를 생성하는 등의 작업을 시도합니다. 이는 브로커를 단일 장애 지점(Single Point of Failure)으로 만들고, 확장성을 저해하며, 디버깅을 어렵게 만듭니다.
- 해결법: 이벤트 브로커는 단순한 메시지 전달자 역할에 충실해야 합니다. 복잡한 필터링이나 라우팅, 이벤트 조합 등의 비즈니스 로직은 구독자 서비스 내에서 처리하도록 합니다. 각 서비스가 자신이 필요한 이벤트를 구독하고, 그 이벤트를 바탕으로 비즈니스 로직을 수행하도록 설계해야 합니다.
실수 3: 비동기 처리의 어려움 간과 (특히 데이터 일관성)
- 문제: EDA는 본질적으로 비동기적이며, 이는 데이터의 '최종 일관성(Eventual Consistency)'을 가져올 수 있습니다. 즉, 이벤트가 발행된 직후 모든 구독자가 데이터를 즉시 업데이트하지 않을 수 있습니다. 이로 인해 사용자에게 일시적으로 오래된 정보가 보이거나, 여러 서비스 간에 데이터 불일치가 발생할 수 있습니다.
- 해결법: 최종 일관성을 이해하고, 시스템 설계에 반영해야 합니다. 중요하고 즉각적인 일관성이 요구되는 작업은 전통적인 동기식 트랜잭션을 사용하고, 그렇지 않은 부분에 EDA를 적용합니다. 또한, 일관성 없는 상태에 대한 사용자 경험을 고려하고(예: "주문 처리 중입니다. 잠시 후 새로고침 해주세요"), 사가 패턴(Saga Pattern)과 같은 분산 트랜잭션 관리 패턴을 고려하여 복잡한 비즈니스 프로세스에서 데이터 일관성을 유지하는 방법을 마련합니다.
실수 4: 이벤트 폭증으로 인한 시스템 부하
- 문제: 모든 것을 이벤트로 처리하려다 보면, 너무 많은 이벤트가 발행되어 이벤트 브로커나 구독자 서비스에 과도한 부하를 주거나, 불필요한 이벤트로 인해 시스템이 복잡해질 수 있습니다.
- 해결법: 이벤트는 '중요한 비즈니스 사실'만을 대표하도록 신중하게 정의해야 합니다. 모든 데이터 변경을 이벤트로 발행하기보다는, 비즈니스적으로 의미 있는 상태 변화에만 이벤트를 사용합니다. 또한, 이벤트 브로커의 스케일링 전략과 구독자 서비스의 처리 용량을 충분히 고려하고, 필요에 따라 Rate Limiting 등의 기술을 적용하여 과도한 이벤트를 제어해야 합니다.
6. 더 공부할 리소스 추천
- 책:
- "Designing Event-Driven Systems: Concepts and Patterns for Scalable, Resilient, and Maintainable Systems" by Ben Stopford: EDA의 개념과 실전 패턴을 깊이 있게 다룹니다.
- "Kafka: The Definitive Guide" by Gwen Shapira, Neha Narkhede, Todd Palino: 이벤트 스트리밍 플랫폼인 Kafka를 중심으로 EDA를 이해하는 데 큰 도움이 됩니다.
- 온라인 자료:
- Martin Fowler의 블로그: Event-Driven Architecture, Event Sourcing 등 관련 아티클이 많습니다. (martinfowler.com)
- AWS, Azure, GCP 공식 문서: 각 클라우드 벤더의 메시징 및 스트리밍 서비스(SQS, SNS, Kafka, Event Hubs, Pub/Sub 등)에 대한 문서를 통해 실제 구현 사례를 익힐 수 있습니다.
- 기술 스택:
- Apache Kafka: 고성능 분산 이벤트 스트리밍 플랫폼 (실무에서 가장 많이 사용됨)
- RabbitMQ: 범용 메시지 브로커
- Redis Pub/Sub: 간단한 Pub/Sub 패턴 구현에 사용 가능
- AWS SQS/SNS, Azure Service Bus, Google Cloud Pub/Sub: 클라우드 기반 메시징 서비스
EDA는 처음에는 복잡하게 느껴질 수 있지만, 현대 분산 시스템의 근간을 이루는 중요한 개념입니다. 위에서 제시된 핵심 원리와 예제를 바탕으로 개념을 확실히 이해하고, 실무에 적용하면서 경험을 쌓는다면 더욱 견고하고 유연한 시스템을 설계하는 데 큰 도움이 될 것입니다.
