본문 바로가기

SW 설계/Design Pattern

[Design Pattern] Behavioral - 중재자 패턴 (Mediator Pattern)

728x90

Mediator Pattern 은 Communication 중심의 디자인 패턴으로, Communication 이 Objects 들 간에 매우 복잡하게 진행되고 있을 때 그 역할을 위임하는 방식 등의 Controller 를 도입하는 패턴이다. 

 

 

통신이 필요한 객체들 사이에서 중재하는 Control Tower 와 같은 역할을 한다

 

 

Communication 을 하고자 하는 Colleague 들이 1:1로 직접 하는 것이 아니라, 중앙 Control Tower (Mediator) 을 둠으로써 Meidator 객체가 지속적으로 통신을 수집 / 분배하는 역할을 하게 된다. (통신 Colleague 들이 직접 수행을 한다면 N^2 의 Path 가 발생하여 복잡성이 증대된다)

 

 

Meidator Pattern 의 일반적인 UML

 

 

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

 

 

위 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) 중앙대학교 소프트웨어학부 이찬근 교수님 디자인 패턴 수업자료 중

728x90