Mediator 패턴
복잡하게 얽힌 객체들의 소통을 중재자가 대신 관리해주는 Mediator 패턴을 알아봅니다.
#단톡방이 없던 시절을 상상해보자
친구 10명과 여행 계획을 세운다고 해보자.
단톡방이 없으면? 각자가 나머지 9명에게 일일이 연락해야 한다. 한 명이 일정을 바꾸면, 9명에게 각각 알려야 한다. 총 통신 경로는 45개나 된다.

하지만 단톡방이 있으면? 그냥 단톡방에 한 번 말하면 끝이다. 모든 소통이 단톡방이라는 중재자를 통해 이루어진다.
이것이 바로 Mediator 패턴의 핵심이다.
visible: false
Mediator란?
객체들이 서로 직접 통신하지 않고, 중재자(Mediator) 객체를 통해서만 소통하도록 만드는 패턴
객체 간의 복잡한 관계(many-to-many) 를 중재자와의 관계(one-to-many) 로 단순화한다.
핵심 구조는 이렇다.
- Mediator: 소통 규칙을 정의하는 인터페이스
- Concrete Mediator: 실제 조율 로직을 구현
- Colleague: 중재자를 통해 다른 객체와 소통하는 참여자들
visible: false
코드로 보기
채팅방 예제
가장 직관적인 예제부터 시작하자.
// Mediator 인터페이스
interface ChatMediator {
sendMessage(message: string, sender: User): void;
addUser(user: User): void;
}
// Colleague: 채팅 참여자
class User {
constructor(
public name: string,
private mediator: ChatMediator,
) {}
send(message: string): void {
console.log(`${this.name}: "${message}" 전송`);
this.mediator.sendMessage(message, this);
}
receive(message: string, from: string): void {
console.log(`${this.name}이(가) 수신 ← [${from}]: ${message}`);
}
}
// Concrete Mediator: 채팅방
class ChatRoom implements ChatMediator {
private users: User[] = [];
addUser(user: User): void {
this.users.push(user);
console.log(`📢 ${user.name}님이 입장했습니다`);
}
sendMessage(message: string, sender: User): void {
// 보낸 사람을 제외한 모든 유저에게 전달
this.users
.filter((user) => user !== sender)
.forEach((user) => user.receive(message, sender.name));
}
}// 사용
const chatRoom = new ChatRoom();
const alice = new User("Alice", chatRoom);
const bob = new User("Bob", chatRoom);
const charlie = new User("Charlie", chatRoom);
chatRoom.addUser(alice);
chatRoom.addUser(bob);
chatRoom.addUser(charlie);
alice.send("내일 여행 갈 사람?");
// Alice: "내일 여행 갈 사람?" 전송
// Bob이(가) 수신 ← [Alice]: 내일 여행 갈 사람?
// Charlie이(가) 수신 ← [Alice]: 내일 여행 갈 사람?
bob.send("나 갈게!");
// Bob: "나 갈게!" 전송
// Alice이(가) 수신 ← [Bob]: 나 갈게!
// Charlie이(가) 수신 ← [Bob]: 나 갈게!Alice, Bob, Charlie는 서로의 존재를 모른다. 오직 ChatRoom(중재자) 만 알고 있을 뿐이다.

visible: false
실전 예제: 연동되는 폼 필드
실무에서 더 자주 마주치는 상황을 보자. 국가를 선택하면 도시 목록이 바뀌고, 도시를 선택하면 우편번호 형식이 바뀌는 폼이다.
// Mediator
interface FormMediator {
notify(sender: FormField, event: string, data?: any): void;
}
// Colleague: 폼 필드 기본 클래스
abstract class FormField {
constructor(
protected name: string,
protected mediator: FormMediator,
) {}
abstract setValue(value: any): void;
abstract reset(): void;
}
// 국가 선택 필드
class CountrySelect extends FormField {
private value = "";
setValue(country: string): void {
this.value = country;
console.log(`🌍 국가 선택: ${country}`);
this.mediator.notify(this, "countryChanged", country);
}
reset(): void {
this.value = "";
}
}
// 도시 선택 필드
class CitySelect extends FormField {
private options: string[] = [];
private value = "";
setOptions(cities: string[]): void {
this.options = cities;
this.value = "";
console.log(`🏙️ 도시 목록 갱신: [${cities.join(", ")}]`);
}
setValue(city: string): void {
this.value = city;
console.log(`🏙️ 도시 선택: ${city}`);
this.mediator.notify(this, "cityChanged", city);
}
reset(): void {
this.options = [];
this.value = "";
}
}
// 우편번호 필드
class ZipCodeInput extends FormField {
private pattern = "";
private value = "";
setPattern(pattern: string): void {
this.pattern = pattern;
this.value = "";
console.log(`📮 우편번호 형식 변경: ${pattern}`);
}
setValue(zip: string): void {
this.value = zip;
console.log(`📮 우편번호 입력: ${zip}`);
}
reset(): void {
this.pattern = "";
this.value = "";
}
}
// 배송비 표시 필드
class ShippingLabel extends FormField {
setValue(text: string): void {
console.log(`🚚 배송비: ${text}`);
}
reset(): void {
console.log(`🚚 배송비: -`);
}
}이제 중재자가 필드 간의 복잡한 연동 로직을 모두 관리한다.
class AddressFormMediator implements FormMediator {
private cityData: Record<string, string[]> = {
한국: ["서울", "부산", "제주"],
일본: ["도쿄", "오사카", "교토"],
미국: ["뉴욕", "LA", "시카고"],
};
private zipPatterns: Record<string, string> = {
한국: "##### (5자리)",
일본: "###-#### (7자리)",
미국: "##### (5자리)",
};
private shippingCost: Record<string, string> = {
한국: "무료",
일본: "15,000원",
미국: "25,000원",
};
constructor(
private country: CountrySelect,
private city: CitySelect,
private zipCode: ZipCodeInput,
private shipping: ShippingLabel,
) {}
notify(sender: FormField, event: string, data?: any): void {
if (event === "countryChanged") {
// 국가가 바뀌면 → 도시 목록 갱신, 우편번호 형식 변경, 배송비 갱신
const cities = this.cityData[data] ?? [];
this.city.setOptions(cities);
this.zipCode.setPattern(this.zipPatterns[data] ?? "");
this.shipping.setValue(this.shippingCost[data] ?? "확인 필요");
}
if (event === "cityChanged") {
// 도시가 바뀌면 → 우편번호 초기화
this.zipCode.reset();
console.log(`📮 우편번호 초기화 (도시 변경)`);
}
}
}// 사용
const country = new CountrySelect("country", null as any);
const city = new CitySelect("city", null as any);
const zipCode = new ZipCodeInput("zipCode", null as any);
const shipping = new ShippingLabel("shipping", null as any);
const mediator = new AddressFormMediator(country, city, zipCode, shipping);
// mediator 연결 (실제로는 생성자에서 주입)
(country as any).mediator = mediator;
(city as any).mediator = mediator;
// 국가 선택 → 연쇄 반응!
country.setValue("한국");
// 🌍 국가 선택: 한국
// 🏙️ 도시 목록 갱신: [서울, 부산, 제주]
// 📮 우편번호 형식 변경: ##### (5자리)
// 🚚 배송비: 무료
city.setValue("서울");
// 🏙️ 도시 선택: 서울
// 📮 우편번호 초기화 (도시 변경)CountrySelect는 CitySelect의 존재를 모르고, CitySelect는 ZipCodeInput의 존재를 모른다. 모든 연동 로직은 Mediator 안에만 존재한다.
필드를 하나 추가하거나 연동 규칙을 바꿔야 할 때, Mediator만 수정하면 된다.
visible: false
현실 세계의 Mediator
1. 관제탑 (Air Traffic Control)
비행기들이 서로 직접 통신하면 혼란이 생긴다. 모든 비행기는 관제탑을 통해서만 소통한다. 관제탑이 착륙 순서를 정하고, 충돌을 방지하고, 활주로를 배정한다.

2. Redux / Zustand = 상태 중재자
프론트엔드에서 가장 익숙한 Mediator가 바로 상태 관리 라이브러리다.
// Mediator 없이: 컴포넌트끼리 직접 소통
// Header → Cart → ProductList → Sidebar → ...
// 서로가 서로를 참조하는 스파게티 🍝
// Mediator (Redux Store):
// Header → Store ← Cart
// ← ProductList
// ← Sidebar
// 모든 컴포넌트가 Store를 통해서만 소통!
import { create } from "zustand";
// Zustand Store = Mediator
const useStore = create((set) => ({
cart: [],
addToCart: (item) =>
set((state) => ({
cart: [...state.cart, item],
// 다른 관련 상태도 여기서 한번에 업데이트
})),
}));
// 컴포넌트들은 서로를 모른다
// ProductCard → Store.addToCart() → Store가 알아서 모든 구독자에게 알림컴포넌트 A가 상태를 변경하면, Store(중재자) 가 관련된 모든 컴포넌트에 알려준다. A는 B, C, D의 존재를 알 필요가 없다.
3. Event Bus
// 간단한 Event Bus = Mediator
class EventBus {
private handlers = new Map<string, Function[]>();
on(event: string, handler: Function): void {
const list = this.handlers.get(event) ?? [];
list.push(handler);
this.handlers.set(event, list);
}
emit(event: string, data?: any): void {
const list = this.handlers.get(event) ?? [];
list.forEach((handler) => handler(data));
}
}
const bus = new EventBus();
// 모듈들은 서로를 모른다. 오직 EventBus만 안다.
bus.on("order:created", (order) => emailService.sendConfirmation(order));
bus.on("order:created", (order) => inventory.reduce(order.items));
bus.on("order:created", (order) => analytics.track("purchase", order));visible: false
언제 사용하면 좋을까?
-
여러 객체가 복잡하게 서로 참조하고 있을 때
- 컴포넌트 A가 B, C, D를 알고, B가 A, C를 아는 스파게티 상태
-
하나의 변경이 여러 객체에 연쇄적으로 영향을 줄 때
- 폼 필드 연동, UI 상태 동기화
-
객체를 재사용하고 싶은데, 다른 객체들과의 의존성 때문에 어려울 때
- Mediator를 통해 의존성을 끊으면 각 객체를 독립적으로 재사용 가능
visible: false
장단점
| 장점 | 단점 |
|---|---|
| 객체 간 결합도를 크게 줄임 | Mediator가 God Object가 될 수 있음 |
| 객체 간 통신을 한 곳에서 관리 | 중재자 자체가 복잡해질 수 있음 |
| 개별 객체의 재사용성 향상 | 간접 참조로 인한 약간의 성능 오버헤드 |
| OCP — 새로운 Mediator를 추가하기 쉬움 | 단순한 관계에는 오히려 과도한 설계 |
특히 God Object 문제를 조심해야 한다. 모든 로직을 중재자에 몰아넣으면, 중재자 자체가 유지보수 불가능한 괴물이 될 수 있다.
visible: false
정리하며
Mediator는 복잡하게 얽힌 객체들의 소통을 중앙에서 조율하는 패턴이다.
핵심을 한 줄로 요약하면,
"서로 직접 얘기하지 말고, 단톡방에 말해라"
이미 Redux, Zustand, Event Bus를 쓰고 있다면, 당신은 이미 Mediator 패턴의 수혜자다.
visible: false
#Reference
Refactoring Guru - Mediator
refactoring.guru