---
title: "VContainer 는 스코프의 Awake 안에서 컨테이너를 세운다"
description: "등록 두 줄을 적으면 진입점이 불리지만 언제 불리는지 모르면 유니티 Start 와 섞어 씁니다. VContainer 는 스코프의 Awake 안에서 등록을 모아 빌드하고, 진입점은 다음 프레임 플레이어 루프에서 부릅니다."
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-first/
---
```csharp
builder.Register<IGreeter, ConsoleGreeter>(Lifetime.Singleton);
builder.RegisterEntryPoint<GameEntryPoint>();   // Start() 는 언제 불릴까
```

등록 두 줄을 적고 실행하면 로그가 찍힙니다. 그 사이에 무엇이 지나가는지 모르면 `IStartable.Start()` 를 유니티의 `Start` 와 같은 것으로 읽고, 주입이 끝나기 전의 값을 씁니다.

VContainer 는 **씬에 놓인 스코프(모노비헤이비어)의 `Awake` 안에서 등록을 모으고 컨테이너를 빌드하고, 진입점은 다음 프레임에 플레이어 루프에서 부릅니다.** 기준 판본은 1.19.0 입니다.

## 등장인물은 스코프, 빌더, `Resolver` 셋이다

| 이름 | 정체 | 역할 |
| --- | --- | --- |
| `LifetimeScope` | 모노비헤이비어 | 씬에 놓는 진입점. 무엇을 등록할지 선언하고 컨테이너를 소유합니다 |
| `IContainerBuilder` | 순수 C# | `Configure()` 에 넘어오는 등록 수집기 |
| `IObjectResolver` | 순수 C# | 빌드 결과물. 실제로 객체를 만들어 주는 `Resolver` |

스코프가 모노비헤이비어라서 **씬과 함께 죽으면서 자기가 들고 있던 컨테이너를 해제합니다.**

## 최소 예제는 등록 두 줄이다

진입점은 모노비헤이비어가 아니라 순수 C# 클래스입니다.

```csharp
public class GameEntryPoint : IStartable
{
    readonly IGreeter greeter;
    public GameEntryPoint(IGreeter greeter) => this.greeter = greeter;   // 생성자 주입
    public void Start() => greeter.Greet("VContainer");
}

public class GameLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<IGreeter, ConsoleGreeter>(Lifetime.Singleton);
        builder.RegisterEntryPoint<GameEntryPoint>();
    }
}
```

빈 게임오브젝트에 `GameLifetimeScope` 를 붙이고 실행하면 콘솔에 로그가 찍힙니다.

## 진입점과 모노비헤이비어는 이 순서로 불린다

스코프의 `Awake` 는 실행 순서가 `-5000` 으로 박혀 있어 다른 스크립트보다 먼저 돕니다. 그 안에서 `Configure` 로 등록을 모으고, 빌드하면서 순환 참조를 잡고, 진입점 디스패처를 플레이어 루프에 꽂습니다. 그 뒤로는 진입점과 모노비헤이비어 콜백이 번갈아 불립니다.

공식 문서(Entry point)는 진입점마다 "`MonoBehaviour.Start()` 와 거의 같은 때" 처럼 적습니다. 앞인지 뒤인지는 소스(`PlayerLoopHelper`)가 정합니다. 디스패처는 유니티가 모노비헤이비어 콜백을 부르는 단계 **바로 앞**에 진입점을, 그 그룹 **맨 끝**에 Post 를 꽂습니다.

<x-wiring>
<script type="application/json">
{
  "label": "스코프 Awake 부터 한 프레임까지 불리는 순서",
  "width": 620,
  "height": 1326,
  "nodes": [
    {
      "id": "awake",
      "title": "스코프 Awake",
      "sub": "실행 순서 -5000",
      "x": 160,
      "y": 8,
      "w": 300,
      "h": 50
    },
    {
      "id": "init",
      "title": "Initialize · PostInitialize",
      "sub": "빌드 직후, Awake 안",
      "x": 160,
      "y": 98,
      "w": 300,
      "h": 50
    },
    {
      "id": "oawake",
      "title": "다른 스크립트 Awake",
      "sub": "OnEnable 포함",
      "x": 160,
      "y": 188,
      "w": 300,
      "h": 50,
      "aside": true
    },
    {
      "id": "start",
      "title": "IStartable.Start",
      "sub": "EarlyUpdate",
      "x": 160,
      "y": 278,
      "w": 300,
      "h": 50
    },
    {
      "id": "mstart",
      "title": "Start()",
      "sub": "모노비헤이비어",
      "x": 160,
      "y": 368,
      "w": 300,
      "h": 50,
      "aside": true
    },
    {
      "id": "pstart",
      "title": "IPostStartable.PostStart",
      "sub": "EarlyUpdate 끝",
      "x": 160,
      "y": 458,
      "w": 300,
      "h": 50
    },
    {
      "id": "ftick",
      "title": "IFixedTickable",
      "sub": "FixedUpdate 그룹",
      "x": 160,
      "y": 548,
      "w": 300,
      "h": 50
    },
    {
      "id": "mfixed",
      "title": "FixedUpdate()",
      "sub": "모노비헤이비어",
      "x": 160,
      "y": 638,
      "w": 300,
      "h": 50,
      "aside": true
    },
    {
      "id": "pftick",
      "title": "IPostFixedTickable",
      "sub": "그룹 끝",
      "x": 160,
      "y": 728,
      "w": 300,
      "h": 50
    },
    {
      "id": "tick",
      "title": "ITickable.Tick",
      "sub": "Update 그룹",
      "x": 160,
      "y": 818,
      "w": 300,
      "h": 50
    },
    {
      "id": "mupdate",
      "title": "Update()",
      "sub": "모노비헤이비어",
      "x": 160,
      "y": 908,
      "w": 300,
      "h": 50,
      "aside": true
    },
    {
      "id": "ptick",
      "title": "IPostTickable.PostTick",
      "sub": "그룹 끝",
      "x": 160,
      "y": 998,
      "w": 300,
      "h": 50
    },
    {
      "id": "ltick",
      "title": "ILateTickable",
      "sub": "PreLateUpdate 그룹",
      "x": 160,
      "y": 1088,
      "w": 300,
      "h": 50
    },
    {
      "id": "mlate",
      "title": "LateUpdate()",
      "sub": "모노비헤이비어",
      "x": 160,
      "y": 1178,
      "w": 300,
      "h": 50,
      "aside": true
    },
    {
      "id": "pltick",
      "title": "IPostLateTickable",
      "sub": "그룹 끝",
      "x": 160,
      "y": 1268,
      "w": 300,
      "h": 50
    }
  ],
  "edges": [
    {
      "from": "awake",
      "to": "init",
      "bend": 0
    },
    {
      "from": "init",
      "to": "oawake",
      "bend": 0
    },
    {
      "from": "oawake",
      "to": "start",
      "bend": 0,
      "label": "첫 프레임"
    },
    {
      "from": "start",
      "to": "mstart",
      "bend": 0
    },
    {
      "from": "mstart",
      "to": "pstart",
      "bend": 0
    },
    {
      "from": "pstart",
      "to": "ftick",
      "bend": 0,
      "label": "매 프레임"
    },
    {
      "from": "ftick",
      "to": "mfixed",
      "bend": 0
    },
    {
      "from": "mfixed",
      "to": "pftick",
      "bend": 0
    },
    {
      "from": "pftick",
      "to": "tick",
      "bend": 0
    },
    {
      "from": "tick",
      "to": "mupdate",
      "bend": 0
    },
    {
      "from": "mupdate",
      "to": "ptick",
      "bend": 0
    },
    {
      "from": "ptick",
      "to": "ltick",
      "bend": 0
    },
    {
      "from": "ltick",
      "to": "mlate",
      "bend": 0
    },
    {
      "from": "mlate",
      "to": "pltick",
      "bend": 0
    }
  ],
  "steps": [
    {
      "node": "awake",
      "why": "씬이 로드되면 실행 순서가 -5000 인 스코프의 Awake 가 가장 먼저 돕니다. 그 안에서 Configure 로 등록을 모으고 빌드합니다."
    },
    {
      "edge": 0,
      "why": "빌드가 끝나자마자 같은 Awake 안에서 Initialize 와 PostInitialize 를 부릅니다. 플레이어 루프를 타지 않습니다."
    },
    {
      "edge": 1,
      "why": "그다음에야 다른 스크립트의 Awake 와 OnEnable 이 돕니다. 그래서 Initialize 에서는 다른 오브젝트가 아직 준비되지 않았습니다."
    },
    {
      "edge": 2,
      "why": "첫 프레임의 EarlyUpdate 입니다. 유니티가 Start 를 부르는 단계 바로 앞에 VContainer 가 꽂은 러너가 IStartable.Start 를 먼저 부릅니다."
    },
    {
      "edge": 3,
      "why": "그다음 유니티가 모노비헤이비어의 Start() 를 부릅니다."
    },
    {
      "edge": 4,
      "why": "EarlyUpdate 그룹 맨 끝의 러너가 PostStart 를 부릅니다. 모든 Start() 가 끝난 뒤입니다."
    },
    {
      "edge": 5,
      "why": "여기부터 매 프레임입니다. 물리 갱신에서는 FixedTick 이 FixedUpdate() 보다 먼저 돕니다."
    },
    {
      "edge": 6,
      "why": "그다음 모노비헤이비어의 FixedUpdate() 입니다."
    },
    {
      "edge": 7,
      "why": "그룹 맨 끝에서 PostFixedTick 이 돕니다."
    },
    {
      "edge": 8,
      "why": "Update 그룹에서는 Tick 이 먼저입니다."
    },
    {
      "edge": 9,
      "why": "그다음 모노비헤이비어의 Update() 와 코루틴이 돕니다."
    },
    {
      "edge": 10,
      "why": "그룹 맨 끝에서 PostTick 이 돕니다. 모든 Update() 가 끝난 값을 읽을 수 있습니다."
    },
    {
      "edge": 11,
      "why": "LateUpdate 도 같은 모양입니다. LateTick 이 먼저 돕니다."
    },
    {
      "edge": 12,
      "why": "그다음 모노비헤이비어의 LateUpdate() 입니다."
    },
    {
      "edge": 13,
      "why": "마지막으로 PostLateTick 이 돕니다. 다음 프레임은 다시 FixedTick 부터입니다."
    }
  ],
  "note": "진한 상자가 VContainer 가 부르는 진입점, 흐린 상자가 유니티가 부르는 모노비헤이비어 콜백입니다. 줄마다 진입점이 앞, Post 가 뒤입니다."
}
</script>
</x-wiring>

모노비헤이비어의 `Start` 에서 준비한 값을 읽어야 하면 `IPostStartable` 을, 모든 `Update` 가 끝난 값을 읽어야 하면 `IPostTickable` 을 씁니다.

## 주의할 점

### 1. `IStartable.Start()` 를 유니티의 `Start` 로 읽지 않는다

**안 지키면 생기는 문제.** 진입점은 게임오브젝트에 붙어 있지 않아서 유니티가 부르는 것이 아닙니다. 스코프가 빌드를 끝낸 뒤 디스패처가 플레이어 루프에 꽂은 자리에서 불립니다. 모노비헤이비어의 `Start` 와 같은 순서, 같은 조건으로 돈다고 가정하면 순서가 어긋납니다.

**이렇게 쓴다.** 위 흐름도를 기준으로 고릅니다. 모노비헤이비어의 `Start` 뒤여야 하면 `IPostStartable`, `Awake` 안에서 곧바로 끝내야 하면 `IInitializable` 입니다.

### 2. 진단은 개발 중에만 켠다

**안 지키면 생기는 문제.** 설정 자산의 `Enable Diagnostics` 를 켜면 등록마다 스택 추적을 뜨고, `Resolve` 마다 진단 정보를 선형으로 훑고, 만든 인스턴스를 붙들고 있습니다. `Singleton` `Resolve` 1회가 Mono 에서 약 36배(0.13 µs → 4.6 µs), IL2CPP 에서 약 11배(0.27 µs → 2.8 µs) 느려지고 `Resolve` 마다 약 500 B 를 할당했습니다. `Transient` 는 `Resolve` 를 거듭할수록 느려져 10,000번이면 Mono 약 19배, IL2CPP 약 31배였습니다. 켠 채로 누수를 재면 진단이 쥔 참조 때문에 결과가 왜곡되고, `Resolve` 시간은 밀리초 정수라 1밀리초 아래는 0으로 찍힙니다.

**이렇게 쓴다.** 진단 창(`Window > VContainer Diagnostics`)은 등록과 계층을 확인할 때만 켜고, 릴리스 전과 측정할 때는 설정 자산에서 끕니다.

## 정리

VContainer 는 **스코프의 `Awake` 안에서 컨테이너를 세우고, 진입점은 다음 프레임 플레이어 루프에서 부릅니다.**

- 스코프(모노비헤이비어), 빌더(수집), `Resolver`(생성) 셋이 등장인물이고, 스코프가 씬과 함께 죽으며 컨테이너를 해제합니다.
- `Awake`(순서 `-5000`) → `Configure` → 빌드 → `Initialize` 까지가 스코프 `Awake` 안이고, 다른 스크립트 `Awake` 뒤 첫 프레임에 `IStartable.Start()` → `Start()` → `PostStart()` 순입니다.
- 매 프레임은 `Tick()` → `Update()` → `PostTick()` 처럼 진입점이 모노비헤이비어 콜백 앞에, Post 가 뒤에 옵니다.
- 진단은 `Resolve` 를 수십 배 느리게 하고 인스턴스를 붙듭니다. 개발 중에만 켭니다.