[Design Pattern] 행동 패턴 1. 중재자 (Mediator)
Table of Contents
중재자 패턴 #
- 한 집합에 속해 있는 객체의 상호작용을 캡슐화하는 객체를 정의한다.
- 객체들이 직접 서로 참조하지 않도록 하여, 객체 사이의 느슨한 커플링(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();
}
}