|

코틀린 디자인 패턴 (17) – Mediator 패턴으로 객체 간 얽힘 줄이기

코틀린 Mediator 패턴 대표 이미지
Mediator 패턴은 객체 간 직접 참조를 줄이고 중앙 조정자를 통해 협력 규칙을 모은다

코틀린 Mediator 패턴은 객체가 많아질수록 왜 서로 직접 아는 구조가 불편해지는지를 설명하기 좋은 패턴입니다. 이름만 보면 중간에서 전달만 해 주는 단순한 구조처럼 보이지만, 실제 핵심은 복잡한 상호작용 규칙을 한곳으로 모아 결합을 낮추는 것에 있습니다.

이번 글에서는 Mediator 패턴을 정의로만 설명하지 않고, 왜 폼 검증이나 채팅방 같은 장면에서 자주 등장하는지, Observer와는 무엇이 다른지, 코틀린에서는 어디까지 가볍게 구현할 수 있는지까지 같이 정리하겠습니다.

Mediator 패턴 핵심 카드
이번 글에서 먼저 잡아야 할 핵심 판단 기준

기본 개념 참고로는 Refactoring.Guru의 Mediator 패턴 설명, 코틀린 구현 바탕으로는 Kotlin interfaces 문서를 참고했습니다. 이번 글은 그 내용을 코틀린 실전 감각으로 다시 풀어 설명합니다.

왜 이 패턴이 필요할까

객체가 적을 때는 서로 직접 참조해도 큰 문제가 없어 보입니다. 버튼이 텍스트 필드를 알고, 텍스트 필드가 체크박스를 알고, 체크박스가 다시 버튼 상태를 바꾸는 식으로 연결해도 처음에는 빠르게 구현됩니다.

문제는 기능이 늘어날수록 관계가 거미줄처럼 늘어난다는 점입니다. 이때 각 객체는 자기 책임보다 다른 객체 반응까지 알아야 하고, 한 군데 수정이 여러 객체에 동시에 번지기 쉬워집니다.

Mediator 패턴은 여기서 방향을 바꿉니다. 객체들이 서로를 직접 많이 아는 대신, 중간 조정자에게 알리고 그 조정자가 필요한 상호작용을 통제하게 만듭니다.


대표 예시: 폼 상호작용을 떠올리면 쉽다

이 패턴이 자주 폼 예시와 함께 설명되는 이유는 상호작용 규칙이 눈에 잘 보이기 때문입니다. 체크박스를 켜면 입력칸이 열리고, 입력이 비면 저장 버튼이 비활성화되고, 특정 옵션이 바뀌면 다른 필드가 잠기는 식의 규칙은 한두 개일 때는 단순하지만 금방 복잡해집니다.

이 로직을 각 필드 내부에 분산시키면 재사용성과 수정 가능성이 급격히 떨어집니다. 반대로 dialog나 form coordinator 같은 mediator가 규칙을 모으면, 각 컴포넌트는 자기 이벤트를 알리기만 하고 협력 규칙은 한곳에서 읽히게 됩니다.

실무적으로는 필터 패널, 검색 옵션 패널, 결제 화면의 입력 검증처럼 여러 UI 조각이 서로 영향을 주는 장면에서도 mediator 감각이 잘 드러납니다. 예를 들어 배송 옵션 하나를 바꾸면 결제 버튼 상태, 경고 문구, 다른 체크박스 선택 가능 여부가 연쇄적으로 바뀔 수 있습니다.

화면 필터 패널 조정 예시 카드
실전 UI에서도 mediator가 왜 필요한지 감각적으로 보여주는 카드
Mediator 패턴 흐름 도식
컴포넌트 이벤트가 mediator로 모이고, mediator가 다시 필요한 상태 변화를 조정하는 흐름

코틀린 코드로 보면 어떻게 생길까

interface ChatMediator {
    fun send(message: String, from: Participant)
}

interface Participant {
    val name: String
    fun receive(message: String)
}

class ChatRoomMediator : ChatMediator {
    private val participants = mutableListOf<Participant>()

    fun join(participant: Participant) {
        participants += participant
    }

    override fun send(message: String, from: Participant) {
        participants
            .filterNot { it == from }
            .forEach { it.receive("${from.name}: $message") }
    }
}

class User(
    override val name: String,
    private val mediator: ChatRoomMediator,
) : Participant {
    fun send(message: String) {
        mediator.send(message, this)
    }

    override fun receive(message: String) {
        println("[$name] $message")
    }
}

이 예제에서 각 사용자 객체는 다른 사용자 목록을 직접 들고 있지 않습니다. 메시지 전달 규칙은 mediator가 알고 있고, 참여자는 mediator를 통해서만 소통합니다. 즉 협력 구조의 중심이 participant가 아니라 mediator 쪽으로 이동합니다.


Observer와는 무엇이 다를까

Mediator와 Observer는 둘 다 객체 간 직접 결합을 줄이는 것처럼 보여서 자주 헷갈립니다. 하지만 Observer는 변화 전파 구조에 가깝고, Mediator는 상호작용 조정 구조에 더 가깝습니다.

Observer에서는 누가 변했는지와 누가 구독하는지가 중요합니다. 반면 Mediator에서는 여러 객체 사이의 협력 규칙 자체가 중요합니다. 그래서 Observer는 이벤트 확산에, Mediator는 복잡한 상호작용 조정에 더 잘 맞습니다.

쉽게 말해 Observer는 “누가 바뀌었고 누가 그 변화를 듣는가” 쪽에 가깝고, Mediator는 “이 이벤트가 발생했을 때 전체 협력 규칙을 누가 판단하는가” 쪽에 더 가깝습니다. 그래서 같은 결합 감소라도 문제의 초점이 다릅니다.

예를 들어 가격이 바뀌었다는 사실을 여러 곳에 알리는 것은 Observer 쪽 감각에 가깝습니다. 반면 가격 필터 변경이 정렬 상태, 경고 메시지, 검색 버튼 활성 여부까지 함께 바꾸는 규칙을 한곳에서 통제하는 것은 Mediator 쪽 감각에 더 가깝습니다.

Mediator와 Observer 비교 카드
둘 다 결합을 낮추지만 중심에 두는 문제가 다르다

코틀린다운 포인트는 무엇일까

코틀린에서는 mediator를 꼭 무거운 UML 구조처럼 만들 필요는 없습니다. interface 하나와 몇 개의 구현체, 그리고 명확한 책임 구분만 있어도 충분히 읽기 좋은 구조를 만들 수 있습니다.

특히 data class, sealed class, higher-order function, delegation 같은 코틀린 도구를 알고 있으면, 전통적인 패턴 설명보다 훨씬 간단한 코드로 핵심 의도를 드러낼 수 있습니다. 다만 그렇다고 해서 mediator라는 책임 자체가 사라지는 것은 아닙니다. 코틀린이 줄여 주는 것은 보일러플레이트이지, 협력 규칙 설계 고민은 아닙니다.

코틀린에서는 mediator를 이벤트 중심으로 더 읽기 좋게 만드는 방법도 있습니다. 예를 들어 컴포넌트가 발생시키는 이벤트를 sealed class로 모으고, mediator가 그 이벤트를 받아 어떤 상태 변화를 만들지 결정하게 하면 규칙이 더 분명하게 드러날 수 있습니다.

sealed event 기반 mediator 변형 카드
코틀린에서는 event 타입을 묶어 mediator 규칙을 더 읽기 좋게 만들 수 있다

이 감각은 앞선 Chain of Responsibility 글과도 비교해 볼 수 있습니다. Chain은 요청이 여러 처리기를 따라 흘러가게 만드는 데 가깝고, Mediator는 여러 객체 사이 상호작용 규칙을 중앙에서 조정하는 데 더 가깝습니다.


언제 잘 맞고, 언제 과할까

Mediator는 여러 객체가 서로를 너무 많이 알아서 수정 파급이 커지는 상황에 특히 잘 맞습니다. 폼 상호작용, 채팅방, 게임 내 UI 조정, 복잡한 화면 상태 변경처럼 협력 규칙이 많을수록 가치가 올라갑니다.

하지만 모든 상호작용을 한곳에 몰아넣으면 mediator가 거대한 God object가 되기 쉽습니다. 결국 결합은 줄었는데 복잡도는 mediator 안으로만 옮겨간 셈이 됩니다.

여기서 같이 구분해야 할 것이 단순 coordinator와 mediator의 차이입니다. coordinator는 화면 이동이나 처리 순서를 연결하는 데 더 가깝고, mediator는 여러 객체가 서로 영향을 주는 규칙 자체를 중앙에서 조정하는 데 더 가깝습니다. 둘을 같은 것으로 보면 패턴이 과하게 보이거나 반대로 필요한데도 놓치기 쉽습니다.

coordinator와 mediator 차이 비교 카드
흐름 연결과 상호작용 규칙 조정은 비슷해 보여도 초점이 다르다

또 하나 주의할 점은 mediator에 조건문이 계속 쌓이면 결국 객체 간 결합 대신 중앙 객체의 복잡도만 커질 수 있다는 점입니다. 그래서 mediator는 결합을 낮추는 도구이면서도 동시에 비대화 위험을 항상 함께 보는 패턴입니다.

Mediator가 과해지는 경우 비교 카드
잘 맞는 경우와 과한 경우를 같이 봐야 균형이 잡힌다

실전에서 판단할 때 스스로 던질 질문

  1. 객체들이 서로 직접 아는 관계가 너무 많아졌는가
  2. 협력 규칙을 한곳에서 읽고 바꾸는 편이 더 자연스러운가
  3. 이 규칙이 observer식 이벤트 확산보다 중앙 조정에 더 가까운가
  4. mediator가 모든 책임을 삼켜 비대해질 조짐은 없는가

이 질문에 yes가 많아질수록 mediator 후보일 가능성이 높습니다. 반대로 규칙이 아주 작거나 단순 이벤트 통지만 필요하다면, 더 가벼운 구조가 나을 수 있습니다.


시리즈 흐름으로 보면 이번 Mediator 패턴은 앞선 Iterator 편처럼 협력 인터페이스를 보는 관점에서 이어지고, 다음 Memento 편처럼 객체 상태를 어떻게 안전하게 다룰지 고민하는 흐름과도 연결됩니다. 즉 행동 패턴 파트 안에서 객체들 사이의 관계를 정리하는 축을 한 단계 더 넓혀 주는 챕터라고 볼 수 있습니다.

마무리

Mediator 패턴의 핵심은 중간 객체를 하나 더 만드는 데 있지 않습니다. 여러 객체가 서로를 직접 아는 대신, 상호작용 규칙을 한곳으로 모아 결합과 수정 파급을 줄이는 데 있습니다.

객체끼리 직접 얽혀서 수정이 번지는 문제가 보일 때, Mediator 패턴은 꽤 실용적인 선택지가 됩니다. 다만 중앙 조정자가 너무 비대해지면 다른 문제를 만들 수 있으므로, 언제 조정이 필요하고 어디서부터 과한지를 같이 보는 것이 중요합니다.

코틀린 디자인 패턴 시리즈

코틀린 디자인 패턴 시리즈(0) – 왜 아직도 디자인 패턴을 배워야 할까

코틀린 디자인 패턴(1) – Singleton 패턴은 언제 쓰고 object는 어떻게 다를까

코틀린 디자인 패턴(2) – Factory Method 패턴으로 생성 책임 나누기

코틀린 디자인 패턴(3) – Abstract Factory 패턴은 언제 필요할까

코틀린 디자인 패턴(4) – Builder 패턴과 named argument는 어떻게 다를까

코틀린 디자인 패턴(5) – Prototype 패턴과 data class copy 정리

코틀린 디자인 패턴(6) – Adapter 패턴으로 기존 코드를 새 인터페이스에 맞추기

코틀린 디자인 패턴(7) – Bridge 패턴은 상속 폭발을 어떻게 줄일까

코틀린 디자인 패턴(8) – Composite 패턴으로 트리 구조 다루기

코틀린 디자인 패턴(9) – Decorator 패턴은 상속 대신 어떻게 확장할까

코틀린 디자인 패턴(10) – Facade 패턴으로 복잡한 시스템 감추기

코틀린 디자인 패턴(11) – Flyweight 패턴은 메모리를 어떻게 아낄까

코틀린 디자인 패턴(12) – Proxy 패턴은 Decorator와 무엇이 다를까

코틀린 디자인 패턴(13) – Chain of Responsibility 패턴으로 조건 분기 줄이기

코틀린 디자인 패턴(14) – Command 패턴으로 실행과 취소를 다루는 법

코틀린 디자인 패턴(15) – Interpreter 패턴은 어디까지 써야 할까

코틀린 디자인 패턴(16) – Iterator 패턴과 sequence는 어떻게 연결될까

함께보면 좋은 글