메세지나 이벤트를 보내는 시점과 처리하는 시점을 디커플링 한다.

 

이벤트는 큐를 통해 OS로부터 애플리케이션으로 전달된다.

 

게임에서 자체 이벤트 큐를 만들어 중추 통신 시스템으로 활용한다. 게임 시스템들이 디커플링 상태를 유지한 채로 서로 고수준 통신을 하고 싶을 때 사용한다.

 

이벤트 요청을 받는 부분과 요청을 처리하는 부분을 분리할 수 있게 된다.

 

15.3 패턴

큐는 요청이나 알림을 들어온 순서대로 저장한다. 알림을 보내는 곳에서는 요청을 큐에 넣은 뒤에 결과를 기다리지 않고 리턴한다. 요청을 처리하는 곳에서는 큐에 들어 있는 요청을 나중에 처리한다. 요청은 그곳에서 직접 처리될 수도 있고, 다른 여러 곳으로 보내질 수도 있다. 이를 통해 요청을 보내는 쪽과 받는 쪽을 코드뿐만 아니라 시간 측면에서도 디커플링한다.

 

15.4 언제 쓸 것인가?

메세지를 보내는 곳과 받는 곳을 분리하고 싶을 뿐이라면 관찰자 패턴(4장)이나 명령 패턴(2장)을 사용하면 처리할 수 있

다. 메세지를 보내는 시점과 받는 시점을 분리하고 싶을 때만 큐가 필요하다. (상대적으로 간단한 관찰자, 명령 패턴으로 해결할 수 있으면 그렇게 하는 편이 낫다)

 

A코드가 메세지를 보내면 (큐에 요청을 push) B 코드는 자기가 편할 때 요청을 가져온다 (큐에서 pop)

메세지를 보내는 쪽(A 코드)은 요청을 보내기만 할 뿐 응답을 받지 못한다.

 

15.5 주의사항

중앙 이벤트 큐는 전역 변수와 같다

ㄴ 어디에서나 접근 가능하기 때문에 주의해야 한다.

월드 상태는 언제든 바뀔 수 있다

ㄴ 큐에 요청을 보낸 시점과 요청을 처리하는 시점이 다를 수 있기 때문에 월드 상태가 다를 수 있다.

ㄴ 동기적으로 처리되는 이벤트보다 큐에 들어가는 이벤트에는 데이터가 훨씬 더 많이 필요하다.

피드팩 루프에 빠질 수 있다

ㄴ 일반적으로 이벤트를 처리하는 코드 내에서는 이벤트를 보내지 않는 방법으로 해결한다.

 

15.6 예제 코드

사운스 시스템에서의 패턴 적용 예시

 

사운드 출력 작업을 지연시키고, playSound()가 바로 리턴하게 만들어야 한다. 

이 예제에서는 일반 구조체(사운드를 출력할 때 필요한 정보를 담은) 배열을 큐로 사용한다.

큐는 원형 버퍼 방식으로 구현한다.

 

class Audio {
public:
	static void init() {
    	head_ = 0;
        tail_ = 0;
        numPending_ = 0;
    }
    
    static void update() {
    	if(head_ == tail_) return; // 보류된 요청이 없다면 아무것도 하지 않는다.
        ResourceId resource = loadSound(pending_[head_].id);
        int channel = findOpenChannel();
        if (channel == -1) return;
        startSound(resource, channel, pending_[head_].volume);
        head_ = (head_ + 1) % MAX_PENDING;
        
    }
    // 메서드
    
private:
    static int head_;
    static int tail_;
    static int MAX_PENDING = 16;
    static PlayMessage pending_[MAX_PENDING];
    static int numPending_;
};

void Audio::playSound(SoundId id, int volume) {
    for (int i = head_; i != tail_; i = (i + 1) % MAX_PENDING) {
    	if(pending_[i].id == id) {
        	// 같은 소리를 동시에 틀면 소리가 너무 커지는 현상이 있기 때문에 
            // 그 중 가장 큰 소리 하나를 사용한다.
        	pending_[i].volume = max(volume, pending_[i].volume);
            // 이 요청은 큐에 넣지 않는다.
        	return;
        }
    }
    pending_[tail_].id = id;
    pending_[tail_].volume = volume;
    tail_ = (tail_ + 1) % MAX_PENDING;
}

스레드에 코드를 분배하는 방법은 다양하지만, 오디오, 렌더링, AI같이 분야별로 할당하는 전략을 많이 쓴다.

멀티스레드 환경에서는 큐가 동시에 수정되는 것만 막으면 된다.

 

15.7 디자인 결정

큐에 무엇을 넣을 것인가?

1. 큐에 이벤트를 넣는 경우

ㄴ 이미 발생한 사건을 표현한다. 복수개의 리스너로 이벤트를 원하는 누구에든지 전파하는 용도로 사용된다.

 

2. 큐에 메세지를 넣는 경우

ㄴ 나중에 실행했으면 하는 행동을 표현한다.

ㄴ 대부분은 리스너가 하나다.

 

누가 큐를 읽는가?

1. 싱글캐스트 큐

ㄴ 큐가 어떤 클래스(Audio)의 API 일부일 때 적합하다.

ㄴ 클래스(Audio) 내부에 큐를 정의한다.

ㄴ 리스너 간에 경쟁을 고민하지 않아도 된다. (리스너가 하나 이기 때문에)

 

2. 브로드캐스트 큐

ㄴ 이벤트가 무시될 수 있다. 리스너가 없을 때 발생한 이벤트는 버려진다.

ㄴ 이벤트 필터링이 필요할 수 있다. 이벤트 개수 X 리스너 수만큼 이벤트 핸들러가 자주 호출되기 떄문에 리스너가 받고 싶은 이벤트 집합을 조절하여 이벤트 핸들러 호출 횟수를 줄일 수 있다.

 

3. 작업 큐

ㄴ 브로드캐스트 큐와 마찬가지로 리스너가 여러 개 있지만 큐에 들어있는 데이터가 리스너 중에서 한곳에만 간다.

ㄴ 스레드가 여러 개가 동시에 실행 중인 스레드 풀에 작업을 나눠줘야 할 때 일반적으로 사용하는 패턴이다.

ㄴ 어느 리스너에 데이터를 보낼지 정하는 작업 분배 알고리즘이 필요하다.

 

누가 큐에 값을 넣는가?

1. 넣는 측이 하나라면

ㄴ 하나의 특정 객체에서만 이벤트를 만들 수 있기 때문에 모든 리스너는 누가 데이터를 보냈는지를 추측할 수 있다.

 

2. 넣는 측이 여러 개라면

ㄴ 누가 보냈는지 정보를 추가해야 한다. (어떤 개체가 playSound () 를 호출하여 큐에 요청을 넣었는지)

ㄴ 이벤트 순환을 주의해야 한다.

 

큐에 들어간 객체의 생명주기는 어떻게 관리할 것인가?

1. 소유권을 전달한다

ㄴ unique_ptr<T>를 사용한다.

2. 소유권을 공유한다.

ㄴ shared_ptr<T>를 사용한다.

3. 큐가 소유권을 가진다.

ㄴ 큐가 메세지를 만들고 레퍼런스를 메세지 보내는 쪽에 돌려준다. (큐의 클래스화?)

+ Recent posts