발행 / 구독 패턴이란, 두 객체의 상호작용을 대리자를 통해 진행하는 패턴이다.
언리얼 엔진의 Delegate에도 사용되었다고 하며, 대표적인 객체지향 디자인 패턴 중 하나이다.
발행 구독 패턴
잡지사 A가 있다고 해보자.
몇몇 사람들은 잡지사 A의 신간이 발매되면, 해당 신간을 배송받기 위해 구독 서비스를 가입했다.
그렇다면, 잡지사 A는 신간이 발매되면 구독 서비스에 가입한 모든 사람들에게 잡지를 배송해야 한다.
이 과정을 코드로 구현해보자.
class Subscriber
{
public:
void Read(int _Num)
{
std::cout << _Num << "번째 구독자는 신간 잡지를 읽었습니다." << std::endl;
}
};
class Person1 : public Subscriber
{
};
class Person2 : public Subscriber
{
};
class MagazinePub
{
public:
void ReleaseNewMagazine()
{
std::cout << "신간 잡지가 발매되었습니다." << std::endl;
DeliverMagazine();
}
void DeliverMagazine()
{
for (int i = 0; i < Subs.size(); i++)
{
std::cout << i + 1 << "번째 구독자에게 신간 잡지를 배송했습니다." << std::endl;
Subs[i].Read(i + 1);
}
}
void AddSubScriber(Subscriber& _SubScriber)
{
Subs.push_back(_SubScriber);
}
private:
std::vector<Subscriber> Subs;
};
int main()
{
MagazinePub Publisher;
Person1 SubScriber1;
Person2 SubScriber2;
Publisher.AddSubScriber(SubScriber1);
Publisher.AddSubScriber(SubScriber2);
Publisher.ReleaseNewMagazine();
return 0;
}
이런 식으로 구현할 수 있을 것이다.
Publisher는 모든 Subscriber를 자료구조에 저장한 뒤, 신간잡지가 발매되면 모든 SubScriber의 함수를 호출하는 것이다.
하지만, 이 방식은 좋은 방법이 아니다.
Publisher와 Subscriber는 서로 의존성이 생기며, 결합도가 높아진다.
또한, Publisher의 구독을 하려면 반드시 Subscriber 클래스를 상속받아야만 한다.
그렇다면, 이런 방법은 어떨까?
별도의 클래스를 하나 만들어놓은 뒤, 해당 클래스를 통해 Publisher 와 Subscriber가 소통하도록 하는 것이다.
예를 들어, 잡지사와 구독자 사이에 대행업체가 하나 생겼다고 가정해보자.
구독자는 대행업체를 통해, 구독 의사를 알리며 대행업체는 구독자들의 정보를 보관하게 된다.
잡지사는 신간이 발매되면, 대행업체에 신간 발매 소식을 전한 뒤 신간 잡지를 대행업체에 보내준다.
이후, 대행업체는 보유한 정보를 기반으로 모든 구독자들에게 신간 잡지를 뿌려주는 것이다.
이를 더 쉽게 이해하기 위해, 다른 예시를 하나 들어보겠다.
어떤 보스 몹이 소환되어 있다고 해보자. 해당 보스몹은 작은 몬스터 세마리를 소환하였다.
그리고, 보스몹이 사망하였다면 생존해있는 작은 몬스터 세마리도 함께 소멸해야 할 것이다.
이 때, 이를 구현하기 위해 일반적으로 생각할 수 있는 아이디어는 위에서 코드로 적은 것과 동일할 것이다.
작은 몬스터의 포인터를 모두 자료구조에 저장해놓은 뒤, 사망하는 순간 모든 작은 몬스터들의 사망 함수를 호출하는 것이다. 하지만, 위에서 말했듯이 이건 좋은 방법이 아니다.
모든 작은 몬스터의 자료형이 다르다면, 보스 몬스터는 모든 자료형을 알아야 할 것이며 인터페이스를 사용한다면 모든 작은 몬스터들이 이 한 번의 소통을 위해 인터페이스를 추가로 상속받아야 한다.
이를 아래와 같은 방법으로 해결해보자
class Delegate
{
public:
void Bind(std::function<void()> _Func)
{
Funcs.push_back(_Func);
}
void Execute()
{
for (int i = 0; i < Funcs.size(); i++)
{
Funcs[i]();
}
}
private:
std::vector<std::function<void()>> Funcs;
};
먼저, 발행구독 패턴의 핵심인 Delegate를 보자.
내부에는 함수포인터를 저장한 배열이 있다.
그리고, 멤버함수에는 함수포인터를 자료구조에 추가하는 Bind 함수와, 모든 Funcs의 함수포인터를 실행하는 Execute가 있다.
이처럼, 함수포인터를 사용하여 외부에서 함수를 Funcs에 저장하기 때문에 자료형이 수만가지여도 Delegate는 손쉽게 대응할 수 있다.
//Subscriber
class MiniMob1
{
public:
void Die()
{
std::cout << "MiniMob1 이 죽었습니다." << std::endl;
}
};
//Subscriber
class MiniMob2
{
public:
void Die()
{
std::cout << "MiniMob2 가 죽었습니다." << std::endl;
}
};
//Subscriber
class MiniMob3
{
public:
void Die()
{
std::cout << "MiniMob3 이 죽었습니다." << std::endl;
}
};
작은 몬스터 3마리의 클래스를 이렇게 선언했다고 해보자.
각 Die함수들을 Delegate에 Bind해줄것이다.
//Publisher
class Boss
{
public:
void SetDelegate(Delegate* _Delegate)
{
MyDelegate = _Delegate;
}
void Die()
{
std::cout << "Boss가 죽었습니다. " << std::endl;
if (MyDelegate != nullptr)
{
MyDelegate->Execute();
}
}
private:
Delegate* MyDelegate = nullptr;
};
그리고 보스 몬스터는 사망하는 순간 해당 델리게이트의 Execute를 호출함으로써 델리게이트에 Bind된 함수를 모두 실행해줄 것이다.
메인함수를 보자.
int main()
{
Delegate NewDelegate;
Boss NewBoss;
MiniMob1 NewMob1;
MiniMob2 NewMob2;
MiniMob3 NewMob3;
NewBoss.SetDelegate(&NewDelegate);
NewDelegate.Bind(std::bind(&MiniMob1::Die, &NewMob1));
NewDelegate.Bind(std::bind(&MiniMob2::Die, &NewMob2));
NewDelegate.Bind(std::bind(&MiniMob3::Die, &NewMob3));
NewBoss.Die();
return 0;
}
델리게이트를 선언한 뒤, 작은 몹들은 델리게이트에 각 함수를 bind해주고 있다.
그리고 마지막에 보스의 Die가 호출되었다.
결과는 이렇다.
보스몹은 모든 작은 몬스터를 알지 못하고, 직접 함수를 호출해주지도 않았다.
하지만, 보스가 사망하는 순간 모든 작은 몬스터들도 사망한 것을 확인할 수 있다.
이처럼, Delegate 클래스와 같은 대리자를 별도로 두고 해당 클래스를 통해 소통하는 것이 발행 구독 패턴이다.
이로 인해, 보스몹과 작은 몬스터들은 결합도와 의존성이 매우 낮아지며, 객체지향적으로 더 완성도 있게 기능을 설계할 수 있게 된다.
그림으로 보면, 이런 관계가 형성되는 것이다.
이처럼, 중간에 대리자 클래스를 두어 서로 다른 클래스간의 소통을 대신 진행하는 패턴을 구독/발행 패턴이라고 한다.
'C++ > 디자인 패턴' 카테고리의 다른 글
디자인 패턴 - 프록시 패턴 (Proxy Pattern) (0) | 2024.06.25 |
---|---|
디자인 패턴 - 컴포넌트 패턴 (0) | 2024.04.21 |
디자인 패턴 - 팩토리 메서드 패턴, 추상 팩토리 패턴 (0) | 2024.04.02 |
디자인 패턴 - 싱글 톤 패턴 (1) | 2024.04.02 |