무엇인가
발행자와 구독자가 서로를 모른 채 버스를 통해서만 만난다. 발행자는 “이런 일이 생겼다”고 버스에 던지고, 그 종류를 구독한 쪽이 알아서 받는다.
브로커가 “요청을 알맞은 한 곳으로 넘긴다”면, 이벤트 버스는 “관심 있는 모두에게 뿌린다.” 응답을 기다리지 않는 것도 차이다.
구조
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의 시그널/슬롯.