객체 사이에 일 대 다의 의존 관계를 정의해두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지 받고 자동으로 업에이트될 수 있게 만듭니다. (GoF의 디자인 패턴 382p)
모델-뷰-컨트롤러(MVC) 구조를 쓰는 프로그램이 발에 차일 정도로 MVC패턴은 많이 사용되는데, 그 기반에는 관찰자 패턴이 있다.
업적 달성
- 좀비 100마리 죽이기
- 다리에서 떨어지기
위와 같은 특정 기준을 달성하면 배지를 얻을 수 있는데, 배지 종류는 수백 개가 넘는다고 하자.
업정 종류가 광범위하고 달성할 수 있는 방법도 다양하다 보니 깔끔하게 구현하기가 어렵다. 조금만 방심해도 업적 시스템 코드가 암세포처럼 구석구석 퍼져 나갈 것이다. '다리에서 떨어지기'는 어떻게든 물리 엔진이랑 연결해야겠지만, 충돌 검사 알고리즘의 선형대수 계산 코드 한가운데에서 unlockFallOffBridge()를 호출하고 싶진 않을 것이다.
특정 기능을 담당하는 코드는 항상 한데 모아두는게 좋다. 문제는 업적을 여러 게임 플레이 요소에서 발생시킬 수 있다는 점이다. 이런 코드 전부와 커플링되지 않고도 업적 코드가 동작하게 하려면 어떻게 해야 할까?
이럴 때 관찰자 패턴을 쓰면 된다. 관찰자 패턴을 적용하면 어떤 코드에서 흥미로은 일이 생겼을 때 누가 받든 상관없이 알림을 보낼 수 있다.
void Physics::updateEntitiy(Entity& entity) {
bool wasOnSurface = entity.isOnSurface();
entity.accelerate(GRAVITY);
entity.update();
if(wasOnSuface && !entity.isOnSurface()) {
notify(entity, EVENT_START_FALL);
}
}
위 코드는 '이게 방금 떨어지기 시작했으니 누군지는 몰라도 알아서 하시오'라고 알려주는게 전부다.
업적 시스템은 물리 엔진이 알림을 보낼 때마다 받을 수 있도록 스스로를 등록한다. 업적 시스템은 떨어지는 물체가 캐릭터가 맞는지, 그리고 떨어지기 전에 다리 위에 있었는지를 확인한 뒤에 업적을 잠금해재한다. 이런 과정을 물리 코드는 전혀 몰라도 된다.
이렇게 물리 엔진 코드는 전혀 건드리지 않은 채로 업적 목록을 바꾸거나 아예 업적 시스템을 떼어낼 수도 있다. 물리 코드는 누가 받든 말든 계속 알림을 보낸다.
관찰자 클래스
class Observer {
public:
virtual ~Observer() {}
virtual void onNotify(const Entity& entity, Event evet) = 0;
};
어떤 클래스든 Observer 인터페이스를 구현하기만 하면 관찰자가 될 수 있다.
class Achievements : public Observer {
public:
virtual void onNotify(const Entity& entity, Event event) {
switch(event) {
case EVENT_ENTITY_FELL:
if(entity.isHero() && heroIsOnBridge_) {
unlock(ACHIEVMENT_FELL_OFF_BRIDGE);
}
break;
// 그 외 다른 이벤트를 처리하고...
// heroIsBridge_ 값을 업데이트 한다.
}
}
private:
void unlock(Achivement achievement) {
// 아직 업적이 잠겨 있다면 잠금해제한다...
}
bool heroIsOnBridgh_;
};
대상
알림 메서드는 관찰당하는 객체가 호출한다. GoF에서는 이럴 객체를 '대상(subject)'이라고 한다. 대상에게는 두 가지 임무가 있는데 그중 하나는 알림을 끈질기게 기다리는 관찰자 목록을 들고 있는 일이다.
class Subject {
private:
Observer* observers_[MAX_OBSERVERS];
int numObservers_;
};
여기에서 중요한 것은 관찰자 목록을 밖에서 변경할 수 있도록 다음과 같이 API를 public으로 열어놨다는 점이다.
class Subject {
public:
void addObserver(Observer* observer) {
// 배열에 추가한다...
}
void removeObserver(Observer* observer) {
// 배열에서 제거한다...
}
// 그 외...
};
이를 통해 누가 알림을 받을 것인지를 제어할 수 있다. 대상은 관찰자와 상호작용 하지만, 서로 커플링되어 있지 않다. 물리 코드 어디에서도 업적에 관련된 부분은 없지만 업적 시스템으로 알림을 보낼 수는 있다. 이것이 관찰자 패턴의 장점이다.
관찰자를 여러 개 등록할 수 있게 하면 관찰자들이 각자 독립적으로 다뤄지는 걸 보장할 수 있다. 관찰자는 월드에서 같은 대상을 관찰하는 다른 관찰자가 있는지를 못한다. 대상의 다른 임무는 알림을 보내는 것이다.
class Subject {
protected:
void notify(const Entity& entity, Event event) {
for(int i = 0; i < numObservers_; ++i) {
ovservers_[i]->onNotify(entity, event);
}
}
// 그 외...
};
물리 관찰
남은 작업은 물리 엔진에 훅을 걸어 알림을 보낼 수 있게 하는 일과 업적 시스템에서 알림을 받을 수 있도록 스스로를 등록하게 하는 일이다. 최대한 GoF의 디자인 패턴에 나온 방식과 비슷하게 만들기 위해 Subject 클래스를 상속 받는다.
class Physics : public Subject {
public:
void updateEntity(Entity& entity);
};
이렇게 하면 Subject 클래스의 notify() 메서드를 protected로 만들 수 있다. Subject를 상속받은 Physics 클래스는 notify() 를 통해서 알림을 보낼 수 있지만, 밖에서는 notify() 에 접근할 수 없다. 반면, addObserver() 와 removeObserver() 는 public이기 때문에 물리 시스템에 접근할 수 만 있다면 어디서나 물리 시스템을 관찰할 수 있다.
이제 물리 엔진에서 뭔가 중요한 일이 생기면, notify()를 호출해 전체 관찰자에게 알림을 전달하여 일을 처리하게 한다.
너무 느리면
관찰자 패턴은 그냥 목록을 돌면서 필요한 가상 함수를 호출하면 알림을 보낼 수 있다. 정적 호출보다야 약간 느리긴 하겠지만, 진짜 성능에 민감한 코드가 아니라면 이 정도 느린건 문제가 되지 않는다. 그저 인터페이스를 통해 동기적으로 메서드를 간접 호출할 뿐 메시징용 객체를 할당하지도 않고, 큐잉도 하지 않는다.
주의해야할 점은 관찰자 패턴이 동기적이라는 점이다. 대상이 관찰자 메서드를 직접 호출하기 때문에 모든 관찰자가 알림 메서드를 반환하기 전에는 다음 작업을 진행할 수 없다. 그러므로 관찰자를 멀티스레드, 락과 함께 사용할 때는 정말 조심해야 한다. 어떤 관찰자가 대상의 락을 물고 있다면 게임 전체가 교착상태에 빠질 수 있다. 엔진에서 멀티스레드를 많이 쓰고 있다면, 이벤트 큐를 이용해 비동기적으로 상호작용하는 게 더 좋을 수도 있다.
소년코딩 - C++ 디자인 패턴 03. 관찰자 패턴, 옵저버 패턴, Observer Pattern (tistory.com)
'CS > 디자인패턴' 카테고리의 다른 글
[C++] 추상 팩토리 메서드 패턴 코드 (0) | 2024.09.22 |
---|---|
[C++] 싱글톤 패턴, Singleton Pattern (0) | 2023.08.14 |
[C++] 프로토타입 패턴, Prototype Pattern (0) | 2023.08.09 |
[C++] 경량 패턴, Flyweight Pattern (0) | 2023.08.08 |
[C++] 명령 패턴, 커맨드 패턴, Command Pattern (0) | 2023.08.08 |