옵저버 패턴(Observer Pattern)


처음에 보고 스타크래프트 옵저버가 생각났는데 나만 그런거는 아니었나보다 : 참조 -> https://pjh3749.tistory.com/266

옵저버 패턴

옵저버 패턴은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 각 옵저버에게 통지하도록 하는 디자인 패턴이다.

주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다.

GoF에는 객체 사이에 일 대 다의 의존 관계를 정의해둬서, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지 받고 자동으로 업데이트 될 수 있게 만든다. 라고 되어있다.

정리하면 어떤 객체가 변하면 연관된 객체들에게 알림을 보내는 디자인 패턴을 의미한다.

모델-뷰-컨트롤러(MVC) 구조의 기반이라고 생각해도 된다.

구현 예제 1

선생님과 학생들이 있다. 학생들은 선생님이 하는 일들을 notify 받아야 한다. 즉 선생님이 “밥을 먹는다”면 모든 학생들은 선생님이 밥을 먹었다는 것을 알아야 한다.

우선 선생님이 있어야 하니 선생님 인터페이스가 있어야 한다. 또한 학생들도 있어야 하니 학생들 인터페이스도 있어야 한다.

선생님은

  1. 학생들을 등록한다
  2. 학생들을 해제한다
  3. 학생들에게 행동을 알린다

이 세가지 기능을 할 수 있다.

Teacher Interface

public interface Teacher {
  void subscribe(Student s);
  void unsubscribe(Student s);
  void notifyStudent(String msg);
}

Student Interface

public interface Student {
  void update(String msg);
}

이 선생님 인터페이스를 구현하는 라이언 클래스를 만들면 다음과 같다.

public class Ryan implements Teacher {
  private List<Student> Students = new ArrayList<>();
  
  public void eatFood() {
    System.out.println("선생님이 밥을 먹고 있단다");
    notifyStudent("선생님이 밥을 먹고 있단다");
  }
  
  public void runAway() {
    System.out.println("선생님이 월급루팡을 하고 있단다");
    notifyStudent("선생님이 월급루팡을 하고 있단다");
  }
  
  @Override
  public void subscribe(Student s) {
    Students.add(s);
  }
  
  @Override
  public void unsubscribe(Student s) {
    Students.remove(s);
  }
  
  @Override
  public void notifyStudent(String msg) {
    Students.forEach(student -> student.update(msg));
  }
}

코드를 보면 알 수 있듯이, 선생님은 밥을 먹거나, 월급루팡을 하거나, 학생을 등록, 해제, 학생들에게 알림을 할 수 있다. 그래서 가지고 있는 학생들 list을 통해 알림을 수행한다.

이번에는 학생 죠르디의 클래스를 구현해보자.

class Jordi implements Student {
  @Override
  public void update(String msg) {
    System.out.println("수신된 내용 : "+msg);
  }
}

이제 메인 함수다.

public static void main(String[] args) {
  Teacher r = new Ryan();
  Student j = new Jordi();
  
  r.subscribe(j);
  
  r.runAway();
}

이제 이러면 학생 j에게 메세지가 전달된다.

r.unsubsribe(j);

이제 구독해제가 되어서 학생 j에게 알림이 가지 않는다.

이렇게 객체의 상태가 변화할 때 연관된 객체들에게 알림을 보낼 수 있는데 이것이 옵저버 패턴이다.

구현 예제2

업적 시스템을 추가한다고 해보자. 업적에는 다음 두가지가 있다.

  • 좀비 100마리 죽이기
  • 다리에서 떨어지기

위의 조건을 만족하면 배지를 얻을 수 있는데, 배지 종류가 굉장히 많다고 한다.

업적 종류가 많고 달성할 방법이 다양해서 깔끔하게 구현하기가 어렵다. 업적은 여러 플레이 상황에서 발생될 수 있다. 코드 전부와 커플링 되지 않고도 업적 코드가 동작하게 하려면 관찰자 패턴을 쓰면 된다.

void Physics::updateEntity(Entity& entity) {
  bool wasOnSurface = entity.isOnSurface();
  entity.accelerate(GRAVITY);
  entity.update();
  
  if(wasOnSurface && !entity.isOnSurface()) {
    notify(entity, EVENT_START_FALL);
  }
}

위의 코드를 보면 엔티티가 뭐던간에 엔티티를 받으면 그 엔티티에게 알림을 준다. 이러면 물리 코드는 전혀 몰라도 업적 잠금 해제 코드를 사용하는 것이 가능하다.

이제 다른 객체를 관찰할 (여기에서는 다리에서 떨어지는 것에 관심이 많은 업적 클래스일 것이다.) observer 클래스를 보자. Observer 클래스는 다음과 같은 인터페이스로 정의된다.

class Observer {
  public:
    virtual ~Observer(){}
    virtual void onNotify(const Entity& entity, Event event) = 0;
}

이 인터페이스를 구현하는 클래스는 관찰자가 될 수 있다.

class Achievements: public Observer {
  public:
    virtual void onNotify(const& entity, Event event) {
      switch(event) {
        case EVENT_ENTITY_FELL:
          if(entity.isHero() && heroIsOnBridge_) {
            unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
          }
          
          break;
          // 이제 업적을 깨고,,, 값을 업데이트 한다
      }
    }
   
  private:
    void unlock(Achivement achievement) {
      //만약 업적이 잠겨 있다면 잠금해제한다
    }
    
    bool heroIsOnBridge_;
}

알림 메서드는 관찰당하는 객체가 호출한다. GoF에서는 이런 객체를 대상이라고 한다. 대상에게는 두 가지 임무가 있는데 그 중 하나는 알람을 끈질기게 기다리는 관찰자 목록을 보유하고 있는 것이다. 구현 예제1에서도 관찰자 목록들을 리스트로 가지고 있었다.

class Subject {
  private:
    Observer* observers_[MAX_OBSERVERS];
    int numObservers_;
    
  public:
    void addObserver(Observer* observer) {
      // 관찰자 등록
    }
    
    void removeObserver(Observer* observer) {
      // 관찰자 해제
    }
}

이제 외부에서도 관찰자 목록을 변경할 수 있어야 하기 때문에 관찰자 등록과 해제를 public으로 열어놨다.

대상은 관찰자와 상호작용 하지만, 서로 커플링 되어 있지는 않다. 관찰자를 여러개 등록하면 관찰자들이 각자 독립적으로 다뤄지는 걸 보장할 수 있다. 관찰자들끼리는 다른 관찰자가 있는 것을 모른다.

대상은 또 알림을 보낼 수 있다고 했다.

class Subject {
  protected:
    void notify(const Entity& entity, Event event) {
      for(int i=0; i<numObservers_; i++){
        observers_[i] -> onNotify(entity, event);
      }
    }
}

문제는 관찰자 패턴은 동기적이다는 것이다. 대상이 관찰자 메서드를 직접 호출하기 때문에 모든 관찰자가 알림 메서드를 반환하기 전에는 다음 작업을 진행할 수 없다.

그래서 관찰자를 멀티스레드, 락과 함께 사용할 때는 조심해야 한다. 어떤 관찰자가 대상의 락을 가지고 있으면 게임 전체가 데드락에 빠질 수 있다.

출처 : https://boycoding.tistory.com/107