코딩 · 디자인 패턴
중재자 패턴 예제로 쉽게 이해하기
핵심 답변
중재자(Mediator)는 객체들이 중앙의 중재자 객체를 통해 통신하게 하여 객체 간 직접 의존성을 줄이는 행동 패턴입니다. 컴포넌트가 서로를 호출하는 대신 무언가 일어났음을 중재자에게 알리고, 중재자가 다른 어떤 컴포넌트를 갱신할지 결정합니다. 이렇게 하면 뒤엉킨 다대다 참조 관계가 단순한 일대다 연결로 바뀝니다.
의도와 해결하는 문제
의도: 객체 집합이 상호작용하는 방식을 캡슐화하는 객체를 정의합니다. 중재자는 객체들이 서로를 명시적으로 참조하지 않게 하여 느슨한 결합을 촉진하고, 상호작용을 독립적으로 바꿀 수 있게 합니다. GoF(Gang of Four) 책에 실린 행동 패턴 중 하나입니다.
“회사 계정” 체크박스, 회사명 필드, 이메일 필드, 제출 버튼이 있는 가입 폼을 생각해 봅시다. 체크박스를 선택하면 회사명 필드가 나타나고 필수가 되어야 합니다. 제출 버튼은 모든 필수 필드가 유효할 때만 활성화되어야 합니다. 각 컴포넌트가 다른 컴포넌트를 직접 갱신하면 체크박스는 회사명 필드와 버튼을 알아야 하고, 이메일 필드는 버튼을 알아야 하는 식이 됩니다. 규칙이 늘 때마다 상호 참조가 늘고, 어떤 컴포넌트도 다른 폼에서 재사용할 수 없습니다.
중재자를 두면 컴포넌트는 “내가 바뀌었다”와 같은 이벤트만 보고합니다. 중재자 역할을 하는 폼 객체가 필드들이 서로에게 어떤 영향을 주는지에 대한 모든 규칙을 가집니다. 컴포넌트는 단순하고 재사용 가능해지며, 상호작용 로직은 한 곳에 모입니다.
구조와 참여자
| 역할 | 책임 |
|---|---|
| 중재자(Mediator) | 동료 객체가 이벤트를 보고하려고 호출하는 메서드(예: notify(sender, event))를 가진 인터페이스 |
| 구체 중재자(Concrete mediator) | 모든 동료 객체를 알고 그들 사이의 조정 규칙을 구현합니다 |
| 동료(Colleague) | 중재자에 대한 참조를 가지고, 다른 동료를 호출하는 대신 자신의 이벤트를 보고하는 컴포넌트 |
중재자가 없으면 서로 통신하는 컴포넌트 n개는 최대 n × (n − 1) / 2개의 연결이 필요할 수 있습니다. 중재자가 있으면 각 컴포넌트는 중재자와의 연결 하나만 가집니다.
TypeScript 구현
각 필드는 변경을 중재자에게 보고합니다. SignUpForm이 구체 중재자로, 회사명 필드를 보이거나 숨기고 버튼을 활성화합니다.
interface Mediator {
notify(sender: Component, event: string): void;
}
class Component {
constructor(protected mediator: Mediator, readonly name: string) {}
}
class Checkbox extends Component {
checked = false;
toggle() { this.checked = !this.checked; this.mediator.notify(this, "change"); }
}
class TextField extends Component {
value = "";
visible = true;
type(text: string) { this.value = text; this.mediator.notify(this, "change"); }
}
class Button extends Component { enabled = false; }
class SignUpForm implements Mediator {
isCompany = new Checkbox(this, "isCompany");
company = new TextField(this, "company");
email = new TextField(this, "email");
submit = new Button(this, "submit");
constructor() { this.company.visible = false; }
notify(sender: Component, event: string) {
if (sender === this.isCompany) this.company.visible = this.isCompany.checked;
const emailOk = this.email.value.includes("@");
const companyOk = !this.company.visible || this.company.value.length > 0;
this.submit.enabled = emailOk && companyOk;
console.log(sender.name, event, "-> submit enabled:", this.submit.enabled);
}
}
const form = new SignUpForm();
form.email.type("[email protected]"); // email change -> submit enabled: true
form.isCompany.toggle(); // isCompany change -> submit enabled: false
form.company.type("Acme"); // company change -> submit enabled: trueCheckbox와 TextField는 다른 필드가 존재한다는 사실조차 모릅니다. 다른 중재자를 주면 완전히 다른 폼에서도 이들을 재사용할 수 있습니다.
언제 쓰고 언제 쓰지 말아야 할까
- 객체 그룹이 복잡하지만 잘 정의된 방식으로 통신하고, 그들 사이의 의존성을 따라가기 어려울 때 사용합니다.
- 서로를 참조하는 탓에 지금은 분리할 수 없는 컴포넌트를 재사용하고 싶을 때 사용합니다.
- 상호작용 규칙이 자주 바뀌어 한 곳에서 고치고 싶을 때 사용합니다.
- 두 객체가 서로를 단순히 호출하는 경우에는 사용하지 않습니다. 직접 호출이 더 명확합니다.
- 컴포넌트가 관심 있는 누구에게나 이벤트를 방송하기만 하면 될 때는 사용하지 않습니다. 단순한 이벤트 이미터(옵저버)가 더 가볍습니다.
실제 사용 사례
- GUI 프레임워크의 폼과 대화상자 컨트롤러: 부모 컴포넌트가 상태를 소유하고 자식 입력이 서로에게 어떤 영향을 주는지 결정하며, 자식은 이벤트만 발생시킵니다. React에서 공통 부모로 상태를 끌어올리는 ‘상태 끌어올리기’가 이 발상을 따릅니다.
- 채팅 애플리케이션: 사용자는 서로에게 직접 메시지를 보내지 않습니다. 채팅방이나 서버가 각 메시지를 받아 알맞은 구성원에게 전달합니다.
- 서비스 간 메시지 브로커와 이벤트 버스: 보내는 쪽은 브로커에 게시하고, 브로커는 보내는 쪽이 모르는 수신자에게 메시지를 라우팅합니다.
- 항공 교통 관제가 전형적인 비유입니다. 비행기는 다른 모든 비행기와 협상하지 않고 관제탑하고만 교신합니다.
중재자 패턴과 옵저버 등 다른 패턴 비교
| 패턴 | 중재자 패턴과의 차이 |
|---|---|
| 옵저버 | 옵저버는 단방향 방송입니다. 서브젝트는 임의의 수의 리스너에게 알리며 그들이 무엇을 하는지 모릅니다. 중재자는 동료들을 알고 양방향으로 조정합니다. 중재자는 옵저버를 사용해 구현되는 경우가 많습니다. |
| 퍼사드 | 퍼사드는 클라이언트에게 서브시스템에 대한 더 단순한 인터페이스를 제공하며, 서브시스템은 퍼사드의 존재를 모릅니다. 동료는 자신의 중재자를 알고 그에게 말합니다. |
| 책임 연쇄 | 체인은 요청을 핸들러의 줄을 따라 전달하고, 중재자는 각 이벤트를 알맞은 컴포넌트에 보내는 단일 허브입니다. |
| 커맨드 | 동료는 커맨드 객체를 중재자에게 보낼 수 있고, 중재자가 누가 실행할지 결정합니다. |
자주 묻는 질문
중재자 패턴과 옵저버 패턴의 차이는 무엇인가요?
옵저버에서는 서브젝트가 자신이 알지 못하는 구독자에게 이벤트를 방송하며 통신은 단방향입니다. 중재자에서는 중앙 객체가 참여하는 컴포넌트들을 알고 그들이 서로에게 어떤 영향을 주는지 조정합니다.
이벤트 버스는 중재자인가요?
이벤트를 구독자에게 전달하기만 하는 이벤트 버스는 옵저버, 즉 발행-구독에 더 가깝습니다. 컴포넌트가 서로에게 어떻게 반응할지 결정하는 규칙을 담고 있으면 중재자가 됩니다.
중재자 패턴을 쓰면 코드를 따라가기 더 어려워지나요?
그럴 수 있습니다. 직접 호출은 추적하기 쉽지만 중재를 거치는 호출은 중앙 객체를 통과합니다. 대안이 서로를 참조하는 많은 컴포넌트라면 이 패턴이 값어치를 합니다.
컴포넌트가 중재자를 둘 이상 가질 수 있나요?
보통 컴포넌트는 생성할 때 설정한 중재자 하나를 가집니다. 컴포넌트는 맥락에 따라 다른 중재자와 함께 재사용될 수 있으며, 이것이 이 패턴의 주요 이점 중 하나입니다.