Concepts/Design Pattern

행동 패턴 - 중재자(Mediator)

ocwokocw 2021. 6. 11. 22:31

- 참조: https://refactoring.guru/design-patterns/mediator

- 중재자(Mediator) 패턴

중재자 패턴은 객체들간의 무질서한 의존성을 줄여주는 행동 패턴이다. 이 패턴은 객체들간의 직접적인 통신을 제한하고, 중재자 객체를 통해서만 협력하도록 강제한다.


- 문제점

고객의 프로필을 만들고 수정하는 대화상자가 있다고 가정해보자. 이 대화상자에는 text 필드, 체크박스, 버튼 과 같은 다양한 요소가 있다. 웹서핑을 하다보면 form 요소들끼리 서로 연관있는 UI 를 본적이 있을것이다. 예를 들어 어떤 체크박스를 체크하면 인풋박스가 갑자기 나타나거나 저장버튼을 누르면 입력 필드에 대해서 Validation 메시지를 띄우기도 한다.

 

이런 로직의 코드가 form 요소안에 있으면 이런 요소의 클래스들은 다른 form 에서 재사용하기가 어려워진다. 예를 들어 위에서 언급한 체크박스 체크시 인풋박스를 보여주는 로직이 해당 체크박스 안에 있으면 이 체크박스를 다른 form 에서는 재사용할 수 없다.


- 해결책

중재자 패턴은 독립적으로 구성하고싶은 컴포넌트들간에 직접적인 통신을 막아준다. 대신 요청을 적절한 컴포넌트로 전달시키는 중재자 객체를 호출해서 간접적으로 소통해야 한다. 그렇게 되면 컴포넌트들내의 복잡한 의존관계가 아니라, 하나의 중재자 클래스에 대해서만 의존성을 가지게 된다.

 

프로필 수정 예제에서 대화상자 클래스가 중재자라고 할 수 있다. 대화상자 클래스는 모든 하위 요소들을 알고 있어서, 대화상자 클래스에 새로운 의존관계를 설정하지 않아도 된다.

 

제출 버튼이 있다고 가정해보자. 이전에는 유저가 제출 버튼을 클릭할 때, form 요소들 각각의 값에 대해서 유효성 체크를 해야만 했다. 하지만 지금은 click을 하면 대화상자에게 통보만 하면 된다. 대화상자는 자체적으로 유효성체크를 수행하거나, 해당 작업을 개별 요소들에게 전달한다. 제출 버튼은 form의 수많은 요소들에 엮여있지 않고, 대화상자 클래스 하나에만 의존하게 된다.

 

대화상자들의 모든 타입을 위한 공통 인터페이스를 추출하면 의존성을 더 줄일 수 있다. 공통 인터페이스에서는 모든 form 요소들에서 일어나는 이벤트를 대화상자에게 알릴 수 있는 메소드를 정의한다. 이렇게 구현하면 제출버튼은 해당 인터페이스를 구현하는 어떤 대화상자와도 동작할 수 있다.

 

이런 방법으로 중재자 패턴은 하나의 중재자 객체로 다양한 객체들 사이의 복잡한 관계망을 캡슐화한다. 의존관계가 덜 복잡한 클래스는 수정하기 쉬우며, 재사용하거나 확장도 훨씬 수월하다.


- 실생활 예제

공항 관제 구역에 접근하거나 출발하는 항공기안에 파일럿들은 서로 직접 통신하지 않는다. 대신 활주로 근처 타워안에 있는 항공 관제관과 통신한다. 항공 관제관이 없으면 파일럿들은 공항인근의 모든 비행기안에 있는 파일럿들과 통신해서 착륙 우선순위를 정해야 한다.


- 참여자

중재자 패턴의 동작구조와 참여자들을 알아보자.

  • Components(ComponentA~ComponentD): Component 들은 비즈니스 로직을 포함하는 클래스들이다. 각 Component 는 Mediator 인터페이스로 선언된 중재자를 참조하고 있다. Component 는 중재자의 실제 클래스는 알지 못하므로, 다른 중재자에 연결하여 재사용할 수도 있다.
  • Mediator(Mediator): Mediator 인터페이스는 component 들간의 소통을 위한 메소드를 정의하고 있는데, 대게 알림을 위한 단일 메소드를 정의하고 있다.
  • Concrete Mediator(ConcreteMediator): 다양한 Component 들간의 관계를 캡슐화 한다. 종종 Concrete Mediator 는 모든 Component 에 대한 참조를 가지고 있으면서, 가끔은 Component 들의 생명주기를 관리하기도 한다.

 

하나의 Component 는 다른 Component 에 대해서 알면 안 된다. 만약 Component 내부나 어떤 Component 에게 중요한 이벤트가 발생하면 중재자에게만 알려야 한다. 중재자는 알림을 받으면 sender를 식별할 수 있고, 어떤 Component 가 반응해야 하는지 판단할 수 있다.

 

Component의 관점에서 보면 마치 블랙박스와 같다고 할 수 있다. Sender 는 누가 요청을 다루게 될지 알 수 없고, Receiver는 최초에 누가 요청을 보냈는지 알 수 없다.


- 시나리오

이번 예제에서는 Mediator 패턴을 이용하여 다양한 버튼이나 체크박스, 텍스트 field와 같은 UI 클래스들간의 상호 의존관계를 제거해본다.

위 다이어그램은 Dialog 안에 Button, Textbox, Checkbox 의 요소들이 있는 상황을 다이어그램으로 표현한것이다. User 에 의해 이벤트가 일어난 요소는 다른 요소들과 직접적으로 통신하지 않는다. 대신 해당 요소는 중재자에게 해당 이벤트를 알려준다.

 

위 예제에서는 AuthenticationDialog 자체가 중재자가 된다. AuthenticationDialog 는 Button, Textbox, Checkbox 들이 어떻게 서로 협력해서 동작하는지 알고 있고, 간접적인 의사소통을 원활하게 해주고 있다. 이벤트 알림을 받으면 Dialog 는 어떤 요소가 이벤트를 처리해야하는지 그리고 호출을 적절하게 리다이렉팅 하는지 결정한다.


- Java 예제

예제를 통해 알아보자. 먼저 Mediator 부터 정의해보자. Mediator 는 notify 단일 메소드로 정의했다. Event를 발생시키는 요소 인자 sender와 어떤 이벤트인지에 대한 인자 event 를 선언하였다.

 

public interface Mediator {

	void notify(Component sender, String event);
}

 

그 다음 Dialog 요소들의 부모 클래스인 Component를 정의한다. Mediator를 참조하는 중재자 dialog를 참조하고 있고, click이나 keypress 이벤트가 발생할 때 중재자에게 현재 자신의 객체 this와 발생한 이벤트를 넘기고 있다.

 

public class Component {

	protected Mediator dialog;

	public Component(Mediator dialog) {
		super();
		this.dialog = dialog;
	}

	public void click() {
		dialog.notify(this, "click");
	}
	
	public void keypress() {
		dialog.notify(this, "keypress");
	}
}

 

Component 의 세부클래스인 Button, Textbox, Checkbox를 정의해보자. Button과 Textbox는 Component 에서 정의한 click과 keypress 를 그대로 사용하면 된다. 

 

Checkbox의 경우 약간 다르게 정의해보았는데, Checkbox는 현재 체크된 상태를 알려주는 메소드가 있어야 한다고 생각했다. 그리고 Checkbox 클릭시 중재자에게 notify를 할 때 "click" 과 별도로 "check" 이벤트를 전달해야 한다고 생각해서 중재자에게 "click" 이벤트를 공지하고 하였다. 그 다음 super.click() 을 호출한것은 'click" 이벤트도 전파되어야 하기 때문이라고 가정했다.

 

public class Button extends Component{

	public Button(Mediator dialog) {
		super(dialog);
	}
}

public class Textbox extends Component {

	public Textbox(Mediator dialog) {
		super(dialog);
	}
}

public class Checkbox extends Component{

	private boolean checked;
	public Checkbox(Mediator dialog) {
		super(dialog);
		
		checked = false;
	}
	
	public boolean checked() {
		return checked;
	}
	
	@Override
	public void click() {
		checked = !checked;
		dialog.notify(this, "check");
		super.click();
	}
}

 

이제 AuthenticationDialog 클래스를 살펴보자. Dialog 안에는 많은 요소들이 있다. 이 글의 초반부에서 설명한것과 같이 만약 중재자 패턴이 아니었다면 isJoinForm 체크박스를 클릭할 때, 회원가입 요소들(regUsername, regPassword, regEmail)을 컨트롤 하므로, 다른 Dialog 에서 체크박스를 재사용하지 못했을것이다.

 

아래 코드에서 생성자를 눈여겨보자. 위의 클래스 다이어그램에서 각 요소들(Button, Textbox, Checkbox)은 중재자를 참조해야한다. AuthenticationDialog는 Mediator 인터페이스를 구현하는 중재자이다. 생성자에서 자기자신 this를 각 요소의 생성자함수에 넘겨줌으로써 중재자인 자신을 참조하도록 하고 있다.

 

Dialog 내의 모든 요소간 통신이나 제어는 notify 메소드에서 담당한다. ok 버튼을 누르거나 login 및 회원가입 form간 전환, ID 저장 기능은 각 해당 요소에서 제어하지 않는다.

 

public class AuthenticationDialog implements Mediator {

	public String title;
	public Checkbox isJoinForm;
	
	public Textbox loginUserName;
	public Textbox loginPassword;
	
	public Textbox regUsername;
	public Textbox regPassword;
	public Textbox regEmail;
	
	public Button ok;
	public Button cancel;
	
	public Checkbox remeberMe;
	
	public AuthenticationDialog() {
		
		super();
		
		title = "Login";
		isJoinForm = new Checkbox(this);
				
		loginUserName = new Textbox(this);
		loginPassword = new Textbox(this);
		
		regUsername = new Textbox(this);
		regPassword = new Textbox(this);
		regEmail = new Textbox(this);
		
		ok = new Button(this);
		cancel = new Button(this);
		
		remeberMe = new Checkbox(this);
	}

	public void printCurrentStatus() {
		System.out.println("Title: " + title);
		System.out.println("isJoinForm: " + isJoinForm.checked());
		System.out.println("remeberMe: " + remeberMe.checked());
	}

	@Override
	public void notify(Component sender, String event) {

		if(sender.equals(ok) && "click".equals(event)) {
			
			if(isJoinForm.checked()) {
				System.out.println("Validate [regUsername, regPassword, regEmail] elements");
				System.out.println("And user join this system.");
			}
			else {
				System.out.println("Login!");
			}
		}
		else if(sender.equals(isJoinForm) && "check".equals(event)) {
			
			if(isJoinForm.checked()) {
				title = "User Join Form";
				System.out.println("show [regUsername, regPassword, regEmail] elements");
			}
			else {
				title = "Login";
				System.out.println("hide [regUsername, regPassword, regEmail] elements");
			}
		}
		else if(sender.equals(remeberMe) && "check".equals(event)) {
			
			if(remeberMe.checked()) {
				System.out.println("set ID in cookie");
			}
			else {
				System.out.println("remove ID from cookie");
			}
		}
	}
}

 

중재자 패턴을 테스트하는 코드를 작성해보자. 시나리오는 다음과 같다.

 

Login 에서 회원가입 양식으로 전환하여 OK버튼(회원가입)을 클릭한 후, 다시 Login 양식으로 전환한다. 그리고 ID 기억하기 체크박스를 체크 한 후 Login을 하는 시나리오이다.

 

public static void main(String[] args) {

		AuthenticationDialog dialog = new AuthenticationDialog();
		
		System.out.println("==== 1.Current state");
		dialog.printCurrentStatus();
		
		System.out.println();
		System.out.println("==== 2. Convert from login to user join form");
		dialog.isJoinForm.click();
		
		System.out.println();
		System.out.println("==== 3. submit user join");
		dialog.ok.click();
		
		System.out.println();
		System.out.println("==== 4. Convert from user join to login");
		dialog.isJoinForm.click();
		
		System.out.println();
		System.out.println("==== 5. Check remember me");
		dialog.remeberMe.click();
		
		System.out.println();
		System.out.println("==== 6. Do login");
		dialog.ok.click();
	}

==== 1.Current state
Title: Login
isJoinForm: false
remeberMe: false

==== 2. Convert from login to user join form
show [regUsername, regPassword, regEmail] elements

==== 3. submit user join
Validate [regUsername, regPassword, regEmail] elements
And user join this system.

==== 4. Convert from user join to login
hide [regUsername, regPassword, regEmail] elements

==== 5. Check remember me
set ID in cookie

==== 6. Do login
Login!