UNInject
UNInject – High-Performance Unity Dependency Injection SDK
Current Version : 2.1.0
UNInject는 Unity를 위해 설계된 고성능 의존성 주입 프레임워크임.
editor-time baking, Roslyn source generation, three-tier scoping,
그리고 타입이partial로 선언된 경우 reflection 없는 런타임 경로를 목표로 설계되었음.
UNInject는 editor-time dependency baking, Roslyn Source Generation,
그리고 hierarchical scoping을 결합하여 전통적인 DI 구현에서 흔히 발생하는 reflection 병목이나
runtime lookup 오버헤드 없이 매우 효율적인 runtime dependency injection을 가능하게 함.
기존 DI 시스템들이 FindObjectsOfType 호출이나 무거운 reflection 초기화에 의존하는 것과 달리
UNInject는 성능,
결정론적 dependency resolution, 개발자 사용성을 중심으로 설계된 완전한 injection architecture를 제공함.
https://github.com/NightWish-0827/UNInject.git?path=/com.nightwishlab.uninject
UPM Add package from git URL
Table of Contents
- Core Features
- Pure C# service layer
- API Reference & Usage
- Usage patterns: concrete types & MonoBehaviour
- IScope, Registry & Named Bindings
- Per-installer API surface
- Constructor Injection &
Create<T>() - Tickables & Scope Teardown
- User Callbacks — Reference
- Lifecycle & Internal Architecture
- Dynamic Object Support
- Performance & Injection Paths
- Editor Supports
- Common Editor Warnings
Core Features
Editor-Backed Bake Architecture
ObjectInstaller는 [Inject] 필드를 편집 단계에서 hierarchy에 대해 resolve함 (컨텍스트 메뉴 Bake Dependencies).
연결 정보는 serialize되어 저장되므로, 해당 필드에 대해 runtime hierarchy 스캔이 발생하지 않음.
✦ Roslyn Source Generator — IL2CPP 완전 지원
UNInject는 Roslyn Source Generator 기반의 코드 생성 파이프라인을 도입하여
기존 Expression Tree 방식이 갖고 있던 IL2CPP/AOT 환경에서의 제약을 완전히 해소했음.
동작 방식:
Generator는 [GlobalInject] / [SceneInject] 필드를 가진 클래스를 컴파일 타임에 자동 탐지하고,
두 종류의 소스를 메모리 내에서 직접 생성하여 함께 컴파일함 (디스크에 사용자 파일로 저장되지 않음).
코드 저장 → Unity 컴파일 시작
→ UNInjectGenerator 실행 (저장 시마다 자동)
→ ① {TypeName}.UNInject.g.cs — partial 클래스 확장 + AggressiveInlining setter
→ ② UNInjectPlanRegistry.g.cs — AfterAssembliesLoaded 등록기
→ 함께 컴파일 완료
생성된 코드는 디스크에 저장되지 않으며, Expression.Compile() 을 일절 사용하지 않음.
단순 캐스트와 직접 대입만으로 구성되어 모든 AOT 환경에서 안전하게 동작함.
사용자에게 요구되는 유일한 변경사항:
[GlobalInject] 또는 [SceneInject] 필드를 가진 클래스 (또는 생성된 생성자 플랜이 필요한 타입)에
public partial class 선언 추가.
// 변경 전
public class PlayerController : MonoBehaviour { ... }
// 변경 후
public partial class PlayerController : MonoBehaviour { ... }
partial 없이 Play 진입 시 UNInjectFallbackGuard가 즉시 경고를 출력함.
또한 Roslyn 컴파일 타임에 UNI001 경고가 발행되어, IDE 레벨에서도 인지할 수 있음.
Runtime Injection Tiers
dependency injection 수행 시 런타임 주입 경로는 우선순위 순으로 세 단계로 구성됨.
| 우선순위 | 경로 | 비고 |
|---|---|---|
| 1 | Roslyn 생성 플랜 | Dictionary 조회 + AggressiveInlining setter |
| 2 | Expression Tree 폴백 | 캐싱된 delegate; Mono 지향 |
| 3 | FieldInfo.SetValue |
최후 수단; 핫 경로에서 IL2CPP 위험 |
Three-Tier Deterministic Scoping
전통적인 DI 시스템들은 lifecycle 관리가 모호해지는 문제가 자주 발생함.
UNInject는 이를 해결하기 위해 세 가지 명확한 scope 계층을 강제함.
| Scope | 컴포넌트 | Lifetime |
|---|---|---|
| Global | MasterInstaller |
DontDestroyOnLoad |
| Scene | SceneInstaller |
현재 scene (아래 정책 참조) |
| Local | ObjectInstaller |
installer root 하위 서브트리 |
Scene 언로드 동작은 SceneInstaller의 SceneExitPolicy 로 제어됨.
Clear— 소멸 시 registry를 비움 (기본값).Preserve— 언로드 이후에도 registry 항목을 유지함 (예: additive 로딩).
소유권 맵은 초기화되므로, stale한UnregisterByOwner경로가 preserved 바인딩을 제거하지 않음.
PlayMode Guard Protection
Baked 기반 시스템에서 발생할 수 있는 잠재적 문제들을 두 개의 독립적인 가드로 방어함.
① MasterInstallerPlayModeGuard — 레지스트리 공백 감지
개발자가 PlayMode 진입 전에 global registry를 갱신하지 않았을 때 발생하는
empty registry 문제를 방지함.
MasterInstaller가 scene에 존재하고_globalReferrals.arraySize == 0이면 즉시 warning 로그를 출력함.
[MasterInstaller] Global registry is empty. Did you forget to click 'Refresh Global Registry' before Play?
② UNInjectFallbackGuard — IL2CPP 폴백 타입 감지
partial 선언 없이 Expression Tree 폴백으로 동작할 타입을 Play 진입 시점에 탐지함.
IL2CPP 빌드에서 FieldInfo.SetValue 폴백까지 내려갈 수 있는 타입 목록을 명시적으로 출력함.
[UNInject] 다음 타입은 'partial' 선언이 없어 Roslyn 생성 플랜 대신
Expression Tree 폴백으로 동작합니다.
• MyGame.PlayerController
• MyGame.EnemyAI
두 가드는 감시 대상과 검사 방식이 완전히 독립적이므로 별도 파일로 분리되어 있으며,
동일한 Play 진입 시 두 가드가 모두 발동할 수 있음.
Runtime Registry Shape
Registry는 Dictionary<RegistryKey, Component> 구조를 사용하며, RegistryKey = (Type, Id) 임.
Id == string.Empty는 v1.x 방식의 키 없는 등록과 매칭됨.
Named referral은 [Referral] / [SceneReferral] 및 [GlobalInject] / [SceneInject]
(필드와 생성자 파라미터 모두)에서 동일한 Id 를 사용해 매칭됨.
Cache-Friendly Injection Execution
UNInject는 내부적으로 cached structural mapping 구조를 사용함.
global 및 scene registry는
미리 baked된 리스트 구조를 기반으로 생성되며
Awake 시점에 빠른 Dictionary<Type, Component> lookup 구조로 변환됨.
TypeDataCache는 생성 플랜 캐시와 Reflection 캐시를 분리하여 관리함.
생성 플랜이 등록된 타입은 Dictionary 조회 한 번으로 처리가 완료되며, Warmup() 호출조차 사실상 무비용으로 동작함.
이러한 data-oriented 구조는 scene hierarchy를 직접 순회하는 방식보다 훨씬 높은 성능을 제공함.
Pure C# service layer
UNInject는 두 가지 런타임 역할을 명확히 구분함.
| Layer | 정의 | 등록 방식 | 일반적 용도 |
|---|---|---|---|
| Manager layer | [Referral] / [SceneReferral]을 가진 MonoBehaviour |
Editor refresh → baked lists → Awake registry 구축 |
UnityEngine.Object lifetime, scene 또는 DDOL |
| Service layer | plain C# class (MonoBehaviour 없음), inject 필드를 위해 partial 선언 |
hierarchy에 없음 — IScope.Create<T>() 를 통해서만 생성 |
[GlobalInject] / [SceneInject]로 manager를 소비하는 application/domain 로직, facade, 시스템 |
Service layer 인스턴스의 특징:
[InjectConstructor](또는public생성자가 정확히 하나인 경우 attribute 없이도 동작)로 구성되는
ReferenceType객체이며, zero-reflection 생성을 위한 RoslynTryGetGeneratedFactory경로를 선택적으로 사용함.MonoBehaviour와 동일한RegistryKey규칙으로Component의존성을 resolve함: 생성자 파라미터에
[GlobalInject]/[SceneInject](namedId와optional포함)를 선언할 수 있음.- Unity 메시지를 받지 않음 (
Update/OnDestroy없음). 참여는 명시적으로 이루어짐:ITickable/IFixedTickable/ILateTickable→Create를 호출한MonoBehaviourinstaller가
(Update/FixedUpdate/LateUpdate를 포워딩) 구동함.IScopeDestroyable→ 해당 installer의OnDestroy실행 시 teardown됨 (Tickables & Scope Teardown 참조).
- Lifetime은 소유 scope의
GameObject에 바인딩됨:MasterInstaller/SceneInstaller/ObjectInstaller가 소멸되면TickableRegistry.ClearWithDestroy()가 등록된 destroyable에 대해OnScopeDestroy()를 실행한 뒤 tick 목록을 초기화.
Create<T>()에 사용할 scope 선택 기준
MasterInstaller.Create<T>()— resolver가 global 먼저 탐색 후SceneInstallerfallthrough.
scene 로드 이후에도 살아남아야 하는 game-wide 서비스에 사용 (installer가 DDOL).SceneInstaller.Create<T>()— resolver가 scene 먼저 탐색 후MasterInstaller. session/scene 단위 로직에 사용.ObjectInstaller.Create<T>()— resolver가 local registry →_parentScope체인(선택적) → scene → global 순 탐색.
서브트리 로컬 서비스 (예: 하나의 UI root 또는 풀링된 서브시스템 하위)에 사용.
Create<T>()에서 지원하지 않는 타입
MonoBehaviour— Unity가 생성자 기반 구성을 허용하지 않음.
대신SpawnInjected/InjectTarget/InjectGameObject를 사용할 것.
ScriptableObject관련 주의:Create<T>()는 생성자 호출(또는 생성된 factory)을 사용함.
UnityScriptableObject인스턴스는 일반적으로ScriptableObject.CreateInstance로 생성하므로,
Create<T>()는 non-UnityEngine.Objectservice 타입을 대상으로 하는 것으로 간주해야 함.
API Reference & Usage
Resolve는 항상 RegistryKey(Type, Id) 를 사용함. 선언된 필드 (또는 생성자 파라미터)의 Type 이 registry가 보유한 키와 일치해야 함.
해당 키는 concrete MonoBehaviour 등록 (“wide” auto-mapping) 또는 narrow [Referral(BindType)] (“expert” 단일 키)에서 비롯될 수 있음. 초보자와 전문가 모두 동일한 Resolve 를 사용하며, 차이는 등록 방식에 있음.
Usage patterns: concrete types & MonoBehaviour
Pattern 1 — Global manager: concrete type만 사용 (interface 불필요)
많은 팀이 AudioManager 나 GameSettings 를 직접 참조함. interface를 반드시 도입할 필요는 없음.
Provider (MasterInstaller에 bake됨):
// [Referral] with no BindType → wide registration: concrete + mappable interfaces + bases
[Referral]
public partial class AudioManager : MonoBehaviour
{
public void PlaySfx() { /* ... */ }
}
Consumer (partial + generator):
public partial class PlayerController : MonoBehaviour
{
[GlobalInject] private AudioManager _audio;
}
동등한 “interface-first” 스타일 (동일한 Resolve, 테스트 seam 명확):
public interface IAudioManager { void PlaySfx(); }
[Referral(typeof(IAudioManager))]
public partial class AudioManager : MonoBehaviour, IAudioManager
{
public void PlaySfx() { /* ... */ }
}
public partial class PlayerController : MonoBehaviour
{
[GlobalInject] private IAudioManager _audio;
}
Refresh Global Registry 이후, scene에 AudioManager가 하나 존재하면 두 consumer 모두 동일한 인스턴스를 받음.
Pattern 2 — Scene manager: concrete vs SceneReferral(BindType)
[SceneReferral]
public partial class WaveSpawner : MonoBehaviour { /* ... */ }
// 동일 scene의 ObjectInstaller 하위 Consumer
public partial class HordeDirector : MonoBehaviour
{
[SceneInject] private WaveSpawner _waves;
}
추상화를 사용하는 경우:
public interface IWaveSpawner { }
[SceneReferral(typeof(IWaveSpawner))]
public partial class WaveSpawner : MonoBehaviour, IWaveSpawner { }
public partial class HordeDirector : MonoBehaviour
{
[SceneInject] private IWaveSpawner _waves;
}
활성 SceneInstaller 에서 Refresh Scene Registry 를 실행할 것.
Pattern 3 — Local subtree: [Inject] + sibling / child MonoBehaviour (global registry 없음)
전형적인 “beginner” hierarchy: 하나의 ObjectInstaller root 하위에 concrete MonoBehaviour 참조만 사용하는 구성.
public partial class HUD : MonoBehaviour
{
[Inject] [SerializeField] private HealthBar _healthBar; // 동일 root 하위 child 또는 sibling
[Inject] [SerializeField] private PlayerController _player; // SerializeField + Bake Dependencies
}
[Inject]는 editor에서 resolve됨 (Bake Dependencies); runtime에는 Unity native deserialization이 자동으로
참조를 복원함 —[GlobalInject]불필요.- 동일 타입에
[GlobalInject]/[SceneInject]필드가 있다면public partial class선언 필요.
Pattern 4 — Named bindings: 동일 field type을 공유하는 여러 인스턴스
Id 없이는 키당 하나의 등록만 유효함 (중복 시 경고 로그). AudioManager 오브젝트가 둘 이상이라면 named referral과
매칭 inject 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;
}
BindType 을 interface로 지정할 수도 있음; [Referral] 과 [GlobalInject] 의 Id 는 여전히 쌍으로 일치해야 함.
Pattern 5 — Runtime-spawned MonoBehaviour (concrete Register vs Register<IBind>)
bake-time global discovery에 의존하지 않는 enemy prefab 스폰:
public partial class EnemyView : MonoBehaviour, IEnemyView { /* ... */ }
// Instantiate 이후 —
var scope = GetComponent<ObjectInstaller>(); // 또는 캐싱된 참조
scope.Register<EnemyView>(enemyView, owner: this); // expert: EnemyView 키만 바인딩
scope.Register(enemyView, owner: this); // beginner-friendly: inspector [Referral]과 동일
prefab 클래스에 [Referral(typeof(IEnemyView))] 가 있다면 Register(enemyView) 가 BindType 을 자동으로 반영함.
concrete class만 있는 경우에도 typeof(EnemyView) 키로 등록되므로,
다른 타입에서 [GlobalInject] private EnemyView _x 를 interface 없이도 사용할 수 있음.
Pattern 6 — ObjectInstaller 참조 방식
다음 방식들 중 어느 것이든, 동일한 인스턴스를 호출하면 유효함.
[SerializeField] private ObjectInstaller _scope; // inspector에서 드래그 (초보자에게 일반적)
// 또는
private ObjectInstaller Scope => GetComponentInParent<ObjectInstaller>();
// 또는 (scene-local singleton 경로)
SceneInstaller.Instance.Create<MyService>(); // Scene scope에서 서비스 생성 — ObjectInstaller 필드 불필요
local registry + _parentScope 를 고려한 injection이 필요하면 ObjectInstaller 를 사용하고,
서비스가 해당 installer의 native chain만 사용해야 한다면 MasterInstaller / SceneInstaller 를 사용할 것.
Pattern 7 — Create<T>()와 concrete 생성자 파라미터 (service layer)
public partial class SessionStats
{
[GlobalInject] private IAnalytics _analytics; // ctor body 실행 후 주입됨
[InjectConstructor]
public SessionStats([GlobalInject] AudioManager audio, WaveSpawner waves)
{
// [GlobalInject] / [SceneInject] 필드에서 얻는 것과 동일한 인스턴스
}
}
// var stats = sceneInstaller.Create<SessionStats>();
AudioManager / WaveSpawner는 GlobalInject 필드와 완전히 동일하게 registry에서 resolve되는 concrete Component 타입임. 해당 타입들이 interface 키로 등록되어 있다면 interface도 동일하게 동작함.
Provider attributes (registration)
// Expert: 단일 bind type (BindType + Id 키만 등록됨)
[Referral(typeof(IInputService))]
public partial class DesktopInputManager : MonoBehaviour, IInputService { }
// Named expert binding
[Referral("profileA", typeof(IProfileService))]
public partial class ProfileServiceA : MonoBehaviour, IProfileService { }
// Scene expert binding
[SceneReferral(typeof(ILevelRules))]
public partial class LevelRules : MonoBehaviour, ILevelRules { }
// Beginner-friendly: 최소 attribute; Refresh Global Registry가 이 타입을 자동 수집
[Referral]
public partial class AudioManager : MonoBehaviour { /* ... */ }
(Refresh는 component class에 [Referral] / [SceneReferral] 이 있는 타입만 탐지함.
순수 런타임 오브젝트는 attribute 없이 Register 를 직접 사용할 수 있음.)
editor Refresh Global Registry / Refresh Scene Registry 는 scene을 스캔하여 serialize된 리스트를 채우고,
Awake에서 빠른 dictionary를 재구성함.
Consumer attributes
public partial class PlayerController : MonoBehaviour
{
[Inject] [SerializeField] private Animator _animator;
[GlobalInject] private IInputService _input;
[GlobalInject] private AudioManager _audioConcrete; // AudioManager가 해당 Type으로 등록된 경우 유효
[GlobalInject("profileA")] private IProfileService _profile;
[SceneInject(optional: true)] private LevelManager _level;
}
Optional: [GlobalInject(optional: true)] / [SceneInject(optional: true)]
바인딩이 없어도 injection이 실패하지 않으며, 해당 필드에 대한 경고도 출력되지 않음.
Inspector 표기 정책: optional 미바인딩 필드는 회색, 필수 미등록 바인딩은 에러/경고로 표시됨.
Injected State Alarm
각종 Function 이라 불리우는 Start, Awake 와 마찬가지로, UNInject 또한 명시적인 주입 완료 시점 Function을 제공함.
public partial class PlayerController : MonoBehaviour, IInjected
{
[GlobalInject] private IInputService _input;
[SceneInject(true)] private IStageContext _stageContext;
public void OnInjected()
{
// ObjectInstaller가 이 컴포넌트에 대해 주입을 수행했고,
// "필수 의존성" 주입이 모두 성공했을 때 자동으로 호출됩니다.
// 호출 주체: ObjectInstaller.TryInjectTarget() 내부에서 success == true이고
// 대상이 IInjected를 구현한 경우 OnInjected()를 호출합니다.
// 호출 조건: 필수(Required) 의존성 주입이 모두 성공했을 때만 호출됩니다.
// (Optional로 표시된 의존성 누락은 성공/실패에 영향 없음)
// 호출 시점/횟수: InjectTarget() / InjectGameObject() / Awake()에서
// 주입이 실행될 때마다(즉, 주입을 여러 번 호출하면) 그때마다 호출될 수 있습니다.
}
}
(현재는 IInjected 인터페이스를 상속받아야만 해당 OnInjected 멤버를 사용 가능하지만,
추후 Native 영역으로 이를 추출하여, 별도의 상속 없이도 OnInjected 멤버를 사용할 수 있도록 구조를 변경할 예정임.)
IScope, Registry & Named Bindings
IScope 는 MasterInstaller, SceneInstaller, ObjectInstaller 모두가 구현함.
Register
Register(Component comp)—comp타입의[Referral]/[SceneReferral]메타데이터(bind type + id)를 사용하여 등록.Register(Component comp, MonoBehaviour owner)— 동일;owner가 소멸되면 해당 owner로 등록된 모든 component가 자동 unregister됨 (ScopeOwnerTracker).Register<TBind>(Component comp, MonoBehaviour owner = null)— 코드 우선 바인딩:comp를TBind로 등록 (attribute bind type 선택 사항).
중복 키는 경고 로그를 출력하고 최초 등록을 유지함.
Unregister
Unregister(Type type)— 키 없는 제거 (Id == default).Unregister(Type type, string id)— named 키 제거.Unregister(Component comp)—MasterInstaller/SceneInstaller전용: 값이comp인 모든 registry 키를 제거. (ObjectInstaller에는 해당 오버로드 없음; type/id 또는 owner 기반 정리를 사용할 것.)
Resolve
키 없는 방식:
Resolve(Type type),TryResolve(Type, out Component),Resolve<T>() where T : Component,ResolveAs<T>() where T : class
Named 방식:
Resolve(Type type, string id),TryResolve(Type, string id, out Component),ResolveAs<T>(string id)
MasterInstaller 는 Instance 위에 ResolveStatic<T>() 도 편의용으로 제공함.
ObjectInstaller resolve chain
ObjectInstaller 는 다음 순서로 resolve함.
- Local registry (
RegistryKey) _parentScope가 설정된 경우 — 해당 installer의 체인 (재귀)- 그 외 —
SceneInstaller.Instance이후MasterInstaller.Instance
_parentScope 는 선택 사항이며, 중첩 installer 그래프(풀, 격리 서브트리)를 위해 serialize됨.
Per-installer API surface
IScope 외에 존재하는 public 진입점 정리 (editor API는 명시된 경우만 포함):
MasterInstaller
| 분류 | Members |
|---|---|
| 접근 | Instance, ResolveStatic<T>() |
IScope |
Create<T>(), UnregisterTickable 변형 포함 전체 계약 |
| Registry 정리 | Unregister(Component comp) (해당 인스턴스의 모든 키) |
| Try 패턴 | TryResolve<T>(out T), TryResolveAs<T>(out T) (ObjectInstaller에는 없음) |
| Editor | RefreshRegistry(), GetGlobalComponent / GetGlobalComponent<T> |
SceneInstaller
| 분류 | Members |
|---|---|
| 접근 | Instance |
IScope |
전체 계약 |
| 정책 | SceneExitPolicy 필드 (Clear / Preserve) |
| Registry 정리 | Unregister(Component comp) |
| Try 패턴 | TryResolve<T>(out T), TryResolveAs<T>(out T) |
| Editor | RefreshSceneRegistry() |
ObjectInstaller
| 분류 | Members |
|---|---|
IScope |
Register, Register<TBind>, Unregister(Type[, id]), Resolve / TryResolve, Create<T>(), UnregisterTickable — Unregister(Component) 없음, generic TryResolve<T>(out T) 없음 |
| Hierarchy injection | InjectTarget, TryInjectTarget, InjectGameObject, SpawnInjected 오버로드들 |
| Pooling | InjectTargetFromPool, ReleaseTargetToPool |
| Scope graph | _parentScope (serialize됨) |
| Editor | BakeDependencies() |
ObjectInstaller — injection, spawn, pooling 동작 참조
이 메서드들은 런타임 injection과 동일한 TypeDataCache 필드 플랜을 사용함.
[Inject] (local bake)는 여기에서 적용되지 않으며, [SceneInject] 이후 [GlobalInject] 순으로 처리됨.
| 메서드 | 시그니처 / 비고 |
|---|---|
InjectTarget |
void InjectTarget(MonoBehaviour target) — 얇은 래퍼: TryInjectTarget(target, logWarnings: true, isReinjection: false) |
TryInjectTarget |
bool TryInjectTarget(MonoBehaviour target, bool logWarnings = true, bool isReinjection = false) — 각 inject 필드를 installer chain (local → parent / scene / master)의 Resolve(field.FieldType, field.Id) 로 resolve. 반환값: 필수 필드가 모두 resolve된 경우에만 true; optional miss는 false를 강제하지 않음. logWarnings: true 시 필수 실패에 Debug.LogWarning 출력. isReinjection: true 시 IInjected.OnInjected 를 건너뜀; 성공 시 IPoolInjectionTarget.OnPoolGet 이 대신 실행됨 (구현된 경우). |
InjectGameObject |
void InjectGameObject(GameObject root, bool includeInactive = true) — GetComponentsInChildren<MonoBehaviour> 이후 각 인스턴스에 InjectTarget 적용. |
SpawnInjected (prefab) |
GameObject SpawnInjected(GameObject prefab) — identity에서 Instantiate; InjectGameObject(instance) 실행. |
GameObject SpawnInjected(GameObject prefab, Transform parent) — parent 지정 버전. |
|
GameObject SpawnInjected(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent = null) — 위치 지정 instantiate + InjectGameObject |
|
SpawnInjected (MonoBehaviour) |
T SpawnInjected<T>(T prefab) where T : MonoBehaviour — Instantiate(prefab) + InjectTarget(instance) (단일 컴포넌트). |
T SpawnInjected<T>(T prefab, Vector3, Quaternion, Transform parent = null) — 위치 지정 버전 + InjectTarget |
|
InjectTargetFromPool |
void InjectTargetFromPool(MonoBehaviour target) → TryInjectTarget(target, logWarnings: true, isReinjection: true) |
ReleaseTargetToPool |
void ReleaseTargetToPool(MonoBehaviour target) — IPoolInjectionTarget.OnPoolRelease 호출(있는 경우); 이후 해당 타입의 [GlobalInject] / [SceneInject] 필드 전체를 TypeDataCache setter를 통해 null 로 설정. |
Register / Register<TBind> 요점 (local scope): 매핑은 concrete Component 타입의 ReferralAttribute / SceneReferralAttribute 에서 BindType 과 Id 를 가져오며, Register<TBind> 가 지정되면 bind type을 재정의함.
InstallerRegistryHelper 는 concrete 타입, 모든 mappable interface (System.*, UnityEngine.* 등 제외),
MonoBehaviour 직전까지의 base 타입 모두를 동일한 RegistryKey 로 등록함.
TypeDataCache (진단 / warmup)
startup 또는 테스트에서 호출 가능한 public 헬퍼:
Warmup(Type)/Warmup(params Type[])— 필드 및 생성자 캐시를 미리 채움.HasAnyInjectField,HasGeneratedGlobalPlan,HasGeneratedScenePlan,HasGeneratedFactory— 툴링용 introspection.GetGlobalInjectFields/GetSceneInjectFields— 캐싱된CachedInjectField목록 반환.
RegisterGenerated* API는 generator 전용 ([EditorBrowsable(Never)]).
Constructor Injection & Create<T>()
IScope.Create<T>() where T : class 는 다음 정확한 순서로 InstallerRegistryHelper.CreateAndInject<T> 를 실행함.
- Construction —
TypeDataCache.TryGetGeneratedFactory또는[InjectConstructor]/ 단일 publicctor+ConstructorInfo.Invoke. resolve 불가한 필수 ctor 파라미터 →InvalidOperationException. - Field injection — 모든
[SceneInject]필드, 이후[GlobalInject]필드 (component와 동일한 optional/required 규칙). 필수 미스 시 내부success를 false로 설정하고 경고를 출력함. IInjected.OnInjected()— 모든 필수 inject 필드가 성공한 경우 field injection 내부에서 호출됨
(optional 미스는 성공을 막지 않음).RegisterTickables(object)—T가ITickable/IFixedTickable/ILateTickable/IScopeDestroyable를 구현하면,OnInjected이후 호출 installer의TickableRegistry에 등록됨.
ctor/field의 Resolve fallthrough 는 각 installer에 매칭됨 (Master → scene; Scene → master; Object → local → parent → scene → master).
Tickables & Scope Teardown
Create<T>() 로 생성된 plain C# 서비스는 PlayerLoop 조작 없이 Unity 프레임 콜백을 받을 수 있음.
| Interface | 호출 출처 |
|---|---|
ITickable |
Host Update → Tick() |
IFixedTickable |
FixedUpdate → FixedTick() |
ILateTickable |
LateUpdate → LateTick() |
규칙
- host는 항상
Create를 호출한MonoBehaviourIScope임. - Tick은 부모 scope로 전파되지 않음.
UnregisterTickable(...)은 해당 scope에서 제거함;Tick()은 스냅샷을 사용하므로,
이터레이션 중 제거는 다음 프레임에 적용됨.- 하나의 오브젝트가
ITickable/IFixedTickable/ILateTickable중 여럿을 구현할 수 있으며,
각 매칭 목록에 모두 등록됨.
Teardown 시 IScopeDestroyable
installer OnDestroy 에서 TickableRegistry.ClearWithDestroy() 가 실행됨.
_destroyables를 순회하며OnScopeDestroy()를 호출함 (항목별try/catch— 하나의 실패가 다른 항목을 건너뛰지 않음).- tick 목록 및 내부 스냅샷 상태를 초기화함.
따라서 OnScopeDestroy 는 tick 목록이 초기화되기 전에 실행되며, 프레임 단위 tick 종료 시에는 실행되지 않음.
User Callbacks — Reference
| Callback | 대상 | 실행 시점 | 호출자 / 경로 |
|---|---|---|---|
IInjected.OnInjected() |
IInjected 를 구현한 MonoBehaviour 또는 Create<T>() 인스턴스 |
해당 패스에서 필수 [GlobalInject]/[SceneInject] 필드가 모두 적용된 후 |
TryInjectTarget (!isReinjection) 또는 CreateAndInject 내부 InjectFields |
IPoolInjectionTarget.OnPoolGet() |
pool interface를 구현한 MonoBehaviour |
TryInjectTarget(..., isReinjection: true) 성공 후 |
InjectTargetFromPool 전용 — 동일 성공 분기에서 OnInjected 와 함께 호출되지 않음 |
IPoolInjectionTarget.OnPoolRelease() |
동일 | ReleaseTargetToPool 시작 시, inject 필드를 null 로 초기화하기 전 |
ReleaseTargetToPool |
IScopeDestroyable.OnScopeDestroy() |
Create<T>() 인스턴스 (MonoBehaviour 아님) |
installer OnDestroy, ClearWithDestroy 중 |
TickableRegistry |
IUnregistered.OnUnregistered() |
임의 (interface 존재) | 현재 런타임에서 호출되지 않음 | 예약됨; 현재 SDK 자동 dispatch 없음 |
IInjected — 상세
- Required vs optional: optional 의존성 누락은
success를 false로 만들지 않음.
모든 required 필드가 resolve된 경우OnInjected는 정상 실행됨. - Re-injection:
isReinjection: true는OnInjected를 건너뜀. 대상이IPoolInjectionTarget을 구현하면
OnPoolGet이 대신 실행됨. 그 외에는 해당 패스에서 inject-completion 콜백이 발동되지 않음. - 재진입성: 성공한
TryInjectTarget/ 성공한Createfield phase가 발생할 때마다OnInjected가 다시 발동될 수 있음 — “인스턴스당 전역 1회”가 아님.
IPoolInjectionTarget — field semantics
OnPoolRelease 이후 ReleaseTargetToPool 은 TypeDataCache setter를 통해 해당 타입의 [GlobalInject] 및
[SceneInject] 필드 전체를 null 로 할당함 — 풀 인스턴스가 stale Component 참조를 보유하지 않도록 함.
IScopeDestroyable — tick과의 조합
UnityEngine.Object 소멸에 의존하지 않는 plain C#의 해제 (이벤트 구독 취소, 핸들 닫기 등)에 사용함.
동일 서비스에서 ITickable 과 함께 구현할 수 있으며,
destroy 콜백은 ClearWithDestroy 에서 실행되므로 프레임별 tick 스냅샷 규칙과 독립적임.
Lifecycle & Internal Architecture
UNInject는 엄격하게 정렬된 실행 파이프라인을 기반으로 object lifecycle을 관리함.
[Compile]
UNInjectGenerator → partial setter + 플랜 registry 등록
[RuntimeInitialize SubsystemRegistration]
TypeDataCache 정적 캐시 전체 초기화
[AfterAssembliesLoaded]
생성된 플랜을 TypeDataCache에 등록
[Order -1000] MasterInstaller.Awake → baked 리스트로부터 Global Registry 재구성
[Order -900] SceneInstaller.Awake → baked 리스트로부터 Scene Registry 재구성
[Order -500] ObjectInstaller.Awake → InjectGlobalDependencies (TypeDataCache 플랜 / 폴백)
Safety net (Master / Scene): registry 빌드 후 첫 번째 조회 miss 시 1회 조용한 재빌드를 트리거할 수 있음.
플래그는 명시적 Register / editor refresh / Rebuild*Registry 에서 다시 활성화됨.
Dynamic Object Support
runtime-spawned 오브젝트는 정적 오브젝트와 동일한 TypeDataCache 경로를 사용함.
ObjectInstaller scope = ...;
var instance = Instantiate(enemyPrefab);
scope.InjectTarget(instance);
scope.InjectGameObject(instanceRoot, includeInactive: true);
partial 타입은 런타임에도 생성된 경로를 유지하므로 (generator가 실행된 경우 IL2CPP-safe),
대량의 동적 생성 객체에서도 allocation 없이 매우 빠른 주입이 가능함.
Performance & Injection Paths
[Inject] / bake: 해당 참조에 대해 런타임에서는 deserialization만 발생함.
| 경로 | 조건 | IL2CPP | 비용 |
|---|---|---|---|
| Roslyn 생성 플랜 | partial 선언 있음 |
✅ 안전 | Dictionary 조회 + 직접 호출 |
| Expression Tree | partial 없음, Mono |
⚠️ 출시용 비권장 | 1회 컴파일 후 캐싱 |
FieldInfo.SetValue |
폴백 | ⚠️ 위험 | 매 호출마다 boxing 발생 |
생성된 setter는 [MethodImpl(AggressiveInlining)] 이 적용되어
JIT/AOT 모두에서 캐스트 + 대입 수준의 비용으로 동작함.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void __UNInject_Global_input(object __v) => _input = (IInputService)__v;
한 번 캐싱되면 10,000개의 객체 injection도 마이크로초 단위로 수행 가능함.
이는 복잡한 UI 생성이나 대규모 레벨 인스턴스 생성 시 발생하는 GC spike 및 frame drop 문제를 방지함.
Editor Supports
dependency graph를 관리하고 시각화하기 위한 직관적인 Inspector 도구를 제공함.
Inspector tooling
커스텀 Editor 스크립트가 MasterInstaller, SceneInstaller, ObjectInstaller inspector를 구동함
(registry refresh 버튼, 의존성 시각화, 읽기 전용 [Inject] drawer, optional 필드 색상 표기).
| 색상 | 의미 |
|---|---|
| 🟢 초록 | 레지스트리에 등록됨 (정상) |
| ⚫ 회색 | Optional — 미등록 (의도된 상태) |
| 🟠 주황 / 빨강 | Required — 미등록 (주의 필요) |
의존성 목록은 잘리지 않음 — 목록이 길다는 것은 UI 노이즈가 아니라 아키텍처 신호로 취급됨.
런타임 ContextMenu 항목: Bake Dependencies (ObjectInstaller), Refresh Global Registry (MasterInstaller),
Refresh Scene Registry (SceneInstaller) — inspector 워크플로와 동일한 동작.
Dependency Graph (UNInjectGraphWindow)
메뉴: Window > UNInject > Dependency Graph
클래스: UNInjectGraphWindow — 현재 editor 상태 (저장된 asset 아님)에 대한 GraphView (UI Toolkit)를 빌드함.
표시 항목
| 노드 / 요소 | 의미 |
|---|---|
| Installer | MasterInstaller [Global] 및 SceneInstaller [Scene] (보라색 헤더). |
| Referral | 유효한 scene에서 [Referral] 또는 [SceneReferral] 을 가진 컴포넌트. 시안 = global, 초록 = scene. referral에 named Id가 있으면 레이블에 [id:"…"] 가 포함됨. |
| Inject target | [GlobalInject] 또는 [SceneInject] 필드가 하나 이상 있는 타입. 모든 필수 의존성 edge가 resolve되면 금색, 하나라도 RegistryKey(fieldType, fieldId) 가 referral map에 없으면 빨간색. |
| Edges | Installer → Referral = 등록 edge. Referral → Inject target = 의존성 edge; 해당 consumer 타입에 해당 필드에 대한 Roslyn 생성 플랜이 있으면 초록 틴트 (TypeDataCache.HasGeneratedGlobalPlan / HasGeneratedScenePlan), 폴백을 사용할 경우 노란색. |
툴바: Refresh 는 로드된 어셈블리와 scene 컴포넌트를 재스캔함 (registry refresh 또는 코드 변경 후 사용).
Inject target의 어셈블리 스캔은 다른 진단과 동일한 ShouldSkipAssembly 정책을 사용하여 프레임워크 어셈블리를 무시함.
Bake Validator (UNInjectBakeValidator)
메뉴: Window > UNInject > Validate Bake
클래스: UNInjectBakeValidator — IPreprocessBuildWithReport (callbackOrder == 0) 를 구현하므로, 각 player build 전에 자동으로도 실행됨.
알고리즘 요약
- 로드된 game 어셈블리에서 non-optional
[GlobalInject]/[SceneInject]필드를 스캔함.- 키 없는 필드는
field.FieldType을 기여함. - Named 필드는
RegistryKey(field.FieldType, id)를 기여함 (키 없는 패스와 혼용되지 않음).
- 키 없는 필드는
EditorBuildSettings에 활성화된 각 scene에 대해 additively 열고 (또는 이미 열린 scene 사용),
SerializedObject를 통해MasterInstaller._globalReferrals/SceneInstaller._sceneReferrals를 읽어 커버되는 concrete 및 abstract/interface 키를 표시함 (런타임RegisterTypeMappingswidening을 반영).- baked 리스트에 매칭 referral이 없는 필수 inject마다
Debug.LogError를 출력함.
결과
- 기본: 에러가 로그되고 빌드는 계속됨 (strict mode 미적용 시).
UNINJECT_STRICT_BUILD: 검증 에러 발생 시BuildFailedException을 throw하고 빌드를 중단함.
메뉴 커맨드는 DisplayDialog 요약을 표시하며; RunValidation() 은 public static 이므로 CI나 커스텀 editor 버튼에서
동일 검사를 호출할 수 있음.
Play mode guards (editor-only)
| 타입 | 트리거 | 역할 |
|---|---|---|
MasterInstallerPlayModeGuard |
Edit Mode → Play 전환 | scene에 MasterInstaller 가 존재하고 _globalReferrals.arraySize == 0 이면 empty global registry 경고를 출력함. |
UNInjectFallbackGuard |
동일 전환 | Expression Tree / reflection 경로에 의존하는 타입 목록 출력 (missing partial, 생성 플랜 없는 런타임 register 후보, 생성자 폴백 사용 등). |
두 가드는 독립적 hook이며, 동일한 Play 진입에서 모두 발동될 수 있음.
Scripting define symbols (선택적 툴링)
| Symbol | 효과 |
|---|---|
UNINJECT_STRICT_BUILD |
UNInjectBakeValidator 가 baked 리스트에 required referral이 없을 때 BuildFailedException 을 throw함. |
UNINJECT_PROFILING |
UNInjectProfiler 를 컴파일함. ObjectInstaller.TryInjectTarget 이 타입별 Stopwatch 타이밍을 기록하며; UNInjectProfiler.PrintReport(), GetStats(), Reset() 으로 조회 가능함 (stats는 Play 시 SubsystemRegistration 에서도 reset됨). 주입 비용을 의도적으로 측정하는 경우가 아니라면 출시 빌드에서 제외할 것. |
Profiler public API (symbol이 설정된 경우에만): RecordInjection(Type, double) (TryInjectTarget 에서 호출),
GetStats(), PrintReport(), Reset().
Common Editor Warnings
Empty global registry (Play)
[MasterInstaller] Global registry is empty... — Refresh Global Registry 실행 필요. (MasterInstallerPlayModeGuard)
Missing partial (Play)
[UNInject] ... Expression Tree fallback ... — IL2CPP 전에 표시된 타입에 partial 추가 필요. (UNInjectFallbackGuard)
참고 및 향후 업데이트 방향성
현재 지원하는 컴파일러
IL2CPP / AOT 환경을 공식 지원함.
Roslyn Source Generator가 생성한 코드는 Expression.Compile()을 사용하지 않으므로
모든 AOT 플랫폼(iOS, 콘솔 등)에서 안전하게 동작함.
partial 선언이 없는 타입에 한해 Expression Tree 폴백이 적용되며,
이 경우 UNInjectFallbackGuard가 Play 진입 시 명시적으로 경고를 출력함.
릴리즈 브랜치에서는 UNINJECT_STRICT_BUILD 를 활성화하여,
EditorBuildSettings scene에 serialized registry 리스트에 없는 referral을 참조하는 consumer가 있을 때 빌드가 실패하도록 설정할 것.
──────────────────────────────────────────────────────────────────────
1.1.0 — IL2CPP 완전 지원 (릴리즈 완료)
──────────────────────────────────────────────────────────────────────
Roslyn Source Generator 도입. partial 클래스를 통한 IL2CPP 안전 setter 자동 생성.
UNInjectFallbackGuard 추가. Inspector Optional 3단계 표기. 의존성 전체 목록 표시.
ALL PASSED (24 + 51 tests)
──────────────────────────────────────────────────────────────────────
1.1.1 — 내부 무결성 강화 (릴리즈 완료)
──────────────────────────────────────────────────────────────────────
공개 API 변경 없음. 동작 결과 변경 없음.
모든 개선은 구조적 견고성과 테스트 커버리지 확보에 집중된 내부 작업임.
InstallerRegistryHelper 도입 (신규 internal 클래스)
RegisterTypeMappings / IsMappableAbstraction / TryAdd 를
MasterInstaller, SceneInstaller 양쪽에서 추출하여 단일 헬퍼로 통합.
정책 변경 시 한 곳만 수정하면 됨.
Safety Net 방어 강화 — MasterInstaller.Resolve()
기존: 캐시 미스 시마다 _globalReferrals 전체 순회 무조건 실행.
개선: armed/disarmed 패턴 (_safetyNetArmed) 도입.
레지스트리 빌드 사이클당 복구 시도 1회 제한, 명시적 리빌드 시 재무장.
무장 해제 후 미스는 Dictionary 조회 + bool 체크만으로 처리됨.
어셈블리 격리 — 3개 독립 asmdef 단위
UNInject.Runtime / UNInject.Editor / UNInject.Tests.EditMode
에디터 코드가 플레이어 빌드에서 구조적으로 제외됨.
EditMode 단위 테스트 43개 신규 추가
InstallerRegistryHelper (18개): 필터 정책, 충돌/null 처리, 매핑 경로.
TypeDataCache (25개): 필드 수집, 캐시 동일성, setter 쓰기 검증, Warmup, null 방어.
정적 캐시 오염 수정
HasGeneratedPlan 테스트가 실행 순서에 따라 비결정론적으로 실패하던 문제 해소.
읽기 대상과 쓰기 대상을 센티넬 타입으로 구조적으로 분리함.
ALL PASSED (43 EditMode 단위 테스트)
──────────────────────────────────────────────────────────────────────
2.1.0 — Scope/Lifetime 아키텍처 완성 (릴리즈 완료)
──────────────────────────────────────────────────────────────────────
IScope 인터페이스 도입 (MasterInstaller / SceneInstaller / ObjectInstaller 공통 계약).
Pure C# service layer:
IScope.Create<T>() — 생성자 주입 + 필드 주입 + Tickable 등록 통합 경로.
[InjectConstructor] attribute 및 생성자 파라미터 [GlobalInject] / [SceneInject] 지원.
IScopeDestroyable / ITickable / IFixedTickable / ILateTickable 인터페이스 추가.
IPoolInjectionTarget (OnPoolGet / OnPoolRelease) 및 풀링 API 추가.
Named bindings — RegistryKey = (Type, Id) 구조로 전환.
[Referral("id")] / [SceneReferral("id")] / [GlobalInject("id")] / [SceneInject("id")] 지원.
SceneExitPolicy (Clear / Preserve) — SceneInstaller 언로드 동작 정책 제어.
SpawnInjected 오버로드 확장 (GameObject / MonoBehaviour, 위치/부모 지정 변형).
ReleaseTargetToPool — 풀 반환 시 inject 필드 자동 null 처리.
Editor tooling 확장:
UNInjectGraphWindow — Dependency Graph (GraphView, UI Toolkit).
UNInjectBakeValidator — IPreprocessBuildWithReport 기반 빌드 전 자동 검증.
UNINJECT_STRICT_BUILD / UNINJECT_PROFILING scripting define symbol 추가.
UNInjectProfiler — 타입별 주입 비용 Stopwatch 측정 (UNINJECT_PROFILING 시).