관찰자 패턴

1 minute to read

개요

객체 사이에 일 대 다의 의존 관계를 정의해두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지 받고 자동으로 업데이트될 수 있게 만듭니다. -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)
    1. 대상이 삭제되면서 더 이상 알림을 받을 수 없는데도 관찰자가 알람을 기다릴 수 있다.
    2. 이 경우 대상에 추가되지 않은 관찰자는 쓸모가 없으므로 스스로 삭제될 수 있도록 대상이 삭제되기 전에 사망 알림을 보내는 것으로 해결할 수 있다.
    3. 알림을 받은 관찰자는 필요한 작업을 알아서 하면 된다.
  • 관찰자를 멀티스레드, 락과 함께 사용하는 경우에는 정말 조심해야 한다.
    1. 어떤 관찰자가 대상의 락을 가진다면 게임 전체가 교착상태에 빠질 수 있다.
    2. 엔진에서 멀티스레드를 많이 쓴다면 이벤트 큐를 이용해 비동기적으로 상호작용하는 게 더 좋을 수도 있다.

응용

  • 동적 할당을 해야 하는 배열 대신 연결 리스트를 사용한다.(단순 연결 리스트 대신 이중 연결 리스트 사용)
    1. Subject 클래스에 배열 대신 관찰자 연결 리스트의 첫째 노드를 가리키는 포인터를 둔다.
    2. Observer에 연결 리스트의 다음 관찰자를 가리키는 포인터를 추가한다.
    3. 관찰자 객체 그 자체를 리스트 노드로 활용하기 때문에, 관찰자는 하나의 대상 관찰자 목록에만 등록할 수 있다.
  • 위의 3번 문제를 피하기 위한 방법으로 리스트 노드 풀이 있다.
    1. 전과 마찬가지로 대상이 관찰자 연결 리스트를 들고 있다.
    2. 노드는 관찰자 객체 대신 따로 간단한 노드를 만들어, 관찰자와 다음 노드를 포인터로 가리키게 한다.
    3. 같은 관찰자를 여러 노드에서 가리킬 수 있게 된다.