[디자인 패턴] #13_이벤트 큐, Event Queue
게임 디자인 패턴 중 "디커플링 패턴"에 대해 알아보겠습니다.
"게임 프로그래밍 패턴"의 15 항목, "이벤트 큐"에 해당하는 내용입니다.
의도
- 이벤트를 보내는 시점과 처리하는 시점의 디커플링
동기
1. 예제 코드
class Audio
{
public:
static void playSound(SoundID soundId, int volume);
};
void Audio::playSound(SoundID soundId, int volume)
{
ResourceId resource = loadSound(soundId);
//특정 Sound를 Play하기 위한 처리 코드...
}
- 사운드 재생을 처리하기 위한 "Audio" 클래스를 가정합니다.
2. 문제점
- Audio::playSound()는 동기적이며 다음 호출을 블록 하여, 성능 저하의 요인이 됩니다.
- 멀티 쓰레드 환경 이곳저곳에서 호출되는 playSound()는 "동기화"처리가 필수적입니다.
- "즉시성"때문에, 처리 시점에 다른 처리 과정에 "인터럽트"를 발생시켜 처리 위치와 시점을 관리하기 힘듭니다
패턴
1. 목표
- 메세지를 보내는 곳과 받는 곳을 분리하는 것뿐만 아니라 보내는 시점과 받는 시점을 분리합니다.
- 전달받은 요청 사항들을 취합하는 것과 더불어 "동기화"를 통해 처리 시점을 관리합니다.
- 요청을 보내는 곳과 처리 결과를 받는 곳이 다르다는 가정하에 "요청 처리"의 제어권을 "받는 곳"이 독점합니다.
2. 이벤트 큐의 역할?
- 요청이 들어온 순서대로 "큐"에 저장합니다.
- 요청을 보내는 곳에서는 요청을 큐에 보내고 기다리지 않고 리턴합니다.
- 요청의 처리는 "즉시" 수행되지 않고, 큐에 들어온 순서대로 나중에 처리됩니다.
- 요청을 받는 시점 + 요청을 처리하는 시점의 "디커플링"을 목표로 합니다.
3. 사용 전에 고려해야 할 점
- 중앙 "이벤트 큐"는 전역 변수와 같다.
- 이벤트를 보낸 시점의 게임 "World"의 상태와 이벤트를 처리하는 시점의 게임 "World"의 상태는 다르다!
- 요청을 보내는 곳과 처리하는 곳이 같을 경우 비 정상적인 "피드백 루프"에 빠질 수 있습니다!
예제 코드
struct PlayMessage
{
SoundId id;
int volume;
};
class Audio
{
public:
static void Init()
{
_head = 0;
_tail = 0;
numPending = 0;
}
static void playSound(SoundID soundId, int volume)
{
assert((_tail + 1) % MAX_PENDING != _head);
// pending[_tail]에 새로 들어온 요청을 저장합니다.
pending[_tail].id = soundId;
pending[_tail].volume = volume;
// "_tail"을 증가시킵니다.
_tail = (_tail + 1) % MAX_PENDING;
}
static void Update()
{
// 이벤트 큐에 쌓인 이벤트 요청이 없으면 리턴합니다.
if (_head == _tail) return;
// pending[_head], 즉 "_head"가 가리키는 이벤트 요청을 처리합니다.
// _head를 증가시킵니다.
_head = (head_ + 1) % MAX_PENDING;
}
private:
static int _head = 0;
static int _tail = 0;
static const int MAX_PENDING = 16;
static int numPending;
static PlayMessage pending[MAX_PENDING];
};
1. 기본 구조
- Message 정보를 기억하기 위한 메모리 공간, 구조체 정의
- 새로 들어온 요청을 취합하고, "tail" 위치에 저장하는 작업 -> Audio::playSound()
- "head"가 가리키는 요청을 처리하는 작업 -> Audio::Update()
원형 버퍼
1. 왜?
- 일반 배열과 다르게 "연결 리스트"의 기능을 차용해서 "삽입"+"제거" 시 이동 작업을 생략할 수 있습니다!
2. 개념
- "머리"는 큐에서 처리할 요청을 읽을 위치입니다.
- "꼬리"는 반대로 새로 들어올 요청이 저장될 위치입니다. "마지막 요청이 저장된 위치"가 아닙니다!
- Queue의 "머리" 위치부터 요청이 처리되어 Queue의 앞부분에 빈 공간이 발생합니다.
- 이때, 단순히 "head += 1" 이동하는 것이 아니라, "head = (head+1) % MAX_PENDING"으로 이동합니다.
- "tail" 또한 마찬가지입니다.
디자인 결정 요소
1. 큐에 들어갈 이벤트? 혹은 메시지?
- 이벤트 : N개의 리스너를 지원하며, 리스너의 범위(종류의 범위)가 더 넓습니다.
- 메시지 : 대부분의 경우 리스너가 한 개입니다.
2. 누가 요청을 보내는가?
- 하나라면, 암시적으로 누가 보낼지 미리 알 수 있으며, 리스너가 여러 개일 가능성이 높습니다.
- 여러 개라면, 큐의 순환을 주의해야 하며, 요청을 구분하기 위해 보낸 측의 추가적인 정보가 필요할 수 있습니다.
'언어 > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴]#15_객체 풀, Object Pooling (0) | 2022.11.02 |
---|---|
[디자인 패턴]#14_서비스 중개자 패턴, Service Locator (0) | 2022.10.09 |
[디자인 패턴]#12_Component Pattern, 컴포넌트 패턴 (0) | 2022.09.25 |
[디자인 패턴]#11_타입 객체, Type Object (0) | 2022.09.19 |
[디자인 패턴]#10_Sandbox Pattern, 샌드박스 패턴 (0) | 2022.09.12 |