디자인패턴

관찰자(Observer) 패턴

띠애모 2024. 3. 15. 19:28

관찰자 패턴 : 객체 사이에 일 대 다의 의존관계를 정의해두어 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지받고 자동으로 업데이트될 수 있게 만듭니다.

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 객체가 생긴다. 등록취소는 유의해야만 한다.