회원 가입 대화상자를 하나 짠다고 하자. 체크박스 하나(‘사업자 회원’), 텍스트 필드 하나(‘사업자 번호’), 등급을 고르는 콤보박스, 그리고 ‘가입’ 버튼이 있다. 요구는 소박하다. 체크박스가 켜지면 사업자 번호 필드가 활성화되고, 그 필드가 비어 있으면 가입 버튼이 눌리지 않는다. 등급이 ‘무료’면 사업자 항목 전체가 회색으로 죽는다. 각 위젯은 저 혼자로는 아무 판단도 못 한다. 다른 위젯의 상태를 봐야 자기가 어떻게 굴어야 할지 정해진다.

가장 곧장 떠오르는 구현은 위젯끼리 서로를 필드로 쥐는 것이다. 체크박스는 텍스트 필드를 알고, 텍스트 필드는 버튼을 알고, 콤보박스는 이 셋을 다 안다. 위젯이 넷이면 이 앎의 실선이 사방으로 뻗어 그물이 된다. 위젯을 하나 더 붙이는 순간 그물코가 또 늘고, 이 대화상자를 통째로 다른 화면에서 재사용하려 들면 위젯 하나가 나머지 전부를 끌고 온다. 중재자(Mediator) 패턴은 이 그물을 걷어, 위젯들이 서로가 아니라 가운데 앉은 중재자 하나에게만 말을 걸게 만든다.

무엇이 문제인가 — 동기

문제의 정체는 다대다 결합이다. 서로 상호작용해야 하는 객체가 N개 있고 각자가 상대를 직접 참조하면, 관계의 수는 객체 수의 제곱에 가깝게 자란다. 이 폭발이 세 군데를 동시에 아프게 한다.

첫째, 하나가 나머지 전부를 안다. 체크박스가 자기 상태 변화를 반영하려면 “사업자 번호 필드를 켜라, 버튼 상태를 다시 계산하라”를 스스로 지시해야 하고, 그러려면 그 둘을 필드로 들고 있어야 한다. 위젯 하나가 대화상자의 배선도 전체를 머릿속에 담는다. 상호작용 규칙이 개별 위젯 안에 흩어져 박히니, “언제 버튼이 활성화되는가”라는 하나의 정책이 여러 위젯에 조각조각 나뉜다.

둘째, 재사용이 막힌다. Button은 원래 어디서나 쓰이는 범용 위젯이어야 한다. 그런데 이 대화상자의 협력 규칙이 Button 안으로 새어 들어가면, 그 버튼은 이제 ‘이 대화상자 전용 버튼’이 된다. 협력의 논리가 부품에 눌어붙어, 부품 하나를 떼어 다른 곳에 쓸 수 없다.

셋째, 행동을 바꾸려면 여러 클래스를 연다. “무료 등급이면 사업자 항목을 죽인다”는 규칙 하나를 고치려 해도 콤보박스·체크박스·필드를 다 열어야 한다. 협력 방식이 곧 서브클래싱 없이는 못 바꾸는 형태로 굳는다 — 상호작용을 커스터마이즈하려면 얽힌 위젯들을 전부 상속해야 한다.

판단 기준: 객체 여럿이 서로의 상태를 봐 가며 협력하고, 그 협력 규칙이 개별 객체 안에 흩어져 “관계가 그물이 됐다” 싶으면 중재자를 의심한다. 함정: 객체 둘이 단순히 한 방향으로 참조하는 정도(A가 B를 호출)는 다대다가 아니다 — 그물이 아니라 실선 하나면 중재자는 과잉이다.

패턴의 구조 — 누가 무엇을 하나

중재자는 관계를 객체로 승격시키는 패턴이다. 지금까지 위젯과 위젯 ‘사이’에 흩어져 있던 상호작용을, 그 사이를 전담하는 객체 하나에 몰아 준다. 배역은 넷이다.

Mediator(중재자 인터페이스). 동료들이 “나에게 무슨 일이 생겼다”를 알리는 창구를 선언한다. 대개 changed(Colleague c) 하나. 동료는 자기에게 변화가 생기면 이 메서드로 중재자에게 통지할 뿐, 그다음 누구에게 무엇이 전파되는지는 모른다.

ConcreteMediator(구체 중재자). 협력 규칙 전부가 여기 모인다. 자신이 조율하는 동료들을 전부 참조로 쥐고 있다가, 어떤 동료가 changed로 알려 오면 “누가 바뀌었으니 누구를 어떻게 조정할지”를 판단해 동료들의 상태를 조절한다. 대화상자의 활성/비활성 정책, 값의 연동 규칙이 죄다 이 한 객체 안에 들어온다.

Colleague(동료들). 협력하는 개별 객체들 — 위젯들이다. 결정적인 제약은 이것이다. 동료는 다른 동료를 모른다. 자기가 아는 유일한 상대는 중재자다. 상태가 바뀌면 옆 위젯이 아니라 중재자에게 통지하고, 자기 상태를 바꾸라는 지시도 중재자에게서만 받는다.

협력의 그림이 이렇게 바뀐다. 원래는 위젯끼리 직접 말을 거는 다대다였다. 중재자가 들어오면, 모든 위젯이 중재자 한 점을 향하는 **일대다(성형·star)**가 된다. 위젯 수가 N개라도 관계선은 N개다 — 제곱이 선형으로 내려앉는다. 통신량 자체가 준 것은 아니다. 위젯끼리 직접 주고받던 메시지가 이제 전부 중재자를 경유할 뿐이다. 얽힌 그물을 걷은 대가로, 그 복잡성이 중재자 한 곳에 응축된다. 이 응축이 중재자의 이득이자 위험의 씨앗이다.

판단 기준: 협력 규칙을 “어느 한 객체에 모았을 때 나머지가 서로를 완전히 잊을 수 있는가”를 물어라. 그럴 수 있으면 중재자가 앉을 자리다. 함정: 중재자를 뒀는데도 동료들이 여전히 서로를 필드로 참조하고 있으면, 성형이 아니라 그물 위에 허브를 하나 더 얹은 것이다 — 동료 간 직접 참조를 끊어야 비로소 일대다다.

코드로 — 패턴이 자라나는 과정

위젯끼리 직접 호출하는 그물에서 출발해, 상호작용을 중재자 하나로 걷어 오고, 각 동료가 중재자에게만 통지하도록 바꾸는 과정을 단계로 따라간다.

Refactoring Step 위젯끼리 직접 호출한다 — 관계가 그물이 된다
1/4
// 체크박스가 필드를, 필드가 버튼을 직접 쥔다. 협력 규칙이 위젯 안에 흩어진다.public class BizCheckBox {    private BizNumberField numberField;   // 옆 위젯을 직접 참조    private SubmitButton submitButton;    // 또 다른 위젯도 직접 참조    public void setChecked(boolean checked) {        this.checked = checked;        numberField.setEnabled(checked);          // 사업자면 번호 필드 활성화        submitButton.setEnabled(checked && numberField.isFilled());    }}public class BizNumberField {    private SubmitButton submitButton;    // 필드도 버튼을 직접 안다    public void onTextChanged(String text) {        this.text = text;        submitButton.setEnabled(text != null && !text.isEmpty());    }}// 위젯이 하나 늘면 참조선이 사방으로 뻗는다. "버튼은 언제 켜지나"라는 하나의// 규칙이 체크박스와 필드 두 곳에 쪼개져 박혔다. Button을 다른 화면에 재사용하려// 해도 이 대화상자의 규칙이 딸려 온다.

첫 스텝의 위젯 간 참조선이 사라진 것이 핵심이다. 마지막 상태에서 SubmitButton은 자기가 언제 켜지는지조차 모른다 — 그 판단은 RegisterDialog.changed가 쥐고 있다. 위젯끼리 오가던 통신이 없어진 게 아니라, 전부 중재자를 한 번 경유하도록 경로가 바뀌었을 뿐이다. 그 대신 위젯들은 서로를 완전히 잊었고, 흩어져 있던 협력 규칙은 이름을 얻어(changed가 곧 이 대화상자의 정책 문서다) 한자리에 모였다.

판단 기준: 리팩터링 후 “위젯 하나 추가 = 중재자에 규칙 몇 줄 추가, 기존 위젯 0줄 수정”이 성립하면 중재자가 제대로 앉은 것이다. 함정: changed 안에서 origin이 무엇이냐로 거대한 if-else 사슬이 자라나면, 그물을 없앤 대신 중재자를 분기 덩어리로 키우는 중이다 — 규칙이 복잡해지면 중재자 자체를 분할하거나 상태로 모델링할 때다.

결과 — 무엇을 얻고 무엇을 내주나

중재자가 사는 값은 결합의 재편이다. 다대다가 일대다로 내려앉으니 동료들은 서로를 몰라도 되고, 그래서 순수한 재사용 가능 부품으로 돌아온다. 흩어져 있던 협력 규칙이 중재자 한 곳에 모여, “이 화면이 어떻게 동작하는가”를 한 파일에서 읽고 한자리에서 고친다. 상호작용을 바꾸고 싶으면 동료들을 상속할 것 없이 중재자만 교체하면 된다 — 같은 위젯 묶음에 다른 Mediator를 물리면 다른 대화상자가 된다. 객체 간 프로토콜을 다자간 그물에서 각 동료와 중재자 사이의 일대일 관계로 단순화한 것이 이 이득의 뿌리다.

대가는 만만치 않다. 첫째, 중재자가 God object가 될 위험. 걷어낸 복잡성이 사라진 게 아니라 중재자 한 곳에 응축됐다. 협력 규칙이 늘수록 changed가 부풀어, 동료들은 홀쭉해지는데 중재자만 비대해진다. 개별 위젯을 이해하긴 쉬워졌지만, 중재자 자체는 유지보수하기 어려운 거대 클래스로 자라기 쉽다 — 이것이 중재자의 가장 흔한 최후다. 둘째, 간접성 비용. 위젯 A의 변화가 위젯 B에 닿기까지 반드시 중재자를 경유한다. 직접 호출이면 한 줄로 좇을 흐름이, “A가 중재자에게 알림 → 중재자가 판단 → 중재자가 B를 조정”의 세 단계로 늘어 디버깅 때 한 다리를 더 건너야 한다. 셋째, 중앙집중의 그늘. 모든 협력이 한 점을 지나므로, 중재자가 병목이자 단일 장애점이 된다. 규칙 하나를 잘못 고치면 그 화면의 협력 전체가 흔들린다.

운영비용 렌즈로 보면 순이득의 조건이 분명하다. 상호작용하는 객체가 여럿이고 관계가 실제로 그물일 때, 그물을 걷어 얻는 재사용성과 규칙 집중의 이득이 중재자 비대화의 대가를 넘는다. 반대로 객체가 둘셋이고 관계가 단순하면, 중재자는 있으나 마나 한 우체부를 하나 세워 간접성만 더한다.

판단 기준: 얻는 것(동료 결합 해제·규칙 집중·재사용)과 내주는 것(중재자 비대화·간접성·중앙 병목)을 세어 순이득이 양수일 때만 중재자를 앉힌다. 함정: 협력 규칙이 계속 자라는데도 중재자를 쪼개지 않고 방치하면, 다대다 결합을 없앤 자리에 God object 하나를 세운 것이다 — 그물을 한 점으로 옮겼을 뿐 복잡성은 그대로다.

언제 쓰고, 언제 과용인가

중재자가 정확히 맞는 자리는 이렇다. 객체 여럿이 서로 얽혀 상호작용하고 그 의존이 구조화가 안 된 그물일 때(대화상자·폼의 위젯 협력). 한 객체가 다른 여러 객체를 참조하며 소통하느라 재사용이 막힐 때. 여러 클래스에 흩어진 협력 행동을 서브클래싱 없이 커스터마이즈하고 싶을 때. 교과서적 사례는 관제탑이다 — 공항의 항공기들이 서로 직접 교신하며 이착륙 순서를 맞추면 조합이 폭발하지만, 모든 기체가 관제탑하고만 교신하고 관제탑이 전체를 조율하면 각 기체는 서로를 몰라도 된다. 관제탑이 곧 중재자다.

과용의 냄새는 반대편에서 난다. 객체가 둘셋뿐이고 관계가 단순한데 “결합을 낮춘다”며 중재자를 끼우면, 직접 호출 한 줄로 끝날 일에 통지·판단·조정의 세 다리를 놓는 것이다. 협력 규칙이 계속 자라는데 중재자를 쪼개지 않으면 God object가 태어난다. 중재자를 뒀는데 동료들이 여전히 서로를 참조하고 있으면 그물 위에 허브만 얹은 꼴이다.

여기서 이웃 패턴과의 경계를 그어 둘 필요가 있다. [감시자]는 중재자가 동료의 통지를 받는 메커니즘으로 자주 함께 쓰인다 — 동료가 changed로 직접 부르는 대신, 동료를 Subject로 두고 중재자가 관찰자로 구독하면 통지가 느슨해진다. 둘의 관계는 경쟁이 아니라 협업이다. 반면 [퍼사드]와는 겉모습이 닮아 헷갈리기 쉽다. 둘 다 여러 객체 앞에 창구 하나를 세운다. 갈라지는 것은 방향과 대칭성이다. 퍼사드는 서브시스템으로 들어가는 단방향 문이다 — 클라이언트가 퍼사드를 부르고, 서브시스템 부품들은 퍼사드의 존재조차 모른다. 중재자는 동료들과 양방향·다자간으로 대화한다 — 동료가 중재자에게 알리고 중재자가 동료를 조정하며, 동료들은 중재자를 안다. 퍼사드는 복잡한 하위 시스템을 감싸 단순화하고, 중재자는 대등한 동료들의 협력을 조율한다. “밖에서 안으로 들어가는 문”이면 퍼사드, “안에서 서로를 잇는 허브”면 중재자다.

판단 기준: 여러 객체를 한 창구로 묶되 그 객체들이 창구를 모르면 퍼사드, 창구와 양방향으로 주고받으면 중재자다. 함정: 대등한 동료들의 협력을 조율해야 할 자리에 퍼사드를 쓰면, 부품들이 여전히 서로를 직접 참조한 채 앞에 문만 하나 생긴다 — 결합은 그대로다. “누가 누구를 아는가”의 방향으로 둘을 가른다.

다음으로 메멘토