2014년 2월 12일 수요일

채팅과 Mediator



디자인 패턴이라는 카테고리로 포스팅하는 마지막 글이 될 것 같네요.
앞으로 디자인 패턴과 알고리즘은 "로직" 이라는 게시판으로 통합할 예정이예요.

알고리즘은 로직의 Concrete이고, 로직은 패턴으로 이루어지며, 패턴은 곧 로직을 의미해요.
패턴없는 설계는 리팩토링 할 때, 대폭 무너지는 부실 공사에 불과하며,
패턴없는 하드코딩은 유연하게 기능을 수정하거나 추가하기 힘든 강체로 이루어진 저급 소프트웨어를 양산하게 됩니다.

그러나 아쉽게도 IT SW 기술부분에서는 아직 개발도상국에 불과한 우리 나라는 외국에 비해 상대적으로 낮은 기술력만큼 패턴에 대한 인식도 높지 않습니다.

고급 개발자들 사이에서는 패턴이란 익숙한 개념에 불과하지만, 그런 고급 개발자들은 상대적으로 극소수에 불과하며, 그마저도 해외로 빠져나가고 있는 현실이죠. 저는 발에 밟히는 하급 개발자라서 해외로 스카우팅 되지도 않는답니다 ㅠㅠ

저는 삼성SDS에 있을 때, 몇 가지 채팅 프로그램을 만들어봤는데, 거기서 가장 많이 사용한 패턴이 Mediator예요.

Mediator는 관계형 델리게이트라는 개념인데, Delegate Pattern의 진화형이라고도 볼 수 있어요. 물론 DataSource와 Target, Delegate가 삼위일체되는 Delegate Pattern과 동일한 구조를 따르지는 않았어요.

패턴은 개념이고 구조체가 되는 형식은 어디까지나 프로그래머의 기발한 자유에 해당해요.

우선 Mediator Pattern의 마인드맵을 보면 이렇게 구성됩니다.



마인드맵은 자바로 되어 있지만, 저는 C++로 구현할거예요.


채팅 시스템은 메시지를 센드하는 클라이언트가 있고 그 클라이언트의 메시지를 다른 타겟들이 리시브하는 구조입니다. 

만약 제가 thomas와 kevin에게 "소풍가자"라는 메시지를 센드했을때, 중간에서 메시지를 받아채서 처리해주는 관계형 델리게이트인 미디에이터가 없다면, thomas와 kevin이 각각 제가 보낸 메시지를 받아서 응답하는 로직을 갖고 있어야 할 거예요. 하지만 메시지에 응답하는 처리를 따로 캡슐화 시켜서 미디에이터가 관리하게 만든다면, 객체지향의 가장 큰 이점인 디바이드의 유연성을 적합하게 살리는 구조가 될 수 있어요.

연산을 수정하거나 제거할 경우에도 각각의 객체들은 신경쓸 필요없이, 미디에이터만 수정하면 되거든요.

서로 관계형으로 연결되어 있는 객체들을 만들기 위해서 User라는 클래스를 정의할께요.




class User
{
protected :
    iChatMediator * _mediator;
    std::string _name;
    
public :
    virtual ~User(){}
    virtual void send(std::string $message) = 0;
    virtual void receive(std::string $message) = 0;
};



헤더 파일 하나에 추상클래스로 정의했어요.
추상적인 요소는 반드시 클래싱해서 객체를 뽑아내야 합니다.
유저 각각을 관계형으로 묶어줄 미디에이터 객체를 멤버 필드로 갖고 있구, 유저를 고유하게 인덱싱할 때 필요하게될 네임도 갖고 있어요. 모두 클래싱에서 얼록하게될 데이터군들이죠.


class ConcreteUser : public User
{
public :
    ConcreteUser(iChatMediator * mediator, std::string name);
    ~ConcreteUser();

    void send(std::string $message);
    void receive(std::string $message);
};






#include "ConcreteUser.h"


ConcreteUser::ConcreteUser(iChatMediator * mediator, std::string name){
    _mediator = mediator;
    _name = name;
}

ConcreteUser::~ConcreteUser(){
    std::cout << "destroyed "<< _name << std::endl;
}

void ConcreteUser::send(std::string $message){
    std::cout << _name << " + send message : ( " << $message <<  " )" << std::endl;
    _mediator -> sendMessage($message, this);
}

void ConcreteUser::receive(std::string $message){
    std::cout << _name << " + receive message : ( " << $message << " )" << std::endl;
}



헤더파일과 클래스파일에 나눠서 구현했구요. 헤더파일에는 클래스의 선언부를 기록하고, 클래스파일에는 클래스의 바디를 구현했어요. 추상적인 User라는 아이를 상속해서 실제로 객체를 만들어내는 ConcreteUser 입니다.

ConcreteUser는 생성자에서 미디에이터와 자신의 이름을 넘겨받아 멤버필드에 등록합니다.
그리고 메시지를 보내는 send와 응답하는 receive를 갖고 있어요.

제가 짜는 프로그램에서 send는 클라이언트단에서 쏴주지만, 여기서 응답을 처리하는 것은 미디에이터가 관리할거예요. send를 쏴준 주체인 클라이언트를 제외한 나머지 아이들이 응답하게 만들거예요.

만약 제가 "소풍가자" 라는 메시지를 날리면, 토마스와 케빈이 저의 메시지를 receive 할 수 있게 만드는거죠.


이젠 User들의 관계형 관리자인 Mediator를 만들어볼께요.
먼저 Mediator 개체의 인터페이스는 이렇게 만들었습니다.

class iChatMediator
{

public :
    virtual ~iChatMediator(){}
    virtual void addUser(User * user) = 0;
    virtual void sendMessage(std::string message, User * user) = 0;
};


User들을 등록할 수 있고(addUser), 메시지를 전송할 객체와 메시지 내용을 담아서 보낼 수 있는 기능인 sendMessage를 제공합니다.

이 인터페이스를 구현할 ChatMediator를 만들겠습니다.

class ChatMediator : public iChatMediator
{
private :
    std::vector<User *> _users;
    void killUsers();

public :
    ChatMediator();
    ~ChatMediator();

    void addUser(User * user);
    void sendMessage(std::string message, User * user);
};


#include "ChatMediator.h"


ChatMediator::ChatMediator(){
    _users.clear();
}

ChatMediator::~ChatMediator(){
    killUsers();
}

void ChatMediator::addUser(User *user){
    _users.push_back(user);
}

void ChatMediator::sendMessage(std::string message, User *user){
    std::vector<User *>::iterator i;
    for(i = _users.begin(); i!=_users.end(); ++i){
        User * object = dynamic_cast<User *>(*i);
        
        if(object != user)
            object -> receive(message);
    }
}

void ChatMediator::killUsers(){
    std::vector<User *>::iterator i;
    for(i = _users.begin(); i!=_users.end(); ++i){
        User * object = dynamic_cast<User *>(*i);
        delete object; object = NULL;
    }
}



특별히 복잡한 메서드는 없습니다. 루프를 돌리는 부분에서 코드가 길어보이는 것은 STL이라는 라이브러리를 사용했기 때문이지 로직 자체가 복잡한것은 아니예요. 디자인 패턴은 개념이기 때문에 가장 쉽고 간단한 로직으로 구성하는 것이 좋습니다.

인터넷을 보면 디자인 패턴 예제라고 해서 사용한 코드들이 한결같이 내부 API를 쓰거나 그렇지 않다고 해도 책을 따라서, 또는 쓸데없이 복잡하게 꾸며서 기술한 포스트들이 많더라구요. 이것은 글쓰는 당사자들도 패턴을 이해하지 못한채, 예제만 실었다는 것이고, 그 예제를 보며 패턴의 개념을 잡아야할 학생들이나 실무자들에게도 별로 도움이 되지 않는 자료입니다.

복잡한 예제와 코드는 패턴의 "개념"을 가리게되고, 결국 패턴을 익히고 생각해야할 자리에서 무의미한 코드들만 눈으로 구경하는 상황이 되고 맙니다. 특히 Mediator Pattern은 더욱 더 그렇죠. 글을 쓰기 위해 참고할 자료들을 서칭하는데.. 한국 블로그나 웹 자료들은 거의 쓸모있는 포스트가 하나도 없었습니다. 제가 앞서 말한대로 의미없게 복잡한 코드 예제들만 나열되어 있더라구요.

그러나 역시 외국웹은 달랐어요!! 훌륭한 샘플 코드가 있길래 참고했습니다 ㅎㅎ


ChatMediator는  iChatMediator 를 구현했어요.
내부적으로 배열을 갖고 있으며, 이 배열에 각각의 User들을 등록해서 관리합니다.
제가 자바로 코드를 짜지 않고, C로 짠 이유는 메모리 관리를 기술하기 위해서예요.

저는 보통 팩토리에서 개체군을 해제하는 방식을 즐겨쓰는데, 여기선 Mediator에서 개체군을 해제하겠습니다. 그 기능을 처리해주는 것이 killUsers예요. 미디에이터가 메모리에서 해제되면 killUser를 호출하고, killUser는 배열에서 참조하고 있는 개체군들의 pointer들을 모두 제거합니다.

클라이언트나 테스터에선 개체군 하나하나의 메모리에는 신경쓸 필요도 없고, 신경써서도 안되죠. 
미디에이터 하나만 죽여주면, 미디에이터에 등록되어 관리되던 개체군들 또한 함께 소멸하니까요.

자바는 이런 메모리 관리를 가비지컬렉터가 모두 수행해주기 때문에 프로그래머가 신경쓰지 않아도 되지만, 그 편리함은 프로그래머 본인에겐 크게 도움이 되지 않습니다. 객체 라이프 사이클에 대한 이해도에서 닷넷이나 자바 프로그래머가 C프로그래머보다 보편적으로 약한 이유가 거기에 있다는 의견이 포럼에서 지배적이죠.



이제 테스트만 남았네요.





// Mediator 객체 생성.
iChatMediator * mediator = new ChatMediator();

// User 개체군 생성.
// 생성하면서 이름을 정해주고, Mediator 객체를 멤버필드로 얻는다.
User * kitty = new ConcreteUser(mediator, "kitty");
User * thomas = new ConcreteUser(mediator, "thomas");
User * kevin = new ConcreteUser(mediator, "kevin");
    
// 미디에이터가 관리해야할 관계형 객체들을 등록.
mediator -> addUser(kitty);
mediator -> addUser(thomas);
mediator -> addUser(kevin);
    
// kitty라는 클라이언트가 "소풍가자"라는 메시지를 보냄
kitty -> send("소풍가자");
    
// 미디에이터를 소멸시킴.
delete mediator;













kitty가 "소풍가자"라는 메시지를 날리자, thomas랑  kevin이 "소풍가자" 라는 메시지를 전달받고 있다는 것이 콘솔에 출력되었어요.





댓글 없음:

댓글 쓰기