한 줄 정의

이벤트 주도 아키텍처 (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[주문 접수 처리기]
    OP -->|결제 요청| Pay[결제 처리기]
    OP -->|재고 확인| Inv[재고 처리기]
    OP -->|고객 알림| Notif1[알림 처리기]

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

    Pay -->|결제 적용됨| OF[주문 이행 처리기]
    OF -->|주문 이행됨| Notif2[알림 처리기]
    OF -->|주문 이행됨| Ship[배송 처리기]
    Ship -->|주문 배송됨| Done2([배송 완료])

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

핵심은 주문 접수 처리기가 결제·재고·알림 처리기의 존재조차 모른다는 점입니다. 그저 “주문이 접수됨”이라는 사실을 브로커에 공개할 뿐입니다. 나중에 부정 거래 탐지, 적립금 계산, 추천 엔진 학습 같은 새 처리기를 추가해도 주문 접수 처리기는 손댈 필요가 없습니다. 이것이 이벤트 주도 아키텍처가 제공하는 아키텍처 확장성(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에서 없어서는 안 될 구성 요소이며, 처리기는 하나의 입력 이벤트에 대해 둘 이상 의 파생 이벤트를 발생시킬 수도 있습니다.

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

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

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

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

flowchart LR
    OS[주문 접수 서비스] -- 주문 접수됨 --> PS[결제 서비스]
    PS -- 결제 적용됨 --> FD[사기 탐지 처리기]
    PS -- 결제 적용됨 --> CL[신용 한도 처리기]

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

    CL -- 한도 OK --> OK([잔액 충분 알림])
    CL -- 한도 경고 --> Warn([알림 처리기: 한도 근접])
    CL -- 한도 초과됨 --> Over([구매 거부 처리기 /<br/>한도 증액 처리기])
파생 이벤트의 분기

사기 탐지 처리기는 판정 결과에 따라 두 가지 파생 이벤트 중 하나 를 발생시킵니다.

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

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

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

신용 한도 이벤트 처리기는 결과를 세 갈래로 나눕니다.

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

다운스트림

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

실무 해석

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

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

확장 가능한 이벤트 발행

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

flowchart LR
    OF[주문 이행 처리기] -- 주문 이행됨 --> B[(이벤트 브로커)]
    B --> Notif[알림 처리기]
    B --> Ship[배송 처리기]
    B --> New1[이메일 분석 처리기<br/>신규 구독자]
    B --> New2[적립금 계산 처리기<br/>신규 구독자]

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

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

비동기 역량의 대가

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

예시: 온라인 서점의 댓글 기능

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

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

동기 REST (약 3,100ms 대기)

flowchart LR
    U1[고객] -- 요청 --> C1[댓글 서비스]
    C1 -- 3,000ms --> Eval[평가]
    Eval -- 50ms --> Grammar[문법 검사]
    Grammar -- 50ms --> Save[저장]
    Save -- 응답 --> U1

비동기 큐 (약 25ms 대기)

flowchart LR
    U2[고객] -- 요청 --> C2[댓글 서비스]
    C2 -- 큐 적재 25ms --> Q[(큐)]
    C2 -- 즉시 응답 --> U2
    Q -.-> W[워커:<br/>평가 / 문법 검사 / 저장]
대가: 사용자는 결과를 받지 못한다

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

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

동적 퀀텀 얽힘

아키텍처 퀀텀(architectural quantum) 은 독립 배포 가능하고, 기능적으로 응집되어 있으며, 동기적 동적 결합 으로 묶여 있는 컴포넌트들의 집합입니다. EDA에서 결합을 이야기할 때는 정적/동적 두 축으로 나누어 보는데, 퀀텀 경계를 실제로 정하는 것은 동적 결합 쪽입니다.

  • 정적 결합(static coupling): 빌드 타임 의존성. 인터페이스 참조, 공유 라이브러리, DB 스키마처럼 컴파일·배포 시점에 묶이는 관계입니다. EDA에서는 이벤트 계약만 공유하면 되기 때문에 쉽게 제거됩니다.
  • 동적 결합(dynamic coupling): 런타임에 “A가 B의 결과를 기다려야 하는” 관계. 브로커를 거쳐도 동기적으로 결과를 기다리는 순간 두 컴포넌트는 하나의 퀀텀으로 얽힙니다.

여기서 “얽힘(entanglement)“이라는 비유가 중요한데, 브로커를 끼웠다고 해서 퀀텀이 자동으로 쪼개지지 않는다는 뜻입니다. 누군가의 응답을 기다리는 구조 는 브로커 너머에서도 그대로 하나의 운명 공동체로 동작합니다.

예시: 포트폴리오 관리와 거래 체결

자산 관리 시스템에 포트폴리오 관리 서비스, 거래 체결 서비스, 컴플라이언스 서비스 가 있다고 합시다.

flowchart LR
    PM[포트폴리오 관리] -- 매수 요청 --> TO[거래 체결]
    TO -- 체결됨 --> PM
    TO -- 체결됨 --> CP[컴플라이언스]
    PM -- 성과 대시보드 --> UI[UI]

포트폴리오 관리는 고객에게 성과를 보고하려면 거래 체결이 실제로 일어났는지 확인해야 하고, 체결 결과가 도착할 때까지 기다려야 합니다. 브로커와 이벤트가 사이에 있어도 의미적으로는 동기적으로 묶여 있는 셈이고, 두 서비스는 같은 퀀텀 으로 얽힙니다. 한쪽의 가용성·스키마 변경·장애가 다른 쪽에 그대로 전이됩니다.

반면 컴플라이언스 서비스는 체결 이벤트를 받아 감사 로그만 남깁니다. 아무도 컴플라이언스의 응답을 기다리지 않고, 컴플라이언스도 아무것도 되돌려주지 않습니다. 이 관계는 얽힘이 풀린 상태 이고, 컴플라이언스는 별도 퀀텀 으로 분리됩니다. 컴플라이언스가 내려가도 포트폴리오 관리와 거래 체결은 정상 작동합니다.

실무 해석

EDA를 도입한다고 시스템이 자동으로 잘게 쪼개지는 것이 아닙니다. “누구의 응답을 기다리는가” 가 그대로 퀀텀 경계를 그리고, 이 경계가 곧 배포 단위, 장애 격리 단위, 확장 단위 가 됩니다.

설계 시 실용적 질문은 이렇게 바뀝니다.

  • 이 이벤트는 발행하고 끝내도 되는가, 아니면 결과를 기다려야 하는가
  • 기다려야 한다면 두 서비스를 같은 퀀텀 으로 본 SLA·장애 전파·버전 관리 정책을 세웠는가
  • 꼭 기다려야 하는 관계가 아니라면(나중에 반영되어도 되는 관계라면), 퀀텀을 나눠 각자 따로 배포·확장할 수 있는가

브로드캐스팅 능력

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)키 기반(key-based) 이 두 극단이고, 실무는 대개 이 스펙트럼 위 어딘가에 놓입니다. 이벤트 유형마다 다른 방식을 섞어 써도 되며, 시스템 전체를 한 가지로 통일할 필요는 없습니다.

데이터 기반 이벤트 페이로드

데이터 기반 이벤트 페이로드(data-based event payload) 는 다운스트림 처리기들이 필요로 하는 모든 데이터를 이벤트에 실어서 보내는 방식입니다. 주문 접수 서비스가 45개 속성·500 KB 크기의 주문 접수됨 이벤트를 발행하면, 결제 처리기는 이벤트에서 주문 ID·고객 정보·총액만 뽑아 결제를 처리하고, 재고 처리기는 같은 이벤트에서 상품 ID·수량만 뽑아 재고를 조정합니다.

{
  "order_id": "123",
  "cust_id": "456",
  "item_id": "A",
  "quantity": 2,
  "desc": "...",
  "total_amt": 29900,
  "cust_name": "...",
  "addr_1": "...",
  "addr_2": "...",
  "status": "PLACED"
}

가장 큰 장점은 구독자가 원본 DB에 접근할 필요가 없다 는 점입니다. DB 조회 빈도가 낮을수록 반응성·성능·확장성이 좋아지고, 생산자는 누가 이 이벤트에 반응하는지 알 필요조차 없습니다. 도메인별·서비스별로 DB가 분리된 경계 컨텍스트가 엄격한 토폴로지 에서는 사실상 유일하게 현실적인 선택입니다.

단점 1 — 다중 기록 시스템 일관성

이벤트에 데이터를 복사해 보내는 순간 다중 기록 시스템(multiple system of record) 이 됩니다. 원본 DB와 “이미 떠나버린 이벤트 페이로드” 두 곳이 서로 다른 진실을 갖게 되어 데이터 일관성과 무결성 이 깨지기 쉽습니다.

예를 들어 고객이 상품을 100개 주문했다가 직후에 1개로 정정하면, 원본 DB는 1개로 갱신되지만 브로커 큐에 이미 들어간 이벤트 페이로드에는 여전히 100개가 남아 있습니다. EDA에서는 이벤트 타이밍을 제어하기가 매우 어렵기 때문에 새 값이 이전 값보다 먼저 처리되는 역전 현상도 가능하고, 그 경우 잘못된 값이 올바른 값을 덮어씁니다.

단점 2 — 계약과 버전 관리 부담

페이로드에 여러 필드를 실으려면 계약(contract) 이 필요합니다. JSON/XML/Avro 스키마, 엄격함 수준, 버전 헤더까지 전부 결정해야 합니다. 엄격한 계약(strict contract) 은 JSON 스키마·GraphQL 명세·클래스 정의처럼 형식화된 스키마를 쓰고, 느슨한 계약(loose contract) 은 키-값 쌍의 단순한 JSON을 씁니다. 엄격할수록 구독자들 간에 정적 결합이 강해집니다.

버전 관리는 더 까다롭습니다. 벤더 MIME 형식으로 버전을 지정하면 하위 호환성이 보장되지만, 모든 처리기가 같은 버전 관리 로직을 써야 하므로 강력한 거버넌스 가 필수입니다. 고도로 분리되고 비동기적인 EDA에서는 버전 협상과 사용 중지(deprecation) 전략 을 구현하기가 매우 어렵고, 어떤 처리기가 계약 버전을 무시하면 스키마 변경 시 조용히 실패할 가능성이 높습니다.

단점 3 — 스탬프 결합(stamp coupling)

스탬프 결합(stamp coupling) 은 여러 모듈이 공통 자료 구조를 공유하되 각자 일부 필드만 사용할 때 생기는 정적 결합입니다. 45개 속성 중 item_id·quantity 두 개만 쓰는 재고 처리기도, 자기가 쓰지도 않는 addr_2 속성 하나가 제거되면 다시 테스트하고 배포해야 합니다.

flowchart LR
    OP[주문 접수 서비스] --> E([주문 접수됨<br/>45개 속성 페이로드])
    E --> INV[재고 서비스]
    INV -. 실제 사용: item_id, quantity .-> USE[(처리)]

소비자 주도 계약(consumer-driven contract)으로 완화할 수 있지만, EDA는 생산자가 누가 반응하는지 알지 못한 채 브로드캐스팅 하는 특성 때문에 소비자 주도 계약 기법과 잘 맞지 않습니다. 이 점이 많은 아키텍트가 키 기반으로 기울게 만드는 이유입니다.

단점 4 — 대역폭 사용률

클라우드 환경에서 가장 비용이 많이 드는 자원 중 하나는 대역폭 입니다. 500 KB짜리 데이터 기반 이벤트를 초당 500건 발행하면 재고 처리기 한 곳만 해도 초당 250,000 KB(≈ 244 MB/s) 를 소비합니다. 실제로 필요한 30 바이트(item_id + quantity) 만 실었다면 초당 15 KB로 줄어듭니다. 수천 배 차이이므로, 데이터 기반을 쓸 때는 반드시 이 비용을 계산해봐야 합니다.

키 기반 이벤트 페이로드

키 기반 이벤트 페이로드(key-based event payload) 는 이벤트의 맥락을 식별하는 키(주문 ID·고객 ID 등)만 담습니다. 구독자는 처리에 필요한 실제 정보를 원본 서비스의 DB에서 조회합니다.

{ "order_id": "123" }

가장 큰 장점은 단일 기록 시스템(single system of record) 이 유지된다는 점입니다. 데이터가 DB 한 곳에만 있으므로 일관성·무결성 문제가 크게 줄어들고, 이벤트 처리 중에 원본이 바뀌어도 구독자는 조회 시점의 최신 값을 읽습니다.

계약도 매우 간단해서 거의 바뀌지 않습니다. 스키마 진화·버전 협상·사용 중지 고민이 사실상 사라지므로 느슨하고 스키마 없는 JSON/XML 로도 충분합니다. 이벤트에 불투명한 데이터가 없으니 스탬프 결합도 없고, 대역폭 사용량도 아주 적어 네트워크와 메시지 브로커 관점에서 더 빠르게 수행 됩니다.

키 기반의 단점 — 잦은 DB 조회

단점은 명확합니다. 모든 구독자가 자기 처리에 필요한 데이터를 DB에서 꺼내 와야 합니다. 주문 접수됨 키 이벤트에 대해 결제 처리기는 결제 정보를, 재고 처리기는 품목·재고를 각각 조회해야 합니다. 빈번한 DB 쿼리는 반응성·성능·확장성을 떨어뜨리고, 특히 고도로 병렬적이고 비동기적인 EDA에서는 DB가 쉽게 병목 이 됩니다. 필요한 데이터가 다른 서비스의 경계 컨텍스트 안에 있어 접근 자체가 불가능한 경우 에도 곤란해집니다.

트레이드오프 요약
기준데이터 기반키 기반
성능·확장성좋음나쁨
계약 관리나쁨좋음
스탬프 결합나쁨좋음
대역폭 사용률나쁨좋음
제한된 DB 접근 대응좋음나쁨
전반적 시스템 취약성나쁨좋음

핵심은 확장성·성능 대 계약 관리·대역폭 의 줄다리기라는 점입니다. 극한의 규모·성능이 필요한 이벤트에는 데이터 기반이, 처리에 필요한 데이터가 자주 바뀌는 이벤트에는 키 기반이 어울립니다. 실무에서는 두 방식을 혼용 해서 자주 쓰는 요약 필드는 페이로드에 싣고 상세는 키로만 전달해 필요할 때 조회합니다.

빈혈성 이벤트 (스펙트럼의 덫)

페이로드 설계는 키 기반(왼쪽) ↔ 데이터 기반(오른쪽) 스펙트럼 위에서 한 지점을 고르는 일이라는 걸 잊으면 빈혈성 이벤트(anemic event) 안티패턴에 빠집니다. 빈혈성 이벤트는 처리기가 의사결정을 내릴 충분한 맥락이 없는 파생 이벤트입니다.

고객 프로필 서비스가 프로필을 갱신한 뒤 고객 ID만 담은 프로필 갱신됨 이벤트를 발행한다고 합시다. 구독자는 다음을 알 수 없습니다.

  • 구체적으로 어떤 필드가 바뀌었는가? (이름? 주소? 전화번호?)
  • 내가 이 변경에 반응해야 하는가?
  • 변경 전 값은 무엇이었는가?

대부분의 DB는 “어떤 필드가 변경됐는지”와 “이전 값”을 조회 결과로 주지 않으므로, 구독자는 이벤트 처리 자체에 실패합니다. 이를 피하려면 갱신된 값뿐 아니라 변경된 필드 목록과 이전 값 까지 페이로드에 실어 스펙트럼의 적절한 중간 지점에 놓아야 합니다.

키만 얇게 담는 건 주문 생성이나 삭제 처럼 맥락이 자명할 때만 잘 작동합니다.

하루살이 떼 안티패턴

빈혈성 이벤트가 이벤트 페이로드의 세분도 문제라면, 하루살이 떼(Swarm of Gnats)이벤트 자체의 세분도(한 이벤트 처리기가 발생시키는 파생 이벤트의 수)에 관한 문제입니다.

파생 이벤트를 너무 잘게 쪼개 발행하면 얼굴 주위를 윙윙거리는 하루살이처럼 시스템 전체를 산만하게 만들어 이벤트 흐름을 아무도 추적하지 못하게 됩니다.

반대로 너무 적게 쪼개도 문제입니다. 세분도는 스펙트럼이고, 양쪽 극단 모두 피해야 합니다. 핵심 원칙은 “처리·상태 변경의 결과(outcome) 단위로 묶는다” 입니다.

극단 A — 세분도가 너무 낮은 경우 (신용카드 사기 검사)

고객이 주문을 넣고 신용카드로 결제하면 결제 서비스가 결제 적용됨 이벤트를 발행하고, 사기 탐지 서비스가 이를 받아 사기 여부를 판정한 뒤 단 하나의 사기 검사됨 이벤트를 발행하는 설계를 봅시다. 사기 여부는 페이로드 안에 플래그로 담습니다.

flowchart LR
    PAY[결제 서비스] --> E1([결제 적용됨])
    E1 --> FD[사기 탐지 서비스]
    FD --> E2([사기 검사됨<br/>payload: is_fraud])
    E2 --> CCL[신용카드 잠금 서비스]
    E2 --> CN[고객 알림 서비스]
    E2 --> PP[구매 프로필 서비스]

세 구독자(신용카드 잠금·고객 알림·구매 프로필 서비스)는 자기가 이 이벤트에 반응해야 할지 판단하기 위해 모두 페이로드를 열어 플래그를 확인 해야 합니다. 사기가 아니라면 구매 프로필 처리기만 알고리즘을 갱신하면 되지만, 그걸 결정하기 위해 세 처리기 모두가 이벤트를 수신·파싱·분기 판정을 수행합니다.

대역폭과 처리 능력의 낭비입니다.

개선은 파생 이벤트를 결과 단위로 분기 하는 것입니다.

flowchart LR
    PAY[결제 서비스] --> E1([결제 적용됨])
    E1 --> FD[사기 탐지 서비스]
    FD --> E2([사기 탐지됨])
    FD --> E3([사기 탐지되지 않음])
    E2 --> CCL[신용카드 잠금 서비스]
    E2 --> CN[고객 알림 서비스]
    E3 --> PP[구매 프로필 서비스]

사기 탐지됨사기 탐지되지 않음 두 파생 이벤트를 따로 발행하면, 맥락이 페이로드 바깥(이벤트 이름 자체) 에 드러납니다. 각 처리기는 페이로드를 들여다보지 않고도 반응 여부를 결정합니다. 흐름이 명시적이 되고 불필요한 처리가 사라집니다.

극단 B — 세분도가 너무 높은 경우 (프로필 필드별 이벤트)

반대 극단도 위험합니다. 고객이 이사로 인해 프로필(청구지·배송지·전화번호)을 한 번에 바꾸고 저장 버튼을 누르는 시나리오에서, 고객 프로필 서비스가 “필드 하나당 하나의 이벤트”로 발행한다고 합시다.

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

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

개선은 개별 상태 변경을 결과 단위로 통합 하는 것입니다.

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

프로필 갱신됨 이벤트 하나로 묶고, 변경된 모든 필드의 이전/이후 값을 페이로드에 실어 보냅니다. 구독자는 자기가 관심 있는 필드만 페이로드에서 꺼내 쓰면 됩니다. (빈혈성 이벤트를 피하기 위해서도 이전/이후 값이 필요합니다.)

원칙 — 결과(outcome)에 초점

두 극단을 관통하는 원칙은 같습니다. 이벤트는 “어떤 상태가 바뀌었나”가 아니라 “어떤 결과가 일어났나”에 초점을 맞춥니다.

  • 사기 검사 의 결과는 “탐지됨” 또는 “탐지되지 않음” 두 가지 — 이 결과 단위로 파생 이벤트를 분기합니다
  • 프로필 변경 의 결과는 “프로필이 갱신됨” 하나 — 내부 필드 변경은 페이로드로 전달합니다

결과 단위로 끊으면 구독자들이 페이로드를 뒤질 필요도, 같은 사안에 대해 여러 이벤트를 조립할 필요도 없어집니다. 이벤트 수와 페이로드 크기 양쪽이 적정선에 놓이고, 시스템 전체의 흐름이 사람이 읽을 수 있는 수준으로 유지됩니다.

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

비동기 요청에서는 오류가 발생해도 동기적으로 반응할 사용자가 없습니다. 예외를 로그에 찍는 것 말고는 방법이 없어서, 데이터 손실·처리 지연·혼란이 생깁니다. 작업흐름 이벤트(Workflow Event) 패턴 은 이 공백을 메우는 반응형 아키텍처 계열의 표준 기법으로, 회복탄력성(resiliency)과 반응성 을 동시에 달성합니다.

세 가지 능력 — 위임·봉쇄·복구

패턴의 핵심은 작업흐름 대리자(workflow delegate) 라는 컴포넌트입니다. 이벤트 소비자는 오류가 발생하면 문제 해결에 시간을 쏟지 않고 즉시 대리자에게 넘기고 다음 메시지로 넘어갑니다. 이 과정에서 시스템에 세 가지 능력이 도입됩니다.

능력의미구현 방식
위임(delegation)오류를 전담 컴포넌트로 넘김소비자가 별도 오류 대기열에 메시지를 넣고 다음으로 넘어감
봉쇄(containment)한 건의 오류가 전체 처리를 막지 못하도록 격리소비자는 오류 해결에 관여하지 않으므로 뒷 메시지들의 반응성 유지
복구(repair)자동 또는 수동으로 원본을 고쳐 재제출대리자가 프로그래밍적으로 수정 → 원래 대기열 재제출, 불가 시 담당자 대시보드를 통해 처리

소비자가 직접 오류를 처리하면 봉쇄가 깨집니다. 오류 해결 동안 뒷 메시지들이 줄줄이 지연되어 전체 반응성이 망가지기 때문입니다. 그래서 “소비자는 오류 해결에 시간을 쓰지 않는다” 가 이 패턴의 제1규칙입니다.

flowchart LR
    P[이벤트 생산자] --> EC[(이벤트 채널)]
    EC --> C[이벤트 소비자]
    C -. 오류 발생 시 위임 .-> WQ[(오류 대기열)]
    WQ --> W[작업흐름 처리기]
    W -- 자동 복구 후 재제출 --> EC
    W -. 복구 불가 .-> D[담당자 대시보드]
    D -- reply-to 원래 대기열 --> EC

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

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

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

2WE35HF6DHF,BUY,AAPL,8756 SHARES

마지막 필드가 Long 타입이어야 하는데 "8756 SHARES" 처럼 단어가 붙어 있어 NumberFormatException 이 터집니다. 작업흐름 이벤트 패턴 없이는 거래 주문 서비스가 로그만 찍고 끝이고, 그 주문은 조용히 사라집니다.

패턴을 적용하면 오류를 거래 오류 처리 서비스(작업흐름 대리자) 에 위임해 6단계로 복구합니다.

flowchart LR
    TA[거래 중개인] --> EC[(거래 대기열)]
    EC --> TP[거래 주문 서비스]
    TP -. ② 오류 발견<br/>③ 위임 .-> EQ[(오류 대기열)]
    EQ --> EP[거래 오류 처리 서비스]
    EP -- ④ 수정<br/>⑤ 재제출 --> EC
    TP -. ⑥ 재처리 성공 .-> OK[정상 처리]
  1. 거래 중개인이 비동기로 거래 주문들을 보냅니다
  2. 거래 주문 서비스가 포맷 계약 위반을 발견합니다
  3. 오류 주문을 오류 대기열로 보내고 곧바로 다음 주문으로 넘어갑니다 (봉쇄)
  4. 거래 오류 처리 서비스가 "8756 SHARES""8756" 로 수정합니다
  5. 수정된 주문을 원래 대기열로 재제출합니다
  6. 거래 주문 서비스가 수정된 주문을 문제없이 처리합니다
메시지 순서 유지 트릭

작업흐름 이벤트 패턴의 숨은 함정은 메시지 순서가 깨진다 는 점입니다. 오류 주문이 대리자를 거쳐 돌아오는 사이에 뒤 주문들이 먼저 처리되기 때문입니다. 같은 증권 계좌의 거래가 IBM SELL → AAPL BUY 순서로 처리되어야 하는데 AAPL BUY가 먼저 처리되면 잔고 계산이 꼬입니다.

해결책은 같은 맥락(계좌 번호 등)의 뒷 거래들을 임시 대기열에 FIFO로 저장 해 두고 오류 복구 후 순서대로 꺼내는 것입니다. 거래 주문 서비스는 오류 발생 계좌의 후속 거래를 별도 큐(메모리 또는 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[개시 이벤트] --> Q0[개시 대기열] --> EM{{이벤트 중재자}}
    EM -- create-order --> C1[이벤트 채널] --> H1[주문 접수 처리기]
    EM -- apply-payment --> C2[이벤트 채널] --> H2[결제 처리기]
    EM -- adjust-inventory --> C3[이벤트 채널] --> H3[재고 처리기]
    EM -- ship-order --> C4[이벤트 채널] --> H4[배송 처리기]

컴포넌트는 개시 이벤트, 이벤트 대기열, 이벤트 중재자, 이벤트 채널, 이벤트 처리기 로 구성됩니다. 중재자는 각 단계에 해당하는 파생 메시지(derived message) 를 생성해 전용 채널로 점대점 전송하고, 처리기는 작업 결과를 중재자에게만 돌려줍니다. 코레오그래피형과 달리 처리기가 추가 파생 이벤트를 시스템에 브로드캐스팅하지 않습니다.

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

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

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

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

도메인별 다중 중재자로 SPOF 회피

중재자 토폴로지 구현에서는 중재자가 하나가 아니라 여러 개 인 경우가 많습니다. 각 중재자는 특정 도메인이나 이벤트 그룹을 전담합니다.

  • 고객 중재자: 신규 가입, 프로필 업데이트 등 고객 관련 이벤트
  • 주문 중재자: 장바구니 항목 추가, 결제 등 주문 관련 활동

이렇게 도메인별로 쪼개면 단일 장애 지점(SPOF) 위험을 낮추고 전반적 처리량·성능이 올라갑니다. 중재자 토폴로지의 태생적 약점인 중앙 집중 병목을 도메인 경계로 수평 분할해 완화하는 현실적 처방입니다.

이벤트 복잡도 분류: 단순 / 어려움 / 복잡

이벤트 중재자 구현 방식을 고를 때 가장 중요한 기준은 중재자가 다뤄야 할 메시지의 성격과 복잡성 입니다. 모든 이벤트가 한 유형에 깔끔히 들어맞는 경우는 드물기 때문에, 복잡도를 세 단계로 분류해 각각에 맞는 중재자에 위임하는 모델이 널리 쓰입니다.

복잡도특징구현 도구
단순(simple)선형 단계 나열, 가벼운 오류 처리, 약간의 오케스트레이션Apache Camel, Mule ESB, Spring Integration
어려움(hard)조건부 분기, 복잡한 오류 처리, 다중 동적 경로Apache ODE, Oracle BPEL Process Manager (BPEL 기반)
복잡(complex)사람 개입, 장기 실행 트랜잭션, 수동 승인jBPM 같은 BPM(Business Process Management) 엔진
BPEL과 BPM의 자리

BPEL(Business Process Execution Language) 은 이벤트 처리 단계들을 XML 유사 구조로 선언하는 언어로, 오류 처리·재지정(redirection)·다중 캐스팅(multicasting) 같은 구조적 요소를 제공합니다. 강력하지만 배우기 까다로워서 보통 BPEL 엔진 제품군의 GUI 도구로 중재자를 구현합니다. 복잡하고 동적인 작업흐름에는 적합하지만, 사람 개입이 필요하거나 오래 실행되는 트랜잭션에는 부적합 합니다.

BPM 엔진(jBPM 등) 은 바로 그 빈자리를 채웁니다. 주식 거래에서 거래 주문 접수됨 개시 이벤트가 들어왔는데 거래 수량이 한도를 넘어서 수석 트레이더의 수동 승인을 기다려야 한다면, 중재자는 처리를 중단하고 승인을 받을 때까지 대기해야 합니다. 이런 human-in-the-loop 시나리오에는 BPM 엔진이 적합합니다.

반대 조합은 낭비입니다. 복잡한 인간 상호작용이 필요한 이벤트에 Apache Camel을 쓰는 것은 매우 어렵고 유지보수도 힘듭니다. 단순한 이벤트 흐름에 BPM 엔진을 쓰는 것은 Camel로 며칠에 끝낼 일에 몇 달을 낭비하는 셈입니다.

중재자 위임 모델

실무 권장 구조는 모든 이벤트를 일단 단순 중재자에 보내고, 단순 중재자가 복잡도에 따라 자기가 직접 처리하거나 BPEL·BPM 중재자에 위임 하는 계층형 모델입니다.

flowchart TB
    IE[개시 이벤트] --> SQ[이벤트 대기열] --> SM{{단순 이벤트 중재자<br/>소스 코드}}
    SM -- 직접 처리 --> EH1[이벤트 처리기]
    SM -- 어려운 이벤트 위임 --> HM{{어려운 이벤트 중재자<br/>BPEL}}
    SM -- 복잡한 이벤트 위임 --> CM{{복잡한 이벤트 중재자<br/>BPM}}
    HM --> EH2[이벤트 처리기]
    HM --> EH3[이벤트 처리기]
    CM --> EH4[이벤트 처리기]
    CM --> EH5[이벤트 처리기]

단순 중재자는 위임한 작업흐름의 완료 시점을 알아야 할 수도 있고(예: 클라이언트 알림), 작업흐름 전체를 다른 중재자에 넘기고 잊을 수도 있습니다. 어느 쪽을 선택할지는 개시 이벤트에 대한 회사의 구체적 업무 규칙 에 따라 갈립니다.

실전 예제: 5단계 주문 처리 작업흐름

소매 주문 시스템을 중재자 토폴로지로 다시 설계하면 중재자는 주문 처리에 필요한 단계들 을 알고 있습니다.

flowchart TB
    S1[단계 1: 주문 접수<br/>- 주문 생성]
    S2[단계 2: 주문 처리<br/>- 접수 알림 이메일<br/>- 결제 적용<br/>- 재고 수량 감소]
    S3[단계 3: 주문 이행<br/>- 상품 포장<br/>- 필요 시 공급업체 발주]
    S4[단계 4: 주문 배송<br/>- 배송 준비 알림<br/>- 고객에게 배송]
    S5[단계 5: 고객 알림<br/>- 배송 완료 알림]
    S1 --> S2 --> S3 --> S4 --> S5

단계들은 순차로 진행되지만 각 단계 내부의 작업들은 동시에 처리 됩니다. 단계 4의 배송 알림 이메일 전송과 실제 배송 시작은 병렬로 가능합니다.

PlaceOrder 개시 이벤트를 고객 중재자가 받아 단계별 파생 메시지를 생성합니다.

  • 단계 1 (주문 생성): 중재자가 create-order 메시지를 발행. 주문 접수 처리기가 주문을 검증·생성하고 중재자에게 주문 ID와 승인 메시지를 돌려줍니다. 이 시점에 고객에게 주문 ID를 알려 접수 사실을 바로 전달할 수도 있고, 전체 단계가 끝날 때까지 기다릴 수도 있습니다.
  • 단계 2 (주문 처리): email-customer(접수 알림), apply-payment(결제 적용), adjust-inventory(재고 조정) 세 파생 메시지를 동시에 전송해 알림, 결제, 재고 처리기가 병렬 실행합니다. 세 처리기의 성공 응답을 모두 받은 뒤에만 단계 3으로 넘어갑니다. 하나라도 실패하면 중재자가 문제를 바로잡는 작업을 진행합니다.
  • 단계 3 (주문 이행): fulfill-order(주문 이행 처리기)와 order-stock(창고 처리기) 두 파생 메시지를 병렬 전송. 두 처리기의 확인 메시지를 받으면 단계 4로 전진합니다.
  • 단계 4 (주문 배송): ship-order(배송 처리기)와 email-customer(배송 준비 알림) 두 파생 메시지를 생성합니다.
  • 단계 5 (고객 알림): 다시 email-customer 를 발행해 배송 완료를 알리고 작업흐름을 완료(complete) 로 표시한 뒤 개시 이벤트 관련 상태를 제거합니다.

동일한 email-customer 메시지라도 단계별로 맥락이 다르게 사용 된다는 점이 중요합니다. 접수 알림, 배송 준비 알림, 배송 완료 알림은 모두 같은 채널을 공유하지만 중재자가 다른 시점에 다른 페이로드로 호출합니다.

오류 복구의 구체 사례

중재자가 작업흐름을 소유 한다는 말의 실질적 의미는 오류 복구 시나리오에서 드러납니다. 단계 2에서 고객의 신용카드가 만료되어 결제가 적용되지 않았다고 하면, 중재자는 결제 적용(단계 2)이 완료돼야 주문 이행(단계 3)을 시작할 수 있음을 알고 있으므로 작업흐름을 중단하고 요청의 상태를 자체 영구 저장소에 기록합니다. 이후 고객이 결제 수단을 업데이트해 결제가 적용되면, 작업흐름을 중단된 지점(3단계 시작)부터 재시작 할 수 있습니다.

이 수준의 오류 복구가 코레오그래피형에서는 곤란합니다. 단일 지점에서 상태를 복원·재개할 주체가 없기 때문입니다.

단점: 선언적 모델링의 한계와 하이브리드

중재자 토폴로지가 코레오그래피형의 문제를 해결하는 대신 새로운 제약을 가져옵니다.

  • 선언적 모델링이 어려운 동적 처리: 복잡한 이벤트 흐름 안에서 발생하는 동적 분기·예외를 선언적으로 모델링하기가 매우 어렵습니다. 이 때문에 중재자 토폴로지에는 일반적 흐름만 선언 해두고, 재고 부족이나 비전형적 오류 같은 동적·복잡 이벤트는 중재자 + 코레오그래피를 결합한 하이브리드(hybrid) 모델 로 처리하는 접근이 흔히 쓰입니다.
  • 중재자 자체도 확장 필요: 처리기들은 얼마든지 확장할 수 있지만 중재자 자체가 병목이 되므로 도메인별 분할·수평 확장을 설계해야 합니다.
  • 처리기 분리도 저하: 중재자가 처리기 호출 순서와 조건을 알기 때문에, 코레오그래피형만큼 처리기가 독립적 이지 않습니다. 처리기 시그니처 변경이 중재자 워크플로에 파급됩니다.
  • 성능 저하: 중재자를 거쳐야 하므로 코레오그래피형의 순수 브로드캐스트 성능에는 미치지 못합니다.

결국 코레오그래피형과 중재자 토폴로지의 트레이드오프는 높은 성능·확장성작업흐름 제어·오류 처리 능력 사이의 균형입니다. 중재자 토폴로지의 성능과 확장성도 양호한 편이지만 코레오그래피형만큼 높지는 않다는 사실을 전제해야 합니다.

선택 기준
조건코레오그래피형중재형
워크플로 분기·조건 로직적음많음
처리기 수많음적거나 중간
장애 복구·보상분산 (어려움)중앙 집중 (쉬움)
결합도매우 낮음낮음~중간
확장성매우 높음중재자 병목 가능
가시성낮음 (추적 도구 필수)높음 (중재자가 상태 보유)

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

데이터 토폴로지

EDA에서 이벤트 처리기들이 데이터를 어떻게 나눠 가질 것인가 도 아키텍처 결정의 한 축입니다.

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

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

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

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

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

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

flowchart TB
    OP[주문 접수 처리기] --> OD[(주문 DB)]
    OF[주문 이행 처리기] --> OD
    Pay[결제 처리기] --> PD[(결제 DB)]
    Inv[재고 처리기] --> ID[(재고 DB)]
    Ship[배송 처리기] --> SD[(배송 DB)]

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

전용 데이터 토폴로지

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

flowchart TB
    OP[주문 접수 처리기] --> D1[(주문 접수 DB)]
    OF[주문 이행 처리기] --> D2[(주문 이행 DB)]
    Pay[결제 처리기] --> D3[(결제 DB)]
    Inv[재고 처리기] --> D4[(재고 DB)]
    Ship[배송 처리기] --> D5[(배송 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 설계가 훨씬 수월해집니다.

일반적인 위험

EDA가 가장 먼저 안고 시작하는 네 가지 구조적 위험이 있습니다. 실무에서 마주치는 구체적 함정들은 이 네 가지가 겉으로 드러난 결과라고 봐도 무방합니다.

비결정론적 처리의 부작용

이벤트 처리기가 예상치 못한 파생 이벤트를 발생시키거나, 응답해야 할 이벤트에 반응하지 않을 수 있습니다.

EDA에서는 이벤트 작업흐름이 매우 복잡해지면 어떤 이벤트가 발생했을 때 정확히 어떤 일이 벌어질지 예측하기 어려운 경우가 많습니다. 더불어 이벤트 처리 순서 자체가 기본적으로 보장되지 않으므로, 순서가 중요한 도메인(거래·계좌)에서는 파티셔닝 키(Kafka) 또는 계좌별 FIFO 큐로 순서를 명시적으로 강제해야 합니다.

정적 결합 과다

EDA는 기본적으로 동적 결합도 가 낮지만, 이벤트 페이로드 계약 때문에 정적 결합 은 강해질 여지가 있습니다. 한 이벤트에 어떤 처리기들이 반응하는지 아키텍트가 항상 알 수 있는 것은 아니므로 계약을 변경하기가 쉽지 않고, 페이로드가 바뀌면 여러 처리기에 부정적 영향이 미쳐 전반적 취약성이 증가합니다.

키 기반 이벤트 페이로드 로 이 위험을 완화할 수 있지만, 대신 확장성·성능이 나빠지고 빈혈성 이벤트(anemic events) 가 발생할 여지가 생깁니다. 초기부터 스키마 레지스트리와 계약 테스트를 갖춰 브레이킹 체인지를 CI 게이트에서 차단 하는 것이 근본 처방입니다.

동기적 통신 과다

EDA의 위력은 동적 분리도 가 높기 때문에 생깁니다. 이벤트 처리기들이 계속해서 동기적으로 통신해야 한다면 그 위력이 사라집니다.

시스템 안에 동기 호출이 지배적이라면, EDA가 가장 적합한 아키텍처 스타일이 아니라는 강력한 신호 입니다. 이 경우 서비스 기반 아키텍처나 마이크로서비스처럼 요청·응답을 1급 시민으로 다루는 스타일을 재검토해야 합니다.

전반적 상태 관리의 어려움

개시 이벤트가 완전히 처리되었는지 판단하기가 매우 어렵습니다. EDA의 비결정론적이고 비동기적인 병렬 이벤트 처리 때문입니다.

아키텍트가 최종 처리 지점을 식별해 “종료(ending) 이벤트” 를 정의 하고, 개시 이벤트를 받은 처리기가 그 종료 이벤트를 구독하게 해서 해결할 수도 있지만, 대부분의 경우 최종 처리 지점을 식별하는 것 자체 가 쉽지 않습니다. 결과적으로 “지금 주문이 어느 단계까지 진행됐는가?” 같은 현재 상태조차 파악하기 어려운 경우가 많아, 상관 ID·분산 추적·사가 상태 저장소 같은 장치로 타협할 수밖에 없습니다.

실무 함정 모음

위 네 가지가 실제 시스템에서 구체적 패턴으로 드러나는 모습입니다.

  • 독성 이벤트 방치: 재처리 실패를 격리·감시하는 DLQ와 모니터링이 없으면 단일 이벤트가 처리기 전체를 마비시킬 수 있습니다.
  • 이벤트와 메시지 혼동: 브로드캐스트성 사실(이벤트)을 큐로 보내거나, 특정 워커에게 할 일(메시지)을 토픽으로 방송하면 중복 처리·누락·확장성 문제가 발생합니다.
  • 하루살이 떼 안티패턴: 파생 이벤트를 필드 단위로 너무 잘게 쪼개면 시스템 전체 흐름을 이해할 수 없게 됩니다. 비즈니스 결과 단위로 묶고 페이로드에 상세를 담아야 합니다.
  • 오류를 소비자가 직접 처리: 작업흐름 이벤트 패턴 없이 소비자에서 try-catch로 복구하려 들면 전체 대기열 처리 반응성이 깨집니다.
  • 데이터 손실 지점 누락: 생산자 발행·소비자 수신·DB 저장 세 지점 중 하나라도 보호 장치가 없으면 조용히 데이터가 사라집니다.
  • 요청-응답을 임시 대기열로 남발: 고빈도 시스템에서 요청마다 대기열 생성·삭제는 브로커를 질식시킵니다. CID 기반이 기본.
  • 반합성 이벤트(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 로 구현된다는 점 때문에 주로 기술적으로 분할된 아키텍처 로 간주됩니다. 도메인 경계에 정렬된 전문화된 교차 기능 팀일 때 잘 작동하지만, 기술적 분할 성질 때문에 일부 팀 토폴로지 유형과는 잘 맞지 않습니다.

팀 유형적합도이유
스트림 정렬 팀 (기능 전담 팀)조건부하나의 도메인·하위도메인이 다수의 이벤트 처리기와 파생 이벤트로 구현되므로 동적 요소 전체를 한 팀이 파악하기 쉽지 않습니다. 주문 처리 작업흐름에 새 단계를 추가하려면 여러 이벤트 처리기를 변경하고 파생 이벤트의 발생 방식·시점까지 재구성해야 할 수 있습니다. 아키텍처가 크고 복잡해질수록 효과가 떨어집니다
활성화 팀 (역량 코칭 팀)낮음EDA는 이벤트 처리기들을 파생 이벤트와 그 계약을 기반으로 연동시켜야 하기 때문에 잘 작동하지 않습니다. 활성화 팀은 특정 스트림 안에서만 효과적이라 팀 전체 이벤트 흐름의 이해·관리를 방해할 수 있고, 스트림 정렬 팀과의 조정(coordination) 비용이 과도하게 발생합니다
난해한 하위시스템 팀 (전문 도메인 팀)높음EDA의 분리되고 비동기적인 성질 덕분에 복잡한 처리를 별도 이벤트 처리기로 쉽게 격리할 수 있습니다. 동적 분리도가 높아 스트림 정렬 팀과의 조정은 정적 이벤트 페이로드 계약과 파생 이벤트 수준으로 제한됩니다. Kafka 튜닝, Flink/Kafka Streams, Camunda·Temporal 오케스트레이션 같은 영역이 대표적입니다
플랫폼 팀 (공통 기반 팀)높음기술적 분할 방식 덕분에 공통 도구·서비스·API·작업을 활용하기가 수월합니다. 특히 메시지 브로커, 이벤트 허브·버스, 이벤트 채널 요소 같은 인프라 자산을 플랫폼 영역으로 취급할 때 가치가 극대화됩니다

장단점

아키텍처 특성평가
전반적인 비용높음 ($$$)
분할 방식기술적
퀀텀 개수1 이상
단순성★★☆☆☆
모듈성★★★★☆
유지보수성★★★★☆
테스트성★★☆☆☆
배포성★★★☆☆
진화성★★★★★
반응성★★★★★
확장성★★★★☆
탄력성★★★☆☆
내결함성★★★★★

별 5개 특성이 세 개(반응성·진화성·내결함성)나 되는 만큼 종합적인 평가가 매우 높은 편이지만, 단순성과 테스트성이 2점 이라는 대가가 분명합니다. 고성능·고확장·고내결함 이 필요한 동적 비즈니스 처리에 가장 어울리는 스타일입니다.

반응성·내결함성 ★★★★★

비동기 통신과 고도로 병렬화된 처리 의 조합이 반응성 만점의 근거입니다. 내결함성은 분리되고 비동기적인 이벤트 처리기 덕분에 다운스트림 장애가 상류로 전파되지 않고 큐에 쌓인 이벤트가 나중에 처리되는 구조에서 나옵니다. 최종 일관성(eventual consistency)을 감수할 수 있다면 시스템 전체가 잘 버팁니다.

확장성 ★★★★☆

이벤트 처리기의 프로그래밍적 부하 분산 으로 실현됩니다. 경쟁하는 소비자(competing consumers) 또는 소비자 그룹(consumer group) 패턴으로 처리기를 추가하면 늘어난 부하를 바로 흡수할 수 있습니다. 만점이 아닌 이유는 공유 DB 때문입니다. 이 제약을 푼 예가 공간 기반 아키텍처(별 5개)입니다.

진화성 ★★★★★

새 이벤트 처리기를 붙이는 수준 으로 기능 확장이 끝납니다. 파생 이벤트들이 기능 확장을 위한 연결 고리(hook) 역할을 하기 때문에, 적절한 이벤트와 데이터가 준비되어 있다면 인프라나 기존 처리기를 건드릴 필요가 없습니다.

단순성·테스트성 ★★☆☆☆

둘 다 2점인 근본 원인은 비결정론적 작업흐름(nondeterministic workflow) 입니다. 요청 기반 모델은 처리 흐름이 결정론적이라 경로와 결과를 미리 알 수 있지만, 이벤트 주도 모델은 아키텍트조차 처리기가 어떤 메시지를 생성할지 모를 때 가 있습니다. 이벤트 트리 도표가 수백·수천 시나리오로 뻗어 거버넌스와 테스트가 극도로 어려워집니다.

퀀텀 개수 1 이상

비동기 호출로 통신하더라도 모든 처리기가 하나의 DB 인스턴스를 공유 하면 전부 동일 퀀텀입니다. 요청-응답도 마찬가지입니다. 처리기 A가 B의 응답(예: 주문 ID)이 있어야 다음 단계를 진행할 수 있다면, B가 다운될 때 A도 작업을 못하므로 비동기 메시지로 결합했더라도 두 처리기는 같은 퀀텀 에 속합니다.

단점: 작업흐름 제어와 복구성

EDA가 떠안는 근본 한계는 제어성과 복구성 입니다. 중재자 토폴로지를 쓰지 않는 한 비즈니스 트랜잭션을 모니터링·제어하는 주체가 없어서, 한 서비스에서 장애가 발생해도 다른 서비스들은 이를 알지 못합니다. 주문 시스템 예: 결제 처리기가 다운되어 결제가 실패해도 재고 처리기는 여전히 재고를 조정하고, 나머지 처리기도 모든 것이 정상인 것처럼 각자 작업을 계속합니다.

복구성은 더 나쁩니다. 개시 이벤트 처리 중 여러 작업이 비동기로 실행되기 때문에 개시 이벤트를 다시 제출하는 것은 사실상 불가능 합니다. 부분 실패에 대한 현실적 대안은 보상 트랜잭션(saga) 이나 중재자 토폴로지의 중단·재시작 모델 입니다.

트레이드오프 정리

Quote

반응성·내결함성·진화성을 극한으로 끌어올리는 대가로 단순성·테스트성·작업흐름 제어를 내줍니다. 처리 흐름이 결정론적이어야 하거나 트랜잭션 무결성이 중요한 도메인에는 부적합하고, 동적·비정형적 사용자 처리에 압도적으로 유리합니다.

요청 기반 vs 이벤트 기반 선택

두 모델 중 올바른 쪽을 고르는 것이 성공의 관건입니다. 작업흐름을 확실히 통제·보장해야 하고 데이터가 잘 구조화 되어 있다면 요청 기반 모델 이 낫고, 높은 반응성·확장성이 요구되며 사용자 처리가 복잡·동적 이라면 이벤트 기반 모델 이 맞습니다.

이벤트 기반 — 요청 기반 대비 장점트레이드오프
동적 사용자 콘텐츠에 더 잘 반응최종 일관성만 지원
확장성·탄력성 우월처리 흐름 제어 부족
민첩성·변경 관리 우월이벤트 흐름의 최종 결과 확실성 부족
적응성·확장 능력 우월테스트와 디버깅 어려움
반응성·성능 우월
실시간 의사결정 우월
상황 인식 기반 반응 우월

적합한 상황

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

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

예시

시스템 내부·외부에서 일어나는 사건(이벤트)에 대한 반응 이 중심인 비즈니스 문제라면 EDA의 좋은 후보입니다. 주문을 여러 요소로 분리해 병렬 처리 할 수 있는 시스템도 마찬가지이고, 반응성·성능·확장성·내결함성·탄력성 이 동시에 요구되는 시스템도 적합합니다.

GGG 온라인 경매 시스템

이 책이 반복해서 드는 예시인 고잉·고잉·곤(GGG) 경매 시스템 이 EDA의 위력을 가장 잘 보여줍니다. 이 시스템에서 사용자는 경매에 올라온 물품에 입찰하고, 더 높은 가격을 부르는 입찰자가 나오지 않으면 최종 입찰자가 낙찰받습니다.

EDA가 적합한 이유:

  • 입찰자 수를 미리 알 수 없음 → 확장성과 탄력성 필수
  • 경매 시간 제한이 있고 마감이 가까워질수록 트래픽 급증 → 탄력성이 더욱 중요
  • 실시간 입찰 전파가 필요 → 높은 반응성 요구
  • 무엇보다 결정적: EDA는 입찰을 시스템에 대한 요청이 아니라 “발생한 이벤트”로 간주 합니다. 이 관점의 차이가 전체 설계를 자연스럽게 이벤트 방송·구독 모델로 이끕니다.
flowchart LR
    U["사용자<br/>&quot;이 물품에 $100 입찰&quot;"] --> BC[입찰 수신 서비스]
    BC -. 입찰 접수됨 .-> A[경매 진행 서비스]
    BC -. 입찰 접수됨 .-> BS[입찰 스트리밍 서비스]
    BC -. 입찰 접수됨 .-> BT[입찰자 추적 서비스]
    A --> W[경매 웹사이트]
    BS --> W
    BT --> DB[(DB)]

입찰 작업흐름:

  1. 사용자가 입찰 → 입찰 수신 서비스 가 입찰가를 기존 최고가와 비교 해서 유효하면 입찰 접수됨 이벤트를 발행합니다.
  2. 경매 진행 서비스 가 반응해서 웹사이트의 새 입찰가를 업데이트합니다.
  3. 동시에 입찰 스트리밍 서비스 가 같은 이벤트에 반응해서 웹사이트 입찰 기록이나 개별 입찰자에게 실시간 스트리밍 합니다.
  4. 입찰자 추적 서비스 도 동일 이벤트에 반응해서 추적·감사(audit) 목적으로 입찰자 정보를 저장합니다.

세 처리기가 하나의 이벤트에 동시 반응 하는 구조 덕에 반응성·내결함성·확장성·탄력성이 모두 높게 나옵니다.

EDA로 가치를 보는 다른 도메인
  • 증권·주식 거래 시스템: 주문 → 검증 → 체결 → 청산 → 규제 보고의 각 단계가 이벤트로 연결. 초당 수만~수십만 건을 처리해야 하고, 일부 노드 장애가 거래소 전체를 멈추지 못하게 해야 함
  • IoT 텔레메트리: 수백만 디바이스가 내보내는 이벤트 스트림을 알람·집계·ML 학습·장기 보관 처리기가 동시에 소비
  • 이커머스 주문 처리: 주문 접수 → 결제·재고·알림 병렬 → 이행 → 배송
  • 소셜 피드·알림 플랫폼: 사용자 활동 이벤트가 친구 피드·푸시 알림·추천 학습으로 분배
주의: 요청 기반이면 EDA가 아니다

EDA는 복잡하고 강력한 스타일입니다. 아키텍트는 비즈니스 문제에 필요한 작업흐름과 처리를 면밀히 분석 해서 EDA의 강력함이 그 복잡성을 감수할 만큼 가치가 있는지 판단해야 합니다. 필요한 처리의 대부분이 요청 기반 이라면 Ch18 마이크로서비스 아키텍처 를 고려하는 것이 낫습니다.

내 생각

  • 백엔드에서 결제·주문·알림처럼 시간적 결합을 끊어야 하는 도메인 에서 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)