본문 바로가기

CS/디자인패턴

[디자인패턴] Mediator 패턴

중재자 패턴을 설명하는 그림1
중재자 패턴을 설명하는 그림2

설명

 다양한 기능을 객체 단위로 분산하면 해당 객체들 사이에 다양한 연관 관계가 발생한다. 시스템을 객체 단위로 분할하면 재사용성이 높아질 수 있지만, 분할 수준이 높아지다보면 객체들이 상호작용에 따른 연관관계가 많아지고, 서로에 의존하게 되는 문제가 발생한다. 이 경우 객체를 분할했음에도 객체들 사이의 결합도가 높아 크게 보면 사실상 하나의 큰 덩어리로 구현하는 것과 유사한 상황이 나타난다. 아래 가상의 기기를 보면서 구체적으로 생각해보자.

상상속의 기기

위 기기는 다음과 같이 동작한다.

  • A ~ F 버튼을 누르면 클릭한 버튼의 문자가 디스플레이에 추가된다.
  • SAVE 버튼을 누르면 현재까지 누른 버튼 리스트를 외부로 전달한다.
  • RESET 버튼을 누르면 누른 버튼 리스트를 초기화한다. 디스플레이도 초기화된다.
  • LED는 버튼을 누를 때마다 빛을 낸다.

이러한 정보를 기반으로 현재 기기에 필요한 클래스들을 나열해보자.

  • 알파벳 버튼: 대응되는 알파벳 정보를 알고 있다. 눌렸을 때 기록에 추가된다.
  • SAVE 버튼: 눌렸을 때 여태까지의 기록을 전달한다.
  • RESET 버튼: 눌렸을 때 여태까지의 기록을 초기화한다.
  • LED: 버튼을 누를 때 빛을 낸다.
  • 디스플레이: 기록을 보여준다.

 위 정보를 기반으로 아래처럼 설계했다고 생각해보자.

개략적인 설계

 각각의 버튼은 Display 및 LED에 대한 레퍼런스를 직접 가지고 있으며, press을 호출했을 때 특정 동작을 수행하게 된다. 문제는 버튼들이 직접 다른 객체들을 참조하고 있다는 점이다.  각 버튼을 Display와 LED가 아닌 다른 클래스, 예를 들어 Audio와 사용하기 위해서는 각 버튼에 Audio 객체에 대한 참조 및 대응되는 코드를 추가로 작성해야 하는 문제가 있다.

 위 상황에서 버튼 클래스들의 재사용성을 높이기 위해서는 display 및 LED에 직접적인 참조 및 상호작용을 제거할 필요가 있다. 분리된 상호작용 참조는 mediator 클래스가 버튼들 대신 수행하게 될 것이다.

 더 넓은 측면에서 보면, Button 자체에 대한 재사용은 상당히 어렵다. 만약 알파벳이나 save, reset 같은 로직이 필요한 경우가 아니라면 위 버튼 클래스들은 사용 자체가 불가능하다. 버튼에 요구하는 원초적인 기능은 눌림 이벤트를 어딘가로 넘겨주는 것인데, 위 클래스들은 너무 구체적인 정보를 가지고 있으므로 재사용이 상당히 제한된다. 다만 이 부분은 각 버튼만의 독특한 동작이 있다고 가정하고 넘어가도록 하자. 만약 Button 클래스 단위로 재사용하고 싶다면 중재자 클래스 내에 특정 버튼이 눌렸을 때 수행할 동작을 정의해 둔 딕셔너리를 두는 것을 고려할 수 있을 것 같다.

class Display1 {
    private history: string;

    constructor(private historyLength: number) {
        this.history = "";
    }

    addChar(ch: string) {
        this.history += ch;
        this.historyLength += 1;
    }

    resetHistory() {
        this.history = "";
        this.historyLength = 0;
    }

    getHistory() {
        return this.history;
    }

    isHistoryEmpty() {
        return this.history.length <= 0;
    }

    isHistoryFull() {
        return this.history.length >= this.historyLength;
    }

    canWrite() {
        return this.history.length >= 0 && !this.isHistoryFull();
    }

    display() {
        console.log(this.history);
    }
}

class LED1 {
    blink() {
        console.log("*");
    }
}

class AlphaButton {
    constructor(
        private alpha: string,
        private display: Display1,
        private led: LED1) {
    }

    press() {
        this.display.addChar(this.alpha);
        this.display.display();
        this.led.blink();
    }
}

class SaveButton {
    constructor(
        private display: Display1,
        private led: LED1) {
    }

    press() {
        this.led.blink();
        this.save();
    }

    private save() {
        return this.display.getHistory();
    }
}

class ResetButton {
    constructor(
        private display: Display1,
        private led: LED1) {
    }

    press() {
        this.led.blink();
        this.reset();
        this.display.display();
    }

    private reset() {
        this.display.resetHistory();
    }
}

중재자 패턴은 다음 상황에서 사용된다.

  • 여러 객체들이 잘 정의되었으나, 복잡한 상호작용을 가질 때. 객체간 의존성이 구조화되지 않고 이해하기 어려울 때.
  • 한 객체가 다른 객체를 너무 많이 참조, 의사소통해서 재사용이 어려울 때
  • 여러 클래스에 분산된 행동들이 상속 없이 상황에 맞게 수정되어야 할 때

중재자 패턴의 특징은 다음과 같다.

  • 객체들 사이의 결합 관계를 줄인다. Mediator 및 Colleague 각각을 독립적으로 다양화하고 재사용할 수 있다.
  • 객체간의 관계를 M대 N 관계에서 1대 N 관계로 단순화할 수 있다.
  • 객체 사이의 통신을 중재자 하에 캡슐화하므로, 사용자는 객체의 동작에 상관 없이 객체간 연결에만 집중할 수 있다. 이를 통해 객체가 어떻게 동작하는지 명확히 하는데 도움이 된다.
  • 모든 통제가 Mediator으로 집중되므로 자칫하면 Mediator이 모든 것을 수행하도록 설계될 수 있다. 이 경우 중재자 클래스의 복잡도가 상승하며, 유지보수가 어려워질 수 있다.

구현시 고려사항은 다음과 같다.

  • 객체들이 하나의 Mediator과만 동작한다면 굳이 Mediator을 추상클래스로 정의하고 이를 상속하는 ConcreteMediator을 두는 대신, Mediator에서 해당 동작을 수행하도록 구현한다. 즉, 해당 객체들로 다른 작업을 하도록 서브클래싱 하지 않을거라면 굳이 추상클래스로 만들 필요는 없다.
  • 객체들에서 이벤트가 발생하여 중재자 클래스와 자료를 주고받을 때 observer 패턴을 적용할 수 있다. 특정 객체에서 이벤트 발생을 알리면 중재자 클래스는 이에 대한 변동 사항을 각 옵저버(객체들)에게 통보하는 방식으로 구현된다.
  • 객체가 Mediator에게 통지할 때 자신을 인자로 넘겨 누가 메시지를 보낸 것인지 알릴 수 있다.

퍼사드 패턴과 비교되는 경우가 있는데, 이 둘은 다르다. 퍼사드 패턴은 복잡한 클래스들을 전부 알지 않고도 간단하게 사용할 수 있도록 추상화하여 편리한 인터페이스를 제공하는 것이 목적이지만, 중재자 패턴은 객체간의 상호작용을 추상화하고 연관 관계를 1대 N으로 설정하여 객체들 사이의 종속성을 줄이고 시스템의 확장성을 높이는 것이 목적이다. 메시지 측면에서 퍼사드 패턴은 일방적이지만, 중재자 패턴은 앙뱡향이다.

구성 요소

  • Mediator: Colleague 객체와 교류하는데 필요한 인터페이스를 정의한다.
  • ConcreteMediator: Colleague 객체와 조화를 이뤄 협력 행동을 구현하고, 자신이 맡을 colleague을 파악 및 관리한다.
  • Colleagues: 중재자 객체를 파악하고 있다. 만약 다른 객체와 통신이 필요하다면 중재자를 통해 통신을 수행한다.

예시 코드

class Mediator {
    constructor(private display: Display2,
        private led: LED2,
        private alphaButtons: AlphaButton2[],
        private saveButton: SaveButton2,
        private resetButton: ResetButton2
    ) { }

    setAlpha(ch: string) {
        if(this.display.canWrite()) {
            this.display.addChar(ch);
        }
        this.led.blink();
    }

    save() {
        const message = this.display.getHistory();
        // message을 메인 컴퓨터에 넘기는 로직
        this.led.blink();
    }

    reset() {
        this.display.resetHistory();
        this.led.blink();
    }
}

class Display2 {
    private history: string;

    constructor(private historyLength: number) {
        this.history = "";
    }

    addChar(ch: string) {
        // 허용량 이상 작성하면 에러난다고 가정.
        this.history += ch;
        this.historyLength += 1;
    }

    resetHistory() {
        this.history = "";
        this.historyLength = 0;
    }

    getHistory() {
        return this.history;
    }

    isHistoryEmpty() {
        return this.history.length <= 0;
    }

    isHistoryFull() {
        return this.history.length >= this.historyLength;
    }

    canWrite() {
        return this.history.length >= 0 && !this.isHistoryFull();
    }

    display() {
        console.log(this.history);
    }
}

class LED2 {
    blink() {
        console.log("*");
    }
}

class AlphaButton2 {
    constructor(
        private mediator: Mediator,
        private alpha: string,
    ) {
    }

    press() {
        this.mediator.setAlpha(this.alpha);
    }
}

class SaveButton2 {
    constructor(private mediator: Mediator) {
    }

    press() {
        this.mediator.save();
    }
}

class ResetButton2 {
    constructor(private mediator: Mediator) {
    }

    press() {
        this.mediator.reset();
    }
}

 앞에서 제시한 코드들을 Mediator을 기반으로 동작하도록 작성했다. 버튼 클래스들이 display 및 led를 직접 접근하던 구조를 중재자 기반으로 변경하여 각 클래스에서는 mediator을 통해 다른 객체와 통신하도록 코드를 수정했다.

 버튼의 경우 로직이 중재자로 이동하여 클래스로 구분할 필요가 없을 것 같다는 생각이 든다. 일반적인 Button 클래스로 변경하고 press 메서드에서 자기 자신을 파라미터로 넘기고, 구체적인 로직은 중재자 내부에서 처리하도록 구현하더라도 문제는 없을 것 같다. 현재 코드는 중재라는 동작에 중심을 두고 있으므로 그렇게까지 수정하지는 않았다.

 

'CS > 디자인패턴' 카테고리의 다른 글

[디자인패턴] Chain Of Responsibility 패턴  (0) 2023.05.06
[디자인패턴] Proxy 패턴  (0) 2023.05.02
[디자인패턴] Facade 패턴  (0) 2023.04.30
[디자인패턴] SOLID 원칙  (0) 2023.04.27
[디자인패턴] Bridge 패턴  (0) 2023.04.25