com.kylin.di 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.github/workflows/publish.yml +22 -0
  2. package/CHANGELOG.md +17 -0
  3. package/CLAUDE.md +58 -0
  4. package/LICENSE +21 -0
  5. package/README.md +704 -0
  6. package/Runtime/Attributes/InjectAttribute.cs +11 -0
  7. package/Runtime/Attributes/InjectAttribute.cs.meta +2 -0
  8. package/Runtime/Attributes/ViewModelAttribute.cs +17 -0
  9. package/Runtime/Attributes/ViewModelAttribute.cs.meta +2 -0
  10. package/Runtime/Attributes.meta +8 -0
  11. package/Runtime/Core/DIBehaviour.cs +70 -0
  12. package/Runtime/Core/DIBehaviour.cs.meta +2 -0
  13. package/Runtime/Core/LifetimeScope.cs +264 -0
  14. package/Runtime/Core/LifetimeScope.cs.meta +2 -0
  15. package/Runtime/Core.meta +8 -0
  16. package/Runtime/DI/DependencyBuilder.cs +114 -0
  17. package/Runtime/DI/DependencyBuilder.cs.meta +2 -0
  18. package/Runtime/DI/DependencyInjector.cs +104 -0
  19. package/Runtime/DI/DependencyInjector.cs.meta +2 -0
  20. package/Runtime/DI/InstanceFactory.cs +36 -0
  21. package/Runtime/DI/InstanceFactory.cs.meta +2 -0
  22. package/Runtime/DI/KDI.cs +54 -0
  23. package/Runtime/DI/KDI.cs.meta +2 -0
  24. package/Runtime/DI/Registration.cs +29 -0
  25. package/Runtime/DI/Registration.cs.meta +2 -0
  26. package/Runtime/DI/Scope.cs +183 -0
  27. package/Runtime/DI/Scope.cs.meta +2 -0
  28. package/Runtime/DI/ScopeBuilder.cs +68 -0
  29. package/Runtime/DI/ScopeBuilder.cs.meta +2 -0
  30. package/Runtime/DI/ScopeExtensions.cs +70 -0
  31. package/Runtime/DI/ScopeExtensions.cs.meta +2 -0
  32. package/Runtime/DI.meta +8 -0
  33. package/Runtime/Debug/ClosureAnalyzer.cs +377 -0
  34. package/Runtime/Debug/ClosureAnalyzer.cs.meta +2 -0
  35. package/Runtime/Debug/ClosureProfilerWindow.cs +435 -0
  36. package/Runtime/Debug/ClosureProfilerWindow.cs.meta +2 -0
  37. package/Runtime/Debug/SubscriberInfo.cs +661 -0
  38. package/Runtime/Debug/SubscriberInfo.cs.meta +3 -0
  39. package/Runtime/Debug.meta +3 -0
  40. package/Runtime/Kylin.DI.asmdef +14 -0
  41. package/Runtime/Kylin.DI.asmdef.meta +7 -0
  42. package/Runtime/SubscribableProperty/Reaction.cs +61 -0
  43. package/Runtime/SubscribableProperty/Reaction.cs.meta +2 -0
  44. package/Runtime/SubscribableProperty/SubscribableCollection.cs +325 -0
  45. package/Runtime/SubscribableProperty/SubscribableCollection.cs.meta +3 -0
  46. package/Runtime/SubscribableProperty/SubscribableCollectionExtensions.cs +24 -0
  47. package/Runtime/SubscribableProperty/SubscribableCollectionExtensions.cs.meta +3 -0
  48. package/Runtime/SubscribableProperty/SubscribableCommand.cs +52 -0
  49. package/Runtime/SubscribableProperty/SubscribableCommand.cs.meta +2 -0
  50. package/Runtime/SubscribableProperty/SubscribableDictionary.cs +350 -0
  51. package/Runtime/SubscribableProperty/SubscribableDictionary.cs.meta +3 -0
  52. package/Runtime/SubscribableProperty/SubscribableProperty.cs +119 -0
  53. package/Runtime/SubscribableProperty/SubscribableProperty.cs.meta +2 -0
  54. package/Runtime/SubscribableProperty/SubscribablePropertyExtensions.cs +39 -0
  55. package/Runtime/SubscribableProperty/SubscribablePropertyExtensions.cs.meta +3 -0
  56. package/Runtime/SubscribableProperty/SubscribablePropertyLinq.cs +86 -0
  57. package/Runtime/SubscribableProperty/SubscribablePropertyLinq.cs.meta +2 -0
  58. package/Runtime/SubscribableProperty.meta +8 -0
  59. package/Runtime/Update/IFixedUpdatable.cs +16 -0
  60. package/Runtime/Update/IFixedUpdatable.cs.meta +2 -0
  61. package/Runtime/Update/ILateUpdatable.cs +16 -0
  62. package/Runtime/Update/ILateUpdatable.cs.meta +2 -0
  63. package/Runtime/Update/IUpdatable.cs +16 -0
  64. package/Runtime/Update/IUpdatable.cs.meta +2 -0
  65. package/Runtime/Update/IUpdatePriority.cs +15 -0
  66. package/Runtime/Update/IUpdatePriority.cs.meta +2 -0
  67. package/Runtime/Update/UpdateLoopManager.cs +240 -0
  68. package/Runtime/Update/UpdateLoopManager.cs.meta +2 -0
  69. package/Runtime/Update.meta +8 -0
  70. package/Runtime.meta +8 -0
  71. package/package.json +19 -0
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 3e5f7e6b086bb9e4f9c911136e39f546
@@ -0,0 +1,17 @@
1
+ using System;
2
+
3
+ namespace Kylin.DI
4
+ {
5
+ [AttributeUsage(AttributeTargets.Class)]
6
+ public class ViewModelAttribute : Attribute
7
+ {
8
+ public bool IsGlobal { get; }
9
+ public string[] SceneNames { get; }
10
+
11
+ public ViewModelAttribute(bool isGlobal = false, params string[] sceneNames)
12
+ {
13
+ IsGlobal = isGlobal;
14
+ SceneNames = sceneNames ?? Array.Empty<string>();
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: d5d76843a2ec370499bd54d883ab5e6b
@@ -0,0 +1,8 @@
1
+ fileFormatVersion: 2
2
+ guid: e4ee0f753d4557943bf029f184852761
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
@@ -0,0 +1,70 @@
1
+ using Kylin.SubscribableProperty;
2
+ using UnityEngine;
3
+
4
+ namespace Kylin.DI
5
+ {
6
+ /// <summary>
7
+ /// DI 지원 MonoBehaviour 기본 클래스.
8
+ /// LifetimeScope.Initialize() 시 Push 방식으로 주입됨.
9
+ /// 동적 생성 시 scope.Instantiate() 또는 scope.InjectGameObject() 사용.
10
+ /// OnDisable 시 구독 정리.
11
+ ///
12
+ /// 사용 예시:
13
+ /// <code>
14
+ /// public class PlayerController : DIBehaviour
15
+ /// {
16
+ /// [Inject] private IPlayerService _playerService;
17
+ /// [Inject] private IInputService _inputService;
18
+ ///
19
+ /// void Start()
20
+ /// {
21
+ /// _playerService.Health.Subscribe(OnHealthChanged).AddTo(_cd);
22
+ /// }
23
+ /// }
24
+ /// </code>
25
+ /// </summary>
26
+ public abstract class DIBehaviour : MonoBehaviour, IInjectable
27
+ {
28
+ /// <summary>
29
+ /// 구독 정리용 CompositeDisposable.
30
+ /// Subscribe().AddTo(_cd) 패턴으로 사용.
31
+ /// </summary>
32
+ protected CompositeDisposable _cd = new();
33
+
34
+ /// <summary>
35
+ /// 캐싱된 스코프.
36
+ /// </summary>
37
+ private IScope _cachedScope;
38
+
39
+ /// <summary>
40
+ /// 현재 사용 중인 스코프.
41
+ /// </summary>
42
+ protected IScope Scope => _cachedScope;
43
+
44
+ protected virtual void OnEnable() { }
45
+
46
+ protected virtual void OnDisable()
47
+ {
48
+ Dispose();
49
+ }
50
+
51
+ /// <summary>
52
+ /// 구독 정리 및 리소스 해제.
53
+ /// OnDisable에서 자동 호출됨.
54
+ /// </summary>
55
+ public virtual void Dispose()
56
+ {
57
+ _cd?.Dispose();
58
+ _cd = new CompositeDisposable();
59
+ }
60
+
61
+ /// <summary>
62
+ /// LifetimeScope.InjectChildren() 또는 scope.InjectGameObject()에서 호출.
63
+ /// Push 주입 시 스코프 참조를 캐싱.
64
+ /// </summary>
65
+ internal void SetInjected(IScope scope)
66
+ {
67
+ _cachedScope = scope;
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: cd9f6a435250dea4a92e2853488740d0
@@ -0,0 +1,264 @@
1
+ using System.Collections.Generic;
2
+ using UnityEngine;
3
+
4
+ namespace Kylin.DI
5
+ {
6
+ /// <summary>
7
+ /// 스코프 기반 의존성 관리를 위한 추상 MonoBehaviour.
8
+ /// Initialize() 시 하위 계층의 IInjectable MonoBehaviour에 일괄 주입 (Push).
9
+ /// child LifetimeScope 경계에서 탐색 중단 (하위 스코프의 주입 영역 침범 방지).
10
+ ///
11
+ /// 사용 방법:
12
+ /// 1. 이 클래스를 상속받은 클래스 생성
13
+ /// 2. Configure(ScopeBuilder builder)에서 builder.Bind 등으로 서비스 등록
14
+ /// 3. 씬에 해당 컴포넌트를 하나 배치
15
+ ///
16
+ /// 예시:
17
+ /// <code>
18
+ /// public class BattleSceneScope : LifetimeScope
19
+ /// {
20
+ /// protected override void Configure(ScopeBuilder builder)
21
+ /// {
22
+ /// builder.Bind&lt;IBattleService&gt;().To&lt;BattleService&gt;().AsScoped();
23
+ /// }
24
+ /// }
25
+ /// </code>
26
+ /// </summary>
27
+ public abstract class LifetimeScope : MonoBehaviour
28
+ {
29
+ // === Static Registry ===
30
+ private static readonly List<LifetimeScope> _activeScopes = new();
31
+
32
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
33
+ private static void ResetStatic()
34
+ {
35
+ _activeScopes.Clear();
36
+ }
37
+
38
+ // === Inspector Fields ===
39
+
40
+ [Header("Scope Hierarchy")]
41
+ [SerializeField]
42
+ [Tooltip("부모 LifetimeScope. 없으면 RootScope로 생성됨.")]
43
+ private LifetimeScope _parent;
44
+
45
+ [SerializeField]
46
+ [Tooltip("true면 Awake에서 자동 초기화, false면 수동으로 Initialize() 호출 필요")]
47
+ private bool _autoInitialize = true;
48
+
49
+ // === Instance State ===
50
+
51
+ private IScope _scope;
52
+ private bool _isInitialized;
53
+ private int _hierarchyDepth;
54
+
55
+ /// <summary>
56
+ /// 이 LifetimeScope의 IScope.
57
+ /// </summary>
58
+ public IScope Scope => _scope;
59
+
60
+ /// <summary>
61
+ /// 초기화 완료 여부
62
+ /// </summary>
63
+ public bool IsInitialized => _isInitialized;
64
+
65
+ #region Unity Lifecycle
66
+
67
+ protected virtual void Awake()
68
+ {
69
+ if (_autoInitialize)
70
+ {
71
+ Initialize();
72
+ }
73
+ }
74
+
75
+ protected virtual void OnDestroy()
76
+ {
77
+ Dispose();
78
+ }
79
+
80
+ #endregion
81
+
82
+ #region Public API
83
+
84
+ /// <summary>
85
+ /// 수동 초기화.
86
+ /// _autoInitialize가 false일 때 사용.
87
+ /// </summary>
88
+ public void Initialize()
89
+ {
90
+ if (_isInitialized)
91
+ {
92
+ Debug.LogWarning($"[LifetimeScope] {GetType().Name} is already initialized.");
93
+ return;
94
+ }
95
+
96
+ var builder = new ScopeBuilder();
97
+ Configure(builder);
98
+
99
+ if (_parent != null)
100
+ {
101
+ if (!_parent.IsInitialized)
102
+ {
103
+ _parent.Initialize();
104
+ }
105
+ _scope = builder.Build(_parent.Scope);
106
+ }
107
+ else
108
+ {
109
+ // parent가 없으면 RootScope로 빌드
110
+ _scope = builder.Build(parent: null);
111
+ KDI.SetRootScope(_scope);
112
+ }
113
+
114
+ // static registry 등록
115
+ _hierarchyDepth = ComputeDepth(transform);
116
+ _activeScopes.Add(this);
117
+
118
+ _isInitialized = true;
119
+
120
+ // 하위 계층 IInjectable 일괄 주입 (Push)
121
+ InjectChildren();
122
+
123
+ Debug.Log($"[LifetimeScope] {GetType().Name} initialized.");
124
+ }
125
+
126
+ /// <summary>
127
+ /// 스코프 정리.
128
+ /// OnDestroy에서 자동 호출됨.
129
+ /// </summary>
130
+ public void Dispose()
131
+ {
132
+ if (!_isInitialized)
133
+ return;
134
+
135
+ _activeScopes.Remove(this);
136
+ _scope?.Dispose();
137
+ _scope = null;
138
+ _isInitialized = false;
139
+
140
+ Debug.Log($"[LifetimeScope] {GetType().Name} disposed.");
141
+ }
142
+
143
+ #endregion
144
+
145
+ #region Push Injection
146
+
147
+ /// <summary>
148
+ /// 하위 계층의 IInjectable MonoBehaviour를 찾아 일괄 주입.
149
+ /// child LifetimeScope 경계에서 탐색을 중단하여 하위 스코프 영역을 침범하지 않는다.
150
+ /// </summary>
151
+ private void InjectChildren()
152
+ {
153
+ InjectHierarchy(transform);
154
+ }
155
+
156
+ private void InjectHierarchy(Transform current)
157
+ {
158
+ for (int i = 0; i < current.childCount; i++)
159
+ {
160
+ var child = current.GetChild(i);
161
+
162
+ // child LifetimeScope가 있으면 탐색 중단 (그 scope의 영역)
163
+ if (child.TryGetComponent<LifetimeScope>(out _))
164
+ continue;
165
+
166
+ // 이 GameObject의 IInjectable 컴포넌트 주입
167
+ var injectables = child.GetComponents<IInjectable>();
168
+ foreach (var injectable in injectables)
169
+ {
170
+ DependencyInjector.Inject(injectable, _scope);
171
+
172
+ if (injectable is DIBehaviour dib)
173
+ dib.SetInjected(_scope);
174
+ }
175
+
176
+ // [Inject] 필드가 있지만 IInjectable 미구현인 MonoBehaviour 경고
177
+ var behaviours = child.GetComponents<MonoBehaviour>();
178
+ foreach (var mb in behaviours)
179
+ {
180
+ if (mb == null || mb is IInjectable) continue;
181
+ DependencyInjector.WarnIfHasInjectFieldsWithoutIInjectable(mb);
182
+ }
183
+
184
+ // 재귀
185
+ InjectHierarchy(child);
186
+ }
187
+ }
188
+
189
+ #endregion
190
+
191
+ #region Static Helpers (Registry 기반)
192
+
193
+ /// <summary>
194
+ /// static registry에서 가장 가까운 LifetimeScope 찾기.
195
+ /// GetComponentInParent 대신 Transform.IsChildOf (네이티브 호출) 사용.
196
+ /// </summary>
197
+ public static LifetimeScope Find(Transform from) => FindInternal(from);
198
+
199
+ public static LifetimeScope Find(GameObject from) => FindInternal(from?.transform);
200
+
201
+ public static LifetimeScope Find(Component from) => FindInternal(from?.transform);
202
+
203
+ private static LifetimeScope FindInternal(Transform from)
204
+ {
205
+ if (from == null) return null;
206
+
207
+ LifetimeScope best = null;
208
+ int bestDepth = -1;
209
+
210
+ for (int i = 0; i < _activeScopes.Count; i++)
211
+ {
212
+ var scope = _activeScopes[i];
213
+ if (scope == null || !scope._isInitialized) continue;
214
+
215
+ if (from.IsChildOf(scope.transform))
216
+ {
217
+ if (scope._hierarchyDepth > bestDepth)
218
+ {
219
+ bestDepth = scope._hierarchyDepth;
220
+ best = scope;
221
+ }
222
+ }
223
+ }
224
+
225
+ return best;
226
+ }
227
+
228
+ /// <summary>
229
+ /// 씬에서 루트 LifetimeScope 찾기.
230
+ /// </summary>
231
+ public static LifetimeScope FindRoot()
232
+ {
233
+ for (int i = 0; i < _activeScopes.Count; i++)
234
+ {
235
+ if (_activeScopes[i]._parent == null)
236
+ return _activeScopes[i];
237
+ }
238
+ return null;
239
+ }
240
+
241
+ private static int ComputeDepth(Transform t)
242
+ {
243
+ int depth = 0;
244
+ while (t.parent != null)
245
+ {
246
+ depth++;
247
+ t = t.parent;
248
+ }
249
+ return depth;
250
+ }
251
+
252
+ #endregion
253
+
254
+ #region Abstract
255
+
256
+ /// <summary>
257
+ /// 서비스 등록.
258
+ /// 하위 클래스에서 구현하여 builder.Bind 등으로 서비스 등록.
259
+ /// </summary>
260
+ protected abstract void Configure(ScopeBuilder builder);
261
+
262
+ #endregion
263
+ }
264
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: f4993c3e3e695584782dec19defb682a
@@ -0,0 +1,8 @@
1
+ fileFormatVersion: 2
2
+ guid: 9901d5cd5f1532c4ca9782523c9eeb10
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
@@ -0,0 +1,114 @@
1
+ using System;
2
+
3
+ namespace Kylin.DI
4
+ {
5
+ public class DependencyBuilder<T> where T : class
6
+ {
7
+ private readonly ScopeBuilder _builder;
8
+ private readonly Type _serviceType;
9
+ private Type _implementationType;
10
+ private Lifetime _lifetime = Lifetime.Singleton;
11
+ private object _instance;
12
+ private Func<IScope, object> _factory;
13
+
14
+ internal DependencyBuilder(ScopeBuilder builder, Type serviceType)
15
+ {
16
+ _builder = builder;
17
+ _serviceType = serviceType;
18
+ }
19
+
20
+ /// <summary>
21
+ /// 구현타입 지정
22
+ /// </summary>
23
+ public DependencyBuilder<T> To<TImplementation>()
24
+ where TImplementation : IDependencyObject, T
25
+ {
26
+ _implementationType = typeof(TImplementation);
27
+ return this;
28
+ }
29
+
30
+ /// <summary>
31
+ /// 싱글톤 (RootScope에서만 허용)
32
+ /// </summary>
33
+ public void AsSingleton()
34
+ {
35
+ _lifetime = Lifetime.Singleton;
36
+ FinishRegistration();
37
+ }
38
+
39
+ /// <summary>
40
+ /// 매번 새 인스턴스
41
+ /// </summary>
42
+ public void AsTransient()
43
+ {
44
+ _lifetime = Lifetime.Transient;
45
+ FinishRegistration();
46
+ }
47
+
48
+ /// <summary>
49
+ /// 스코프 내 단일 인스턴스
50
+ /// </summary>
51
+ public void AsScoped()
52
+ {
53
+ _lifetime = Lifetime.Scoped;
54
+ FinishRegistration();
55
+ }
56
+
57
+ public void FromInstance(T instance)
58
+ {
59
+ if (instance is IDependencyObject)
60
+ {
61
+ _instance = instance;
62
+ FinishRegistration();
63
+ }
64
+ else
65
+ {
66
+ throw new ArgumentException($"Instance must implement IDependencyObject");
67
+ }
68
+ }
69
+
70
+ /// <summary>
71
+ /// 팩토리 등록
72
+ /// </summary>
73
+ public DependencyBuilder<T> FromFactory(Func<IScope, T> factory)
74
+ {
75
+ _factory = scope => factory(scope);
76
+ return this;
77
+ }
78
+
79
+ private void FinishRegistration()
80
+ {
81
+ if (_instance != null)
82
+ {
83
+ _builder.AddRegistration(new Registration
84
+ {
85
+ ServiceType = _serviceType,
86
+ Instance = _instance,
87
+ Lifetime = Lifetime.Scoped
88
+ });
89
+ }
90
+ else if (_factory != null)
91
+ {
92
+ _builder.AddRegistration(new Registration
93
+ {
94
+ ServiceType = _serviceType,
95
+ Factory = _factory,
96
+ Lifetime = _lifetime
97
+ });
98
+ }
99
+ else if (_implementationType != null)
100
+ {
101
+ _builder.AddRegistration(new Registration
102
+ {
103
+ ServiceType = _serviceType,
104
+ ImplementationType = _implementationType,
105
+ Lifetime = _lifetime
106
+ });
107
+ }
108
+ else
109
+ {
110
+ throw new InvalidOperationException("[KDI] Registration is incomplete");
111
+ }
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 5809585d6abc44d4994f34b9dfe288d9
@@ -0,0 +1,104 @@
1
+ using System;
2
+ using System.Collections.Concurrent;
3
+ using System.Collections.Generic;
4
+ using System.Linq;
5
+ using System.Reflection;
6
+ using UnityEngine;
7
+
8
+ namespace Kylin.DI
9
+ {
10
+ public static class DependencyInjector
11
+ {
12
+ private static readonly ConcurrentDictionary<Type, FieldInfo[]> _fieldCache
13
+ = new ConcurrentDictionary<Type, FieldInfo[]>();
14
+
15
+ public static FieldInfo[] GetCachedInjectableFields(Type type)
16
+ {
17
+ return _fieldCache.GetOrAdd(type, t =>
18
+ {
19
+ var fieldList = new List<FieldInfo>();
20
+ var currentType = t;
21
+ while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object))
22
+ {
23
+ var fields = currentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)
24
+ .Where(f => f.GetCustomAttribute<InjectAttribute>() != null);
25
+ fieldList.AddRange(fields);
26
+ currentType = currentType.BaseType;
27
+ }
28
+ return fieldList.ToArray();
29
+ });
30
+ }
31
+
32
+ /// <summary>
33
+ /// scope 없이 호출 시 KDI.RootScope에서 Resolve
34
+ /// </summary>
35
+ public static void Inject(this IInjectable target)
36
+ {
37
+ Inject(target, KDI.RootScope);
38
+ }
39
+
40
+ public static void Inject(this IInjectable target, IScope scope)
41
+ {
42
+ if (target == null || scope == null) return;
43
+
44
+ var fields = GetCachedInjectableFields(target.GetType());
45
+ if (fields.Length == 0)
46
+ {
47
+ if (target is IPostInjectable post)
48
+ post.PostInject();
49
+ return;
50
+ }
51
+
52
+ // Phase 1: 모든 의존성 Resolve 시도 — 하나라도 실패하면 전체 주입 중단
53
+ var resolved = new object[fields.Length];
54
+ for (int i = 0; i < fields.Length; i++)
55
+ {
56
+ try
57
+ {
58
+ resolved[i] = scope.Resolve(fields[i].FieldType);
59
+ }
60
+ catch (Exception ex)
61
+ {
62
+ Debug.LogError(
63
+ $"[KDI] {target.GetType().Name}.{fields[i].Name} ({fields[i].FieldType.Name}) resolve 실패: {ex.Message}\n" +
64
+ $" → 주입 중단: {target.GetType().Name}의 모든 [Inject] 필드가 주입되지 않았습니다.");
65
+ return;
66
+ }
67
+ }
68
+
69
+ // Phase 2: 전부 성공 — 일괄 주입
70
+ for (int i = 0; i < fields.Length; i++)
71
+ {
72
+ fields[i].SetValue(target, resolved[i]);
73
+ }
74
+
75
+ if (target is IPostInjectable postInjectable)
76
+ {
77
+ postInjectable.PostInject();
78
+ }
79
+ }
80
+
81
+ /// <summary>
82
+ /// [Inject] 필드가 있지만 IInjectable을 구현하지 않은 타입 경고.
83
+ /// Scope.CreateInstance 및 LifetimeScope.InjectHierarchy에서 호출.
84
+ /// </summary>
85
+ public static void WarnIfHasInjectFieldsWithoutIInjectable(object target)
86
+ {
87
+ if (target == null || target is IInjectable) return;
88
+
89
+ var fields = GetCachedInjectableFields(target.GetType());
90
+ if (fields.Length > 0)
91
+ {
92
+ var fieldNames = string.Join(", ", fields.Select(f => f.Name));
93
+ Debug.LogWarning(
94
+ $"[KDI] {target.GetType().Name}에 [Inject] 필드({fieldNames})가 있지만 " +
95
+ $"IInjectable을 구현하지 않았습니다. 주입이 수행되지 않습니다.");
96
+ }
97
+ }
98
+
99
+ public static void ClearCache()
100
+ {
101
+ _fieldCache.Clear();
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 28225fb11a729c44b8709cad344b2080
@@ -0,0 +1,36 @@
1
+ using System;
2
+ using System.Collections.Concurrent;
3
+ using System.Linq.Expressions;
4
+ using System.Reflection;
5
+ using UnityEngine;
6
+
7
+ namespace Kylin.DI
8
+ {
9
+ internal static class InstanceFactory
10
+ {
11
+ private static readonly ConcurrentDictionary<Type, Func<object>> _cache = new();
12
+
13
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
14
+ private static void Reset() => _cache.Clear();
15
+
16
+ public static object Create(Type type)
17
+ {
18
+ return _cache.GetOrAdd(type, BuildFactory)();
19
+ }
20
+
21
+ private static Func<object> BuildFactory(Type type)
22
+ {
23
+ var ctor = type.GetConstructor(
24
+ BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null);
25
+
26
+ if (ctor == null)
27
+ throw new InvalidOperationException(
28
+ $"[KDI] {type.Name}에 public 파라미터 없는 생성자가 없습니다.");
29
+
30
+ var newExpr = Expression.New(ctor);
31
+ var lambda = Expression.Lambda<Func<object>>(
32
+ Expression.Convert(newExpr, typeof(object)));
33
+ return lambda.Compile();
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: dd04a076fe661f547810aba13c355ff9