UNInject
UNInject – 고성능 Unity 의존성 주입 SDK
현재 버전 : 2.1.0
UNInject는 Unity를 위해 설계된 고성능 의존성 주입 프레임워크입니다.
에디터 시점 베이크, Roslyn 코드 생성, 3 Tier 스코프,partial기반의 리플렉션 없는 런타임 경로를 제공합니다.
https://github.com/NightWish-0827/UNInject.git?path=/com.nightwishlab.uninject
UPM에서 git URL로 패키지를 추가하시면 됩니다.
주요 기능
에디터 베이크 아키텍처
ObjectInstaller는 [Inject] 필드를 에디터 단계에서 계층 구조에 대해 미리 처리합니다.
연결 정보가 직렬화되어 저장되므로, 런타임에 계층 구조 스캔이 발생하지 않습니다.
Roslyn 코드 생성 — IL2CPP 완전 지원
Roslyn 코드 생성 파이프라인을 통해 기존 Expression Tree 방식의 IL2CPP/AOT 환경 제약을 해소했습니다.
[GlobalInject] / [SceneInject] 필드를 가진 클래스를 컴파일 시점에 자동 탐지하여,
partial 클래스 확장 코드와 플랜 등록 코드를 자동 생성합니다.
생성된 코드는 Expression.Compile()을 사용하지 않으므로 모든 AOT 플랫폼에서 안전하게 동작합니다.
사용자에게 필요한 유일한 변경사항은 partial 키워드 추가입니다.
// 변경 전
public class PlayerController : MonoBehaviour { ... }
// 변경 후
public partial class PlayerController : MonoBehaviour { ... }
partial 없이 플레이 모드에 진입하면 UNInjectFallbackGuard가 경고를 출력하며,
Roslyn 컴파일 시점에도 UNI001 경고가 발행되어 IDE 수준에서 바로 확인할 수 있습니다.
3단계 결정론적 스코프
의존성의 생명 주기를 명확하게 관리하기 위해 세 가지 스코프 계층을 제공합니다.
| 스코프 | 컴포넌트 | 생명 주기 |
|---|---|---|
| Global | MasterInstaller |
DontDestroyOnLoad |
| Scene | SceneInstaller |
현재 씬 |
| Local | ObjectInstaller |
인스톨러 루트 하위 서브트리 |
씬 언로드 동작은 SceneInstaller의 SceneExitPolicy 로 제어됩니다.
Clear— 소멸 시 레지스트리를 초기화합니다 (기본값).Preserve— 언로드 이후에도 레지스트리 항목을 유지합니다 (예: 추가 로딩 시).
런타임 주입 경로
주입 수행 시 우선순위 순으로 세 단계를 거칩니다.
| 우선순위 | 경로 | 비고 |
|---|---|---|
| 1 | Roslyn 생성 플랜 | 딕셔너리 조회 + AggressiveInlining 세터 |
| 2 | Expression Tree 폴백 | 캐싱된 델리게이트; Mono 전용 |
| 3 | FieldInfo.SetValue |
최후 수단; IL2CPP 비권장 |
생성된 세터는 [MethodImpl(AggressiveInlining)]이 적용되어 캐스트 + 대입 수준의 비용으로 동작합니다.
플레이 모드 보호
두 가지 독립적인 가드로 잠재적 문제를 사전에 차단합니다.
MasterInstallerPlayModeGuard— 전역 레지스트리가 비어 있을 때 경고를 출력합니다.UNInjectFallbackGuard—partial선언 없이 Expression Tree 폴백으로 동작할 타입 목록을 출력합니다.
두 가드는 감시 대상과 검사 방식이 완전히 독립적이므로, 동일한 플레이 진입 시 모두 발동될 수 있습니다.
코드 사용법
UNInject의 핵심은 등록 어트리뷰트(Provider)와 주입 어트리뷰트(Consumer)의 쌍으로 이루어집니다.
| 어트리뷰트 | 역할 | 스코프 |
|---|---|---|
[Referral] |
전역 레지스트리에 등록 | Global |
[SceneReferral] |
씬 레지스트리에 등록 | Scene |
[GlobalInject] |
전역 레지스트리에서 주입 | Global |
[SceneInject] |
씬 레지스트리에서 주입 | Scene |
[Inject] |
에디터 베이크로 로컬 주입 | Local |
Global 스코프
MasterInstaller가 관리하는 전역 레지스트리입니다. DontDestroyOnLoad로 씬을 넘어 유지됩니다.
에디터에서 Refresh Global Registry를 실행하면 [Referral] 어트리뷰트가 붙은 컴포넌트가 자동으로 등록됩니다.
// 등록 — MasterInstaller에 Refresh Global Registry
[Referral]
public partial class AudioManager : MonoBehaviour
{
public void PlaySfx(string key) { /* ... */ }
}
// 주입 — GlobalInject 필드를 가진 클래스는 partial 선언 필요
public partial class PlayerController : MonoBehaviour
{
[GlobalInject] private AudioManager _audio;
private void Start()
{
_audio.PlaySfx("jump");
}
}
인터페이스로 추상화하는 경우 BindType을 지정합니다. 테스트 교체가 쉬워지는 장점이 있습니다.
public interface IAudioManager { void PlaySfx(string key); }
[Referral(typeof(IAudioManager))]
public partial class AudioManager : MonoBehaviour, IAudioManager { /* ... */ }
public partial class PlayerController : MonoBehaviour
{
[GlobalInject] private IAudioManager _audio;
}
Scene 스코프
SceneInstaller가 관리하는 씬 단위 레지스트리입니다.
에디터에서 Refresh Scene Registry를 실행하면 [SceneReferral] 컴포넌트가 등록됩니다.
// 등록 — SceneInstaller에 Refresh Scene Registry
[SceneReferral]
public partial class WaveSpawner : MonoBehaviour
{
public void StartWave(int level) { /* ... */ }
}
// 주입
public partial class HordeDirector : MonoBehaviour
{
[SceneInject] private WaveSpawner _waves;
private void OnEnable()
{
_waves.StartWave(1);
}
}
Local 스코프
ObjectInstaller 루트 하위의 서브트리 안에서만 유효한 로컬 의존성입니다.
[Inject] 필드는 에디터 컨텍스트 메뉴 Bake Dependencies로 미리 처리, 런타임에는 Unity 역직렬화로 참조가 복원됩니다.
// ObjectInstaller 루트 하위 컴포넌트들
public partial class HUD : MonoBehaviour
{
[Inject] [SerializeField] private HealthBar _healthBar;
[Inject] [SerializeField] private PlayerController _player;
}
[GlobalInject] / [SceneInject] 필드가 없다면 partial 선언은 필요 없습니다.
Optional 주입
바인딩이 없어도 주입 실패로 처리하지 않으려면 optional: true를 지정합니다.
public partial class PlayerController : MonoBehaviour
{
[GlobalInject] private IInputService _input; // 필수
[SceneInject(optional: true)] private IStageContext _stage; // 선택
}
Named 바인딩
같은 타입의 인스턴스가 여럿인 경우 Id로 구분합니다.
[Referral("music", typeof(AudioManager))]
public partial class MusicManager : AudioManager { }
[Referral("sfx", typeof(AudioManager))]
public partial class SfxManager : AudioManager { }
public partial class MixerHub : MonoBehaviour
{
[GlobalInject("music")] private AudioManager _music;
[GlobalInject("sfx")] private AudioManager _sfx;
}
주입 완료 콜백 — IInjected
Start / Awake처럼, UNInject도 주입이 완료된 시점을 명시적으로 받을 수 있습니다.
필수 의존성이 모두 채워진 경우에만 호출됩니다.
public partial class PlayerController : MonoBehaviour, IInjected
{
[GlobalInject] private IInputService _input;
[SceneInject(optional: true)] private IStageContext _stage;
public void OnInjected()
{
// 필수 의존성 주입이 모두 완료된 후 자동 호출
_input.Enable();
}
}
순수 C# 서비스 — Create<T>()
MonoBehaviour 없이 순수 C# 클래스도 의존성을 주입받아 생성할 수 있습니다.
IScope.Create<T>()는 생성자 주입 → 필드 주입 → 틱 등록을 순서대로 처리합니다.
public partial class SessionStats
{
[GlobalInject] private IAnalytics _analytics;
[InjectConstructor]
public SessionStats([GlobalInject] AudioManager audio)
{
// 생성자 파라미터도 레지스트리에서 자동으로 주입됩니다
}
}
// 사용 — 호출한 인스톨러의 스코프로 의존성을 탐색합니다
var stats = sceneInstaller.Create<SessionStats>();
프레임 콜백이 필요하다면 ITickable / IFixedTickable / ILateTickable을 구현합니다.
해당 인터페이스를 구현한 서비스는 Create를 호출한 인스톨러의 Update / FixedUpdate / LateUpdate에 자동으로 연결됩니다.
인스톨러가 소멸될 때 IScopeDestroyable.OnScopeDestroy()가 호출되어 정리 로직을 수행할 수 있습니다.
public partial class EnemyAIService : ITickable, IScopeDestroyable
{
[SceneInject] private IWaveSpawner _spawner;
public void Tick() { /* 매 Update마다 실행 */ }
public void OnScopeDestroy() { /* 씬 종료 시 정리 */ }
}
런타임 오브젝트 주입
씬에 미리 존재하지 않고 런타임에 생성되는 오브젝트에도 동일하게 주입할 수 있습니다.
// 프리팹을 Instantiate하고 주입까지 한 번에
GameObject instance = objectInstaller.SpawnInjected(enemyPrefab, spawnPos, Quaternion.identity);
// 이미 생성된 오브젝트에 주입
objectInstaller.InjectTarget(existingMonoBehaviour);
// 오브젝트 전체 계층 구조에 일괄 주입
objectInstaller.InjectGameObject(rootGameObject);
런타임 등록/해제도 코드로 직접 처리할 수 있습니다.
// 등록 — [Referral] 어트리뷰트의 BindType을 자동으로 반영
objectInstaller.Register(enemyView, owner: this);
// 또는 타입을 직접 지정
objectInstaller.Register<IEnemyView>(enemyView, owner: this);
// owner가 소멸되면 해당 owner로 등록된 항목이 자동으로 해제됩니다
오브젝트 풀링 지원
풀에서 꺼낼 때 의존성을 재주입하고, 풀에 반환할 때 참조를 자동으로 정리합니다.
public partial class EnemyView : MonoBehaviour, IPoolInjectionTarget
{
[SceneInject] private IWaveContext _wave;
public void OnPoolGet() { /* 풀에서 꺼낼 때 호출 */ }
public void OnPoolRelease() { /* 풀에 반환하기 전 호출, 이후 inject 필드 자동 null 처리 */ }
}
// 풀에서 꺼내며 주입
objectInstaller.InjectTargetFromPool(enemyView);
// 풀에 반환 — inject 필드를 null로 초기화
objectInstaller.ReleaseTargetToPool(enemyView);
성능 비교
Zenject (Reflection), VContainer (Expression Tree), UNInject (Roslyn) 세 프레임워크를
동일 조건에서 벤치마크한 결과입니다. 수치가 낮을수록 좋습니다.
내부 전처리문 벤치마크
성능 검증은 최대한 보수적이고 객관적인 환경에서 진행됩니다.
- 대상:
BenchmarkTarget(전역 필드 5개) - 조건: 30회 반복, 평균값
- 측정: 100,000회 인젝션 (JIT 워밍업 후)

| 경로 | Cold Start (ms/call) | Hot 100,000회 (ms total) |
|---|---|---|
| Reflection — Zenject | 0.0107 ms | 96.67 ms |
| Expression Tree — VContainer | 0.4799 ms | 6.61 ms |
| Roslyn — UNInject | 0.0059 ms | 6.02 ms |
| 항목 | 결과 |
|---|---|
| Hot 패스 — Roslyn vs Reflection | 16.1× 빠름 |
| Cold 속도 — Roslyn vs Expression Tree | 81.9× 빠름 |
| IL2CPP 안전 | Roslyn ✓ / Expression Tree ✗ |
| VContainer 비고 | Roslyn 옵션 제공 → UNInject는 기본 내장 |
Cold Start에서 Expression Tree는 Lambda 컴파일 비용으로 Roslyn 대비 81.9배 느립니다.
Roslyn 경로는 컴파일 시점에 세터가 이미 생성되어 있어 초기 비용 자체가 발생하지 않습니다.
유니티 프로파일러 측정

최초 주입(Cold) 시 Expression Tree는 0.8 MB GC 할당이 발생하지만,
Roslyn 경로는 0.6 KB에 그치며 캐싱 이후(Hot)에는 GC 할당이 0 B입니다.
에디터 도구
의존성 그래프를 관리하고 시각화하기 위한 직관적인 인스펙터 도구를 제공합니다.
| 색상 | 의미 |
|---|---|
| 🟢 초록 | 레지스트리에 등록됨 (정상) |
| ⚫ 회색 | Optional — 미등록 (의도된 상태) |
| 🟠 주황 / 빨강 | Required — 미등록 (주의 필요) |
의존성 그래프 (UNInjectGraphWindow)
메뉴: Window > UNInject > Dependency Graph
씬 내 의존성 관계를 시각적으로 확인할 수 있는 그래프 뷰를 제공합니다.
Roslyn 생성 플랜이 적용된 엣지는 초록색, 대안 경로를 사용하는 엣지는 노란색으로 구분됩니다.
베이크 검증기 (UNInjectBakeValidator)
메뉴: Window > UNInject > Validate Bake
플레이어 빌드 전 자동으로 실행되며, 직렬화된 레지스트리 목록에 없는 필수 의존성을 탐지합니다.
UNINJECT_STRICT_BUILD 심볼을 설정하면 검증 실패 시 빌드를 중단시킬 수 있습니다.
릴리즈 노트
──────────────────────────────────────────────────────────────────────
1.1.0 — IL2CPP 완전 지원 (릴리즈 완료)
──────────────────────────────────────────────────────────────────────
Roslyn Source Generator 도입. partial 클래스를 통한 IL2CPP 안전 세터 자동 생성.
UNInjectFallbackGuard 추가. 인스펙터 Optional 3단계 표기.
ALL PASSED (24 + 51 tests)
──────────────────────────────────────────────────────────────────────
1.1.1 — 내부 무결성 강화 (릴리즈 완료)
──────────────────────────────────────────────────────────────────────
공개 API 변경 없음. InstallerRegistryHelper 도입으로 레지스트리 정책 단일화.
Safety Net armed/disarmed 패턴 도입. 어셈블리 격리 (3개 독립 asmdef).
EditMode 단위 테스트 43개 신규 추가.
ALL PASSED (43 EditMode 단위 테스트)
──────────────────────────────────────────────────────────────────────
2.1.0 — 스코프/생명 주기 아키텍처 완성 (릴리즈 완료)
──────────────────────────────────────────────────────────────────────
IScope 인터페이스 도입 (세 인스톨러 공통 계약).
순수 C# 서비스 레이어:
IScope.Create<T>() — 생성자 주입 + 필드 주입 + 틱 등록 통합 경로.
[InjectConstructor] 어트리뷰트 및 생성자 파라미터 주입 지원.
IScopeDestroyable / ITickable / IFixedTickable / ILateTickable 인터페이스 추가.
IPoolInjectionTarget (OnPoolGet / OnPoolRelease) 및 풀링 API 추가.
명명된 바인딩 — RegistryKey = (Type, Id) 구조로 전환.
SceneExitPolicy (Clear / Preserve) 추가.
SpawnInjected 오버로드 확장.
에디터 도구 확장:
UNInjectGraphWindow — 의존성 그래프 (GraphView, UI Toolkit).
UNInjectBakeValidator — 빌드 전 자동 검증 (IPreprocessBuildWithReport).
UNINJECT_STRICT_BUILD / UNINJECT_PROFILING 심볼 추가.