dycjd 한 줄 정의

이벤트 주도 아키텍처 (Event-Driven Architecture, EDA)

분리된(decoupled) 이벤트 처리 컴포넌트 들이 비동기 발사 후 망각(fire-and-forget) 방식으로 이벤트를 발생·반응하며 흐르는, 고확장성·고성능 분산 아키텍처 스타일입니다. 대규모 애플리케이션은 물론이고 소규모 애플리케이션, 다른 아키텍처 스타일에 포함된 하이브리드(hybrid) 구조(예: 이벤트 주도 마이크로서비스)로도 널리 쓰입니다.

쉽게 말하면

요청 기반 모델전화 통화 라면, 이벤트 기반 모델라디오 방송 입니다.

전화는 특정한 한 사람 에게 걸어서, 상대가 받을 때까지 기다리고, 대답을 듣고 끊습니다. 상대가 바쁘거나 전원을 꺼두면 내 쪽도 막힙니다. 이게 요청-응답(동기) 모델 의 본질입니다.

라디오 방송은 전혀 다릅니다. 방송국은 “이런 뉴스가 있습니다” 를 전파로 쏘기만 하고, 누가 듣는지 확인하지 않습니다. 관심 있는 청취자가 알아서 주파수를 맞춰 듣고, 각자 다른 방식으로 반응합니다(운전자는 경로를 바꾸고, 상인은 가격을 조정하고, 행인은 그냥 흘려듣습니다). 방송국은 청취자가 누군지조차 모르지만, 그게 방송국의 일이 아닙니다. 이게 이벤트 기반(비동기) 모델 입니다.

실제 코드로 비교하면 차이가 확연합니다.

  • 요청 기반: paymentService.charge(order) — 결제 서비스를 이름으로 지목 해서 부르고, 리턴을 기다립니다. 결제 서비스가 다운되면 내 주문 처리도 멈춥니다.
  • 이벤트 기반: broker.publish("OrderPlaced", order) — “주문이 접수됐습니다”를 브로커에 공개 발행 만 하고 끝. 누가 들을지는 모르고 관심도 없습니다. 결제, 적립금, 추천 엔진, 감사 로그가 각자 알아서 구독하고 반응합니다.

이벤트가 연쇄적으로 번져 나가는 모습은 계주 경기(relay race) 와 닮았습니다. 한 주자가 다음 주자에게 바통을 넘기면 자기 임무는 끝이고, 다음 주자가 어떻게 뛰는지 감시하지 않습니다. 각 주자(처리기)는 자기 구간만 책임지고, 전체 경주는 독립적인 처리의 합으로 완성됩니다.

왜 이렇게 설계했는가?

  • 해결하는 문제:

    • 전통적인 요청-응답(동기) 호출의 시간적 결합(temporal coupling) 해소
    • 컴포넌트 간 강한 의존 관계 제거 (호출자가 피호출자의 주소, 인터페이스, 가용성을 알아야 하는 문제)
    • 높은 처리량(throughput)과 확장성이 필요한 워크로드에서, 순차적 동기 호출로 인한 병목과 대기 제거
    • 업무 프로세스에 새로운 단계를 추가할 때 기존 코드를 수정해야 하는 경직성 해소
  • 이게 없다면:

    • 주문 하나를 처리하려면 주문 서비스가 결제, 알림, 재고, 배송 서비스를 순차 호출해야 하고, 그중 하나라도 느려지거나 죽으면 전체가 멈춥니다.
    • 새로운 부가 기능(예: 부정 거래 탐지)을 추가하려면 주문 서비스 코드를 수정해야 합니다.
    • 트래픽 스파이크에 대응해 서비스 단위로 수평 확장하기 어렵고, 동기 호출 체인 전체를 같이 확장해야 합니다.

토폴로지

EDA는 두 가지 주요 토폴로지로 나뉩니다. 브로커 토폴로지(broker topology) 는 중재자 없이 처리기들이 이벤트 브로커를 통해 서로 반응하는 코레오그래피형(choreographed) 구조이고, 중재자 토폴로지(mediator topology) 는 중심에 이벤트 중재자(event mediator) 가 있어 작업흐름을 통제하는 오케스트레이션형(orchestrated) 구조입니다.

요청 기반 모델

대부분의 애플리케이션은 요청 기반 모델(request-based model)을 따릅니다. 예를 들어 고객이 지난 6개월 간의 주문 내역을 요청하면, 그 요청은 먼저 요청 오케스트레이터(request orchestrator) 로 갑니다. 일반적으로 요청 오케스트레이터는 하나의 사용자 인터페이스(UI) 이지만, API 계층, 오케스트레이션 서비스, 이벤트 허브, 이벤트 버스, 통합 허브 로 구현할 수도 있습니다.

요청 오케스트레이터의 역할은 요청을 여러 요청 처리기(request processor) 로 전달하는 것입니다. 요청 처리기는 결정론적이고 동기적 인 방식으로 요청을 처리하고, 필요에 따라 데이터베이스에 접근합니다.

flowchart TB
    U[요청] <--> RO[요청 오케스트레이터<br/>UI · API 계층 · 통합 허브]
    RO <--> RP1[요청 처리기 1]
    RO <--> RP2[요청 처리기 2]
    RO <--> RP3[요청 처리기 3]
    RP1 <--> DB[(데이터베이스)]
    RP2 <--> DB
    RP3 <--> DB

오케스트레이터와 각 요청 처리기는 내부에 요청 상태(request state) 를 보유하여 현재 처리 중인 요청의 진행 상황을 추적합니다.

이벤트 기반 모델과의 대조
flowchart LR
    subgraph 요청기반["요청 기반"]
        U1[요청] --> RO1[오케스트레이터]
        RO1 --> P1[처리기 1]
        RO1 --> P2[처리기 2]
        P1 -. 응답 .-> RO1
        P2 -. 응답 .-> RO1
    end

    subgraph 이벤트기반["이벤트 기반"]
        P[발행자] -- 이벤트 --> B[(브로커)]
        B --> C1[처리기 1]
        B --> C2[처리기 2]
        B --> C3[처리기 3]
    end

요청 기반은 “무엇을 해달라”(명령) 를 오케스트레이터가 특정 처리기에 보내고 응답을 모으는 중앙 통제 구조입니다. 이벤트 기반은 “무슨 일이 일어났다”(사실) 를 브로커에 공개하고 구독자들이 자율적으로 반응하는 분산 협업 구조입니다. 이 방향성의 차이가 결합도·확장성·장애 전파 방식을 근본적으로 바꿉니다.

4가지 핵심 컴포넌트
flowchart LR
    IE[개시 이벤트<br/>Initiating Event] --> EB[(이벤트 브로커<br/>Event Broker)]
    EB --> EP1[이벤트 처리기 1<br/>Event Processor]
    EB --> EP2[이벤트 처리기 2]
    EP1 -- 파생 이벤트<br/>Derived Event --> EB
    EP2 -- 파생 이벤트 --> EB

개시 이벤트(Initiating Event): 전체 이벤트 흐름을 촉발하는 최초 이벤트입니다. 예를 들어 “주문이 접수됨”, “경매 입찰이 들어옴” 같은 비즈니스 사실이 개시 이벤트가 됩니다.

이벤트 브로커(Event Broker): 이벤트를 받아 구독자에게 분배하는 중앙 허브입니다. 단일 인스턴스가 아니라 연합(federation) 으로 배치되는 것이 일반적입니다. 물리적으로 여러 브로커 인스턴스가 클러스터 또는 지역별로 배치되어 가용성과 처리량을 확보합니다. 실무에서는 AMQP(Advanced Message Queuing Protocol) 같은 표준 프로토콜과 발행-구독(publish-subscribe) 모델 을 기반으로 합니다. Kafka, RabbitMQ, ActiveMQ, AWS SNS/SQS, Google Pub/Sub 등이 여기에 해당합니다.

이벤트 처리기(Event Processor): 특정 이벤트 유형을 구독하고, 해당 이벤트가 들어오면 자신의 비즈니스 로직을 수행하는 독립 컴포넌트입니다. 서로의 존재를 모르고, 브로커를 통해서만 상호작용합니다.

파생 이벤트(Derived Event): 처리기가 자기 일을 끝내고 결과로 발행하는 새 이벤트입니다. 이 파생 이벤트가 또 다른 처리기의 입력이 되면서 이벤트 체인이 이어집니다. “결제가 완료됨” → “주문 이행 시작” → “주문이 이행됨” → “배송 시작” 같은 식입니다.

예시: 소매 주문 입력 시스템
flowchart LR
    Start([주문 접수됨]) --> OP[Order Placement]
    OP -->|결제 요청| Pay[Payment]
    OP -->|재고 확인| Inv[Inventory]
    OP -->|고객 알림| Notif1[Notification]

    Inv -->|재고 갱신됨| WH[Warehouse]
    WH -->|재고 보충됨| Done1([창고 보충 완료])

    Pay -->|결제 적용됨| OF[Order Fulfillment]
    OF -->|주문 이행됨| Notif2[Notification]
    OF -->|주문 이행됨| Ship[Shipping]
    Ship -->|주문 배송됨| Done2([배송 완료])

주문 하나가 접수되면 Order Placement 처리기가 세 가지 파생 이벤트(알림, 결제, 재고)를 동시에 뿌립니다. 이 이벤트들은 병렬로 처리되고, 각 처리기는 자기 일이 끝나면 다시 파생 이벤트를 발행합니다. Order Fulfillment는 “결제 적용됨” 이벤트를 기다렸다가 처리를 시작하고, 완료되면 “주문 이행됨” 이벤트를 발행해 배송·알림 처리기를 트리거합니다.

핵심은 Order Placement가 Payment, Inventory, Notification 서비스의 존재조차 모른다는 점입니다. 그저 “주문이 접수됨”이라는 사실을 브로커에 공개할 뿐입니다. 나중에 부정 거래 탐지, 적립금 계산, 추천 엔진 학습 같은 새 처리기를 추가해도 Order Placement는 손댈 필요가 없습니다. 이것이 이벤트 주도 아키텍처가 제공하는 아키텍처 확장성(architectural extensibility) 입니다.

독성 이벤트(Poison Event)

처리기가 이벤트를 역직렬화하거나 처리하다가 계속 실패하는 경우, 해당 이벤트가 큐에 영원히 남으며 동일한 실패를 반복 유발합니다. 이를 독성 이벤트 라고 합니다. 방치하면 처리기가 무한 재시도 루프에 빠지거나 스레드 풀을 고갈시켜 서비스 자체가 마비됩니다.

실무 대응은 Dead Letter Queue(DLQ) 로 일정 횟수 실패한 이벤트를 격리하고, 별도 재처리·알람 파이프라인을 두는 것입니다. 카프카에서는 오프셋을 스킵하거나 재처리 토픽을 두고, RabbitMQ는 DLX(Dead Letter Exchange)를 활용합니다.

핵심 내용

이벤트 대 메시지

EDA에서 가장 흔히 혼동되는 개념입니다. 같은 브로커, 같은 비동기 통신처럼 보이지만 의미와 흐름이 다릅니다.

개념적 차이

이벤트(Event)“무슨 일이 일어났다” 는 사실의 공표입니다. 발행자는 누가 이 사실에 관심 있는지 모르고, 관심도 없습니다. 뉴스 속보처럼 공개적으로 방송됩니다.

메시지(Message)“이 일을 해달라” 는 특정 수신자 대상의 명령 또는 요청입니다. 발행자는 수신자를 알고 있고, 응답이나 작업 완료를 기대합니다.

4가지 차이점
구분이벤트 (Event)메시지 (Message)
본질일어난 사실의 공표 (broadcast)특정 대상에게 보내는 명령/요청
응답 기대없음 (fire-and-forget)보통 있음 (응답 또는 완료 확인)
대상일대다 (multiple subscribers)일대일 (single consumer)
채널 유형토픽(topic) / 스트림(stream)대기열(queue)
혼용 전략

실무에서는 이 둘이 한 시스템에 섞입니다. 예를 들어 “주문 접수됨” 이벤트가 발행되면, 알림 서비스는 이를 받아 “이메일 발송” 메시지 를 이메일 워커 큐에 던집니다. 이벤트는 사실의 전파에, 메시지는 특정 워커에게 일을 맡기는 데 각각 적합합니다.

브로커 차원에서도 Kafka(이벤트 스트림), RabbitMQ(메시지 큐잉) 같은 기술 선택이 이 의미 구분과 맞물립니다. 하지만 두 브로커 모두 기술적으로는 양쪽을 지원하기 때문에, 의미론적 결정 이 먼저이고 기술 선택은 그 다음입니다.

판별 연습

네 가지 사례로 감을 잡을 수 있습니다. 특히 “여러 대상에게 방송한다고 해서 자동으로 이벤트가 되지는 않는다” 는 점이 핵심입니다. 판별 기준은 수신자 수가 아니라 “이미 일어난 사실의 공표인가, 아직 할 일을 시키는 명령인가”입니다.

  1. 관제탑의 지시 — “어드벤처러스 항공 6557편, 230도 방향으로 좌회전하라”는 특정 항공기에 대한 명령이고, 해당 항공기의 응답이 요구되므로 메시지 입니다. 다른 조종사들도 통신을 들을 수는 있지만, 대상이 하나임은 명확합니다.
  2. 한랭전선 접근 뉴스 — “한랭전선이 이 지역으로 이동해 왔습니다”는 이미 일어난 사실을 다수에게 방송하고, 응답을 기대하지 않으므로 이벤트 입니다. 농부·항공사·시민은 각자 알아서 반응합니다.
  3. 교실의 지시 — “자, 여러분, 문제집 145페이지를 펴세요”는 메시지 입니다. 여러 학생에게 방송되지만 이미 일어난 일을 알리는 것이 아니라 무언가를 하라는 명령 이기 때문입니다. 브로드캐스트 채널로 보내도 내용이 명령이면 이벤트가 아닙니다.
  4. 회의 지각 인사 — “안녕하세요, 여러분! 회의에 늦어서 죄송합니다”는 이벤트 입니다. 이미 지각했다는 사실의 공표이고, 여러 사람에게 방송되며, 응답을 기대하지 않습니다.

파생 이벤트

파생 이벤트(Derived Event) 는 이벤트 처리기가 개시 이벤트(또는 다른 파생 이벤트)를 받은 후에 발생시키는 후속 이벤트입니다. EDA에서 없어서는 안 될 구성 요소이며, 처리기는 하나의 입력 이벤트에 대해 둘 이상 의 파생 이벤트를 발생시킬 수도 있습니다.

하나의 이벤트 처리기, 여러 파생 이벤트

Payment 이벤트 처리기가 신용카드에 요금을 청구하는 예를 보면, 한 번의 청구 작업 안에 여러 판단이 엮여 있습니다.

  • Fraud Detection 이벤트 처리기: 구매의 사기 가능성 판별
  • Credit Limit 이벤트 처리기: 신용카드 잔액·한도 판별

EDA에서는 “결제 적용됨” 이라는 하나의 이벤트를 통해 이 두 활동을 동시에 병렬로 트리거할 수 있습니다. 요청-응답 방식이라면 “사기 확인 요청 → 응답 → 한도 확인 요청 → 응답”처럼 순차 호출해야 했을 것을, 이벤트 하나의 발행으로 해결합니다.

flowchart LR
    OS[주문 접수 서비스] -- 주문 접수됨 --> PS[결제 서비스]
    PS -- 결제 적용됨 --> FD[Fraud Detection]
    PS -- 결제 적용됨 --> CL[Credit Limit]

    FD -- 사기 탐지됨 --> FDYes([사기 처리 라인])
    FD -- 사기 탐지 안 됨 --> FDNo([정상 라인])

    CL -- 한도 OK --> OK([잔액 충분 알림])
    CL -- 한도 경고 --> Warn([Notification: 한도 근접])
    CL -- 한도 초과됨 --> Over([Decline Purchase /<br/>Extend Credit Limit])
파생 이벤트의 분기

Fraud Detection은 판정 결과에 따라 두 가지 파생 이벤트 중 하나 를 발생시킵니다.

  • 사기 탐지됨: 후속 처리기가 카드 정지, 고객 확인 요청, 사기 로그 적재 등을 수행
  • 사기 탐지 안 됨: 정상 결제 라인이 계속 진행

두 파생 이벤트 모두 필요한 이유는 탐지 결과에 따라 전혀 다른 다운스트림 처리기들이 붙기 때문입니다. “아무 일도 없음”을 이벤트로 발행하지 않으면, 정상 케이스에 관심 있는 처리기(예: 결제 확정 기록)가 트리거될 수 없습니다.

파생 이벤트의 페이로드 설계

Credit Limit 이벤트 처리기는 결과를 세 갈래로 나눕니다.

  • 한도 OK — 이 구매에 위험이 없고 잔액이 충분함을 시스템에 알립니다. 이때 현재 잔액 수치를 이벤트 페이로드에 실을지 가 설계 포인트입니다. 다운스트림 처리기(예: 잔액 기반 추천)에 유용하다면 포함하고, 민감한 금융 정보이므로 필요 없는 곳에는 전달하지 않는 식으로 페이로드 범위를 결정합니다.
  • 한도 경고 — 잔액이 한도에 가깝다는 경고입니다. Notification 이벤트 처리기는 이를 받아 고객에게 “한도에 근접했습니다” 알림을 발송할 수 있습니다.
  • 한도 초과됨 — 가장 치명적입니다. Notification 처리기가 고객에게 알릴 수도 있고, Decline Purchase(구매 거부) 처리기가 거래를 막을 수도 있으며, 마케팅 관점에서 Extend Credit Limit(신용 한도 추가) 처리기가 자동으로 한도를 상향해 구매를 허용하도록 설계할 수도 있습니다.

다운스트림

처리 흐름·파이프라인에서 현재 요소보다 뒤쪽에 있는 요소 를 다운스트림(downstream)이라고 합니다. 반드시 바로 다음 요소일 필요는 없고, 반대 방향은 업스트림(upstream)입니다.

실무 해석

이 구조가 주는 실질적 이점은 비즈니스 규칙이 한 서비스에 뭉치지 않는다 는 점입니다. “한도 초과 시 어떻게 대응할지”는 Credit Limit 처리기가 결정하지 않고, 한도 초과됨 이벤트를 구독하는 다수의 처리기가 각자 결정 합니다. 보수적 정책(거부)과 공격적 정책(한도 자동 증액) 중 어떤 걸 쓸지, 심지어 둘 다 쓰고 실험할지를 구독자 구성만 바꿔서 결정할 수 있습니다.

하나의 “결제 적용됨” 이벤트에서 분기된 파생 이벤트들이 다시 각자의 파생 이벤트를 낳으며 워크플로가 조립되기 때문에, 이벤트 명명과 의미 설계가 도메인 모델링의 일부 가 됩니다. “뭘 상태로 두고 뭘 이벤트로 둘 것인가”를 선언적으로 규정하는 작업이 초기부터 반드시 필요합니다.

확장 가능한 이벤트 발행

EDA의 킬러 기능 중 하나는 새 비즈니스 기능 추가가 기존 코드 수정 없이 이루어진다 는 점입니다. 주문 시스템에서 “주문 이행됨” 이벤트가 발행되면, 이 이벤트에 관심 있는 새 처리기(예: 추천 엔진 학습기, 적립금 계산기, 부정 거래 감시 ML 모델)를 구독자로 추가만 하면 워크플로에 자연스럽게 합류합니다.

flowchart LR
    OF[Order Fulfillment] -- 주문 이행됨 --> B[(이벤트 브로커)]
    B --> Notif[Notification]
    B --> Ship[Shipping]
    B --> New1[Email Analyzer<br/>신규 구독자]
    B --> New2[Loyalty Points<br/>신규 구독자]

요청 기반 모델과 비교해보면 차이가 뚜렷합니다. 요청 기반에서 새 기능을 추가하려면 Order Fulfillment 서비스의 코드를 고쳐서 새 서비스의 엔드포인트를 호출하게 해야 합니다. 이는 원본 서비스의 단위 테스트, 배포 파이프라인, 릴리즈 일정 까지 모두 건드린다는 뜻입니다.

EDA에서는 원본 처리기가 자신이 방송한 이벤트를 누가 듣고 있는지조차 모릅니다. 이 무관심이 바로 아키텍처 확장성의 근본 원리이고, 실무에서 신규 기능 추가 주기를 기존 도메인 배포 주기에서 완전히 분리 할 수 있게 해줍니다.

비동기 역량의 대가

EDA의 또 다른 특징은 요청-응답 대신 비동기 통신(asynchronous communication) 을 기본으로 쓴다는 점입니다. 비동기 요청의 반응성은 동기 REST 호출과 비교하면 극단적입니다.

예시: 온라인 서점의 리뷰(Comment) 기능

고객이 책에 댓글을 남기면, Comment 서비스가 평가 분석, 문법 검사, 저장 등 여러 단계를 거쳐야 합니다.

  • 동기 REST 호출: 고객은 전체 단계(3,000ms + 50ms + 50ms = 약 3,100ms)를 기다려야 응답을 받습니다. 시스템 부하가 올라가면 이 대기 시간이 5,000ms를 넘어 타임아웃 에러가 터지기도 합니다.
  • 비동기 큐 전달: 고객은 메시지가 큐에 들어가는 데 필요한 약 25ms 만 기다리고 응답을 받습니다. 부하가 급증해도 큐에 쌓아두고 결국 처리되므로 사용자 경험이 붕괴하지 않습니다.
flowchart LR
    subgraph 동기["동기 REST (3,100ms 대기)"]
        U1[고객] -- 요청 --> C1[Comment 서비스]
        C1 -.-> Eval[평가]
        Eval -.-> Grammar[문법 검사]
        Grammar -.-> Save[저장]
        Save -- 응답 --> U1
    end

    subgraph 비동기["비동기 큐 (25ms 대기)"]
        U2[고객] -- 요청 --> C2[Comment 서비스]
        C2 -- 큐 적재 25ms --> Q[(큐)]
        C2 -- 즉시 응답 --> U2
        Q -.-> W[Worker:<br/>평가/문법/저장]
    end
대가: 사용자는 결과를 받지 못한다

비동기의 본질적 제약은 응답을 받지 않는다 는 점입니다. “기다리지 않는다”가 아니라 “애초에 기다릴 응답이 없다”가 정확한 표현입니다. 고객은 댓글이 실제로 저장·게시됐는지를 요청 시점에서 알 수 없고, “댓글이 반영되기까지 시간이 걸릴 수 있습니다” 같은 최종 일관성(eventual consistency) 사용자 경험이 필요합니다.

이것이 비동기를 아무 데나 쓰면 안 되는 이유입니다. 은행 이체, 결제 승인처럼 즉시성과 확정성이 필수 인 작업은 동기가 맞고, 리뷰·알림·집계·추천처럼 지연이 허용되는 작업에 비동기를 씁니다.

정적 결합 vs 동적 결합

EDA에서 “결합(coupling)“은 두 차원으로 나뉩니다.

  • 정적 결합(static coupling): 컴파일 타임/빌드 타임 의존성. 한쪽이 다른 쪽의 인터페이스를 직접 참조해야 동작하는 관계입니다.
  • 동적 결합(dynamic coupling): 런타임에 서로의 존재를 알고 있어야 하는 관계. 이벤트 브로커를 거친다 해도 의미적으로 “A가 B의 결과가 필요함”이라는 의존이 있으면 동적 결합이 존재합니다.

EDA는 정적 결합을 완전히 제거 하지만, 동적 결합은 남아 있을 수 있습니다.

예시: Portfolio Management와 Trade Order

자산 관리 시스템에서 Portfolio Management, Trade Order, Compliance 서비스가 있다고 합시다.

flowchart LR
    PM[Portfolio Management] -- 매수 요청 --> TO[Trade Order]
    TO -- 체결됨 --> PM
    TO -- 체결됨 --> CP[Compliance]
    PM -- ARIA 대시보드 --> UI[UI]

Portfolio Management는 투자 성과를 고객에게 보고하려면 Trade Order가 실제로 체결되었는지 확인 해야 하고, Trade Order의 출력이 도착할 때까지 기다려야 합니다. 두 서비스 사이에 브로커와 이벤트가 있어도 의미적으로는 결합 되어 있다는 뜻입니다.

한편 Compliance 서비스는 체결 이벤트를 받아 감사 로그를 남기기만 합니다. Portfolio Management와 Trade Order의 관계와는 달리, Compliance는 순수하게 방관자(observer) 로만 존재합니다. 이런 관계가 진정한 동적 결합 해제(dynamic decoupling) 입니다.

실무 해석

EDA를 도입한다고 모든 결합이 사라지는 게 아닙니다. “누가 누구의 결과를 필요로 하는가” 라는 도메인 의존은 그대로 남고, 이는 이벤트 명세와 SLA, 재시도·복구 정책에 그대로 드러납니다. EDA가 해체하는 것은 코드 수준의 의존과 시간적 동기 입니다.

브로드캐스팅 능력

EDA의 핵심 차별화 요소 중 하나는 브로드캐스팅(broadcasting) — 하나의 이벤트를 구독하는 모든 처리기가 동시에 받는 능력입니다.

flowchart LR
    P[이벤트 생산자] -- 이벤트 --> B[(이벤트 브로커)]
    B --> C1[이벤트 처리기 1]
    B --> C2[이벤트 처리기 2]
    B --> C3[이벤트 처리기 3]
    B --> C4[이벤트 처리기 4]

생산자는 누가 이벤트를 받는지 알지도, 관심도 없습니다. 구독자 수가 1개든 100개든 생산자의 코드는 변하지 않습니다. 이 성질이 CQRS, 이벤트 소싱, 최종 일관성 기반 분산 시스템, 복잡한 병렬 분기 워크플로 의 기술적 토대입니다.

브로드캐스팅은 요청-응답으로는 흉내 낼 수 없는 EDA 고유의 장점입니다. 같은 이벤트에 대해 처리기 A는 캐시를 갱신하고, B는 검색 인덱스를 재구축하고, C는 분석 데이터 웨어하우스에 적재하는 식으로 독립적 병렬 처리 가 가능해집니다.

이벤트 페이로드 설계

이벤트 페이로드를 어떻게 실을지는 계약 복잡도, 성능, 일관성 의 삼각 트레이드오프입니다. 두 가지 기본 방식이 있습니다.

데이터 기반 vs 키 기반

데이터 기반 이벤트 페이로드(data-based event payload) 는 이벤트에 필요한 모든 데이터를 실어서 보냅니다. Order Placement가 주문됨 이벤트를 발행하면 상품 ID, 수량, 가격, 고객 정보, 배송지까지 페이로드에 전부 담습니다.

{
  "order_id": "123",
  "customer_id": "456",
  "items": [{"sku": "A", "qty": 2}],
  "total": 29900,
  "shipping_addr": "...",
  "cust_addr": "...",
  "status": "PLACED"
}

키 기반 이벤트 페이로드(key-based event payload) 는 식별자만 실어서 보내고, 구독자가 원본 서비스의 API나 DB로 가서 세부 정보를 조회합니다.

{ "order_id": "123" }
두 방식의 비교
구분데이터 기반키 기반
성능높음 (자체 완결)낮음 (재조회 필요)
계약 복잡도높음 (큰 페이로드)낮음 (ID만)
디커플링높음 (원본 서비스 불필요)낮음 (원본 서비스 의존)
네트워크 호출없음구독자마다 조회 필요
데이터 일관성낮음 (이벤트와 DB 시차)높음 (단일 진실 원천)
페이로드 크기작음
선택 기준

데이터 기반 은 구독자가 원본 서비스를 호출할 필요가 없으므로 동적 결합이 낮고 성능이 높지만, 이벤트에 실린 값이 원본 DB와 시차가 생길 수 있어 단일 진실 원천(single source of record) 원칙이 약해집니다. 이벤트 소싱, CQRS의 read model 투영에 주로 어울립니다.

키 기반일관성이 높고 계약이 단순하지만, 구독자마다 원본 서비스를 호출해야 하므로 동적 결합이 강하고 성능 손실 이 있습니다. 데이터의 민감도·크기가 커서 이벤트에 싣기 부담스러울 때 선택합니다.

실무에서는 두 방식을 혼용 합니다. 자주 접근하는 요약 필드는 페이로드에 싣고, 상세는 키로만 전달해 필요할 때 조회하는 식입니다.

하루살이 떼 안티패턴

파생 이벤트는 자유롭게 쪼갤수록 좋은 것이 아닙니다. 세분도(granularity)를 잘못 잡으면 시스템 전체 흐름을 아무도 이해하지 못하게 되는 안티패턴에 빠집니다.

문제 상황

웹사이트에서 고객이 이사로 인해 프로필(청구지 주소·배송지 주소·전화번호)을 한 번에 바꾸고 저장 버튼을 누르는 시나리오를 봅시다. Customer Profile 서비스가 DB를 갱신한 뒤 파생 이벤트를 발행하는데, “필드 하나당 하나의 이벤트”로 설계하면 다음과 같이 필요 이상으로 잘게 쪼개집니다.

flowchart LR
    User((고객)) --> CPS[Customer Profile 서비스]
    CPS -.-> E1[청구지 갱신됨]
    CPS -.-> E2[배송지 갱신됨]
    CPS -.-> E3[전화번호 갱신됨]

세 이벤트 모두 사용자 프로필 갱신 이라는 같은 사안에 속하는데, 채널에는 각각 따로 흘러갑니다. 이런 하루살이 떼(swarm of gnats) 안티패턴 은 전염성이 있어서 다른 처리기들도 작은 파생 이벤트를 남발하게 만들고, 결국 전체 이벤트 흐름을 추적하는 일이 불가능해집니다.

해결: 결과 중심으로 통합

개별 필드 변경을 묶어서 “프로필 갱신됨” 이라는 하나의 파생 이벤트 로 발행하고, 변경된 모든 필드의 이전/이후 값을 페이로드에 실어 보내면 해결됩니다.

flowchart LR
    User((고객)) --> CPS[Customer Profile 서비스]
    CPS -.-> E[프로필 갱신됨<br/>payload: 변경된 모든 필드 before/after]

구독자는 자기가 관심 있는 필드만 페이로드에서 꺼내 쓰면 됩니다. 교훈은 “이벤트는 처리·상태 변경의 결과(outcome)에 초점을 맞춘다” 는 것입니다. 개별 상태 변경이 아니라, 비즈니스적으로 의미 있는 한 사건 을 이벤트 경계로 삼아야 합니다.

오류 처리 — 작업흐름 이벤트 패턴

비동기 요청에서는 오류가 발생해도 동기적으로 반응할 사용자가 없습니다. 예외를 로그에 찍는 것 말고는 방법이 없어서, 데이터 손실·처리 지연·혼란이 생깁니다.

작업흐름 이벤트 패턴

작업흐름 이벤트(Workflow Event) 패턴 은 반응형 아키텍처 계열에서 비동기 오류 처리를 다루는 표준 기법입니다. 작업흐름 대리자(workflow delegate) 라는 컴포넌트를 통해 시스템에 위임(delegation), 봉쇄(containment), 복구(repair) 능력을 도입합니다.

flowchart LR
    P[이벤트 생산자] --> EC[(이벤트 채널)]
    EC --> C[이벤트 소비자]
    C -- 오류 발생 시 위임 --> W[작업흐름 처리기<br/>workflow processor]
    W -- 프로그래밍적 복구 --> EC
    W -. 복구 불가 .-> D[담당자 대시보드<br/>수동 수정]
    D -- reply-to 원래 대기열 --> EC

핵심은 “소비자는 오류 해결에 시간을 쓰지 않는다” 입니다. 오류가 나면 즉시 workflow processor에 위임하고 다음 메시지로 넘어가기 때문에, 한 건의 오류가 전체 처리 반응성을 깎지 않습니다. 반대로 소비자가 직접 오류를 해결하려 들면 뒤의 모든 메시지가 지연됩니다.

workflow processor는 메시지를 분석해 정적·결정론적 오류 (포맷 위반 등)인지 동적 오류 (일시적 장애 등)인지 판단합니다. 경우에 따라 ML·AI로 비정상 데이터를 찾기도 합니다. 자동 복구가 가능하면 원본 데이터를 프로그래밍적으로 수정해 원래 대기열에 재제출합니다. 실패하면 별도의 대기열 → 담당자 데스크톱 대시보드 로 보내 사람이 수동 수정 후 reply-to 헤더 등으로 원래 대기열에 다시 넣습니다.

예시: 증권 거래 주문 포맷 오류

거래 중개인(trading advisor)이 증권거래사에 ACCOUNT,SIDE,SYMBOL,SHARES 포맷의 주문 바스켓을 비동기로 보내는데, 한 주문이 다음과 같이 포맷 계약을 위반 합니다.

2WE35HF6DHF,BUY,AAPL,8756 SHARES

마지막 필드가 Long 타입이어야 하는데 "8756 SHARES"처럼 단어가 붙어 있어 NumberFormatException이 터집니다. 작업흐름 이벤트 패턴 없이는 TradePlacement 서비스가 로그만 찍고 끝입니다.

패턴을 적용하면 다음과 같이 흐릅니다.

  1. TradePlacement 서비스가 계약 위반을 발견
  2. 오류 주문을 Trade Placement Error 서비스(작업흐름 대리자) 대기열로 위임
  3. TradePlacement 서비스는 곧바로 다음 주문으로 넘어감 (반응성 보존)
  4. Error 서비스가 "8756 SHARES""8756" 로 수정
  5. 원래 대기열로 재제출 → TradePlacement가 성공적으로 처리
메시지 순서 유지 트릭

작업흐름 이벤트 패턴의 숨은 함정은 메시지 순서가 깨진다 는 점입니다. 오류 주문이 돌아오는 사이에 뒤 주문들이 먼저 처리되기 때문입니다. 증권 계좌의 거래는 반드시 순서대로 처리되어야 하는 경우(예: IBM SELL이 AAPL BUY보다 먼저) 문제가 됩니다.

해결책은 “같은 맥락(계좌 번호 등)의 뒷 거래들을 임시 대기열에 FIFO로 저장” 해 두고, 오류가 복구된 뒤 순서대로 꺼내는 것입니다. TradePlacement 서비스는 오류 발생 계좌에 속하는 후속 거래를 DB나 메모리 큐에 쌓아뒀다가 복구 후 일괄 처리합니다.

데이터 손실 방지

비동기 통신 아키텍처에서 가장 무거운 우려가 데이터 손실(data loss) — 이벤트가 중간에 사라지거나 최종 목적지에 도달하지 못하는 현상입니다. 다행히 손실 지점별로 바로 적용 가능한 기법들이 잘 정립되어 있습니다.

손실이 발생할 수 있는 세 지점
flowchart LR
    A[이벤트 처리기 A<br/>생산자] -- 1 --> Q[(이벤트 채널)]
    Q -- 2 --> B[이벤트 처리기 B<br/>소비자]
    B -- 3 --> DB[(데이터베이스)]
  1. A → 채널: A가 발행했지만 브로커 승인 전에 A 또는 브로커가 다운
  2. 채널 → B: B가 이벤트를 받았지만 처리 전에 B가 다운
  3. B → DB: B가 DB 저장에 실패

각 지점은 이벤트 전달(Event Forwarding) 패턴 의 세 하위 기법으로 막을 수 있습니다.

손실 지점해결 기법원리
1. A → 채널영속적 대기열 + 동기적 전송브로커가 이벤트를 메모리가 아니라 디스크에 저장해 보장된 전달(guaranteed delivery) 을 제공하고, 생산자는 브로커 영속화 승인을 받을 때까지 차단식 대기(blocking wait)
2. 채널 → B클라이언트 승인 모드기본 자동 승인(auto acknowledge)은 읽자마자 큐에서 제거. client acknowledge 모드는 읽어도 큐에 유지하고, 클라이언트 ID를 붙여 다른 소비자가 못 읽게 함. B가 다운되면 재처리 가능
3. B → DBACID 트랜잭션 + LPSDB commit으로 영속화 보장. LPS(Last Participant Support) 가 “모든 처리 완료·영속화 완료”를 승인해야 큐에서 이벤트 제거
브로커 선택과 전달 보장

AMQP(Advanced Message Queuing Protocol) 브로커(RabbitMQ, Solace, Amazon SNS, Azure Event Hubs)는 익스체인지(exchange) 로 이벤트를 받아 바인딩 규칙에 따라 구독자 대기열로 전달하며, 이벤트 전달 패턴을 직접 지원합니다.

Jakarta Messaging(구 JMS)은 대기열 대신 토픽(topic) 을 쓰지만, 지속적 구독자(durable subscriber) 로 구성하면 소비자가 다운되어 있어도 이벤트가 토픽에 보존됩니다.

Kafka는 이벤트 브로커(event broker)로 이벤트 스트리밍을 구현하며, 로그 기반 저장 이라 전달 보장 방식이 AMQP와 다릅니다. 오프셋 커밋과 리플리케이션 설정으로 손실을 막습니다.

요청-응답 처리 (의사동기 통신)

지금까지는 소비자가 즉시 응답할 필요가 없는 비동기 요청이었습니다. 하지만 한 처리기가 다른 처리기로부터 즉시 정보나 확인 ID를 받아야만 다음으로 진행할 수 있는 경우가 있습니다. 요청-응답 메시징으로 의사동기적 통신(pseudosynchronous communication) 을 구현합니다.

기본 흐름
flowchart LR
    P[이벤트 생산자] -- 요청 --> RQ[요청 대기열]
    RQ --> C[이벤트 소비자]
    C -- 응답 --> RPQ[응답 대기열]
    RPQ -. WAIT .- P

생산자는 요청을 보내고 제어권은 즉시 반환 받지만, 응답 대기열에서 특정 응답이 올 때까지 차단 대기합니다. “진짜 동기”가 아닌 이유는 메시징 인프라가 실행 차단 없이 비동기 수단으로 진행하기 때문이고, 생산자 입장에서만 응답 대기 동안 블로킹됩니다.

구현 1: 상관관계 ID (CID)

응답 대기열에는 여러 생산자의 응답이 섞여서 들어옵니다. 내가 보낸 요청의 응답만 골라내려면 상관관계 ID(Correlation ID) 를 씁니다.

  1. 생산자가 요청 메시지를 보내면서 고유 ID(124)를 기록
  2. 응답 대기열에서 메시지 필터(message selector)CID == 124 조건만 차단 대기
  3. 소비자가 응답을 만들 때 CID를 원래 요청의 ID(124)로 설정
  4. CID 124 응답이 도착하면 필터가 골라서 생산자에게 전달

장점은 하나의 응답 대기열을 여러 요청이 공유 할 수 있다는 점입니다.

구현 2: 임시 대기열 (Temporary Queue)

요청마다 임시 대기열 을 하나 만들고, 요청이 끝나면 삭제하는 방식입니다. reply-to 헤더에 임시 대기열 이름을 넣어 보내면 소비자가 그쪽으로 응답합니다. 이 대기열은 해당 요청의 생산자만 아는 전용 이므로 CID가 필요 없습니다.

단점이 큽니다. 브로커가 요청마다 대기열을 생성·삭제 해야 하므로 느려지고, 고빈도 시스템에서는 전체 성능·반응성이 확연히 나빠집니다. 실무 권장은 CID 방식 입니다.

중재된 이벤트 주도 아키텍처

지금까지 설명한 EDA는 모두 코레오그래피형(choreographed) EDA 입니다. 이벤트 처리기들이 브로드캐스팅된 이벤트에 각자 알아서 반응하며 춤추듯 협업합니다. 유연하지만 복잡한 워크플로의 엄격한 제어·에러 복구·상태 추적 이 어렵습니다.

코레오그래피 vs 오케스트레이션

더 엄격한 제어가 필요할 때는 중재자 토폴로지(mediator topology) = 오케스트레이션형(orchestrated) EDA 를 씁니다. 중심에 이벤트 중재자(event mediator) 가 앉아 개시 이벤트의 작업흐름을 관리·통제 합니다.

flowchart LR
    IE[개시 이벤트] --> EM{{이벤트 중재자}}
    EM -- 주문 검증 --> P1[Validator]
    P1 -- 완료 --> EM
    EM -- 주문 배송 --> P2[Shipping]
    P2 -- 완료 --> EM
    EM -- 주문 알림 --> P3[Notification]

컴포넌트는 개시 이벤트, 이벤트 대기열, 이벤트 중재자, 이벤트 채널, 이벤트 처리기 로 구성됩니다.

핵심 차이: 이벤트가 아니라 “메시지”를 사용

중요한 점은 중재형이 일반적으로 이벤트가 아니라 메시지를 쓴다 는 것입니다.

  • 코레오그래피형: “주문 배송됨” (이미 일어난 사실 = 이벤트)
  • 중재형: “주문 배송” (앞으로 해야 할 일에 대한 명령 = 메시지)

중재자는 각 단계에 “이 일을 해달라”고 특정 처리기에게 명령을 보내고, 완료 응답을 기다립니다. 전체 워크플로의 상태와 진행을 중재자가 소유 하므로 장애 복구·재시도·보상(saga compensation)이 중앙에서 깔끔하게 구현됩니다. 대신 중재자가 병목이자 단일 실패 지점이 될 수 있고, 처리기 간 결합도가 (간접적으로) 올라갑니다.

단순 중재자 vs 복잡 중재자

중재자는 복잡도 기준으로 두 부류로 나뉩니다.

  • 단순 이벤트 중재자(simple event mediator): 직선적 단계 나열에 적합. Apache Camel, Mule ESB, Spring Integration 등이 해당하며, 간단한 주문 처리처럼 분기·조건 로직이 적은 워크플로에 씁니다.
  • 복잡 이벤트 중재자(complex event mediator): 조건 분기, 병렬, 반복, 보상 트랜잭션이 얽힌 워크플로용. BPEL(Business Process Execution Language), BPMN(BPM), Apache Camel K, AWS Step Functions, Temporal, Camunda 등이 해당합니다.
선택 기준
조건코레오그래피형중재형
워크플로 분기·조건 로직적음많음
처리기 수많음적거나 중간
장애 복구·보상분산 (어려움)중앙 집중 (쉬움)
결합도매우 낮음낮음~중간
확장성매우 높음중재자 병목 가능
가시성낮음 (추적 도구 필수)높음 (중재자가 상태 보유)

실무에서는 시스템 전체를 하나로 고를 필요가 없습니다. 주문 플로우 같은 복잡한 트랜잭션 경계는 중재형(예: Camunda, Temporal, AWS Step Functions), 이벤트 전파·알림·로그 수집 같은 사이드 플로우는 코레오그래피형으로 나누는 혼합 설계 가 보편적입니다.

데이터 토폴로지

EDA에서 이벤트 처리기들이 데이터를 어떻게 나눠 가질 것인가 도 아키텍처 결정의 한 축입니다. 책은 세 가지 데이터 토폴로지를 제시합니다.

모놀리스 데이터베이스 토폴로지

모든 이벤트 처리기가 하나의 공통 데이터베이스 를 공유하는 방식입니다.

flowchart TB
    OP[Order Placement] --> DB[(모놀리스 DB)]
    Pay[Payment] --> DB
    Inv[Inventory] --> DB
    OF[Order Fulfillment] --> DB
    Ship[Shipping] --> DB
  • 장점: 조인·참조 무결성·트랜잭션 일관성을 SQL로 간단히 달성
  • 단점: 데이터베이스가 단일 실패 지점 이자 확장 병목. 한 처리기의 스키마 변경이 전 처리기에 파급. 자원 경쟁(connection pool, lock)으로 처리기 간 장애 격리가 깨짐

EDA의 디커플링 장점을 데이터 계층에서 잃어버리는 구조입니다. 레거시 시스템을 EDA로 포장할 때 흔히 나오지만, 장기적으로는 바람직하지 않습니다.

도메인 데이터베이스 토폴로지

도메인 경계(예: 주문, 결제, 재고)별로 공유 데이터베이스 범위를 좁히는 절충안입니다.

flowchart TB
    OP[Order Placement] --> OD[(주문 DB)]
    OF[Order Fulfillment] --> OD
    Pay[Payment] --> PD[(결제 DB)]
    Inv[Inventory] --> ID[(재고 DB)]
    Ship[Shipping] --> SD[(배송 DB)]

같은 도메인 안의 처리기들은 한 DB를 공유하지만, 도메인 간에는 별도 DB를 씁니다. 도메인 간 데이터 교환은 이벤트·API로 해야 해서 동기 결합이 생기지만, 모놀리스 DB의 블라스트 라디우스는 대폭 줄어듭니다.

전용 데이터 토폴로지

처리기(서비스)마다 자기 전용 DB 를 가지는 database-per-service 방식입니다.

flowchart TB
    OP[Order Placement] --> D1[(OP DB)]
    OF[Order Fulfillment] --> D2[(OF DB)]
    Pay[Payment] --> D3[(Pay DB)]
    Inv[Inventory] --> D4[(Inv DB)]
    Ship[Shipping] --> D5[(Ship DB)]

최대 디커플링이 가능하지만, 필요한 데이터는 이벤트·쿼리로 받아와야 합니다. 같은 데이터를 여러 DB에 복제·동기화하는 책임이 붙고, 최종 일관성을 감수해야 합니다. 마이크로서비스 아키텍처가 전형적으로 쓰는 토폴로지로, bounded context(제한된 컨텍스트) 를 명확히 정의해야 유지 가능합니다.

선택 기준
토폴로지결합도일관성확장성적합한 상황
모놀리스 DB강함강함낮음레거시 포팅, 엄격한 ACID 필요
도메인 DB중간도메인 내 강함중간대부분의 EDA의 현실적 출발점
전용 DB약함최종 일관성높음마이크로서비스, 고확장성

클라우드 고려 사항

관리형 메시지 브로커(AWS SNS/SQS, GCP Pub/Sub, Azure Service Bus, Confluent Cloud 등)를 쓰면 브로커 운영 부담을 완전히 떼어낼 수 있고, 자동 확장·이중화를 기본으로 제공합니다. 단, 이벤트당 비용(per-message pricing), 출력 대역폭 요금, 벤더 종속(lock-in) 이 치명적일 수 있어서 초기부터 비용 모델을 추적해야 합니다.

클라우드 EDA의 또 다른 특징은 지역(region) 간 이벤트 복제 가 관리형 서비스로 제공된다는 점입니다. MSK Multi-Region Replication, Pub/Sub Replication 등으로 DR 설계가 훨씬 수월해집니다.

일반적인 위험

  • 독성 이벤트 방치: 재처리 실패를 격리·감시하는 DLQ와 모니터링이 없으면 단일 이벤트가 처리기 전체를 마비시킬 수 있습니다.
  • 이벤트와 메시지 혼동: 브로드캐스트성 사실(이벤트)을 큐로 보내거나, 특정 워커에게 할 일(메시지)을 토픽으로 방송하면 중복 처리·누락·확장성 문제가 발생합니다.
  • 하루살이 떼 안티패턴: 파생 이벤트를 필드 단위로 너무 잘게 쪼개면 시스템 전체 흐름을 이해할 수 없게 됩니다. 비즈니스 결과 단위로 묶고 페이로드에 상세를 담아야 합니다.
  • 오류를 소비자가 직접 처리: 작업흐름 이벤트 패턴 없이 소비자에서 try-catch로 복구하려 들면 전체 대기열 처리 반응성이 깨집니다.
  • 데이터 손실 지점 누락: 생산자 발행·소비자 수신·DB 저장 세 지점 중 하나라도 보호 장치가 없으면 조용히 데이터가 사라집니다.
  • 요청-응답을 임시 대기열로 남발: 고빈도 시스템에서 요청마다 대기열 생성·삭제는 브로커를 질식시킵니다. CID 기반이 기본.
  • 비결정론(non-determinism): 이벤트 처리 순서는 기본적으로 보장되지 않습니다. 순서가 중요한 도메인(거래·계좌)에서는 파티셔닝 키(Kafka) 또는 계좌별 FIFO 큐로 순서를 보장해야 합니다.
  • 반합성 이벤트(synthetic events): 도메인 사건이 아닌 내부 구현을 위해 꾸며낸 이벤트 (예: “이벤트 A가 처리되었음을 알리는 이벤트”)는 시스템을 인식 불가능하게 만듭니다. 이벤트는 반드시 비즈니스 사건이어야 합니다.
  • 도메인 경계 위반: 한 처리기의 내부 상태 전이까지 이벤트로 흘리면 다른 처리기가 그것을 구독해 로직을 짜기 시작하고, 도메인 캡슐화가 무너집니다.

거버넌스

EDA의 거버넌스는 크게 관찰성, 계약 관리, 스키마 진화 세 축으로 이뤄집니다.

관찰성(Observability)

요청-응답 모델과 달리 EDA는 단일 호출 스택이 없습니다. “주문이 어디서 멈췄는가?”를 답하려면 상관 ID(correlation ID) 를 모든 파생 이벤트에 전파하고, 분산 추적(OpenTelemetry, Zipkin, Jaeger) 을 깔아야 합니다. 이게 없으면 디버깅은 로그 grep 노가다로 전락합니다.

이벤트 계약과 스키마 진화

이벤트 페이로드는 사실상 공개 API 입니다. 생산자의 사소한 필드 변경이 구독자를 깨뜨리므로, 초기부터 스키마 레지스트리(Schema Registry) + Avro/Protobuf + 계약 테스트(contract test) 조합을 구축합니다. JSON+JSON Schema도 가능하지만 진화 규칙이 느슨해서 중·대규모에서는 Avro/Protobuf를 권장합니다.

버전 관리와 호환성 규칙

이벤트 스키마는 뒤로 호환(backward compatible) 을 기본으로 삼습니다. 필드 추가는 허용, 필드 삭제·타입 변경은 금지가 일반적입니다. 브레이킹 체인지가 필요하면 새 이벤트 타입을 발행하고 기존 타입을 deprecation 기간 동안 병행 하다가 구독자 마이그레이션 후 폐기합니다.

적합성 함수

EDA의 아키텍처 적합성 함수는 보통 다음을 포함합니다.

  • DLQ 메시지 수 임계치 감시
  • 구독자 lag(카프카 consumer lag) 임계치 감시
  • 브로커 가용성과 에러율 SLO
  • 스키마 레지스트리의 브레이킹 체인지 감지 CI 게이트
  • 이벤트 계약 테스트(생산자 / 소비자 양측)

팀 토폴로지 고려 사항

EDA는 도메인별로 자기 서비스·이벤트 채널·데이터를 소유하는 구조라서, 팀 경계가 도메인 경계 = 이벤트 경계 와 정렬될 때 가장 잘 작동합니다. 정렬이 어긋나면 한 팀이 다른 팀의 이벤트 스키마에 끊임없이 의존하고 배포 조율 비용이 폭발합니다.

팀 유형적합도이유
스트림 정렬 팀 (기능 전담 팀)매우 높음도메인별 이벤트 생산자·소비자·스키마·DB를 엔드-투-엔드로 소유하기 좋습니다. 이벤트 경계가 팀 경계와 자연스럽게 일치합니다
활성화 팀 (역량 코칭 팀)높음이벤트 설계 원칙, 스키마 진화 규칙, 관찰성 대시보드 패턴, 계약 테스트 방법을 스트림 팀에 전수하고 코칭합니다
난해한 하위시스템 팀 (전문 도메인 팀)매우 높음Kafka 클러스터 튜닝, Flink/Kafka Streams 스트림 처리, Camunda·Temporal 같은 복잡한 오케스트레이션 엔진 등 깊은 전문성이 필요한 영역에 잘 맞습니다
플랫폼 팀 (공통 기반 팀)매우 높음메시지 브로커 인프라, 스키마 레지스트리, 분산 추적·관찰성 플랫폼, DLQ·재처리 도구를 셀프서비스로 제공해 스트림 팀의 생산성을 극대화합니다

장단점

평가표
아키텍처 특성평점설명
전반적 비용$$$브로커 인프라·관찰성 도구 비용
분할 방식기술 기반도메인보다 이벤트 채널 단위로 자름
퀀텀 개수1개 이상브로커 경계로 다수 가능
단순성★☆☆☆☆비동기·분산으로 복잡도 높음
시험성★★☆☆☆비동기 흐름 E2E 테스트 어려움
배포성★★★★☆처리기 독립 배포
진화성★★★★★구독자 추가만으로 확장
내결함성★★★★★처리기 장애 격리
모듈성★★★★☆처리기 단위 모듈화
전체 비용★★★☆☆중간
성능★★★★★병렬 처리, 비동기
확장성★★★★★브로커·처리기 독립 확장
성능

메시지 브로커가 있어 한 번 거쳐가는 홉이 생기지만, 구독자들이 병렬로 동시 실행 되고 생산자는 즉시 응답을 받기 때문에 전체 처리량은 요청-응답 체인 방식보다 훨씬 높습니다. 브로커 자체는 초당 수백만 메시지를 처리하도록 튜닝 가능합니다.

확장성

구독자는 자기 큐를 독립적으로 컨슈머 그룹 수평 확장할 수 있고, 생산자도 자기 이벤트를 독립 확장합니다. 각 구간이 병목에 맞춰 따로 확장되므로 체인 전체를 같이 확장해야 하는 요청-응답 모델과 대조됩니다.

진화성

새 기능 추가가 구독자 추가 로 끝납니다. 기존 처리기를 전혀 건드리지 않기 때문에 진화성 평가가 가장 높습니다.

내결함성

한 처리기가 죽어도 이벤트는 브로커 큐에 남아 있고, 복구 후 재처리 가능합니다. DLQ로 독성 이벤트를 격리하면 한 이벤트가 전체를 멈추지 못합니다.

단순성 / 시험성

비동기 분산 시스템의 본질적 복잡성이 큽니다. 관찰성 투자 없이는 디버깅이 거의 불가능하고, E2E 테스트는 여러 서비스의 이벤트 타이밍 을 맞춰야 해서 플래키 테스트가 쉽게 나옵니다. 계약 테스트(Pact 등) + 시뮬레이션 브로커로 보완해야 합니다.

전체 비용

브로커 인프라, 스키마 레지스트리, 관찰성 도구, DLQ 재처리 파이프라인, 계약 테스트 인프라까지 초기 투자가 큽니다. 대신 이후 기능 추가 비용은 낮아져서 장기적으로는 회수됩니다.

적합한 상황

이벤트 기반을 선택해야 할 때
  • 복잡하고 비결정적인 워크플로: 주문·결제·이행처럼 분기와 병렬이 많고, 각 단계가 독립적으로 확장·장애 격리 되어야 하는 경우
  • 높은 반응성·확장성·내결함성 이 요구되는 경우 (전자상거래 피크, 금융 트랜잭션, IoT 텔레메트리)
  • 다수의 이벤트 처리기 간 동적 조율 이 필요한 경우 (예: 실시간 경매, 주식 트레이딩)
  • 기능 추가 주기가 자주 있고, 도메인 팀이 독립적으로 배포해야 하는 경우
  • 사이드 효과가 많은 도메인: 하나의 사실(주문 접수)에서 파생되는 관심사(알림, 추천, 분석, 감사)가 많을 때
요청 기반을 선택해야 할 때
  • 잘 구조화된 결정적 워크플로: 입력이 들어오면 정해진 단계로 흐르는 단순 CRUD, 보고서 조회
  • 응답의 즉시성 이 필수인 경우 (결제 승인, 잔액 조회)
  • 제어와 확실성 이 핵심인 경우 (규제·감사가 엄격한 금융 핵심 원장)

실무에서는 하나의 시스템이 둘 다 써야 하는 경우가 대부분 입니다. 사용자 결제는 동기, 결제 완료 후 사이드 이펙트(알림·적립·추천)는 비동기 이벤트로 나누는 식의 하이브리드가 보편적입니다.

예시

  • 경매 사이트: 입찰(Bid Capture) → Bid Tracker(입찰 기록) → Auctioneer(경매 진행) → Bid Streamer(실시간 스트림 전송) → Bidder Tracker(참여자 통계). 경매 이벤트가 다수 구독자에게 동시에 흘러가며 실시간성과 확장성을 모두 달성합니다.
  • 증권·주식 거래 시스템: 주문 → 검증 → 체결 → 청산 → 규제 보고의 각 단계가 이벤트로 연결. 초당 수만~수십만 건을 처리해야 하고, 일부 노드 장애가 전체 거래소를 멈추게 하면 안 됩니다.
  • 복권/추첨 시스템: 티켓 구매 이벤트가 여러 집계·검증·알림 처리기로 방송
  • IoT 텔레메트리: 수백만 디바이스가 내보내는 이벤트 스트림을 여러 처리기(알람, 집계, ML 학습, 장기 보관)가 동시에 소비
  • 이커머스 주문 처리: 주문 접수 → 결제·재고·알림 병렬 → 이행 → 배송의 일반적 패턴
  • 소셜 피드/알림 플랫폼: 사용자 활동이 이벤트로 방송되어 친구 피드·푸시 알림·추천 학습 등으로 분배

비교 / 트레이드오프

vs 요청 기반 모델 (Ch10 계층형 아키텍처 / Ch11 모듈형 모놀리스 아키텍처 / Ch14 서비스 기반 아키텍처)

요청 기반은 제어·확실성 에 강하지만 시간적 결합과 동기 대기 를 피할 수 없습니다. EDA는 정반대입니다.

vs Ch17 오케스트레이션 주도 SOA

SOA의 오케스트레이션 엔진(ESB 등)은 EDA의 중재자 토폴로지 와 닮았지만, SOA가 기업 전체의 비즈니스 프로세스를 강하게 중앙 통제하는 반면 EDA는 브로드캐스팅 중심의 느슨한 협업이 기본입니다.

vs Ch18 마이크로서비스 아키텍처

마이크로서비스는 서비스 경계 규칙 에, EDA는 통신 방식 에 초점이 있습니다. 실무에서는 둘이 결합해 “이벤트 주도 마이크로서비스” 하이브리드로 가장 많이 쓰입니다. 전용 데이터 토폴로지와 코레오그래피형이 자연스럽게 얹힙니다.

내 생각

백엔드에서 결제·주문·알림처럼 시간적 결합을 끊어야 하는 도메인 에서 EDA의 가치가 뚜렷합니다. 주문 서비스가 결제 서비스의 장애에 묶이지 않고, 결제 실패는 DLQ로 격리되어 별도 재처리 파이프라인이 처리합니다. 이 구조가 가용성과 복원력을 크게 끌어올립니다.

반면 실무에서 가장 큰 함정은 관찰 가능성(observability) 입니다. 요청-응답 모델에서는 호출 스택과 트레이스 ID로 흐름을 추적할 수 있지만, 이벤트 주도에서는 이벤트가 토픽을 여러 번 거치며 분기·병합되기 때문에 분산 추적 도구(OpenTelemetry 등)이벤트 상관 ID(correlation ID) 설계가 필수입니다. 이게 없으면 “주문이 어디서 멈췄는가?”를 답하기 위해 로그를 수십 개 서비스에서 모아야 합니다.

또한 “이벤트 스키마”를 버전 관리하는 문화가 없으면, 생산자의 사소한 필드 변경이 구독자 전체를 깨뜨립니다. Avro + Schema Registry, 또는 JSON Schema + 계약 테스트(contract test)를 운영 초기부터 깔아두는 게 현실적인 방어선입니다.

언제 쓰지 말아야 할까 에 대한 현장 감각도 중요합니다. 팀이 2~3개이고, 도메인이 단순한 CRUD이고, 트래픽도 평탄하다면 EDA는 과공학입니다. 메시지 브로커 운영, 스키마 관리, DLQ 재처리, 관찰성 구축 비용이 실제 얻는 확장성·유연성보다 커집니다. EDA의 투자가 ROI를 내는 분기점을 정직하게 평가 하고 도입해야 합니다.

더 알아볼 것

  • Kafka vs RabbitMQ 선택 기준과 각각의 보장(ordering, at-least-once 등)
  • AMQP, MQTT, Kafka 프로토콜 비교
  • DLQ 설계 패턴과 재처리 전략
  • 이벤트 스키마 진화(schema evolution)와 호환성 규칙
  • Saga 패턴과 이벤트 주도 아키텍처의 관계
  • OpenTelemetry 기반 이벤트 분산 추적 구축
  • Kafka Streams / Flink를 이용한 스트림 처리와 EDA의 접점
  • Camunda·Temporal·AWS Step Functions 같은 워크플로 엔진의 실전 비교

관련 개념

출처

  • 소프트웨어 아키텍처 The Basics, 15장 이벤트 주도 아키텍처 (Mark Richards & Neal Ford)