'게임 프로그래밍 패턴' 교재를 학습하며 핵심 내용과 함께 개인적으로 보충한 내용을 정리해보았다.
- 관찰자 패턴이란?
- 객체 사이에 일대 다의 의존 관계를 정의하여 특정 객체의 상태가 변화할 때, 그 객체에 의존성을 가진 다른 객체들이 변화를 통지 받아 자동으로 업데이트 할 수 있게 만드는 패턴을 뜻한다.
- MVC(모델-뷰-컨트롤러) 구조에 기반이 되는 패턴이다.
- 흔히 사용되는 패턴으로 Java에서는 핵심 라이브러리인 java.utill.observer에 들어가 있고, C#에서는 event 키워드로 지원한다. - 관찰자 패턴을 사용 했을 때 장점은?
- 여러 플레이 요소에서 발생시킬 수 있는 기능(ex. 업적)을 구현할 때 코드끼리 디커플링 된 상태에서 동작시킬 수 있다.
- 디커플링 상태이므로 어떤 코드에 변화가 생겼을 때 누가 받든 조건에 상관없이 알림을 보낼 수 있다. 덕분에 광범위한 기능을 깔끔하게 구현할 수 있으며 유지보수가 용이하다.
- 특정 인터페이스를 구현한 인스턴스 포인터 목록을 관리하는 클래스 하나만 있으면 간단하게 관찰자 패턴을 만들 수 있다. - 구체적인 사용 예시 : 업적 시스템
- 특정 기준을 달성하면 배지를 획득할 수 있는 업적 시스템을 구현한다고 할 때, 관찰자 패턴을 사용하면 물리 엔진 코드와 같은 기타 코드를 뒤섞이지 않아도 해당 코드가 알림을 보낼 때마다 받을 수 있도록 스스로를 등록시킬 수 있다. 이렇게 되면 알림을 받은 업적 시스템이 업적을 잠금 해제하기 때문에 다른 코드에 영향을 끼치지 않고 업적 목록을 바꾸거나 아예 업적 시스템을 분리시킬 수도 있다. - 관찰자 패턴의 작동원리
1. 관찰자
- 인터페이스로 정의된 관찰자(observer) 클래스는 다른 객체가 무엇을 하는지 지켜보는 역할을 한다.
- 어떤 클래스든 observer 인터페이스를 구현하기만 하면 관찰자가 될 수 있다.
2. 대상(Subject)
- 알림 메서드를 호출하기 위해 관찰 당할 객체이다.
- 관찰자와 커플링 되지 않은 상태로 상호작용 한다.
- 대상은 다음과 같은 두 가지의 목적을 가진다.
첫 번째, 알림을 기다리는 관찰자 목록을 선언해 가지고 있는다.
: 이 때 관찰자 목록을 밖에서 변경할 수 있도록 API를 public으로 열어 둔다.
-> 이를 통해 알림을 받을 대상을 제어할 수 있게 된다.
: 또한, 관찰자를 여러 개의 목록으로 관리한다.
관찰자를 여러 개 등록할 수 있게 하면 관찰자들이 각자 독립적으로 다뤄지는 걸 보장할 수 있다. 즉, 관찰자끼리 서로를 인지할 필요가 없어지기 때문에 여러개의 관찰자를 사용해도 작동에 방해가 되거나 꼬이지 않게 된다.
두 번째, 알림을 보내는 것
세 번째, 관찰
- 알림이 필요한 곳(ex. 물리 엔진)에 훅(hook)을 걸어 알림을 보낼 수 있게 subject 클래스를 상속 받는다.
- 이렇게 하면 notify() 함수를 통해 알람을 보낼 수 있지만 밖에서는 notify()에 접근할 수 없다.
- 반면 addObserver()와 removeObserver()는 public 이므로 물리시스템 접근이 가능하다면 어디서나 물리 시스템에 접근할 수 있다.
- notify()를 호출해 전체 관찰자에게 알림을 전달해 일을 처리하게 하면 된다.
- 또한 특정 인터페이스를 구현한 인터페이스 포인터 목록을 관리하는 클래스 하나만 있으면 간단하게 관찰자 패턴을 만들 수 있다. - 관찰 패턴의 문제점에 대한 오해
1. 속도가 느리다?
- 관찰 패턴이 알림이 있을 때마다 동적할당을 하거나 큐잉 하는 등의 행위로 속도가 느릴 수 있는 이벤트, 메시지, 데이터 바인딩과 유사하여 CPU를 낭비할 것이라는 생각이 들 수 있다.
-> 하지만 관찰자 패턴은 목록을 돌면서 필요한 가상 함수를 호출하면 알림을 보낼 수 있기 때문에 전혀 느리지 않다.
-> 정적 호출보다 약간 느리긴 하지만, 진짜 성능에 민감한 코드가 아니라면 문제가 되지 않을 정도의 속도다.
-> 관찰자 패턴은 성능에 민감하지 않은 곳에 가장 잘 맞으므로 동적 패치를 써도 크게 상관없다.
-> 이 점을 제외하면 성능이 나쁠 이유가 없다. 인터페이스를 통해 동기적으로 메서드를 간접 호출할 뿐 메시징용 객체를 할당하지 않고, 큐잉도 하지 않기 때문이다.
2. 너무 빠르다?
- 관찰자 패턴은 동기적이고, 대상이 관찰자 메서드를 직접 호출하기 때문에 모든 관찰자가 알림 메서드를 반환하기 전에는 다음 작업을 진행할 수 없다.
-> 따라서 이벤트에 동기적으로 반응한다면 최대한 빨리 작업을 끝내고 제어권을 다시 넘겨주어 UI가 멈추지 않도록 해야 한다. 오래 걸리는 작업이 존재한다면 다른 스레드에 넘기거나 작업 큐를 활용해야 한다.
- 관찰자를 멀티스레드, 락과 함께 사용할 때는 정말 조심해야 한다. 어떤 관찰자가 대상의 락을 물고 있다면 게임 전체가 교착 상태에 빠질 수 있다.
- 엔진에서 멀티스레드를 많이 쓰고 있다면 이벤트 큐(15장)을 이용해 비동기적으로 상호작용 하는 게 더 좋을 수도 있다.
3. 동적할당을 너무 많이 한다?
- 관찰자가 추가, 삭제 될 때 크기가 알아서 늘었다가 줄어드는 동적할당 컬렉션을 사용한다.
- 관찰자 추가 시에만 메모리를 할당하기 때문에 알림을 보낼 때는 메서드를 호출할 뿐 동적할당은 전혀 하지 않는다.
- 이렇게 게임 코드가 실행될 때 처음 관찰자를 등록하고 건드리지 않는다면 메모리 할당은 거의 일어나지 않게 된다. - 그래도 성능이 걱정된다면? 동적 할당 없이 관찰자를 등록 / 해제 하는 법
- 관찰자 연결 리스트 만들기
: 대상(head)에 포인터 컬렉션을 따로 두지 않고, 관찰자(Next) 객체가 연결 리스트의 노드가 되도록 observer에 상태를 조금 추가하면 관찰자가 스스로를 엮게 만들어 동적 할당 문제를 해결할 수 있다.
- 관찰자 연결 리스트의 구현 방법
: 먼저 subject 클래스에 배열 대신 관찰자 연결 리스트의 첫 째 코드를 가르키는 포인터를 준다. 그 다음, observer에 연결 리스트와 다음 관찰자를 가리키는 포인터를 추가한다.
'CS > 디자인패턴' 카테고리의 다른 글
3장. 경량 패턴(Flyweight Pattern) (0) | 2022.08.28 |
---|---|
2장. 명령 패턴(Command Pattern) (0) | 2022.08.23 |
1장. 도입 (0) | 2022.08.16 |