VContainer
VContainer는 Unity를 위한 경량화된 DI(Dependency Injection) 컨테이너이다.
DI는 객체 간의 의존성을 외부에서 주입하는 디자인 패턴으로, 코드의 결합도를 낮추고 유지보수성을 향상시키는데 도움을 준다.
주요 특징
- 가볍고 빠른 성능
- Unity의 컴포넌트 시스템과의 원활한 통합
- 직관적인 API 제공
- 코드의 모듈화와 테스트 용이성 향상
https://vcontainer.hadashikick.jp/
기본 구성요소
LifetimeScope
LifetimeScope는 VContainer의 핵심 컴포넌트로, 의존성의 생명주기를 관리한다.
public class GameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.Register<IGameManager, GameManager>(Lifetime.Singleton);
builder.Register<IUIManager, UIManager>(Lifetime.Singleton);
builder.Register<ISoundManager, SoundManager>(Lifetime.Singleton);
}
}
의존성 주입 방식
- 생성자 주입
public class GameManager
{
private readonly IUIManager _uiManager;
public GameManager(IUIManager uiManager)
{
_uiManager = uiManager;
}
}
- 필드 주입
public class GameController : MonoBehaviour
{
[Inject]
private IGameManager _gameManager;
}
Lifetime 옵션
- Singleton: 전체 애플리케이션에서 단일 인스턴스
- Scoped: 현재 스코프 내에서만 단일 인스턴스
- Transient: 매번 새로운 인스턴스
추가적으로 VContainer에서 자동으로 주입하는 객체가 MonoBehaviour를 상속받은 객체라면 에디터에서 경고 메시지가 출력될 것이다.
이는 VContainer가 기본적으로 new라는 키워드를 사용하여 객체를 생성하기 때문이다.
MonoBehaviour는 new라는 키워드를 사용하여 생성하게 되면 경고 메시지를 출력한다.
따라서, 이를 처리하기 위해서는 다음과 같이 진행하면 된다.
public class GameLifetimeScope : LifetimeScope
{
[SerializeField]
private PlayerComponent playerPrefab;
[SerializeField]
private GameObject gameObjectWithComponents;
protected override void Configure(IContainerBuilder builder)
{
// 1. 프리팹의 컴포넌트 등록
builder.RegisterComponent(playerPrefab);
// 2. GameObject에 있는 특정 컴포넌트 등록
builder.RegisterComponent(gameObjectWithComponents.GetComponent<PlayerComponent>());
// 3. 씬에 이미 존재하는 컴포넌트 등록
builder.RegisterComponent(FindObjectOfType<PlayerComponent>());
// 4. 인터페이스로 등록
builder.RegisterComponent<IPlayerComponent>(playerPrefab);
// 5. GameObject의 모든 컴포넌트 등록
builder.RegisterComponentInHierarchy<PlayerComponent>();
}
}
장점과 단점
장점
- 의존성 관리 용이성
- 객체 생성과 의존성 해결을 자동화
- 순환 참조 방지
- 코드의 결합도 감소
- 테스트 용이성
- 목업(Mock) 객체 주입이 쉬움
- 단위 테스트 작성 용이
- 컴포넌트 격리 테스트 가능
- 확장성
- 새로운 기능 추가가 용이
- 기존 코드 수정 최소화
- 모듈식 개발 지원
단점
- 학습 곡선
- DI 개념 이해 필요
- 설정 방법 숙지 필요
- MonoBehaviour 제약
- Unity 컴포넌트 생성 시 특별한 처리 필요
- new 키워드 사용 불가
- 초기 설정 부담
- LifetimeScope 설정 필요
- 의존성 관계 명시적 정의 필요
활용 방안
매니저 시스템 구축
public interface IGameManager
{
void Initialize();
void StartGame();
void PauseGame();
}
public class GameManager : IGameManager
{
private readonly IUIManager _uiManager;
private readonly ISoundManager _soundManager;
public GameManager(IUIManager uiManager, ISoundManager soundManager)
{
_uiManager = uiManager;
_soundManager = soundManager;
}
}
모듈식 시스템 설계
여러 LifetimeScope를 활용한 모듈화:
// 루트 스코프
public class RootLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.Register<IDataManager, DataManager>(Lifetime.Singleton);
}
}
// 게임 스코프
public class GameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.Register<IGameManager, GameManager>(Lifetime.Singleton);
}
}
팩토리 패턴 구현
MonoBehaviour 컴포넌트 생성을 위한 팩토리:
public class ComponentFactory<T> where T : Component
{
private readonly IObjectResolver container;
private readonly T prefab;
public ComponentFactory(IObjectResolver container, T prefab)
{
this.container = container;
this.prefab = prefab;
}
public T Create()
{
var instance = GameObject.Instantiate(prefab);
container.InjectGameObject(instance.gameObject);
return instance;
}
}
실제 적용 예시
데이터 매니저 시스템
public interface IDataManager
{
void SaveGameData();
void LoadGameData();
T GetData<T>(string key) where T : class;
}
public class DataManager : IDataManager
{
private readonly IPlayerDataRepository _playerDataRepository;
public DataManager(IPlayerDataRepository playerDataRepository)
{
_playerDataRepository = playerDataRepository;
}
}
UI 시스템
public interface IUIManager
{
void ShowWindow<T>() where T : UIWindow;
void HideWindow<T>() where T : UIWindow;
}
public class UIManager : IUIManager
{
private readonly Dictionary<Type, UIWindow> _windows;
private readonly IObjectResolver _container;
public UIManager(IObjectResolver container)
{
_container = container;
_windows = new Dictionary<Type, UIWindow>();
}
}