본문 바로가기
개발/Architecture & Design Pattern

C++ - 옵저버 패턴 (Observer Pattern)

by 피로물든딸기 2024. 1. 30.
반응형

C, C++ 전체 링크
Architecture & Design Pattern 전체 링크
 
옵저버 패턴 (Observer  Pattern) - 행동 패턴
- 객체 간의 일대다 종속성을 정의하는 패턴
- 객체의 상태가 변경될 때, 해당 객체에 종속된 다른 객체들이 자동으로 알림을 받아 갱신하도록 한다.
 
구현
- 주제(Subject) 객체는 옵저버들을 관리하고 상태의 변경을 감지하고, 옵저버들에게 알림을 보낸다.
- 옵저버(Observer) 객체는 주제의 상태 변경을 감지하고 처리한다.
- 주제와 옵저버 클래스를 추상화하여 각각의 기본 동작을 정의하고 구체적인 클래스를 구현한다.
 
장점
- OCP(개방-폐쇄 원칙), 기존 코드를 변경하지 않고 새로운 주제나 옵저버를 추가할 수 있다.
- 주제와 옵저버의 관계가 느슨해서 주제가 변경되어도 옵저버에 영향을 주지 않는다. (느슨한 결합, Loose Coupling)
- 런타임에 새로운 옵저버를 추가하거나 기존 옵저버를 제거할 수 있다. (확장성, Scalability)
- 주제와 옵저버는 독립적인 모듈로 구성된다. (Modularity)
- 주제는 옵저버가 특정 인터페이스를 구현하는 것만 알고, 옵저버를 추가한다고 해서 주제를 변경할 필요가 없다.
 
단점
- 주제 객체의 상태가 변경될 때마다 모든 옵저버에게 알림을 보내야 해서 처리 속도가 떨어질 수 있다.
- 주제와 옵저버 간의 순환 참조가 발생할 수 있다.


책을 판매하는 서점
 
서점에는 오프라인 서점, 온라인 서점(알라딘, Yes24 등)이 있다.
각 서점은 자율적으로 책을 원하는 가격에 판매할 수 있다.
여기서는 편의상 책을 한 권만 판다고 가정하자.
 
책 정보를 가지고 있는 BookData 클래스는 다음과 같다.

class BookData
{
public:
	BookData() = default;
	string getName() { return name; }
	int getPrice() { return price; }

	void setData(const string& newName, int newPrice)
	{
		name = newName;
		price = newPrice;
	}

private:
	string name;
	int price;
};

 
오프라인 서점은 원래 책의 가격(BookData - price)대로 판매하려고 한다.
알라딘은 10% 할인된 가격에 판매하고,
Yes24는 1000원 할인된 가격에 판매하려고 한다.
 
각 서점은 Store 를 상속받아 showPrice에서 원하는 가격을 보여주도록 하자.

class Store
{
public:
	virtual void showPrice() = 0;
	void update(BookData *data)
	{
		name = data->getName();
		price = data->getPrice();
	}
protected:
	string name;
	int price;
};

 
예를 들어 Yes24의 경우 아래와 같이 클래스를 만들 수 있다. (price - 1000)

class Yes24 : public Store
{
public:
	void showPrice()
	{
		cout << "Yes24" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price - 1000 << endl << endl;
	}
};

 
2만원짜리 해리포터 책을 각 서점에서 얼마에 판매하는지 다음과 같이 확인할 수 있다.

int main(void)
{
	BookData* data = new BookData();
	data->setData("harry-potter", 20000);

	BookStore* bookStore = new BookStore();
	Aladin* aladin = new Aladin();
	Yes24* yes24 = new Yes24();

	bookStore->update(data);
	aladin->update(data);
	yes24->update(data);

	bookStore->showPrice();
	aladin->showPrice();
	yes24->showPrice();

	return 0;
}

 
그런데 책의 가격이 3만원으로 변경되었다.
BookData의 setData를 이용해 가격을 변경하였지만,
각 서점은 data를 받아 update 하는 코드가 있어야 변경된 데이터에 대해 알 수 있다.

data.setData("harry-potter", 30000);

bookStore->update(&data);
aladin->update(&data);
yes24->update(&data);

bookStore->showPrice();
aladin->showPrice();
yes24->showPrice();

 
setData 이후 update를 하지 않으려면 BookData 클래스가 setData 이후 change 메서드를 호출하면 된다.
하지만 BookData가 각 서점의 클래스를 알아야하고, 새로운 서점이 추가되면 change 함수가 변경되어야 한다.

class BookData
{
public:
	...
    
    void setData(const string& newName, int newPrice)
	{
		...
		change();
	}
    
	void change()
	{
		...

		bookStore->update(&data);
		aladin->update(&data);
		yes24->update(&data);

		...
	}

};

 
전체 코드는 다음과 같다.

#include <iostream>
#include <string>

using namespace std;

class BookData
{
public:
	BookData() = default;
	string getName() { return name; }
	int getPrice() { return price; }

	void setData(const string& newName, int newPrice)
	{
		name = newName;
		price = newPrice;
	}

private:
	string name;
	int price;
};

/* ----------------------------------------------------------------------------------- */

class Store
{
public:
	virtual void showPrice() = 0;
	void update(BookData *data)
	{
		name = data->getName();
		price = data->getPrice();
	}
protected:
	string name;
	int price;
};

class BookStore : public Store
{
public:
	void showPrice()
	{
		cout << "BookStore" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price << endl << endl;
	}
};

class Aladin : public Store
{
public:
	void showPrice()
	{
		cout << "Aladin" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price * 0.9 << endl << endl;
	}
};

class Yes24 : public Store
{
public:
	void showPrice()
	{
		cout << "Yes24" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price - 1000 << endl << endl;
	}
};

int main(void)
{
	BookData* data = new BookData();
	data->setData("harry-potter", 20000);

	BookStore* bookStore = new BookStore();
	Aladin* aladin = new Aladin();
	Yes24* yes24 = new Yes24();

	bookStore->update(data);
	aladin->update(data);
	yes24->update(data);

	bookStore->showPrice();
	aladin->showPrice();
	yes24->showPrice();

	cout << "update data" << endl << endl;

	data->setData("harry-potter", 30000);

	bookStore->update(data);
	aladin->update(data);
	yes24->update(data);

	bookStore->showPrice();
	aladin->showPrice();
	yes24->showPrice();

	return 0;
}

옵저버 패턴 (Observer Pattern)
 
옵저버는 상태 변경을 감지하고 처리하는 역할을 한다.
따라서 옵저버 인터페이스에는 update 메서드가 있으며, 각 옵저버는 update를 구현해야 한다.

class Observer {
public:
	virtual ~Observer() = default;
	virtual void update(string newName, int newPrice) = 0;
};

 
Subject 인터페이스는 옵저버를 관리하기 위한 메서드가 필요하다. (구독, 구독 해지, 알림)

class Subject
{
public:
	Subject() = default;
	virtual ~Subject() = default;
	virtual void subscribe(Observer *o) = 0;
	virtual void unsubscribe(Observer *o) = 0;
	virtual void notify() = 0;
};

 
책의 이름과 가격 정보를 가지고 있는 BookDataSubject 인터페이스를 상속받아 구현한다.
책의 정보가 setData에 의해 갱신되면 notify에서 모든 옵저버에게 알림을 보낸다.

class BookData : public Subject
{
public:
	BookData() = default;
	void subscribe(Observer *o) override { .. }
	void unsubscribe(Observer *o) override { ... }	
	void notify() override {...}

	void setData(const string& newName, int newPrice)
	{
		name = newName;
		price = newPrice;
		notify();
	}

private:
	string name;
	int price;
	list<Observer *> observers = {};
};

 
각 서점은 Observer를 상속 받아 update를 구현한다.
여기서는 update에서 showPrice에서 판매 가격을 보여주도록 한다.
서점이 생성될 때, BookData를 구독(subscribe)한다.

class Yes24 : public Observer
{
public:
	Yes24() = default;
	Yes24(Subject* bd) : boodData(bd) { boodData->subscribe(this); }

	void update(string newName, int newPrice) override
	{
		name = newName;
		price = newPrice;
		showPrice();
	}

	void showPrice()
	{
		cout << "Yes24" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price - 1000 << endl << endl;
	}

private:
	string name;
	int price;
	Subject* boodData = nullptr;
};

 
각 서점을 생성할 때, BookData를 구독하도록 한다.
그리고 data가 갱신되면 구독하고 있는 모든 서점이 알림을 받아 data를 update한다.
또한 런타임 subscribeunsubscribe를 이용해서 서점을 구독 / 해지 할 수 있다.

int main(void)
{
	BookData* bookData = new BookData();

	BookStore* bookStore = new BookStore(bookData);
	Aladin* aladin = new Aladin(bookData);
	Yes24* yes24 = new Yes24(bookData);

	bookData->setData("harry-potter", 20000);

	cout << "unsubscribed yes24" << endl;
	bookData->unsubscribe(yes24);

	cout << "update data" << endl << endl;
	bookData->setData("harry-potter", 30000);

	return 0;
}

 
클래스 다이어그램으로 그려보면 다음과 같다.

 
전체 코드는 다음과 같다.

#include <iostream>
#include <string>
#include <list>

using namespace std;

class Observer {
public:
	virtual ~Observer() = default;
	virtual void update(string newName, int newPrice) = 0;
};

class Subject 
{
public:
	Subject() = default;
	virtual ~Subject() = default;
	virtual void subscribe(Observer *o) = 0;
	virtual void unsubscribe(Observer *o) = 0;
	virtual void notify() = 0;
};

class BookData : public Subject
{
public:
	BookData() = default;
	void subscribe(Observer *o) override { observers.push_back(o); }

	void unsubscribe(Observer *o) override
	{
		if (!observers.empty())
		{
			delete o;
			observers.remove(o); 
		}
	}

	void notify() override 
	{
		for (auto o : observers) 
			o->update(name, price);
	}

	void setData(const string& newName, int newPrice)
	{
		name = newName; 
		price = newPrice;
		notify();
	}

private:
	string name;
	int price;
	list<Observer *> observers = {};
};

/* ----------------------------------------------------------------------------------- */

class BookStore : public Observer
{
public:
	BookStore() = default;
	BookStore(Subject* bd) : boodData(bd) { boodData->subscribe(this); }

	void update(string newName, int newPrice) override 
	{ 
		name = newName; 
		price = newPrice; 
		showPrice();
	}

	void showPrice() 
	{
		cout << "BookStore" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price << endl << endl;
	}

private:
	string name;
	int price;
	Subject* boodData = nullptr;
};

class Aladin : public Observer
{
public:
	Aladin() = default;
	Aladin(Subject* bd) : boodData(bd) { boodData->subscribe(this); }

	void update(string newName, int newPrice) override
	{
		name = newName;
		price = newPrice;
		showPrice();
	}

	void showPrice()
	{
		cout << "Aladin" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price * 0.9 << endl << endl;
	}

private:
	string name;
	int price;
	Subject* boodData = nullptr;
};

class Yes24 : public Observer
{
public:
	Yes24() = default;
	Yes24(Subject* bd) : boodData(bd) { boodData->subscribe(this); }

	void update(string newName, int newPrice) override
	{
		name = newName;
		price = newPrice;
		showPrice();
	}

	void showPrice()
	{
		cout << "Yes24" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price - 1000 << endl << endl;
	}

private:
	string name;
	int price;
	Subject* boodData = nullptr;
};

int main(void)
{
	BookData* bookData = new BookData();

	BookStore* bookStore = new BookStore(bookData);
	Aladin* aladin = new Aladin(bookData);
	Yes24* yes24 = new Yes24(bookData);

	bookData->setData("harry-potter", 20000);

	cout << "unsubscribed yes24" << endl;
	bookData->unsubscribe(yes24);

	cout << "update data" << endl << endl;
	bookData->setData("harry-potter", 30000);

	return 0;
}

 
구독 해지된 yes24를 제외하고 다른 서점의 책 정보가 잘 반영된 것을 알 수 있다.


notify 최적화
 
모든 서점은 BookData가 변경될 때마다 매번 알림을 받는다.
여기서 변경된 데이터가 특정 조건을 만족할 때만 알림을 보낼 수 있다.
 
만약에 책의 가격이 기존보다 떨어지는 경우라면 서점은 책 가격을 변경하지 않는다고 가정하자.
 
Subject 클래스상태 관리를 위한 변수 메서드를 제공해야 한다.

class Subject
{
public:
	...
protected:
	void setChanged(bool value) { changed = value; }
	bool getChanged() { return changed; }
private:
	list<Observer *> observers;
	bool changed = false;
};

 
새로운 가격이 더 큰 경우만 changedtrue로 변경하고, notifychangedtrue인 경우만 실행하도록 구현한다.

	void notify() override
	{
		if (getChanged() == false) return;

		for (auto o : observers)
			o->update(name, price);

		setChanged(false);
	}

	void setData(const string& newName, int newPrice)
	{
		if (newPrice > price) setChanged(true);

		name = newName;
		price = newPrice;
		notify();
	}

 
해리포터 책의 가격이 20,000 → 10,000이 된 경우에는 알림이 발생하지 않는 것을 알 수 있다.

int main(void)
{
	BookData bookData = BookData();

	BookStore* bookStore = new BookStore(&bookData);
	Aladin* aladin = new Aladin(&bookData);
	Yes24* yes24 = new Yes24(&bookData);

	bookData.setData("harry-potter", 20000);

	cout << "unsubscribed yes24" << endl;
	bookData.unsubscribe(yes24);

	cout << "update data" << endl << endl;
	bookData.setData("harry-potter", 10000);

	return 0;
}

 
전체 코드는 다음과 같다.

#include <iostream>
#include <string>
#include <list>

using namespace std;

class Observer {
public:
	virtual ~Observer() = default;
	virtual void update(string newName, int newPrice) = 0;
};

class Subject
{
public:
	Subject() = default;
	virtual ~Subject() = default;
	virtual void subscribe(Observer *o) = 0;
	virtual void unsubscribe(Observer *o) = 0;
	virtual void notify() = 0;
protected:
	void setChanged(bool value) { changed = value; }
	bool getChanged() { return changed; }
private:
	list<Observer *> observers;
	bool changed = false;
};

class BookData : public Subject
{
public:
	BookData() = default;
	void subscribe(Observer *o) override { observers.push_back(o); }

	void unsubscribe(Observer *o) override
	{
		if (!observers.empty())
		{
			delete o;
			observers.remove(o);
		}
	}

	void notify() override
	{
		if (getChanged() == false) return;

		for (auto o : observers)
			o->update(name, price);

		setChanged(false);
	}

	void setData(const string& newName, int newPrice)
	{
		if (newPrice > price) setChanged(true);

		name = newName;
		price = newPrice;
		notify();
	}

private:
	string name;
	int price;
	list<Observer *> observers = {};
};

/* ----------------------------------------------------------------------------------- */

class BookStore : public Observer
{
public:
	BookStore() = default;
	BookStore(Subject* bd) : boodData(bd) { boodData->subscribe(this); }

	void update(string newName, int newPrice) override
	{
		name = newName;
		price = newPrice;
		showPrice();
	}

	void showPrice()
	{
		cout << "BookStore" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price << endl << endl;
	}

private:
	string name;
	int price;
	Subject* boodData = nullptr;
};

class Aladin : public Observer
{
public:
	Aladin() = default;
	Aladin(Subject* bd) : boodData(bd) { boodData->subscribe(this); }

	void update(string newName, int newPrice) override
	{
		name = newName;
		price = newPrice;
		showPrice();
	}

	void showPrice()
	{
		cout << "Aladin" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price * 0.9 << endl << endl;
	}

private:
	string name;
	int price;
	Subject* boodData = nullptr;
};

class Yes24 : public Observer
{
public:
	Yes24() = default;
	Yes24(Subject* bd) : boodData(bd) { boodData->subscribe(this); }

	void update(string newName, int newPrice) override
	{
		name = newName;
		price = newPrice;
		showPrice();
	}

	void showPrice()
	{
		cout << "Yes24" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price - 1000 << endl << endl;
	}

private:
	string name;
	int price;
	Subject* boodData = nullptr;
};

int main(void)
{
	BookData* bookData = new BookData();

	BookStore* bookStore = new BookStore(bookData);
	Aladin* aladin = new Aladin(bookData);
	Yes24* yes24 = new Yes24(bookData);

	bookData->setData("harry-potter", 20000);

	cout << "update data" << endl << endl;
	bookData->setData("harry-potter", 10000);

	return 0;
}
반응형

댓글