중재자 패턴(Mediator Pattern)

  • 객체지향의 특징은 **단일 책임 원칙(SRP)**에 의해 하나의 객체는 하나의 책임(행동)을 갖습니다.
  • 또한 클래스 재사용을 극대화하기 위해 작은 클래스를 많이 만들게 됩니다.
  • 객체지향 프로그램은 다수의 작은 클래스간의 결합을 통해 하나의 행동을 합니다. 즉 작은 행동들이 모여 큰 하나의 행동을 만듭니다.
  • 하지만 다수의 클래스가 다수의 클래스와의 관계가 이어지면 복잡한 결합 관계로 성장합니다.
  • 중재자 패턴은 다수의 클래스 간에 설정된 복잡한 관계를 느슨한 관계로 개선하기 위한 패턴입니다.
  • 한 모임에는 총무 역할의 단일 사람이 있고, 한 반에는 단일 사람의 반장이 있는 것과 비슷합니다.
    • 총무는 다수의 모임 구성원들과 다수의 가게들과의 관계를 중재하는 중재자입니다.
    • 반장은 다수의 학급 구성원들과 다수의 선생님들과의 관계를 중재하는 중재자입니다.

중재자 패턴 구조

  • 다수의 클래스 A는 다수의 클래스 B와의 관계를 설정합니다. 이를 M : N 관계라고 합니다.
  • 중재자는 A와 B 사이에 개입하여 1 : 1 또는 1 : M 관계로 느슨하게 조정합니다.
  • 중재자 패턴은 하나의 중재자와 다수의 동료 객체로 구성되어 있습니다.
    • Mediator
    • ConcreteMediator
    • Colleague
    • ConcreteColleague
  • 중재자 패턴 예제로 많이 사용하는 채팅 프로그램을 작성해보겠습니다.

중재자 패턴: 중재자(Mediator) 인터페이스 클래스

  • 중재자 패턴은 관찰자 패턴(Observer Pattern)과 유사한 부분이 있습니다.
  • 중재자 패턴 또한 동료 객체를 추가, 삭제하는 메서드를 갖고 있습니다.
  • 또한 채팅 프로그램의 기능으로 다른 객체들에게 메세지를 다시 전달할 sendMessage() 메서드가 있습니다.
  • 모두 가상 함수로 구성하여 인터페이스 클래스를 선언합니다.
// Mediator
class Mediator
{
public:
    virtual void addUser(std::shared_ptr<Colleague> colleague) = 0;
    virtual void deleteUser(std::shared_ptr<Colleague> colleague) = 0;
    virtual void sendMessage(std::string message, std::shared_ptr<Colleague> sender) = 0;
};

중재자 패턴: 동료(Colleague) 인터페이스 클래스

  • Mediator 클래스를 구현하기 전 동료 객체의 인터페이스 클래스인 Colleague 클래스를 선언합니다.
  • 메세지를 전달하고, 수신하는 기능들로 구성되어 있으며 어려운 내용은 없습니다.
  • 다만 Mediator 클래스와 Colleague 클래스는 상호 참조 관계로 구성되어 있습니다.
// Colleague
class Colleague
{
public:
    Colleague(std::shared_ptr<Mediator> mediator, std::string name)
    {
        m_mediator = mediator;
        m_name = name;
    }

    virtual void sendMessage(std::string message) = 0;
    virtual void recvMessage(std::string message) = 0;

protected:
    std::shared_ptr<Mediator> m_mediator;
    std::string m_name;
};

중재자 패턴: 중재자 클래스 구현

  • 관찰자 패턴에서 사용한 구조와 매우 유사합니다.
  • 동료 객체를 내부 컨테이너에 추가&삭제하는 기능을 구현합니다.
// ConcreteMediator
class chatMediator :
    public Mediator
{
public:
    void addUser(std::shared_ptr<Colleague> colleague) override
    {
        m_colleagues.push_back(colleague);
    }

    void deleteUser(std::shared_ptr<Colleague> colleague) override
    {
        for (auto iter = m_colleagues.begin(); iter != m_colleagues.end(); iter++)
        {
            if (*iter == colleague)
            {
                m_colleagues.erase(iter);
                break;
            }
        }
    }

    void sendMessage(std::string message, std::shared_ptr<Colleague> sender) override
    {
        for (const auto& colleague : m_colleagues)
        {
            if (colleague != sender)
            {
                colleague->recvMessage(message);
            }
        }
    }

private:
    std::vector<std::shared_ptr<Colleague>> m_colleagues;
};

중재자 패턴: 동료 클래스 구현

  • 상호 참조 관계로 인해 C 또는 C++에서 많이 사용하던 this 포인터가 필요합니다.
  • Morden C++에서는 스마트 포인터를 사용하기 때문에 shared_ptr<T>this 포인터로 변환하기 위한 STL 클래스를 상속합니다.
  • shared_from_this()을 사용하면 this 포인터를 shared_ptr<T> 타입으로 전달할 수 있습니다.
    • 그치만 다중 상속을 하는 경우에는 복잡한 생성자의 관계로 인해 버그가 발생할 수 있습니다.
    • shared_from_this()는 생성자에서 호출하는 경우 this 포인터가 만들어지지 않기 때문에 Null-Pointer Exception이 발생합니다.
    • 이를 해결하는 방법으로 lambda 함수를 사용하여 캐스팅하는 방법이 있습니다.
// ConcreteColleague
class User :
    public Colleague,
    public std::enable_shared_from_this<User>
{
public:
    User(std::shared_ptr<Mediator> mediator, std::string name) :
        Colleague(mediator, name)
    {

    }

    void sendMessage(std::string message)
    {
        std::cout << m_name + " [send] : " << message << std::endl;
        m_mediator->sendMessage(message, shared_from_this());
    }

    void recvMessage(std::string message)
    {
        std::cout << m_name + " [recv] : " << message << std::endl;
    }
};

중재자 패턴: 실행 코드

  • 중재자 패턴의 사용 방법을 알아봅니다.
int main(const int argc, const char* argv[])
{
    std::shared_ptr<chatMediator> chat = std::make_shared<chatMediator>();

    std::shared_ptr<User> kim = std::make_shared<User>(chat, "kil");
    std::shared_ptr<User> lee = std::make_shared<User>(chat, "lee");
    std::shared_ptr<User> park = std::make_shared<User>(chat, "park");

    chat->addUser(kim);
    chat->addUser(lee);
    chat->addUser(park);

    kim->sendMessage("Hello, I'm Kim");

    return 0;
}
  • 실행 코드
kil [send] : Hello, I'm Kim
lee [recv] : Hello, I'm Kim
park [recv] : Hello, I'm Kim
  • 통신을 위한 높은 레벨의 인터페이스를 제공한다는 점에서 중재자 패턴과 파사드 패턴은 비슷합니다.
  • 하지만 중재자 패턴은 양방향 통신이 가능하고, 파사드 패턴은 단방향 통신만 가능하다는 점에서 차이가 있습니다.