Unity Addressables와 AssetBundle 메모리 릴리즈: Release·Unload 시점 관리 가이드

Unity Addressables와 AssetBundle에서 메모리가 남거나 에셋이 너무 일찍 사라지는 원인을 설명하고 참조 카운트·Release·UnloadUnusedAssets를 기준으로 안전한 동적 언로드 전략과 C# 구현 예제를 정리한다.

✓ 복사 완료!
챕터일반 가이드
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
Unity Addressables와 AssetBundle 메모리 릴리즈: Release·Unload 시점 관리 가이드

핵심 요약

Addressables의 Release는 로드 핸들의 참조 카운트를 줄이는 작업이며 즉시 모든 네이티브 메모리를 비운다는 뜻은 아니다. 로드한 오브젝트를 더 이상 사용하지 않을 때 Addressables.Release 또는 Addressables.ReleaseInstance를 호출하고 씬 전환·로딩 화면처럼 안전한 구간에서 필요할 때만 Resources.UnloadUnusedAssets()를 실행하는 방식이 안정적이다.

AssetBundle도 Unload(true)를 너무 이르게 호출하면 번들에서 생성한 에셋이 파괴될 수 있다. 따라서 소유권, 활성 사용 여부, 해제 시점을 분리해 관리해야 한다.

Addressables 메모리가 예상대로 줄지 않는 이유는 무엇인가?

Addressables는 AsyncOperationHandle의 참조 카운트를 기반으로 로드 결과와 의존 번들을 관리한다. 같은 키를 여러 번 로드했다면 각 로드에 대응하는 해제가 필요하다. 단 하나의 Release만 호출하면 남은 참조가 번들과 의존 에셋을 계속 붙잡는다.

상황권장 해제 API주의점
LoadAssetAsync<T>로 프리팹·텍스처를 로드Addressables.Release(handle)생성한 오브젝트는 자동 파괴되지 않는다.
InstantiateAsync로 인스턴스 생성Addressables.ReleaseInstance(instance) 또는 핸들 해제일반 Destroy만 호출하면 추적 정보가 남을 수 있다.
LoadAssetsAsync로 여러 에셋 로드반환 핸들 1회 해제개별 결과를 임의로 중복 해제하지 않는다.
씬을 LoadSceneAsync로 로드UnloadSceneAsync씬 핸들과 수명 주기를 함께 관리한다.

특히 Addressables.Release는 C# 관리 힙의 수집, Unity 네이티브 오브젝트 파괴, GPU 리소스 반납을 한 번에 강제하는 명령이 아니다. 참조가 0이 된 번들과 에셋이 언로드 가능한 상태가 되도록 만드는 것이 핵심이다.

Addressables 핸들 참조 카운트와 번들 의존성 해제 흐름

Addressables Release와 ReleaseInstance는 어떻게 구분할까?

LoadAssetAsync<GameObject>는 원본 에셋을 가져온다. 이를 직접 Instantiate했다면 인스턴스의 파괴와 원본 핸들 해제를 각각 관리해야 한다.

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public sealed class EnemyPrefabCache : MonoBehaviour
{
    private AsyncOperationHandle<GameObject> _prefabHandle;
    private GameObject _enemy;

    public async void Spawn()
    {
        _prefabHandle = Addressables.LoadAssetAsync<GameObject>("EnemyGoblin");
        var prefab = await _prefabHandle.Task;
        _enemy = Instantiate(prefab);
    }

    public void Despawn()
    {
        if (_enemy != null)
        {
            Destroy(_enemy);
            _enemy = null;
        }

        if (_prefabHandle.IsValid())
        {
            Addressables.Release(_prefabHandle);
        }
    }
}

반대로 InstantiateAsync는 Addressables가 생성 인스턴스를 추적한다. 이 경우에는 Destroy 대신 ReleaseInstance를 사용한다.

using UnityEngine;
using UnityEngine.AddressableAssets;

public sealed class AddressableSpawner : MonoBehaviour
{
    private GameObject _instance;

    public async void Spawn()
    {
        _instance = await Addressables.InstantiateAsync("EnemyGoblin").Task;
    }

    public void Despawn()
    {
        if (_instance == null) return;

        Addressables.ReleaseInstance(_instance);
        _instance = null;
    }
}

동적 언로드 시점은 어떻게 설계해야 할까?

1. 에셋의 소유자를 먼저 정한다

UI 화면, 게임플레이 지역, 전투 세션처럼 에셋을 사용하는 단위를 소유자로 정한다. 소유자는 자신이 로드한 핸들을 보관하고 종료 시점에만 해제한다. 전역 싱글턴이 모든 핸들을 보관하면 실제 사용 종료 시점을 판단하기 어려워져 메모리 누수와 조기 해제가 모두 발생하기 쉽다.

2. 화면·지역 이탈 시 핸들을 해제한다

팝업이 닫힐 때, 스테이지에서 나갈 때, 매치가 끝날 때처럼 해당 에셋을 다시 참조하지 않는 경계에서 Release를 호출한다. 프리로딩한 에셋은 재진입 가능성과 메모리 예산을 고려해 별도 캐시 정책으로 관리한다.

3. 큰 정리는 안전 구간에서 실행한다

Resources.UnloadUnusedAssets()는 사용되지 않는 에셋을 탐색하고 언로드하므로 프레임 비용이 발생할 수 있다. 전투 도중이나 스크롤 중에 호출하지 말고 씬 전환, 로딩 화면, 백그라운드 복귀 직후 같은 구간에 배치한다.

using System.Collections;
using UnityEngine;

public sealed class MemoryCleanup : MonoBehaviour
{
    public IEnumerator CleanupAfterSceneTransition()
    {
        yield return Resources.UnloadUnusedAssets();
        System.GC.Collect(); // 프로파일링으로 필요성이 확인된 경우에만 사용
    }
}

System.GC.Collect()는 C# 관리 메모리 회수를 유도하지만 Unity 텍스처·메시·오디오 같은 네이티브 리소스의 언로드를 보장하지 않는다. 습관적으로 호출하면 프레임 끊김을 유발할 수 있으므로 메모리 프로파일러 측정 결과가 있을 때만 제한적으로 사용한다.

AssetBundle.Unload(false)와 Unload(true)의 차이는 무엇인가?

호출번들 파일 메모리번들에서 로드한 에셋적합한 시점
AssetBundle.Unload(false)해제 시도이미 로드된 에셋은 유지번들 컨테이너만 더 이상 필요 없을 때
AssetBundle.Unload(true)해제 시도해당 번들에서 로드된 미사용 여부와 무관하게 제거될 수 있음생성 에셋을 더 이상 참조하지 않는 것이 확실할 때

다음 예제처럼 번들에서 프리팹을 로드한 직후 Unload(true)를 호출하면 현재 씬에서 사용 중인 오브젝트가 의존한 리소스까지 예상보다 빨리 사라질 위험이 있다. 반면 Unload(false)는 로드된 에셋을 유지하므로 런타임 인스턴스가 남아 있는 동안의 안전성이 높다.

AssetBundle bundle = await AssetBundle.LoadFromFileAsync(bundlePath);
GameObject prefab = bundle.LoadAsset<GameObject>("Boss");
GameObject boss = Instantiate(prefab);

// 번들 컨테이너만 닫고 boss가 사용하는 로드 에셋은 유지한다.
bundle.Unload(unloadAllLoadedObjects: false);

메모리 예산 기반으로 캐시를 판단하는 방법

무조건 즉시 언로드하면 재다운로드, 디스크 I/O, 재압축 해제로 로딩 시간이 늘어난다. 반대로 모든 에셋을 캐시하면 장시간 플레이에서 메모리가 누적된다. 따라서 최근 사용 시간과 메모리 예산을 함께 둔다.

캐시 해제 후보는 다음처럼 점수화할 수 있다.

evictionScore=ageSeconds×estimatedMemoryMBevictionScore = ageSeconds \times estimatedMemoryMB

evictionScore가 큰 에셋부터 해제하면 오래 사용하지 않았고 메모리 영향도 큰 항목을 우선 정리할 수 있다. 다만 실제 Unity 메모리 사용량은 의존성 공유, 텍스처 압축 형식, 인스턴스 수에 따라 달라지므로 estimatedMemoryMB는 Memory Profiler 측정값으로 보정해야 한다.

자주 묻는 질문 (FAQ)

Addressables.Release를 호출하면 GameObject도 사라지나요?

아니다. LoadAssetAsync로 로드한 프리팹을 직접 Instantiate했다면 인스턴스는 Destroy로 파괴하고 로드 핸들은 Release로 해제해야 한다. InstantiateAsync로 만들었다면 ReleaseInstance를 사용한다.

Resources.UnloadUnusedAssets()를 매 씬마다 호출해도 되나요?

가능하지만 항상 바람직하지는 않다. 호출 비용과 이후 재로드 비용을 프로파일링하고 메모리 압박이 큰 씬 전환 구간에 한정하는 편이 일반적으로 낫다.

AssetBundle을 언로드했는데 텍스처 메모리가 남는 이유는 무엇인가요?

다른 오브젝트·번들이 해당 텍스처를 참조하거나 아직 언로드 가능한 상태가 아닐 수 있다. 번들 이름이 아니라 실제 에셋 참조와 의존성, Addressables 핸들 참조 카운트를 함께 확인해야 한다.

정리

Addressables와 AssetBundle 메모리 관리는 API 하나를 호출하는 문제가 아니라 수명 주기를 설계하는 문제다. 로드한 핸들의 소유자를 명확히 하고 사용 종료 시 Release 또는 ReleaseInstance를 정확히 한 번 호출하자. 이후 UnloadUnusedAssets는 프레임 영향이 허용되는 안전 구간에서만 사용하면 메모리 안정성과 로딩 성능을 함께 관리할 수 있다.