[Unity/VContainer] 핵심 정리

안건우·2025년 10월 28일

Container를 사용하면서 자주 헷갈리거나 다시 찾아보게 되는 핵심 개념 3가지를 정리한다.


목차

  1. 생명주기 인터페이스: MonoBehaviour를 넘어서
  2. 의존성 주입의 3가지 방법: [Inject] 어노테이션
  3. 의존성 등록: VContainer에게 작업 지시하기

1. 생명주기 인터페이스: MonoBehaviour를 넘어서

VContainer는 MonoBehaviour가 아닌 순수 C# 클래스(POCO)도 Unity의 생명주기(Awake, Start, Update 등)에 맞춰 동작할 수 있도록 다양한 인터페이스를 제공한다. EntryPoint나 특정 인터페이스를 구현한 클래스는 VContainer가 자동으로 적절한 시점에 해당 메소드를 호출해준다.

인터페이스Unity 대응호출 시점주요 용도
IInitializableAwake()객체 생성 직후, 주입 직후자신의 내부 상태 초기화 (다른 객체 의존 X)
IStartableStart()모든 객체 생성/주입 완료 후다른 매니저가 필요한 복잡한 초기화, 로직 시작
IAsyncStartableasync Start()IStartable과 동일 시점서버 통신, 파일 로딩 등 비동기 초기화
ITickableUpdate()매 프레임게임 루프, 입력 처리 등
IFixedTickableFixedUpdate()고정 시간 간격물리 계산, Rigidbody 조작
ILateTickableLateUpdate()Update() 끝난 후카메라 추적 등 후처리 로직
IDisposableOnDestroy()LifetimeScope 파괴 시이벤트 구독 해제, 리소스 해제 등 정리 작업

핵심: EntryPoint 클래스에 이 인터페이스들을 구현하면, MonoBehaviour 없이도 게임의 핵심 로직과 루프를 완벽하게 구성할 수 있다.


2. 의존성 주입의 3가지 방법: [Inject] 어노테이션

[Inject] 어노테이션은 VContainer에게 "이곳에 의존성을 주입해줘!"라고 알려주는 표시다. 위치에 따라 3가지 방식이 있으며, 각각의 장단점이 명확하다.

① 생성자 주입 (Constructor Injection) ⭐ Best Practice

클래스의 생성자를 통해 의존성을 주입받는 방식. VContainer에서 가장 권장된다.

public class GameController
{
    // readonly로 불변성 보장!
    private readonly IAudioManager _audioManager;
    private readonly IInputManager _inputManager;

    // 생성자를 통해 의존성을 '요구'한다.
    // 생성자가 하나뿐이면 [Inject] 어노테이션 생략 가능.
    public GameController(IAudioManager audioManager, IInputManager inputManager)
    {
        _audioManager = audioManager;
        _inputManager = inputManager;
    }
}

장점:

  • 불변성: readonly 키워드로 의존성이 변경될 위험을 원천 차단한다. (가장 안전)
  • 명확성: 생성자만 봐도 이 클래스가 동작하기 위해 무엇이 필요한지 명확히 알 수 있다.
  • 테스트 용이성: new GameController(new MockAudio(), new MockInput()) 처럼 가짜(Mock) 객체를 쉽게 넣어 단위 테스트를 할 수 있다.

② 메소드 주입 (Method Injection)

일반 메소드를 통해 의존성을 주입받는 방식. MonoBehaviour에 주입할 때 사실상의 표준이다.

public class PlayerView : MonoBehaviour
{
    private IEffectManager _effectManager;

    // MonoBehaviour는 new로 생성하지 않으므로,
    // 생성자 대신 이 메소드로 주입받는다.
    [Inject]
    public void Construct(IEffectManager effectManager)
    {
        _effectManager = effectManager;
    }

    void Start()
    {
        // Start 시점에는 Construct가 이미 호출되었음이 보장된다.
        _effectManager.PlaySpawnEffect();
    }
}

장점: 씬에 이미 배치된 MonoBehaviour에 의존성을 주입하는 표준적인 방법이다.

주의: 객체 생성 직후 ~ Construct 메소드 호출 전까지 아주 잠깐 필드가 null인 상태가 존재한다. (Awake에서 접근 시 NullReferenceException 위험!)

③ 필드 주입 (Field Injection)

클래스의 멤버 필드(변수)에 직접 주입하는 방식. 가급적 사용을 피하는 것이 좋다.

public class Enemy : MonoBehaviour
{
    [Inject]
    private IGameStatus _gameStatus; // 필드에 직접 주입
}

단점:

  • 의존성 은닉: 외부에서 이 클래스가 무엇을 필요로 하는지 파악하기 어렵다.
  • 테스트 어려움: private 필드에 가짜 객체를 주입하기 매우 번거롭다.

결론:

  • 순수 C# 클래스(POCO): 무조건 생성자 주입을 사용한다.
  • MonoBehaviour: 메소드 주입을 사용한다.

3. 의존성 등록: VContainer에게 작업 지시하기

LifetimeScopeConfigure 메소드 안에서 builder.Register...()를 통해 VContainer에게 어떤 객체를 어떻게 관리할지 지시한다.

등록 방식설명주요 사용처
builder.Register<T>(lifetime)가장 기본. 순수 C# 클래스를 등록. VContainer가 new T()로 직접 생성.AudioManager, DataManager 등 모든 서비스/매니저 클래스.
builder.RegisterComponentInHierarchy<T>()씬에 이미 배치된 MonoBehaviour를 찾아서 관리.씬에 미리 만들어둔 UIManager, Player 등.
builder.RegisterInstance(instance)이미 생성된 객체를 VContainer에게 넘겨서 관리시킴.ScriptableObject 설정 파일, 외부 라이브러리 객체.
builder.RegisterEntryPoint<T>()능동적으로 실행되는 시작점을 등록. VContainer가 생성부터 생명주기까지 관리.GameController, InputProcessor 등 게임 로직을 시작하고 실행하는 클래스.
builder.RegisterComponentInNewPrefab(prefab)Prefab을 인스턴스화하면서 주입.동적으로 생성되는 적, 아이템, UI 요소 등 Prefab 기반 객체.

요약:

  • 일반 로직 클래스: Register<T>
  • 씬에 이미 있는 컴포넌트: RegisterComponentInHierarchy<T>
  • 게임 로직의 시작과 실행: RegisterEntryPoint<T>

이 세 가지 핵심 개념만 확실히 이해하고 있어도 VContainer를 사용하여 대부분의 상황에 유연하게 대처하고, 깨끗하고 테스트하기 쉬운 코드를 작성할 수 있다.

0개의 댓글