[VContainer] Unity와 Dependency Injection
Unity의 의존성 주입(DI)과 VContainer 에셋 소개
2025.06.21
1 | 개요
이번에 이직하게된 DEVSISTERS의 채용공고를 보니, 팀에서 VContainer라는 플러그인을 사용하는 것 같았다. 물론 공고에 따르면 입사 후 배워 나갈 수 있는 것으로 보였지만, 그래도 미리 공부를 해두면 배울 때 훨씬 빠르게 배울 수 있을 것 같아서, 정리를 해보고자 한다.

먼저 의존성 주입이란 무엇인지 알아보고, Unity에서 이를 지원하기 위한 3rd 파티 플러그인인 VContainer
에 대해 알아보도록 한다.
2 | C#과 의존성 주입 (Dependency Injection)
🔷 2-1. 의존성 주입이란?
의존성 주입(Dependency Injection)은 클래스가 필요한 의존 객체를 외부로부터 전달받는 설계 방식이다. 이름은 굉장히 어렵게 들리지만, 개념 자체는 단순하다. 객체 내부에서 직접 다른 객체를 생성하지 않고, 필요한 것을 외부에서 주입받는다
는 점이 핵심이다.
이해를 돕기위해, 코드를 살펴보도록 하자. 아래 코드는 의존성 주입이 적용되지 않은 일반적인 C#코드의 한 예이다. 게임 전반을 관리하는 매니저 클래스인 GameManager
와, 외부와 연결된 GameServer
가 있다.
먼저, GameManager
는 아래와 같다.
public class GameManager
{
// 멤버 변수
private GameServer gameServer;
private int score;
// 생성자
public GameManager()
{
gameServer = new GameServer(); // 직접 생성
}
// 점수 저장 메서드
public void SaveScore()
{
gameServer.SaveData(score);
}
}
GameServer
클래스는 다음과 같다고 하자.
public class GameServer
{
public void SaveData(int score)
{
// =======================================
// 서버 통신용 코드
// =======================================
Console.WriteLine($"저장된 점수 : ${score}");
}
}
GameManager
와 GameServer
는 서로 Composition(구성)관계임을 알 수 있다. 이를 UML 클래스 다이어그램으로 표현하면 다음과 같다.
Composition(구성) 관계에 대해 잘 모른다면, 이전 블로그 게시글의
Relationships between classes
부분을 참고하면 좋을 듯 하다.

이 방식의 문제는 GameManager가 GameServer의 구체적인 구현에 강하게 묶여 있다는 점이다. 이는 다음과 같은 문제점을 유발 할 수 있다.
🔶 문제점 1. 테스트의 어려움
테스트에서 GameManager
를 사용할 경우 GameServer
를 그대로 사용한다면 실제 서버에 테스트 데이터가 저장될 위험이 있기 때문에, 테스트를 할 때에는 모의객체 (Mock) 를 써야한다. 따라서 테스트용 모의객체 클래스 TestGameServer
를 만든 뒤 GameManager
생성자에서 new GameServer()
대신 new TestGameServer()
로 객체를 만들도록 코드를 수정하여
멤버 변수에 저장해야 할 것이다. 그러나 이는 곧 테스트를 할 때 마다 이런식으로 GameManager
의 코드를 변경해주어야 한다는 뜻인데 이런식의 코드 변경은 휴먼에러가 생길 수 있는 큰 구멍으로, 코드의 안정성을 대폭 떨어트린다.
모의객체(Mock)를 잘 모른다면, 이전 블로그 게시글의
대역의 종류
부분을 참고하면 좋을 듯 하다.
🔶 문제점 2. 낮은 확장성
개발이 진행되면서, GameServer
를 CloudGameServer
따위로 교체할 필요가 생겼다고 하자. 이럴 경우 개발자가 GameManager
클래스의 코드를 변경해 주어야 할텐데, 이는 문제점 1번과 마찬가지로 코드의 확장성이 부족해지는 요인이 될 수 있다.
이런 문제들이 있다는것은 잘 알았다. 그런데 그럼 어떻게 바꿔야 하는가? 일단, 구현 된 객체를 의존하지 않도록, 새로운 인터페이스를 만들어준다.
아래는 새로 만든 IGameServer
인터페이스이다.
public interface IGameServer
{
void SaveData(int score);
}
그리고 IGameServer
라는 인터페이스가 생겼으니, 인터페이스를 구현하도록 기존 GameServer
를 변경해준다.
public class GameServer : IGameServer // IGameServer 구현
{
public void SaveData(int score)
{
// =======================================
// 서버 통신용 코드
// =======================================
Console.WriteLine($"저장된 점수 : ${score}");
}
}
이 인터페이스를 구현하면 테스트용 모의객체도 쉽게 만들 수 있다. 예를 들면 다음과 같다.
public class MockGameServer : IGameServer
{
public bool WasSaveCalled { get; private set; } = false;
public void SaveData(int score)
{
WasSaveCalled = true;
Console.WriteLine($"[테스트용] 저장된 점수: {score}");
}
}
또한 GameManager
의 멤버 변수의 자료형을 GameServer
와 같은 구상 클래스가 아니라 IGameServer
와 같은 인터페이스로 변경한다. 그 후 GameManager
에서 이를 직접 생성해서 사용하는 것이 아니라 외부에서 주는 객체를 받아와서 저장하도록 만들어준다. 즉, 두 클래스를 Composition 관계에서 Aggregation 관계로 만들어 주도록 수정한다.
public class GameManager
{
// 멤버 변수 ( IGameServer 인터페이스 사용 )
private IGameServer gameServer;
// 생성자에서 받아와서 멤버 변수에 저장하도록 변경 ( Aggregation관계가 되도록 )
public GameManager(IGameServer server)
{
gameServer = server;
}
// 후략
}
이로써 GameManager
는 생성될 때 변수로 어떤 인스턴스를 받느냐에따라 동작이 달라지게 된다. 기존과 다르게 GameManager
의 코드의 변경 없이 가능해 졌다는 점을 기억해야한다.
즉, 다양한 상황에 따라 다른 구현을 유연하게 주입할 수 있다. 예를 들어 테스트에서는 다음과 같이 가짜 객체를 전달할 수 있다.
var mockGameServer = new MockGameServer();
var manager = new GameManager(server : mockGameServer);
이것이 바로 의존성 주입이다!
의존성 주입(Dependency Injection / DI)
은 객체가 직접 필요한 의존성을 생성하는 것이 아니라, 외부에서 전달받는 방식이다. 이를 통해 객체 간의 결합도를 낮추고, 보다 유연하고 확장 가능한 구조를 만들 수 있다. 특히 테스트 코드 작성 시, 실제 객체 대신 테스트용 객체를 전달할 수 있어 테스트의 정확성과 효율성이 향상된다. 또한 새로운 기능을 추가하거나 기존 로직을 수정할 때도 최소한의 변경으로 적용이 가능하다는 장점이 있다. 결국 이는 유지보수성과 코드의 품질을 높이는 데 매우 중요한 역할을 한다.
개념적으로는 레고 블록
에 비유할 수 있다. 조립식 블록처럼, 각 구성 요소를 갈아 끼우는 것이 쉬워진다. 하나의 블록(클래스)이 다른 블록에 결합될 때, 직접 붙이는 것이 아니라 외부에서 알맞게 연결해주는 셈이다.
이러한 유연성이 생기는 이유는 GameManager
가 구상 클래스에 의존성이 걸려있는 것이 아니라 인터페이스에 의존성이 걸리도록 변경했기 때문이다. UML 그림으로 보면 다음과 같다.

자세히 보면 포스트의 첫번째 UML (그림 A) 과 이번 UML (그림 B) 에서 화살표의 방향
이 조금 달라진 것을 볼 수 있다. 첫번째 에서는 한 방향 (왼쪽에서 오른쪽으로 향함) 이었는데, 이번 UML 에서는 클래스 사이에 인터페이스가 생기고, 두 방향으로 된 것을 볼 수 있다. (양쪽에서 가운데로 향함) 이를 제어의 역전, Inversion Of Control 이라고 한다.

📚 참고 문서
- [ 5분 개발지식 유튜브 ] : 의존성 주입 3분만에 이해하기 (Dependency Injection, Inversion of Control)
- [ dev.KwonTaeHyeong 블로그 ] : 제어의 역전(IoC)
🔷 2-2. IoC Container & Composition Root
다시 코드를 살펴보자.
public class GameManager
{
// 멤버 변수 ( IGameServer 인터페이스 사용 )
private IGameServer gameServer;
// 생성자에서 받아와서 멤버 변수에 저장하도록 변경
public GameManager(IGameServer server)
{
gameServer = server;
}
// 후략
}
앞서 설명한 대로, 구상클래스의 의존성을 낮추기 위해 gameServer
필드에 객체를 할당할 때 직접 new ()
로 객체를 만들지 않도록 했다. 대신 생성자에서 IGameServer
인터페이스를 구현한 인스턴스를 받아오도록 변경했다.
이로써 GameManager
클래스는 GameServer
라는 구상 클래스를 알 필요가 없게 됐지만, 대신 다른 곳에서 GameServer
인스턴스를 만들어서 GameManager
생성자에게 전달해 주어야만 하게 되었다. 이러한 객체 생성을 담당하고 의존성을 주입하는 역할을 하는 프레임워크를 IoC Container라고 하며,
IoC Container
에 의존성 구성을 알려주는 역할을 하는 클래스를 Composition Root라고 한다.
public class Program
{
static void Main(string[] args)
{
var compositionRoot = new CompositionRoot();
compositionRoot.Run();
}
}
public class CompositionRoot
{
public void Run()
{
// 역할을 설명하기 위해 수동으로 의존성을 주입했지만,
// 실제 DI 프레임워크가 내부적으로 수행하는 방식과 유사하다
IGameServer gameServer = new GameServer();
GameManager gameManager = new GameManager(gameServer);
}
}
맨 처음에 설명한대로, 일반적으로는 다른 클래스를 사용하려는 곳에서 객체를 결정/생성하고 그 객체의 메서드를 호출하는 방식이 자연스러울 수 있다. Program
클래스가 GameManager
를 생성하고, 그 GameManager
에서 GameServer
를 생성하는 식이다. 그림으로 보면 아래 그림과 같을 것이다.

앞서 설명한 제어의 역전 (Inversion of Control) 이란 이러한 제어의 흐름을 역전하는 것을 의미한다. IoC 가 적용된다면 CompositionRoot
가 그러한 업무를 모두 담당하게 된다. 필요한 객체를 생성하고, 관계에 맞게 전달하여 각 클래스간의 의존성을 낮춰주게 된다.

가끔 IoC Container를 Factory Pattern과 혼동하여 IoC 관련 라이브러리를 그냥 Factory처럼 사용하는 경우도 종종 있는데, Factory는 단순히 object를 생성하는 assembler에 가깝고 IoC Container는 거기에 제어의 역전 개념이 적용되어야 한다. IoC Container를 그냥 사용한다고 제어가 역전 되는 게 아니다.
📚 참고 문서
- [ Develogs 블로그 ] : 제어의 역전(Inversion of Control, IoC) 이란?
🔷 2-3. Unity와 Dependency Injection
C#에서는 DI가 비교적 자연스럽게 적용되지만, Unity에서는 몇 가지 구조적인 제약 때문에 DI를 도입하는 데 어려움이 있다. MonoBehaviour
기반 구조, 생명주기 관리, GameObject 종속성 등이 복합적으로 작용하며 일반적인 DI 방식과 충돌한다.

대표적인 예로, MonoBehaviour
를 상속받은 클래스를 새롭게 추가하려고 하는 경우를 생각해보자. 문제는 MonoBehaviour
는 우리가 직접 new ()
키워드로 인스턴스 생성을 할 수 없고, 반드시 GameObject에 붙여야만 한다는 점이다.
public class MyGameMonster : MonoBehaviour
{
// ...
}
public class MyGameSpawner
{
var monster = new MyGameMonster(); // 컴파일은 되지만 제대로 작동하지 않음
}
위와 같은 코드는 컴파일은 정상적으로 되지만, 다음과 같은 워닝이 뜨는것을 볼수있다.

🔎 Why can't create MonoBehaviour with “new’ keyword?
MonoBehaviours are Components. This means they are inextricably attached to GameObjects at all times. If you create a MonoBehaviour with new, then obviously it is not attached to a GameObject. It cannot exist in this state, so Unity gives you a warning that this is bad. ... MonoBehaviours are meant to only exist when they are attached to GameObjects. You can create one by Instantiating an existing one (such as from a prefab), or by using the AddComponent method on GameObject.
MonoBehaviour는 컴포넌트입니다. 즉, 항상 게임 오브젝트에 불가분의 관계로 연결되어 있습니다. new를 사용하여 MonoBehaviour를 생성하면 게임 오브젝트에 연결되지 않습니다. 이 상태로는 존재할 수 없으므로 Unity는 이러한 상태가 좋지 않다는 경고를 표시합니다. ... MonoBehaviour는 게임 오브젝트에 연결될 때만 존재합니다. 기존 MonoBehaviour를 인스턴스화하거나(예: 프리팹에서) GameObject의 AddComponent 메서드를 사용하여 MonoBehaviour를 생성할 수 있습니다.
또한 Unity는 MonoBehaviour
의 생성자를 호출하지 않기 때문에, 생성자 주입(Constructor Injection)이 불가능하다. 의존성 주입의 가장 일반적인 방식이 막혀 있는 셈이다.
public class Player : MonoBehaviour
{
private readonly IGun _gun; // 컴파일 에러
public Player(IGun gun) { ... } // 호출되지 않음
}
대신 Unity에서는 대부분 Start()
, Awake()
같은 생명주기 메서드에서 의존 객체를 초기화하지만, 이 역시 명시적인 주입보다는 Find(...)
나 GetComponent(...)
등으로 직접 의존성을 해결하는 방식이 많다. 이 방식은 외부에서 객체 간 의존 관계를 제어하기 어렵게 만든다. 이는 다음과 같은 코드로 이어지기 쉽다.
public class ItemManager : MonoBehaviour
{
// ...
}
public class Item : MonoBehaviour
{
private ItemManager manager;
void Awake()
{
manager = FindObjectOfType<ItemManager>();
}
}
이러한 관계는 다음과 같은 문제가 있다.
1. 명시적인 의존 관계가 코드에 드러나지 않는다
지금은 코드가 짧아 Item
클래스가 ItemManager
에 대해 의존성을 가지고 있는 것을 쉽게 파악할 수 있지만, 만약 Awake 부분의 코드가 길어지거나, 추가적인 로직이많이 들어가게되면 금방복잡해 지게되어 의존관계 파악이 어려워지게 된다.
2. 객체의 생애 주기를 외부에서 제어할 수 없다
Unity에서는 대부분의 객체가 다음과 같은 방식으로 만들어진다.
-
Scene에 미리 배치된 GameObject
-
Instantiate()
를 통해 런타임에 생성 -
ScriptableObject.CreateInstance()
호출
예를 들어 다음과 같이 Item
오브젝트를 Instantiate()
할 경우를 생각해보자.
var item = Instantiate(itemPrefab);
이 객체는 Unity 내부에서 Awake()
→ OnEnable()
→ Start()
순서로 생명주기를 자동 처리하며, 우리는 그 흐름을 외부에서 개입하기 어렵다. 즉, 이 시점에 Item이 생성될 때 필요한 ItemManager가 이미 준비되어 있을까? 같은 질문을 코드 수준에서 보장하기 힘들다. 결국 FindObjectOfType
, DontDestroyOnLoad
, StartCoroutine
등을 동원해 생명주기를 땜질식으로 맞춰야 한다.
요약하면, MonoBehaviour 를 상속받은 class는 객체의 생성 시점과 파괴 시점을 개발자가 명시적으로 통제할 수 없고, 의존성 주입을 동기적으로 안전하게 보장하기 어려우며 라이프사이클이 Unity 내부 구현에 강하게 의존한다는 걸 의미한다.
이런 문제를 해결하기 위해 Unity에 특화된 의존성 주입이 가능한 구조를 만들 필요하다. 그리고 이 시점에서 등장하는 것이 Unity에 특화된 DI 프레임워크, VContainer이다.
📚 참고 문서
- [ Unity 공식 문서 ] : MonoBehaviour
- [ Unity 포럼 ] : Cannot create MonoBehaviour with “new’ keyword?
- [ 나의 삽질일지 블로그 ] : MonoBehaviour
3 | VContainer
🔷 3-1. VContainer란
VContainer는 Unity에 최적화된 경량 Dependency Injection 프레임워크
다. Zenject와 같은 기존 DI 프레임워크가 가진 복잡성, 런타임 비용, 학습 난이도 등의 문제를 보완하기 위해 만들어졌으며, 성능과 생산성 모두를 고려한 구조를 지향한다. VContainer는 일본의 게임 개발자 hadashiA 가 개발하고 있으며, GitHub에서 오픈소스로 관리된다. Unity Asset Store에 등록된 유료 패키지는 아니지만, 오픈소스 프로젝트로서 MIT 라이선스 하에 자유롭게 사용할 수 있다.
VContainer의 “V”는 Unity의 첫 글자 “U”를 더 얇고 견고하게 만드는 것을 의미한다고 한다.

VContainer
는 일반적인 .NET DI 프레임워크를 Unity에 그대로 적용하는 데 한계가 있다는 점에서 출발한다. 앞에서 설명했듯 Unity는 MonoBehaviour
, ScriptableObject
, GameObject
기반의 독특한 실행 환경을 가지고 있는데, VContainer는 이 문제를 해결하기 위해, 다음과 같은 특징을 제공한다.
🔸 Unity 생명주기와 자연스럽게 통합되는 LifetimeScope 구조
VContainer는 Unity의 씬 시스템과 라이프사이클을 깊이 있게 통합하는 구조를 제공한다. 가장 핵심적인 개념은 후술할 LifetimeScope이다. 이를 통해 Unity 특유의 씬 전환, 프리팹 Instantiate, DontDestroyOnLoad 구조에서도 명확하게 의존성 범위를 나눌 수 있게 된다. 또한 MonoBehaviour의 Awake()
, Start()
타이밍 전에 이미 의존성이 주입되므로, 기존 Unity 코드 흐름을 크게 변경하지 않아도 된다.
🔸 Constructor Injection을 지원하는 방식으로 비-MonoBehaviour 객체를 안전하게 관리
Unity의 MonoBehaviour가 아닌 일반 순수 C# 클래스에게도 전통적인 방식의 생성자 주입 형태로 의존성을 주입할 수 있다. 이런 비-MonoBehaviour 클래스에 대해서는 VContainer가 DI 컨테이너처럼 생성자를 호출하고, 내부 의존성을 자동으로 주입해주게 된다.
🔸 Zero GC(가비지 컬렉션 없음) 을 지향하는 경량 컨테이너
VContainer의 가장 큰 특징 중 하나는 Zero GC(가비지 컬렉션 없음)
을 지향하는 구조다. 기존 DI 프레임워크들은 내부적으로 런타임에 Reflection이나 Expression Tree를 활용하는 경우가 많아, 런타임 중 할당이 발생하고 이는 곧 GC 수집 타이밍에 영향을 준다. 그러나 VContainer는 컴파일 타임 분석과 코드 생성 방식을 활용해, 런타임 오버헤드를 최소화한다고 한다. 의존성 해석과 인스턴스 생성은 초기화 단계에서 한 번 수행되고, 이후에는 GC에 영향을 주지 않는 방식으로 동작한다. 이러한 특성 덕분에 VContainer는 모바일, 콘솔 등 GC 민감한 플랫폼에서도 안정적으로 사용할 수 있다!
VContainer는 Unity의 고유한 구조에 잘 녹아드는 DI 프레임워크로, 기존의 .NET DI 컨테이너들과는 철학이 다르다는 것을 알 수 있다. 다만 “Unity 프로젝트에서 DI를 실용적으로 쓸 수 있을까?” 라는 질문에 실질적인 해답을 제공한다는 점에서 의미가 크다.
🔷 3-2. VContainer 설치
다음과 같은 방식으로 VContainer를 설치할 수 있다. 지원하는 Unity 에디터 버전은 2018.4
이상 버전을 사용해야 한다.
(2025.05 기준, ECS(Entity Component System)과 함께 사용하고 싶다면 Untity 2019.3 이상을 사용해야한다.)
- Unity 프로젝트의 Root에 존재하는
Packages
폴더로 이동하여manifest.json
파일을 연다. dependencies
항목에 다음과 같은 내용을 추가한다.{ "dependencies": { //...(중략) "jp.hadashikick.vcontainer": "https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.16.8" } }
- 에디터로 돌아오면, UPM이 자동으로 패키지를 설치한다.
🔷 3-3. VContainer 기본 사용법
기본적으로, VContainer를 사용하는 방법은 크게 다음 3단계로 나누어진다.
LifetimeScope
를 상속한 컴포넌트를 Scene에 추가한다.LifetimeScope
하나당 container 하나와 scope 하나를 가지고 있다.LifetimeScope
를 상속한 클래스에서, 주입하고자하는 종속성을 구성하고 등록한다. (이를 CompositionRoot라고 한다.)- Scene을 Play하면
LifetimeScope
가 필요한 객체를 생성하고, 종속성을 주입힌다.
라고 한다면 이해하기 어려울 것이므로, 간단한 코드를 통해 살펴 보도록 하자. 예를들어, 게임에 하나의 EquipmentManagerService
와 StatManagerService
가 있다고하자. 캐릭터의 Stat을 계산할 땐 장비의 능력치가 필요하므로, StatManagerService
는 EquipmentManagerService
에게 의존성을 가지고 있다. 테스트할 것이기 때문에, 다음과 같은 아주 간단한 코드를 작성해 보았다.
public interface IEquipmentManagerService
{
float GetAttackPower();
}
public class EquipmentManagerService : IEquipmentManagerService
{
public float GetAttackPower()
{
return 100f;
}
}
그리고 이에대해 의존성을가지는 StatManagerService
클래스를 작성해준다.
public interface IStatManagerService
{
float GetTotalAttackPower();
}
public class StatManagerService : IStatManagerService
{
private IEquipmentManagerService _equipmentManagerService;
private float _characterAttackPower = 100f;
public StatManagerService(IEquipmentManagerService equipmentManagerService)
{
_equipmentManagerService = equipmentManagerService;
}
public float GetTotalAttackPower()
{
return _characterAttackPower +
_equipmentManager.GetAttackPower();
}
}
이제 이 두 클래스를 VContainer로 연결해 보도록 하자.
Unity 프로젝트에 가서 새로운 C# 파일을 만들고 이름을 MyGameLifetimeScope.cs
로 설정해 둔다. 사실 다른 이름이어도 별 상관없지만, ~ LifetimeScope.cs
로 끝나는 파일을 만들면 자동으로 코드 템플릿이 적용되기 때문에 편리하기 때문이다. 그리고, Configure
메서드에 두 클래스를 등록해 둔다. 이를 통해 두 클래스를 등록해둔다.
using VContainer;
using VContainer.Unity;
public class MyGameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.Register<IEquipmentManagerService, EquipmentManagerService>(Lifetime.Singleton);
builder.Register<IStatManagerService, StatManagerService>(Lifetime.Singleton);
}
}
구현한 내용을 UML로 그려보면 다음과 같다.

여기서 Lifetime.Singleton
이라는 부분을 볼 수 있는데, 이 enum값은 의존성의 생명주기를 정의하는데 사용되는 타입이다. 이는 객체가 언제 생성되고 언제 파괴되는지를 결정하는 중요한 설정이다. 문서에 따르면, VContainer는 다음과 같은 3가지 주요 Lifetime을 제공한다.
🔸
Lifetime.Singleton
애플리케이션 전체에서 한 번만 생성되며, 모든 요청에 대해 같은 인스턴스가 제공된다. 주로 설정 값, 서비스 관리자, 로거, 네트워크 매니저 등 게임 전역에서 공유되는 서비스에 주로 사용된다. 위 예제에선 하나의
EquipmentManagerService
와StatManagerService
가 있다고했으므로,Lifetime.Singleton
을 사용했다. 가장 흔히 사용되는 타입으로 만약 이 객체가 상태를 갖고 있는 경우, 여러 컴포넌트에서 동시에 접근할 수 있으므로 동기화에 유의해야 한다.🔸
Lifetime.Transient
일시적이라는 뜻을 가진 Transient라는 단어에서 알 수 있듯이, 요청이 있을 때 마다 새로운 인스턴스를 만든다. 주로 상태를 갖지 않거나 짧은 시간동안만 사용되는 객체에 쓰이며, 무거운 리소스가 없고 객체를 재사용하지 않을 때 사용하면 좋다. 주의사항으로는 자주 생성되므로 GC에 부담이 갈 수 있다.
🔸
Lifetime.Scoped
현재
LifetimeScope
내에서 한 번만 생성 되며, 같은 Scope 내에서는 같은 인스턴스가 공유된다. 달리 말하면, 서로 다른 Scope에서 요청을 하면 서로 다른 인스턴스가 생성된다는 뜻이다. 특정 범위 내에서 ‘만’ 인스턴스를 공유하므로, Scene, UI Window, 전투 시스템 등 스코프 단위로 상태나 리소스를 관리해야 할 때 유용하다. 예를 들어, 씬마다 다른 GameManager나 UIController를 사용할 경우 적합하다. 이런 특징 때문에 Scope 간 관계를 명확히 인지하고 사용해야 한다!
모든 준비를 다 마쳤으면, 다음과 같이 빈 GameObject에 LifetimeScope
컴포넌트를 추가한다.

이렇게 두면, VContainer
가 StatManagerService
객체에게 EquipmentManagerService
객체를 주입하게된다.
근데, 막상 에디터의 플레이 버튼을 눌러보면 이 상태에서는 아무런 동작도 하지 않는다. 당연하게도, EquipmentManagerService
와 StatManagerService
의 어떤 메서드나 필드도 (Unity의 라이프사이클과 관련이 있는) MonoBehaviour 클래스에서 호출되지 않기 때문이다. 이런 상태라면 주입이 됐는지 안됐는지 알 길이 딱히 없다.

우리는 Unity에서 VContainer를 사용 할 것이기 때문에, Unity Lifecycle과의 연동이 필요하고, VContainer에서도 이를 잘 알기 때문에 MonoBehaviour 클래스를 상속받지 않은 일반 C# 클래스도 Unity Lifecycle과 연동 될 수 있도록 여러가지 인터페이스를 제공하고 있다. 자세한 내용은 다음과 같다.
VContainer entry point | Timing |
---|---|
IInitializable.Initialize() |
컨테이너가 만들어진 직후 |
IPostInitializable.PostInitialize() |
IInitializable.Initialize() 이후 |
IStartable.Start() |
MonoBehaviour.Start() 와 비슷한 시기 |
IAsyncStartable.StartAsync() |
MonoBehaviour.Start() 와 비슷한 시기 (비동기) |
IPostStartable.PostStart() |
MonoBehaviour.Start() 이후 |
IFixedTickable.FixedTick() |
MonoBehaviour.FixedUpdate() 와 비슷한 시기 |
IPostFixedTickable.PostFixedTick() |
MonoBehaviour.FixedUpdate() 이후 |
ITickable.Tick() |
MonoBehaviour.Update() 와 비슷한 시기 |
IPostTickable.PostTick() |
MonoBehaviour.Update() 이후 |
ILateTickable.LateTick() |
MonoBehaviour.LateUpdate() 와 비슷한 시기 |
IPostLateTickable.PostLateTick() |
MonoBehaviour.LateUpdate() 이후 |
🔎 Unity & VContainer Order of Execution

연동이 잘되는 것을확인하기 위해, 매 프레임마다 총 공격력을 로그로 찍어보는 기능을 구현해보도록 하자. 이를위해 StatManagerService
를 ITickable
인터페이스를 상속하도록 변경하고, ITickable.Tick
메서드를 구현하였다.
public class StatManagerService : IStatManagerService, ITickable // ITickable 추가
{
private IEquipmentManagerService _equipmentManagerService;
private float _characterAttackPower = 100f;
public StatManagerService(IEquipmentManagerService equipmentManagerService)
{
_equipmentManagerService = equipmentManagerService;
}
public float GetTotalAttackPower()
{
return _characterAttackPower +
_equipmentManager.GetAttackPower();
}
// ITickable.Tick 메서드 추가
void ITickable.Tick()
{
var totalAttackPower = GetTotalAttackPower();
Debug.Log($"<color=oragne>캐릭터 Stat : {totalAttackPower}</color>");
}
}
그 후, MyGameLifetimeScope
에서 이 StatManager
를 Unity Lifecycle에 연동을 시켜줘야 한다. 다음과 같이 RegisterEntryPoint
메서드로 수정하도록 한다.
using VContainer;
using VContainer.Unity;
public class MyGameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.Register<IEquipmentManagerService, EquipmentManagerService>(Lifetime.Singleton);
builder.RegisterEntryPoint<StatManagerService>(); // 이 부분 수정
}
}
뒤에 더 추가로 설명하겠지만, builder.RegisterEntryPoint<StatManager>()
는 builder.Register<StatManager>().As<IStatManager>();
또는 builder.Register<IStatManager, StatManager>(Lifetime.Singleton);
과 같은 동작을 한다. 다른 점이라면 RegisterEntryPoint는 Unity Lifecycle 과 연동된다는 점이다. 수정을 마치고 Editor의 Play버튼을 누르면 다음과 같이 동작하는 모습을 볼 수 있다.

🔷 3-4. VContainer로 IoC를 구현하는 방법
흠… 이런 예제로는 아직 어디에 쓸지 딱히 모르겠다고 생각했는가?

그럼 본격적으로 VContainer
를 이용하여 IoC를 구현해보도록 하자. 바로 위의 예제에서, 매 Update마다 전체 공격력을 로그로 찍어봤는데, 이번에는 UI 버튼을 눌렀을 때만 전체 공격력을 출력하도록 해보자. 일단, 버튼으로 호출 시킬 것이므로 다시 RegisterEntryPoint
를 원복시키고, StatManagerService
에서도 ITickable
인퍼테이스 구현을 제거한다. 그리고, 다음과 같이 UI Canvas
와 Button
을 만들었다.

그리고 버튼을 눌렀을 때, 동작을 시키기 위해 다음과 같은 UI 코드를 작성했다고 하자. 이름은 MyGameUIView.cs
라고 정해보았다.
public interface IMyGameUIView
{
Action OnButtonClickedCallback { get; set; }
}
public class MyGameUIView : MonoBehaviour, IMyGameUIView
{
[SerializeField] private Button _button;
public Action OnButtonClickedCallback { get; set; }
private void Awake()
{
_button.onClick.RemoveAllListeners();
_button.onClick.AddListener(OnButtonClicked);
}
private void OnButtonClicked()
{
Debug.Log("<color=cyan>버튼을 눌렀습니다!</color>");
OnButtonClickedCallback?.Invoke();
}
}
그리고 이 컴포넌트를 View 오브젝트의 적당한 곳에 추가해 둔다. 본 예제에서는 MyGameViewUI
라는 오브젝트를 만들어두었으므로, 그 오브젝트에 추가해두었다. 그리고 입력을 받을 버튼을 연결해 두었다.

만약 이 IoC를 적용하지 않는 일반적인 Unity C# 프로그래밍이라면 이 MyGameUIView
코드에 비즈니스 로직을 추가해야 했을 것이다. 하지만 VContainer를 사용한다면 IoC를 적용
할 수 있게되어 View와 Control을 분리할 수 있게 된다. 위와 같이 MonoBehaviour
를 상속받은 View에서는 사용자에게 보여주거나 입력을 받는 I/O 작업만을 처리하도록 구현했다.
중요 처리 로직은 다른 클래스에게 위임해야 하는데, 본 예에서 중요 처리 로직이란 전체 공격력을 계산해서 알아내는 것 일 것이다. 다음과 같은 MyGameUIPresenter.cs
를 작성해본다.
using UnityEngine;
using VContainer.Unity;
public class MyGameUIPresenter : IInitializable
{
private readonly IMyGameUIView _myGameUIView;
private readonly IStatManagerService _statManagerService;
// 생성자 주입 방법 사용
public MyGameUIPresenter( IMyGameUIView myGameUIView,
IStatManagerService statManagerService)
{
_myGameUIView = myGameUIView;
_statManagerService = statManagerService;
}
void IInitializable.Initialize()
{
_myGameUIView.OnButtonClickedCallback = HandleOnButtonClicked;
}
private void HandleOnButtonClicked()
{
Debug.Log($"<color=pink>캐릭터 Stat : {_statManagerService.GetTotalAttackPower()}</color>");
}
}
이제 앞서 언급한대로 두 클래스간 종속성을 자동으로 주입시키기 위해 이 클래스들을 LifetimeScope에 등록하면 되는데, 기존 MyGameLifetimeScope가 아닌 새로운 LifetimeScope를 만들고 그곳에 등록을 할 계획이다. 앞에서 보았듯, StatManagerService
와 같은 게임 로직 관련 서비스는 게임 전반에 걸쳐 공통적으로 사용되므로 메인 Scope인 MyGameLifetimeScope
에 등록해 두었다. 하지만 UI의 경우엔 조금 다르다. 게임 전반에 걸쳐 사용되는 시스템이 아니라, 특정 상황 (예: 전투 UI, 인벤토리 UI, 메인 메뉴 등) 에서만 일시적으로 등장하고 사라지는 구조인 경우가 많다.
이러한 차이점 때문에, UI에서만 사용되는 Presenter
나 View
와 같은 컴포넌트는 별도의 Scope
로 분리하여 관리하는 것이 바람직하다. 그렇게 하면 UI가 필요 없어졌을 때 해당 Scope만 제거하면 되고, 게임 전체 로직에는 영향을 주지 않게 되기 때문이다. 이렇게 각 기능을 자신이 필요한 범위에서만 관리하면 메모리 관리, 재사용성, 테스트 용이성 등의 측면에서 큰 이점을 얻을 수 있다.
따라서 다음과 같이 MyGameUILifetimeScope.cs
를 작성한다.
using VContainer;
using VContainer.Unity;
public class MyGameUILifetimeScope : LifetimeScope
{
// 다음과 같이 MyGameUIPresenter와 IMyGameUIView를 LifetimeScope에 등록
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterEntryPoint<MyGameUIPresenter>();
builder.RegisterComponentInHierarchy<IMyGameUIView>();
}
}
여기서 builder.RegisterComponentInHierarchy
메서드는 말 그대로, 현재 LifetimeScope 하위 계층에 존재하는 MonoBehaviour 중에서 IMyGameUIView
를 구현한 컴포넌트를 자동으로 찾아서 등록하는 코드이다. Scene에 이미 존재하는 오브젝트를 기반으로 DI 컨테이너에 자동 등록하게 되어 매우 자주 사용되는 메서드이다.
그리고 게임 오브젝트들 중 이 컴포넌트를 적당한 곳에 추가한다. 본 예에서는 MyGameViewUI
라는 게임 오브젝트에 추가해두었다.

MyGameUILifetimeScope
를 사용할 때 주의해야할 점은, 반드시 상위 Scope를 MyGameLifetimeScope로 지정해 주어야 한다는 것이다. 그래야 UI Scope 내부에서 IStatManagerService
처럼 상위 Scope에 정의된 서비스를 참조할 수 있다. 만약 부모 Scope를 명시하지 않으면, 서로 다른 컨테이너로 분리되기 때문에 상위 서비스에 접근할 수 없고 의존성 주입이 실패하게 된다.
위 내용들을 UML로 확인한다면 다음과 같은 모습이 되었음을 확인할 수 있다.

이제 버튼을 눌러보면, 아래 gif 처럼 정상적으로 캐릭터의 전체 스탯이 출력되는 모습을 잘 확인할 수 있다.
장비 공격력 100과 캐릭터 공격력 100을 더한 200이 출력되는 모습

예제를 통해 VContainer를 통해 IoC를 구현하는 방법을 알아보았다. 의존성 주입을 제대로 적용하려면 단순히 객체를 외부에서 주입하는 것만으로는 부족하다. 특히 Unity 같은 프레임워크 환경에서는 View
, Presenter
, Scope
를 명확히 분리하는 것이 필수적이다. View
는 UI 구성과 사용자 입력 처리만 담당하고, 실제 로직은 Presenter
에 위임하는 구조로 나누면 역할이 명확해지고 유지보수성이 높아진다. 여기에 Scope
까지 적절히 분리해주면, 필요한 시점에만 의존성을 생성·사용하고, 더 이상 필요하지 않다면 해제할 수 있어, 리소스 관리 측면에서도 매우 유리하다.
그러나 이러한 구조를 수동으로 관리하려면 코드가 복잡해지고, 버그 가능성이 높아지게 된다. 하지만 VContainer는 Unity의 구조적 제약을 고려하여, Scope 기반 객체 수명 관리, EntryPoint 기반 Unity 생명주기 연동, 컴포넌트 자동 주입 기능 등을 통해 이 복잡한 구조를 간결하게 해결해준다. 즉, Unity에서도 IoC 원칙을 제대로 적용할 수 있는 실용적인 방법을 제공하는 DI 프레임워크라고 할 수 있다.
VContainer를 사용하면 View와 Presenter를 깔끔하게 분리하고, 필요한 Scope에만 의존성을 설정함으로써 테스트 가능한, 확장 가능한 구조를 손쉽게 구현할 수 있다.
다음 챕터에는 VContainer를 사용하는 상세한 방법에 대해 기술하도록 한다.
🔷 3-4. VContainer 상세 사용법
🔶 3-4-1. 주입하기 (Injecting / Resolving)
VContainer를 통해 필요한 의존성을 주입받는 다양한 방법에 대해 알아보도록 한다. 하기 내용들은 대부분 VContainer 공식 Document를 참고해서 작성했다.
🔻 3-4-1-1. 생성자 주입 (Constructor Injection)
일단 가장 일반적인 C#에서 가장 많이 사용되는 생성자 주입에 대해 알아보도록 하자. 앞서 언급한대로 MonoBehaviour
를 상속받지 않은 일반 C# Class의 경우, 다음과 같이 작성할 수 있다.
class ClassA
{
private readonly IServiceA _serviceA;
private readonly SomeUnityComponent _component;
// 생성자를 통한 주입... IServiceA 와 SomUnityComponent, ClassA가 모두
// LifetimeScope에 등록되어 있을 때, 아래 생성자가 호출된다.
public ClassA( IServiceA serviceA, SomeUnityComponent component )
{
_serviceA = serviceA;
_component = component;
}
}
위 코드처럼 일반적인 생성자를 구성하고 이를 VContainer의 LifetimeScope
에 등록해두면, VContainer가 알아서 이 생성자를 통해 의존성을 주입해주게 된다. 앞서서 든 예에서 StatManagerService
나 MyGameUIPresenter
가 이 방식을 사용해 의존성을 전달했다. 이는 그냥 일반 생성자이기 때문에, 원한다면 개발자가 이 생성자를 사용해 ( new ClassA( ~~~ )
를 직접 호출하여 ) 객체를 만들 수 도 있다.
다만 생성자 주입을 사용할 때 주의할 점은, 빌드시 코드 스트리핑을 의식해야 한다는 점이다. IL2CPP
백앤드로 Unity 프로젝트를 빌드 하는 경우, Unity는 최종 빌드 크기 감소
와 빌드 시간 절약
을 위해 사용되지 않는 코드를 빌드에서 제외하게 되는데, 이를 코드 스트리핑 이라고 한다.

이는 앞의 예제에서도 문제로 작용할 수 있는데, 본 예제 코드 어디에서도 StatManagerService
나 MyGameUIPresenter
와 같은 여러 클래스들의 생성자를 직접 호출한 부분이 없기 때문이다. 이 상태 그대로 빌드를 진행하게 될 경우, Unity에디터는 해당 생성자가 불필요한 코드라고 생각하게 돼 빌드 시 해당 생성자를 지워버리게 되어 문제가 발생하게 된다.
VContainer는 C#의 Reflection 기능을 통해 생성자에 접근하고 있다.
이를 방지하기 위해서는 다음과 같은 3가지 방법이 존재한다.
IL2CPP
를 사용하지 않거나,bytecode striping
을 사용하지않기- 블랙리스트 파일 link.xml 에 해당 생성자 추가하기
- 보존하고싶은 생성자에
[Inject]
어트리뷰트 추가하기
1번은 사실 사이드 이펙트가 클 수 있어서 좋지 못한 해결방법이고, 2번은 Unity 공식 문서에서도 추천하는 방식이긴 하나, 사용할 때마다 xml 파일을 수정해야 한다는건 번거로운 일이 아닐 수 없다. VContainer에서는 이를 해결하기 위해 3번과 같은 별도의 어트리뷰트를 제공하는데, 바로 [Inject] 어트리뷰트이다. 생성자 위에 [Inject]
를 추가하면 코드스트리핑 단계에서 참조되지 않아 제거되는 것을 막을 수 있게 된다.
public class ClassA
{
[Inject]
public ClassA( /* ... */ )
{
// ...
}
}
한 클래스에 생성자가 여러개일 경우, [Inject]
가 달려 있는 생성자만 VContainer가 사용할 수 있게 된다. (개발자는 어트리뷰트와 무관하게 아무 생성자나 쓸 수 있다.) 또한 만약 Inject 어트리뷰트가 여러개일 경우, Exception이 발생하니 실수하지 않도록 주의해야 한다.
마지막으로, 생성자 주입을 사용할 땐 readonly
필드와 함께 사용하는 것을 추천한다. 생성자에서 초기화된다는 취지에도 잘 맞기 때문이다.
🔻 3-4-1-2. 메서드 주입 (Method Injection)
앞서 언급했듯, MonoBehaviour
는 생성자를 사용할 수 없기때문에, 생성자 주입을 사용할 수 없다. 이렇듯 생성자 주입
을 사용할 수 없을 땐 메서드 주입을 사용해야 한다.
public class SomeBehaviour : MonoBehaviour
{
private private float speed;
[Inject]
public void Construct(GameSettings settings)
{
speed = settings.speed;
}
}
마치 생성자처럼 메서드에 [Inject]
어트리뷰트를 추가하면, VContainer
에서 이를 감지해서 파라미터를 전달해주게된다. 역시나 리플렉션을 사용해 접근하기 때문에 접근제한자는 어떤것이든 상관 없으며, 메서드 이름또한 상관없다.
다만, 생성자가 아닌 메서드에서 주입을 해주는 것이기 때문에, 받는 필드는 readonly
가 될 수 없음에 주의해야 한다.
🔻 3-4-1-3. 속성 / 필드 주입 (Property / Field Injection)
메서드 주입이 너무 길다고 느껴진다면, 그냥 필드나 속성에 [Inject]
어트리뷰트를 추가할 수 있다.
class ClassA
{
[Inject] IServiceA serviceA { get; set; }
[Inject] IServiceB serviceB;
}
🔻 3-4-1-4. MonoBehaviour에 주입하는 방법 (Injecting into MonoBehaviours)
VContainer
는 의존성 주입을 위해 앞서 설명한 다양한 방법을 제공하고 있다. 이때 공통적으로 중요한 것은 [Inject]
어트리뷰트를 추가한다는 것인데, MonoBehaviour
의 경우 별도의 추가 작업이 필요하다. MonoBehaviour
스크립트에 달려있는 [Inject]
어트리뷰트를 정상적으로 활용하라면, 다음과 같은 3가지 추가 작업 중 한 가지를 선택해서 구현하면 된다.
1️⃣ LifetimeScope의 인스펙터에서 직접 게임 오브젝트를 추가하는 방법
LifetimeScope
가 달려있는 GameObject를 살펴보면, Auto Inject Game Objects
라는 리스트를 확인해 볼 수 있다.

이 리스트에 GameObject
를 추가하면, LifetimeScope가 초기화 된 후, 이 리스트에 포함된 GameObject
안에 들어있는 MonoBehaviour
컴포넌트들을 순회하면서, [Inject]
프로퍼티가 달려있는 생성자 / 필드 / 프로퍼티 / 메서드에 대해 주입을 해주게 된다. (LifetimeScope가 모두 초기화 된 후 컴포넌트를 순회한다는 점을 명심해야한다!)
2️⃣ RegisterComponent~~~
메서드를 사용하는 방법
LifetimeScope
의 Configure(IContainerBuilder builder)
에서 RegisterComponent~~~
종류의 메서드를 사용해 MonoBehaviour
를 상속받은 스크립트에게 주입을 해줄 수 있다. 이 방식은 앞서 설명한 예제에서도 볼 수 있는데, 앞에서 썼던 메서드들 중 builder.RegisterComponentInHierarchy
가 그 예이다. 관련 메서드들은 다음과 같다.
builder.RegisterComponent()
builder.RegisterComponentInHierarchy<T>()
builder.RegisterComponentInNewPrefab(prefab, Lifetime.Scoped)
builder.RegisterComponentOnNewGameObject<T>(Lifetime.Scoped, "NewGameObjectName");
각 메서드별 상세 설명은 뒤에서 추가로 설명하기로 한다. 이 메서드들은 호출되면 자동으로 등록 (Register)되지만, 동시에 주입도 된다.
In this case, the registered MonoBehaviour will both
Inject
and beInjected
into other classes. 참조
3️⃣ IObjectResolver.Instantiate
메서드를 사용하는 방법
이건 사실 특수한 상황에서만 사용할 수 있는 것인데, 예를들어 팝업
이나 이펙트
등과 같이 런타임에서 생성되는 여러가지 프리팹
을 쓸 때는 UnityEngine.Object.Instantiate
메서드 말고 IObjectResolver.Instantiate
메서드를 활용해 생성과 주입을 동시에 할 수 있다. 이때 IObjectResolver
란 의존성 주입 컨테이너 내부에서 인스턴스를 생성하거나 반환하는 데 사용되는 인터페이스인데, 쉽게 말해 VContainer DI 컨테이너에 직접 접근할 수 있는 인터페이스 라고 보면된다. 자세한 내용은 뒤에서 더 설명하도록 한다.
🔻 3-4-1-5. DI 컨테이너 API (Container API)
앞서 설명했듯, IObjectResolver를 통해 VContainer DI
컨테이너에 직접 접근할 수도 있다. VContainer는 IObjectResolver
를 자동으로 등록하고 필요한 곳에 주입하므로, 여러 의존성을 IObjectResolver
를 통해 주입받을 수 있다.
일단, 아래와 같이 생성자로 주입을 받을 수 있다.
class ClassA
{
public ClassA(IObjectResolver container)
{
// container를 통해 다양한 api를 사용할 수 있다.
}
}
또는 [Inject]
어트리뷰트를 통한 필드 주입도 가능하다.
[Inject] IObjectResolver _container;
1️⃣ IObjectResolver.Resolve
메서드
var serviceA = container.Resolve<ServiceA>();
ServiceA
클래스를 등록한 적이 있다면, 그 클래스의 인스턴스를 가져온다 (없으면 새로 생성한다). 만약, ServiceA
를 상속받은 하위 클래스를 등록했다면, 그 하위 클래스를 가져온다.
2️⃣ IObjectResolver.Inject
메서드
container.Inject(instanceObject);
해당 인스턴스에 필요한 모든 의존성을 분석하고 추가한다. otherInstance
에 달려있는 모든 [Inject]
어트리뷰트가 달려있는 필드 / 메서드가 호출되며, 두번 호출하면 덮어 씌여진다.
3️⃣ IObjectResolver.InjectGameObject
메서드
container.InjectGameObject(gameObject);
해당 메서드에 GameObject
객체를 전달하면, 그 객체가 들고있는 모든 MonoBehaviour
스크립트에 존재하는 [Inject]
어트리뷰트가 달려있는 필드가 주입되고 메서드가 호출된다. 중요한 점은, 전달한 GameObject가 Active 인지 아닌지 판단하거나 달려있는 MonoBehaviour 스크립트들이 켜져있고 꺼져있는지는 판단하지 않는다는 점이다.
4️⃣ IObjectResolver.Instantiate
메서드
var gameObject1 = container.Instantiate(prefabObject);
var gameObject2 = container.Instantiate(prefabObject, parent);
var gameObject3 = container.Instantiate(prefabObject, position, rotation, parent);
해당 메서드에 프리팹 GameObject
객체를 전달하면, 그 객체를 소환하고 모든 종속성을 주입해준다. IObjectResolver.InjectGameObject
와 마찬가지로 [Inject]
가 달려있는 모든 필드가 주입되고 메서드가 호출된다. 다만, 프리팹을 Scene에 불러오기 위해 GameObject.Instantiate
가 아닌 Addressable와 같은 다른 방법을 사용할 경우, 그냥 그 방식을 사용한 뒤, 얻어온 GameObject
를 container.InjectGameObject
메서드로 주입하는 것을 추천하고 있다.
🔶 3-4-2. 주입하기 (Injecting / Resolving)
VContainer를 통해 의존성을 주입하기 위해서는 미리 Container에 등록을 해 두어야 한다. 등록하는 방법에 대해 알아보도록 한다. 하기 내용들은 대부분 VContainer 공식 Document를 참고해서 작성했다.
🔻 3-4-2-1. 일반 C# 타입 등록하기 (Register Plain C# Type)
일단, 예를 들기 위해 다음과 같은 타입이 있다고 생각하자.
class ServiceA : IServiceA, IInputPort, IDisposable { /* ... */ }
일단, ServiceA
자체를 등록할 수도 있다. 아래처럼 등록해둘 경우, ServiceA
를 주입받고 싶어하는 클래스가 있다면 받을 수 있게 된다. 이처럼 인터페이스나 추상 클래스가 아닌 구상 클래스를 등록하는 것을 콘크리트 타입을 등록한다 라고도 한다.
builder.Register<ServiceA>(Lifetime.Singleton);
또한, IService
인터페이스로 등록할 수도 있다. 아래처럼 등록해둘 경우, IServiceA
를 주입받고 싶어하는 클래스가 있다면 받을 수 있게 된다.
builder.Register<IServiceA, ServiceA>();
만약 여러 인터페이스로서 등록을 해 두고싶다면, 아래처럼 As
를 통해 등록할 수 있다. 아래처럼 등록하게 되면, IServiceA
뿐 아니라 IInputPort
를 주입 받고 샆어하는 클래스가 있다면 받을 수 있게 된다.
builder.Register<ServiceA>(Lifetime.Singleton)
.As<IServiceA, IInputPort>();
인터페이스가 너무 많거나, As에 일일히 다 입력하기 귀찮은 경우 AsImplementedInterfaces
를 통해 등록할 수 있다. 이럴 경우 상속받은 모든 인터페이스를 대상으로 등록하게 된다.
builder.Register<ServiceA>(Lifetime.Singleton)
.AsImplementedInterfaces();
또한 인터페이스 외에도 콘크리트 타입
까지 등록하려면, AsSelf
까지 적어주면 된다.
builder.Register<ServiceA>(Lifetime.Singleton)
.AsImplementedInterfaces()
.AsSelft();
앞에서 설명했듯, 특정 인터페이스를 구현했을 경우 Unity Lifecycle과 통합되어 등록된다. 예를 들어, 아래와 같이 IStartable
, ITickable
, IDisposable
을 구현한 클래스가 있다고 하자.
public class GameController : IStartable, ITickable, IDisposable { /* ... */ }
그럼 다음과 같이 등록하여 사용할 수 있다.
builder.RegisterEntryPoint<GameController>();
이 메서드는 사실
builder.Register<GameController>(Lifetime.Singleton).AsImplementedInterfaces()
와 똑같이 동작하게 된다. 다른 점이라면, 이 메서드는 Unity Lifecycle과 호환되어 동작하게 된다는 것이다.
EntriPoint에는 Exception Handler를 추가할 수 있다. 다음과 같이 빌더에 RegisterEntryPointExceptionHandler
메서드를 통해 Action<System.Exception>
델리게이트를 전달해 주면 된다.
builder.RegisterEntryPoint<GameController>();
builder.RegisterEntryPointExceptionHandler(ex =>
{
Debug.Log($"<color=yellow>[VContainer 예외 발생]</color>\n{ex}");
});
예를 들어, GameController
가 다음과 같다고 하자.
public class GameController : IStartable
{
[Inject]
public GameController() { /* ... */ }
public void IStartable.Start()
{
throw new Exception("GameController Start 호출중 문제 발생!");
}
}
GameController
의 EntiryPoint 관련 메서드에서 에러가 발생할 경우, 다음과 같이 잡아내는 모습을 볼 수 있다.

또는 엔트리 포인트가 여러개라면, 다음과 같이 UseEntryPoints
메서드를 사용할 수도 있다.
builder.UseEntryPoints(entryPoints =>
{
entryPoints.Add<ScopedEntryPointA>();
entryPoints.Add<ScopedEntryPointB>();
entryPoints.Add<ScopedEntryPointC>().AsSelf();
entryPoints.OnException(ex => ...)
});
VContainer
를 통해 이미 생성된 이미 생성된 인스턴스를 등록할 수도 있는데, 아래처럼 RegisterInstance
메서드를 사용하면 된다.
// ...
var obj = new ServiceA();
// ...
builder.RegisterInstance(obj);
주입이 필요할 때 생성되는 경우와 다르게, 개발자가 임의로 생성한 Instance를 등록하는 것이기 때문에, 이 방식으로 등록한 객체는 VContainer
가 수명을 조절할 수 없다는 점을 명심해야 한다.
RegisterInstane
로 등록한 객체는 언제나 Lifetime.Singleton 으로 등록된다.
등록을 진행할 때, 다음과 같이 파라미터를 직접 넘길수 도 있다. 예를들면, 팝업을 생성할 때, 팝업 이름이나 팝업 옵션등을 직접 넣어주는 것이다.
builder.Register<ISimplePopup, SimplePopup>(Lifetime.Transient)
.WithParameter<string>("아이디 확인");
또는 직접 파라미터 이름을 명시하는 것도 가능하다.
builder.Register<ISimplePopup, SimplePopup>(Lifetime.Transient)
.WithParameter("popupName", "아이디 확인");
🔻 3-4-2-2. 델리게이트 사용해서 등록하기 (Register using delegate)
Register
메서드에는 Func<IObjectResolver, TInterface>
델리게이트를 파라미터로 전달할 수 있다. 이를 통해 인스턴스를 생성하면서 등록할 수 있다.
builder.Register<ITableLoader>( _ =>
{
var skillTableLoader = new SkillTableLoader();
skillTableLoader.Initialize();
return skillTableLoader
}, Lifetime.Singleton);
델리게이트의 첫번째 파라미터인 IObjectResolver
를 통해 델리게이트 안에서 등록한 객체를 주입 받을 수 있다.
builder.Register<IGachaManager>( container =>
{
var gameSetting = contianer.Resolve<GameSetting>();
var gachaManager = new GachaManager(gameSetting.GetGachaTable());
return gachaManager;
}, Lifetime.Scoped);
IObjectResolver.Instantiate
를 사용하면 GameObject를 사용해서 생성과 동시에 등록할 수 있다. ( 물론 이 역시도 Addressable 등등을 사용한다면 비추천한다. )
builder.Register(container =>
{
return container.Instantiate(prefabGameObject);
}, Lifetime.Scope);
🔻 3-4-2-4. 팩토리 등록하기 (Register Factories)
의존성을 생성할 때마다 다른 인스턴스를 만들어야 하거나, 내부적으로 복잡한 생성 과정이 필요한 경우에는 Factory를 사용해 등록하는 것이 좋다.
VContainer
는 이 경우를 위해 RegisterFactory
메서드를 제공하고 있으며, Factory를 통해 생성된 객체 역시 DI 컨테이너의 의존성 주입 대상이 될 수 있다.
RegisterFactory
는 DI 컨테이너가 객체를 만들 때만 생성하는 Register
와 달리, 언제든지 호출 가능한 팩토리 함수를 등록할 수 있는 메서드다. 이는 객체 생성 시점이나 파라미터가 유동적인 경우에 매우 유용하다. 혹시 팩토리가 뭔지 모른다면, 팩토리 패턴에 대해 살펴보고 오면 더욱 이해하기 좋을 것이다.
- 🏭 기본 팩토리 (Factory Class 형태)
class FooFactory
{
public FooFactory(DependencyA dependencyA) { /* ... */ }
public Foo Create(int a)
{
return new Foo(a, dependencyA);
}
}
builder.Register<FooFactory>(Lifetime.Singleton);
builder.RegisterFactory(container =>
{
return container.Resolve<FooFactory>().Create
}, Lifetime.Singleton);
위 코드는 앞의 내용을 바탕으로 작성한 기본 팩토리 코드이다. FooFactory
는 생성 시점에 DependencyA
를 주입받게 되는데, Create(int a)
호출 할 때 마다 처음 받았던 DependencyA
를 사용해 Foo
를 반환하게 된다. 이는 이후 팩토리 호출 시 의존성 재조회가 없음을 의미한다.
- ⚙️ 델리게이트 팩토리 (
Func<>
형태)
builder.RegisterFactory<DependencyA, IFoo>(dependencyA =>
{
new Foo(dependencyA)
});
// 주입 예시
class ClassA
{
public ClassA(Func<DependencyA, IFoo> factory) { ... }
}
위 코드는 IFoo를 요청할 때마다 새로운 Foo
인스턴스를 생성해서 반환한다. 즉, 매번 다른 객체를 반환하고 싶은 경우 사용할 수 있다. 특징으로는 매번 생성때마다 DpendencyA
를 새로 주입받는다는 점이다. 또는 Func<IObjectResolver, Func<TParam1, T>>
을 파라미터로 넘겨줘서 의존성 + 런타임 파라미터 조합을 섞어서 사용 할 수도 있다. 아래는 외부 값(x
)과 DI된 의존성(dependencyA
)을 조합해 Foo
를 생성하는 예제이다.
builder.RegisterFactory<int, Foo>(container =>
{
var dep = container.Resolve<Dependency>();
return x =>
{
return new Foo(x, dep);
}
}, Lifetime.Scoped);
실제로, builder.RegisterFactory
메서드의 원형을 찾아가보면 다음과 같은 모습을 확인할 수 있다.

🚨 주의사항 🚨
팩토리 호출 시마다 새로운 인스턴스를 만들 수 있지만, 팩토리가 관리하는 객체의 수명은 직접 관리해야 한다. 또한
IDisposable
객체를 반환할 경우, 직접.Dispose()
해야 하며, 팩토리 클래스를 등록할 땐 팩토리 클래스가IDisposable
이면 컨테이너에 의해 정리된다.
🔻 3-4-2-5. 모노비헤이비어 컴포넌트 등록하기 (Register MonoBehaviour)
MonoBehaviour
타입은 일반 타입과 달리 씬(GameObject)에 존재하는 컴포넌트이므로, 등록/주입 방식도 조금 다르다. VContainer는 이를 위해 여러 RegisterComponent...
API를 제공하며, 이 메서드들을 통해 Auto Inject와 의존성 수명(Lifetime) 관리까지 한 번에 해결할 수 있다. 일단, 다음과 같이 LifetimeScope
의 멤버 변수를 추가하고 접근 한정자를 SerializeField private
으로 (또는 public
으로) 설정해 인스펙터에서 직접 등록할 수 있다.
[SerializeField] private PlayerController playerController;
// Configure 안에서...
builder.RegisterComponent(playerController);
앞서 설명한 RegisterInstance
와 유사하지만, 차이점은 Scene에 있는 MonoBehaviour
라도 주입 대상이 될 수 있다는 점이다. 위 에제에서는 인스펙터에 드래그 & 드랍으로 가져온 playerController
에 대해 주입과 등록을 모두 진행하게 된다. 또한 직접 드래그 & 드랍으로 등록하지 않고, Scene 내부의 MonoBehaviour
자동 탐색하여 등록할 수도 있다.
builder.RegisterComponentInHierarchy<EnemyAI>();
Scene 위에 이미 배치된 EnemyAI
컴포넌트를 찾아 등록하게 된다. 이 방식은 씬 전체 범위에서 Scoped 수명을 가지므로, 씬 전환 시 자동으로 해제된다. 프리팹에서 객체를 인스턴스화 하면서, 그 프리팹에 있는 컴포넌트를 찾아 자동으로 등록해주는 메서드도 있다.
[SerializeField] private DroppedItem dropppedItem;
// Configure 안에서...
builder.RegisterComponentInNewPrefab(dropppedItem, Lifetime.Scoped);
위 예제에서는 DroppedItem
프리팹을 DI 컨테이너가 자동으로 Instantiate후 등록 및 주입까지 해주게 된다. 굳이 프리팹이 없더라도 새로 게임 오브젝트를 생성하면서 등록하는 방법도 제공하고 있다.
builder.RegisterComponentOnNewGameObject<SpawnEffect>(Lifetime.Scoped, "SpawnFX");
위 예제 코드에서는 런타임에 new GameObject("SpawnFX")
를 자동 생성하고 SpawnEffect
컴포넌트를 붙여 등록/주입해주게 된다.
또한 위 메서드들 모두 AsImplementedInterfaces
를 통해 구현한 인터페이스들로도 등록이 가능하다.
builder.RegisterComponentInHierarchy<GameManager>()
.AsImplementedInterfaces();
위 예제는 GameManager
뿐 아니라 구현한 인터페이스 (예: IGameManager
, IManager
등등)로도 등록과 주입을 하는 코드이다. 뿐만 아니라 비슷한 방식으로 .UnderTransform(parentTransform)
메서드를 연결하면 특정 부모 아래에 생성 가능하고, .DontDestroyOnLoad()
메서드를 연결하면 씬 전환에도 파괴되지 않도록 설정할 수 있다.
🔻 3-4-2-6. 스크립터블 오브젝트 등록하기 (Register Scriptable Object)
Scriptable Object로 저장된 설정 데이터를 DI 컨테이너에 등록하면, 에셋 기반 설정 관리가 가능해지게 된다. VContainer는 이를 위해 RegisterInstance
를 활용해 스크립터블 오브젝트 내부 데이터를 주입할 수 있도록 지원하고 있다.
예를들어, 카메라 관련 상세 설정 같은 경우, Scriptable Object
로 관리할 수 있을 것이다. 예제에서 보이기 위해 간단히 다음과 같은 Scriptable Object 스크립트를 작성해 보았다.
using UnityEngine;
[CreateAssetMenu(fileName = "CameraSetting", menuName = "MyGame/CameraSettings")]
public class CameraSetting : ScriptableObject
{
public float MoveSpeed = 10f;
public float DefaultDistance = 5f;
public float ZoomMax = 20f;
public float ZoomMin = 5f;
}
CreateAssetMenu
를 통해, 손쉽게 Scriptable Object 에셋을 만들 수 있다.

이 Scriptable Object를 주입하기 위해, 다음과 같이 LifetimeScope
스크립트를 작성한다. [SerializeField]
어트리뷰트를 사용해서 (또는 public
접근 한정자를 사용해서) 인스펙터에서 전달 받을 수 있게 해놓고, RegisterInstance
를 통해 등록해두었다.
using UnityEngine;
using VContainer;
using VContainer.Unity;
public class MainCameraLifetimeScope : LifetimeScope
{
[SerializeField] private CameraSetting cameraSetting;
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterInstance(cameraSetting);
builder.RegisterEntryPoint<CameraManager>().
As<ICameraManager>();
}
}
이제 아까 만든 Scriptable Object를 MainCameraLifetimeScope
인스펙터에 할당하면 된다.

사용할때는 다른 의존성 주입받는 방법과 마찬가지로 사용하면 된다. 아래는 간단히 작성한 예제 코드이다.
using UnityEngine;
using VContainer.Unity;
public interface ICameraManager
{
void InitZoom();
}
public class CameraManager : IInitializable, ICameraManager
{
readonly CameraSetting _cameraSetting;
[Inject]
public CameraManager( CameraSetting cameraSetting )
{
_cameraSetting = cameraSetting;
}
public void IInitializable.Initialize()
{
InitZoom();
}
public void InitZoom()
{
Debug.Log($"<color=pink>cameraSetting.Zoom : {_cameraSetting.ZoomMin} ~ {_cameraSetting.ZoomMax}</color>");
}
}
실제로 에디터에서 플레이버튼을 누르면 다음과 같이 Scriptable Object에서 작성한 내용이 출력되는 모습을 확인 할 수 있다.

🔻 3-4-2-7. 컬렉션 등록하기 (Register Collection)
VContainer는 같은 타입으로 여러 개 등록된 경우, IEnumerable<T>
또는 IReadOnlyList<T>
형식으로 한 번에 묶어서 주입받을 수 있도록 지원한다.
이를 통해 Dispose
대상이나 ITickable
등 반복 처리해야 하는 여러 인스턴스를 손쉽게 받을 수 있게 된다.
builder.Register<IShutdownManagable, A>(Lifetime.Scoped);
builder.Register<IShutdownManagable, B>(Lifetime.Scoped);
위 예제처럼 동일한 타입 IDisposable
으로 A
, B
를 등록했다고 하자.
class GameShutdownHandler
{
public GameShutdownHandler(IEnumerable<IShutdownManagable> shutdownManagables)
{
foreach (var shutdownManagable in shutdownManagables)
{
shutdownManagable.Shutdown();
}
}
}
이때 위 코드처럼 GameShutdownHandler
에서 IEnumerable<IShutdownManagable>
를 통해 (또는 IReadOnlyList<IShutdownManagable>
를 통해) 두 객체를 한 묶음으로 받아올 수 있게 된다.
[ ❓ ] 그럼 여러개를 등록하고, 하나만 주입 받으면 어떻게 될까?
실험 결과, 위 예제에서
IEnumerable<IShutdownManagable>
이 아니라IShutdownManagable
을 주입받으면, 맨 마지막에 등록한 객체 하나만 (예제에서는B
만) 주입받게 된다.
🔻 3-4-2-8. 등록후 콜백 설정하기 (Register Callback)
VContainer는 컨테이너가 구성되거나 폐기될 시점에 특정 로직을 실행할 수 있는 콜백 등록 기능을 제공하고 있다.
builder.RegisterBuildCallback(container =>
{
// 이 시점에 추가 초기화 로직 작성 가능
});
해당 델리게이트는 컨테이너가 완전히 초기화된 Build
시점에 호출된다. 콜백 인자로 IObjectResolver container
가 전달되어, 이 시점에 Resolve
, Inject
API 사용이 가능한다.
builder.RegisterDisposeCallback(container =>
{
// 컨테이너 수명 종료 시 실행할 로직
});
LifetimeScope
나 컨테이너 자체가 파괴될 때 호출된다. Dispose 직전에 리소스 정리나 로그 기록 등을 수행할 때 사용하면 좋다고 한다.
🔶 3-4-3. 통합 (Intergration)
VContainer는 현업에서 자주 사용되는 다양한 3rd-Party 라이브러리들과 다양한 연계를 지원하고 있다. 주로 UniTask
, UniRx
, ECS
등등이 있다.
🔻 3-4-3-1. VContainer + UniTask
UniTask는 Unity에 최적화된 async / await 라이브러리로, VContainer와 연동 시 비동기 진입점을 직관적으로 처리할 수 있게 해준다. 처음에 VContainer를 프로젝트에 설치 할 때, VCONTAINER_UNITASK_INTEGRATION
커스텀 스크립팅 심볼이 자동 활성화되어, IAsyncStartable
을 구현할 수 있게된다. 예를들어, 다음처럼 등록된 GameInitialFlowManager
가 있다고 하자.
// 등록
builder.RegisterEntryPoint<GameInitialFlowManager>();
그럼 아래처럼 StartAsync
메서드가 비동기로 호출된다.
public class GameInitialFlowManager : IAsyncStartable
{
public async UniTask IAsyncStartable.StartAsync(CancellationToken cancellation)
{
await LoadGameAssetsAsync(cancellation);
await InitializePlayerAsync(cancellation);
// ...
}
}
StartAsync
메서드는 LifetimeScope
시작 시 자동 호출되며, CancellationToken
은 해당 Scope가 파괴되면 자동으로 취소된다.
🔻 3-4-3-2. VContainer + UniRX
UniRX는 는 Unity 환경에 최적화된 Reactive Extension 라이브러리이다. VContainer
와 함께 사용하면 DI 기반으로 리액티브 패턴을 쉽게 설계할 수 있다. 아래는 VContainer의 IStartable
과 IDisposable
인터페이스를 구현한 클래스에서 UniRX
를 적용한 예이다.
public class EnemySpawner : IStartable, IDisposable
{
readonly CompositeDisposable disposables = new CompositeDisposable();
void IStartable.Start()
{
spawnTimer
.Where(_ => canSpawn) // 소환가능한 타이머가 있으면
.Subscribe(_ => SpawnEnemy()) // 적을 소환한다.
.AddTo(disposables);
}
void IDisposable.Dispose() => disposables.Dispose();
void SpawnEnemy() { /* ... */ }
}
EnemySpawner
클래스는 IStartable.Start
메서드에서 게임 시작시 spawnTimer 스트림을 구독해 적을 스폰하는 리액티브 로직을 실행하고, IDisposable.Dispose
메서드에서 컨테이너가 파괴될 때 모든 구독을 CompositeDisposable
을 통해 안전하게 해제한다.
🔻 3-4-3-3. VContainer + Unity Entities
Unity Entities는 Entity - Component - System구조를 통해 고성능 데이터 중심 프로그래밍을 가능하게 해주는 Unity의 DOTS
핵심 구성 요소이다.
VContainer
는 Unity의 Entities
와의 연동을 공식 지원하며, 실험적(beta) 기능으로 ECS 기반 시스템에 DI를 적용할 수 있게 도와줍니다.
Unity 2019.3
이상, com.unity.entities
패키지가 설치되어 있어야 사용 가능하다.
일단 다음과 같이 SystemBase를 상속받은 SystemA
가 있다고 하자.
class SystemA : SystemBase
그럼 다음과 같이 SystemA
를 등록할 수 있게 된다.
builder.RegisterSystemFromDefaultWorld<SystemA>();
여러 시스템을 한번에 등록하고 싶다면, UseDefaultWorld
를 사용하면된다.
builder.UseDefaultWorld(systems =>
{
systems.Add<SystemA>();
systems.Add<SystemB>();
});
4 | 마치며

VContainer
를 공부하면서 느낀 점은, Unity라는 환경이 가진 제약 속에서도 DI 구조를 실용적으로 구현할 수 있는 방법이 분명히 존재한다는 것이다. 기존에는 Unity의 생명주기나 MonoBehaviour
기반의 구조 때문에 IoC
를 적용하기가 막막했지만, VContainer
는 그런 한계들을 자연스럽게 녹여내면서도 강력한 설계 유연성을 제공한다. 특히 LifetimeScope
를 중심으로 한 구조 덕분에 의존성의 범위나 수명을 명확히 관리할 수 있어, 기존 Unity 코드에서 보기 어려웠던 역할 분리, 모듈화, 테스트 가능성 등을 확보할 수 있게 된다.
또한, 단순히 DI 컨테이너 기능을 넘어서 Unity와의 라이프사이클 통합, 자동 주입, Zero GC 지향, 다양한 EntryPoint 지원 등은 실무 개발자 입장에서 정말 실용적인 포인트였다. 실제로 몇몇 시스템에 적용해보니, View
와 Presenter
의 책임이 명확히 나뉘고, 테스트 코드도 훨씬 간결하게 작성할 수 있었다. 그리고 Scope를 기준으로 설계가 정리되면서, 리소스 해제나 씬 전환 시점 관리 등도 수월해졌다.
물론 처음 접할 땐 다소 생소하고, DI의 철학 자체가 익숙하지 않은 경우엔 도리어 복잡하게 느껴질 수도 있다. 하지만 글에서 다뤘듯이, 예제를 따라가며 구조를 하나씩 쪼개어 보는 경험을 하다 보면, 점차 왜 필요한지 / 어디에 쓰이는지가 분명히 와 닿을 것이다. 특히, 프로젝트의 규모가 커지고, 유지보수가 복잡해질수록 VContainer
를 활용한 DI 구조는 빛을 발할 것이다.
앞으로는 VContainer
와 관련된 고급 사용법이나, 실무에서 사용해볼 수 있는 몇 가지 패턴 예제, 다른 DI 프레임워크와의 비교 등을 추가로 정리해볼 계획이다. Unity에서도 제대로 된 설계 원칙을 적용하고 싶은 개발자라면, VContainer
는 분명 좋은 출발점이 될 것이다.