MessagePipe 는 자식 스코프에서 꺼내도 나뉘지 않는다
battleScope = rootScope.CreateChild(child =>
{
child.Register<BattleHud>(Lifetime.Scoped); // 생성자에서 ISubscriber<Damage> 를 받는다
});
전투 구간을 자식 스코프로 떼었으니 BattleHud 는 전투 안의 Damage 만 받을 것 같습니다. VContainer 로 재 보면 다른 자식 스코프에서 보낸 Damage 도 받습니다.
MessagePipe 브로커의 구독 목록은 옵션에 정한 수명을 따르고, 기본이 싱글턴입니다. 어느 스코프에서 꺼내든 루트의 목록 하나를 씁니다. 스코프 수명으로 등록해야 나뉘고, 그때는 스코프를 닫는 것만으로 그 안의 구독이 한꺼번에 걷힙니다.
기본 수명이 싱글턴이라 목록이 루트에 하나다
IPublisher<T> 와 ISubscriber<T> 는 컨테이너가 따로 만든 MessageBroker<T> 객체입니다. 둘이 같은 구독자에게 닿는 것은 생성자에서 같은 MessageBrokerCore<T> 를 받기 때문이고, 구독 목록은 이 Core 가 듭니다.
var options = builder.RegisterMessagePipe(); // 옵션의 기본 InstanceLifetime 은 Singleton
builder.RegisterMessageBroker<Damage>(options); // 그 수명으로 Core 와 브로커를 등록한다
Core 가 싱글턴이면 자식 스코프 어디서 꺼내도 루트에 있는 같은 Core 가 옵니다.
| 상황 (기본 설정) | 결과 |
|---|---|
| 자식 스코프 1 에서 구독, 자식 스코프 2 에서 발행 | 받는다 |
스코프가 있어도 기본 설정에서는 나뉘지 않습니다.
스코프 수명으로 등록하면 나뉜다
Core 가 스코프마다 따로 생기게 하면 됩니다. 옵션의 수명을 바꾸거나, 그 자리만 스코프 수명으로 고정된 인터페이스를 받습니다.
옵션을 Scoped 로 바꿔 루트에 한 번 등록해 두면, 자식 스코프가 생길 때마다 그 스코프의 브로커가 따로 생깁니다. 위의 BattleHud 는 전투 스코프의 구독자를 받아 다른 스코프의 Damage 를 받지 않습니다.
스코프를 닫으면 구독이 한꺼번에 걷힌다
스코프 수명 브로커는 스코프를 닫을 때 컨테이너가 Core 를 Dispose 합니다. 목록은 칸을 하나씩 비우지 않고 빈 배열로 갈아 끼웁니다.
// Runtime/Internal/FreeList.cs (Dispose, 줄임)
lock (gate)
{
if (isDisposed) return;
isDisposed = true;
values = Array.Empty<T>(); // 배열을 통째로 갈아 끼운다
count = 0;
}
| 스코프를 닫은 뒤 | 결과 |
|---|---|
| 닫기 전에 받아 둔 발행자로 발행 | 아무도 안 불린다 |
| 닫힌 스코프의 구독자로 새로 구독 | 예외 없이 빈 IDisposable, 메시지는 안 온다 |
같은 상황에서 옵션을 HandlingSubscribeDisposedPolicy.Throw 로 |
ObjectDisposedException |
닫힌 브로커에 구독하면 기본 정책 Ignore 가 빈 IDisposable 을 돌려줍니다. 구독한 것처럼 보이는데 아무것도 오지 않습니다.
발행은 시작할 때 배열 참조를 한 번 읽어 두므로, 발행 도중에 닫아도 그 발행은 옛 배열을 끝까지 돕니다. 첫 핸들러 A 가 자기 브로커의 Core 를 닫게 했더니 그 발행에서 A, B, C 가 다 불렸고, 다음 발행부터는 아무도 불리지 않았습니다.
주의할 점
1. 나누려면 스코프 수명으로 등록한다
안 지키면 생기는 문제. 방마다, 씬마다 스코프를 나눴는데 브로커가 싱글턴이라 다른 방의 Damage 를 받습니다. 스코프를 닫아도 루트가 살아 있으니 구독도 그대로 남습니다.
이렇게 쓴다. 옵션을 InstanceLifetime.Scoped 로 바꾸거나, 나눌 자리에서만 IScopedPublisher<T> 와 IScopedSubscriber<T> 를 받습니다.
2. 닫힌 스코프의 브로커에 구독하지 않는다
안 지키면 생기는 문제. 기본 정책 Ignore 때문에 예외 없이 빈 IDisposable 이 옵니다. 씬을 넘긴 뒤 구독했는데 아무것도 안 오고 에러도 없는 증상이 이것입니다.
이렇게 쓴다. 구독은 살아 있는 스코프의 브로커에 합니다. 개발 중에는 정책을 Throw 로 바꿔 바로 드러나게 합니다. 옵션은 등록할 때 한 번 읽힙니다.
var options = builder.RegisterMessagePipe(o =>
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
o.HandlingSubscribeDisposedPolicy = HandlingSubscribeDisposedPolicy.Throw;
#endif
});
3. 핸들러 안에서 스코프를 닫아도 진행 중인 발행은 끝까지 간다
안 지키면 생기는 문제. 씬을 끝내는 메시지를 받은 핸들러가 그 자리에서 스코프를 닫아도, 이미 시작한 발행은 뒤 핸들러들을 마저 부릅니다. 뒤 핸들러는 정리가 시작된 오브젝트를 건드립니다.
이렇게 쓴다. 핸들러 안에서 스코프를 닫는 일은 발행이 끝난 뒤(다음 프레임 등)로 미룹니다.
정리
MessagePipe 는 스코프 수명으로 등록해야 스코프마다 나뉩니다.
- 기본 수명이 싱글턴이라 자식 스코프에서 꺼내도 루트의 Core 하나를 씁니다.
- 옵션을
Scoped로 바꾸거나IScopedPublisher와IScopedSubscriber를 쓰면 나뉩니다. - 스코프를 닫으면 구독이 한꺼번에 걷히고, 닫힌 브로커에 구독하면 기본값에서는 조용히 빈 구독이 됩니다.