VitalRouter 인터셉터의 next 는 커서 하나다
Esc

    VitalRouter 인터셉터의 next 는 커서 하나다

    2026년 9월 30일 수정–Unity라이브러리메시징
    public async ValueTask InvokeAsync<T>(T command, PublishContext context, PublishContinuation<T> next) where T : ICommand
    {
        try { await next(command, context); }
        catch { await next(command, context); }   // 한 번 더: 뒤 인터셉터도 다시 돌까?
    }

    VitalRouter 는 라우터와 구독자 사이에 인터셉터를 끼웁니다. 로그, 권한 확인, 순서 제어가 여기 들어갑니다. next 를 부르면 다음으로 넘어가고, 안 부르면 전파가 멈추는 흔한 미들웨어 모양입니다. 그런데 next 를 두 번 부르면 기대와 다르게 돕니다.

    VitalRouter 의 next 는 인터셉터마다 따로 있지 않습니다. 발행 문맥 하나가 가진 델리게이트를 모두가 나눠 쓰고, int 커서 하나가 다음 인터셉터를 고릅니다. 그래서 체인을 위해 새로 만드는 객체가 없는 대신, 커서가 공유 상태가 됩니다.

    인터셉터는 라우터를 지나는 모든 커맨드를 받는다

    InvokeAsync<T> 가 메서드 제네릭이라 인터셉터 하나가 이 라우터의 모든 커맨드 타입을 받습니다. 구독자처럼 “내 타입인가” 를 가려 건너뛰는 단계가 없습니다. Hit 만 기록할 생각이면 인터셉터 안에서 typeof(T) 로 걸러야 하고, 라이브러리의 타입 지정 래퍼도 안에서 이 비교를 한 뒤 다른 타입은 next 로 흘려보냅니다.

    next 는 모두가 나눠 쓰는 델리게이트 하나다

    발행 문맥은 생성자에서 continuation = InvokeRecursiveAsync 를 한 번 만들고, 모든 인터셉터에 next 로 이것을 넘깁니다. next 를 부르면 다음 인터셉터로 곧장 가는 것이 아니라 이 함수로 돌아오고, 여기서 커서를 한 칸 올려 다음을 고릅니다.

    인터셉터 둘을 두고 첫째가 next 를 두 번 부르게 하니 로그가 첫째, 둘째, 핸들러, 핸들러 였습니다. 두 번째 호출은 둘째를 거치지 않았고, .NET, Mono, IL2CPP 가 같았습니다.

    지웠다 다시 넣은 인터셉터는 앞에 끼어든다

    인터셉터 목록도 빈 칸을 가장 앞의 것부터 다시 쓰는 배열이고, 커서는 칸 번호 오름차순으로 돕니다.

    호출 인터셉터 배열 발행했을 때 도는 순서
    AddFilter(Log), AddFilter(Auth) [Log, Auth] Log, Auth, 핸들러
    RemoveFilter(Log) [(빈 칸), Auth]
    AddFilter(Metric) [Metric, Auth] Metric, Auth, 핸들러

    세 런타임 모두 Metric, Auth, 핸들러 가 나왔습니다. 등록한 순서대로 도는 것은 지운 적이 없을 때뿐입니다.

    WithFilter 는 인터셉터가 아니라 자식 라우터를 만든다

    AddFilter 는 같은 라우터에 붙이고 void 를 돌려줍니다. 이름이 비슷한 WithFilter 는 자식 라우터를 새로 만들어 거기에 인터셉터를 붙이고, 부모의 자식 목록에 등록한 뒤 돌려줍니다. 부모로 발행하면 자식 전부에 퍼지고, 구독자가 없는 자식도 매 발행마다 들릅니다.

    부모 발행 1회 .NET 10 Mono IL2CPP
    자식 0개 30.3 ns 83.5 ns 57.0 ns
    자식 16개 1,060.3 ns 2,942.2 ns 2,631.3 ns
    자식 1개당 약 64 ns 약 179 ns 약 161 ns

    자식은 만들어지는 순간 부모의 인터셉터 목록을 복사해 둡니다. 그 뒤에 부모에 붙인 인터셉터는 자식으로 직접 발행할 때 돌지 않습니다.

    주의할 점

    1. next 는 한 번만 부른다

    안 지키면 생기는 문제. 예외가 나면 next 를 다시 부르는 재시도 인터셉터는 두 번째에 뒤 인터셉터를 건너뛰고 구독자만 다시 부릅니다. 뒤에 권한 확인이나 순서 제어가 있었다면 재시도 때는 빠지고, 데미지 커맨드라면 데미지가 두 번 들어갑니다.

    이렇게 쓴다. 인터셉터에서는 next 를 한 번만 부르고, 재시도가 필요하면 구독자 안에서 처리합니다.

    2. 실행 중에 인터셉터를 지웠다 넣지 않는다

    안 지키면 생기는 문제. “로그를 찍은 다음 권한을 확인한다” 처럼 순서에 기댄 구성에서 로그 인터셉터를 한 번 껐다 켜면 앞 빈 칸에 들어가 순서가 바뀝니다.

    이렇게 쓴다. 순서가 중요한 인터셉터는 시작할 때 한 번에 등록합니다. 켜고 끌 일이 있으면 지우지 말고 인터셉터 안의 플래그로 next 만 부르고 지나가게 합니다.

    3. WithFilter 의 자식은 한 번 만들고 Dispose 한다

    안 지키면 생기는 문제. OnEnable 에서 router.WithFilter(log) 로 구독하고 자식을 버리지 않으면, 구독을 해제해도 자식은 부모 목록에 남습니다. 켰다 끌 때마다 하나씩 쌓여 16번이면 부모 발행 한 번이 유니티에서 2.6~2.9 µs 가 됩니다.

    이렇게 쓴다. WithFilter 의 결과는 필드에 한 번만 만들어 두고, 주인이 사라질 때 Dispose 합니다. 부모의 인터셉터는 자식을 만들기 전에 모두 붙입니다.

    정리

    VitalRouter 인터셉터의 next 는 모두가 나눠 쓰는 델리게이트 하나이고, 커서 int 하나가 다음을 고릅니다.

    • 체인을 위해 새로 만드는 객체가 없지만, next 를 두 번 부르면 두 번째는 뒤 인터셉터를 건너뜁니다.
    • 인터셉터 목록은 앞 빈 칸부터 다시 써서, 지웠다 넣은 인터셉터가 앞에 끼어듭니다.
    • WithFilter 는 자식 라우터를 만들어 부모에 등록하고, 자식 하나가 부모 발행마다 유니티에서 160~180 ns 를 더합니다.