관찰자 패턴
개요
객체 사이에 일 대 다의 의존 관계를 정의해두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지 받고 자동으로 업데이트될 수 있게 만듭니다. -GoF의 디자인 패턴, 382쪽
관찰자 패턴은 워낙 흔하다 보니 자바에서는 핵심 라이브러리(java.util.Observer)에 들어 있고, C#에서는 event 키워드로 지원한다.
작동 원리
관찰자
- Observer 클래스는 다음과 같은 인터페이스로 정의된다.
class Observer { public: virtual ~Observer() {} virtual void onNotify(const Entity& entity, Event event) = 0; };
- 어떤 클래스든 Observer 인터페이스를 구현하기만 하면 관찰자가 될 수 있다.
대상
- 알림 메서드는 관찰당하는 객체가 호출한다. GoF에서는 이런 객체를 대상(subject)이라고 부른다.
- 대상은 알림을 기다리는 관찰자 목록을 들고 있어야 한다.
class Subject { public: void addObserver(Observer* observer) { //배열에 추가 } void removeObserver(Observer* observer) { //배열에서 제거 } private: Observer* observers_[MAX_OBSERVERS]; int numObservers_; };
- 이를 통해 누가 알림을 받을 것인지 제어할 수 있다.
- 대상은 관찰자와 상호작용하지만, 서로 커플링되어 있지 않다.
- 대상의 다른 임무는 알림을 보내는 것이다.
class Subject { protected: void notify(const Entity& entity, Event event) { for(int i = 0; i < numObservers; i++) { observers_[i]->onNotify(entity, event); } } };
결과
장점
- 객체간의 커플링을 줄여준다.
단점
- 관찰자 패턴은 동기적이라는 점이다.
- 대상(Subject)이 관찰자 메서드를 직접 호출하기 때문에 모든 관찰자가 알림 메서드를 반환하기 전에는 다음 작업을 진행할 수 없다.
주의사항
- 관찰자를 삭제하는 과정에서 대상에 있는 포인터가 이미 삭제된 개체를 가리킬 수 있다.(NullPointException)
- 대상이 삭제되면서 더 이상 알림을 받을 수 없는데도 관찰자가 알람을 기다릴 수 있다.
- 이 경우 대상에 추가되지 않은 관찰자는 쓸모가 없으므로 스스로 삭제될 수 있도록 대상이 삭제되기 전에 사망 알림을 보내는 것으로 해결할 수 있다.
- 알림을 받은 관찰자는 필요한 작업을 알아서 하면 된다.
- 대상이 삭제되면서 더 이상 알림을 받을 수 없는데도 관찰자가 알람을 기다릴 수 있다.
- 관찰자를 멀티스레드, 락과 함께 사용하는 경우에는 정말 조심해야 한다.
- 어떤 관찰자가 대상의 락을 가진다면 게임 전체가 교착상태에 빠질 수 있다.
- 엔진에서 멀티스레드를 많이 쓴다면 이벤트 큐를 이용해 비동기적으로 상호작용하는 게 더 좋을 수도 있다.
- 어떤 관찰자가 대상의 락을 가진다면 게임 전체가 교착상태에 빠질 수 있다.
응용
- 동적 할당을 해야 하는 배열 대신 연결 리스트를 사용한다.(단순 연결 리스트 대신 이중 연결 리스트 사용)
- Subject 클래스에 배열 대신 관찰자 연결 리스트의 첫째 노드를 가리키는 포인터를 둔다.
- Observer에 연결 리스트의 다음 관찰자를 가리키는 포인터를 추가한다.
- 관찰자 객체 그 자체를 리스트 노드로 활용하기 때문에, 관찰자는 하나의 대상 관찰자 목록에만 등록할 수 있다.
- 위의 3번 문제를 피하기 위한 방법으로 리스트 노드 풀이 있다.
- 전과 마찬가지로 대상이 관찰자 연결 리스트를 들고 있다.
- 노드는 관찰자 객체 대신 따로 간단한 노드를 만들어, 관찰자와 다음 노드를 포인터로 가리키게 한다.
- 같은 관찰자를 여러 노드에서 가리킬 수 있게 된다.