지난 글에서는 DI(Dependency Injection) 개념과 Unity환경에서 왜 필요한지를 정리했다.(꼭 유니티 뿐만 아니죠)
이번에는 VContainer를 실제 프로젝트에 적용해, 로그인 흐름을 깔끔하고 테스트하기 쉬운 구조로 개선해보자
1. VContainer는 무엇인가?
VContainer는 Unity용 DI(의존성 주입) 프레임워크다.
쉽게 말하면, "객체를 직접 생성하지 않고도 필요한 인스턴스를 주입받게 해주는 시스템"이다.
기존 방식
// LoginMananger.cs
var auth = new AuthService();
var login = new LoginManager(auth);
-> LoginManager는 AuthService에 직접적인 의존관계가 형성된다.
즉, 나중에 auth를 교체하려고 할때 코드를 직접 수정해야한다.
VContainer 방식
public class LoginManager
{
private readonly IAuthService _auth;
public LoginManager(IAuthService auth)
{
_auth = auth;
}
}
LoginManager가 이러한 구조일때
protected override void Configure(IContainerBuilder builder)
{
#if UNITY_EDITOR || MOCK
builder.Register<IAuthService, MockAuthService>(Lifetime.Singleton);
#else
builder.Register<IAuthService, RealAuthService>(Lifetime.Singleton);
#endif
builder.Register<LoginManager>(Lifetime.Singleton);
}
이런식으로, 코드 변경 없이 환경(MockAuthService/RealAuthService)을 전환할 수 있다.
의존성 해제 OK, 근데 인터페이스와 뭐가 다른데?
DI를 그냥 인터페이스로 나누는 것과, VContainer로 주입하는 것의 본질적 차이는
아래 예시를 보고 이해해보자
public class LoginManager
{
private IAuthService _auth;
public LoginManager()
{
_auth = new MockAuthService(); //직접 생성
}
}
여기서 LoginMananger는 여전히 MockAuthService의 구현체를 알고 있다.
즉, "의존성의 방향"은 인터페이스로 향하지만 "생성"은 여전히 LoginMananger내부에 있다.
결국 누가 어떤 구현체를 쓸지 코드안에서 직접 결정하기 때문에 테스트 전환및 유지보수가 어렵다.
VContainer는 생성책임을 외부로 분리한다.
public class LoginManager
{
private readonly IAuthService _auth;
public LoginManager(IAuthService auth) // 주입받음
{
_auth = auth;
}
}
이제 LoginMananger는 어떤 구현체가 들어올지 모른다.
VContainer 주입방법
DI컨테이너 "어떤 인터페이스에 어떤 구현체를 주입할지"를 관리해주는 객체 생성 관리자다.
이 과정을 LifetimeScope에서 정의한다.
// LoginLifetimeScope : LifetimeScope
protected override void Configure(IContainerBuilder builder)
{
builder.Register<IAuthService, RealAuthService>(Lifetime.Singleton);
builder.Register<LoginManager>(Lifetime.Singleton);
}
LifetimeScope란?
의존성의 생명주기를 관리하는 단위다. 쉽게 말하면 DI컨테이너의 실행 범위를 정의하는 클래스다.
Container : 객체를 생성하고, 의존성을 주입해주는 관리자
LifeTimeScope : 그 컨테이너 작동하는 "범위"를 정의(씬 단위, 게임 전체)
builder.Register() : 어떤 타입을 어떤 Lifetime으로 관리할지 등록
Lifetime.Singleton : 게임이 실행되는 동안 하나의 인스턴스로 유지
Lifetime.Transient : 매번 새 인스턴스를 생성
Lifetime.Scoped : 현재 스코프 안에서만 동일 인스턴스 유지
객체 라이프사이클에 맞게 선택해서 사용하면 된다.
주입예시
로그인 샘플 프로젝트. 코드 전체
public class LoginLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
var initialEnv = ServerEnv.DEV; // 기본값
// EnvironmentService 싱글톤 등록
var envService = new EnvironmentService(initialEnv);
builder.RegisterInstance<IEnvironmentService>(envService);
builder.RegisterInstance(initialEnv);
builder.Register<AuthService>(Lifetime.Singleton);
builder.Register<MockService>(Lifetime.Singleton);
builder.Register<IAuthService, AuthServiceRouter>(Lifetime.Singleton);
builder.Register<IUserService, UserService>(Lifetime.Scoped);
builder.Register<LoginWork>(Lifetime.Transient);
builder.Register<LogoutWork>(Lifetime.Transient);
//mono
builder.RegisterComponentInHierarchy<LoginPanel>();
builder.RegisterComponentInHierarchy<UIHUDInfo>();
}
}
1) IEnvironmentService (Singleton, Instance 주입)
var initialEnv = ServerEnv.DEV;
var envService = new EnvironmentService(initialEnv);
builder.RegisterInstance<IEnvironmentService>(envService);
builder.RegisterInstance(initialEnv); // 필요 시 enum 자체도 주입
- AuthServiceRouter.cs가 현재 환경(DEV/LIVE)을 보고 Real/Mock중 어디로 보낼지 결정한다.
- 런타임에 envService.SetEnv(ServerEnv.REAL) 처럼 환경을 바꿀 수 있도록 되어있다.
2) IAuthService → AuthServiceRouter
builder.Register<AuthService>(Lifetime.Singleton); // Real
builder.Register<MockService>(Lifetime.Singleton); // Mock
builder.Register<IAuthService, AuthServiceRouter>(Lifetime.Singleton); // 라우터
- 주입 지점 : LoginWork의 생성자에서 IAuthService로 주입받음
- 동작 방식 : AuthServiceRouter가 IEnvironmentService의 현재 값을 확인하고 내부적으로 AuthService(Real) 혹은 MockService(Mock)로 위임 호출
- 의미 : 등록은 고정, 전환은 환경 값만 변경해서 처리(코드 수정없음)
//더보기 AuthServiceRouter.cs
더보기
public sealed class AuthServiceRouter : IAuthService
{
private readonly IEnvironmentService _env;
private readonly AuthService _real; // LIVE용
private readonly MockService _mock; // DEV/QA용
public AuthServiceRouter(IEnvironmentService env, AuthService real, MockService mock)
{
_env = env;
_real = real;
_mock = mock;
}
3) IAuthService → AuthServiceRouter
builder.Register<IUserService, UserService>(Lifetime.Scoped);
- scoped : 씬 교체 시 정리(ex 유저 세션)
- LoginWork생성자에서 IUserService주입 -> 로그인 성공 후 유저 데이터 로드에 사용
4) LoginWork / LogoutWork (Transient, “작업 단위”)
builder.Register<LoginWork>(Lifetime.Transient);
builder.Register<LogoutWork>(Lifetime.Transient);
- Transient : 로그인/로그아웃은 매번 새 인스턴스로 실행
public class LoginWork : IWork<LoginParam, LoginResult>
{
private readonly IAuthService _authService; // 라우터로 주입
private readonly IUserService _userService; // Scoped로 주입
public LoginWork(IAuthService authService, IUserService userService) { ... }
public async UniTask<LoginResult> ExecuteWorkAsync(LoginParam p)
{
var token = await _authService.LoginAsync(p.UserID, p.Password);
var user = await _userService.LoadUserDataAsync(p.UserID, token);
...
}
}
- UI에서 주입받아 ExecuteWorkAsync 실행용 트리거
5) MonoBehaviour 주입 (LoginPanel, UIHUDInfo)
builder.RegisterComponentInHierarchy<LoginPanel>();
builder.RegisterComponentInHierarchy<UIHUDInfo>();
- 씬에 이미 존재하는 MonoBehaviour들을 컨테이너 관리하에 주입한다.
- [Inject]을 꼭 써줘야한다.
//더보기 LoginPanel.cs 사용 예시
더보기
[Inject] //mono
public void Construct(LoginWork loginWork)
{
_loginWork = loginWork;
Debug.Log("Construct");
}
정리
이번 글에서는 VContainer의 핵심 개념과 실제 프로젝트 적용 예시를 통해 알아봤다.
프로젝트 구현 요약
- LoginPanel/UIHUDInfo는 RegisterComponentInHierarchy 덕분에 [Inject]가 꽂힌다.
- LoginWork는 Transient로 주입되어 로그인 버튼 클릭 시 매번 “깨끗한 작업”을 실행한다.
- IAuthService는 AuthServiceRouter를 통해 환경(DEV/REAL) 에 맞춰 Real/Mock으로 자동 라우팅된다.
- IUserService는 Scoped로 씬 교체 시 자연스럽게 수명이 종료된다.
다음 포스팅에는
실제로 로그인버튼 클릭했을때 동작 방식과 결과를 확인해보자