Notice
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Mediator Pattern 본문

Design Pattern/Behavioral Pattern

Mediator Pattern

defacto standard 2018. 2. 16. 23:50

1. 가정


 - 채팅을 구현한다.

 - 채팅은 최대 4명이 가능하다.



2. Naive Code


- User

public class User {
private String name;
private String message;
List<User> userList;

public User(String name) {
this.name = name;
userList = new ArrayList<User>();
}

public void setMessage(String message) {
this.message = message;
}

public void addUser(User user) {
userList.add(user);
}

public void chat() {
for(User user : userList)
user.receiveMessage(this);
}

public void receiveMessage(User user) {
System.out.println("< " + this.name + "의 채팅창 >");
System.out.println(user.name + " : " + user.message);
}

}

채팅하는 사람을 의미하는 User클래스이다.


name은 유저의 이름을 의미하고, chatString은 자신이 전송하려는 메시지에 해당한다.

userList는 자신이 메시지를 보낼 다른 User객체를 담는 리스트에 해당한다.


chat()같은 경우 현재 User 객체가 다른 User 객체에게 메시지를 전송하는 메서드이며

reveiveChat()같은 경우 현재 User 객체가 다른 User 객체로부터 메시지를 전송받는 메서드이다.


 - NaiveClient

public class NaiveClient {
public static void main(String args[]) {
User user1 = new User("User1");
User user2 = new User("User2");
User user3 = new User("User3");
User user4 = new User("User4");

user1.addUser(user1);
user1.addUser(user2);
user1.addUser(user3);
user1.addUser(user4);

/*
* user2~4.addUser(user1~4);
*/

user1.setMessage("I'm User1");
user1.chat();

}
}

User1만 말하는 코드이다. 


수행 결과는 다음과 같다.


3. 문제점


현재 소스코드는 User객체와 User객체가 직접적으로 통신한다.


채팅할 수 있는 복수의 User가 존재함에 따라, 일괄적인 메시지 전송을 위해, User 클래스 내부에는 자신을 포함하는 User의 리스트가 존재하여 이를 통한 일괄전송이 가능하다.


위 소스코드는 여러 방면으로 문제점이 존재한다.


만약, 다음과 같은 요구사항의 변경이 생겼다고 가정해보자.

 - 채팅할 수 있는 유저 수의 제한이 없다.


우선, User객체가 현재 4개인데, User 객체 안에는 List가 존재한다. User객체 할당 시에 List가 생성이 된다.

이 말은, 동일한 List의 내용이 4개가 존재한다는 점이다.


그말은 같은 내용을 담는 리스트가, User 숫자의 크기만큼, User 숫자의 갯수만큼 존재한다는 것을 의미한다.


또한, 모든 User 객체가 Full-Connection을 맺어야 한다.


위에서 user1.addUser()만 4번을 불렀는데, 이는 user1 만이 채팅메시지를 보내기 때문이다.

만약 user2~4가 모두 채팅을 제대로 하려면, user2~4.addUser() 역시도 실행 시켜야 한다.


이는 새로운 User가 추가되었을 때,

새로운 User객체의 UserList에 기존의 User객체들을 모두 추가하여야 하고,

기존의 User객체들의 UserList에도 새로운 User객체를 추가하여야 함을 의미한다.


메모리 사용량은 메모리 사용량대로, 갯수는 갯수대로 늘어나며,

중복된 소스코드로 유지보수는 복잡해지며 UserList를 유지보수해야 하는 연산때문에 성능은 저하된다.


 - 귓속말 기능을 추가한다.

기능을 추가하는 것이기 때문에 소스코드가 변경되는 것은 어쩔 수 없다. 하지만, 현재 소스코드같은 경우 User 클래스가 모든 기능을 수행하는 GodClass가 되어간다.


현재 User 객체들의 리스트 자체가 User 클래스에 있기 때문에, 귓속말 기능 역시 User 클래스에 추가될 확률이 높아진다.

따라서 User 클래스는 더욱 GodClass에 가까워질 확률이 높아진다.



4. 해결방안


현재 문제가 되는 이유는 User클래스 내부에 User객체들간의 관리를 수행하는 로직이 작성되어있고, 이에 따른 User객체 간의 직접적인 참조때문이다.


즉, SRP를 위반하여 발생한다. User객체를 관리하는 클래스를 따로 두어 관리를 한다면, User를 추가하는 로직은 하나만 있어도 상관이 없을 것이다.


또한 이 클래스를 통해 채팅을 구현하게 한다면, 어떤 채팅 로직이 쓰이는지를 클래스별로 분리시켜 SRP를 도모하는 것 역시도 수월해질 것이다.


현재 User에게 충분한 것은 name, message, chat(), receiveChat() 정도면 충분하다. 다른 요소들은 다른 클래스를 만들어서 User클래스의 부담을 줄일 필요가 있다.



User객체간의 통신을, 중간에 클래스를 두고 통신을 하게 만든다면, User객체 끼리의 참조는 사라진다.


중간 클래스에 UserList를 통해 User객체의 관리를 하게 된다면, User에서 UserList는 사라지게 된다.


UserList가 중간 클래스라는 한 부분에서만 관리되기 때문에, User가 추가되더라도 기존의 User객체와 새로운 User객체간의 Connection을 맺지 않아도 된다.




이렇게 중간에 클래스를 두고 서로 다른 객체간의 통신을 구현한 것을 Mediator Pattern이라고 한다.


중간에서 중개역할을 하는 클래스를 Mediator 라고 한다.


예제에 적용한다면 유저를 추가/제거하고 특정 유저에게 채팅을 전송하는 로직을 구현하는 '채팅방' 정도를 Mediator라고 할 수 있다.


채팅방의 종류가 여러개가 있을 수 있다.


예를 들면 게임을 같이 할 수 있는 게임채팅방과, 채팅만 가능한 일반 채팅방 등의 분류가 있을 수 있다.


따라서 Mediator는 추상적인 요소로 두고, 이를 구현/상속하는 ConcreteMediator를 두는 것이 좋다.


여기서는 채팅을 예로 들어서 User만 필요할 수도 있다.

하지만 시스템/관리자라는 개념이 생기고, 이 요소들이 User들에게 메시지를 전송할 일이 있을 수 있다.


예를 들면 게임채팅방에서 게임 진행과 관련된 시스템 메시지를 전송하여야 하는 경우가 있을 수 있고,

점검을 해야 해서 관리자가 직접 메시지를 전송하던지 스케쥴러에 의해 지정된 시간 등에 시스템 메시지가 날아가는 등의 구현을 할 수도 있다.


따라서 User, System, Admin 등과 같은 클래스를 만들어서 컨트롤 하는 것이 적합할 것이다.

User가 할 수 있는 일, System이 할 수 있는 일, Admin이 할 수 있는 일이 전부 다르기 때문이다.


User는 자신이 입력한 채팅 메시지를 주고받는다.

System은 지정된 문자열 메시지를 보낸다.

Admin은 관리자가 입력한 메시지를 보낸다. 

와 같은 예를 들 수 있겠다.


물론 User클래스로도 이러한 부분은 해결할 수 있으나, 내부적으로 System, Admin 만이 가지고 있는 기능을 구현하기에는 적합하지 않다.


하지만 '통신' 한다는 관점에서는 같기 때문에, 이를 추상화 하여 관리한다면 

User, System, Admin의 역할을 클래스로 분명히 할 수 있으면서도 이들을 일관적으로 다룰 수 있게 된다.


이러한 통신을 수행하는 클래스들을 추상화한 개념을 Colleague라고 한다.

그리고 User, System, Admin등은 Colleague를 상속받거나 구현하는 ConcreteColleague라고 한다.



5. Solution Code


 - Colleague

public abstract class Colleague {
private Mediator mediator;
protected String name;
private String message;

public Colleague(String name){
this.name = name;
}

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

public String getName() {
return this.name;
}

public void setMessage(String message) {
this.message = message;
}

public String getMessage(){
return this.message;
}

public void send() {
System.out.println(this.name + " send()");
System.out.println();
mediator.mediate(this);
}

public abstract void receive(Colleague colleague);
}

다른 객체와 통신을 하는 요소이다. 예제의 User, Admin, System의 추상적인 요소에 해당한다. 통신에 필요한 메서드를 가지고 있다.

다른 객체와의 통신은 직접하지 않으며, Mediator 객체를 통해 요청을 하는 방식으로 통신한다.


 - ConcreteColleague1~3

// User
public class ConcreteColleague1 extends Colleague {

public ConcreteColleague1(String name) {
super(name);
}

@Override
public void receive(Colleague colleague) {
System.out.println(this.name + " recevied " + colleague.getName() + "'s Message : " + colleague.getMessage());
}

}
// System
public class ConcreteColleague2 extends Colleague {

public ConcreteColleague2(String name) {
super(name);
}

@Override
public void receive(Colleague colleague) {
System.out.println("System can't receive messages");
}

}
// Admin
public class ConcreteColleague3 extends Colleague {

public ConcreteColleague3(String name) {
super(name);
}

@Override
public void receive(Colleague colleague) {
System.out.println("Admin can't receive messages");
}

}

예제의 User, Admin, System에 해당하는 클래스이다. Colleague클래스를 상속받음으로써 통신에 필요한 메서드를 구현해야 한다.

User는 메시지를 받을 수 있으나, Admin/System은 보내는 것만 가능하다.


 - Mediator

public interface Mediator {
public void addColleague(Colleague colleague);
public void mediate(Colleague colleague);
}

객체간의 통신을 중재하는 클래스이다. 통신의 대상이 되는 Colleague를 추가하는 메서드가 존재해야 한다.


 - ConcreteMediator

public class ConcreteMediator1 implements Mediator {
private List<Colleague> colleagueList;

public ConcreteMediator1() {
this.colleagueList = new ArrayList<Colleague>();
}

@Override
public void addColleague(Colleague colleague) {
this.colleagueList.add(colleague);
}

@Override
public void mediate(Colleague colleague) {
for(Colleague receiverColleague : colleagueList) {
System.out.println("\tMediating " + colleague.getName() + " to " + receiverColleague.getName());
receiverColleague.receive(colleague);
}
}
}

Mediator를 구현하는, 실질적으로 중재하는 로직을 가지는 클래스이다.

서로 통신하는 객체들을 가지고있어야 하며, 특정 Colleague로부터 요청이 들어오면, 상대 Colleague를 찾아서 해당 메시지를 전달한다.


Mediator는 Colleague에 대한 레퍼런스를 가지고 있고

Colleague는 Mediator에 대한 레퍼런스를 가지고 있다.


즉, 양방향 연관관계(Bi-Directional Associations)를 가진다.


 - Client

public class Client {
public static void main(String args[]) {
Mediator mediator1 = new ConcreteMediator1();
Colleague colleague1 = new ConcreteColleague1("User1");
Colleague colleague2 = new ConcreteColleague1("User2");
Colleague colleague3 = new ConcreteColleague2("System");
Colleague colleague4 = new ConcreteColleague3("Admin");

colleague1.setMediator(mediator1);
colleague2.setMediator(mediator1);
colleague3.setMediator(mediator1);
colleague4.setMediator(mediator1);

mediator1.addColleague(colleague1);
mediator1.addColleague(colleague2);
mediator1.addColleague(colleague3);
mediator1.addColleague(colleague4);

colleague1.setMessage("I'm User1");
colleague1.send();
}
}

채팅을 구현한 Client이다. 


수행 결과는 다음과 같다.



실제로 채팅 프로그램을 만든다면,


사용자가 로그인을 하면 Colleague 객체가 만들어지고, 채팅방을 만들면 Mediator객체가 만들어진다.


채팅방에 접속하는 경우, Colleague와 Mediator가 서로 setter와 add 함수로 양방향 연관관계를 가진다.


이후 사용자가 접속을 할 때마다 새로운 Colleague 객체와 Mediator객체가 서로 참조하여 관계를 가질 것이다.


Colleague는 Thread로 작성되어 내용을 입력하면 실시간으로 Mediator에 요청을 하고,

Mediator는 계속해서 Colleague들을 중재(채팅메시지 전달)하게 될 것이다.



6. Mediator Pattern


객체간의 복잡한 커뮤니케이션이 필요할 때, 직접적으로 관계를 맺어서 통신하는 것이 아닌, 이를 중재하는 중간 클래스를 통해 커뮤니케이션하는 패턴이다.


예제에서 볼 수 있듯이, 객체가 직접적으로 교류하게 된다면 한 클래스에서 복잡한 로직을 가지고 있게 되고, 비효율적인 자료구조와 비효율적인 연산으로 어플리케이션의 질을 떨어트린다.


현실세계에서는 라우터가 Mediator Pattern을 구현한다고 생각하면 쉽다.


네트워크 상에서 모든 Node끼리 연결되어 있는 것을 Fully Connected Network Topology라고 한다.

네트워크 상에서는 하나의 노드가 더이상 사용할 수 없게 되더라도 다른 노드와 1:1로 연결되어있기에 통신에 장애가 발생하지 않는다는 장점이 있다고는 하지만, 이를 소프트웨어의 디자인에 도입한다면 엄청난 유지보수가 필요하게 된다.


Mediator Pattern은 이러한 네트워크 트래픽을 중간에서 중재하여 적절한 노드에게 포워딩 시키는 방식이다.

Network상에서는 Star Network Topology에 해당한다.


네트워크에서는 중재자가 고장나면 완전히 쓸모없게 된다. 소프트웨어에서도 이러한 중재자 역할을 하는 Mediator를 잘못 코딩한다면 문제가 발생할 것이다.


하지만 한 클래스에서 과도한 역할을 분리해내고, 한 곳에서 집중하는 것이 한 클래스에 모든 기능을 넣어서 관리하는 것 보다 몇 배는 유지보수에 힘이 덜 든다는 것만 기억하자.



- UML & Sequence Diagram


- Class Diagram



7. 장단점

장점 :

 - 객체가 서로 복잡하게 통신해야하는 경우, 통신하는 객체의 클래스가 GodClass가 되는 것을 방지한다. 이로 인해 시스템이 커져도 점점 복잡해지는 것을 방지할 수 있다.


단점 :


'Design Pattern > Behavioral Pattern' 카테고리의 다른 글

Visitor Pattern  (0) 2018.02.16
Chain Of Responsibility Pattern  (0) 2018.02.16
Iterator Pattern / Iterable Interface  (0) 2018.02.15
Template Method Pattern (기본)  (0) 2017.09.26
Observer Pattern  (0) 2017.09.26
Comments