---
title: "VContainer 의 CreateScope 스코프는 씬이 바뀌어도 닫히지 않는다"
description: "자식 스코프는 CreateChild 로 만들면 게임오브젝트라 씬과 함께 닫히고, CreateScope 로 만들면 순수 C# 객체라 씬이 바뀌어도 부모를 닫아도 살아 있습니다. 씬 단위는 CreateChild, 직접 관리할 것만 CreateScope 로 만듭니다."
author: "유니얼"
pubDate: 2026-09-16
updatedDate: 2026-09-30
category: "game-dev/unity/libraries"
tags: ["Unity", "DI", "라이브러리"]
canonical: https://blog.unialgames.com/posts/unity-vcontainer-scope/
---
```csharp
var child = container.CreateScope(builder =>
{
    builder.Register<StageData>(Lifetime.Scoped);
});

SceneManager.LoadScene("Title");
```

씬을 갈아엎었으니 `StageData` 도 정리됐을 것 같습니다. 재 보면 아닙니다.

| 시점 | `StageData` 해제됐나 | 비고 |
| --- | --- | --- |
| 씬 전환 뒤 | 아니오 | 다시 `Resolve` 하면 같은 인스턴스, 지금까지 만든 수 1 |
| `child.Dispose()` 뒤 | 예 | 만든 쪽이 직접 닫아야 해제된다 |
| `parent.Dispose()` 뒤 (child 는 안 닫음) | 아니오 | 부모를 닫아도 `CreateScope` 로 만든 자식은 같이 안 닫힌다 |

**`CreateScope` 로 만든 스코프는 씬이 바뀌어도 그대로 살아 있고, 그 안의 객체도 해제되지 않습니다.** 누군가 `Dispose()` 를 부를 때까지 갑니다.

자식 스코프를 만드는 방법은 둘이고, 차이는 **스코프가 게임오브젝트냐 아니냐** 하나입니다. VContainer 의 해제는 스코프의 게임오브젝트가 파괴되는 데서 시작하기 때문입니다.

| | `CreateChild` (게임오브젝트 스코프) | `container.CreateScope` (C# 스코프) |
| --- | --- | --- |
| 정체 | 게임오브젝트에 붙은 `LifetimeScope` | 순수 C# 객체 |
| 씬이 바뀌면 | 게임오브젝트와 함께 파괴되고, 안의 객체가 해제된다 | **그대로 살아 있다** |
| 부모 스코프가 없어지면 | 부모 게임오브젝트 밑에 붙어 있어 같이 파괴된다 | **살아 있다** |
| 누가 닫나 | 유니티 (게임오브젝트 파괴) | 만든 코드가 `Dispose()` |
| 이럴 때 쓴다 | 씬, 전투처럼 유니티 오브젝트와 같이 사라질 것 | 씬과 상관없이 직접 열고 닫는 작업 단위 (`using` 으로 감쌀 수 있는 것) |

정리하면, **씬이나 전투 단위의 스코프는 `CreateChild` 로 만들고, `CreateScope` 는 닫는 코드를 직접 쓸 때만 씁니다.**

## 계층은 수명이 같은 것끼리 묶는다

자식은 부모의 등록을 보고, 부모는 자식을 못 봅니다. 그래서 계층은 **수명이 같은 것끼리** 묶습니다.

<x-wiring>
<script type="application/json">
{
  "label": "수명이 같은 것끼리 묶은 스코프 세 층",
  "width": 620,
  "height": 294,
  "nodes": [
    {
      "id": "root",
      "title": "루트 스코프",
      "sub": "DontDestroyOnLoad · IAudioService, ISaveService (Singleton)",
      "x": 50,
      "y": 8,
      "w": 520,
      "h": 58
    },
    {
      "id": "scene",
      "title": "씬 스코프",
      "sub": "씬마다 하나 · IStageRepository (Scoped), HudPresenter (EntryPoint)",
      "x": 50,
      "y": 118,
      "w": 520,
      "h": 58
    },
    {
      "id": "child",
      "title": "전투 스코프",
      "sub": "전투 시작에 만들고 끝에 버림 · StageData (Instance), IBattleSession (Scoped)",
      "x": 50,
      "y": 228,
      "w": 520,
      "h": 58
    }
  ],
  "edges": [
    {
      "from": "root",
      "to": "scene",
      "label": "부모",
      "bend": 0
    },
    {
      "from": "scene",
      "to": "child",
      "label": "부모",
      "bend": 0
    }
  ],
  "steps": [
    {
      "node": "root",
      "why": "게임 내내 사는 것만 둡니다. 씬을 넘겨도 살아야 하니 DontDestroyOnLoad 입니다."
    },
    {
      "edge": 0,
      "why": "씬마다 하나 서는 스코프입니다. 루트의 등록을 볼 수 있고, 씬이 내려가면 같이 닫힙니다."
    },
    {
      "edge": 1,
      "why": "전투 한 판 동안만 사는 것을 둡니다. 전투가 끝나면 이 스코프만 닫습니다."
    }
  ],
  "note": "아래 층은 위 층의 등록을 보고, 위 층은 아래 층을 못 봅니다."
}
</script>
</x-wiring>

깊이는 셋이면 충분합니다. 더 깊으면 조회가 계층 수만큼 위로 올라가고, 무엇이 어디 등록됐는지 사람이 못 따라갑니다.

## CreateChild 는 게임오브젝트, CreateScope 는 순수 C# 객체다

자식 스코프를 만드는 방법이 둘이고, 죽는 조건이 다릅니다.

<x-compare>
<script type="application/json">
{
  "label": "자식 스코프를 만드는 두 가지",
  "tabs": [
    { "label": "CreateChild", "code": "public TScope CreateChild<TScope>(IInstaller installer = null, string childScopeName = null)\n    where TScope : LifetimeScope\n{\n    var childGameObject = new GameObject(childScopeName ?? \"LifetimeScope (Child)\");\n    childGameObject.SetActive(false);\n    childGameObject.transform.SetParent(transform, false);\n\n    var child = childGameObject.AddComponent<TScope>();\n    if (installer != null)\n        child.localExtraInstallers.Add(installer);\n    child.parentReference.Object = this;\n    childGameObject.SetActive(true);\n    return child;\n}", "text": "게임오브젝트를 새로 만들어 자기 밑에 붙입니다. 부모가 파괴되면 유니티가 같이 파괴하고, OnDestroy 에서 컨테이너가 해제됩니다. 씬 전환이 정리해 주는 쪽입니다. 비활성으로 만들었다가 인스톨러를 넣고 켜는 것은 Awake 가 먼저 돌아 버리는 것을 막기 위해서입니다.", "tone": "good" },
    { "label": "container.CreateScope", "code": "public IScopedObjectResolver CreateScope(Action<IContainerBuilder> installation = null)\n{\n    var containerBuilder = new ScopedContainerBuilder(Root, this) { ApplicationOrigin = ApplicationOrigin };\n    installation?.Invoke(containerBuilder);\n    return containerBuilder.BuildScope();\n}", "text": "만들어서 돌려주고 끝입니다. 부모가 자식을 disposables 에 넣지 않습니다. 게임오브젝트가 아니라 순수 C# 객체이고, 씬 전환은 게임오브젝트만 정리합니다. 만든 사람이 직접 Dispose() 를 불러야 합니다.", "tone": "bad" }
  ]
}
</script>
</x-compare>

`CreateScope` 가 잘못된 것은 아닙니다. 씬과 무관한 수명을 직접 관리할 때 쓰라고 있는 것이고, **관리 책임이 호출부로 넘어온다**는 뜻입니다.

## 씬 전환의 해제는 게임오브젝트 파괴를 타고 내려간다

<x-wiring>
<script type="application/json">
{
  "label": "씬 전환에서 해제가 전파되는 길",
  "width": 720,
  "height": 560,
  "nodes": [
    { "id": "unload", "title": "씬 언로드", "sub": "SceneManager", "x": 120, "y": 8, "w": 180, "h": 54 },
    { "id": "go", "title": "씬 스코프 파괴", "sub": "게임오브젝트가 사라진다", "x": 120, "y": 128, "w": 180, "h": 54 },
    { "id": "od", "title": "OnDestroy", "sub": "DisposeCore()", "x": 120, "y": 248, "w": 180, "h": 54 },
    { "id": "cont", "title": "Container.Dispose", "sub": "안쪽 스코프부터", "x": 120, "y": 368, "w": 180, "h": 54 },
    { "id": "sc", "title": "Scoped 객체 해제", "sub": "생성 역순", "x": 120, "y": 488, "w": 180, "h": 54 },
    { "id": "orphan", "title": "CreateScope 로 만든 스코프", "sub": "순수 C# 객체", "x": 470, "y": 248, "w": 220, "h": 54, "aside": true }
  ],
  "edges": [
    { "from": "unload", "to": "go", "label": "유니티가 한다", "bend": 62 },
    { "from": "go", "to": "od", "label": "MonoBehaviour 콜백", "bend": 62 },
    { "from": "od", "to": "cont", "label": "컨테이너를 닫는다", "bend": 62 },
    { "from": "cont", "to": "sc", "label": "스택에서 하나씩", "bend": 62 }
  ],
  "steps": [
    { "edge": 0, "why": "씬이 내려가면 그 씬의 게임오브젝트가 파괴됩니다. 루트 스코프는 DontDestroyOnLoad 라 여기 안 걸립니다." },
    { "edge": 1, "why": "스코프가 MonoBehaviour 라서 OnDestroy 가 옵니다. 해제가 여기서 시작됩니다." },
    { "edge": 2, "why": "컨테이너를 닫고 참조를 널로 만듭니다. 해제 뒤에 쓰면 조용히 도는 대신 바로 터집니다." },
    { "edge": 3, "why": "등록해 둔 해제 대상을 생성 역순으로 꺼내 닫습니다." },
    { "node": "orphan", "why": "이 체인 어디에도 안 걸립니다. 게임오브젝트가 아니라서 파괴 대상이 아니고, 부모가 소유 목록에 넣지도 않았습니다." }
  ],
  "note": "해제는 게임오브젝트가 죽는 것에서 시작합니다. 게임오브젝트가 아닌 것은 시작점이 없습니다."
}
</script>
</x-wiring>

스코프가 `MonoBehaviour` 라 `OnDestroy` 에서 `DisposeCore()` 가 돌고, 컨테이너가 안쪽 스코프부터 해제 목록을 비웁니다. 루트 스코프는 `DontDestroyOnLoad` 라 씬 전환에 걸리지 않습니다. **게임오브젝트가 아닌 것은 이 체인의 시작점이 없습니다.**

## 주의할 점

### 1. 전부 루트에 등록하지 않는다

**안 지키면 생기는 문제.** 루트는 씬 전환에 안 걸리므로 씬을 넘겨도 상태가 남습니다. 컨테이너를 쓰는 전역 싱글턴 모음이 되고, 씬을 다시 열어도 지난 판의 값으로 시작합니다.

**이렇게 쓴다.** 앱 내내 살 것은 루트에, 씬마다 초기화할 것은 씬 스코프에, 세션마다 새로 시작할 것은 그 아래에 둡니다.

### 2. `CreateScope` 로 만든 스코프는 만든 쪽이 닫는다

**안 지키면 생기는 문제.** 게임오브젝트가 아니고 부모의 해제 목록에도 없어서, 씬 전환도 부모 해제도 닫아 주지 않습니다. 전투마다 새로 만들면 스코프가 쌓이고, 그 안의 진입점이 쌓인 만큼 겹쳐 돕니다.

**이렇게 쓴다.** 참조를 들고 있다가 다음 것을 만들기 전에 `Dispose()` 를 부르거나 `using` 으로 감쌉니다. 씬과 같이 죽어야 하면 `CreateChild` 를 씁니다.

### 3. 다음 씬 스코프에 넘기는 `Enqueue` 는 `using` 으로 감싼다

**안 지키면 생기는 문제.** 다음 씬의 스코프에 등록을 예약하는 `LifetimeScope.Enqueue` 는 정적 변수 없이 값을 넘기는 가장 안전한 길이지만, 예약이 안 걷히면 그 뒤에 만들어지는 모든 스코프에 같은 등록이 따라 붙습니다.

**이렇게 쓴다.** 씬 로드 한 번을 `using` 블록 하나로 감쌉니다. 블록을 벗어나는 순간 예약이 걷힙니다.

```csharp
using (LifetimeScope.Enqueue(builder => builder.RegisterInstance(stage)))
{
    await SceneManager.LoadSceneAsync("Battle");
}
```

## 정리

VContainer 스코프는 **게임오브젝트일 때만 씬과 함께 닫힙니다.**

- 자식은 부모를 보고 부모는 자식을 못 보므로, 수명이 같은 것끼리 셋 이내의 계층으로 묶습니다.
- `CreateChild` 는 스코프 밑에 게임오브젝트를 만들어 같이 파괴되고, `CreateScope` 는 부모가 죽어도 삽니다.
- 해제는 `OnDestroy` 에서 시작하므로, 순수 C# 스코프는 `Dispose()` 나 `using` 으로 직접 닫습니다.