using UnityEngine; using UnityEngine.SceneManagement; using UVC.Core; namespace Sample { /// /// Injector 샘플 테스트 클래스 - 4가지 타입별 의존성 주입 사용법을 테스트합니다. /// /// /// [ Unity Editor 설정 가이드 ] /// /// 1. 씬 구조 설정 /// /// [SampleScene] /// ├── AppContext ← InjectorSampleAppContext 컴포넌트 추가 /// ├── SceneContext ← InjectorSampleSceneContext 컴포넌트 추가 /// ├── NetworkManager ← InjectorSampleNetworkManager 컴포넌트 추가 (선택적, 자동 생성 가능) /// ├── Player ← InjectorSamplePlayerController 컴포넌트 추가 /// ├── TestRunner ← InjectorSampleTest 컴포넌트 추가 (이 클래스) /// └── EnemySpawner ← InjectorSampleEnemySpawnerExample 컴포넌트 추가 /// /// /// 2. 프리팹 생성 (Assets/Sample/Prefabs 폴더 권장) /// /// InjectorSampleUIManagerType C: Prefab 기반 MonoBehaviour (AppContext에 연결) /// InjectorSampleSceneUIScene용 UI 프리팹 (SceneContext에 연결) /// InjectorSampleEnemySpawner적 스포너 프리팹 (SceneContext에 연결) /// InjectorSampleEnemy동적 생성될 적 프리팹 (EnemySpawnerExample에 연결) /// /// /// 3. AppContext Inspector 설정 /// /// UI Manager Prefab: InjectorSampleUIManager 프리팹 연결 /// /// /// 4. SceneContext Inspector 설정 /// /// Scene UI Prefab: InjectorSampleSceneUI 프리팹 연결 (선택) /// Enemy Spawner Prefab: InjectorSampleEnemySpawner 프리팹 연결 (선택) /// /// /// 5. EnemySpawnerExample Inspector 설정 /// /// Enemy Prefab: InjectorSampleEnemy 프리팹 연결 /// Spawn Point: 스폰 위치 Transform 연결 (선택) /// /// /// 6. Singleton 타입별 설정 방법 /// /// /// InjectorSampleSettingsManager /// Singleton (순수 C#) - 씬 설정 불필요, 코드에서 Instance 접근 시 자동 생성 /// /// /// InjectorSampleNetworkManager /// SingletonApp (MonoBehaviour) - 씬에 배치 가능 또는 Instance 접근 시 자동 생성 /// /// /// /// 7. 실행 순서 (자동 설정됨) /// /// InjectorAppContext (-1000) - App 서비스 등록 /// InjectorSceneContext (-900) - Scene 서비스 등록 /// 일반 MonoBehaviour (0) - 의존성 주입된 상태로 Start 실행 /// /// /// [ 등록 순서와 인스턴스 생성 시점 ] /// Register와 Resolve는 별개의 작업입니다: /// /// Register: 서비스 메타데이터만 등록 (인스턴스 생성 안 함) /// Resolve: 처음 호출될 때 인스턴스 생성 (Lazy Instantiation) /// /// 따라서 등록 순서는 중요하지 않으며, Resolve 시점에 의존성이 등록되어 있기만 하면 됩니다. /// /// [ Unity 실행 순서 타임라인 ] /// /// ┌─────────────────────────────────────────────────────────────────────┐ /// │ InjectorAppContext.Awake (-1000) │ /// │ └→ Injector 생성 + App 서비스 메타데이터 등록 │ /// ├─────────────────────────────────────────────────────────────────────┤ /// │ InjectorSceneContext.Awake (-900) │ /// │ └→ Scene 서비스 메타데이터 등록 │ /// │ └→ 씬 오브젝트들에 의존성 주입 (InjectGameObject) │ /// │ └→ 이 시점에 [Inject] 필드에 Resolve하여 인스턴스 할당 │ /// ├─────────────────────────────────────────────────────────────────────┤ /// │ InjectorSampleTest.Awake (0) │ /// │ └→ [Inject] 필드(_logger, _audioManager 등)에 이미 인스턴스 주입됨 │ /// │ └→ ⚠ 다른 MonoBehaviour의 Awake 실행 순서는 보장 안 됨 │ /// ├─────────────────────────────────────────────────────────────────────┤ /// │ InjectorSampleTest.Start │ /// │ └→ ✅ 안전: 모든 Awake 완료 후 실행, 모든 의존성 사용 가능 │ /// │ └→ RunAllTests() 호출 │ /// └─────────────────────────────────────────────────────────────────────┘ /// /// /// [ 의존성 접근 권장 시점 ] /// /// /// 메서드 /// 안전성 /// /// /// Awake() /// ⚠ 주의 - [Inject] 필드 사용 가능하나, 다른 컴포넌트 Awake 순서 보장 안 됨 /// /// /// Start() /// ✅ 안전 - 모든 Awake 완료 후 실행, 의존성 사용 권장 (이 클래스에서 사용) /// /// /// /// [ Awake에서 반드시 의존성이 필요한 경우 해결 방법 ] /// /// /// 방법 /// 설명 및 예시 /// /// /// 1. DefaultExecutionOrder /// 컴포넌트 실행 순서 명시적 지정 (가장 권장) /// /// /// 2. 직접 접근 /// InjectorAppContext.Instance.Get() 사용 /// /// /// 3. Lazy 패턴 /// 프로퍼티로 첫 접근 시 초기화 /// /// /// 4. 코루틴 대기 /// 조건 충족까지 대기 후 초기화 /// /// /// /// 방법 1 예시: DefaultExecutionOrder /// /// [DefaultExecutionOrder(-100)] // InjectorSceneContext(-900) 이후, 일반(0) 이전 /// public class EarlyInitService : MonoBehaviour /// { /// [Inject] private ILogService _logger; /// private void Awake() { _logger?.Log("Early init"); } /// } /// /// [DefaultExecutionOrder(-50)] // EarlyInitService 이후 /// public class LateInitService : MonoBehaviour /// { /// [Inject] private EarlyInitService _early; // 이미 초기화됨 /// private void Awake() { _early.DoSomething(); } // ✅ 안전 /// } /// /// /// 방법 2 예시: 직접 접근 /// /// public class MyComponent : MonoBehaviour /// { /// private ILogService _logger; /// private void Awake() /// { /// _logger = InjectorAppContext.Instance.Get(); /// _logger.Log("Awake에서 직접 접근"); /// } /// } /// /// /// 방법 3 예시: Lazy 패턴 /// /// public class MyComponent : MonoBehaviour /// { /// private ILogService _logger; /// private ILogService Logger => _logger ??= InjectorAppContext.Instance.Get(); /// private void Awake() { Logger.Log("Lazy 접근"); } /// } /// /// /// [ 권장 순서 ] /// /// Start() 사용 (가장 단순하고 안전) /// DefaultExecutionOrder (명시적 순서 필요 시) /// 직접 접근 또는 Lazy 패턴 (Awake 필수인 경우) /// 코루틴 대기 (복잡한 초기화 의존성) /// /// /// [ RegisterSingleton vs RegisterInstance 사용 가이드 ] /// Singleton 타입을 등록할 때는 항상 RegisterSingleton()를 사용하세요. /// /// /// /// 상황 /// 등록 방법 /// /// /// Singleton (순수 C#) /// RegisterSingleton() /// /// /// SingletonApp (씬 배치 or 자동 생성) /// RegisterSingleton() /// /// /// SingletonScene (씬 배치 or 자동 생성) /// RegisterSingleton() /// /// /// 일반 MonoBehaviour (씬에 미리 배치) /// RegisterInstance(instance) /// /// /// /// [ 왜 SingletonScene도 RegisterSingleton을 사용하나요? ] /// SingletonScene.Instance 프로퍼티가 내부적으로 FindFirstObjectByType을 사용하여 /// 씬에 이미 배치된 인스턴스를 자동으로 찾기 때문입니다. /// /// /// // 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(); // 없으면 생성 /// } /// return instance; /// } /// } /// /// // 등록 예시 /// // ✅ 올바름 - Singleton 타입 /// Injector.RegisterSingleton(); // Singleton /// Injector.RegisterSingleton(); // SingletonApp /// Injector.RegisterSingleton(); // SingletonScene /// /// // ✅ 올바름 - 일반 MonoBehaviour /// [SerializeField] private MyManager myManager; /// Injector.RegisterInstance(myManager); /// /// // ❌ 불필요 - Singleton에 RegisterInstance /// // Injector.RegisterInstance(FindObjectOfType()); /// /// /// 8. 테스트 실행 확인 /// Play 모드 실행 시 Console에 다음 로그가 출력되면 성공: /// /// [InjectorSampleAppContext] All App services registered /// ========== Injector Sample Tests ========== /// ----- Type A: Pure C# Class ----- /// [ConsoleLogger] Type A Test: Logger is working! /// ... /// ========== All Tests Completed ========== /// /// public class InjectorSampleTest : MonoBehaviour { [Header("Test Settings")] [SerializeField] private bool runTestOnStart = true; [Inject] private ILogService _logger; [Inject] private IAudioManager _audioManager; [Inject] private IGameService _gameService; [Inject(Optional = true)] private IUIManager _uiManager; [Inject] private InjectorSampleSettingsManager _settings; private void Start() { if (runTestOnStart) RunAllTests(); } [ContextMenu("Run All Tests")] public void RunAllTests() { Debug.Log("========== Injector Sample Tests =========="); TestTypeA_PureCSharp(); TestTypeB_MonoBehaviour(); TestTypeC_Prefab(); TestTypeD_Singleton(); TestLifecycles(); Debug.Log("========== All Tests Completed =========="); } private void TestTypeA_PureCSharp() { Debug.Log("----- Type A: Pure C# Class -----"); _logger?.Log("Type A Test: Logger is working!"); _gameService?.Initialize(); _gameService?.SaveGame(); } private void TestTypeB_MonoBehaviour() { Debug.Log("----- Type B: MonoBehaviour (Dynamic) -----"); _audioManager?.PlayBGM("main_theme"); _audioManager?.PlaySFX("click"); } private void TestTypeC_Prefab() { Debug.Log("----- Type C: Prefab-based MonoBehaviour -----"); _uiManager?.ShowLoading(); _uiManager?.HideLoading(); _uiManager?.ShowPopup("Test", "This is a test popup"); } private void TestTypeD_Singleton() { Debug.Log("----- Type D: Existing Singleton -----"); // 1. Inject를 통한 접근 테스트 _logger?.Log($"[Inject] Master Volume: {_settings?.MasterVolume}"); _logger?.Log($"[Inject] SFX Volume: {_settings?.SFXVolume}"); _logger?.Log($"[Inject] BGM Volume: {_settings?.BGMVolume}"); _logger?.Log($"[Inject] Language: {_settings?.Language}"); // 2. 직접 Instance 접근 테스트 (순수 C# Singleton) var directSettings = InjectorSampleSettingsManager.Instance; _logger?.Log($"[Direct] Master Volume: {directSettings.MasterVolume}"); // 3. 동일 인스턴스 확인 bool isSameInstance = ReferenceEquals(_settings, directSettings); _logger?.Log($"Same instance check: {isSameInstance}"); // 4. 값 변경 및 저장 테스트 if (_settings != null) { float originalVolume = _settings.MasterVolume; _settings.MasterVolume = 0.5f; _settings.SFXVolume = 0.7f; _settings.Language = "en"; _settings.Save(); _logger?.Log($"Changed Master Volume: {originalVolume} -> {_settings.MasterVolume}"); _logger?.Log($"Changed Language: ko -> {_settings.Language}"); // 직접 접근으로도 변경된 값 확인 _logger?.Log($"[Direct] Verify changed value: {directSettings.MasterVolume}"); // 값 복원 _settings.MasterVolume = originalVolume; _settings.Language = "ko"; _settings.Load(); } } private void TestLifecycles() { Debug.Log("----- Lifecycle Tests -----"); _logger?.Log("App services persist across scenes"); var levelManager = InjectorAppContext.Instance?.TryGet(); if (levelManager != null) { _logger?.Log($"Current Level: {levelManager.CurrentLevel}"); levelManager.AddScore(100); } else { _logger?.Log("LevelManager not available (Scene service)"); } } [ContextMenu("Test Dynamic Instantiation")] public void TestDynamicInstantiation() { Debug.Log("----- Dynamic Instantiation Test -----"); var go = new GameObject("DynamicObject"); var component = go.AddComponent(); InjectorAppContext.Instance?.InjectInto(go); component.Test(); Destroy(go, 2f); } #region Scene Transition & Memory Test /// /// 씬 전환 테스트용 메서드 /// /// 이동할 씬 이름 /// /// [ 사용법 ] /// /// // 코드에서 호출 /// GetComponent().MoveScene("NextScene"); /// /// // 또는 Inspector에서 Button OnClick에 연결 /// /// public void MoveScene(string sceneName) { Debug.Log($"[SceneTransition] Moving to scene: {sceneName}"); LogMemoryStatus("Before Scene Load"); SceneManager.LoadScene(sceneName); } public void TestMoveScene() { string sceneName = "InjectorSample2"; Debug.Log($"========== Scene Move Test to {sceneName} =========="); LogMemoryStatus("Before Scene Load"); LogServiceStatus("Before Scene Load"); SceneManager.sceneLoaded += OnSceneLoadedForTest; SceneManager.LoadScene(sceneName); } /// /// 씬 전환 테스트 - 현재 씬을 다시 로드하여 Scene 라이프사이클 서비스 정리 확인 /// [ContextMenu("Test Scene Reload (Memory Check)")] public void TestSceneReload() { Debug.Log("========== Scene Reload Test =========="); LogMemoryStatus("Before Reload"); LogServiceStatus("Before Reload"); string currentScene = SceneManager.GetActiveScene().name; Debug.Log($"[SceneTransition] Reloading current scene: {currentScene}"); // 씬 로드 완료 이벤트 등록 SceneManager.sceneLoaded += OnSceneLoadedForTest; SceneManager.LoadScene(currentScene); } /// /// 씬 전환 테스트 - 다른 씬으로 이동 후 다시 돌아오기 /// /// 이동할 대상 씬 /// 돌아올 때까지 대기 시간 (초) [ContextMenu("Test Scene Round Trip")] public void TestSceneRoundTrip() { TestSceneRoundTrip("InjectorTestScene2", 2f); } public void TestSceneRoundTrip(string targetScene, float returnAfterSeconds) { Debug.Log("========== Scene Round Trip Test =========="); LogMemoryStatus("Before Round Trip"); LogServiceStatus("Before Round Trip"); string originalScene = SceneManager.GetActiveScene().name; Debug.Log($"[SceneTransition] {originalScene} -> {targetScene} -> {originalScene}"); // 원래 씬 이름 저장 (static으로 씬 전환 후에도 유지) _returnSceneName = originalScene; _returnDelay = returnAfterSeconds; SceneManager.sceneLoaded += OnSceneLoadedForRoundTrip; SceneManager.LoadScene(targetScene); } private static string _returnSceneName; private static float _returnDelay; private static bool _isReturning; private static bool _pendingSceneReloadTest; private static bool _pendingRoundTripTest; private static readonly WaitForSeconds _waitOneSecond = new(1f); /// /// 씬 로드 완료 후 새로운 씬의 InjectorSampleTest 인스턴스에서 처리 /// private void OnEnable() { // 씬 로드 후 대기 중인 테스트가 있으면 실행 if (_pendingSceneReloadTest) { _pendingSceneReloadTest = false; OnSceneReloadCompleted(); } else if (_pendingRoundTripTest) { OnRoundTripSceneLoaded(); } } private void OnDisable() { // 이벤트 핸들러 정리 (씬 전환 시 호출됨) SceneManager.sceneLoaded -= OnSceneLoadedForTest; SceneManager.sceneLoaded -= OnSceneLoadedForRoundTrip; } private static void OnSceneLoadedForTest(Scene scene, LoadSceneMode mode) { SceneManager.sceneLoaded -= OnSceneLoadedForTest; Debug.Log($"[SceneTransition] Scene loaded: {scene.name}"); // 새로운 씬의 InjectorSampleTest에서 처리하도록 플래그 설정 _pendingSceneReloadTest = true; } private void OnSceneReloadCompleted() { LogMemoryStatusStatic("After Reload"); LogServiceStatusStatic("After Reload"); // GC 강제 실행 후 메모리 재확인 StartCoroutine(ForceGCAndLogMemory()); } private static void OnSceneLoadedForRoundTrip(Scene scene, LoadSceneMode mode) { SceneManager.sceneLoaded -= OnSceneLoadedForRoundTrip; Debug.Log($"[SceneTransition] Arrived at: {scene.name}"); LogMemoryStatusStatic($"At {scene.name}"); LogServiceStatusStatic($"At {scene.name}"); // 새로운 씬의 InjectorSampleTest에서 처리하도록 플래그 설정 _pendingRoundTripTest = true; } private void OnRoundTripSceneLoaded() { if (!_isReturning && SceneManager.GetActiveScene().name != _returnSceneName) { _isReturning = true; _pendingRoundTripTest = false; // 잠시 대기 후 원래 씬으로 복귀 StartCoroutine(ReturnToOriginalScene()); } else if (_isReturning) { _isReturning = false; _pendingRoundTripTest = false; Debug.Log("========== Round Trip Completed =========="); StartCoroutine(ForceGCAndLogMemory()); } } private System.Collections.IEnumerator ReturnToOriginalScene() { Debug.Log($"[SceneTransition] Waiting {_returnDelay}s before returning..."); yield return new WaitForSeconds(_returnDelay); Debug.Log($"[SceneTransition] Returning to: {_returnSceneName}"); LogMemoryStatusStatic("Before Return"); SceneManager.sceneLoaded += OnSceneLoadedForRoundTrip; SceneManager.LoadScene(_returnSceneName); } private System.Collections.IEnumerator ForceGCAndLogMemory() { Debug.Log("[Memory] Forcing GC collection..."); // GC 강제 실행 System.GC.Collect(); System.GC.WaitForPendingFinalizers(); System.GC.Collect(); // 1프레임 대기 yield return null; // Resources.UnloadUnusedAssets는 코루틴으로 실행 var unloadOperation = Resources.UnloadUnusedAssets(); yield return unloadOperation; LogMemoryStatus("After GC"); Debug.Log("========== Memory Check Completed =========="); } /// /// 현재 메모리 상태를 로그로 출력 /// /// 로그 라벨 [ContextMenu("Log Memory Status")] public void LogMemoryStatus() { LogMemoryStatus("Current"); } public void LogMemoryStatus(string label) { // Unity 메모리 정보 long totalMemory = UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong(); long usedMemory = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong(); long monoHeap = UnityEngine.Profiling.Profiler.GetMonoHeapSizeLong(); long monoUsed = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong(); Debug.Log($"[Memory] === {label} ==="); Debug.Log($"[Memory] Total Reserved: {FormatBytes(totalMemory)}"); Debug.Log($"[Memory] Total Allocated: {FormatBytes(usedMemory)}"); Debug.Log($"[Memory] Mono Heap: {FormatBytes(monoHeap)}"); Debug.Log($"[Memory] Mono Used: {FormatBytes(monoUsed)}"); // GC 정보 Debug.Log($"[Memory] GC Total Memory: {FormatBytes(System.GC.GetTotalMemory(false))}"); } /// /// 현재 등록된 서비스 상태를 로그로 출력 /// /// 로그 라벨 [ContextMenu("Log Service Status")] public void LogServiceStatus() { LogServiceStatus("Current"); } public void LogServiceStatus(string label) { Debug.Log($"[Services] === {label} ==="); var appContext = InjectorAppContext.Instance; if (appContext == null) { Debug.Log("[Services] InjectorAppContext not found!"); return; } Debug.Log($"[Services] AppContext Initialized: {appContext.IsInitialized}"); // App 라이프사이클 서비스 확인 var logger = appContext.TryGet(); var audio = appContext.TryGet(); var game = appContext.TryGet(); var ui = appContext.TryGet(); var settings = appContext.TryGet(); Debug.Log($"[Services] App Services:"); Debug.Log($" - ILogService: {(logger != null ? "OK" : "NULL")}"); Debug.Log($" - IAudioManager: {(audio != null ? "OK" : "NULL")} {GetMonoBehaviourStatus(audio)}"); Debug.Log($" - IGameService: {(game != null ? "OK" : "NULL")}"); Debug.Log($" - IUIManager: {(ui != null ? "OK" : "NULL")} {GetMonoBehaviourStatus(ui)}"); Debug.Log($" - SettingsManager: {(settings != null ? "OK" : "NULL")}"); // Scene 라이프사이클 서비스 확인 var sceneContext = FindAnyObjectByType(); Debug.Log($"[Services] SceneContext: {(sceneContext != null ? $"Found ({sceneContext.IsInitialized})" : "NULL")}"); var levelManager = appContext.TryGet(); var enemySpawner = appContext.TryGet(); Debug.Log($"[Services] Scene Services:"); Debug.Log($" - LevelManager: {(levelManager != null ? "OK" : "NULL (expected after scene change)")}"); Debug.Log($" - EnemySpawner: {(enemySpawner != null ? "OK" : "NULL (expected after scene change)")} {GetMonoBehaviourStatus(enemySpawner)}"); } private string GetMonoBehaviourStatus(object obj) { if (obj is MonoBehaviour mb) { if (mb == null) return "(Destroyed)"; return $"(GameObject: {mb.gameObject.name})"; } return ""; } private static string FormatBytes(long bytes) { string[] sizes = { "B", "KB", "MB", "GB" }; int order = 0; double size = bytes; while (size >= 1024 && order < sizes.Length - 1) { order++; size /= 1024; } return $"{size:0.##} {sizes[order]}"; } /// /// Static 버전의 메모리 상태 로그 - 씬 전환 중에도 호출 가능 /// private static void LogMemoryStatusStatic(string label) { long totalMemory = UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong(); long usedMemory = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong(); long monoHeap = UnityEngine.Profiling.Profiler.GetMonoHeapSizeLong(); long monoUsed = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong(); Debug.Log($"[Memory] === {label} ==="); Debug.Log($"[Memory] Total Reserved: {FormatBytes(totalMemory)}"); Debug.Log($"[Memory] Total Allocated: {FormatBytes(usedMemory)}"); Debug.Log($"[Memory] Mono Heap: {FormatBytes(monoHeap)}"); Debug.Log($"[Memory] Mono Used: {FormatBytes(monoUsed)}"); Debug.Log($"[Memory] GC Total Memory: {FormatBytes(System.GC.GetTotalMemory(false))}"); } /// /// Static 버전의 서비스 상태 로그 - 씬 전환 중에도 호출 가능 /// private static void LogServiceStatusStatic(string label) { Debug.Log($"[Services] === {label} ==="); var appContext = InjectorAppContext.Instance; if (appContext == null) { Debug.Log("[Services] InjectorAppContext not found!"); return; } Debug.Log($"[Services] AppContext Initialized: {appContext.IsInitialized}"); // App 라이프사이클 서비스 확인 var logger = appContext.TryGet(); var audio = appContext.TryGet(); var game = appContext.TryGet(); var ui = appContext.TryGet(); var settings = appContext.TryGet(); Debug.Log($"[Services] App Services:"); Debug.Log($" - ILogService: {(logger != null ? "OK" : "NULL")}"); Debug.Log($" - IAudioManager: {(audio != null ? "OK" : "NULL")} {GetMonoBehaviourStatusStatic(audio)}"); Debug.Log($" - IGameService: {(game != null ? "OK" : "NULL")}"); Debug.Log($" - IUIManager: {(ui != null ? "OK" : "NULL")} {GetMonoBehaviourStatusStatic(ui)}"); Debug.Log($" - SettingsManager: {(settings != null ? "OK" : "NULL")}"); // Scene 라이프사이클 서비스 확인 var sceneContext = Object.FindAnyObjectByType(); Debug.Log($"[Services] SceneContext: {(sceneContext != null ? $"Found ({sceneContext.IsInitialized})" : "NULL")}"); var levelManager = appContext.TryGet(); var enemySpawner = appContext.TryGet(); Debug.Log($"[Services] Scene Services:"); Debug.Log($" - LevelManager: {(levelManager != null ? "OK" : "NULL (expected after scene change)")}"); Debug.Log($" - EnemySpawner: {(enemySpawner != null ? "OK" : "NULL (expected after scene change)")} {GetMonoBehaviourStatusStatic(enemySpawner)}"); } private static string GetMonoBehaviourStatusStatic(object obj) { if (obj is MonoBehaviour mb) { if (mb == null) return "(Destroyed)"; return $"(GameObject: {mb.gameObject.name})"; } return ""; } /// /// 메모리 누수 테스트 - 여러 번 씬 전환 후 메모리 증가 확인 /// /// 반복 횟수 [ContextMenu("Test Memory Leak (5 iterations)")] public void TestMemoryLeak() { TestMemoryLeak(5); } public void TestMemoryLeak(int iterations) { StartCoroutine(MemoryLeakTestCoroutine(iterations)); } private System.Collections.IEnumerator MemoryLeakTestCoroutine(int iterations) { Debug.Log($"========== Memory Leak Test ({iterations} iterations) =========="); string currentScene = SceneManager.GetActiveScene().name; long initialMemory = System.GC.GetTotalMemory(true); Debug.Log($"[MemoryLeak] Initial GC Memory: {FormatBytes(initialMemory)}"); for (int i = 0; i < iterations; i++) { Debug.Log($"[MemoryLeak] Iteration {i + 1}/{iterations}"); // 씬 리로드 var loadOp = SceneManager.LoadSceneAsync(currentScene); yield return loadOp; // 1초 대기 yield return _waitOneSecond; // GC 실행 System.GC.Collect(); System.GC.WaitForPendingFinalizers(); System.GC.Collect(); yield return Resources.UnloadUnusedAssets(); long currentMemory = System.GC.GetTotalMemory(false); long diff = currentMemory - initialMemory; Debug.Log($"[MemoryLeak] Iteration {i + 1} - GC Memory: {FormatBytes(currentMemory)} (diff: {FormatBytes(diff)})"); } long finalMemory = System.GC.GetTotalMemory(true); long totalDiff = finalMemory - initialMemory; Debug.Log($"[MemoryLeak] Final GC Memory: {FormatBytes(finalMemory)}"); Debug.Log($"[MemoryLeak] Total Difference: {FormatBytes(totalDiff)}"); if (totalDiff > 1024 * 1024) // 1MB 이상 증가 { Debug.LogWarning($"[MemoryLeak] Potential memory leak detected! Memory increased by {FormatBytes(totalDiff)}"); } else { Debug.Log("[MemoryLeak] No significant memory leak detected."); } Debug.Log("========== Memory Leak Test Completed =========="); } #endregion } /// /// 동적 생성 테스트용 컴포넌트 /// public class InjectorSampleDynamicTestComponent : MonoBehaviour { [Inject] private ILogService _logger; [Inject] private IAudioManager _audioManager; public void Test() { _logger?.Log("DynamicTestComponent: Injection successful!"); _audioManager?.PlaySFX("test_sound"); } } }