태크놀로지

단점을 보안한 옵저버 패턴 본문

디자인 패턴

단점을 보안한 옵저버 패턴

원택 2020. 9. 15. 16:16

옵저버 패턴의 단점

  • 옵저버 패턴을 구현시 알림이 있을 때마다 동적할당을 하거나 큐잉하기 때문에 실제로 느릴 수 있습니다. 하지만 인터페이스를 통해 동기적으로 메서드를 간접호출할뿐 메시지용 객체를 할당하지도 않고, 큐잉도 하지 않습니다.
  • 주의해야할점은 동기적이라는 점입니다. 대상이 관찰자 메서드를 직접 호출하기 때문에 모든 관찰자가 알림 메서드를 반환하기 전에는 다음 작업을 진행할 수 없습니다. 관찰자 중 하나라도 느리면 대상이 블록될 수 도있기 때문입니다. 이벤트에 동기적으로 반응한다면 최대한 빨리 작업을 끝내고 제어권을 넘겨주어 프로그램이 멈추지 않도록 해야합니다. ( 멀티쓰레드 사용시 이벤트 큐를 이용해 비동기적으로 상호작용하는것을 추천 )
  • 동적할당이 많이 일어날까? 게임에선 옵저버 개수가 정적인것이 아니기 때문에 동적배열을 사용하여 관리할것입니다.  처음 게임이 시작될때 옵저버들을 모두 등록해놓은뒤 시작하면 메모리할당은 거의 일어나지 않을것입니다. 그래도 동적할당없이 관찰자를 등록, 해제하는 방법을 구현해보겠습니다.

관찰자 연결 리스트

현재 Subject는 Observer의 포인터 배열을 들고 있습니다. 이떄 관찰자 객체가 연결리스트의 노드가 되어 연결리스트 형식을 이룹니다.

 

변경된 Subject 인터페이스

class Observer;
class Entity;
class Subject
{
protected:
	//std::vector<Observer*> m_Observers;
	Observer* m_HeadPtr;

protected:
	void Notify(int _event, const Entity& data);

public:
	void AddObserver(Observer* observer);
	void RemoveObserver(Observer* observer);

public:
	Subject() : m_HeadPtr(nullptr) {}
	virtual ~Subject() = default;
};
void Subject::Notify(int _event, const Entity& data)
{
	//for (auto& o : m_Observers) o->OnNotify(_event, data);
	Observer* observer = m_HeadPtr;
	while (observer != nullptr)
	{
		observer->OnNotify(_event, data);
		observer = observer->m_NextPtr;
	}
}

void Subject::AddObserver(Observer* observer)
{
	if (!observer) return;

	//m_Observers.push_back(observer);
	// 앞쪽에 추가하는 방식
	observer->m_NextPtr = m_HeadPtr;
	m_HeadPtr = observer;
}

void Subject::RemoveObserver(Observer* observer)
{
	if (!observer) return;

	// 노드가 1개일때
	if (m_HeadPtr == observer)
	{
		m_HeadPtr = observer->m_NextPtr;
		observer->m_NextPtr = nullptr;
		return;
	}

	// 노드가 1개이상일때
	Observer* current = m_HeadPtr;
	while (current != nullptr)
	{
		if (current->m_NextPtr == observer)
		{
			current->m_NextPtr = observer->m_NextPtr;
			observer->m_NextPtr = nullptr;
			return;
		}
		current = current->m_NextPtr;
	}
}

동적할당 컨테이너인 vector 사용대신, 연결된 Observer 포인터를 소유하고 있습니다. 연결리스트는 노드를 앞쪽에 추가하였고 포인터를 따라가며 Notify를 수행합니다.

 

변경된 Observer 인터페이스

class Entity;
class Subject;
class Observer
{
	friend class Subject;
private:
	Observer* m_NextPtr;

public:
	virtual void OnNotify(int _event, const Entity& data) = 0;

public:
	Observer() : m_NextPtr(nullptr) {};
	virtual ~Observer() = default;
};

다음 노드인 Observer를 가리키기 위해 nextPtr를 소유하고 있습니다. Subject에서 Observer의 Next를 지정해주기 위해 Subject를 friend로 선언합니다.

 

실행결과

vector와 동일한 결과가 나옵니다. 리스트 노드를 사용하였기 때문에 추가 삭제는 더욱 빨라졌습니다. 또한 옵저버를 추가할때 포인터만 지정해주면 되기때문에 메모리할당을 하지 않습니다. 하지만 Subject하나당 여러 Observer들이 등록되는게 일반적인 경우입니다. 지금 구조는 하나의 Observer만 알고 다른 Observer를 알 수 없습니다. 이 경우 객체풀에 미리 할당하여 동적 메모리 할당 없이 재사용할 수 있습니다.


Observer 혹은 Subject가 제거됬을경우

Observer를 부주의하게 삭제하다 보면 대상에 있는 포인터가 이미 삭제된 객체를 가리킬 수 있다. Observer가 Subject를 참조하지 않게 구현하기 때문에 Subject를 제거하기에 상대적으로 쉽다.

Subject의 Observer명단에서 Observer가 삭제될 경우(등록되지 않은 Observer) 문제는 되지 않지만, Observer가 삭제된줄도 모르고 계속해서 알림을 기다릴 수도 있다. 

이를 해결할 방법은 Subject가 삭제되기 직전 마지막으로 "사망"이라는 메시지를 보내면 된다. 이 후 메시지를 받은 Observer들은 Subject를 해제해주면 된다.

 

Subject 소멸자

Subject::~Subject()
{
	// default Entity를 전송함 -> NullEntity 클래스를 생성해서 Null객체관리를 하는것을 추천
	Entity* enti = new Entity();
	Notify(EVENT_SUBJECT_DIE, *enti);
	m_HeadPtr = nullptr;
}

해제되기전 모든 Observer에게 Die 이벤트를 전송후 메모리 해제

 

Observer에서 Event 처리

void Achievements::OnNotify(int _event, const Entity& data)
{
	switch (_event)
	{
	case EVENT_START_JUMP:
	{
		if (data.m_IsHero && !m_Achieve_JumpCount) {
			cout << "점프! ";
			if (m_JumpCount < 500)
				m_JumpCount += 1;
			else
				UnLockJumpCount();
		}
		break;
	}
	case EVENT_SUBJECT_DIE:
	{
		// Subject가 해제되었다는 메시지
		cout << "Subject 사망" << endl;
		m_NextPtr = nullptr;
	}
	default:
		break;
	}
}

Subject가 삭제될 예정이라는 메시지를 받으면 포인터를 nullptr로 초기화해주고 그에 따른 행동을 하면됩니다. 필자는 Subject가 제거될때 소멸자에서 자동으로 모든 대상으로부터 등록을 취소하도록 구현하였습니다.


출처

도서, 게임프로그래밍 패턴, 한빛미디어

'디자인 패턴' 카테고리의 다른 글

옵저버 패턴이란?  (0) 2020.09.13
커맨드 패턴이란?  (0) 2020.09.11
컴포넌트 패턴이란  (0) 2020.09.08
인덱스를 사용한 씬관리 방법  (0) 2020.09.03
싱글턴 패턴이란  (0) 2020.09.02