유니티에서 중재자(Mediator) 패턴 활용하기

2025. 3. 29. 20:58유니티 unity/디자인패턴

게임을 만들다 보면 시스템 간 통신이 점점 복잡해지고, 서로의 존재를 직접 알아야만 동작하는 구조가 만들어지기 시작합니다. 예를 들어:

  • 여러 UI 버튼들이 서로의 상태를 변경해야 하는 경우
  • 플레이어가 죽었을 때, 카메라 줌, 사운드 재생, UI 출력 등 다양한 시스템이 함께 작동해야 하는 경우
  • 스킬 하나를 사용했을 때 여러 연출과 효과가 동기화되어야 하는 경우

이렇게 서로 얽히고설킨 의존성을 일일이 직접 연결하면 유지보수는 금방 악몽이 됩니다.
이런 상황에서 중앙에서 모든 소통을 중재해주는 구조, 즉 중재자(Mediator) 패턴이 효과를 발휘합니다.


중재자 패턴이란?

중재자(Mediator) 패턴은 여러 객체 간의 직접적인 소통을 막고, 모든 통신을 하나의 중재자 객체를 통해 간접적으로 수행하는 패턴입니다.

즉, 객체들이 서로 직접 대화하지 않고, 중재자를 통해 메시지를 주고받음으로써 의존성을 낮추고 소통을 관리하는 방식입니다.

"직접 얘기하지 마. 나한테 말해. 내가 처리할게."


유니티에서 왜 중재자 패턴이 유용한가요?

  • UI 요소들 간 상호작용을 쉽게 구현할 수 있습니다.
  • 게임 시스템 간의 소통을 명확하게 관리할 수 있습니다.
  • 각 오브젝트가 서로를 직접 참조하지 않기 때문에 결합도가 낮아집니다.
  • 기능을 추가하거나 제거할 때 다른 클래스에 영향을 주지 않습니다.
  • 디버깅과 테스트가 쉬워집니다.

실전 예제: 커스텀 이벤트 객체를 활용한 중재자 패턴

중재자 패턴을 처음 적용할 때 가장 흔한 접근 방식은 단순히 string 값을 메시지로 사용하는 것입니다.
예를 들어 "PlayerDied"나 "EnemySpawned" 같은 문자열을 기준으로 분기하곤 하죠.
하지만 실전에서는 문자열 기반 메시지는 유지보수에 취약합니다. 오타, 자동완성 미지원, 타입 불일치 등 다양한 문제가 생기기 때문이죠.

그래서 보통은 이벤트를 구조화한 클래스를 만들어 사용합니다. 이 방식은 명확한 데이터 전달, 타입 안정성, 코드 추적 용이성 등을 모두 만족시킵니다.

아래는 유니티에서 중재자 패턴을 실전적으로 적용한 구조 예제입니다.

1. 이벤트 인터페이스 및 Enum 정의

public enum GameEventType
{
    PlayerDied,
    BossSpawned,
    QuestCompleted
}

public interface IGameEvent
{
    GameEventType EventType { get; }
}

2. 커스텀 이벤트 클래스

public class PlayerDiedEvent : IGameEvent
{
    public GameEventType EventType => GameEventType.PlayerDied;
    public Vector3 deathPosition;
    public string reason;

    public PlayerDiedEvent(Vector3 pos, string reason)
    {
        deathPosition = pos;
        this.reason = reason;
    }
}

3. 수신자 인터페이스

public interface IGameEventReceiver
{
    void OnEvent(IGameEvent gameEvent);
}

4. 중재자 클래스 구현

public class GameEventMediator
{
    private List<IGameEventReceiver> receivers = new();

    public void Register(IGameEventReceiver receiver)
    {
        receivers.Add(receiver);
    }

    public void Send(IGameEvent gameEvent)
    {
        foreach (var r in receivers)
        {
            r.OnEvent(gameEvent);
        }
    }
}

5. 사용 예시: 사운드와 카메라 시스템

public class CameraSystem : MonoBehaviour, IGameEventReceiver
{
    public void OnEvent(IGameEvent e)
    {
        if (e is PlayerDiedEvent died)
        {
            Debug.Log($"카메라: 플레이어가 {died.deathPosition}에서 죽음. 줌 아웃 처리");
        }
    }
}

public class AudioSystem : MonoBehaviour, IGameEventReceiver
{
    public void OnEvent(IGameEvent e)
    {
        if (e is PlayerDiedEvent)
        {
            Debug.Log("사운드: 사망 효과음 재생");
        }
    }
}

6. 이벤트 발송

var deathEvent = new PlayerDiedEvent(player.transform.position, "화염구 맞음");
mediator.Send(deathEvent);

이처럼 각 시스템은 중재자만 알고 있을 뿐 서로를 전혀 모르기 때문에, 시스템을 추가하거나 제거할 때도 기존 코드를 수정할 필요가 없습니다.


다양한 활용 예시

  • UI 동기화: 슬라이더가 바뀌면 텍스트와 색상도 연동됨
  • 스킬 시스템: 스킬 사용 시 연출, 사운드, 데미지를 Mediator가 일괄 중계
  • 미니게임 컨트롤: 타이머, 결과 UI, 이펙트를 중재자가 연결함
  • 튜토리얼: 단계 진행에 따라 UI와 시스템을 유기적으로 연결

장점 정리

장점 설명

✅ 느슨한 결합 객체 간 직접 참조를 없애고 의존성 최소화
✅ 소통 중앙화 이벤트 흐름이 한 곳에서 관리되어 추적 쉬움
✅ 유지보수 용이 한 객체가 변경되어도 다른 객체에는 영향 없음
✅ 타입 안정성 구조화된 이벤트 객체로 안전한 통신 가능
✅ 확장성 우수 다양한 이벤트 타입을 쉽게 추가 가능

퍼사드, 이벤트 버스와의 차이점

패턴 목적 구조

퍼사드 여러 서브 시스템 통합 단방향 호출 인터페이스
이벤트 버스 전역 이벤트 브로드캐스트 단방향 구독/발행
중재자 동료 간 상호작용 조율 양방향, 소규모 이벤트 조율
  • 퍼사드: 시스템을 하나의 진입점으로 단순화함
  • 이벤트 버스: 메시지를 전체에 퍼뜨리는 전역 구조
  • 중재자: 선택적 양방향 소통, 협력 시스템 중심의 구조

마무리하며

중재자 패턴은 객체 간 복잡한 소통을 중앙 집중화하여 정리할 수 있게 해주는 강력한 구조입니다.
유니티처럼 오브젝트가 많고, UI와 시스템이 유기적으로 연결되어야 하는 환경에서는 유지보수성과 확장성 면에서 특히 큰 이점을 제공합니다.

특히 문자열 기반 메시지를 지양하고, 구조화된 이벤트 객체를 사용하는 방식은 실전에서 안정성과 효율성을 동시에 확보할 수 있는 핵심 전략입니다.

중재자 패턴은 UI 시스템, 게임 이벤트, 미니게임, 연출 타이밍 등 다양한 곳에 활용될 수 있으며,
코드 구조를 깔끔하게 유지하고 싶다면 꼭 한 번 적용해보시길 추천드립니다.