VitalRouter 의 Sequential 은 다른 클래스와도 줄을 선다
Esc

    VitalRouter 의 Sequential 은 다른 클래스와도 줄을 선다

    2026년 9월 30일 수정–Unity라이브러리메시징동시성
    [Routes(CommandOrdering.Sequential)]
    public partial class DialogPresenter { ... }      // 대사만 차례로 보이려 했는데
    
    [Routes(CommandOrdering.Sequential)]
    public partial class InventoryPresenter { ... }   // 이쪽 처리가 끝나야 대사가 나온다

    대사를 보여 주는 중에 다음 대사 버튼이 또 눌리는 식으로, 게임 UI 에서는 커맨드가 겹칩니다. C# event 라면 이 겹침을 코루틴이나 상태 머신으로 직접 막아야 하는데, VitalRouter 는 이것을 CommandOrdering 속성 하나로 다룹니다.

    VitalRouter 의 순서 제어는 여러 발행을 줄 세우는 평범한 인터셉터입니다. 그래서 줄을 같이 서는 범위는 속성을 붙인 자리가 아니라 그 인터셉터 인스턴스를 나눠 갖는 범위입니다.

    줄 세우는 대상은 구독자가 아니라 발행이다

    한 번의 발행에서 비동기 구독자는 발행 루프가 전부 시작합니다. CommandOrdering 은 동시에 발행된 커맨드들 사이를 다룹니다. 1.x 시절 자료의 “구독자 H1 이 끝나면 H2” 그림은 2.8.0 과 다릅니다.

    값 구현 앞 커맨드를 처리하는 중에 새 커맨드가 오면
    Parallel (기본) 아무것도 안 붙임 같이 돈다
    Sequential 비동기 락 앞 것이 끝날 때까지 기다린다
    Drop 실행 중 표시를 CAS 새 것을 예외 없이 버린다
    Switch 앞 토큰 취소 앞 것의 취소 토큰을 취소하고 새 것을 처리한다

    100 ms 기다리는 핸들러에 첫 발행을 보내고 20 ms 뒤 두 번째를 보냈습니다. 세 런타임이 같았습니다.

    순서 제어 로그
    Drop 시작(1), 끝(1)
    Switch 시작(1), 시작(2), 취소됨(1), 끝(2)
    Sequential 시작(1), 끝(1), 시작(2), 끝(2)

    줄의 범위는 인스턴스를 나눠 갖는 범위다

    거는 방법 줄을 같이 서는 범위
    router.AddFilter(Sequential), VContainer 의 routing.Ordering 그 라우터로 가는 모든 커맨드 타입
    SubscribeAwait(cb, Sequential) 그 구독 하나 (구독마다 새 인스턴스)
    [Route(Sequential)] 여러 개 + 직접 MapTo(router, new SequentialOrdering()) 그 클래스의 Sequential 라우트 전부
    [Routes(Sequential)] + VContainer 컨테이너 안에서 같은 순서 제어를 쓰는 모든 클래스

    마지막 줄이 이름과 어긋납니다. 소스 제너레이터는 순서 지정을 MapTo 의 인터셉터 인자로 바꾸고, VContainer 연동은 그 인자 타입을 타입마다 싱글턴 하나로 등록합니다. [Routes(Sequential)] 클래스가 몇 개든 같은 락을 받습니다.

    IL2CPP 플레이어에서 PresenterA, PresenterB 를 각각 [Routes(CommandOrdering.Sequential)] 로 만들고, 같은 커맨드를 받아 60 ms 기다리게 해서 VContainer 에 등록했습니다.

    시각 일어난 일
    5 ms PresenterB 진입
    65 ms PresenterB 끝, PresenterA 진입
    125 ms PresenterA 끝 (발행 한 번에 120 ms)

    컨테이너에서 SequentialOrdering 을 두 번 꺼내 비교하니 같은 인스턴스였습니다.

    다른 클래스인데 60 ms 가 아니라 120 ms 였습니다. Mono 플레이어에서도 겹친 구간이 없었습니다.

    Sequential 핸들러가 같은 라우터로 await 발행하면 멈춘다

    락은 재진입을 허용하지 않습니다. 락을 쥔 핸들러가 같은 라우터에 발행하고 기다리면 서로를 기다립니다.

    Mono 와 IL2CPP 빌드 모두 재발행 시도까지 찍히고 3초가 지나도 끝나지 않았습니다.

    주의할 점

    1. 줄을 따로 세울 곳은 인스턴스나 라우터를 나눈다

    안 지키면 생기는 문제. 대사를 차례로 보이려고 라우터 전체에 Sequential 을 걸면 Move, Attack 까지 앞 커맨드를 기다려, 대사 연출 중에 이동 입력이 먹지 않습니다. [Routes(Sequential)] 를 VContainer 로 등록하면 대사 Presenter 가 인벤토리 Presenter 의 처리를 기다립니다.

    이렇게 쓴다. 차례가 필요한 구독에만 SubscribeAwait(cb, CommandOrdering.Sequential) 로 겁니다. 클래스 단위로 따로 세우려면 MapTo(router, new SequentialOrdering()) 로 인스턴스를 직접 넘기거나, 대사 전용 라우터를 둡니다.

    2. Sequential 핸들러 안에서 같은 라우터로 await 발행하지 않는다

    안 지키면 생기는 문제. 두 발행이 서로를 기다리며 예외도 로그도 없이 멈춥니다. 커맨드 처리 중 다른 커맨드를 발행하는 일은 흔해서, 라우터 전체에 Sequential 을 걸었다면 어느 핸들러에서든 걸립니다.

    이렇게 쓴다. 안쪽 발행은 다른 라우터로 보내고 기다립니다. 기다리지 않는 router.Publish(cmd) 로 보낼 수도 있지만, 돌려받은 ValueTask 를 버리는 메서드라 안쪽 비동기 구독자의 실패는 어디에도 오지 않습니다.

    3. Drop 은 버려져도 되는 커맨드에만 쓴다

    안 지키면 생기는 문제. Drop 은 처리 중에 온 커맨드를 예외도 로그도 없이 성공으로 끝냅니다. 발행한 쪽은 처리됐다고 알고 넘어가니, 연타 방지로 건 Drop 에 저장이나 보상 지급 커맨드가 섞이면 조용히 사라집니다.

    이렇게 쓴다. 버튼 연타처럼 버려도 결과가 같은 커맨드에만 겁니다. 반드시 처리돼야 하는 커맨드는 Sequential 로 줄을 세웁니다.

    4. Switch 는 토큰을 따르는 핸들러에, 메인 스레드에서만 쓴다

    안 지키면 생기는 문제. Switch 는 앞 실행의 토큰을 취소할 뿐 멈추지 않아서, 핸들러가 토큰을 보지 않으면 두 이동이 겹쳐 돕니다. 발행마다 토큰 소스 둘을 만들어 유니티에서 528 B 를 쓰고, 이 교체에 락이 없어 두 스레드에서 동시에 발행하니 ObjectDisposedException 이 났습니다(IL2CPP 66.3%).

    이렇게 쓴다. 핸들러 안의 await 에 ctx.CancellationToken 을 넘깁니다. 새 목적지 지정처럼 가끔 오는 커맨드를 메인 스레드에서만 발행하고, 매 프레임 커맨드에는 쓰지 않습니다.

    정리

    VitalRouter 의 순서 제어는 발행 사이를 줄 세우는 인터셉터이고, 줄의 범위는 그 인스턴스를 나눠 갖는 범위입니다.

    • Sequential, Drop, Switch 는 모두 평범한 인터셉터이고, 한 번의 발행 안의 구독자 순서와는 상관없습니다.
    • 라우터에 걸면 모든 커맨드 타입이, [Routes(Sequential)] 를 VContainer 로 등록하면 다른 클래스끼리도 한 줄로 섭니다(60 ms 두 개가 120 ms).
    • Sequential 락은 재진입을 허용하지 않아, 핸들러 안에서 같은 라우터로 await 발행하면 멈춥니다.