객체가 늘어날수록 코드가 스파게티가 되는 순간이 옵니다. 화면 하나에 뷰 컨트롤러, 네트워크, 캐시, 로거가 서로를 직접 부르기 시작하면 어디를 고쳐도 다른 데가 터지죠.
이럴 때 후보로 떠오르는 게 미디에이터, 옵저버, 퍼사드 세 가지입니다. 셋 다 “객체 통신을 정리”한다고 소개되는데, 사실 푸는 문제가 서로 다릅니다.
미디에이터는 서로 물고 물린 관계를 중앙 통제로, 옵저버는 상태 변화를 일대다 알림으로, 퍼사드는 복잡한 내부를 단순한 창구로 정리합니다.
이 한 줄만 기억해도 절반은 끝난 거예요. 아래에서 각각 언제 쓰는지, 어떻게 다른지 코드까지 붙여 풀어보겠습니다.
세 패턴, 한 줄 요약부터
헷갈릴 때 다시 보라고 핵심부터 정리해둘게요.
- 미디에이터(Mediator) — 여러 객체가 서로 직접 참조하지 않고, 중재자 하나를 거쳐 소통
- 옵저버(Observer) — 한 객체의 상태가 바뀌면 이를 구독한 여러 객체에게 자동 통지
- 퍼사드(Facade) — 복잡한 하위 시스템 앞에 단순한 인터페이스 하나를 세워 감춤
같은 “통신 정리”라도 결이 다릅니다.
미디에이터는 관계의 복잡성을 줄이고, 옵저버는 변화의 전파를 다루고, 퍼사드는 사용의 복잡성을 낮춥니다.
미디에이터 vs 옵저버, 뭐가 다른가요?
이 둘이 제일 많이 헷갈립니다. 둘 다 객체들이 서로 직접 안 부르게 만드니까요.
차이는 관계의 방향이에요.
옵저버는 방향이 명확합니다. 주체(Subject) 하나가 바뀌면 구독자들에게 한 방향으로 흘러가요. 채팅방 새 메시지가 오면 화면, 알림 배지, 소리가 각자 반응하는 식이죠.
미디에이터는 방향이 얽혀 있습니다. 버튼을 누르면 텍스트필드가 활성화되고, 그게 다시 저장 버튼 상태를 바꾸고… 서로가 서로에게 영향을 줍니다. 이 다대다 관계를 중재자가 대신 조정해요.
// 옵저버: 상태 변화가 한 방향으로 전파
protocol Observer: AnyObject { func update(_ count: Int) }
final class Cart {
private var observers: [Observer] = []
var items = 0 { didSet { observers.forEach { $0.update(items) } } }
func subscribe(_ o: Observer) { observers.append(o) }
}
final class Badge: Observer {
func update(_ count: Int) { print("장바구니 배지: \(count)") }
}
let cart = Cart()
cart.subscribe(Badge())
cart.items = 3
// 출력: 장바구니 배지: 3
장바구니가 바뀌자 배지가 알아서 따라옵니다. Cart는 Badge가 누군지 몰라도 됩니다.
반면 미디에이터는 객체들 사이 규칙을 중재자가 들고 있어요.
// 미디에이터: 얽힌 규칙을 중재자가 관리
final class FormMediator {
var isAgreed = false
func agreedChanged(_ value: Bool, submit: Button) {
isAgreed = value
submit.isEnabled = value // 규칙: 동의해야 제출 활성화
}
}
final class Button { var isEnabled = false }
let submit = Button()
let mediator = FormMediator()
mediator.agreedChanged(true, submit: submit)
print(submit.isEnabled)
// 출력: true
체크박스와 버튼이 서로를 직접 몰라도, 중재자가 “동의하면 제출 켠다”는 규칙을 대신 지킵니다.
퍼사드는 결이 조금 다릅니다
퍼사드는 앞의 둘과 목적 자체가 달라요. 통신을 조정하는 게 아니라, 복잡한 내부를 안 보이게 감싸는 패턴입니다.
주문 하나를 처리하려고 결제, 재고, 배송, 알림 모듈을 순서대로 다 호출해야 한다고 해볼게요. 호출하는 쪽 코드가 지저분해집니다.
퍼사드는 이걸 창구 하나로 묶습니다.
final class OrderFacade {
func placeOrder(_ id: String) {
Payment().charge(id)
Stock().reduce(id)
Delivery().book(id)
print("주문 완료: \(id)")
}
}
OrderFacade().placeOrder("A-1024")
// 출력: 주문 완료: A-1024
바깥에서는 placeOrder 하나만 부르면 끝입니다. 내부에 모듈이 몇 개든 몰라도 되죠.
핵심은 이거예요. 퍼사드는 객체들끼리 소통시키는 게 아니라, 나(호출자)와 복잡한 시스템 사이를 단순화합니다. 방향이 안쪽으로 향해요.
한눈에 비교
2026년 지금 실무에서 제가 나누는 방식을 표로 정리했습니다.
| 구분 | 미디에이터 | 옵저버 | 퍼사드 |
|---|---|---|---|
| 푸는 문제 | 얽힌 다대다 관계 | 상태 변화 전파 | 복잡한 내부 감추기 |
| 관계 방향 | 서로 얽힘(중앙 조정) | 일대다(한 방향) | 호출자→시스템 |
| 결합도 변화 | 객체 간 결합↓ | 주체·구독자 분리 | 사용 복잡도↓ |
| 대표 예 | 폼 유효성, 채팅방 조율 | 알림, 데이터 바인딩 | 결제·주문 통합 API |
표로 보면 셋이 겹치지 않는다는 게 확 와닿습니다.
언제 쓰고 언제 피해야 할까
패턴은 남용하면 오히려 독입니다. 얽힌 관계를 전부 미디에이터로 묶으면 중재자가 God object가 되기 쉽거든요.
- 미디에이터 — 객체 3~4개 이상이 서로를 직접 참조하며 규칙이 얽힐 때. 단, 규칙이 단순하면 중재자가 비대해지니 피하세요.
- 옵저버 — 한 곳의 변화를 여러 곳이 알아야 할 때. 구독 해제를 안 하면 메모리 누수가 나니 주의하세요.
- 퍼사드 — 복잡한 호출 절차를 감추고 싶을 때. 내부 세밀 제어가 꼭 필요한 곳엔 오히려 방해가 됩니다.
한 문장으로는, 관계가 얽히면 미디에이터, 변화를 알려야 하면 옵저버, 절차를 감추려면 퍼사드입니다.
면접에서는 이렇게 물어봅니다
Q. 미디에이터와 옵저버의 차이는 무엇인가요?
옵저버는 주체의 상태 변화가 구독자들에게 한 방향으로 전파되는 일대다 구조입니다. 미디에이터는 서로 얽힌 객체들의 상호작용 규칙을 중재자가 중앙에서 조정하는 다대다 구조라는 점이 다릅니다. 방향성과 관계의 복잡도, 이 둘이 핵심 구분점입니다.
Q. 퍼사드도 결합도를 낮추는데, 미디에이터와 어떻게 다른가요?
퍼사드는 하위 시스템 앞에 단순한 인터페이스를 세워 호출자와 시스템 사이의 사용 복잡도를 낮춥니다. 반면 미디에이터는 동료 객체들 사이의 상호 통신을 조정하는 데 목적이 있습니다. 퍼사드는 단방향 단순화, 미디에이터는 상호작용 조정이라고 답하면 좋습니다.
세 패턴을 외우려 하기보다, 지금 내 코드가 “관계가 얽힌 문제”인지 “변화 전파 문제”인지 “절차가 복잡한 문제”인지 먼저 물어보세요. 답이 정해지면 패턴은 자연스럽게 따라옵니다. 오늘 리팩터링에서 딱 하나만 골라 적용해보세요.
