jblog
Mediator 패턴
ArchitectureGuru 디자인 패턴 #16

Mediator 패턴

복잡하게 얽힌 객체들의 소통을 중재자가 대신 관리해주는 Mediator 패턴을 알아봅니다.

2026-01-2311 min readdesign-pattern, behavioral, architecture

#단톡방이 없던 시절을 상상해보자

친구 10명과 여행 계획을 세운다고 해보자.

단톡방이 없으면? 각자가 나머지 9명에게 일일이 연락해야 한다. 한 명이 일정을 바꾸면, 9명에게 각각 알려야 한다. 총 통신 경로는 45개나 된다.

혼란

하지만 단톡방이 있으면? 그냥 단톡방에 한 번 말하면 끝이다. 모든 소통이 단톡방이라는 중재자를 통해 이루어진다.

이것이 바로 Mediator 패턴의 핵심이다.

visible: false

Mediator란?

객체들이 서로 직접 통신하지 않고, 중재자(Mediator) 객체를 통해서만 소통하도록 만드는 패턴

객체 간의 복잡한 관계(many-to-many)중재자와의 관계(one-to-many) 로 단순화한다.

핵심 구조는 이렇다.

  1. Mediator: 소통 규칙을 정의하는 인터페이스
  2. Concrete Mediator: 실제 조율 로직을 구현
  3. 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("서울");
// 🏙️ 도시 선택: 서울
// 📮 우편번호 초기화 (도시 변경)

CountrySelectCitySelect의 존재를 모르고, CitySelectZipCodeInput의 존재를 모른다. 모든 연동 로직은 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

언제 사용하면 좋을까?

  1. 여러 객체가 복잡하게 서로 참조하고 있을 때

    • 컴포넌트 A가 B, C, D를 알고, B가 A, C를 아는 스파게티 상태
  2. 하나의 변경이 여러 객체에 연쇄적으로 영향을 줄 때

    • 폼 필드 연동, UI 상태 동기화
  3. 객체를 재사용하고 싶은데, 다른 객체들과의 의존성 때문에 어려울 때

    • 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

댓글

댓글을 불러오는 중...