[Design Pattern] 중재자 패턴

olwooz·2023년 2월 25일

Design Pattern

목록 보기
16/22
객체 간의 혼란스러운 의존성을 줄이는 행동 패턴
객체 간의 직접적인 소통을 제한하고 중재자 객체를 통해서만 협업하도록 함

문제

고객 프로필 생성/관리 대화 상자 가정

  • 텍스트 필드, 체크박스, 버튼 등 다양한 form control로 구성

form 요소 중 일부는 다른 요소들과 상호작용할 수도 있음

  • e.g. “저는 반려동물이 있습니다” 라는 체크박스를 체크하면 반려동물 이름을 적는 숨겨진 텍스트 필드가 보여질 수 있음
  • e.g. 제출 버튼을 누르면 데이터를 저장하기 전에 모든 필드의 데이터를 검증

form 요소에 직접적으로 이런 로직들을 구현하면 재사용이 어려워짐

해결책

중재자 패턴 - 독립적으로 만들고 싶은 컴포넌트들 간의 직접적인 소통을 멈추고, 적절한 컴포넌트들로 호출을 리다이렉트하는 중재자 객체를 호출해 간접적으로 협업하게끔 함

  • 컴포넌트들은 여러 개의 다른 컴포넌트들과 서로 결합되지 않고 단일 중재자 클래스에만 의존하게 됨

위 예제에서는 대화 상자 클래스 자체가 중재자 역할을 할 수 있음

  • 중재자 클래스는 이미 자식 요소들에 대해 인지하고 있을 가능성이 높으므로 해당 클래스에 새로운 의존성을 도입할 필요조차 없음

가장 두드러지는 변화는 실제 form 요소들에서 일어남

e.g. 제출 버튼

  • 이전에는 유저가 버튼을 클릭하면 각 form 요소들을 검증했지만, 지금은 대화 상자에 클릭 이벤트를 알리는 작업만 함
  • 대화상자는 해당 알림을 받으면 스스로 검증을 실행하거나 작업을 각 요소에 전달함
  • 버튼은 form 요소들에게 결합되지 않고 대화 상자 클래스에만 의존함

모든 종류의 대화 상자에 대해 공통 인터페이스를 추출해 의존성을 더 느슨하게 할 수 있음

  • 인터페이스는 모든 form 요소들이 자신에게 발생한 이벤트에 대해 대화 상자에게 알림을 보내는 데 사용할 수 있는 알림 메서드를 선언 → 제출 버튼은 해당 인터페이스를 구현하는 어떤 대화 상자와도 호환 가능

중재자 패턴 - 다양한 객체들 간의 복잡한 관계를 단일 중재자 객체 안에 캡슐화

  • 클래스가 적은 의존성을 가질 수록 변경, 확장, 재사용이 쉬워짐

현실의 관제탑과 유사

구조

1. 컴포넌트 - 비즈니스 로직을 가지는 다양한 클래스들
   - 각 컴포넌트는 중재자 인터페이스 타입으로 선언된 중재자에 대한 참조를 가지고 있음
   - 컴포넌트는 중재자의 실제 클래스에 대해 알지 못함 → 다른 프로그램에서 다른 중재자에 연결해 재사용 가능
    
2. 중재자 - 컴포넌트들과의 소통 메서드(주로 단일 알림 메서드)를 선언하는 인터페이스
   - 발신자의 클래스와 수신하는 컴포넌트 간 결합이 일어나지 않는 선에서, 
   컴포넌트들은 자신의 객체를 포함한 어떤 컨텍스트든 해당 메서드의 인수로 전달 가능
    
3. concrete 중재자 - 다양한 컴포넌트 사이 관계를 캡슐화함
   - 종종 관리하는 모든 컴포넌트에 대한 참조를 가지고 있고, 때로는 생명 주기까지 관리함
    
4. 컴포넌트들은 다른 컴포넌트에 대해 알지 못해야 함
   - 만약 컴포넌트에게 또는 컴포넌트 내부에서 중요한 일이 발생하면 중재자에게만 알려야 함
   - 중재자는 알림을 받으면 쉽게 발신자를 식별할 수 있고, 
     이는 응답으로 어떤 컴포넌트가 작동해야 하는지 결정하는 데 충분함
   - 컴포넌트 관점에서 블랙박스와 같음 → 발신자는 누가 요청을 처리할지 모르고, 수신자는 누가 요청을 보냈는지 모름

적용

클래스가 다른 여러 클래스들과 단단히 결합돼 변경이 어려운 경우

- 중재자 패턴은 클래스들 간의 모든 관계를 별도의 클래스로 추출해 
  특정 컴포넌트에게 일어나는 변화를 나머지 컴포넌트들로부터 격리 가능

컴포넌트가 다른 컴포넌트들에 너무 많이 의존해 다른 프로그램에서 재사용할 수 없는 경우

- 중재자 패턴을 적용하면 각 컴포넌트는 다른 컴포넌트들에 대해 알지 못함
- 중재자 객체를 통해 서로 간접적으로는 소통 가능
- 컴포넌트를 다른 앱에서 재사용하려면 새로운 중재자 클래스를 제공해줘야 함

다양한 컨텍스트에서 단지 몇 가지의 기본 행동을 재사용하기 위해 수많은 컴포넌트 서브클래스들을 생성하고 있는 경우

- 모든 컴포넌트들 간의 관계는 중재자 안에 포함되어 있기 때문에, 
  새 중재자 클래스를 도입하면 컴포넌트들 자체를 변경하지 않고 컴포넌트들이 협업하는 방식을 완전히 바꿀 수 있음

구현방법

1. 독립적으로 만들면 더 좋을 것 같은 단단히 결합된 클래스들을 식별 (e.g. 유지보수, 재사용 등의 이유)

2. 중재자 인터페이스를 선언하고, 중재자와 컴포넌트들 간의 커뮤니케이션 프로토콜 설명
   - 대부분 컴포넌트들로부터 알림을 받는 단일 메서드면 충분
   - 이 인터페이스는 컴포넌트 클래스들을 다양한 컨텍스트에서 재사용하고 싶을 때 중요 
   → 컴포넌트가 일반 인터페이스를 통해 중재자와 협업한다면, 컴포넌트를 중재자의 다른 구현과도 연결할 수 있음
    
3. concrete 중재자 클래스 구현
   - 중재자 내부에 모든 컴포넌트에 대한 참조를 저장하는 것을 고려 → 중재자 메서드에서 어떤 컴포넌트든 호출 가능
    
4. 중재자가 컴포넌트 객체의 생성/파괴를 담당하게 할 수도 있음
   - 이 경우 중재자가 팩토리나 퍼사드를 닮게 됨
    
5. 컴포넌트들은 중재자 객체에 대한 참조를 저장해야 함
   - 연결은 주로 중재자 객체가 인수로 전달되는 컴포넌트의 생성자에서 수립
    
6. 다른 컴포넌트의 메서드 대신 중재자 알림 메서드를 호출하도록 컴포넌트들의 코드 변경
   - 다른 컴포넌트를 호출하는 코드를 추출해 중재자 클래스로 이동, 
     중재자가 컴포넌트로부터 알림을 받으면 해당 코드 실행

장단점

장점

- SRP - 다양한 객체들 간 커뮤니케이션을 한 곳으로 추출해 이해, 유지보수가 더 쉬워짐
- OCP - 실제 객체를 변경하지 않고 새로운 중재자 도입 가능
- 컴포넌트들 간의 결합 감소
- 개별 객체 재사용 쉬워짐

단점

- 중재자가 시간이 지나며 God Object가 될 수 있음

다른 패턴과의 관계

- 책임 연쇄, 커맨드, 중재자, 옵저버 패턴은 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룸
  - 책임 연쇄 패턴 - 요청이 처리될 때까지 잠재적 수신자들로 구성된 동적 체인을 따라 전달함
  - 커맨드 - 수신자와 발신자 단방향 커넥션 수립
  - 중재자 - 수신자와 발신자 사이의 직접적인 연결을 제거하고 중재자 객체를 통해서만 소통하게 함
  - 옵저버 - 수신자들이 동적으로 요청 수신을 구독/구독 취소할 수 있음
  
- 퍼사드 패턴 & 중재자 패턴 - 비슷한 역할, 밀접하게 결합된 클래스들의 협업을 체계화함
    퍼사드 - 하위 시스템 객체들에 대한 단순화된 인터페이스를 정의하지만 새로운 기능을 도입하지 않음, 
            하위 시스템은 퍼사드의 존재를 모름, 하위 시스템 내부 객체들끼리는 직접 소통 가능
    중재자 - 시스템의 컴포넌트들 간의 소통을 중앙화함, 
            컴포넌트들은 서로 직접 소통하지 않고 중재자 객체에 대해서만 알고 있음
    
- 중재자와 옵저버의 차이가 종종 애매함
  - 대부분 둘 중 한 패턴을 구현하지만 때로는 둘 다 동시에 적용할 수 있음
  - 중재자의 목표 - 시스템 컴포넌트들의 집합 간 상호 의존성을 없애는 것, 컴포넌트들은 단일 중재자 객체에 의존
  - 옵저버의 목표 - 객체들 간에 동적 단방향 커넥션 수립, 일부 객체는 다른 객체의 종속자 역할 수행
  - 옵저버에 의존하는 중재자 패턴의 인기 있는 구현 존재 - 중재자 객체는 발행자, 
    컴포넌트들은 중재자 이벤트를 구독/구독 취소하는 구독자 역할을 함 → 옵저버와 비슷
  - 중재자 패턴은 다른 방식들로 구현 가능
    e.g. 모든 컴포넌트들을 같은 중재자 객체에 영구적으로 연결할 수 있음 
    → 옵저버와 닮지 않았지만 여전히 중재자 패턴
  - 모든 컴포넌트들이 발행자가 돼 서로 동적 연결을 허용하면 
    중앙화된 중재자 객체는 없고 옵저버의 분산된 집합만 존재하게 됨

TypeScript 예제

/**
 * The Mediator interface declares a method used by components to notify the
 * mediator about various events. The Mediator may react to these events and
 * pass the execution to other components.
 */
interface Mediator {
    notify(sender: object, event: string): void;
}

/**
 * Concrete Mediators implement cooperative behavior by coordinating several
 * components.
 */
class ConcreteMediator implements Mediator {
    private component1: Component1;

    private component2: Component2;

    constructor(c1: Component1, c2: Component2) {
        this.component1 = c1;
        this.component1.setMediator(this);
        this.component2 = c2;
        this.component2.setMediator(this);
    }

    public notify(sender: object, event: string): void {
        if (event === 'A') {
            console.log('Mediator reacts on A and triggers following operations:');
            this.component2.doC();
        }

        if (event === 'D') {
            console.log('Mediator reacts on D and triggers following operations:');
            this.component1.doB();
            this.component2.doC();
        }
    }
}

/**
 * The Base Component provides the basic functionality of storing a mediator's
 * instance inside component objects.
 */
class BaseComponent {
    protected mediator: Mediator;

    constructor(mediator?: Mediator) {
        this.mediator = mediator!;
    }

    public setMediator(mediator: Mediator): void {
        this.mediator = mediator;
    }
}

/**
 * Concrete Components implement various functionality. They don't depend on
 * other components. They also don't depend on any concrete mediator classes.
 */
class Component1 extends BaseComponent {
    public doA(): void {
        console.log('Component 1 does A.');
        this.mediator.notify(this, 'A');
    }

    public doB(): void {
        console.log('Component 1 does B.');
        this.mediator.notify(this, 'B');
    }
}

class Component2 extends BaseComponent {
    public doC(): void {
        console.log('Component 2 does C.');
        this.mediator.notify(this, 'C');
    }

    public doD(): void {
        console.log('Component 2 does D.');
        this.mediator.notify(this, 'D');
    }
}

/**
 * The client code.
 */
const c1 = new Component1();
const c2 = new Component2();
const mediator = new ConcreteMediator(c1, c2);

console.log('Client triggers operation A.');
c1.doA();

console.log('');
console.log('Client triggers operation D.');
c2.doD();
// Output.txt

Client triggers operation A.
Component 1 does A.
Mediator reacts on A and triggers following operations:
Component 2 does C.

Client triggers operation D.
Component 2 does D.
Mediator reacts on D and triggers following operations:
Component 1 does B.
Component 2 does C.

참고 자료: Refactoring.guru

0개의 댓글