무엇인가

클라이언트와 서버 사이에 중개자를 두어, 클라이언트가 “누가 이 일을 하는지” 모른 채 무엇이 필요한지만 말하게 하는 구조다. 브로커가 알맞은 서버를 찾아 연결한다.

클라이언트-서버가 “누구에게 요청할지 안다”면, 브로커는 그 앎을 걷어낸다. 서버가 늘고 줄고 옮겨 다녀도 클라이언트는 그대로다.

구조

graph TD
    C1[클라이언트 A] --> B{브로커}
    C2[클라이언트 B] --> B
    B -->|이름으로 찾아 넘김| S1[서비스: echo]
    B --> S2[서비스: sum]
    B --> S3[서비스: upper]
    S1 -.등록.-> B
    S2 -.등록.-> B
    S3 -.등록.-> B

C++로 보기

서버가 자기 이름을 브로커에 등록하고, 클라이언트는 그 이름으로만 부른다.

#include <cctype>
#include <functional>
#include <iostream>
#include <iterator>
#include <numeric>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
 
using Handler = std::function<std::string(const std::string&)>;
 
class Broker {
    std::unordered_map<std::string, Handler> services_;
 
  public:
    void registerService(const std::string& name, Handler h) {
        services_[name] = std::move(h);
    }
    void unregisterService(const std::string& name) { services_.erase(name); }
 
    std::string call(const std::string& name, const std::string& arg) const {
        auto it = services_.find(name);
        if (it == services_.end()) throw std::runtime_error("no such service: " + name);
        return it->second(arg);
    }
};
 
int main() {
    Broker broker;
 
    // 서버들이 스스로 등록한다. 클라이언트는 이 코드를 안 본다.
    broker.registerService("echo", [](const std::string& s) { return s; });
    broker.registerService("upper", [](const std::string& s) {
        std::string out = s;
        for (auto& c : out) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
        return out;
    });
    broker.registerService("sum", [](const std::string& s) {
        std::istringstream in(s);
        std::vector<int> nums{std::istream_iterator<int>(in), std::istream_iterator<int>()};
        return std::to_string(std::accumulate(nums.begin(), nums.end(), 0));
    });
 
    // 클라이언트는 이름만 안다
    std::cout << broker.call("upper", "hello broker") << '\n';
    std::cout << broker.call("sum", "1 2 3 4 5") << '\n';
 
    // 서비스가 사라져도 클라이언트 코드는 그대로다 — 실패가 런타임으로 옮겨갈 뿐
    broker.unregisterService("echo");
    try {
        broker.call("echo", "anyone?");
    } catch (const std::exception& e) {
        std::cout << "실패: " << e.what() << '\n';
    }
}

마지막 블록이 이 패턴의 핵심 대가를 보여 준다. 결합을 끊은 대신 “그런 서비스 없음”이 컴파일 타임이 아니라 런타임 오류가 된다. 이름을 문자열로 다루는 순간 컴파일러의 도움을 포기한 것이다.

언제 쓰나

  • 서비스가 동적으로 늘고 줄 때(스케일 아웃, 배포 중 교체).
  • 클라이언트가 서버의 위치·개수를 몰라야 할 때.
  • 이기종 시스템을 붙일 때. 브로커가 프로토콜 변환 자리를 겸한다.

대가

  • 브로커 자신이 단일 장애점이자 병목이다. 이중화하면 그 자체가 분산 문제가 된다.
  • 한 단계를 더 거치므로 지연이 는다.
  • 위에서 본 대로 오류가 런타임으로 밀린다. 이름 오타가 배포 후에야 드러난다.

실제로 만나는 곳

메시지 브로커(RabbitMQ, Kafka), 서비스 디스커버리(Consul, etcd), CORBA·gRPC 같은 RPC 미들웨어, 그리고 마이크로서비스의 API 게이트웨이.