π English Abstract
This technical deep-dive addresses memory leak issues caused by unreleased AsyncOperationHandle instances during scene transitions in Unity's Addressable Asset System. It explains the mechanics of underlying AssetBundle reference counting and pinpoints edge cases where async handles remain dangling after scene unloads. Furthermore, it provides robust architectural patterns, C# code implementations, and profiling techniques to automate handle lifecycle management and optimize total memory footprint.
μ λν°(Unity) νλ‘μ νΈκ° λννλκ³ λμ 리μμ€ λ‘λ© λ°©μμ΄ λ¨μ Resources ν΄λ λ°©μμμ μ΄λλ μλΈ μμ μμ€ν (Addressable Asset System)μΌλ‘ μΈλκ΅μ²΄κ° μ΄λ€μ§λ©΄μ, λ©λͺ¨λ¦¬ κ΄λ¦¬μ μ£ΌλκΆμ μμ§ λ΄λΆμμ κ°λ°μμ μ½λλ‘ μ΄λνμ΅λλ€. κ·Έλ¬λ μ΄λλ μλΈμ λμ ν λ§μ νλ‘μ νΈκ° κ°λ° μ€λ° μ΄ν 곡ν΅μ μΌλ‘ μ§λ©΄νλ μΉλͺ μ μΈ λ¬Έμ κ° μμ΅λλ€. λ°λ‘ μ¬ μ ν(Scene Transition) μ λ°μνλ λΆμμ ν λ©λͺ¨λ¦¬ ν΄μ λ° μμ λ²λ€(AssetBundle) λμ νμμ λλ€.
κΈ°μ‘΄ `Resources.UnloadUnusedAssets()`λ μ¬ μ ν μμ μλ GC(Garbage Collection)μ μμ‘΄νλ μλμ λ©λͺ¨λ¦¬ κ΄λ¦¬ λ°©μμ μ΄λλ μλΈμ μ°Έμ‘° μΉ΄μ΄ν (Reference Counting) ꡬ쑰μ μΆ©λμ μΌμΌν΅λλ€. νΉν `AsyncOperationHandle`μ μ νν ν΄μ νμ§ μμΌλ©΄ μ¬μ΄ μΈλ‘λλμ΄λ μμ λ²λ€μ΄ λ¨(RAM) λ° VRAMμ μꡬν μμ£Όνκ² λ©λλ€. λ³Έ ν¬μ€ν μμλ μ΄λ¬ν νΈλ¬λΈμν μ¬λ‘λ₯Ό λ°νμΌλ‘ κ·Όλ³Έμ μΈ μμΈμ λΆμνκ³ κ΅¬μ‘°μ ν΄κ²°μ± μ μ μν©λλ€.
1. Addressables μ°Έμ‘° μΉ΄μ΄ν (Reference Counting) λμ λ©μ»€λμ¦
μ΄λλ μλΈ μμ€ν μ ν΅μ¬μ λ΄λΆμ μΈ μ°Έμ‘° μΉ΄μ΄νΈ(Reference Count) κ΄λ¦¬μ λλ€. μμ μ΄λ μ¬, λ²λ€μ λ‘λν λλ§λ€ ν΄λΉ 리μμ€μ μ°Έμ‘° μΉ΄μ΄νΈκ° 1μ© μ¦κ°νκ³ , λͺ μμ μΌλ‘ ν΄μ (Release)λ₯Ό νΈμΆν λ 1μ© κ°μν©λλ€. μ΄ μΉ΄μ΄νΈκ° μ νν 0μ λλ¬ν΄μΌλ§ μ€μ AssetBundle.Unload(true)κ° νΈμΆλμ΄ λ©λͺ¨λ¦¬μμ μ κ±°λ©λλ€.
AsyncOperationHandleμ μν κ³Ό μλͺ μ£ΌκΈ°(Lifecycle)
`Addressables.LoadAssetAsync
- Addressables.Release(handle): ν΄λΉ νΈλ€μ΄ κ°λ¦¬ν€λ μμ μ μ°Έμ‘° μΉ΄μ΄νΈλ₯Ό 1 κ°μμν΅λλ€. μΉ΄μ΄νΈκ° 0μ΄ λλ©΄ μμ λ° ν¬ν¨λ λ²λ€μ΄ λ©λͺ¨λ¦¬μμ μΈλ‘λλ©λλ€.
- Addressables.ReleaseInstance(gameObject): `InstantiateAsync`λ‘ μμ±λ κ²μ μ€λΈμ νΈ μΈμ€ν΄μ€λ₯Ό νκ΄΄(Destroy)ν¨κ³Ό λμμ λ΄λΆ νΈλ€μ μ°Έμ‘° μΉ΄μ΄νΈλ₯Ό κ°μμν΅λλ€.
λ¬Έμ λ `AsyncOperationHandle`μ΄ κ° νμ (Value Type Struct)μ΄λΌλ μ μ λλ€. μ¬λ¬ ν΄λμ€λ λ³μ μμμ΄λ‘ 볡μ¬λμ΄ μ λ¬λ μ μμ§λ§, μ΅μ’ μ μΌλ‘ ν΄μ ν¨μμ μ λ¬λμ΄μΌ νλ κ²μ μ΅μ΄μ λ‘λλμ΄ μΉ΄μ΄νΈλ₯Ό μ¦κ°μν¨ λμΌν λ΄λΆ μμ μν(AsyncOperationImpl)μ λλ€. λ§μ½ C# μ€ν¬λ¦½νΈκ° νκ΄΄λλ©΄μ μ΄ νΈλ€μ μμ΄λ²λ¦¬κ±°λ(Orphaned Handle) ν΄μ νμ§ μμΌλ©΄, μμ λ²λ€μ μ°Έμ‘° μΉ΄μ΄νΈλ 1λ‘ κ³ μ λμ΄ μꡬμ μΈ λ©λͺ¨λ¦¬ λμ(Memory Leak)κ° λ°μν©λλ€.
2. μ¬ μ ν μ λ°μ λΉλκ° λμ μ£μ§ μΌμ΄μ€(Edge Cases) λΆμ
λ¨μν `OnDestroy()`μμ `Release()`λ₯Ό λΆλ₯΄λ κ²λ§μΌλ‘λ λͺ¨λ λ©λͺ¨λ¦¬ λμλ₯Ό λ§μ μ μμ΅λλ€. μ€λ¬΄ κ°λ° νκ²½μμ λΉλ²νκ² λ°μνλ μ£μ§ μΌμ΄μ€λ λ€μκ³Ό κ°μ΅λλ€.
μΌμ΄μ€ A: λΉλκΈ° λ‘λ© λ―Έμλ£ μνμμ μ¬ μ ν (Interrupted Async Operation)
μμ λ‘λ©(`LoadAssetAsync`)μ μμ²ν μ§ν, λ‘λ©μ΄ μλ£λκΈ° μ μ μ μ κ° μ¬μ μ ννκ±°λ λ€λ₯Έ μ¬μΌλ‘ μ΄ννλ κ²½μ°μ λλ€. λΉλκΈ° μμ (Task λλ Coroutine)μ΄ μλ£λμ§ μμ μνμμ μ¬μ΄ μΈλ‘λλλ©΄, μλ£ μ½λ°±(`Completed`)μ΄ νΈμΆλμ§ μκ±°λ μ½λ°± λ΄λΆμμ μ°Έμ‘°νλ €λ κ°μ²΄κ° μ΄λ―Έ `Null` μνκ° λμ΄ ν΄μ λ‘μ§μ΄ μ μ€λ©λλ€.
μΌμ΄μ€ B: InstantiateAsync νΈμΆ ν Destroy()λ₯Ό μ λν° κΈ°λ³Έ ν¨μλ‘ νΈμΆ
`Addressables.InstantiateAsync()`λ‘ μμ±ν Prefab μΈμ€ν΄μ€λ₯Ό νκ΄΄ν λ `UnityEngine.Object.Destroy(gameObject)`λ₯Ό μ¬μ©νλ μ€μμ λλ€. μ λν°μ κΈ°λ³Έ `Destroy()`λ C# λ° C++ μμ§ λ 벨μ ν리 κ²μμ€λΈμ νΈλ§ μ κ±°ν λΏ, μ΄λλ μλΈμ μ°Έμ‘° μΉ΄μ΄ν°λ₯Ό κ°μμν€μ§ μμ΅λλ€. μ΄λ‘ μΈν΄ μ¬μ μ€λΈμ νΈλ μ¬λΌμ‘μ§λ§ memory μμ μμ λ²λ€μ΄ κ·Έλλ‘ λ¨μμλ νμμ΄ λ°μν©λλ€.
μΌμ΄μ€ C: μ’ μ μμ (Dependencies)μ κΌ¬μ νμ
A μ¬μμ λ‘λν UI ν μ€μ²κ° B μ¬μ 3D λͺ¨λΈ λ¨Έν°λ¦¬μΌκ³Ό λμΌν μμ λ²λ€μ λ¬Άμ¬μλ κ²½μ°μ λλ€. A μ¬μ΄ μ’ λ£λ λ νΈλ€μ ν΄μ νμ§ μμΌλ©΄, B μ¬μ΄ μ’ λ£λκ³ B μμ μ ν΄μ νλλΌλ κ³΅ν΅ λ²λ€μ μ°Έμ‘° μΉ΄μ΄νΈκ° 0μ΄ λμ§ μμ λ©λͺ¨λ¦¬κ° μ§μμ μΌλ‘ λμ λ©λλ€.
3. μ€λ¬΄ νΈλ¬λΈμν : λ©λͺ¨λ¦¬ λμ λ°©μ§ μ½λ μν€ν μ²
μ΄λ¬ν λ¬Έμ λ₯Ό ν΄κ²°νκΈ° μν΄ 1) λΉλκΈ° μ·¨μ ν ν°(CancellationToken)μ λκΈ°ν, 2) νΈλ€ μΆμ μλν λνΌ(Wrapper) ν¨ν΄, 3) MonoBehavior μλͺ μ£ΌκΈ° μ°λ(Auto-Release Agent)μ ꡬνν΄μΌ ν©λλ€.
μμ ν λΉλκΈ° λ‘λ© λ° μλ ν΄μ μ»΄ν¬λνΈ κ΅¬ν
μλ μ½λλ GameObjectμ `OnDestroy` νμλ°κ³Ό `CancellationToken`μ κ²°ν©νμ¬, λ‘λ© λμ€ μ¬μ΄ μ νλκ±°λ κ°μ²΄κ° νκ΄΄λλλΌλ νΈλ€μ΄ λ―Έν΄μ λλ μΌμ μλ²½ν λ°©μ§νλ νλ‘λμ λ 벨μ C# ν¨ν΄μ λλ€.
using System;
using System.Threading;
using Cysharp.Threading.Tasks; // UniTask νμ© μΆμ²
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class AddressableAssetLoader : MonoBehaviour
{
private AsyncOperationHandle _handle;
private bool _isLoaded = false;
/// <summary>
/// μμ νκ² μμ
μ λ‘λνκ³ κ²μ μ€λΈμ νΈμ μλͺ
μ£ΌκΈ°μ λ°μΈλ©ν©λλ€.
/// </summary>
public async UniTaskVoid LoadAssetSafe<T>(AssetReferenceT<T> assetReference, Action<T> onComplete) where T : UnityEngine.Object
{
CancellationToken cancellationToken = this.GetCancellationTokenOnDestroy();
try
{
// λΉλκΈ° μμ
μμ λ° νΈλ€ μ μ₯
_handle = assetReference.LoadAssetAsync();
// Task λκΈ° μ€ μ¬ μ ν λ±μΌλ‘ μΈν΄ GameObjectκ° Destroyλλ©΄ CancellationException λ°μ
T result = await _handle.ToUniTask(cancellationToken: cancellationToken);
if (_handle.Status == AsyncOperationStatus.Succeeded)
{
_isLoaded = true;
onComplete?.Invoke(result);
}
}
catch (OperationCanceledException)
{
// μ¬ μ ν λ±μΌλ‘ λ‘λ© λμ€ κ°μ²΄κ° νκ΄΄λ κ²½μ° μ²λ¦¬
Debug.LogWarning($"[Addressables] Loading canceled for {assetReference.RuntimeKey}. Releasing handle.");
ReleaseHandle();
}
catch (Exception ex)
{
Debug.LogError($"[Addressables] Failed to load asset: {ex.Message}");
ReleaseHandle();
}
}
private void ReleaseHandle()
{
if (_handle.IsValid())
{
Addressables.Release(_handle);
_isLoaded = false;
Debug.Log("[Addressables] Handle successfully released.");
}
}
private void OnDestroy()
{
// μ΄λ―Έ μλ£λ λ‘λμ λν νΈλ€ ν΄μ
ReleaseHandle();
}
}
μ¬ μ ν μ μ μ νΈλ€ νΈλνΉ λ§€λμ (Addressable Tracker Manager)
κ°λ³ μ€ν¬λ¦½νΈ μΈμλ μ¬ λ¨μλ‘ νΈμΆλ λͺ¨λ `AsyncOperationHandle`μ μ€μμμ λ±λ‘νκ³ , μ¬ μ ν μ΄λ²€νΈ(`SceneManager.sceneUnloaded`) λ°μ μ λ―Έν΄μ λ νΈλ€μ μ μ μ‘°μ¬νμ¬ κ°μ ν΄μ νλ μμ μ₯μΉ(Safety Net) μν€ν μ²κ° νμν©λλ€.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
public class AddressableSceneScopeManager : MonoBehaviour
{
private readonly List<AsyncOperationHandle> _trackedHandles = new List<AsyncOperationHandle>();
private void Awake()
{
SceneManager.sceneUnloaded += OnSceneUnloaded;
}
public void TrackHandle(AsyncOperationHandle handle)
{
if (handle.IsValid())
{
_trackedHandles.Add(handle);
}
}
private void OnSceneUnloaded(Scene current)
{
// μ¬ μ ν μ λμλ λͺ¨λ νΈλ€ μΌκ΄ Release
foreach (var handle in _trackedHandles)
{
if (handle.IsValid())
{
Addressables.Release(handle);
}
}
_trackedHandles.Clear();
SceneManager.sceneUnloaded -= OnSceneUnloaded;
}
}
4. Addressables Profiler & Event Viewerλ₯Ό νμ©ν λλ²κΉ λ° λΆμ
μ½λ μμ± ν μ€μ λ©λͺ¨λ¦¬μμ μμ λ²λ€μ΄ μ ννκ² ν΄μ λκ³ μλμ§ κ²μ¦νλ €λ©΄ μ λν° λ΄μ₯ ν΄μΈ Addressables Event Viewerλ₯Ό νμ©ν΄μΌ ν©λλ€.
μ€μ λ° λͺ¨λν°λ§ λ°©λ²:
- `Window` > `Asset Management` > `Addressables` > `Event Viewer` λ©λ΄λ₯Ό μ½λλ€.
- Addressables System Settingsμμ Send Profiler Events νλͺ©μ νμ±ν(Check)ν©λλ€.
- κ²μ νλ μ΄ λͺ¨λ μ§μ ν μ¬ μ νμ μνν©λλ€.
π‘ Event Viewer κ·Έλν μ½λ λ²:
- λ
Ήμ κ·Έλν(Ref Count): νΉμ μμ
λ²λ€μ μ°Έμ‘° μΉ΄μ΄νΈλ₯Ό λνλ
λλ€. μ¬μ΄ νκ΄΄λμμμλ λ
Ήμ κ·Έλνκ° 0μΌλ‘ λ¨μ΄μ§μ§ μκ³ ννμ μ μ μ§νλ€λ©΄ ν΄λΉ μ¬μ AsyncOperationHandle λμκ° μ‘΄μ¬νλ κ²μ
λλ€.
- λΆνμ/λΆμμ λΈλ‘: λ©λͺ¨λ¦¬μ μμ£Όνλ μ€μ μμ
λ°μ΄ν°μ
λλ€. μ°Έμ‘° μΉ΄μ΄νΈκ° 0μ΄ λλ μ¦μ ν΄λΉ λΈλ‘μ΄ νλ©΄μμ μ¬λΌμ ΈμΌ μ μμ
λλ€.
5. μ΅μ’ μμ½ λ° νλ‘λμ νκ²½ κΆμ₯ κ°μ΄λ
Unity Addressables κΈ°λ°μ λμ μμ κ΄λ¦¬ μμ€ν μ μμ μ μΌλ‘ μ΄μ©νκΈ° μν΄μλ λ€μ μμΉμ κ°λ° νμ€μΌλ‘ μ μν΄μΌ ν©λλ€.
- InstantiateAsync μ¬μ© μ λ¨μ§ κ·μΉ: `InstantiateAsync`λ‘ λ§λ κ°μ²΄λ μ λλ‘ μΌλ° `Destroy()`λ‘ μ§μ°μ§ λ§κ³ , λ°λμ `Addressables.ReleaseInstance()`λ₯Ό ν΅ν΄μλ§ νκ΄΄ν©λλ€.
- CancellationToken νμ μ μ©: λΉλκΈ° λ‘λ© λ©μλλ μ¬ μ ν μ λ‘λ© νλ‘μΈμ€λ₯Ό μ¦μ μ·¨μν μ μλλ‘ UniTaskμ `GetCancellationTokenOnDestroy()` λλ C#μ `CancellationToken`μ μ°κ²°ν©λλ€.
- μμ λ²λ€ λΆλ¦¬ μ΅μ ν: μ¬ λ¨μλ‘ μμ ν μλͺ μ£ΌκΈ°κ° λλμ΄μ§λ μμ λ€μ λμΌν μμ λ²λ€ κ·Έλ£Ήμ λ¬Άμ΄μ§ μλλ‘ μ΄λλ μλΈ κ·Έλ£Ή(Addressable Groups) μ€μ μ μΈλΆνν©λλ€. λ¬Άμ¬μλ μμ μ€ νλλΌλ μ°Έμ‘° μΉ΄μ΄νΈκ° 1μ΄λ©΄ μ 체 λ²λ€μ΄ λ©λͺ¨λ¦¬μ λ¨κΈ° λλ¬Έμ λλ€.
- μ κΈ°μ μΈ Memory Profiler μΊ‘μ²: μ¬ μ ν μ /νμ λ©λͺ¨λ¦¬ μ€λ μ·(Snapshot)μ μ°μ΄ Native Memory Object νλͺ©μ Unloaded λμ§ μμ `AssetBundleData`κ° μμ‘΄νλμ§ μ§μμ μΌλ‘ κ°μ¬(Audit)ν΄μΌ ν©λλ€.
μμ λ²λ€μ μ°Έμ‘° μΉ΄μ΄ν λΌμ΄νμ¬μ΄ν΄μ μ격νκ² κ΄λ¦¬νλ μν€ν μ²λ₯Ό λμ νλ©΄ μ¬ μ ν μ λ°μνλ μμΈ λΆλͺ μ ν¬λμ(OOM - Out Of Memory)μ λ©λͺ¨λ¦¬ νν νμμ μλ²½ν ν΄κ²°ν μ μμ΅λλ€.