지연 이벤트 큐
이벤트를 지금 큐에 넣고, 나중에 여러분이 고른 플러시 지점에서 순서대로 발행합니다.
위험한 지점에서는 이벤트를 큐에 넣고, 여러분이 고른 안전한 지점에서 배치 전체를 순서대로 발행하세요.
| 인터페이스 | IDeferredBus |
| 끄기 스위치 | NullDeferredBus |
| 어셈블리 | CommonGameSystem.Core |
| 시작 | 부팅 시 자동 등록 (23개 서비스 중 하나) — 별도 설정 불필요 |
하는 일
지연 이벤트 큐는 이벤트 버스(IEventBus) 위의 얇은 계층입니다. 이벤트를 즉시 발행하는 대신 Enqueue합니다. 큐는 Flush()를 호출할 때까지 모든 것을 붙잡아 두었다가, 배치 전체를 이벤트 버스를 통해 발행합니다 — 모든 이벤트 타입에 걸쳐, 큐에 넣은 정확히 그 순서대로.
이벤트 발행을 위험한 지점 — 물리 콜백, 변경 중인 컬렉션을 도는 루프, 입력 처리 도중 — 에서 여러분이 고른 안전한 지점으로 옮기는 데 쓰세요. 큐가 소유하는 것은 보관과 순서뿐입니다. 라우팅과 구독자 처리는 이벤트 버스의 몫으로 남습니다. 자동 플러시는 없습니다: 배치가 언제 발화할지는 여러분이 결정합니다. 메인 스레드 전용입니다.
빠른 시작
이벤트를 평범한 클래스로 정의하세요:
public sealed class EnemyDiedEvent
{
public int EnemyId { get; }
public EnemyDiedEvent(int enemyId) { EnemyId = enemyId; }
}
그리고 지금 큐에 넣고, 나중에 발행하세요:
using CommonGameSystem.Core;
using UnityEngine;
public class MyDeferredPublisher : MonoBehaviour
{
private IDeferredBus _bus;
private void Awake() => _bus = ServiceLocator.Resolve<IDeferredBus>();
public void OnEnemyDied(int enemyId)
{
_bus.Enqueue(new EnemyDiedEvent(enemyId)); // queued — NOT published yet
}
private void LateUpdate()
{
_bus.Flush(); // publishes the batch through the Event Bus, in queue order
}
}
Bootstrap이 시작 시 이 서비스를 자동으로 등록합니다. 보여 준 대로 Awake()에서 한 번 resolve해 캐시하세요.
API 레퍼런스
IDeferredBus
| 멤버 | 설명 |
|---|---|
void Enqueue<TEvent>(TEvent evt) where TEvent : class | evt를 지금 큐에 넣습니다. Flush() 전에는 발행되지 않습니다. null 이벤트는 즉시 ArgumentNullException을 throw하므로 스택 트레이스가 호출자를 가리킵니다. class 제약은 이벤트 버스와 정확히 일치합니다 — 구조체 이벤트는 지원되지 않습니다. IL2CPP/AOT 빌드에서 안전하며, 아무것도 박싱되지 않습니다. |
void Flush() | 큐에 쌓인 배치를 큐 순서대로, 모든 이벤트 타입에 걸쳐, 이벤트 하나당 이벤트 버스 Publish 한 번으로 발행합니다. 플러시가 실행되는 동안 큐에 들어온 이벤트는 다음 배치로 갑니다. 실행 중인 플러시 안에서 Flush를 호출하면 아무 일도 일어나지 않습니다. |
int PendingCount { get; } | 큐에 있지만 아직 플러시되지 않은 이벤트 수. 진단용. |
DeferredBusOptions — 생성 시점의 readonly struct
| 멤버 | 설명 |
|---|---|
int MaxDrainPerFlush | Flush당 발행되는 이벤트 상한. 0(기본)은 무제한. 0이 아닌 값(1–1,000,000으로 클램프)은 그만큼만 발행하고 나머지를 다음 배치의 맨 앞으로 넘기며, 스로틀된 경고를 한 번 로그합니다. |
int InitialQueueCapacity | 내부 버퍼의 시작 크기. 기본 16, 0–4096으로 클램프. |
bool LogLifecycle | true이면 에디터 빌드에서 큐/플러시 추적을 로그합니다. 이 추적은 릴리즈 빌드에서는 제거됩니다. 기본 false. |
DeferredBusOptions(int maxDrainPerFlush, int initialQueueCapacity, bool logLifecycle = false) | 명시적 생성자. 숫자 범위는 생성 시 클램프됩니다. |
static DeferredBusOptions Default | 0 / 16 / false. default(DeferredBusOptions)(0 / 0 / false)와는 다르다는 점에 유의하세요. |
DeferredBusOptions With(int? maxDrainPerFlush = null, int? initialQueueCapacity = null, bool? logLifecycle = null) | 이름 있는 오버라이드를 적용한 불변 복사본. |
예제
using CommonGameSystem.Core;
using UnityEngine;
public sealed class DamageDealtEvent
{
public int EnemyId { get; }
public int Amount { get; }
public DamageDealtEvent(int enemyId, int amount) { EnemyId = enemyId; Amount = amount; }
}
public class DamageResolver : MonoBehaviour
{
private IDeferredBus _deferred;
private void Awake() => _deferred = ServiceLocator.Resolve<IDeferredBus>();
// Called from inside a physics callback — publishing immediately here could
// re-enter the collection we are iterating. Queue it instead.
public void OnHit(int enemyId, int amount)
{
_deferred.Enqueue(new DamageDealtEvent(enemyId, amount));
}
// Publish at a known-safe point: the end of the fixed-update step.
private void FixedUpdate()
{
if (_deferred.PendingCount > 0)
_deferred.Flush(); // every queued event publishes here, in queue order
}
}
끄는 방법
ServiceLocator.Replace<IDeferredBus>(new NullDeferredBus());
이렇게 하면 지연이 통째로 무음 처리됩니다. Enqueue는 이벤트를 조용히 버리고(실제 버스로 재라우팅되는 일은 없습니다), Flush는 아무것도 하지 않으며, PendingCount는 0에 머뭅니다. 실제 지연 큐에 연결된 게임은 지연 기능이 꺼진 채 계속 돌아갑니다. 이벤트 버스의 조용한 NullEventBus와 달리, 이 교체 구현은 생성 시 경고를 하나 로그합니다. 조용히 버려지는 큐("이벤트가 큐에는 들어가는 것 같은데 도착하지 않는다")는 진단이 어렵기에, 실수로 이뤄진 교체를 눈에 띄게 만든 것입니다. null 이벤트는 실제 서비스와 똑같이 여전히 throw합니다.
흔한 함정
Flush하기 전에는 아무것도 발화하지 않습니다. 자동 플러시는 없습니다. 큐에 넣고 플러시하지 않으면 이벤트는 영영 발행되지 않고PendingCount는 무한히 자랍니다. 예측 가능한 드레인 지점 하나 —FixedUpdate의 끝, 프레임의 끝 — 를 정해 항상 거기서 플러시하세요.- 플러시 중에 큐에 들어온 이벤트는 다음 플러시를 기다립니다. 배치는
Flush가 시작되는 순간 고정됩니다. 배치가 발행되는 동안 구독자가 큐에 넣은 것은 다음Flush까지 보관됩니다. 이것은 의도된 동작입니다: 모든 플러시 하나하나를 유한하게 유지합니다. MaxDrainPerFlush는 배치를 나눌 뿐 — 이벤트를 버리지 않습니다. 상한에 도달하면 남은 이벤트는 다음 배치의 맨 앞으로 이동하므로, 나중에 큐에 들어온 것보다 여전히 먼저 발행됩니다. 순서는 보존되고, 스로틀된 경고가 한 번 로그됩니다. 압력 밸브이지, 폐기가 아닙니다.- 플러시는 절대 throw하지 않고, 나쁜 이벤트 하나가 나머지를 막을 수 없습니다.
Publish호출이 throw하면 큐가 잡아서 로그하고 남은 이벤트로 계속합니다. 프레임워크 자체의 이벤트 버스는 결함 있는 구독자를 스스로 이미 격리합니다. 이 가드는 그렇게 하지 않는 교체 버스를 대비한 것입니다.