DI 추가

This commit is contained in:
logonkhi
2025-12-15 20:17:38 +09:00
parent ab86affa32
commit 0df2f0d8da
131 changed files with 19661 additions and 554 deletions

View File

@@ -36,6 +36,8 @@ namespace Factory
[SerializeField]
private FactorySideTabBar sideTabBar;
[SerializeField] private PropertyWindow propertyWindow;
public Action Initialized;
@@ -193,7 +195,7 @@ namespace Factory
topMenu.AddMenuItem(new MenuItemData("Settings", "Settings", new SettingOpenCommand()));
topMenu.AddMenuItem(new MenuItemData("PropertyWindow", "PropertyWindow", new ActionCommand(async () =>
{
PropertyWindow.Instance.Show();
propertyWindow.Show();
})));
topMenu.Initialize();
@@ -300,7 +302,7 @@ namespace Factory
private void SetupPropertyWindow()
{
PropertyWindow.Instance.LoadProperties(new List<IPropertyItem>
propertyWindow.LoadProperties(new List<IPropertyItem>
{
new StringProperty("prop1", "String Property", "Initial Value")
{
@@ -400,7 +402,7 @@ namespace Factory
}
});
PropertyWindow.Instance.PropertyValueChanged += (sender, e) =>
propertyWindow.PropertyValueChanged += (sender, e) =>
{
Debug.Log($"Property Id:{e.PropertyId}, type:{e.PropertyType}, newValue: {e.NewValue}");
};

View File

@@ -16,6 +16,8 @@ namespace Simulator
[SerializeField]
private TopMenuController GNB;
[SerializeField] private PropertyWindow propertyWindow;
public Action Initialized;
/// <summary>
@@ -95,8 +97,8 @@ namespace Simulator
{
new MenuItemData("explorer_window", "탐색창", new ActionCommand(()=> Debug.Log("탐색창"))),
new MenuItemData("property_window", "속성창", new ActionCommand(()=> {
if(PropertyWindow.Instance.IsVisible) PropertyWindow.Instance.Hide();
else PropertyWindow.Instance.Show();
if(propertyWindow.IsVisible) propertyWindow.Hide();
else propertyWindow.Show();
})),
}),
new MenuItemData("help", "도움말", subMenuItems: new List<MenuItemData>
@@ -110,7 +112,7 @@ namespace Simulator
private void SetupPropertyWindow()
{
PropertyWindow.Instance.LoadProperties(new List<IPropertyItem>
propertyWindow.LoadProperties(new List<IPropertyItem>
{
new StringProperty("prop1", "String Property", "Initial Value")
{
@@ -210,7 +212,7 @@ namespace Simulator
}
});
PropertyWindow.Instance.PropertyValueChanged += (sender, e) =>
propertyWindow.PropertyValueChanged += (sender, e) =>
{
Debug.Log($"Property Id:{e.PropertyId}, type:{e.PropertyType}, newValue: {e.NewValue}");
};

View File

@@ -0,0 +1,29 @@
#nullable enable
using UnityEngine;
using UVC.Core;
using UVC.UI.Menu;
using UVC.Util;
namespace UVC.Studio
{
public class StudioAppContext : InjectorAppContext
{
/// <summary>
/// App 라이프사이클 서비스들을 등록합니다.
/// Awake 시점에 자동 호출되며, 모든 서비스는 씬 전환 시에도 유지됩니다.
/// </summary>
protected override void RegisterServices()
{
base.RegisterServices();
// 여기에 StudioAppContext에 등록할 서비스들을 추가하세요.
Injector.RegisterSingleton<StudioAppMain>();
Injector.RegisterSingleton<CursorManager>();
Injector.RegisterSingleton<ContextMenuManager>();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7fca4f445611fd64fbbaa0272d625878

View File

@@ -0,0 +1,39 @@
#nullable enable
using UnityEngine;
using UVC.Core;
using UVC.UI.Menu;
using UVC.UI.ToolBar;
using UVC.UI.Window.PropertyWindow;
namespace UVC.Studio
{
public class StudioSceneContext : InjectorSceneContext
{
[SerializeField] private Toolbox? toolbox;
[SerializeField] private StudioSideTabBar? sideTabBar;
[SerializeField] private PropertyWindow? propertyWindow;
[SerializeField] private TopMenuController? topMenu;
/// <summary>
/// Scene 라이프사이클 서비스들을 등록합니다.
/// 씬 로드 시 자동 호출되며, 씬 전환 시 자동으로 정리됩니다.
/// </summary>
protected override void RegisterSceneServices()
{
base.RegisterSceneServices();
// 여기에 StudioSceneContext에 등록할 서비스들을 추가하세요.
Injector.RegisterSingleton<StudioSceneMain>();
if(toolbox != null) Injector.RegisterInstance<Toolbox>(toolbox, ServiceLifetime.Scene);
if(sideTabBar != null) Injector.RegisterInstance<StudioSideTabBar>(sideTabBar, ServiceLifetime.Scene);
if(propertyWindow != null) Injector.RegisterInstance<PropertyWindow>(propertyWindow, ServiceLifetime.Scene);
if(topMenu != null) Injector.RegisterInstance<TopMenuController>(topMenu, ServiceLifetime.Scene);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4ce0e936853bf8546b511833bb0ecae1

View File

@@ -29,6 +29,8 @@ namespace UVC.Studio
[SerializeField]
private StudioSideTabBar sideTabBar;
[Inject]
private PropertyWindow propertyWindow;
public Action Initialized;
@@ -302,7 +304,7 @@ namespace UVC.Studio
private void SetupPropertyWindow()
{
PropertyWindow.Instance.LoadProperties(new List<IPropertyItem>
propertyWindow.LoadProperties(new List<IPropertyItem>
{
new StringProperty("prop1", "String Property", "Initial Value")
{
@@ -402,7 +404,7 @@ namespace UVC.Studio
}
});
PropertyWindow.Instance.PropertyValueChanged += (sender, e) =>
propertyWindow.PropertyValueChanged += (sender, e) =>
{
Debug.Log($"Property Id:{e.PropertyId}, type:{e.PropertyType}, newValue: {e.NewValue}");
};

View File

@@ -10,12 +10,12 @@ using UVC.UI.Tab;
namespace UVC.Studio
{
public class StudioSideTabBar : SingletonScene<StudioSideTabBar>, IPointerEnterHandler, IPointerExitHandler
public class StudioSideTabBar : MonoBehaviour
{
[SerializeField]
private TabController? tabController;
protected override void Init()
private void Awake()
{
if (tabController == null)
{
@@ -47,22 +47,5 @@ namespace UVC.Studio
}
}
/// <summary>
/// 마우스 포인터가 이 UI 요소의 영역 안으로 들어왔을 때 호출됩니다.
/// UI와 상호작용하는 동안 3D 뷰의 카메라가 움직이지 않도록 컨트롤러를 비활성화합니다.
/// </summary>
public virtual void OnPointerEnter(PointerEventData eventData)
{
StudioCameraController.Instance.Enable = false; // 카메라 컨트롤러 비활성화
}
/// <summary>
/// 마우스 포인터가 이 UI 요소의 영역 밖으로 나갔을 때 호출됩니다.
/// 카메라 컨트롤을 다시 활성화하여 3D 뷰를 조작할 수 있도록 합니다.
/// </summary>
public virtual void OnPointerExit(PointerEventData eventData)
{
StudioCameraController.Instance.Enable = true; // 카메라 컨트롤러 활성화
}
}
}

View File

@@ -0,0 +1,65 @@
using RTGLite;
using System;
using UnityEngine;
using UVC.Core;
using UVC.UI.Buttons;
using UVC.UI.Window.PropertyWindow;
namespace UVC.Studio.UI.Buttons
{
public class PropertyWindowToggleButton : MonoBehaviour
{
private ImageToggle toggleButton;
void Awake()
{
toggleButton = GetComponent<ImageToggle>();
toggleButton.OnValueChanged.AddListener(OnToggleValueChanged);
StudioSceneMain.Instance.Initialized += OnSceneInitialized;
}
private void OnSceneInitialized()
{
PropertyWindow propertyWindow = InjectorAppContext.Instance.Get<PropertyWindow>();
if (propertyWindow != null)
{
propertyWindow.Hide();
}
}
private void OnToggleValueChanged(bool visible)
{
PropertyWindow propertyWindow = InjectorAppContext.Instance.Get<PropertyWindow>();
if (propertyWindow != null)
{
propertyWindow.SetVisibility(visible);
// View Gizmo의 Screen Padding 값 수정
// RTGizmos.get.skin.viewGizmoStyle.screenPadding 으로 접근
if (RTGizmos.get != null && RTGizmos.get.skin != null)
{
// PropertyWindow가 보이면 padding을 늘리고, 숨겨지면 원래대로
if (visible)
{
// X: 오른쪽 여백, Y: 위쪽 여백 (TopRight alignment 기준)
RTGizmos.get.skin.viewGizmoStyle.screenPadding = new Vector2(300f, 40f);
}
else
{
RTGizmos.get.skin.viewGizmoStyle.screenPadding = new Vector2(0f, 40f);
}
}
}
}
void OnDestroy()
{
if (toggleButton != null)
{
toggleButton.OnValueChanged.RemoveListener(OnToggleValueChanged);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5fa85141ca6765c40a59d9224631e2b9

View File

@@ -9,7 +9,7 @@ using UnityEngine;
using UVC.Data.Core;
using UVC.Data.Http;
using UVC.Log;
using UVC.network;
using UVC.Network;
namespace UVC.Data.Mqtt
{

View File

@@ -6,7 +6,7 @@ using System.Linq;
using System.Threading;
using UnityEngine;
using UVC.Data.Core;
using UVC.network;
using UVC.Network;
namespace UVC.Data.Mqtt
{

View File

@@ -7,7 +7,7 @@ using System.Text;
using UnityEngine;
using UVC.Log;
namespace UVC.network
namespace UVC.Network
{
/// <summary>
/// MQTT 클라이언트를 관리하고 메시지 송수신을 처리하는 서비스 클래스입니다.

View File

@@ -4,10 +4,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UVC.Core;
using UVC.Factory;
using UVC.Factory.Cameras;
namespace UVC.UI.Window.PropertyWindow
{
@@ -16,7 +12,7 @@ namespace UVC.UI.Window.PropertyWindow
/// Model과 View 사이의 중재자 역할을 합니다.
/// 그룹과 개별 아이템을 혼용하여 사용할 수 있습니다.
/// </summary>
public class PropertyWindow : SingletonScene<PropertyWindow>, IPointerEnterHandler, IPointerExitHandler
public class PropertyWindow : MonoBehaviour
{
[SerializeField]
private PropertyView _view;
@@ -508,27 +504,16 @@ namespace UVC.UI.Window.PropertyWindow
public void Hide()
{
gameObject.SetActive(false);
FactoryCameraController.Instance.Enable = true;
}
#endregion
#region Pointer Events
/// <summary>
/// 마우스 포인터가 이 UI 요소의 영역 안으로 들어왔을 때 호출됩니다.
/// </summary>
public virtual void OnPointerEnter(PointerEventData eventData)
public void ToggleVisibility()
{
FactoryCameraController.Instance.Enable = false;
gameObject.SetActive(!gameObject.activeSelf);
}
/// <summary>
/// 마우스 포인터가 이 UI 요소의 영역 밖으로 나갔을 때 호출됩니다.
/// </summary>
public virtual void OnPointerExit(PointerEventData eventData)
public void SetVisibility(bool visible)
{
FactoryCameraController.Instance.Enable = true;
gameObject.SetActive(visible);
}
#endregion

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fe3997843ad7ef645a7922394d5259d7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d5c0eb21fa2240f47844cee075aeda8b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,247 @@
using System;
namespace UVC.Core
{
/// <summary>
/// 필드 또는 프로퍼티에 의존성 주입을 표시하는 어트리뷰트
/// </summary>
/// <remarks>
/// <para><b>[ 개요 ]</b></para>
/// <para>Injector가 이 어트리뷰트가 붙은 멤버에 자동으로 의존성을 주입합니다.</para>
/// <para>필드(Field)와 프로퍼티(Property) 모두에 사용할 수 있습니다.</para>
///
/// <para><b>[ 주입 시점 ]</b></para>
/// <list type="bullet">
/// <item><description>MonoBehaviour: InjectorSceneContext.PerformSceneInjection() 시점</description></item>
/// <item><description>순수 C# 클래스: Injector가 인스턴스 생성 직후 자동 주입</description></item>
/// <item><description>수동: Injector.Inject(target) 또는 InjectGameObject(go) 호출 시</description></item>
/// </list>
///
/// <para><b>[ 기본 사용법 ]</b></para>
/// <code>
/// public class PlayerController : MonoBehaviour
/// {
/// [Inject] private ILogService _logger;
/// [Inject] private IAudioManager _audioManager;
///
/// private void Start()
/// {
/// _logger.Log("PlayerController initialized");
/// _audioManager.PlaySFX("spawn");
/// }
/// }
/// </code>
///
/// <para><b>[ 선택적 의존성 (Optional) ]</b></para>
/// <para>Optional = true로 설정하면 서비스가 등록되지 않은 경우에도 예외가 발생하지 않습니다.</para>
/// <code>
/// public class DebugOverlay : MonoBehaviour
/// {
/// // 디버그 서비스가 없어도 동작해야 함
/// [Inject(Optional = true)]
/// private IDebugService _debugService;
///
/// private void Update()
/// {
/// // null 체크 필요
/// _debugService?.DrawStats();
/// }
/// }
/// </code>
///
/// <para><b>[ 순수 C# 클래스에서 사용 ]</b></para>
/// <para>MonoBehaviour가 아닌 일반 클래스에서도 의존성 주입이 가능합니다.</para>
/// <code>
/// public class GameService : IGameService
/// {
/// [Inject] private ILogService _logger;
/// [Inject] private ISettingsManager _settings;
///
/// public void Initialize()
/// {
/// _logger.Log($"Language: {_settings.Language}");
/// }
/// }
/// </code>
///
/// <para><b>[ 프로퍼티 주입 ]</b></para>
/// <code>
/// public class UIManager : MonoBehaviour
/// {
/// [Inject]
/// public ILogService Logger { get; private set; }
/// }
/// </code>
///
/// <para><b>[ 주의사항 ]</b></para>
/// <list type="bullet">
/// <item><description>private 필드에도 주입이 가능합니다 (리플렉션 사용)</description></item>
/// <item><description>상속 관계에서 부모 클래스의 [Inject] 필드도 주입됩니다 (Inherited = true)</description></item>
/// <item><description>한 멤버에 여러 번 사용할 수 없습니다 (AllowMultiple = false)</description></item>
/// <item><description>순환 의존성이 있으면 예외가 발생합니다</description></item>
/// </list>
/// </remarks>
/// <seealso cref="Injector"/>
/// <seealso cref="IInjector.Inject"/>
/// <seealso cref="IInjector.InjectGameObject"/>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class InjectAttribute : System.Attribute
{
/// <summary>
/// 선택적 의존성 여부 - true면 서비스가 없어도 예외 발생 안 함
/// </summary>
/// <remarks>
/// <para><b>[ 기본값 ]</b> false (필수 의존성)</para>
///
/// <para><b>[ true일 때 동작 ]</b></para>
/// <list type="bullet">
/// <item><description>서비스가 등록되지 않은 경우 null이 주입됨</description></item>
/// <item><description>예외가 발생하지 않음</description></item>
/// <item><description>코드에서 null 체크가 필요함</description></item>
/// </list>
///
/// <para><b>[ false일 때 동작 ]</b></para>
/// <list type="bullet">
/// <item><description>서비스가 등록되지 않은 경우 InvalidOperationException 발생</description></item>
/// <item><description>필수 의존성임을 명시</description></item>
/// </list>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // 필수 의존성 - 없으면 예외 발생
/// [Inject] private ILogService _logger;
///
/// // 선택적 의존성 - 없으면 null
/// [Inject(Optional = true)] private IAnalyticsService _analytics;
/// </code>
/// </remarks>
public bool Optional { get; set; } = false;
/// <summary>
/// 특정 ID로 등록된 서비스를 주입 (향후 확장용)
/// </summary>
/// <remarks>
/// <para><b>[ 용도 ]</b></para>
/// <para>동일한 인터페이스로 여러 구현체가 등록된 경우 특정 구현체를 선택할 때 사용합니다.</para>
/// <para>현재 버전에서는 구현되지 않았으며, 향후 Named Registration 기능 추가 시 사용됩니다.</para>
///
/// <para><b>[ 예정된 사용법 ]</b></para>
/// <code>
/// // 등록 (향후 지원 예정)
/// Injector.Register&lt;ILogger, FileLogger&gt;("file");
/// Injector.Register&lt;ILogger, ConsoleLogger&gt;("console");
///
/// // 주입
/// [Inject(Id = "file")] private ILogger _fileLogger;
/// [Inject(Id = "console")] private ILogger _consoleLogger;
/// </code>
/// </remarks>
public string Id { get; set; } = null;
/// <summary>
/// 기본 생성자
/// </summary>
public InjectAttribute() { }
/// <summary>
/// ID를 지정하는 생성자
/// </summary>
/// <param name="id">서비스 식별자</param>
public InjectAttribute(string id)
{
Id = id;
}
}
/// <summary>
/// 클래스를 자동으로 Injector에 등록하도록 표시하는 어트리뷰트
/// </summary>
/// <remarks>
/// <para><b>[ 개요 ]</b></para>
/// <para>이 어트리뷰트가 붙은 클래스는 자동으로 Injector에 등록될 수 있습니다.</para>
/// <para>리플렉션을 통해 어셈블리를 스캔하여 자동 등록하는 기능에서 사용됩니다.</para>
///
/// <para><b>[ 주의사항 ]</b></para>
/// <para>현재 버전에서는 자동 스캔 기능이 구현되지 않았습니다.</para>
/// <para>InjectorAppContext 또는 InjectorSceneContext에서 수동으로 등록해야 합니다.</para>
///
/// <para><b>[ 예정된 사용법 ]</b></para>
/// <code>
/// // 클래스에 어트리뷰트 추가
/// [Register(typeof(ILogService), ServiceLifetime.App)]
/// public class ConsoleLogger : ILogService
/// {
/// public void Log(string message) => Debug.Log(message);
/// }
///
/// // 자동 등록 (향후 지원 예정)
/// Injector.RegisterFromAssembly(typeof(ConsoleLogger).Assembly);
/// </code>
///
/// <para><b>[ 다중 인터페이스 등록 ]</b></para>
/// <para>AllowMultiple = true이므로 하나의 클래스에 여러 번 사용할 수 있습니다.</para>
/// <code>
/// [Register(typeof(ILogService), ServiceLifetime.App)]
/// [Register(typeof(IDebugLogger), ServiceLifetime.App)]
/// public class MultiLogger : ILogService, IDebugLogger
/// {
/// // 두 인터페이스로 모두 등록됨
/// }
/// </code>
/// </remarks>
/// <seealso cref="Injector"/>
/// <seealso cref="ServiceLifetime"/>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class RegisterAttribute : System.Attribute
{
/// <summary>
/// 등록할 인터페이스 타입
/// </summary>
/// <remarks>
/// <para>null이면 자기 자신 타입으로 등록됩니다.</para>
/// <para>인터페이스 또는 부모 클래스 타입을 지정할 수 있습니다.</para>
/// </remarks>
public Type InterfaceType { get; }
/// <summary>
/// 서비스 라이프사이클
/// </summary>
/// <remarks>
/// <para>App: 앱 전체, Scene: 현재 씬, Transient: 매번 새로 생성</para>
/// </remarks>
public ServiceLifetime Lifetime { get; }
/// <summary>
/// 자기 자신 타입으로 등록하는 생성자
/// </summary>
/// <param name="lifetime">서비스 라이프사이클 (기본값: App)</param>
/// <example>
/// <code>
/// [Register(ServiceLifetime.App)]
/// public class SettingsManager { }
/// </code>
/// </example>
public RegisterAttribute(ServiceLifetime lifetime = ServiceLifetime.App)
{
InterfaceType = null;
Lifetime = lifetime;
}
/// <summary>
/// 특정 인터페이스 타입으로 등록하는 생성자
/// </summary>
/// <param name="interfaceType">등록할 인터페이스 타입</param>
/// <param name="lifetime">서비스 라이프사이클 (기본값: App)</param>
/// <example>
/// <code>
/// [Register(typeof(ILogService), ServiceLifetime.App)]
/// public class ConsoleLogger : ILogService { }
/// </code>
/// </example>
public RegisterAttribute(Type interfaceType, ServiceLifetime lifetime = ServiceLifetime.App)
{
InterfaceType = interfaceType;
Lifetime = lifetime;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e9e646e0bb06fb5429ba49d47c21b415

View File

@@ -0,0 +1,498 @@
using System;
using UnityEngine;
namespace UVC.Core
{
/// <summary>
/// 의존성 주입(DI) 컨테이너 인터페이스
/// </summary>
/// <remarks>
/// <para><b>[ 개요 ]</b></para>
/// <para>서비스의 등록, 해결, 주입, 라이프사이클 관리를 담당하는 DI 컨테이너의 인터페이스입니다.</para>
/// <para>IDisposable을 구현하여 리소스 정리를 지원합니다.</para>
///
/// <para><b>[ 주요 기능 ]</b></para>
/// <list type="bullet">
/// <item><description><b>Registration:</b> 서비스 등록 (Register, RegisterInstance, RegisterPrefab, RegisterSingleton, RegisterFactory)</description></item>
/// <item><description><b>Resolution:</b> 서비스 해결 (Resolve, TryResolve, IsRegistered)</description></item>
/// <item><description><b>Injection:</b> 의존성 주입 (Inject, InjectGameObject)</description></item>
/// <item><description><b>Lifecycle:</b> 라이프사이클 관리 (OnSceneUnloaded, ClearServices)</description></item>
/// </list>
///
/// <para><b>[ 4가지 등록 타입 ]</b></para>
/// <list type="table">
/// <listheader>
/// <term>타입</term>
/// <description>등록 메서드</description>
/// </listheader>
/// <item>
/// <term>Type A (순수 C#)</term>
/// <description>Register<TInterface, TImpl>()</description>
/// </item>
/// <item>
/// <term>Type B (MonoBehaviour)</term>
/// <description>Register<TInterface, TImpl>() - TImpl이 MonoBehaviour인 경우</description>
/// </item>
/// <item>
/// <term>Type C (Prefab)</term>
/// <description>RegisterPrefab<T>()</description>
/// </item>
/// <item>
/// <term>Type D (Singleton)</term>
/// <description>RegisterSingleton<T>()</description>
/// </item>
/// </list>
///
/// <para><b>[ 체이닝 지원 ]</b></para>
/// <para>모든 등록 메서드는 IInjector를 반환하여 메서드 체이닝을 지원합니다.</para>
/// <code>
/// Injector
/// .Register<ILogService, ConsoleLogger>(ServiceLifetime.App)
/// .Register<IAudioManager, AudioManager>(ServiceLifetime.App)
/// .RegisterSingleton<SettingsManager>();
/// </code>
///
/// <para><b>[ 구현체 ]</b></para>
/// <para>이 인터페이스의 구현체는 <see cref="Injector"/> 클래스입니다.</para>
/// </remarks>
/// <seealso cref="Injector"/>
/// <seealso cref="InjectAttribute"/>
/// <seealso cref="ServiceLifetime"/>
public interface IInjector : IDisposable
{
#region Registration -
/// <summary>
/// 인터페이스와 구현 타입을 등록합니다. (Type A / Type B)
/// </summary>
/// <typeparam name="TInterface">서비스 인터페이스 타입</typeparam>
/// <typeparam name="TImplementation">구현 클래스 타입</typeparam>
/// <param name="lifetime">서비스 라이프사이클 (기본값: App)</param>
/// <returns>체이닝을 위한 IInjector 인스턴스</returns>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>TImplementation이 MonoBehaviour인지에 따라 Type A 또는 Type B로 자동 판별됩니다.</para>
/// <list type="bullet">
/// <item><description>순수 C# 클래스: new T()로 인스턴스 생성</description></item>
/// <item><description>MonoBehaviour: 새 GameObject를 생성하고 AddComponent 수행</description></item>
/// </list>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // Type A: 순수 C# 클래스
/// Injector.Register<ILogService, ConsoleLogger>(ServiceLifetime.App);
///
/// // Type B: MonoBehaviour (자동 판별)
/// Injector.Register<IAudioManager, AudioManager>(ServiceLifetime.App);
/// </code>
/// </remarks>
IInjector Register<TInterface, TImplementation>(ServiceLifetime lifetime = ServiceLifetime.App)
where TInterface : class
where TImplementation : class, TInterface;
/// <summary>
/// 타입을 자기 자신으로 등록합니다.
/// </summary>
/// <typeparam name="T">등록할 타입 (인터페이스가 아닌 구체 클래스)</typeparam>
/// <param name="lifetime">서비스 라이프사이클 (기본값: App)</param>
/// <returns>체이닝을 위한 IInjector 인스턴스</returns>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>인터페이스 없이 구체 클래스를 직접 등록할 때 사용합니다.</para>
/// <para>내부적으로 Register<T, T>()를 호출합니다.</para>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // 인터페이스 없이 직접 등록
/// Injector.Register<GameManager>(ServiceLifetime.App);
///
/// // 사용
/// var manager = Injector.Resolve<GameManager>();
/// </code>
/// </remarks>
IInjector Register<T>(ServiceLifetime lifetime = ServiceLifetime.App) where T : class;
/// <summary>
/// 이미 생성된 인스턴스를 등록합니다.
/// </summary>
/// <typeparam name="TInterface">서비스 인터페이스 타입</typeparam>
/// <param name="instance">등록할 인스턴스 (null 불가)</param>
/// <param name="lifetime">서비스 라이프사이클 (기본값: App)</param>
/// <returns>체이닝을 위한 IInjector 인스턴스</returns>
/// <exception cref="ArgumentNullException">instance가 null인 경우</exception>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>외부에서 생성한 인스턴스를 Injector에 등록합니다.</para>
/// <para>등록된 인스턴스에 대해서는 의존성 주입이 수행되지 않습니다.</para>
///
/// <para><b>[ 사용 사례 ]</b></para>
/// <list type="bullet">
/// <item><description>Context 자신을 등록할 때</description></item>
/// <item><description>외부 라이브러리에서 생성한 인스턴스를 등록할 때</description></item>
/// <item><description>테스트에서 Mock 객체를 등록할 때</description></item>
/// </list>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // Context 자신을 등록
/// Injector.RegisterInstance<InjectorAppContext>(this, ServiceLifetime.App);
///
/// // 외부에서 생성한 인스턴스 등록
/// var config = LoadConfigFromFile();
/// Injector.RegisterInstance<IAppConfig>(config);
/// </code>
/// </remarks>
IInjector RegisterInstance<TInterface>(TInterface instance, ServiceLifetime lifetime = ServiceLifetime.App)
where TInterface : class;
/// <summary>
/// Prefab을 기반으로 MonoBehaviour를 등록합니다. (Type C)
/// </summary>
/// <typeparam name="T">MonoBehaviour 타입</typeparam>
/// <param name="prefab">인스턴스화할 Prefab (null 불가)</param>
/// <param name="lifetime">서비스 라이프사이클 (기본값: App)</param>
/// <returns>체이닝을 위한 IInjector 인스턴스</returns>
/// <exception cref="ArgumentNullException">prefab이 null인 경우</exception>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>Resolve 시 등록된 Prefab을 Instantiate하여 인스턴스를 생성합니다.</para>
/// <para>Inspector에서 설정한 값들이 유지됩니다.</para>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // Inspector에서 할당한 Prefab 등록
/// [SerializeField] private UIManager uiManagerPrefab;
///
/// protected override void RegisterServices()
/// {
/// Injector.RegisterPrefab(uiManagerPrefab, ServiceLifetime.App);
/// }
/// </code>
/// </remarks>
IInjector RegisterPrefab<T>(T prefab, ServiceLifetime lifetime = ServiceLifetime.App)
where T : MonoBehaviour;
/// <summary>
/// Prefab GameObject를 기반으로 특정 컴포넌트(인터페이스)를 등록합니다. (Type C)
/// </summary>
/// <typeparam name="T">컴포넌트 타입 (인터페이스 또는 클래스)</typeparam>
/// <param name="prefab">인스턴스화할 Prefab GameObject (null 불가)</param>
/// <param name="lifetime">서비스 라이프사이클 (기본값: App)</param>
/// <returns>체이닝을 위한 IInjector 인스턴스</returns>
/// <exception cref="ArgumentNullException">prefab이 null인 경우</exception>
/// <exception cref="ArgumentException">Prefab에 T 타입 컴포넌트가 없는 경우</exception>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>인터페이스 타입으로 등록할 때 사용합니다.</para>
/// <para>Prefab의 GameObject에서 T 타입 컴포넌트를 찾아 등록합니다.</para>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // 인터페이스로 등록
/// [SerializeField] private GameObject uiManagerPrefab; // UIManager 컴포넌트 포함
///
/// protected override void RegisterServices()
/// {
/// Injector.RegisterPrefab<IUIManager>(uiManagerPrefab, ServiceLifetime.App);
/// }
///
/// // 사용
/// [Inject] private IUIManager _uiManager;
/// </code>
/// </remarks>
IInjector RegisterPrefab<T>(GameObject prefab, ServiceLifetime lifetime = ServiceLifetime.App)
where T : class;
/// <summary>
/// 기존 Singleton 클래스를 Injector에 연동합니다. (Type D)
/// </summary>
/// <typeparam name="T">Singleton<T>, SingletonApp<T>, SingletonScene<T> 상속 클래스</typeparam>
/// <returns>체이닝을 위한 IInjector 인스턴스</returns>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>기존 Singleton 패턴을 사용하는 클래스를 [Inject] 어트리뷰트로도 주입받을 수 있게 합니다.</para>
/// <para>Resolve 시 T.Instance 프로퍼티를 통해 인스턴스를 반환합니다.</para>
///
/// <para><b>[ 라이프사이클 자동 결정 ]</b></para>
/// <list type="bullet">
/// <item><description>Singleton<T>: App (순수 C#)</description></item>
/// <item><description>SingletonApp<T>: App (MonoBehaviour)</description></item>
/// <item><description>SingletonScene<T>: Scene (MonoBehaviour)</description></item>
/// </list>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // 등록
/// Injector.RegisterSingleton<SettingsManager>();
/// Injector.RegisterSingleton<NetworkManager>();
///
/// // [Inject] 사용
/// [Inject] private SettingsManager _settings;
///
/// // 기존 방식도 동작
/// SettingsManager.Instance.Save();
/// </code>
/// </remarks>
IInjector RegisterSingleton<T>() where T : class;
/// <summary>
/// 팩토리 함수를 사용하여 서비스를 등록합니다.
/// </summary>
/// <typeparam name="TInterface">서비스 인터페이스 타입</typeparam>
/// <param name="factory">인스턴스 생성 팩토리 함수 (null 불가)</param>
/// <param name="lifetime">서비스 라이프사이클 (기본값: App)</param>
/// <returns>체이닝을 위한 IInjector 인스턴스</returns>
/// <exception cref="ArgumentNullException">factory가 null인 경우</exception>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>커스텀 로직으로 인스턴스를 생성해야 할 때 사용합니다.</para>
/// <para>팩토리 함수는 IInjector를 파라미터로 받아 다른 서비스에 의존할 수 있습니다.</para>
///
/// <para><b>[ 사용 사례 ]</b></para>
/// <list type="bullet">
/// <item><description>생성자에 파라미터가 필요한 경우</description></item>
/// <item><description>조건부 인스턴스 생성이 필요한 경우</description></item>
/// <item><description>다른 서비스를 명시적으로 주입해야 하는 경우</description></item>
/// </list>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // 팩토리 함수로 등록
/// Injector.RegisterFactory<ISceneConfig>(injector => new SceneConfig
/// {
/// SceneName = SceneManager.GetActiveScene().name,
/// Difficulty = 1.5f
/// }, ServiceLifetime.Scene);
///
/// // 다른 서비스에 의존하는 경우
/// Injector.RegisterFactory<IPlayerService>(injector =>
/// {
/// var logger = injector.Resolve<ILogService>();
/// var settings = injector.Resolve<ISettingsManager>();
/// return new PlayerService(logger, settings);
/// });
/// </code>
/// </remarks>
IInjector RegisterFactory<TInterface>(Func<IInjector, TInterface> factory, ServiceLifetime lifetime = ServiceLifetime.App)
where TInterface : class;
#endregion
#region Resolution -
/// <summary>
/// 등록된 서비스를 해결(Resolve)합니다.
/// </summary>
/// <typeparam name="T">서비스 타입 (인터페이스 또는 구체 클래스)</typeparam>
/// <returns>서비스 인스턴스</returns>
/// <exception cref="InvalidOperationException">서비스가 등록되지 않은 경우</exception>
/// <exception cref="InvalidOperationException">순환 의존성이 감지된 경우</exception>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>등록된 서비스 타입에 따라 인스턴스를 생성하거나 캐시된 인스턴스를 반환합니다.</para>
///
/// <para><b>[ 라이프사이클별 동작 ]</b></para>
/// <list type="bullet">
/// <item><description>App/Scene: 첫 호출 시 생성 후 캐싱, 이후 캐시된 인스턴스 반환</description></item>
/// <item><description>Transient: 매 호출 시 새 인스턴스 생성</description></item>
/// </list>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // 서비스 해결
/// var logger = Injector.Resolve<ILogService>();
/// logger.Log("Hello!");
///
/// // 또는 InjectorAppContext를 통해
/// var logger = InjectorAppContext.Instance.Get<ILogService>();
/// </code>
/// </remarks>
T Resolve<T>() where T : class;
/// <summary>
/// 등록된 서비스를 해결합니다 (non-generic 버전).
/// </summary>
/// <param name="type">서비스 타입</param>
/// <returns>서비스 인스턴스</returns>
/// <exception cref="InvalidOperationException">서비스가 등록되지 않은 경우</exception>
/// <exception cref="InvalidOperationException">순환 의존성이 감지된 경우</exception>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>리플렉션이나 동적 타입 처리가 필요한 경우 사용합니다.</para>
/// <para>반환값을 적절한 타입으로 캐스팅해야 합니다.</para>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// Type serviceType = typeof(ILogService);
/// var logger = (ILogService)Injector.Resolve(serviceType);
/// </code>
/// </remarks>
object Resolve(Type type);
/// <summary>
/// 서비스 해결을 시도합니다. 실패 시 null을 반환합니다.
/// </summary>
/// <typeparam name="T">서비스 타입</typeparam>
/// <returns>서비스 인스턴스 또는 null</returns>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>서비스가 등록되지 않았거나 해결에 실패한 경우 예외 대신 null을 반환합니다.</para>
/// <para>선택적 의존성을 처리할 때 유용합니다.</para>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // 선택적으로 서비스 사용
/// var analytics = Injector.TryResolve<IAnalyticsService>();
/// analytics?.TrackEvent("game_start");
/// </code>
/// </remarks>
T TryResolve<T>() where T : class;
/// <summary>
/// 서비스 해결을 시도합니다. 실패 시 null을 반환합니다 (non-generic 버전).
/// </summary>
/// <param name="type">서비스 타입</param>
/// <returns>서비스 인스턴스 또는 null</returns>
/// <remarks>
/// <para>리플렉션이나 동적 타입 처리가 필요한 경우 사용합니다.</para>
/// </remarks>
object TryResolve(Type type);
/// <summary>
/// 특정 타입이 등록되어 있는지 확인합니다.
/// </summary>
/// <typeparam name="T">확인할 타입</typeparam>
/// <returns>등록되어 있으면 true, 아니면 false</returns>
/// <remarks>
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// if (Injector.IsRegistered<IAnalyticsService>())
/// {
/// var analytics = Injector.Resolve<IAnalyticsService>();
/// analytics.TrackEvent("event");
/// }
/// </code>
/// </remarks>
bool IsRegistered<T>() where T : class;
/// <summary>
/// 특정 타입이 등록되어 있는지 확인합니다 (non-generic 버전).
/// </summary>
/// <param name="type">확인할 타입</param>
/// <returns>등록되어 있으면 true, 아니면 false</returns>
bool IsRegistered(Type type);
#endregion
#region Injection -
/// <summary>
/// 대상 객체의 [Inject] 어트리뷰트가 붙은 필드/프로퍼티에 의존성을 주입합니다.
/// </summary>
/// <param name="target">주입 대상 객체 (null인 경우 무시)</param>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>리플렉션을 사용하여 [Inject] 어트리뷰트가 붙은 모든 필드와 프로퍼티를 찾아 의존성을 주입합니다.</para>
/// <para>상속 관계의 부모 클래스 멤버도 포함됩니다.</para>
///
/// <para><b>[ 주입 순서 ]</b></para>
/// <list type="number">
/// <item><description>필드(Field) 주입</description></item>
/// <item><description>프로퍼티(Property) 주입</description></item>
/// </list>
///
/// <para><b>[ 성능 최적화 ]</b></para>
/// <para>리플렉션 결과가 캐싱되어 동일 타입에 대한 반복 주입 시 성능이 향상됩니다.</para>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // 순수 C# 객체에 의존성 주입
/// var service = new MyService();
/// Injector.Inject(service);
///
/// // MonoBehaviour에 수동 주입
/// var player = GetComponent<PlayerController>();
/// Injector.Inject(player);
/// </code>
/// </remarks>
void Inject(object target);
/// <summary>
/// GameObject의 모든 컴포넌트에 의존성을 주입합니다.
/// </summary>
/// <param name="gameObject">주입 대상 GameObject (null인 경우 무시)</param>
/// <param name="includeChildren">자식 오브젝트의 컴포넌트도 포함할지 여부 (기본값: true)</param>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>GameObject에 붙은 모든 MonoBehaviour 컴포넌트에 대해 Inject()를 호출합니다.</para>
///
/// <para><b>[ includeChildren 옵션 ]</b></para>
/// <list type="bullet">
/// <item><description>true: GetComponentsInChildren으로 모든 자식의 컴포넌트도 포함</description></item>
/// <item><description>false: GetComponents로 해당 GameObject의 컴포넌트만 포함</description></item>
/// </list>
///
/// <para><b>[ 사용 사례 ]</b></para>
/// <list type="bullet">
/// <item><description>Instantiate로 동적 생성된 객체에 의존성 주입</description></item>
/// <item><description>씬에 배치된 객체에 수동으로 의존성 주입</description></item>
/// </list>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // Instantiate 후 의존성 주입
/// var enemy = Instantiate(enemyPrefab);
/// Injector.InjectGameObject(enemy, true);
///
/// // InjectorSceneContext를 통해
/// InjectorSceneContext.Instance.InjectInstantiated(enemy);
/// </code>
/// </remarks>
void InjectGameObject(GameObject gameObject, bool includeChildren = true);
#endregion
#region Lifecycle -
/// <summary>
/// 씬 언로드 시 Scene 라이프사이클 서비스를 정리합니다.
/// </summary>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>ClearServices(ServiceLifetime.Scene)을 호출하여 Scene 라이프사이클 서비스들을 정리합니다.</para>
/// <para>InjectorSceneContext.OnDestroy()에서 자동으로 호출됩니다.</para>
///
/// <para><b>[ 주의사항 ]</b></para>
/// <para>일반적으로 직접 호출할 필요 없이 InjectorSceneContext가 자동으로 처리합니다.</para>
/// </remarks>
void OnSceneUnloaded();
/// <summary>
/// 특정 라이프사이클의 서비스들을 정리합니다.
/// </summary>
/// <param name="lifetime">정리할 라이프사이클</param>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <para>지정된 라이프사이클의 캐시된 인스턴스들을 정리합니다.</para>
///
/// <para><b>[ 정리 방식 ]</b></para>
/// <list type="bullet">
/// <item><description>MonoBehaviour: GameObject.Destroy() 호출 (Singleton 제외)</description></item>
/// <item><description>IDisposable: Dispose() 호출</description></item>
/// <item><description>기타: 캐시에서 참조 제거</description></item>
/// </list>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // Scene 서비스 정리
/// Injector.ClearServices(ServiceLifetime.Scene);
///
/// // 모든 서비스 정리 (앱 종료 시)
/// Injector.ClearServices(ServiceLifetime.App);
/// </code>
/// </remarks>
void ClearServices(ServiceLifetime lifetime);
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 940de3ad3cfdae94da748874d9bbbee8

View File

@@ -0,0 +1,902 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
namespace UVC.Core
{
/// <summary>
/// 경량 의존성 주입(DI) 컨테이너 구현체
/// </summary>
/// <remarks>
/// <para><b>[ 개요 ]</b></para>
/// <para>Unity 환경에 최적화된 경량 DI 컨테이너입니다.</para>
/// <para>4가지 서비스 타입과 3가지 라이프사이클을 지원합니다.</para>
///
/// <para><b>[ 지원하는 4가지 타입 ]</b></para>
/// <list type="table">
/// <listheader>
/// <term>타입</term>
/// <description>설명</description>
/// </listheader>
/// <item>
/// <term>Type A (PureCSharp)</term>
/// <description>순수 C# 클래스 - new T()로 생성</description>
/// </item>
/// <item>
/// <term>Type B (MonoBehaviour)</term>
/// <description>런타임 GameObject 생성 후 AddComponent</description>
/// </item>
/// <item>
/// <term>Type C (Prefab)</term>
/// <description>등록된 Prefab을 Instantiate</description>
/// </item>
/// <item>
/// <term>Type D (Singleton)</term>
/// <description>기존 Singleton<T>.Instance 연동</description>
/// </item>
/// </list>
///
/// <para><b>[ 지원하는 3가지 라이프사이클 ]</b></para>
/// <list type="bullet">
/// <item><description><b>App:</b> 애플리케이션 전체 수명 (DontDestroyOnLoad)</description></item>
/// <item><description><b>Scene:</b> 현재 씬 수명 (씬 전환 시 파괴)</description></item>
/// <item><description><b>Transient:</b> 매번 새 인스턴스 생성</description></item>
/// </list>
///
/// <para><b>[ 주요 기능 ]</b></para>
/// <list type="bullet">
/// <item><description>필드/프로퍼티 의존성 주입 ([Inject] 어트리뷰트)</description></item>
/// <item><description>순환 의존성 감지</description></item>
/// <item><description>리플렉션 캐싱을 통한 성능 최적화</description></item>
/// <item><description>MonoBehaviour 파괴 감지 및 재생성</description></item>
/// <item><description>IDisposable 자동 정리</description></item>
/// </list>
///
/// <para><b>[ 사용 예시 - 서비스 등록 ]</b></para>
/// <code>
/// // InjectorAppContext에서 서비스 등록
/// protected override void RegisterServices()
/// {
/// base.RegisterServices();
/// Injector.Register<ILogService, ConsoleLogger>(ServiceLifetime.App);
/// Injector.Register<IAudioManager, AudioManager>(ServiceLifetime.App);
/// Injector.RegisterSingleton<SettingsManager>();
/// }
/// </code>
///
/// <para><b>[ 사용 예시 - 의존성 주입 (방법 1: [Inject] 어트리뷰트) ]</b></para>
/// <code>
/// public class PlayerController : MonoBehaviour
/// {
/// [Inject] private ILogService _logger;
/// [Inject] private IAudioManager _audio;
///
/// private void Start()
/// {
/// _logger.Log("Player initialized");
/// }
/// }
/// </code>
///
/// <para><b>[ 사용 예시 - 의존성 주입 (방법 2: 코드에서 직접 접근) ]</b></para>
/// <code>
/// public class GameManager : MonoBehaviour
/// {
/// private ILogService _logger;
/// private IAudioManager _audio;
///
/// private void Awake()
/// {
/// // InjectorAppContext를 통한 접근
/// _logger = InjectorAppContext.Instance.Get<ILogService>();
/// _audio = InjectorAppContext.Instance.Get<IAudioManager>();
///
/// // 또는 Injector 직접 접근
/// var injector = InjectorAppContext.Instance.Injector;
/// _logger = injector.Resolve<ILogService>();
///
/// // 안전한 접근 (없으면 null 반환)
/// var analytics = InjectorAppContext.Instance.TryGet<IAnalyticsService>();
/// analytics?.TrackEvent("game_start");
/// }
/// }
/// </code>
///
/// <para><b>[ 사용 예시 - 동적 생성 객체에 주입 ]</b></para>
/// <code>
/// // Instantiate 후 의존성 주입
/// var enemy = Instantiate(enemyPrefab);
/// InjectorAppContext.Instance.InjectInto(enemy);
///
/// // 또는 InjectorSceneContext 사용
/// var sceneContext = FindObjectOfType<InjectorSceneContext>();
/// sceneContext.InjectInstantiated(enemy);
/// </code>
///
/// <para><b>[ 구조 ]</b></para>
/// <code>
/// Injector
/// ├── _services: Dictionary<Type, ServiceDescriptor> // 등록된 서비스들
/// ├── _resolvingTypes: HashSet<Type> // 순환 의존성 감지용
/// ├── _fieldCache: Dictionary<Type, FieldInfo[]> // 리플렉션 캐시
/// └── _propertyCache: Dictionary<Type, PropertyInfo[]> // 리플렉션 캐시
/// </code>
///
/// <para><b>[ 등록 순서와 인스턴스 생성 시점 ]</b></para>
/// <para>Register와 Resolve는 별개의 작업입니다:</para>
/// <list type="bullet">
/// <item><description><b>Register:</b> 서비스 메타데이터만 등록 (인스턴스 생성 안 함)</description></item>
/// <item><description><b>Resolve:</b> 처음 호출될 때 인스턴스 생성 (Lazy Instantiation)</description></item>
/// </list>
/// <para>따라서 등록 순서는 중요하지 않으며, Resolve 시점에 의존성이 등록되어 있기만 하면 됩니다.</para>
///
/// <para><b>[ Unity 실행 순서와 의존성 접근 ]</b></para>
/// <code>
/// [실행 순서 타임라인]
/// ┌─────────────────────────────────────────────────────────────────────┐
/// │ InjectorAppContext.Awake (-1000) │
/// │ └→ Injector 생성 + App 서비스 메타데이터 등록 │
/// ├─────────────────────────────────────────────────────────────────────┤
/// │ InjectorSceneContext.Awake (-900) │
/// │ └→ Scene 서비스 메타데이터 등록 │
/// │ └→ 씬 오브젝트들에 의존성 주입 (InjectGameObject) │
/// │ └→ 이 시점에 [Inject] 필드에 Resolve하여 인스턴스 할당 │
/// ├─────────────────────────────────────────────────────────────────────┤
/// │ 일반 MonoBehaviour.Awake (0) │
/// │ └→ [Inject] 필드에 이미 인스턴스가 주입된 상태 │
/// │ └→ ⚠ 주의: 다른 MonoBehaviour의 Awake 실행 순서는 보장 안 됨 │
/// ├─────────────────────────────────────────────────────────────────────┤
/// │ 일반 MonoBehaviour.Start │
/// │ └→ ✅ 안전: 모든 Awake 완료 후 실행, 모든 의존성 사용 가능 │
/// └─────────────────────────────────────────────────────────────────────┘
/// </code>
///
/// <para><b>[ 의존성 접근 권장 시점 ]</b></para>
/// <list type="table">
/// <listheader>
/// <term>메서드</term>
/// <description>안전성</description>
/// </listheader>
/// <item>
/// <term>Awake()</term>
/// <description>⚠ 주의 필요 - [Inject] 필드는 사용 가능하나, 다른 컴포넌트의 Awake 실행 보장 안 됨</description>
/// </item>
/// <item>
/// <term>OnEnable()</term>
/// <description>⚠ 주의 필요 - Awake와 동일한 제약</description>
/// </item>
/// <item>
/// <term>Start()</term>
/// <description>✅ 안전 - 모든 Awake/OnEnable 완료 후 실행, 의존성 사용 권장</description>
/// </item>
/// <item>
/// <term>Update() 이후</term>
/// <description>✅ 안전 - 초기화 완료된 상태</description>
/// </item>
/// </list>
///
/// <para><b>[ Awake에서 안전하게 사용하는 방법 ]</b></para>
/// <code>
/// public class MyComponent : MonoBehaviour
/// {
/// [Inject] private ILogService _logger; // InjectorSceneContext가 주입
///
/// private void Awake()
/// {
/// // ✅ [Inject] 필드는 이미 주입된 상태 (InjectorSceneContext.Awake에서)
/// _logger?.Log("MyComponent Awake");
///
/// // ⚠ 다른 MonoBehaviour 참조는 null일 수 있음
/// // var other = FindObjectOfType<OtherComponent>();
/// // other.DoSomething(); // 위험: OtherComponent.Awake가 아직 실행 안 됐을 수 있음
/// }
///
/// private void Start()
/// {
/// // ✅ Start에서는 모든 Awake가 완료되어 안전
/// var other = FindObjectOfType<OtherComponent>();
/// other.DoSomething(); // 안전
/// }
/// }
/// </code>
///
/// <para><b>[ Awake에서 반드시 의존성이 필요한 경우 해결 방법 ]</b></para>
///
/// <para><b>방법 1: DefaultExecutionOrder로 실행 순서 지정</b></para>
/// <para>가장 권장되는 방법입니다. 컴포넌트의 실행 순서를 명시적으로 지정합니다.</para>
/// <code>
/// // B가 A보다 먼저 초기화되어야 함
/// [DefaultExecutionOrder(-100)] // InjectorSceneContext(-900) 이후, 일반(0) 이전
/// public class ServiceB : MonoBehaviour
/// {
/// [Inject] private ILogService _logger;
///
/// private void Awake()
/// {
/// _logger?.Log("ServiceB initialized first");
/// }
/// }
///
/// [DefaultExecutionOrder(-50)] // ServiceB(-100) 이후
/// public class ServiceA : MonoBehaviour
/// {
/// [Inject] private ServiceB _serviceB; // 이미 초기화된 상태
///
/// private void Awake()
/// {
/// _serviceB.DoSomething(); // ✅ 안전
/// }
/// }
/// </code>
///
/// <para><b>방법 2: InjectorAppContext.Instance.Get으로 직접 접근</b></para>
/// <para>[Inject] 대신 코드에서 직접 서비스를 가져옵니다.</para>
/// <code>
/// public class MyComponent : MonoBehaviour
/// {
/// private ILogService _logger;
///
/// private void Awake()
/// {
/// // InjectorAppContext는 -1000 순서로 이미 초기화됨
/// _logger = InjectorAppContext.Instance.Get<ILogService>();
/// _logger.Log("Direct access in Awake");
/// }
/// }
/// </code>
///
/// <para><b>방법 3: Lazy 초기화 패턴</b></para>
/// <para>첫 접근 시점에 서비스를 가져옵니다.</para>
/// <code>
/// public class MyComponent : MonoBehaviour
/// {
/// private ILogService _logger;
/// private ILogService Logger => _logger ??= InjectorAppContext.Instance.Get<ILogService>();
///
/// private void Awake()
/// {
/// Logger.Log("Lazy access"); // 첫 접근 시 초기화
/// }
/// }
/// </code>
///
/// <para><b>방법 4: 초기화 지연 (코루틴)</b></para>
/// <para>특정 조건이 충족될 때까지 대기합니다.</para>
/// <code>
/// public class MyComponent : MonoBehaviour
/// {
/// [Inject] private ILogService _logger;
/// [Inject] private IOtherService _other;
///
/// private void Awake()
/// {
/// StartCoroutine(InitializeWhenReady());
/// }
///
/// private IEnumerator InitializeWhenReady()
/// {
/// // 다른 서비스가 준비될 때까지 대기
/// while (_other == null || !_other.IsReady)
/// {
/// yield return null;
/// }
/// _logger.Log("All dependencies ready");
/// PerformInitialization();
/// }
/// }
/// </code>
///
/// <para><b>[ 권장 순서 ]</b></para>
/// <list type="number">
/// <item><description>Start() 사용 (가장 단순하고 안전)</description></item>
/// <item><description>DefaultExecutionOrder (명시적 순서 필요 시)</description></item>
/// <item><description>직접 접근 또는 Lazy 패턴 (Awake 필수인 경우)</description></item>
/// <item><description>코루틴 대기 (복잡한 초기화 의존성)</description></item>
/// </list>
///
/// <para><b>[ RegisterSingleton vs RegisterInstance 사용 가이드 ]</b></para>
/// <para>Singleton 타입(Singleton<T>, SingletonApp<T>, SingletonScene<T>)을 등록할 때는</para>
/// <para>항상 <b>RegisterSingleton<T>()</b>를 사용하세요.</para>
///
/// <para><b>[ 등록 방법 비교 ]</b></para>
/// <list type="table">
/// <listheader>
/// <term>상황</term>
/// <description>등록 방법</description>
/// </listheader>
/// <item>
/// <term>Singleton<T> (순수 C#)</term>
/// <description>RegisterSingleton<T>() - Instance 접근 시 자동 생성</description>
/// </item>
/// <item>
/// <term>SingletonApp<T> (씬에 미리 배치)</term>
/// <description>RegisterSingleton<T>() - Instance가 FindFirstObjectByType으로 찾음</description>
/// </item>
/// <item>
/// <term>SingletonApp<T> (자동 생성)</term>
/// <description>RegisterSingleton<T>() - Instance 접근 시 자동 생성</description>
/// </item>
/// <item>
/// <term>SingletonScene<T> (씬에 미리 배치)</term>
/// <description>RegisterSingleton<T>() - Instance가 FindFirstObjectByType으로 찾음</description>
/// </item>
/// <item>
/// <term>SingletonScene<T> (자동 생성)</term>
/// <description>RegisterSingleton<T>() - Instance 접근 시 자동 생성</description>
/// </item>
/// <item>
/// <term>일반 MonoBehaviour (씬에 미리 배치)</term>
/// <description>RegisterInstance<T>(instance) - 직접 인스턴스 전달 필요</description>
/// </item>
/// </list>
///
/// <para><b>[ SingletonScene을 씬에 미리 배치한 경우 ]</b></para>
/// <para>SingletonScene<T>.Instance 프로퍼티는 내부적으로 FindFirstObjectByType을 사용하여</para>
/// <para>씬에 이미 존재하는 인스턴스를 자동으로 찾습니다. 따라서 씬에 미리 배치해도</para>
/// <para>RegisterSingleton<T>()만 호출하면 됩니다.</para>
///
/// <code>
/// // SingletonScene.Instance 내부 동작
/// public static T Instance
/// {
/// get
/// {
/// if (instance == null)
/// {
/// // 씬에서 기존 인스턴스 검색
/// instance = (T)FindFirstObjectByType(typeof(T));
/// if (instance == null)
/// {
/// // 없으면 새로 생성
/// instance = new GameObject(typeof(T).Name).AddComponent<T>();
/// }
/// }
/// return instance;
/// }
/// }
/// </code>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // ✅ 올바른 사용법 - 모든 Singleton 타입
/// Injector.RegisterSingleton<MySingletonClass>(); // 순수 C# Singleton
/// Injector.RegisterSingleton<MyAppSingleton>(); // SingletonApp (씬 배치 or 자동 생성)
/// Injector.RegisterSingleton<MySceneSingleton>(); // SingletonScene (씬 배치 or 자동 생성)
///
/// // ✅ 올바른 사용법 - 일반 MonoBehaviour (씬에 미리 배치)
/// [SerializeField] private MyManager myManager;
/// Injector.RegisterInstance<IMyManager>(myManager);
///
/// // ❌ 잘못된 사용법 - Singleton 타입에 RegisterInstance 사용
/// // Injector.RegisterInstance<MySceneSingleton>(FindObjectOfType<MySceneSingleton>());
/// // → RegisterSingleton이 자동으로 처리하므로 불필요
/// </code>
///
/// <para><b>[ RegisterInstance를 사용해야 하는 경우 ]</b></para>
/// <list type="bullet">
/// <item><description>Singleton 패턴을 사용하지 않는 일반 MonoBehaviour를 씬에 미리 배치한 경우</description></item>
/// <item><description>인터페이스로 등록해야 하는 경우 (예: RegisterInstance<IService>(concreteInstance))</description></item>
/// <item><description>외부에서 생성된 인스턴스를 등록해야 하는 경우</description></item>
/// </list>
/// </remarks>
/// <seealso cref="IInjector"/>
/// <seealso cref="ServiceDescriptor"/>
/// <seealso cref="InjectAttribute"/>
/// <seealso cref="InjectorAppContext"/>
/// <seealso cref="InjectorSceneContext"/>
public class Injector : IInjector
{
#region Fields
/// <summary>등록된 서비스들의 메타데이터 저장소 (키: 인터페이스 타입)</summary>
private readonly Dictionary<Type, ServiceDescriptor> _services = new();
/// <summary>순환 의존성 감지를 위한 현재 해결 중인 타입 집합</summary>
private readonly HashSet<Type> _resolvingTypes = new();
/// <summary>Dispose 호출 여부</summary>
private bool _disposed;
/// <summary>[Inject] 필드 캐시 - 동일 타입 반복 주입 시 성능 향상</summary>
private static readonly Dictionary<Type, FieldInfo[]> _fieldCache = new();
/// <summary>[Inject] 프로퍼티 캐시 - 동일 타입 반복 주입 시 성능 향상</summary>
private static readonly Dictionary<Type, PropertyInfo[]> _propertyCache = new();
/// <summary>캐시 접근 동기화를 위한 락 객체</summary>
private static readonly object _cacheLock = new object();
#endregion
#region Registration
public IInjector Register<TInterface, TImplementation>(ServiceLifetime lifetime = ServiceLifetime.App)
where TInterface : class
where TImplementation : class, TInterface
{
ThrowIfDisposed();
var descriptor = new ServiceDescriptor(typeof(TInterface), typeof(TImplementation), lifetime);
_services[typeof(TInterface)] = descriptor;
return this;
}
public IInjector Register<T>(ServiceLifetime lifetime = ServiceLifetime.App) where T : class
{
return Register<T, T>(lifetime);
}
public IInjector RegisterInstance<TInterface>(TInterface instance, ServiceLifetime lifetime = ServiceLifetime.App)
where TInterface : class
{
ThrowIfDisposed();
if (instance == null)
throw new ArgumentNullException(nameof(instance));
var descriptor = new ServiceDescriptor(typeof(TInterface), instance, lifetime);
_services[typeof(TInterface)] = descriptor;
return this;
}
public IInjector RegisterPrefab<T>(T prefab, ServiceLifetime lifetime = ServiceLifetime.App)
where T : MonoBehaviour
{
ThrowIfDisposed();
if (prefab == null)
throw new ArgumentNullException(nameof(prefab));
var descriptor = new ServiceDescriptor(typeof(T), prefab.gameObject, lifetime);
_services[typeof(T)] = descriptor;
return this;
}
public IInjector RegisterPrefab<T>(GameObject prefab, ServiceLifetime lifetime = ServiceLifetime.App)
where T : class
{
ThrowIfDisposed();
if (prefab == null)
throw new ArgumentNullException(nameof(prefab));
var descriptor = new ServiceDescriptor(typeof(T), prefab, lifetime);
_services[typeof(T)] = descriptor;
return this;
}
public IInjector RegisterSingleton<T>() where T : class
{
ThrowIfDisposed();
var descriptor = new ServiceDescriptor(typeof(T));
_services[typeof(T)] = descriptor;
return this;
}
public IInjector RegisterFactory<TInterface>(Func<IInjector, TInterface> factory, ServiceLifetime lifetime = ServiceLifetime.App)
where TInterface : class
{
ThrowIfDisposed();
if (factory == null)
throw new ArgumentNullException(nameof(factory));
var descriptor = new ServiceDescriptor(typeof(TInterface), injector => factory(injector), lifetime);
_services[typeof(TInterface)] = descriptor;
return this;
}
#endregion
#region Resolution
public T Resolve<T>() where T : class
{
return (T)Resolve(typeof(T));
}
public object Resolve(Type type)
{
ThrowIfDisposed();
if (!_services.TryGetValue(type, out var descriptor))
{
throw new InvalidOperationException($"Service of type '{type.FullName}' is not registered.");
}
return ResolveDescriptor(descriptor);
}
public T TryResolve<T>() where T : class
{
return (T)TryResolve(typeof(T));
}
public object TryResolve(Type type)
{
ThrowIfDisposed();
if (!_services.TryGetValue(type, out var descriptor))
{
return null;
}
try
{
return ResolveDescriptor(descriptor);
}
catch
{
return null;
}
}
public bool IsRegistered<T>() where T : class
{
return IsRegistered(typeof(T));
}
public bool IsRegistered(Type type)
{
ThrowIfDisposed();
return _services.ContainsKey(type);
}
private object ResolveDescriptor(ServiceDescriptor descriptor)
{
// Transient는 항상 새 인스턴스 생성
if (descriptor.Lifetime == ServiceLifetime.Transient)
{
return CreateInstance(descriptor);
}
// 캐시된 인스턴스가 있으면 반환
if (descriptor.Instance != null)
{
// MonoBehaviour가 파괴되었는지 확인
if (descriptor.Instance is UnityEngine.Object unityObj && unityObj == null)
{
descriptor.Instance = null;
}
else
{
return descriptor.Instance;
}
}
// 새 인스턴스 생성 및 캐싱
var instance = CreateInstance(descriptor);
descriptor.Instance = instance;
return instance;
}
private object CreateInstance(ServiceDescriptor descriptor)
{
var type = descriptor.InterfaceType;
// 순환 의존성 체크
if (_resolvingTypes.Contains(type))
{
throw new InvalidOperationException(
$"Circular dependency detected while resolving '{type.FullName}'.");
}
_resolvingTypes.Add(type);
try
{
object instance = descriptor.ServiceType switch
{
ServiceType.PureCSharp => CreatePureCSharpInstance(descriptor),
ServiceType.MonoBehaviour => CreateMonoBehaviourInstance(descriptor),
ServiceType.Prefab => CreatePrefabInstance(descriptor),
ServiceType.Singleton => ResolveSingleton(descriptor),
ServiceType.Instance => descriptor.Instance,
_ => throw new InvalidOperationException($"Unknown service type: {descriptor.ServiceType}")
};
return instance;
}
finally
{
_resolvingTypes.Remove(type);
}
}
private object CreatePureCSharpInstance(ServiceDescriptor descriptor)
{
object instance;
if (descriptor.Factory != null)
{
instance = descriptor.Factory(this);
}
else
{
instance = Activator.CreateInstance(descriptor.ImplementationType);
}
Inject(instance);
return instance;
}
private object CreateMonoBehaviourInstance(ServiceDescriptor descriptor)
{
var go = new GameObject($"[Injector] {descriptor.InterfaceType.Name}");
var instance = go.AddComponent(descriptor.ImplementationType);
if (descriptor.Lifetime == ServiceLifetime.App)
{
UnityEngine.Object.DontDestroyOnLoad(go);
}
Inject(instance);
return instance;
}
private object CreatePrefabInstance(ServiceDescriptor descriptor)
{
var go = UnityEngine.Object.Instantiate(descriptor.PrefabSource);
go.name = $"[Injector] {descriptor.InterfaceType.Name}";
if (descriptor.Lifetime == ServiceLifetime.App)
{
UnityEngine.Object.DontDestroyOnLoad(go);
}
var component = go.GetComponent(descriptor.InterfaceType);
if (component == null)
{
component = go.GetComponent(descriptor.ImplementationType);
}
InjectGameObject(go, true);
return component;
}
private object ResolveSingleton(ServiceDescriptor descriptor)
{
var type = descriptor.ImplementationType;
// Instance 프로퍼티 찾기
var instanceProperty = type.GetProperty("Instance",
BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (instanceProperty != null)
{
return instanceProperty.GetValue(null);
}
// Singleton<T>.Instance 패턴 시도
var baseType = type.BaseType;
while (baseType != null)
{
if (baseType.IsGenericType)
{
var genericDef = baseType.GetGenericTypeDefinition();
if (genericDef.Name.StartsWith("Singleton"))
{
instanceProperty = baseType.GetProperty("Instance",
BindingFlags.Public | BindingFlags.Static);
if (instanceProperty != null)
{
return instanceProperty.GetValue(null);
}
}
}
baseType = baseType.BaseType;
}
throw new InvalidOperationException(
$"Cannot resolve singleton '{type.FullName}'. No 'Instance' property found.");
}
#endregion
#region Injection
public void Inject(object target)
{
if (target == null) return;
var type = target.GetType();
// 필드 주입
var fields = GetInjectableFields(type);
foreach (var field in fields)
{
var attr = field.GetCustomAttribute<InjectAttribute>();
var value = attr.Optional ? TryResolve(field.FieldType) : Resolve(field.FieldType);
if (value != null || !attr.Optional)
{
field.SetValue(target, value);
}
}
// 프로퍼티 주입
var properties = GetInjectableProperties(type);
foreach (var property in properties)
{
var attr = property.GetCustomAttribute<InjectAttribute>();
var value = attr.Optional ? TryResolve(property.PropertyType) : Resolve(property.PropertyType);
if (value != null || !attr.Optional)
{
property.SetValue(target, value);
}
}
}
public void InjectGameObject(GameObject gameObject, bool includeChildren = true)
{
if (gameObject == null) return;
if (includeChildren)
{
var components = gameObject.GetComponentsInChildren<MonoBehaviour>(true);
foreach (var component in components)
{
if (component != null)
{
Inject(component);
}
}
}
else
{
var components = gameObject.GetComponents<MonoBehaviour>();
foreach (var component in components)
{
if (component != null)
{
Inject(component);
}
}
}
}
private FieldInfo[] GetInjectableFields(Type type)
{
lock (_cacheLock)
{
if (!_fieldCache.TryGetValue(type, out var fields))
{
var fieldList = new List<FieldInfo>();
var currentType = type;
while (currentType != null && currentType != typeof(object))
{
var typeFields = currentType.GetFields(
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly)
.Where(f => f.GetCustomAttribute<InjectAttribute>() != null);
fieldList.AddRange(typeFields);
currentType = currentType.BaseType;
}
fields = fieldList.ToArray();
_fieldCache[type] = fields;
}
return fields;
}
}
private PropertyInfo[] GetInjectableProperties(Type type)
{
lock (_cacheLock)
{
if (!_propertyCache.TryGetValue(type, out var properties))
{
var propertyList = new List<PropertyInfo>();
var currentType = type;
while (currentType != null && currentType != typeof(object))
{
var typeProperties = currentType.GetProperties(
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly)
.Where(p => p.GetCustomAttribute<InjectAttribute>() != null && p.CanWrite);
propertyList.AddRange(typeProperties);
currentType = currentType.BaseType;
}
properties = propertyList.ToArray();
_propertyCache[type] = properties;
}
return properties;
}
}
#endregion
#region Lifecycle
public void OnSceneUnloaded()
{
ClearServices(ServiceLifetime.Scene);
}
public void ClearServices(ServiceLifetime lifetime)
{
ThrowIfDisposed();
var servicesToClear = _services.Values
.Where(s => s.Lifetime == lifetime && s.Instance != null)
.ToList();
foreach (var service in servicesToClear)
{
DisposeInstance(service.Instance);
service.Instance = null;
}
}
private void DisposeInstance(object instance)
{
if (instance == null) return;
// MonoBehaviour인 경우 GameObject 파괴
if (instance is MonoBehaviour mb && mb != null)
{
// Singleton 타입은 파괴하지 않음 (자체 라이프사이클 관리)
var type = instance.GetType();
if (!IsSingletonType(type))
{
UnityEngine.Object.Destroy(mb.gameObject);
}
}
// IDisposable 구현 시 Dispose 호출
else if (instance is IDisposable disposable)
{
disposable.Dispose();
}
}
private bool IsSingletonType(Type type)
{
var baseType = type.BaseType;
while (baseType != null)
{
if (baseType.IsGenericType)
{
var name = baseType.GetGenericTypeDefinition().Name;
if (name.StartsWith("Singleton"))
{
return true;
}
}
baseType = baseType.BaseType;
}
return false;
}
public void Dispose()
{
if (_disposed) return;
// Scene 서비스 먼저 정리
ClearServices(ServiceLifetime.Scene);
// App 서비스 정리
var appServices = _services.Values
.Where(s => s.Lifetime == ServiceLifetime.App && s.Instance != null)
.ToList();
foreach (var service in appServices)
{
DisposeInstance(service.Instance);
}
_services.Clear();
_disposed = true;
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(Injector));
}
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c966a2569148a3343bcf0e1fc501ef6b

View File

@@ -0,0 +1,323 @@
using System.Collections.Generic;
using UnityEngine;
namespace UVC.Core
{
/// <summary>
/// 앱 레벨 Injector 컨텍스트 - DI 시스템의 진입점
/// </summary>
/// <remarks>
/// <para><b>[ 개요 ]</b></para>
/// <para>애플리케이션 전체 수명 동안 유지되는 서비스들을 등록하고 관리하는 부트스트래퍼입니다.</para>
/// <para>SingletonApp&lt;T&gt;를 상속받아 씬 전환 시에도 유지됩니다 (DontDestroyOnLoad).</para>
///
/// <para><b>[ 실행 순서 ]</b></para>
/// <para>DefaultExecutionOrder(-1000)으로 다른 MonoBehaviour보다 먼저 초기화됩니다.</para>
/// <para>InjectorSceneContext(-900)보다 먼저 실행되어 App 서비스가 먼저 등록됩니다.</para>
///
/// <para><b>[ 역할 ]</b></para>
/// <list type="bullet">
/// <item><description>Injector 인스턴스 생성 및 관리</description></item>
/// <item><description>App 라이프사이클 서비스 등록</description></item>
/// <item><description>씬 로드 시 자동 의존성 주입 (선택적)</description></item>
/// <item><description>서비스 해결을 위한 편의 메서드 제공</description></item>
/// </list>
///
/// <para><b>[ Unity Editor 설정 ]</b></para>
/// <list type="number">
/// <item><description>첫 씬에 빈 GameObject 생성 → 이름: "AppContext"</description></item>
/// <item><description>InjectorAppContext (또는 상속받은 클래스) 컴포넌트 추가</description></item>
/// <item><description>Inspector에서 App Prefab 목록 설정 (선택)</description></item>
/// </list>
///
/// <para><b>[ 사용 방법 - 상속 ]</b></para>
/// <code>
/// public class MyAppContext : InjectorAppContext
/// {
/// [SerializeField] private UIManager uiManagerPrefab;
///
/// protected override void RegisterServices()
/// {
/// base.RegisterServices();
///
/// // Type A: 순수 C# 클래스
/// Injector.Register&lt;ILogService, ConsoleLogger&gt;(ServiceLifetime.App);
///
/// // Type B: MonoBehaviour 동적 생성
/// Injector.Register&lt;IAudioManager, AudioManager&gt;(ServiceLifetime.App);
///
/// // Type C: Prefab 기반
/// Injector.RegisterPrefab&lt;IUIManager&gt;(uiManagerPrefab.gameObject, ServiceLifetime.App);
///
/// // Type D: Singleton 연동
/// Injector.RegisterSingleton&lt;SettingsManager&gt;();
/// }
/// }
/// </code>
///
/// <para><b>[ 서비스 접근 방법 ]</b></para>
/// <code>
/// // 방법 1: [Inject] 어트리뷰트 (권장)
/// [Inject] private ILogService _logger;
///
/// // 방법 2: InjectorAppContext를 통한 접근
/// var logger = InjectorAppContext.Instance.Get&lt;ILogService&gt;();
///
/// // 방법 3: Injector 직접 접근
/// var logger = InjectorAppContext.Instance.Injector.Resolve&lt;ILogService&gt;();
/// </code>
///
/// <para><b>[ 이벤트 ]</b></para>
/// <list type="bullet">
/// <item><description>OnInjectorInitialized: Injector 생성 직후 발생</description></item>
/// <item><description>OnServicesRegistered: 서비스 등록 완료 후 발생</description></item>
/// </list>
/// </remarks>
/// <seealso cref="Injector"/>
/// <seealso cref="InjectorSceneContext"/>
/// <seealso cref="ServiceLifetime"/>
[DefaultExecutionOrder(-1000)]
public class InjectorAppContext : SingletonApp<InjectorAppContext>
{
#region Inspector Fields
[Header("App Lifetime Prefabs")]
[Tooltip("앱 전체 수명 동안 유지될 Prefab들")]
[SerializeField]
private List<MonoBehaviour> appPrefabs = new();
[Header("Settings")]
[Tooltip("씬의 모든 MonoBehaviour에 자동으로 의존성 주입 수행")]
[SerializeField]
private bool autoInjectOnSceneLoad = false;
#endregion
#region Properties
/// <summary>
/// 전역 Injector 인스턴스
/// </summary>
/// <remarks>
/// <para>모든 서비스 등록 및 해결에 사용되는 DI 컨테이너입니다.</para>
/// <para>Init() 메서드에서 생성되며, OnDestroy()에서 Dispose됩니다.</para>
/// </remarks>
public IInjector Injector { get; private set; }
/// <summary>
/// 초기화 완료 여부
/// </summary>
/// <remarks>
/// <para>Init() 완료 후 true가 됩니다.</para>
/// <para>InjectorSceneContext는 이 값이 true가 될 때까지 대기합니다.</para>
/// </remarks>
public bool IsInitialized { get; private set; }
#endregion
#region Events
/// <summary>
/// Injector 초기화 완료 시 발생
/// </summary>
/// <remarks>
/// <para>Injector 인스턴스가 생성되고 서비스 등록이 완료된 직후 발생합니다.</para>
/// </remarks>
public event System.Action OnInjectorInitialized;
/// <summary>
/// 서비스 등록 완료 시 발생
/// </summary>
/// <remarks>
/// <para>RegisterServices() 메서드 완료 후 발생합니다.</para>
/// <para>이 이벤트 이후부터 등록된 서비스들을 Resolve할 수 있습니다.</para>
/// </remarks>
public event System.Action OnServicesRegistered;
#endregion
#region Lifecycle
protected override void Init()
{
base.Init();
// Injector 인스턴스 생성
Injector = new Injector();
// 서비스 등록
RegisterServices();
// 씬 로드 이벤트 구독
if (autoInjectOnSceneLoad)
{
UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
}
IsInitialized = true;
OnInjectorInitialized?.Invoke();
}
private void OnDestroy()
{
if (autoInjectOnSceneLoad)
{
UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded;
}
Injector?.Dispose();
Injector = null;
IsInitialized = false;
}
#endregion
#region Service Registration
/// <summary>
/// 서비스 등록을 수행합니다. 자식 클래스에서 오버라이드하여 추가 서비스를 등록하세요.
/// </summary>
/// <remarks>
/// <para><b>[ 호출 시점 ]</b></para>
/// <para>Init() 메서드에서 Injector 인스턴스 생성 직후 호출됩니다.</para>
///
/// <para><b>[ 오버라이드 시 주의사항 ]</b></para>
/// <para>base.RegisterServices()를 먼저 호출하여 Inspector에서 할당한 Prefab들이 등록되도록 하세요.</para>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// protected override void RegisterServices()
/// {
/// base.RegisterServices(); // Inspector Prefab 등록
///
/// // Type A: 순수 C# 클래스
/// Injector.Register&lt;ILogService, ConsoleLogger&gt;(ServiceLifetime.App);
///
/// // Type B: MonoBehaviour 동적 생성
/// Injector.Register&lt;IAudioManager, AudioManager&gt;(ServiceLifetime.App);
///
/// // Type D: Singleton 연동
/// Injector.RegisterSingleton&lt;SettingsManager&gt;();
/// }
/// </code>
/// </remarks>
protected virtual void RegisterServices()
{
// Inspector에서 할당된 App Prefab 등록
foreach (var prefab in appPrefabs)
{
if (prefab != null)
{
RegisterPrefabByType(prefab, ServiceLifetime.App);
}
}
// 자기 자신 등록
Injector.RegisterInstance<InjectorAppContext>(this, ServiceLifetime.App);
OnServicesRegistered?.Invoke();
}
/// <summary>
/// Prefab을 해당 타입으로 등록합니다.
/// </summary>
private void RegisterPrefabByType(MonoBehaviour prefab, ServiceLifetime lifetime)
{
var type = prefab.GetType();
// 리플렉션을 사용하여 RegisterPrefab<T> 호출
var method = typeof(IInjector).GetMethod("RegisterPrefab", new[] { typeof(GameObject), typeof(ServiceLifetime) });
var genericMethod = method.MakeGenericMethod(type);
genericMethod.Invoke(Injector, new object[] { prefab.gameObject, lifetime });
}
#endregion
#region Scene Events
private void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode)
{
if (!autoInjectOnSceneLoad) return;
// 씬의 루트 오브젝트들에 의존성 주입
var rootObjects = scene.GetRootGameObjects();
foreach (var rootObj in rootObjects)
{
Injector.InjectGameObject(rootObj, true);
}
}
#endregion
#region Public Methods
/// <summary>
/// 수동으로 특정 GameObject에 의존성을 주입합니다.
/// </summary>
/// <param name="target">주입 대상 GameObject</param>
/// <param name="includeChildren">자식 오브젝트 포함 여부 (기본값: true)</param>
/// <remarks>
/// <para>Instantiate로 동적 생성된 객체에 의존성을 주입할 때 사용합니다.</para>
/// <code>
/// var enemy = Instantiate(enemyPrefab);
/// InjectorAppContext.Instance.InjectInto(enemy);
/// </code>
/// </remarks>
public void InjectInto(GameObject target, bool includeChildren = true)
{
Injector?.InjectGameObject(target, includeChildren);
}
/// <summary>
/// 수동으로 특정 객체에 의존성을 주입합니다.
/// </summary>
/// <param name="target">주입 대상 객체</param>
/// <remarks>
/// <para>순수 C# 객체나 개별 컴포넌트에 의존성을 주입할 때 사용합니다.</para>
/// <code>
/// var service = new MyService();
/// InjectorAppContext.Instance.InjectInto(service);
/// </code>
/// </remarks>
public void InjectInto(object target)
{
Injector?.Inject(target);
}
/// <summary>
/// 서비스를 해결합니다.
/// </summary>
/// <typeparam name="T">서비스 타입</typeparam>
/// <returns>서비스 인스턴스</returns>
/// <remarks>
/// <para>Injector.Resolve&lt;T&gt;()의 편의 래퍼입니다.</para>
/// <code>
/// var logger = InjectorAppContext.Instance.Get&lt;ILogService&gt;();
/// logger.Log("Hello!");
/// </code>
/// </remarks>
public T Get<T>() where T : class
{
return Injector?.Resolve<T>();
}
/// <summary>
/// 서비스 해결을 시도합니다. 실패 시 null을 반환합니다.
/// </summary>
/// <typeparam name="T">서비스 타입</typeparam>
/// <returns>서비스 인스턴스 또는 null</returns>
/// <remarks>
/// <para>선택적 서비스를 안전하게 가져올 때 사용합니다.</para>
/// <code>
/// var analytics = InjectorAppContext.Instance.TryGet&lt;IAnalyticsService&gt;();
/// analytics?.TrackEvent("game_start");
/// </code>
/// </remarks>
public T TryGet<T>() where T : class
{
return Injector?.TryResolve<T>();
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: eb9ed33d32cbc6d46a968933981839a1

View File

@@ -0,0 +1,372 @@
using System.Collections.Generic;
using UnityEngine;
namespace UVC.Core
{
/// <summary>
/// 씬 레벨 Injector 컨텍스트 - 현재 씬의 서비스 관리
/// </summary>
/// <remarks>
/// <para><b>[ 개요 ]</b></para>
/// <para>현재 씬 수명 동안만 유지되는 서비스들을 등록하고 관리합니다.</para>
/// <para>씬이 언로드될 때 Scene 라이프사이클 서비스들이 자동으로 정리됩니다.</para>
///
/// <para><b>[ 실행 순서 ]</b></para>
/// <para>DefaultExecutionOrder(-900)으로 InjectorAppContext(-1000) 이후에 초기화됩니다.</para>
/// <para>AppContext가 준비되지 않았으면 코루틴으로 대기합니다.</para>
///
/// <para><b>[ 역할 ]</b></para>
/// <list type="bullet">
/// <item><description>Scene 라이프사이클 서비스 등록</description></item>
/// <item><description>씬 오브젝트들에 자동 의존성 주입</description></item>
/// <item><description>씬 언로드 시 Scene 서비스 자동 정리</description></item>
/// <item><description>동적 생성 객체에 의존성 주입 지원</description></item>
/// </list>
///
/// <para><b>[ Unity Editor 설정 ]</b></para>
/// <list type="number">
/// <item><description>각 씬에 빈 GameObject 생성 → 이름: "SceneContext"</description></item>
/// <item><description>InjectorSceneContext (또는 상속받은 클래스) 컴포넌트 추가</description></item>
/// <item><description>Inspector에서 Scene Prefab 목록 설정 (선택)</description></item>
/// <item><description>Auto Inject 옵션 설정 (선택)</description></item>
/// </list>
///
/// <para><b>[ 사용 방법 - 상속 ]</b></para>
/// <code>
/// public class BattleSceneContext : InjectorSceneContext
/// {
/// [SerializeField] private BattleUI battleUIPrefab;
///
/// protected override void RegisterSceneServices()
/// {
/// base.RegisterSceneServices();
///
/// // Type A: Scene 라이프사이클 순수 C#
/// Injector.Register&lt;ISceneConfig, SceneConfig&gt;(ServiceLifetime.Scene);
///
/// // Type B: Scene 라이프사이클 MonoBehaviour
/// Injector.Register&lt;IEnemySpawner, EnemySpawner&gt;(ServiceLifetime.Scene);
///
/// // Type C: Scene 라이프사이클 Prefab
/// Injector.RegisterPrefab&lt;IBattleUI&gt;(battleUIPrefab.gameObject, ServiceLifetime.Scene);
///
/// // Type D: Scene 단위 Singleton
/// Injector.RegisterSingleton&lt;LevelManager&gt;();
/// }
/// }
/// </code>
///
/// <para><b>[ 자동 주입 옵션 ]</b></para>
/// <list type="bullet">
/// <item><description><b>autoInjectSceneObjects:</b> 씬 로드 시 모든 오브젝트에 자동 주입</description></item>
/// <item><description><b>targetObjects:</b> 특정 오브젝트에만 주입 (비어있으면 전체)</description></item>
/// </list>
///
/// <para><b>[ 이벤트 ]</b></para>
/// <list type="bullet">
/// <item><description>OnSceneServicesRegistered: Scene 서비스 등록 완료 후 발생</description></item>
/// <item><description>OnSceneInjectionCompleted: 의존성 주입 완료 후 발생</description></item>
/// </list>
///
/// <para><b>[ App vs Scene 라이프사이클 ]</b></para>
/// <list type="table">
/// <listheader>
/// <term>구분</term>
/// <description>특징</description>
/// </listheader>
/// <item>
/// <term>App</term>
/// <description>게임 전체 공유: 설정, 네트워크, 오디오 등</description>
/// </item>
/// <item>
/// <term>Scene</term>
/// <description>현재 씬만: 레벨 매니저, 적 스포너, 씬 UI 등</description>
/// </item>
/// </list>
/// </remarks>
/// <seealso cref="Injector"/>
/// <seealso cref="InjectorAppContext"/>
/// <seealso cref="ServiceLifetime"/>
[DefaultExecutionOrder(-900)]
public class InjectorSceneContext : MonoBehaviour
{
#region Inspector Fields
[Header("Scene Lifetime Prefabs")]
[Tooltip("현재 씬 수명 동안만 유지될 Prefab들")]
[SerializeField]
private List<MonoBehaviour> scenePrefabs = new();
[Header("Auto Injection")]
[Tooltip("씬 로드 시 자동으로 모든 GameObject에 의존성 주입")]
[SerializeField]
private bool autoInjectSceneObjects = true;
[Tooltip("특정 GameObject들에만 의존성 주입 (비어있으면 전체 씬)")]
[SerializeField]
private List<GameObject> targetObjects = new();
#endregion
#region Properties
/// <summary>
/// App Context의 Injector 참조
/// </summary>
/// <remarks>
/// <para>InjectorAppContext.Instance.Injector에 대한 편의 프로퍼티입니다.</para>
/// <para>AppContext가 null이면 null을 반환합니다.</para>
/// </remarks>
protected IInjector Injector => InjectorAppContext.Instance?.Injector;
/// <summary>
/// 초기화 완료 여부
/// </summary>
/// <remarks>
/// <para>Initialize() 완료 후 true가 됩니다.</para>
/// <para>Scene 서비스 등록 및 의존성 주입이 완료된 상태를 나타냅니다.</para>
/// </remarks>
public bool IsInitialized { get; private set; }
#endregion
#region Events
/// <summary>
/// Scene 서비스 등록 완료 시 발생
/// </summary>
/// <remarks>
/// <para>RegisterSceneServices() 메서드 완료 후 발생합니다.</para>
/// </remarks>
public event System.Action OnSceneServicesRegistered;
/// <summary>
/// Scene 의존성 주입 완료 시 발생
/// </summary>
/// <remarks>
/// <para>PerformSceneInjection() 메서드 완료 후 발생합니다.</para>
/// <para>모든 씬 오브젝트에 의존성이 주입된 상태를 나타냅니다.</para>
/// </remarks>
public event System.Action OnSceneInjectionCompleted;
#endregion
#region Lifecycle
/// <summary>
/// Unity Awake - InjectorAppContext 준비 확인 후 초기화
/// </summary>
protected virtual void Awake()
{
// InjectorAppContext가 준비될 때까지 대기
if (InjectorAppContext.Instance == null || !InjectorAppContext.Instance.IsInitialized)
{
Debug.LogWarning("[InjectorSceneContext] InjectorAppContext is not initialized. Waiting...");
StartCoroutine(WaitForAppContext());
return;
}
Initialize();
}
/// <summary>
/// InjectorAppContext가 준비될 때까지 대기하는 코루틴
/// </summary>
private System.Collections.IEnumerator WaitForAppContext()
{
while (InjectorAppContext.Instance == null || !InjectorAppContext.Instance.IsInitialized)
{
yield return null;
}
Initialize();
}
/// <summary>
/// 실제 초기화 수행 - 서비스 등록 및 의존성 주입
/// </summary>
private void Initialize()
{
if (Injector == null)
{
Debug.LogError("[InjectorSceneContext] Injector is null. Make sure InjectorAppContext exists in the scene.");
return;
}
// Scene 서비스 등록
RegisterSceneServices();
// 자동 의존성 주입
if (autoInjectSceneObjects)
{
PerformSceneInjection();
}
IsInitialized = true;
}
/// <summary>
/// Unity OnDestroy - Scene 서비스 정리
/// </summary>
/// <remarks>
/// <para>씬 언로드 시 자동으로 호출됩니다.</para>
/// <para>Injector.OnSceneUnloaded()를 호출하여 Scene 라이프사이클 서비스들을 정리합니다.</para>
/// </remarks>
protected virtual void OnDestroy()
{
// Scene 라이프사이클 서비스 정리
Injector?.OnSceneUnloaded();
IsInitialized = false;
}
#endregion
#region Service Registration
/// <summary>
/// Scene 서비스 등록을 수행합니다. 자식 클래스에서 오버라이드하여 추가 서비스를 등록하세요.
/// </summary>
/// <remarks>
/// <para><b>[ 호출 시점 ]</b></para>
/// <para>Initialize() 메서드에서 Injector 확인 후 호출됩니다.</para>
///
/// <para><b>[ 오버라이드 시 주의사항 ]</b></para>
/// <para>base.RegisterSceneServices()를 먼저 호출하여 Inspector에서 할당한 Prefab들이 등록되도록 하세요.</para>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// protected override void RegisterSceneServices()
/// {
/// base.RegisterSceneServices(); // Inspector Prefab 등록
///
/// // Type A: Scene 순수 C# 클래스
/// Injector.Register&lt;ISceneConfig, SceneConfig&gt;(ServiceLifetime.Scene);
///
/// // Type B: Scene MonoBehaviour
/// Injector.Register&lt;IEnemySpawner, EnemySpawner&gt;(ServiceLifetime.Scene);
///
/// // Type D: Scene Singleton
/// Injector.RegisterSingleton&lt;LevelManager&gt;();
/// }
/// </code>
/// </remarks>
protected virtual void RegisterSceneServices()
{
// Inspector에서 할당된 Scene Prefab 등록
foreach (var prefab in scenePrefabs)
{
if (prefab != null)
{
RegisterPrefabByType(prefab, ServiceLifetime.Scene);
}
}
// 자기 자신 등록
Injector.RegisterInstance<InjectorSceneContext>(this, ServiceLifetime.Scene);
OnSceneServicesRegistered?.Invoke();
}
/// <summary>
/// Prefab을 해당 타입으로 등록합니다. (리플렉션 사용)
/// </summary>
private void RegisterPrefabByType(MonoBehaviour prefab, ServiceLifetime lifetime)
{
var type = prefab.GetType();
// 리플렉션을 사용하여 RegisterPrefab<T> 호출
var method = typeof(IInjector).GetMethod("RegisterPrefab", new[] { typeof(GameObject), typeof(ServiceLifetime) });
var genericMethod = method.MakeGenericMethod(type);
genericMethod.Invoke(Injector, new object[] { prefab.gameObject, lifetime });
}
#endregion
#region Injection
/// <summary>
/// 씬 오브젝트들에 의존성 주입을 수행합니다.
/// </summary>
/// <remarks>
/// <para><b>[ 동작 ]</b></para>
/// <list type="bullet">
/// <item><description>targetObjects가 설정되어 있으면 해당 오브젝트들에만 주입</description></item>
/// <item><description>비어있으면 씬의 모든 루트 오브젝트와 자식들에 주입</description></item>
/// </list>
/// </remarks>
private void PerformSceneInjection()
{
if (targetObjects.Count > 0)
{
// 특정 오브젝트들에만 주입
foreach (var target in targetObjects)
{
if (target != null)
{
Injector.InjectGameObject(target, true);
}
}
}
else
{
// 전체 씬에 주입
var scene = gameObject.scene;
var rootObjects = scene.GetRootGameObjects();
foreach (var rootObj in rootObjects)
{
Injector.InjectGameObject(rootObj, true);
}
}
OnSceneInjectionCompleted?.Invoke();
}
#endregion
#region Public Methods
/// <summary>
/// 수동으로 특정 GameObject에 의존성을 주입합니다.
/// </summary>
/// <param name="target">주입 대상 GameObject</param>
/// <param name="includeChildren">자식 오브젝트 포함 여부 (기본값: true)</param>
/// <remarks>
/// <para>autoInjectSceneObjects가 false일 때 수동으로 주입할 때 사용합니다.</para>
/// </remarks>
public void InjectInto(GameObject target, bool includeChildren = true)
{
Injector?.InjectGameObject(target, includeChildren);
}
/// <summary>
/// 수동으로 특정 객체에 의존성을 주입합니다.
/// </summary>
/// <param name="target">주입 대상 객체</param>
public void InjectInto(object target)
{
Injector?.Inject(target);
}
/// <summary>
/// 동적으로 생성된 GameObject에 의존성을 주입합니다. Instantiate 후 호출하세요.
/// </summary>
/// <param name="instantiatedObject">Instantiate로 생성된 오브젝트</param>
/// <remarks>
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // 적 스폰 시
/// var enemy = Instantiate(enemyPrefab, spawnPosition, Quaternion.identity);
/// InjectorSceneContext.Instance.InjectInstantiated(enemy);
///
/// // 또는 FindObjectOfType 사용
/// var sceneContext = FindObjectOfType&lt;InjectorSceneContext&gt;();
/// sceneContext.InjectInstantiated(enemy);
/// </code>
/// </remarks>
public void InjectInstantiated(GameObject instantiatedObject)
{
Injector?.InjectGameObject(instantiatedObject, true);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4863b44e18fd5d14ca59287fa6361032

View File

@@ -0,0 +1,458 @@
using System;
using UnityEngine;
namespace UVC.Core
{
/// <summary>
/// 서비스 등록 타입을 정의하는 열거형
/// </summary>
/// <remarks>
/// <para><b>[ 개요 ]</b></para>
/// <para>Injector에 서비스를 등록할 때 사용되는 4가지 타입 + Instance 타입을 정의합니다.</para>
/// <para>각 타입에 따라 인스턴스 생성 방식과 Unity 통합 수준이 달라집니다.</para>
///
/// <para><b>[ 타입별 특징 ]</b></para>
/// <list type="table">
/// <listheader>
/// <term>타입</term>
/// <description>설명</description>
/// </listheader>
/// <item>
/// <term>Type A (PureCSharp)</term>
/// <description>new T()로 생성, GameObject 불필요</description>
/// </item>
/// <item>
/// <term>Type B (MonoBehaviour)</term>
/// <description>런타임에 새 GameObject 생성 후 AddComponent</description>
/// </item>
/// <item>
/// <term>Type C (Prefab)</term>
/// <description>등록된 Prefab을 Instantiate하여 생성</description>
/// </item>
/// <item>
/// <term>Type D (Singleton)</term>
/// <description>기존 Singleton&lt;T&gt; 클래스의 Instance 연동</description>
/// </item>
/// <item>
/// <term>Instance</term>
/// <description>이미 생성된 인스턴스를 직접 등록</description>
/// </item>
/// </list>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // Type A: 순수 C# 클래스
/// Injector.Register&lt;ILogService, ConsoleLogger&gt;(ServiceLifetime.App);
///
/// // Type B: MonoBehaviour 동적 생성
/// Injector.Register&lt;IAudioManager, AudioManager&gt;(ServiceLifetime.App);
///
/// // Type C: Prefab 기반
/// Injector.RegisterPrefab&lt;IUIManager&gt;(uiManagerPrefab, ServiceLifetime.App);
///
/// // Type D: Singleton 연동
/// Injector.RegisterSingleton&lt;SettingsManager&gt;();
///
/// // Instance: 직접 등록
/// Injector.RegisterInstance&lt;IConfig&gt;(existingConfig);
/// </code>
/// </remarks>
/// <seealso cref="ServiceDescriptor"/>
/// <seealso cref="Injector"/>
public enum ServiceType
{
/// <summary>
/// 순수 C# 클래스 (Type A)
/// </summary>
/// <remarks>
/// <para><b>[ 특징 ]</b></para>
/// <list type="bullet">
/// <item><description>MonoBehaviour를 상속하지 않는 일반 C# 클래스</description></item>
/// <item><description>Activator.CreateInstance()로 생성</description></item>
/// <item><description>GameObject 없이 동작</description></item>
/// <item><description>가장 가벼운 형태의 서비스</description></item>
/// </list>
///
/// <para><b>[ 등록 방법 ]</b></para>
/// <code>Injector.Register&lt;IService, ServiceImpl&gt;(lifetime);</code>
/// </remarks>
PureCSharp,
/// <summary>
/// MonoBehaviour 상속 클래스 - 동적 생성 (Type B)
/// </summary>
/// <remarks>
/// <para><b>[ 특징 ]</b></para>
/// <list type="bullet">
/// <item><description>런타임에 새 GameObject가 생성됨</description></item>
/// <item><description>new GameObject().AddComponent&lt;T&gt;()로 생성</description></item>
/// <item><description>GameObject 이름: "[Injector] {타입명}"</description></item>
/// <item><description>App 라이프사이클이면 DontDestroyOnLoad 적용</description></item>
/// <item><description>Unity 생명주기 메서드 (Awake, Start, Update 등) 사용 가능</description></item>
/// </list>
///
/// <para><b>[ 등록 방법 ]</b></para>
/// <code>Injector.Register&lt;IAudioManager, AudioManager&gt;(lifetime);</code>
///
/// <para><b>[ Type C (Prefab)와의 차이 ]</b></para>
/// <para>Type B는 코드로만 구성되어 Inspector 설정이 불가능합니다.</para>
/// <para>Inspector에서 AudioSource, 참조 오브젝트 등을 설정해야 한다면 Type C를 사용하세요.</para>
/// </remarks>
MonoBehaviour,
/// <summary>
/// MonoBehaviour + Prefab 기반 (Type C)
/// </summary>
/// <remarks>
/// <para><b>[ 특징 ]</b></para>
/// <list type="bullet">
/// <item><description>미리 만들어진 Prefab을 Instantiate하여 생성</description></item>
/// <item><description>Inspector에서 설정한 값들이 유지됨</description></item>
/// <item><description>자식 오브젝트, 참조 컴포넌트 등 복잡한 구조 지원</description></item>
/// <item><description>App 라이프사이클이면 DontDestroyOnLoad 적용</description></item>
/// </list>
///
/// <para><b>[ 등록 방법 ]</b></para>
/// <code>
/// // MonoBehaviour 타입으로 직접 등록
/// Injector.RegisterPrefab&lt;UIManager&gt;(uiManagerPrefab, lifetime);
///
/// // 인터페이스로 등록 (GameObject 사용)
/// Injector.RegisterPrefab&lt;IUIManager&gt;(uiManagerPrefab.gameObject, lifetime);
/// </code>
///
/// <para><b>[ 적합한 경우 ]</b></para>
/// <list type="bullet">
/// <item><description>Canvas, Button 등 UI 컴포넌트가 필요한 UIManager</description></item>
/// <item><description>AudioSource가 미리 설정된 AudioManager</description></item>
/// <item><description>자식 오브젝트가 있는 복잡한 구조의 서비스</description></item>
/// </list>
/// </remarks>
Prefab,
/// <summary>
/// 기존 Singleton 연동 (Type D)
/// </summary>
/// <remarks>
/// <para><b>[ 특징 ]</b></para>
/// <list type="bullet">
/// <item><description>Singleton&lt;T&gt;, SingletonApp&lt;T&gt;, SingletonScene&lt;T&gt; 상속 클래스 지원</description></item>
/// <item><description>기존 Instance 프로퍼티를 통해 인스턴스 획득</description></item>
/// <item><description>Injector가 인스턴스를 직접 생성하지 않음</description></item>
/// <item><description>라이프사이클은 Singleton 타입에 따라 자동 결정</description></item>
/// </list>
///
/// <para><b>[ Singleton 타입별 라이프사이클 ]</b></para>
/// <list type="bullet">
/// <item><description>Singleton&lt;T&gt;: App (순수 C#)</description></item>
/// <item><description>SingletonApp&lt;T&gt;: App (MonoBehaviour + DontDestroyOnLoad)</description></item>
/// <item><description>SingletonScene&lt;T&gt;: Scene (MonoBehaviour, 씬 전환 시 파괴)</description></item>
/// </list>
///
/// <para><b>[ 등록 방법 ]</b></para>
/// <code>Injector.RegisterSingleton&lt;SettingsManager&gt;();</code>
///
/// <para><b>[ 사용 방법 ]</b></para>
/// <code>
/// // [Inject] 어트리뷰트 사용
/// [Inject] private SettingsManager _settings;
///
/// // 기존 방식도 동일하게 동작
/// SettingsManager.Instance.Save();
/// </code>
/// </remarks>
Singleton,
/// <summary>
/// 이미 생성된 인스턴스 등록
/// </summary>
/// <remarks>
/// <para><b>[ 특징 ]</b></para>
/// <list type="bullet">
/// <item><description>외부에서 생성한 인스턴스를 직접 등록</description></item>
/// <item><description>Injector가 인스턴스 생성을 담당하지 않음</description></item>
/// <item><description>등록된 인스턴스에 대해 의존성 주입이 수행되지 않음</description></item>
/// <item><description>Context 자신을 등록할 때 주로 사용</description></item>
/// </list>
///
/// <para><b>[ 등록 방법 ]</b></para>
/// <code>
/// // 이미 생성된 인스턴스 등록
/// var config = new AppConfig { Debug = true };
/// Injector.RegisterInstance&lt;IAppConfig&gt;(config, lifetime);
///
/// // MonoBehaviour 인스턴스 등록
/// Injector.RegisterInstance&lt;InjectorAppContext&gt;(this, ServiceLifetime.App);
/// </code>
/// </remarks>
Instance
}
/// <summary>
/// 서비스 등록 정보를 담는 메타데이터 클래스
/// </summary>
/// <remarks>
/// <para><b>[ 개요 ]</b></para>
/// <para>Injector에 등록된 각 서비스에 대한 메타데이터를 저장합니다.</para>
/// <para>서비스의 타입 정보, 라이프사이클, 생성 방식, 캐시된 인스턴스 등을 관리합니다.</para>
///
/// <para><b>[ 역할 ]</b></para>
/// <list type="bullet">
/// <item><description>서비스 타입 정보 저장 (인터페이스 타입, 구현 타입)</description></item>
/// <item><description>라이프사이클 및 서비스 타입 관리</description></item>
/// <item><description>인스턴스 캐싱 (App/Scene 라이프사이클)</description></item>
/// <item><description>Prefab 소스 및 Factory 함수 저장</description></item>
/// </list>
///
/// <para><b>[ 생성자 종류 ]</b></para>
/// <list type="table">
/// <listheader>
/// <term>생성자</term>
/// <description>용도</description>
/// </listheader>
/// <item>
/// <term>(Type, Type, Lifetime)</term>
/// <description>순수 C# 또는 MonoBehaviour 타입 등록</description>
/// </item>
/// <item>
/// <term>(Type, object, Lifetime)</term>
/// <description>이미 생성된 인스턴스 등록</description>
/// </item>
/// <item>
/// <term>(Type, GameObject, Lifetime)</term>
/// <description>Prefab 기반 등록</description>
/// </item>
/// <item>
/// <term>(Type, Func, Lifetime)</term>
/// <description>Factory 함수 기반 등록</description>
/// </item>
/// <item>
/// <term>(Type)</term>
/// <description>Singleton 타입 등록</description>
/// </item>
/// </list>
///
/// <para><b>[ 내부 사용 ]</b></para>
/// <para>이 클래스는 internal로 선언되어 UVC.Core 어셈블리 내부에서만 사용됩니다.</para>
/// <para>외부에서는 Injector의 등록 메서드를 통해 간접적으로 ServiceDescriptor가 생성됩니다.</para>
/// </remarks>
/// <seealso cref="ServiceType"/>
/// <seealso cref="ServiceLifetime"/>
/// <seealso cref="Injector"/>
internal class ServiceDescriptor
{
/// <summary>
/// 서비스 인터페이스 또는 기본 타입
/// </summary>
/// <remarks>
/// <para>Injector.Resolve&lt;T&gt;()에서 T에 해당하는 타입입니다.</para>
/// <para>예: ILogService, IAudioManager 등의 인터페이스 또는 구체 타입</para>
/// </remarks>
public Type InterfaceType { get; }
/// <summary>
/// 실제 구현 타입
/// </summary>
/// <remarks>
/// <para>실제로 인스턴스화되는 클래스 타입입니다.</para>
/// <para>예: ConsoleLogger, AudioManager 등</para>
/// <para>Singleton 타입의 경우 InterfaceType과 동일합니다.</para>
/// </remarks>
public Type ImplementationType { get; }
/// <summary>
/// 서비스 라이프사이클
/// </summary>
/// <remarks>
/// <para>인스턴스의 수명 주기를 결정합니다.</para>
/// <para>App: 앱 전체, Scene: 현재 씬, Transient: 매번 새로 생성</para>
/// </remarks>
public ServiceLifetime Lifetime { get; }
/// <summary>
/// 서비스 등록 타입
/// </summary>
/// <remarks>
/// <para>인스턴스 생성 방식을 결정합니다.</para>
/// <para>PureCSharp, MonoBehaviour, Prefab, Singleton, Instance 중 하나</para>
/// </remarks>
public ServiceType ServiceType { get; }
/// <summary>
/// 캐시된 인스턴스 (Transient가 아닌 경우)
/// </summary>
/// <remarks>
/// <para>App/Scene 라이프사이클인 경우 첫 Resolve 시 생성된 인스턴스가 캐싱됩니다.</para>
/// <para>MonoBehaviour의 경우 파괴 여부를 확인하여 null이면 다시 생성합니다.</para>
/// <para>Transient 라이프사이클에서는 사용되지 않습니다.</para>
/// </remarks>
public object Instance { get; set; }
/// <summary>
/// Prefab 소스 (ServiceType.Prefab인 경우)
/// </summary>
/// <remarks>
/// <para>RegisterPrefab으로 등록된 경우 원본 Prefab GameObject가 저장됩니다.</para>
/// <para>Resolve 시 이 Prefab을 Instantiate하여 인스턴스를 생성합니다.</para>
/// </remarks>
public GameObject PrefabSource { get; }
/// <summary>
/// 팩토리 함수 (커스텀 생성 로직이 필요한 경우)
/// </summary>
/// <remarks>
/// <para>RegisterFactory로 등록된 경우 인스턴스 생성에 사용되는 함수입니다.</para>
/// <para>IInjector를 파라미터로 받아 다른 서비스에 대한 의존성 해결이 가능합니다.</para>
/// <code>
/// Injector.RegisterFactory&lt;ISceneConfig&gt;(injector => new SceneConfig
/// {
/// SceneName = SceneManager.GetActiveScene().name,
/// Logger = injector.Resolve&lt;ILogService&gt;()
/// });
/// </code>
/// </remarks>
public Func<IInjector, object> Factory { get; }
/// <summary>
/// Prefab 기반인지 확인
/// </summary>
public bool IsPrefab => ServiceType == ServiceType.Prefab;
/// <summary>
/// 기존 Singleton인지 확인
/// </summary>
public bool IsSingleton => ServiceType == ServiceType.Singleton;
/// <summary>
/// MonoBehaviour 기반인지 확인 (동적 생성 또는 Prefab)
/// </summary>
public bool IsMonoBehaviour => ServiceType == ServiceType.MonoBehaviour ||
ServiceType == ServiceType.Prefab;
/// <summary>
/// 순수 C# 클래스용 생성자
/// </summary>
public ServiceDescriptor(Type interfaceType, Type implementationType, ServiceLifetime lifetime)
{
InterfaceType = interfaceType ?? throw new ArgumentNullException(nameof(interfaceType));
ImplementationType = implementationType ?? throw new ArgumentNullException(nameof(implementationType));
Lifetime = lifetime;
ServiceType = DetermineServiceType(implementationType);
}
/// <summary>
/// 인스턴스 직접 등록용 생성자
/// </summary>
public ServiceDescriptor(Type interfaceType, object instance, ServiceLifetime lifetime)
{
InterfaceType = interfaceType ?? throw new ArgumentNullException(nameof(interfaceType));
ImplementationType = instance?.GetType() ?? throw new ArgumentNullException(nameof(instance));
Instance = instance;
Lifetime = lifetime;
ServiceType = ServiceType.Instance;
}
/// <summary>
/// Prefab 등록용 생성자
/// </summary>
public ServiceDescriptor(Type interfaceType, GameObject prefab, ServiceLifetime lifetime)
{
InterfaceType = interfaceType ?? throw new ArgumentNullException(nameof(interfaceType));
PrefabSource = prefab ?? throw new ArgumentNullException(nameof(prefab));
var component = prefab.GetComponent(interfaceType);
if (component == null)
{
throw new ArgumentException($"Prefab does not contain component of type {interfaceType.Name}");
}
ImplementationType = component.GetType();
Lifetime = lifetime;
ServiceType = ServiceType.Prefab;
}
/// <summary>
/// 팩토리 함수 등록용 생성자
/// </summary>
public ServiceDescriptor(Type interfaceType, Func<IInjector, object> factory, ServiceLifetime lifetime, ServiceType serviceType = ServiceType.PureCSharp)
{
InterfaceType = interfaceType ?? throw new ArgumentNullException(nameof(interfaceType));
Factory = factory ?? throw new ArgumentNullException(nameof(factory));
ImplementationType = interfaceType;
Lifetime = lifetime;
ServiceType = serviceType;
}
/// <summary>
/// Singleton 타입 등록용 생성자
/// </summary>
public ServiceDescriptor(Type singletonType)
{
InterfaceType = singletonType ?? throw new ArgumentNullException(nameof(singletonType));
ImplementationType = singletonType;
ServiceType = ServiceType.Singleton;
// Singleton 타입에 따라 라이프사이클 자동 결정
if (IsSingletonAppType(singletonType))
{
Lifetime = ServiceLifetime.App;
}
else if (IsSingletonSceneType(singletonType))
{
Lifetime = ServiceLifetime.Scene;
}
else
{
Lifetime = ServiceLifetime.App; // 기본값
}
}
/// <summary>
/// 구현 타입에 따라 ServiceType 결정
/// </summary>
private ServiceType DetermineServiceType(Type type)
{
if (typeof(UnityEngine.MonoBehaviour).IsAssignableFrom(type))
{
return ServiceType.MonoBehaviour;
}
return ServiceType.PureCSharp;
}
/// <summary>
/// SingletonApp 타입인지 확인
/// </summary>
private bool IsSingletonAppType(Type type)
{
var baseType = type.BaseType;
while (baseType != null)
{
if (baseType.IsGenericType &&
baseType.GetGenericTypeDefinition().Name.StartsWith("SingletonApp"))
{
return true;
}
baseType = baseType.BaseType;
}
return false;
}
/// <summary>
/// SingletonScene 타입인지 확인
/// </summary>
private bool IsSingletonSceneType(Type type)
{
var baseType = type.BaseType;
while (baseType != null)
{
if (baseType.IsGenericType &&
baseType.GetGenericTypeDefinition().Name.StartsWith("SingletonScene"))
{
return true;
}
baseType = baseType.BaseType;
}
return false;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 40693cd93d05170449183fffd29d9bf1

View File

@@ -0,0 +1,135 @@
namespace UVC.Core
{
/// <summary>
/// 서비스 인스턴스의 라이프사이클을 정의하는 열거형
/// </summary>
/// <remarks>
/// <para><b>[ 개요 ]</b></para>
/// <para>DI 컨테이너에 등록되는 서비스의 수명 주기를 결정합니다.</para>
/// <para>라이프사이클에 따라 인스턴스의 생성 시점, 캐싱 여부, 파괴 시점이 달라집니다.</para>
///
/// <para><b>[ 라이프사이클 비교 ]</b></para>
/// <list type="table">
/// <listheader>
/// <term>라이프사이클</term>
/// <description>특징</description>
/// </listheader>
/// <item>
/// <term>App</term>
/// <description>앱 전체에서 단일 인스턴스, DontDestroyOnLoad 적용</description>
/// </item>
/// <item>
/// <term>Scene</term>
/// <description>현재 씬에서만 유효, 씬 전환 시 자동 파괴</description>
/// </item>
/// <item>
/// <term>Transient</term>
/// <description>Resolve 호출 시마다 새 인스턴스 생성</description>
/// </item>
/// </list>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // App 라이프사이클 - 게임 전체에서 공유
/// Injector.Register&lt;ILogService, ConsoleLogger&gt;(ServiceLifetime.App);
/// Injector.Register&lt;IAudioManager, AudioManager&gt;(ServiceLifetime.App);
///
/// // Scene 라이프사이클 - 현재 씬에서만 유효
/// Injector.Register&lt;IEnemySpawner, EnemySpawner&gt;(ServiceLifetime.Scene);
/// Injector.Register&lt;ILevelManager, LevelManager&gt;(ServiceLifetime.Scene);
///
/// // Transient 라이프사이클 - 매번 새 인스턴스
/// Injector.Register&lt;IRequestHandler, RequestHandler&gt;(ServiceLifetime.Transient);
/// </code>
///
/// <para><b>[ MonoBehaviour와의 관계 ]</b></para>
/// <list type="bullet">
/// <item><description>App + MonoBehaviour: DontDestroyOnLoad가 자동 적용됨</description></item>
/// <item><description>Scene + MonoBehaviour: 씬 전환 시 GameObject가 함께 파괴됨</description></item>
/// <item><description>Transient + MonoBehaviour: 매번 새 GameObject가 생성됨 (권장하지 않음)</description></item>
/// </list>
///
/// <para><b>[ 주의사항 ]</b></para>
/// <list type="bullet">
/// <item><description>Scene 라이프사이클 서비스가 App 라이프사이클 서비스를 의존하는 것은 안전함</description></item>
/// <item><description>App 라이프사이클 서비스가 Scene 라이프사이클 서비스를 의존하면 씬 전환 후 null 참조 발생 가능</description></item>
/// <item><description>Transient는 상태를 공유하지 않아야 하는 경우에만 사용 (예: HTTP 요청 핸들러)</description></item>
/// </list>
/// </remarks>
/// <seealso cref="Injector"/>
/// <seealso cref="ServiceDescriptor"/>
/// <seealso cref="InjectorAppContext"/>
/// <seealso cref="InjectorSceneContext"/>
public enum ServiceLifetime
{
/// <summary>
/// 앱 전체 수명 동안 유지 (씬 전환 시에도 유지)
/// </summary>
/// <remarks>
/// <para><b>[ 특징 ]</b></para>
/// <list type="bullet">
/// <item><description>애플리케이션 시작부터 종료까지 단일 인스턴스 유지</description></item>
/// <item><description>첫 Resolve 시 인스턴스 생성 후 캐싱</description></item>
/// <item><description>MonoBehaviour인 경우 DontDestroyOnLoad 자동 적용</description></item>
/// <item><description>InjectorAppContext.RegisterServices()에서 등록 권장</description></item>
/// </list>
///
/// <para><b>[ 적합한 서비스 ]</b></para>
/// <list type="bullet">
/// <item><description>로깅 서비스 (ILogService)</description></item>
/// <item><description>오디오 매니저 (IAudioManager)</description></item>
/// <item><description>네트워크 매니저 (INetworkManager)</description></item>
/// <item><description>게임 설정 (ISettingsManager)</description></item>
/// <item><description>전역 UI 매니저 (IUIManager)</description></item>
/// </list>
/// </remarks>
App,
/// <summary>
/// 현재 씬 수명 동안만 유지 (씬 전환 시 소멸)
/// </summary>
/// <remarks>
/// <para><b>[ 특징 ]</b></para>
/// <list type="bullet">
/// <item><description>씬 로드 시 인스턴스 생성, 씬 언로드 시 자동 파괴</description></item>
/// <item><description>InjectorSceneContext.OnDestroy()에서 자동 정리</description></item>
/// <item><description>MonoBehaviour인 경우 GameObject도 함께 파괴</description></item>
/// <item><description>IDisposable 구현 시 Dispose() 자동 호출</description></item>
/// </list>
///
/// <para><b>[ 적합한 서비스 ]</b></para>
/// <list type="bullet">
/// <item><description>적 스포너 (IEnemySpawner)</description></item>
/// <item><description>레벨 매니저 (ILevelManager)</description></item>
/// <item><description>씬별 UI (ISceneUI)</description></item>
/// <item><description>씬별 설정 (ISceneConfig)</description></item>
/// </list>
/// </remarks>
Scene,
/// <summary>
/// 매번 새로운 인스턴스 생성 (싱글톤이 아님)
/// </summary>
/// <remarks>
/// <para><b>[ 특징 ]</b></para>
/// <list type="bullet">
/// <item><description>Resolve() 호출 시마다 새 인스턴스 생성</description></item>
/// <item><description>인스턴스가 캐싱되지 않음</description></item>
/// <item><description>각 인스턴스는 독립적인 상태 유지</description></item>
/// <item><description>[Inject] 필드가 있으면 매번 의존성 주입 수행</description></item>
/// </list>
///
/// <para><b>[ 적합한 서비스 ]</b></para>
/// <list type="bullet">
/// <item><description>HTTP 요청 핸들러 (IRequestHandler)</description></item>
/// <item><description>이벤트 처리기 (IEventHandler)</description></item>
/// <item><description>일회성 작업 객체</description></item>
/// </list>
///
/// <para><b>[ 주의사항 ]</b></para>
/// <para>MonoBehaviour와 함께 사용 시 매번 새 GameObject가 생성되므로</para>
/// <para>성능에 영향을 줄 수 있습니다. 순수 C# 클래스에만 사용을 권장합니다.</para>
/// </remarks>
Transient
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d83da777d7d649343bcc184d8f56d4a0