본문 바로가기

study/design pattern

[디자인패턴][행위패턴] 매개자 Meditator - C++

728x90

[모던 C++ 디자인 패턴] 책을 바탕으로 공부하는 내용을 정리한 내용이다.


Meditator Pattern

우리가 작성하는 코드의 상당 부분은 서로 다른 컴포넌트(클래스) 간 포인터나 직접적 참조를 통해 커뮤니케이션한다. 어떤 경우 컴포넌트 간 명시적으로 상대방 객체의 존재를 알아야 하는 상황이 불편할 수 있다. 또는 상대방 객체를 알더라도 객체 생성/소멸 시점에 대한 관리 때문에 포인터나 참조로 접근되는 것이 싫을 수 있다.

매개자는 컴포넌트 간 커뮤니케이션을 돕기 위한 메커니즘이다. 매개자 자체는 커뮤니케이션에 동반되는 모든 컴포넌트로부터 접근 가능해야 한다. 즉, 전역 정적 변수이거나 모든 컴포넌트에 참조가 노출되어야 한다.


채팅 룸

채팅 룸은 매개자 디자인 패턴이 적용될 수 있는 가장 전형적인 예다. 매개자 패턴의 설명에 들어가기 전에 단순한 채팅룸이 먼저 필요하다.

// 참여자
struct Person {
     Person(const string &name);
     
     // 메시지 수신
     void receive(const string &origin, const string &message);
     // 채팅 룸의 모든 참여자에게 메시지 송신
     void say(const string &message) const;
     // 개인 메시지(Private Message) 기능으로, 특정 참여자만 지정해 메시지 송신
     void pm(const string &who, const string &message) const;
     
     string name;
     ChatRoom *room = nullptr;
     vector<string> chat_log;
};

// 채팅룸
struct ChatRoom {
     // 채팅 룸에 사용자 입장
     void join(Person *p);
     // 채팅 룸의 모든 참여자에게 메시지 송신
     void broadcast(const string &origin, const string &message);
     // 개인 메시지 송신
     void message(const string &origin, const string &who, const string &message);
     
     // 추가만 된다고 가정
     vector<Person *> people;
};

포인터, 참조, shared_ptr 등 어떤 방식으로 객체를 접근할지는 사람에 따라 다르다. vector를 사용할 경우 참조를 사용할 수 없다는 제약만 인지하면 된다. 여기서는 포인터를 사용한다.

void ChatRoom::join(Person *p) {
     string join_msg = p->name + " joins the chat";
     broadcast("room", join_msg);
     p->room = this;
     people.push_back(p);
}

void ChatRoom::broadcast(const string &origin, const string &message) {
     for ( auto p : people ) {
          // 자기자신 제외 메시지 전송
          if ( p->name != origin ) {
               p->receive(origin, message);
          }
     }
}

void ChatRoom::message(const string &origin, const string &who, const string &message) {
     auto target = find_if(begin(people), end(people), 
          [&](const Person *p) {
               return p->name == who;
          });
     if ( target != end(people) ) {
          (*target)->receive(origin, message);
     }
}

람다 참고

위에서 채팅 룸 API를 준비했기 때문에 Person을 아래와 같이 구현할 수 있다.

void Person::say(const string &message) const {
    room->broadcast(name, message);
}

void Person::pm(const string &who, const string &message) const {
    room->message(name, who, message);
}

void Person::receive(const string &origin, const string &message) {
    string s(origin + ": \"" + message + "\"");
    cout << "[" << name << "'s chat session] " << s << "\n";
    chat_log.emplace_back(s);
}

 

 

이제 여기서 현재의 채팅 룸 세션에서 메시지가 언제 어디서 왔는지 기록하는 기능을 만들게 될 것이다. (책에서는 구현을 안 한다.)


매개자와 이벤트

채팅 룸 예제에서는 일관된 테마가 있다. 누군가 메시지를 올리면 참여자들은 알림을 받아야 한다. 어떤 이벤트를 가진 매개자라는 개념은 모든 참여자에게 해당된다. 참여자는 이벤트의 수신처로 등록할 수 있고, 알림을 발생시킬 수도 있다.

C++ 언어에서는 이벤트 기능을 제고하지 않는다. 따라서 Boost.Signals2 라이브러리를 사용한다. 이벤트 활용에 필수적 기능들을 제공한다.

 

단순한 예인 축구 게임으로 살펴보자. 축구에서 선수가 득점하면 코치는 칭찬을 한다고 해보자. 이때 누가 골을 넣었고, 몇 골이나 넣었는지에 대한 정보가 전달되어야 한다. 이런 정보를 전달하기 위해 이벤트 데이터를 일반화한 베이스 클래스를 아래와 같이 정의할 수 있다.

struct EventData {
    virtual ~Eventata() = default;
    virtual void print() const = 0;
};

struct PlayerScoredData : EventData {
    PlayerScoredData(const string &player_name, const int goals_scored_so_far) :
        player_name(player_name), goals_scored_so_far(goals_scored_so_far) {}
        
    void print() const override {
        cout << player_name << " has scored! (their " << goals_scored_so_far << " goal)" << "\n";
    }
    
    string player_name;
    int goals_scored_so_far;
};

여기에 매개자를 추가해 보자. 이번에는 아무런 동작이 없다. 이벤트 기반 구조에서는 매개자가 직접 일을 수행하지 않는다.

struct Game {
    signal<void(EventData *)> events; // 관찰자
};

극단적인 방법이긴 하지만, 이벤트를 전역 변수로 이벤트를 Game 클래스를 두지 않는 방법도 있다. 명시적으로 Game 객체의 참조가 컴포넌트에 주입되는 코드가 있으면 이벤트와의 종속 관계가 분명하게 드러나는 장점이 있다.

 

이제 Player 클래스를 만들 수 잇고, 매개자인 Game의 참조를 갖는다.

struct Player {
    Player(const string &name, Game &game) : name(name), game(game) {}
    
    void score() {
        goals_scored++;
        // 이벤트를 이용해 PlayerScoredData 생성
        PlayerScoredData ps(name, goals_scored);
        // 수신처로 등록된 객체들에 알림 전송
        game.evnets(&ps);
    }
    
    string name;
    int goals_scored = 0;
    Game &game;
};

struct Coach {
    explicit Coach(Game &game) : game(game) {
        // game.events에 수신 등록
        game.events.connect([](EventData *e) {
            PlayerScoredData *ps = dynamic_cast<PlayerScoredData *>(e);
            if ( ps && ps->goals_scored_so_far < 3 ) {
                cout << "coach says: well done, " << ps->player_name << "\n";
            }
        });
    }
    
    Game &game;
};

Coach 클래스의 람다 함수 인자 타입이 EventData* 임을 유의하자. 골 득점 이벤트에 대해서만 처리하고 싶지만 어떤 이벤트가 올지 알 수 없다. 따라서 dynamic_cast로 목적하는 골 득점 이벤트 타입이 수신되었는지 확인한다. 다른 방법으로 확인해도 가능하다.

마법 같은 부분들은 설정 단계에서 일어난다. 각 이벤트마다 명시적으로 슬롯을 모두 나열할 필요는 없다. 처리하고 싶은 이벤트에 대해서만 처리해도 된다.


요약

매개자 디자인 패턴은 시스템 내 컴포넌트 모두가 참조할 수 있는 어떤 중간자를 컴포넌트 간에 직접적으로 참조하지 않더라도 커뮤니케이션할 수 있게 한다. 매개자를 통해 직접적인 메모리 접근 대신 식별자로 커뮤니케이션할 수 있게 한다.

가장 단순한 구현 형태는 멤버 변수로 리스트를 두고 리스트를 검사해 필요한 항목만 선택적으로 처리하는 함수다.

정교한 구현에서는 발생하는 이벤트들에 대해 수신하길 원하는 객체가 개별적으로 수신 등록할 수 있게 하는 거시다.

728x90