무엇인가
클라이언트와 서버 사이에 중개자를 두어, 클라이언트가 “누가 이 일을 하는지” 모른 채 무엇이 필요한지만 말하게 하는 구조다. 브로커가 알맞은 서버를 찾아 연결한다.
클라이언트-서버가 “누구에게 요청할지 안다”면, 브로커는 그 앎을 걷어낸다. 서버가 늘고 줄고 옮겨 다녀도 클라이언트는 그대로다.
구조
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 게이트웨이.