---
title: "VitalRouter 는 자식 스코프에서도 라우터를 새로 만들지 않는다"
description: "씬 스코프에서 RegisterVitalRouter 로 붙인 로그 인터셉터가 씬을 다섯 번 불러오면 다섯 번 돕니다. 부모의 라우터를 공유하는데 스코프를 닫을 때 인터셉터는 떼지 않아서입니다. Isolated 를 켜면 쌓이지 않습니다."
author: "유니얼"
pubDate: 2026-09-22
updatedDate: 2026-09-25
category: "game-dev/unity/libraries"
tags: ["Unity", "DI", "라이브러리", "메시징"]
canonical: https://blog.unialgames.com/posts/unity-vitalrouter-vcontainer/
---
```csharp
public class BattleLifetimeScope : LifetimeScope      // 씬마다 만들어지는 자식 스코프
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterVitalRouter(routing =>
        {
            routing.Map<BattlePresenter>();
            routing.Filters.Add<LoggingInterceptor>();
        });
    }
}
```

씬 스코프에서 이렇게 등록하면 씬마다 라우터가 하나씩 생기고, 씬을 내리면 같이 사라질 것 같습니다. 그런데 씬을 몇 번 오가면 커맨드 하나에 로그가 여러 번 찍힙니다.

VitalRouter 의 VContainer 연동은 **부모 스코프에 `Router` 가 있으면 새로 만들지 않고 그것을 씁니다.** 그리고 스코프를 닫을 때 라우트만 떼고 인터셉터는 떼지 않아서, 자식이 붙인 인터셉터가 부모 라우터에 쌓입니다.

## 부모에 라우터가 있으면 그것을 쓴다

```csharp
// VitalRouter.Unity VContainerExtensions.cs (줄임)
if (routing.Isolated || !builder.Exists(typeof(Router), findParentScopes: true))
    builder.Register<Router>(Lifetime.Singleton).AsImplementedInterfaces().AsSelf();
```

| 조건 | 자식 스코프가 쓰는 라우터 |
| --- | --- |
| 부모에 `Router` 가 없다 | 이 스코프에 새로 만든다 |
| 부모에 있고 `Isolated = false` (기본) | **부모의 라우터를 그대로 쓴다** |
| `Isolated = true` | 이 스코프 전용 라우터를 새로 만든다 |

기본값이 공유라서 씬 스코프의 Presenter 들은 루트의 라우터 하나를 탑니다. 스코프를 나눴다고 라우터가 나뉘지는 않습니다. 컨테이너가 다 만들어진 뒤에는 그 라우터에 `AddFilter` 로 인터셉터를 붙이고, `Map<T>()` 한 클래스의 생성된 `MapTo` 를 리플렉션으로 찾아 부릅니다.

## 닫을 때 라우트만 떼고 인터셉터는 남긴다

```csharp
// 스코프가 만들어질 때
foreach (var interceptorType in routing.Filters.Types)
    router.AddFilter((ICommandInterceptor)container.Resolve(interceptorType));

// 스코프가 닫힐 때
builder.RegisterDisposeCallback(container =>
{
    for (var i = 0; i < routes.Count; i++)
        routes[i].UnmapRoutesMethod.Invoke(instance, null);   // 라우트만 뗀다. RemoveFilter 가 없다
});
```

붙이는 쪽과 떼는 쪽이 짝이 맞지 않습니다. `routing.Ordering` 도 결국 순서 제어 인터셉터를 붙이는 것이라 같습니다.

<x-flow>
<script type="application/json">
{
  "label": "Isolated 가 아닌 자식 스코프를 씬처럼 여러 번 만들고 닫을 때",
  "lines": ["[스코프 생성] router = 부모의 Router", "[스코프 생성] router.AddFilter(LoggingInterceptor)", "[스코프 생성] presenter.MapTo(router)", "[스코프 파기] presenter.UnmapRoutes()", "[발행] 부모 라우터로 커맨드 발행"],
  "cases": [
    {
      "label": "씬을 세 번 불러올 때",
      "state": "부모 스코프에 Router 가 있다. 자식 스코프가 routing.Filters.Add<LoggingInterceptor>() 를 한다",
      "path": [
        { "line": 0, "why": "첫 로드. 부모 라우터를 받습니다." },
        { "line": 1, "why": "인터셉터가 붙습니다. 부모 라우터의 인터셉터 1개." },
        { "line": 2, "why": "라우트를 붙입니다." },
        { "line": 3, "why": "씬을 내립니다. 라우트만 떨어지고 인터셉터는 남습니다." },
        { "line": 0, "why": "둘째 로드. 같은 부모 라우터입니다." },
        { "line": 1, "why": "또 붙습니다. 인터셉터 2개." },
        { "line": 3, "why": "내립니다. 여전히 2개." },
        { "line": 1, "why": "셋째 로드. 인터셉터 3개." },
        { "line": 4, "why": "이제 커맨드 하나를 발행하면 로깅 인터셉터가 세 번 돕니다.", "out": "로드 횟수만큼 쌓임" }
      ]
    }
  ],
  "note": "스코프를 Dispose 해도 줄지 않습니다. 씬을 오래 오갈수록 발행 한 번이 무거워집니다."
}
</script>
</x-flow>

자식 스코프를 다섯 번 만들고 닫으며, 닫은 뒤 발행 한 번에 인터셉터가 돈 횟수를 셌습니다.

| 몇 번째 로드 | 1 | 2 | 3 | 4 | 5 |
| --- | --- | --- | --- | --- | --- |
| 인터셉터가 돈 횟수 | 1 | 2 | 3 | 4 | 5 |

Mono 와 IL2CPP 플레이어가 같았습니다. 닫힌 스코프가 만든 인스턴스를 계속 부르지만 예외는 나지 않아서, 깨지지 않고 조용히 쌓입니다.

## Isolated 를 켜면 부모에 아무것도 붙지 않는다

`routing.Isolated = true` 를 켜면 이 스코프 전용 라우터를 새로 만들어 거기에 붙입니다. 같은 구성으로 세 번 불러온 뒤 부모로 발행하니 인터셉터가 한 번도 돌지 않았습니다.

```csharp
builder.RegisterVitalRouter(routing =>
{
    routing.Isolated = true;
    routing.Map<BattlePresenter>();
    routing.Filters.Add<LoggingInterceptor>();
});
```

## 주의할 점

### 1. 씬 스코프에서 인터셉터나 순서 제어를 쓰면 Isolated 를 켠다

**안 지키면 생기는 문제.** 다섯 번 불러오면 발행 한 번에 로그가 다섯 번 찍히고, 권한 확인을 다섯 번 하고, 순서 제어가 다섯 겹이 됩니다. 스코프를 닫아도 줄지 않으니 오래 플레이할수록 발행이 무거워집니다.

**이렇게 쓴다.** 셋 중 하나를 고릅니다. 씬 스코프에서 `routing.Isolated = true` 를 켜거나, 부모 라우터를 꼭 공유해야 하면 스코프를 닫을 때 직접 `router.RemoveFilter(...)` 를 부르거나, 인터셉터는 루트 스코프에서 한 번만 등록하고 자식 스코프는 `Map` 만 합니다.

### 2. Isolated 로 나누면 커맨드를 어느 라우터로 보낼지 같이 정한다

**안 지키면 생기는 문제.** `Isolated` 스코프의 Presenter 는 그 스코프 전용 라우터에 연결됩니다. 부모에서 `Router` 를 주입받아 발행하던 코드는 이제 그 Presenter 에 닿지 않고, 씬 안에서 발행한 커맨드도 부모 쪽 구독자에게 가지 않습니다.

**이렇게 쓴다.** 씬 안에서만 오가는 커맨드와 씬을 넘나드는 커맨드를 나누고, 넘나드는 커맨드는 부모 라우터를 따로 주입받아 발행합니다.

### 3. 씬에 있는 MonoBehaviour 는 Map 이 아니라 MapComponent 로 연결한다

**안 지키면 생기는 문제.** `Map<T>()` 에 `MonoBehaviour` 를 넘기면 씬의 컴포넌트가 아니라 새 GameObject 에 붙인 것이 만들어져 라우터에 연결됩니다. 씬에 배치해 둔 Presenter 는 커맨드를 받지 못하고 보이지 않는 복제본이 대신 받습니다. `[Routes]` 를 빠뜨린 클래스를 `Map` 하면 컴파일은 되고 스코프를 만드는 순간 `MapTo` 가 없다는 예외가 납니다.

**이렇게 쓴다.** 씬의 오브젝트는 `MapComponent(instance)` 나 `MapComponentInHierarchy<T>()` 로 연결하고, 연결하는 클래스는 `[Routes]` 를 붙인 `partial class` 로 만듭니다.

## 정리

VitalRouter 의 VContainer 연동은 **자식 스코프에서도 부모의 라우터를 공유하고, 닫을 때 인터셉터를 떼지 않습니다.**

- `Isolated` 가 아니면 부모에 `Router` 가 있는 한 새로 만들지 않습니다.
- 자식이 붙인 인터셉터는 스코프를 닫아도 남아, 씬을 불러온 횟수만큼 발행마다 돕니다.
- `Isolated = true` 면 전용 라우터에 붙어 부모에는 아무것도 남지 않고, 대신 커맨드가 갈 라우터를 나눠 정해야 합니다.