무엇인가

발행자와 구독자가 서로를 모른 채 버스를 통해서만 만난다. 발행자는 “이런 일이 생겼다”고 버스에 던지고, 그 종류를 구독한 쪽이 알아서 받는다.

브로커가 “요청을 알맞은 곳으로 넘긴다”면, 이벤트 버스는 “관심 있는 모두에게 뿌린다.” 응답을 기다리지 않는 것도 차이다.

구조

graph TD
    P1[발행자: 주문 서비스] -->|OrderPlaced| BUS((이벤트 버스))
    P2[발행자: 결제 서비스] -->|PaymentDone| BUS
    BUS -->|OrderPlaced| S1[구독자: 재고 차감]
    BUS -->|OrderPlaced| S2[구독자: 메일 발송]
    BUS -->|PaymentDone| S3[구독자: 배송 준비]

C++로 보기

이벤트 종류를 타입으로 두고, 타입별로 구독자를 모아 둔다.

#include <functional>
#include <iostream>
#include <string>
#include <typeindex>
#include <unordered_map>
#include <vector>
 
class EventBus {
    // 이벤트 타입 → 그 타입을 받는 핸들러들
    std::unordered_map<std::type_index, std::vector<std::function<void(const void*)>>> subs_;
 
  public:
    template <typename Event, typename Fn>
    void subscribe(Fn fn) {
        subs_[std::type_index(typeid(Event))].push_back(
            [fn = std::move(fn)](const void* p) { fn(*static_cast<const Event*>(p)); });
    }
 
    template <typename Event>
    void publish(const Event& e) const {
        auto it = subs_.find(std::type_index(typeid(Event)));
        if (it == subs_.end()) return;          // 아무도 안 듣는다 — 그래도 정상이다
        for (const auto& h : it->second) h(&e);
    }
};
 
// ── 이벤트는 그냥 데이터다 ──
struct OrderPlaced {
    int orderId;
    std::string item;
};
struct PaymentDone {
    int orderId;
    int amount;
};
 
int main() {
    EventBus bus;
 
    // 구독자들은 서로의 존재를 모른다
    bus.subscribe<OrderPlaced>(
        [](const OrderPlaced& e) { std::cout << "[재고] " << e.item << " 차감\n"; });
    bus.subscribe<OrderPlaced>(
        [](const OrderPlaced& e) { std::cout << "[메일] 주문 " << e.orderId << " 확인 발송\n"; });
    bus.subscribe<PaymentDone>(
        [](const PaymentDone& e) { std::cout << "[배송] 주문 " << e.orderId << " 준비\n"; });
 
    // 발행자는 누가 듣는지 모른다
    bus.publish(OrderPlaced{1001, "키보드"});
    bus.publish(PaymentDone{1001, 89000});
    bus.publish(OrderPlaced{1002, "모니터"});
}

구독자를 하나 더 붙이는 데 발행 쪽 코드가 한 줄도 안 바뀐다. 그게 이 패턴을 쓰는 이유다. 동시에 그게 함정이기도 하다 — publish 한 줄만 보고는 무슨 일이 일어나는지 알 수 없다.

언제 쓰나

  • 한 사건에 여러 쪽이 반응해야 하고, 그 목록이 자주 바뀔 때.
  • 모듈 사이 결합을 끊고 싶을 때(플러그인, 확장 지점).
  • UI 이벤트처럼 발생과 처리의 시점을 떼어 놓고 싶을 때.

대가

  • 흐름 추적이 어렵다. 호출 스택이 끊기므로 “왜 이게 실행됐지”를 따라가기 힘들다.
  • 순서 보장이 없다. 구독자끼리 순서에 의존하기 시작하면 그 즉시 취약해진다.
  • 구독 해제를 잊으면 죽은 객체를 부른다. 위 예제는 람다가 값을 캡처해 안전하지만, 객체 포인터를 캡처했다면 수명 관리가 통째로 숙제가 된다.
  • 아무도 안 듣는 이벤트가 조용히 사라진다. 오타가 오류로 안 드러난다.

실제로 만나는 곳

GUI 프레임워크의 이벤트 루프, 게임 엔진의 이벤트 시스템, 마이크로서비스의 이벤트 기반 통신, Qt의 시그널/슬롯.