Skip to main content

[Design Pattern] 행동 패턴 1. 중재자 (Mediator)




중재자 패턴 #

중재자

  • 한 집합에 속해 있는 객체의 상호작용을 캡슐화하는 객체를 정의한다.
  • 객체들이 직접 서로 참조하지 않도록 하여, 객체 사이의 느슨한 커플링(loose coupling) 을 촉진시키고 개발자가 객체의 상호작용을 독립적으로 다양화시킬 수 있게 만든다.

  • 언제 쓸까?
    • 프로그램 어디에서나 접근할 수 있게 하면 싱글톤처럼 문제가 생기기 쉽다. 따라서 절제해서 사용해야 한다.
    • 매개변수로 객체를 넘겨주는 방식으로 문제를 해결할 수 있다면 그렇게 하는 게 좋겠다. 하지만 이 방법이 불필요하거나 도리어 코드를 읽기 어렵게 한다면 중재자 패턴이 좋은 대안이 될 수 있다.
  • 주의사항
    • 싱글톤이나 정적 클래스에는 인스턴스가 항상 준비되어 있지만 중재자 패턴에서는 등록을 먼저 해야하기 때문에, 필요한 객체가 없을 때를 대비해야 한다.
    • 전역에서 접근이 가능하기 때문에 어느 환경에서나 문제 없이 동작해야 한다.

게임에서의 예시 #

  • 예를 들어, 사운드 시스템을 만들고 싶다고 하자.
  • 다음과 같이 사운드 서비스를 제공하는 중재자 패턴을 만들어볼 수 있겠다.
// 서비스 
class Audio
{
public:
  virtual ~Audio() {}
  virtual void playSound(int soundID) = 0;
  virtual void stopSound(int soundID) = 0;
  virtual void stopAllsounds() = 0;
};

class ConsoleAudio : public Audio
{
public:
  virtual void playSound(int soundID)
  {
    // 콘솔의 오디오 API를 사용해서 사운드를 출력한다. 
  }
  
  virtual void stopSound(int soundID)
  {
    // 콘솔의 오디오 API를 사용해서 사운드를 중지한다. 
  }
  
  virtual void stopAllsounds()
  {
    // 콘솔의 오디오 API를 사용해서 모든 사운드를 중지한다. 
  }
};

// 중재자 
class Locator
{
private:
  static Audio* service;
 
public:
  static Audio* getAudio() { return service; }
  static void provide(Audio* s) { service = s; }
};

  • 그러면 다음과 같이 등록하고 사용할 수 있다.
// 오디오를 중재자에 등록하는 방법
ConsoleAudio* audio = new ConsoleAudio();
Locator::provide(audio);

// 오디오를 중재자로 부터 얻어서 사용하는 방법 
Audio* audio = Locator::getAudio();
audio->playSound(VERD_LOAD_BANG);

  • 만약 아직 등록되지 않은 오디오를 얻으려고 한다면? 문제발생!
    • 널 객체 디자인 패턴을 사용해보자. 이것은 객체가 없을 때 안전하게 작업을 진행할 수 있도록 특수한 널 객체를 반환하는 것이다.
// 널 객체 
class NullAudio : public Audio
{
public:
  virtual void playSound(int soundID) { /* 아무것도 안 함 */ }
  virtual void stopSound(int soundID) { /* 아무것도 안 함 */ }
  virtual void stopAllSounds() { /* 아무것도 안 함 */ }
};

// 중재자 
class Locator
{
private:
  static Audio* service;
  static NullAudio* nullService;
 
public:
  static void initailize() { service = &nullService; }
  static Audio* getAudio() { return service; }
  static void provide(Audio* s) 
  { 
    if (s == NULL) service = &nullService; // 널 객체로 돌려놓는다. 
    else           service = s;
  }
};

  • 사운드가 출력될 때 로그를 남겨서 제대로된 순서로 나오는지 확인하고 싶어졌다.
    • 군데 군데 log()함수를 넣으면 나중에 없애기도 불편하고 관리도 어려워진다.
    • 원하는 로그만 켰다 껐다 할 수 있고, 최종 빌드에는 로그를 전부 제거할 수 있다면 좋겠다.
  • 데코레이터 패턴을 여기에 활용해보자.
    • 다른 오디오 클래스를 래핑해서 같은 인터페이스를 상속받는다.
    • 그리고 추가적으로 로그를 남기도록 한다.
class LoggedAudio : public Audio
{
private:
  Audio & wrapped;
  
  void log(const char* message)
  {
    // 로그를 남긴다
  }
  
public:
  LoggedAudio(Audio& w) : wrapped(w) {}
  
  virtual void playSound(int soundID)
  {
    log("사운드 출력");
    wrapped.playSound(soundID);
  }
  
  virtual void stopSound(int soundID)
  {
    log("사운드 중지");
    wrapped.stopSound(soundID);
  }
  
  virtual void stopAllsounds()
  {
    log("모든 사운드 중지");
    wrapped.stopAllsounds();
  }
};
// 그러면 이렇게 오디오 서비스의 로그 기능을 켤 수 있다. 
void enableAudioLogging()
{
  // 기존 서비스를 데코레이트 한다. 
  Audio *service = new LoggedAudio(Locator::getAudio());
  Locator::provide(service);
}

디자인 결정에 고려할 사항들 #

  • 서비스는 어떻게 등록되는가?
    • 외부 코드에서 등록
    • 컴파일할 때 바인딩
    • 런타임에 설정 값 읽기
  • 서비스를 못찾으면 어떻게 할 것인가?
    • 사용자가 알아서 처리하게 한다
    • 게임을 멈춘다
    • 널 서비스를 반환한다
  • 서비스의 범위는 어떻게 잡을 것인가?
    • 전역에서 접근 가능한 경우
    • 접근이 특정 클래스에 제한되면

개념적인 예시 #

// 중재자
public interface IMediator
{
    void Notify(object sender, string ev);
}

class ConcreteMediator : IMediator
{
    private Component1 _component1;
    private Component2 _component2;

    public ConcreteMediator(Component1 component1, Component2 component2)
    {
        this._component1 = component1;
        this._component1.SetMediator(this);
        this._component2 = component2;
        this._component2.SetMediator(this);
    } 

    public void Notify(object sender, string ev)
    {
        if (ev == "A")
        {
            Console.WriteLine("A에 반응한다. 아래의 내용을 트리거한다.");
            this._component2.DoC();
        }
        if (ev == "D")
        {
            Console.WriteLine("D에 반응한다. 아래의 내용을 트리거한다.");
            this._component1.DoB();
            this._component2.DoC();
        }
    }
}

// 컴포넌트들
class BaseComponent
{
    protected IMediator _mediator;

    public BaseComponent(IMediator mediator = null)
    {
        this._mediator = mediator;
    }
    
    public void SetMediator(IMediator mediator)
    {
        this._mediator = mediator;
    }
}

class Component1 : BaseComponent
{
    public void DoA()
    {
        Console.WriteLine("A 수행");
        this._mediator.Notify(this, "A");
    }

    public void DoB()
    {
        Console.WriteLine("B 수행");
        this._mediator.Notify(this, "B");
    }
}

class Component2 : BaseComponent
{
    public void DoC()
    {
        Console.WriteLine("C 수행");
        this._mediator.Notify(this, "C");
    }

    public void DoD()
    {
        Console.WriteLine("D 수행");
        this._mediator.Notify(this, "D");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Component1 component1 = new Component1();
        Component2 component2 = new Component2();
        
        IMediator mediator = new ConcreteMediator(component1, component2);
        component1.SetMediator(mediator);
        component2.SetMediator(mediator);

        // A -> 중재자 반응 -> C 
        component1.DoA();

        // D -> 중재자 반응 -> B, C
        component2.DoD();
    }
}



References #