DevDino

VContainer를 사용한 의존성 주입(Dependency Injection) 도입 본문

개발 일지

VContainer를 사용한 의존성 주입(Dependency Injection) 도입

데브다이노 2026. 3. 23. 16:28

처음에는 프로젝트 내 주요 클래스에만 싱글톤을 썼지만, 나중에는 참조하기 편하다는 이유로 남용하곤 했습니다. 하지만 프로젝트 규모가 커지면서 기존에 사용하던 싱글톤 패턴의 한계가 명확해졌습니다.

 

클래스 간 결합도가 높아져 수정 사항이 생길 때마다 참조하는 코드들을 일일이 찾아 고쳐야 했습니다. 게다가 싱글톤은 게임이 시작될 때부터 끝날 때까지 메모리에 남아 있다 보니, 초기화 순서가 꼬여 에러가 나거나 메모리 관리가 제대로 되지 않는 문제도 잦았습니다. 그렇다고 임의로 생명주기를 건드리면, 다른 클래스에서 언제 해당 싱글톤을 호출할지 몰라 참조 오류가 날 위험이 생겼습니다. 

 

이러한 문제를 해결하기 위해 의존성 주입(DI, Dependency Injection)을 도입했습니다. 이번 글에서는 유니티 환경에서 DI 프레임워크인 VContainer를 프로젝트에 적용하며 겪은 경험을 정리해 보았습니다. 

 

VContainer 1.17.0 환경 기준입니다.

 

1. 설치

https://github.com/hadashiA/VContainer

 

GitHub - hadashiA/VContainer: The extra fast, minimum code size, GC-free DI (Dependency Injection) library running on Unity Game

The extra fast, minimum code size, GC-free DI (Dependency Injection) library running on Unity Game Engine. - hadashiA/VContainer

github.com

VContainer 깃허브의 Release 탭에서 최신 Source code.zip을 다운로드합니다.

압축을 푼 후 생성된 폴더를 유니티 프로젝트의 Assets 폴더에 이동시키면 사용 가능합니다.

 

2. LifetimeScope 생성 및 관리

VContainer의 모든 의존성 관리는 LifetimeScope를 통해 이루어집니다. 이는 특정 범위 내에서 부품을 생성하고 조립하는 공장 역할을 합니다.

이 스코프 내에서 어떤 클래스들을 컨테이너에 등록하고 주입할지 선언합니다.

public class GameLifeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // 부품 등록(싱글톤).
        builder.Register<Translator>(Lifetime.Singleton);

        // 씬에 이미 존재하는 MonoBehaviour를 등록
        builder.RegisterComponentInHierarchy<PanelManager>();
    }
}

 

 

이렇게 등록된 객체는 주입이 허용된 클래스에서 [Inject] 어트리뷰트를 통해 싱글톤처럼 편리하게 참조할 수 있습니다.

public class PanelManager : MonoBehaviour
{
    [Inject]
    Translator _translator;
}

 

3. 부모 Scope를 이용한 전역 데이터 관리

전역적으로 유지해야 하는 시스템과 씬 개별적으로 유지해야하는 시스템의 생명주기를 관리하기 위해 부모 Scope와 자식 Scope를 따로 두어 관리하였습니다.

 

1. 유니티 상단 메뉴에서 Create > VContainer > VContainer Settings에서 설정 에셋을 생성합니다.

2. 전역에서 관리할 스크립트(ProjectLifetimeScope)를 작성하고 빈 오브젝트에 붙인 후 프리팹으로 만듭니다.

3. 생성한 VContainer Settings 에셋의 Root Lifetime Scope 칸에 해당 프리팹을 할당합니다.

 

using VContainer;
using VContainer.Unity;

public class ProjectLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        base.Configure(builder);
        builder.Register<ISaveService, JsonSaveService>(Lifetime.Singleton);
        builder.Register<TableDataManager>(Lifetime.Singleton).AsImplementedInterfaces().AsSelf();
        builder.Register<GameDataManager>(Lifetime.Singleton);
        builder.Register<Translator>(Lifetime.Singleton);
    }
}

 

이후 각 씬마다 개별 LifetimeScope를 배치하면 부모 Scope의 의존성을 물려받으면서 씬 단위의 주입을 관리할 수 있습니다.

using VContainer;
using VContainer.Unity;

public class InitSceneScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        base.Configure(builder);
        builder.RegisterComponentInHierarchy<InitSceneManager>();
    }
}
using UnityEngine;
using VContainer;
using VContainer.Unity;

public class PlaySceneScope : LifetimeScope
{
    [SerializeField]
    FocusManager _focusManager;

    protected override void Configure(IContainerBuilder builder)
    {
        base.Configure(builder);
        builder.RegisterEntryPoint<QuestManager>(Lifetime.Singleton).AsSelf();

        builder.Register<EconomyManager>(Lifetime.Singleton);

        builder.RegisterComponent(_focusManager);
        // ...
    }
}

 

씬에 배치한 Scope 컴포넌트의 Auto Inject Game Objects 리스트에 Canvas나 관리할 최상위 오브젝트를 넣어두면 하위 클래스들 중 [Inject]가 선언된 곳에 자동으로 의존성이 주입됩니다.

세팅 과정에서 직렬화 에러가 나면 유니티를 껐다가 다시 켜보세요!

 

4. 프리팹 동적 생성 시 참조 오류

Awake 시점에서 초기화를 진행하다 보니, 게임 도중 프리팹을 동적으로 생성할 때 주입이 되지 않아 참조 오류(NullReference)가 발생하는 이슈가 있었습니다.

이런 예외적인 상황에서는 LifetimeScope를 직접 찾아 수동으로 주입해 주는 방식으로 해결했습니다.

var scope = FindFirstObjectByType<LifetimeScope>();

if(scope != null && scope.Container != null)
{
    scope.Container.Inject(this);
}

 

5. 마무리 및 적용 후기

처음 DI 개념을 이해하고 아키텍처를 변경하는 과정은 낯설었지만, 막상 적용하고 나니 개발 과정에서 체감되는 이점이 많았습니다.

 

가장 만족스러운 부분은 결합도 완화와 확장성입니다. 예를 들어 광고 서비스를 인터페이스로 분리해 두었기 때문에, 나중에 다른 광고 플랫폼으로 교체하더라도 Scope의 선언부 단 한 줄만 바꾸면 됩니다. DI 개념을 모르고 진행한 예전 프로젝트에서는 광고 플랫폼을 교체하느라 참조 코드를 일일이 찾아 바꿨었습니다.

 

그리고 그동안은 주요 시스템을 싱글톤으로 만들자니 생명주기 관리가 안되고, 매번 직접 참조하자니 반복 작업과 휴먼 에러가 늘 부담이었습니다. VContainer를 도입한 후에는 씬 단위로 Scope를 정의해 원하는대로 생명주기를 통제할 수 있게 되었고, 직관적인 문법 덕분에 편하고 빠르게 DI 환경을 구축했었습니다. 

 

하지만 DI 역시 주입되는 객체가 많아질수록 내부적으로 의존성이 어떻게 연결되어 있는지 추적이 어렵다는 단점이 있습니다. 이후 한계에 부딪힌다면 다른 아키텍처 도입을 고민해 보려 합니다.