옵저버 패턴이란

 

객체의 상태가 변경될 때 해당 객체에 종속된 곳들에 알림을 보내기 위해 해당 객체에 옵저버들의 목록을 등록하고 객체에 상태 변화가 있을 때마다 객체가 옵저버들에게 통지하게 만드는 패턴

 

쉽게 말하면 객체의 상태가 변하면 본인에게 등록된 관찰자들에게 변했다고 알리는 패턴

 

어떤 클래스든 Observer를 상속받아 인터페이스를 구현하면 관찰자가 될 수 있다.

관찰 당하는 객체(subject)가 알림 메서드를 호출한다

 

 

 

옵저버 패턴의 구조

 

 

1) Subject

- 상태가 변경될 객체

- 상태가 변경되면 구독한 옵저버들에게 알림을 보냄(실제 로직은 옵저버의 업데이트를 호출함)

 

2) Observer

- 처음 생성될때 Subject가 본인을 구독하게 만듬

- Subject로부터 알림이 오면 상태를 업데이트 함

- Subject가 변경되면 그쪽에서 옵저버의 update()함수를 호출하므로 그에 따라 업데이트됨

 

 

 

옵저버 패턴을 사용하지 않는다면

/*
Observer패턴을 쓰지 않는다면
*/

#include <iostream>
using namespace std;

/*
책 데이터 클래스
*/
class BookData
{
private:
	string name;
	int price;

public:
	BookData() = default;

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

/*
서점 추상 클래스
*/
class Store
{
protected:
	string name;
	int price;

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

/*
BookStore 오프라인 서점 Concrete 클래스
원가대로 판매
*/
class BookStore : public Store
{
public:
	void showPrice() {
		cout << "BookStore" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price << endl << endl;
	}
};

/*
Aladin 서점 Concrete 클래스
원가에서 10% 할인해서 판매
*/
class Aladin : public Store
{
public:
	void showPrice() {
		cout << "Aladin" << endl;
		cout << "Name : " << name << endl;
		cout << "Price : " << price * 0.9 << endl << endl;
	}
};

/*
Yes24 서점 Concrete 클래스
원가에서 1000원 할인해서 판매
*/
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();

	// 해리포터가 20000원일때
	cout << "해리포터의 원가 20000원일때의 가격\n\n";
	bookStore->update(data);
	aladin->update(data);
	yes24->update(data);

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

	cout << "------------------------------------------\n";
	cout << "해리포터의 원가 30000원으로 변경\n\n";
	
	// 가격이 변경될 시 옵저버 패턴이 없다면
	data->setData("harry-potter", 30000); 
	// 가격은 변경했지만 각 서점들이 알려면 각 서점들의 정보를 업데이트 해야함
	bookStore->update(data);
	aladin->update(data);
	yes24->update(data);

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

	return 0;
}

 

만약 data->setData와 각 서점의 update함수들을 하나로 묶는다면

아래처럼 setData를 한 후 모든 서점들의 update를 호출하는식으로 사용 가능하다.

class BookData
{
public:
	...
    void setData(const string& newName, int newPrice) {
		...
		change();
	}
    
	void change() {
		...
		bookStore->update(&data);
		aladin->update(&data);
		yes24->update(&data);
		...
	}
};

하지만 이렇게 되면 서점이 새로 추가되거나 제거될때마다 BookData클래스를 변경해줘야 하고

각 서점들과 BookData에 커플링이 생기고 순환참조도 생기므로 좋지 않다.

 

 

 

옵저버 패턴을 사용한다면

 

주의해서 볼 점은 Subject가 구독한 Observer들의 목록을 멤버로 갖지만

정작 구독하는 함수는 Observer측에서 호출함

새로운 서점이 생겼으니 책 가격이 변경되면 저한테 알려주세요 라고 요청하는 대기줄에 서는 것처럼 생각하면 됨

 

또한 Subject가 변경되면 nofity()를 호출하는데 그 안에 각 옵저버들의 update함수가 들어있는 셈이므로 정작 update는 또 Subject측에서 호출하게 됨

 

그러니 결국 Subject와 Observer가 전혀 관계 없는 사이가 아닌 순환참조가 발생하게 됨

#include <iostream>
#include <list>
using namespace std;

/*
Observer 추상 클래스
상태변경을 감지하고 처리하는 역할
*/
class Observer {
public:
	virtual ~Observer() = default;
	virtual void update(string newName, int newPrice) = 0;
};

/*
Concrete Observer 클래스
여기서는 각 서점에 해당하며 
각 서점이 책 정보를 확인하는 관찰자이므로 서점이 Observer를 상속받는다.
*/
class Yes24 : public Observer
{
private:
	string name;
	int price;
	Subject* bookData = nullptr;

public:
	Yes24() = default;
	Yes24(Subject* bd) : bookData(bd) { bookData->subscribe(this); }
	// 서점이 새로 생길 때 bookData를 구독함

	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;
	}
};


/*
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;
};

/*
Concrete Subject 클래스
책의 정보를 담은 BookData클래스이며
책의 정보가 변경될시 setData 마지막에 notify()를 해준다.
notify는 observers List에 등록된 옵저버들에게 알림을 보낸다.
*/
class BookData : public Subject
{
private:
	string name;
	int price;
	list<Observer*> observers;
	/*
	Vector등으로 구현해도 괜찮지만 
	List로 구현한 이유는 구독 취소를 할 경우 중간위치에서 삭제되는 경우가 많이 생기므로
	*/

public:
	/* 구독 및 구독취소가 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);

		// 만약 책 가격이 올랐을때만 notify하고 떨어졌을땐 notify하지 않는다면
		// 여기서 if문걸고 알림 안보내면 되는 것
	}

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

 

 

 

장단점

 

1) 장점

- OCP 준수 : 개방폐쇄는 준수하는게 맞는데 커플링이 좀 있긴 해서 단일책임원칙은 준수하지 않는게 아닌가 싶음

- 느슨한 결합 : 그래도 결합이 느슨해서 옵저버를 새로 만든다고 해도 Subject를 변경할 필요는 없음

- 확장성 : 런타임에 새로운 옵저버를 추가하거나 제거할 수 있음

 

2) 단점

- 변경이 발생할때마다 알림을 보내야하니 처리속도면에서 오버헤드가 생길 수 있다.

- Subject와 Observer간에 순환 참조가 발생할 수 있다.

- 옵저버 패턴은 동기적이다. 대상이 관찰자 메서드를 직접 호출하므로 모든 관찰자가 알림에 return하기 전까지 다음 작업을 진행할 수 없다. 그러므로 관찰자를 멀티쓰레드, lock과 함께 사용하게 될 수 있고 그에 따른 위험도 잘 케어해야 한다.

 

 

 

 

 

 

※ 참고 문헌

https://bloodstrawberry.tistory.com/1363