MessagePipe 는 자식 스코프에서 꺼내도 나뉘지 않는다
Esc

    MessagePipe 는 자식 스코프에서 꺼내도 나뉘지 않는다

    2026년 9월 25일 수정–UnityDI라이브러리메시징
    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 를 쓰면 나뉩니다.
    • 스코프를 닫으면 구독이 한꺼번에 걷히고, 닫힌 브로커에 구독하면 기본값에서는 조용히 빈 구독이 됩니다.