관찰자(Observer) 패턴
관찰자 패턴 : 객체 사이에 일 대 다의 의존관계를 정의해두어 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지받고 자동으로 업데이트될 수 있게 만듭니다.
mvc(model-view-controller) 구조를 쓰는 프로그램이 발에 치일 정도로 많다. GoF 패턴중에 관찰자 패턴은 가장 널리 사용되고 있는 패턴이다. 또한 게임에서도 굉장히 많이 사용된다.
필자도 관찰자 패턴을 애용했다. 몬스터를 몇 마리 잡았는지 관찰하고 다음 스테이지로 가는 포털이 열리게 하는 등
많은 곳에서 사용했다.
예를들어 업적 시스템을 만든다고 해보자.
1
2
3
4
5
6
7
8
|
class Observer {
public:
virtual Observer() {}
virtual void onNotify(const Entity& entity, Event event) = 0;
};
|
cs |
어떤 클래스는 observer 인터페이스를 구현하기만 하면 관찰자가 될 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
class Achievements : public Observer ()
{
public:
virtual void onNotify(const Entity& entity, Event event)
{
switch(event)
{
case EVENT_ENTITY_FELL:
if (entity.isHero() && herolsOnBridge_)
{
unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
}
break;
// 그 외 다른 이벤트를 처리하고...
//heroIsOnBridge_ 값을 업데이트한다...
}
}
private:
void unlock(Achievement achievement)
{
// 아직 업적이 잠겨 있다면 잠금해제한다...
}
bool heroIsOnBridge_;
};
|
cs |
다리에서 떨어지기 업적은 다음과 같이 만들 수 있다.
알림 메서드는 관찰당하는 객체가 호출한다. GoF에서는 이러한 객체를 대상 이라고 부른다.
1
2
3
4
5
|
class Subject {
private:
Observer* observers_[MAX_OBSERVERS];
int numObservers_;
}
|
cs |
대상은 알림을 기다리는 관찰자 목록을 들고 있다고 해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class Subject
{
public:
void addobserver(Observer* observer)
{
// 배열에 추가한다...
}
void removeObserver(Observer* observer)
{
// 배열에서 제거한다...
}
// 그 외...
}
|
cs |
여기에서 볼 수 있는 점은 관찰자 목록을 외부에서 변경 할 수 있도록 public으로 열려있다는 점이다.
이를 통해서 누가 알림을 받을것인지 제어 할 수 있다.
대상은 관찰자와 상호작용하지만 서로 커플링 되어있지는 않다.
대상이 관찰자를 목록으로 가지고 있다는 점도 중요하다.
관찰자를 하나만 가지고 있다면
다리에서 떨어지는 업적을 달성 할 때 업적달성 ui를 띄우는 관찰자와
업적 달성 사운드를 발생하는 ui는 동시에 발생 될 수 없고 나중에 발생한 관찰자 이벤트가
먼저 발생된 관찰자 이벤트를 가릴 수 있기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class Subject
{
protected:
void notify(const Entity& entity, Event event)
{
for (int i = 0; i < numObservers_; i++)
{
observers_[i]->onNotify(entity, event);
}
}
};
|
cs |
Physics 클래스가 Subject 인스턴스를 포함하게 만들면.
물리엔진 그 자체를 관찰하기보다는 별도의 '낙하 이벤트 객체가 대상이 된다. 관찰자는 스스로를 physics.entityFell().addObserver(this); 식으로 등록한다.
관찰자 패턴은 동기적이다.
대상이 메서드를 직접 호출하기 때문에 모든 관찰자가 알림 메서드를 반환하기 전에는 다음 작업을 진행 할 수 없다.
관찰자를 멀티스레드나 락(lock)과 함께 사용하는것은 조심해야한다. 어떤 관찰자가 대상의 lock을 물고 있다면
게임 전체가 교착상태에 빠질 수도 있다.
관찰자 연결리스트
관찰자를 연결리스트 형태로 사용 할 수도 있다. 이러한 방식은 관찰자가 관찰자를 스스로 엮게 만들어 동적할당 문제를 해결 할 수 있다.
대상에 포인터 컬렉션을 따로 두지 않고 관찰자 객체 그 자치가 연결리스트에 노드가 되는것이다.
구현은 생략한다.
관찰자 패턴은 빠르며 메모리 관리 측면에서도 깔끔하게 만들 수 있다.
대상과 관찰자 제거
대상이나 관찰자를 제거 할 때 조심해야한다
관찰자를 부주의하게 삭제하다보면 해제된 메모리를 가리키는 무효 포인터에 알림을 보내게 된다면 문제가 발생 할 수 있다.
대상이 삭제되면 더 이상 알림을 받을 수 없지만 관찰자는 그런 줄 모르고 알림을 기다릴 수도 있다
가장 쉬운 방법은 관찰자가 삭제될 때 스스로를 등록 취소 하는것이다.
체력바 갱신 UI를 생각해보자.
체력바가 갱신될때마다 UI는 갱신 알림을 받는다. 하지만 체력바를 껐음에도
캐릭터 관찰자 목록에서 여전히 UI를 참조하기 때문에 GC(가비지 콜렉터) 가 수거해 가지 않는다
체력바를 열 때 마다 인스턴스를 새로 만들어 목록에 추가 될 수도 있다.
캐릭터가 전투할 때 많은 상태변화가 생긴다. 이들이 상태창에 업데이트 된다고 해보자.
하지만 상태창을 닫았음에도 상태창이 업데이트 된다고 하면 눈에 보이지도 않는 ui요소를 업데이트 하느라
cpu클럭을 낭비 할 수도 있다.
이런 문제를 사라진 리스너 문제 (lapsed listener problem) 라고 한다.
대상이 레퍼런스를 유지하기 때문에 메모리에 남아있는 좀비UI 객체가 생긴다. 등록취소는 유의해야만 한다.