C/C++ 디자인 패턴 (16) — 중재자(Mediator)

가입 화면에서 “약관 동의”를 체크하고, 이메일이 올바르고, “회사 계정”을 체크했다면 회사명까지 입력해야 가입 버튼이 켜진다. 각 위젯이 서로를 직접 참조해 규칙을 처리하면 체크박스가 버튼과 입력란을 알고, 입력란이 버튼을 알고… 위젯 N개 사이에 최대 N(N-1)개의 연결이 생긴다. 규칙 하나를 바꾸려면 여러 위젯 클래스를 고쳐야 하고, 위젯을 다른 화면에서 재사용할 수도 없다. 중재자(Mediator)는 객체들 사이의 상호작용을 한 객체에 캡슐화해서, 동료(colleague) 객체끼리 서로를 직접 참조하지 않게 한다. 이 글에서는 대화상자 중재자, 채팅방 라우팅, C로 만든 온도 조절 컨트롤러를 다룬다.

이 시리즈의 다른 글

구조

참여자역할예제
Mediator동료와 통신하는 인터페이스Mediator::widgetChanged()
ConcreteMediator동료들을 알고 상호작용 규칙을 구현SignupDialog, ChatRoom, struct controller
Colleague다른 동료 대신 중재자와만 통신CheckBox, TextField, User, struct sensor
구분직접 참조중재자
위젯 N개의 연결 수최대 N(N-1)N (각자 중재자 하나)
규칙 변경여러 위젯 클래스 수정중재자 한 곳 수정
위젯 재사용다른 위젯에 묶여 어려움중재자만 바꾸면 가능

대화상자 중재자

// dialog.cpp — 위젯끼리 서로를 모르고, 변화는 중재자에게만 알린다
#include <iostream>
#include <string>

class Mediator;

class Widget {                                            // Colleague
public:
	Widget(Mediator &m, std::string name) : m_(m), name_(std::move(name)) {}
	virtual ~Widget() = default;
	const std::string &name() const { return name_; }
	bool enabled = true;
protected:
	void changed();                                       // "나 바뀌었어"만 알린다
private:
	Mediator &m_;
	std::string name_;
};

class Mediator {
public:
	virtual ~Mediator() = default;
	virtual void widgetChanged(Widget &w) = 0;
};

void Widget::changed() { m_.widgetChanged(*this); }

class CheckBox : public Widget {
public:
	using Widget::Widget;
	void set(bool v) { checked = v; changed(); }
	bool checked = false;
};
class TextField : public Widget {
public:
	using Widget::Widget;
	void type(std::string s) { text = std::move(s); changed(); }
	std::string text;
};
class Button : public Widget { public: using Widget::Widget; };

class SignupDialog : public Mediator {                    // ConcreteMediator: 상호작용 규칙이 전부 여기
public:
	SignupDialog() : agree(*this, "agree"), email(*this, "email"), company(*this, "company"),
			 isCompany(*this, "isCompany"), submit(*this, "submit") { update(); }
	void widgetChanged(Widget &w) override
	{
		std::cout << "  " << w.name() << " changed -> ";
		update();
		std::cout << "company.enabled=" << company.enabled << " submit.enabled=" << submit.enabled << '\n';
	}
	CheckBox agree;
	TextField email, company;
	CheckBox isCompany;
	Button submit;
private:
	void update()
	{
		company.enabled = isCompany.checked;
		bool emailOk = email.text.find('@') != std::string::npos;
		bool companyOk = !isCompany.checked || !company.text.empty();
		submit.enabled = agree.checked && emailOk && companyOk;
	}
};

int main()
{
	SignupDialog d;
	std::cout << std::boolalpha;
	d.email.type("kim@example.com");
	d.agree.set(true);
	d.isCompany.set(true);           // 회사 체크 → 회사명 필수
	d.company.type("Junorion");
}
$ g++ -std=c++20 -Wall -Wextra -O2 -o dialog dialog.cpp && ./dialog
  email changed -> company.enabled=false submit.enabled=false
  agree changed -> company.enabled=false submit.enabled=true
  isCompany changed -> company.enabled=true submit.enabled=false
  company changed -> company.enabled=true submit.enabled=true

위젯은 값이 바뀌면 changed()만 부르고, 어떤 위젯을 켜고 끌지는 SignupDialog::update() 한 곳에서 정한다. 회사 계정을 체크하는 순간 버튼이 다시 꺼졌다가 회사명을 입력하자 켜졌다.

채팅방: 메시지 라우팅 규칙의 집중

// chat.cpp — 사용자는 서로를 모르고 방(중재자)에게만 말한다. 라우팅·음소거 규칙은 방에 있다
#include <iostream>
#include <map>
#include <set>
#include <string>

class ChatRoom;

class User {
public:
	User(std::string n, ChatRoom &r);
	void say(const std::string &msg);
	void whisper(const std::string &to, const std::string &msg);
	void receive(const std::string &from, const std::string &msg) const
	{ std::cout << "  [" << name << "] " << from << ": " << msg << '\n'; }
	const std::string name;
private:
	ChatRoom &room_;
};

class ChatRoom {
public:
	void join(User &u) { users_[u.name] = &u; }
	void mute(const std::string &n) { muted_.insert(n); }
	void broadcast(const User &from, const std::string &msg)
	{
		if (muted_.count(from.name)) {
			std::cout << "  (" << from.name << " is muted)\n";
			return;
		}
		for (auto &[n, u] : users_)
			if (n != from.name)
				u->receive(from.name, msg);
	}
	void direct(const User &from, const std::string &to, const std::string &msg)
	{
		auto it = users_.find(to);
		if (it == users_.end())
			from.receive("room", "no such user: " + to);
		else
			it->second->receive(from.name + " (private)", msg);
	}
private:
	std::map<std::string, User *> users_;
	std::set<std::string> muted_;
};

User::User(std::string n, ChatRoom &r) : name(std::move(n)), room_(r) { r.join(*this); }
void User::say(const std::string &m) { room_.broadcast(*this, m); }
void User::whisper(const std::string &to, const std::string &m) { room_.direct(*this, to, m); }

int main()
{
	ChatRoom room;
	User kim("kim", room), lee("lee", room), park("park", room);
	std::cout << "kim says:\n";     kim.say("빌드 깨졌어요");
	std::cout << "lee whispers:\n"; lee.whisper("kim", "제가 볼게요");
	room.mute("park");
	std::cout << "park says:\n";    park.say("광고 메시지");
	std::cout << "kim whispers:\n"; kim.whisper("choi", "안녕");
}
$ g++ -std=c++20 -Wall -Wextra -O2 -o chat chat.cpp && ./chat
kim says:
  [lee] kim: 빌드 깨졌어요
  [park] kim: 빌드 깨졌어요
lee whispers:
  [kim] lee (private): 제가 볼게요
park says:
  (park is muted)
kim whispers:
  [kim] room: no such user: choi

전체 전송, 귓속말, 음소거, 없는 사용자 처리가 모두 ChatRoom에 있다. User는 다른 사용자 객체를 전혀 모르므로, 음소거 같은 규칙을 추가해도 User는 바뀌지 않았다.

양방향 동기화와 재진입

슬라이더와 스핀박스처럼 서로의 값을 맞추는 중재자는, 자기가 바꾼 결과로 다시 통지를 받는다. 갱신 중임을 표시하는 플래그가 없으면 통지가 끝없이 오간다.

// reentry.cpp — 양방향 동기화에서 중재자 재진입: 보호 없이 vs 갱신 중 플래그
#include <iostream>

struct Sync;

struct Control {                                       // 슬라이더, 스핀박스 공통
	const char *name;
	int value = 0;
	Sync *m = nullptr;
	void set(int v);                                    // 값이 바뀌면 중재자에 통지
};

struct Sync {
	Control slider{"slider"}, spin{"spin"};
	bool guard;
	int calls = 0;
	explicit Sync(bool g) : guard(g) { slider.m = spin.m = this; }
	void changed(Control &c)
	{
		if (++calls > 6) {                              // 데모용 안전장치
			std::cout << "  ... stopped after " << calls - 1 << " notifications\n";
			throw 0;
		}
		if (guard && updating_)
			return;                                     // 내가 바꾼 결과로 돌아온 통지는 무시
		updating_ = true;
		Control &other = (&c == &slider) ? spin : slider;
		std::cout << "  " << c.name << "=" << c.value << " -> set " << other.name << '\n';
		other.set(c.value);
		updating_ = false;
	}
private:
	bool updating_ = false;
};

void Control::set(int v) { value = v; m->changed(*this); }

int main()
{
	for (bool g : {false, true}) {
		std::cout << (g ? "with guard:\n" : "without guard:\n");
		Sync s(g);
		try {
			s.slider.set(42);
			std::cout << "  done, notifications=" << s.calls << " slider=" << s.slider.value
				  << " spin=" << s.spin.value << '\n';
		} catch (int) {}
	}
}
$ g++ -std=c++20 -Wall -Wextra -O2 -o reentry reentry.cpp && ./reentry
without guard:
  slider=42 -> set spin
  spin=42 -> set slider
  slider=42 -> set spin
  spin=42 -> set slider
  slider=42 -> set spin
  spin=42 -> set slider
  ... stopped after 6 notifications
with guard:
  slider=42 -> set spin
  done, notifications=2 slider=42 spin=42

예제는 6번에서 강제로 멈췄지만 실제로는 스택이 넘칠 때까지 계속된다. 플래그를 두면 통지 2번(사용자 입력 1 + 되돌아온 통지 1)으로 끝난다.

C 구현: 온도 조절 컨트롤러

임베디드 펌웨어에서 흔한 구조다. 센서는 측정값을 컨트롤러에 넘기기만 하고, 히터·팬·디스플레이를 어떻게 움직일지는 컨트롤러가 정한다.

/* thermostat.c — C 중재자: 센서·히터·팬·디스플레이는 서로를 모르고 컨트롤러가 규칙을 정한다 */
#include <stdio.h>

struct controller;

struct heater  { int on; };
struct fan     { int speed; };
struct display { char line[64]; };

struct controller {                          /* 중재자 */
	struct heater *heater;
	struct fan *fan;
	struct display *display;
	double target;
};

/* 동료(센서)는 값이 바뀌면 중재자에게만 알린다 */
static void controller_on_temperature(struct controller *c, double t)
{
	c->heater->on = t < c->target - 0.5;
	c->fan->speed = t > c->target + 2.0 ? 2 : (t > c->target + 0.5 ? 1 : 0);
	snprintf(c->display->line, sizeof(c->display->line), "%.1fC heater=%s fan=%d",
		 t, c->heater->on ? "on" : "off", c->fan->speed);
}

struct sensor {
	struct controller *mediator;
	double value;
};

static void sensor_update(struct sensor *s, double v)
{
	s->value = v;
	controller_on_temperature(s->mediator, v);
}

int main(void)
{
	struct heater h = { 0 };
	struct fan f = { 0 };
	struct display d = { "" };
	struct controller c = { &h, &f, &d, 22.0 };
	struct sensor s = { &c, 0 };
	double readings[] = { 19.8, 21.7, 22.4, 23.1, 25.0 };

	for (size_t i = 0; i < sizeof(readings) / sizeof(readings[0]); i++) {
		sensor_update(&s, readings[i]);
		printf("%s\n", d.line);
	}
	return 0;
}
$ gcc -std=c11 -Wall -Wextra -O2 -o thermostat thermostat.c && ./thermostat
19.8C heater=on fan=0
21.7C heater=off fan=0
22.4C heater=off fan=0
23.1C heater=off fan=1
25.0C heater=off fan=2

목표 온도 22°C를 기준으로 히스테리시스(±0.5°C)와 팬 2단계 규칙이 한 함수에 있다. 센서를 다른 칩으로 바꾸거나 디스플레이를 없애도 다른 부품 코드는 건드리지 않는다.

퍼사드·옵저버와 비교

패턴통신 방향동료가 그 객체를 아는가목적
중재자양방향(동료 ↔ 중재자)안다동료 간 협력 규칙 집중
퍼사드단방향(클라이언트 → 하위 시스템)모른다사용 단순화
옵저버단방향 통지(주체 → 구독자)주체는 구독자 목록만 앎변화 전파

실무에서는 동료가 중재자에게 알리는 경로를 옵저버(이벤트)로 구현하는 경우가 많다. 다만 “누구에게 무엇을 전할지”를 한 곳에서 정한다는 점이 중재자의 핵심이다.

주의사항

  • 중재자가 신 객체가 되기 쉽다: 동료 사이의 복잡성이 사라진 게 아니라 중재자로 옮겨졌다. 규칙이 많아지면 중재자를 화면 영역별·기능별로 나눈다.
  • 재진입: 위 실행 결과처럼 통지가 순환할 수 있다. SignupDialog::update()처럼 중재자는 상태만 바꾸고 통지하지 않거나, 갱신 중 플래그를 둔다. 멀티스레드라면 플래그는 스레드별이어야 한다.
  • 생성 순서: SignupDialog는 멤버 위젯에 *this를 넘긴다. 동료의 생성자에서 중재자의 다른 멤버에 접근하면 아직 초기화되지 않은 상태를 읽는다. 생성자에서는 참조 저장만 한다.
  • 수명: ChatRoom은 User*를 들고 있다. 사용자가 먼저 사라지면 방에서 등록을 해제해야 한다(소멸자에서 leave()).
  • 제어 루프의 시간: 온도 조절 예제는 측정마다 규칙을 바로 적용한다. 실제 장치에서는 짧은 시간 안에 히터를 반복해서 켜고 끄지 않도록 최소 유지 시간 같은 규칙이 중재자에 추가된다.

마무리

중재자는 다대다 관계를 일대다 관계 둘로 바꾼다. 동료는 단순해지고 재사용할 수 있게 되며, 협력 규칙은 한 곳에서 읽고 바꿀 수 있다. 다음 글에서는 객체의 내부 상태를 저장했다가 복원하는 메멘토(Memento)를 다룬다.

참고

  • Erich Gamma 외, 『Design Patterns: Elements of Reusable Object-Oriented Software』(GoF), Mediator

답글 남기기