Mediator Pattern 은 Communication 중심의 디자인 패턴으로, Communication 이 Objects 들 간에 매우 복잡하게 진행되고 있을 때 그 역할을 위임하는 방식 등의 Controller 를 도입하는 패턴이다.
Communication 을 하고자 하는 Colleague 들이 1:1로 직접 하는 것이 아니라, 중앙 Control Tower (Mediator) 을 둠으로써 Meidator 객체가 지속적으로 통신을 수집 / 분배하는 역할을 하게 된다. (통신 Colleague 들이 직접 수행을 한다면 N^2 의 Path 가 발생하여 복잡성이 증대된다)
1. Object 들 간의 상호 관계성을 encapsulate 한다
2. Colleague 들 간의 Coupling 을 약화시킨다
Colleague 들은 Concrete Class 들로, 자신의 상태의 변화 혹은 메세지를 Mediator 에게 전달하게 되어 있다. 그리고 Mediator 는 이 메세지에 대한 처리를 meidate() 을 통해 진행하게 된다. (이건 패턴이고, 원하는 메세지 형태와 meidate 결과는 직접 제어하는 방식이다)
가장 중요한 점은 Colleague 들 간에는 직접적인 Communication 이 절대 없다는 것이다 (서로에 대한 존재를 알면 안되므로, 유지 / 보수가 유리해진다). 중재자 패턴은 이해하기 쉽지만, Mediator 로직 자체가 앱이 진행하려는 방향대로 설계될 수밖에 없기 때문에 reuse 가 어렵다 (옵저버 패턴과 완전히 반대되는 특성, notify 를 보내는 측도 다르다). 다음과 같은 예시를 살펴보자.
만약 위 상태창에서 Italic 속성을 선택했다면, Font 목록에는 Italic 한 속성들을 지원하는 폰트들만이 존재하도록 refresh 되어야 한다. 또한 특정 폰트들은 Size 를 조절할 수 없는 폰트일 때도, Size 창이 disable 되도록 바꿔줘야 한다. 이렇게 각각의 Element 마다 서로에게 영향을 주는 로직을 각자 갖고 있는 상황일 경우 중재자를 도입하는 것이 좋다.
위 UML 만을 보고는 조금 헷갈릴 수 있으니, 다음과 같은 코드를 통해 살펴보자. 위에서 보는 것처럼 간단하지 않고, 생각보다 복잡한 flow 라 주석에 열심히 설명해 놓았다 (위 UML 만을 보고 대략적인 작동 방식에 대해 생각하고 만들어 봤으니, "데이터 전달을 위임한다" 중심으로 보면 된다).
1) Widget, ListBox
/*
다이얼로그 내부에 있는 각각의 속성을 제어하기 위해 적용된 UI
*/
public abstract class Widget {
// 해당 위젯이 변경될 시, 같은 다이얼로그안에 있는 다른 위젯에게 말해줘야 할 수도 있다
// 이 때, 자신을 탄생시킨 Director 를 Atrribute 로 가지고 있는다
// 자신을 탄생시키는 구조이므로 Dialog Director 는 주입받는게 맞아보인다
private DialogDirector dialogDirector;
public Widget(DialogDirector dialogDirector) {
this.dialogDirector = dialogDirector;
}
// 상태 변화 감지시 호출되는 callback method
// 상태 변화 되는 부분에서 호출하게 될 것이다
public void changed() {
// 특정 상태가 변경된거에 대하여 change 를 전달한다
dialogDirector.widgetChanged(this);
}
}
public class ListBox extends Widget{
private String curSelection;
public ListBox(DialogDirector dialogDirector) {
super(dialogDirector);
}
public String getSelection() {
return this.curSelection;
}
/*
각 ListBox 의 Item 에는 onClick() 과 같은 callback 함수가 지정,
다음과 같은 형태를 띈다
void onClick(){
this.curSelection = CUR_SELECTED_STRING;
super.widgetChanged();
}
*/
}
2) DialogDirector, FontDialogDirector
/*
Font 제어 다이얼로그 뿐만 아니라 여러 Dialog 들을 띄울 수 있는데
이 Dialog 들에 대한 모든 것을 담당함 - 생성, 결과 전달
*/
public abstract class DialogDirector {
protected List<Widget> widgets;
public DialogDirector(List<Widget> widgets) {
this.widgets = createWidgets();
}
// 각 다이얼로그에 필요한 위젯들은 직접 생성한다
protected abstract List<Widget> createWidgets();
protected abstract void widgetChanged(Widget widget);
public void showDialog() {
// this.widget 들을 통해 Dialog 를 생성하고 return
// 지금은 그냥 간단하게 보여주기만 한다
}
}
/*
DialogDirector 도 생성 시점에 생성 후 종료된다고 가정
> 필요 위젯들을 그 때 생성해서 받아둔다
*/
public class FontDialogDirector extends DialogDirector {
public FontDialogDirector(List<Widget> widgets) {
super(widgets);
}
// FontDialog 에 필요한 widget 들은 직접 생성해야 한다
@Override
public List<Widget> createWidgets() {
// ListBox 와 같은 필요한 Widget 들을 직접 생성한다
ListBox listBox = new ListBox(this);
super.widgets.add(listBox);
// .. 더 생성 후 담는다
return super.widgets;
}
/*
Widget 으로 부터 발생한 trigger 에 대해
다른 Widget 에게 상태를 전달하여
다른 Action 발생을 유도한다 >> Callback 의 종류인데, 그것을 한 곳에서 관리하는 것
(Dialog, Widget 들이 많아지면 차라리 Callback Interface 를 만들어서 Strategy 로 가는게 나을 수도)
*/
@Override
public void widgetChanged(Widget widget) {
if (widget instanceof ListBox) {
ListBox lb = (ListBox) widget;
String curSelection = lb.getSelection();
// widgets 내에서 데이터를 전송하고자 하는 Widget 을 찾는다
for (Widget w : widgets) {
if (w instanceof EntryField) {
EntryField ef = (EntryField) w;
ef.setText(curSelection);
}
}
}
/*
NAIVE 하게 구현해본 것..
분기가 들어가니 더 심화된 디자이닝 필요함
*/
// ...
}
}
각 widget 에서 상태 변화를 감지하고, 그 상태 변화 마지막에 change() 함수를 호출하거나 데이터를 전달하게 된다면, 현재 widget 객체를 소유하는 DialogDirector 에서 widgetChanged() 를 통해 자신이 소유한 Widget 들 안에서 필요한 Communication 을 제어해주게 된다 (한 다이얼로그 안에서 위젯이 몇 개 안될 경우는, Collection 에 가지고 있는게 아닌 각자 소유하는게 훨씬 깔끔할 것이다).
이렇게 element 의 상태 변화나 다른 액션들이 서로 다른 element 에게 영향을 주고 그게 각자 다를 경우 사용하게 된다. Trigger 를 해주는 부분에 있어서 Observer Pattern 과 유사한 부분이 있는데, 다음과 같이 차이를 정리해볼 수 있겠다.
Mediator Pattern | Observer Pattern |
* 위처럼 Mediator 코드에 Concrete Class 가 등장하는 등 어플리케이션, Object 에 대한 의존성이 있다 -> Reuse 불가 * Flow 가 간단해서 이해하기 쉽다 (막상 구현해보면 어렵다) |
* Reusability 가 굉장히 높다 (특정 어플리케이션, Object 등에 의존성이 전혀 있을 필요가 없다 -> Java Lib 까지 있음) * Flow 의 복잡성을 이해하기 어렵다 |
정리하며
간단한 Layout 에 비해 막상 코드로 구현해보려고 하니 생각보다 어려웠다. 저서에서 등장하진 않았지만, trigger 해주는 부분과 그걸 받고 전달해야 하는 부분이 생각보다 복잡해지기 쉽기 때문에, Meidator Pattern 이 꼭 필요한 이유를 조금 더 알 수 있었다.
예전에 안드로이드 개발할 때 Callback, Listener 를 정말 많이 구현했었는데, 이와 연관이 깊은 Pattern 같다 (어쩌면 그래서 Observer 랑 많이 헷갈리는 걸지도). 이 때 적용했었으면 훨씬 깔끔하게 관리할 수 있지 않았을까?
출처
1) 에릭 프리먼, 『Head First Design Patterns』, O'Reilly Media (2004)
2) 중앙대학교 소프트웨어학부 이찬근 교수님 디자인 패턴 수업자료 중
'SW 설계 > Design Pattern' 카테고리의 다른 글
[Design Pattern] Creational - 빌더 패턴 (Builder Pattern) (0) | 2023.10.21 |
---|---|
[Design Pattern] Creational - 팩토리 메소드 패턴 (Factory Method Pattern) (0) | 2023.10.20 |
[Design Pattern] Behavior - 템플릿 메소드 패턴 (Template Method Pattern) (0) | 2023.10.19 |
[Design Pattern] Behavioral - 옵저버 패턴 (Observer Pattern) (0) | 2023.10.19 |
[Design Pattern] Behavioral - 전략 패턴 (Strategy Pattern) (0) | 2023.10.19 |