Unity Addressables 동적 리소스 로딩과 언로드 최적화: 세팅부터 메모리 관리까지
Unity Addressables 설정부터 비동기 에셋 로딩, 핸들 기반 언로드, 프리로드까지 실전 흐름을 정리합니다. Release와 ReleaseInstance를 올바르게 사용해 메모리 누수와 로딩 지연을 줄이는 방법을 알아봅니다.

핵심 요약
Unity Addressables는 에셋을 주소(address)나 라벨(label)로 비동기 로드하고 반환받은 AsyncOperationHandle을 Release해 참조를 관리하는 Unity 시스템이다. Destroy만 호출하면 Addressables가 추적하는 참조가 남을 수 있으므로 LoadAssetAsync에는 Release, InstantiateAsync에는 ReleaseInstance를 짝지어야 한다.
이 글에서는 Addressables 기본 설정부터 프리팹 동적 로딩, 안전한 언로드, 라벨 기반 사전 다운로드까지 실무에서 바로 적용할 수 있는 흐름을 다룬다.
Unity Addressables는 왜 필요한가?
Resources.Load는 간단하지만 에셋을 Resources 폴더에 둬야 하고 에셋 수명과 배포 단위를 세밀하게 통제하기 어렵다. Addressables는 에셋을 그룹으로 분류하고 주소 또는 라벨로 불러온다. 프리팹, Sprite, AudioClip, Scene을 같은 비동기 API 흐름으로 다룰 수 있다는 점이 핵심이다.
| 항목 | Resources.Load | Unity Addressables |
|---|---|---|
| 식별 방식 | 경로 문자열 | Address 또는 Label |
| 로딩 방식 | 주로 동기 | AsyncOperationHandle<T> 기반 비동기 |
| 배포 단위 | 플레이어 빌드 중심 | 그룹·번들·원격 배포 설정 가능 |
| 해제 전략 | UnloadUnusedAssets 등에 의존 | Release로 참조 카운트 반환 |
| 의존성 관리 | 직접 설계 | 카탈로그와 번들 의존성 기반 관리 |
모든 에셋을 Addressables로 옮길 필요는 없다. 씬별 프리팹, 대형 텍스처, 캐릭터 스킨, DLC처럼 수명과 배포를 제어해야 하는 콘텐츠부터 적용하는 편이 효과적이다.
Unity Addressables를 어떻게 세팅할까?
1. Addressables 패키지와 설정 파일을 만든다
- Unity 메뉴에서 Window > Package Manager를 연다.
Addressables패키지를 설치한다.- Window > Asset Management > Addressables > Groups를 연다.
- 설정 생성 안내가 표시되면 Create Addressables Settings를 선택한다.
- 관리할 에셋을 선택하고 Inspector에서
Addressable을 활성화한다.
에셋에 부여하는 Address는 파일 경로 대신 안정적인 식별자로 정하는 것이 좋다. 예를 들어 Assets/Prefabs/Enemy/Goblin.prefab보다 prefab/enemy/goblin처럼 지정하면 폴더 구조가 바뀌어도 코드 의존성을 줄일 수 있다.

2. Group과 Label의 역할을 나눈다
Group은 번들 생성과 배포 정책을 정하는 단위다. Label은 여러 에셋을 한 번에 선택하기 위한 분류 기준이다. 둘을 같은 목적으로 사용하면 콘텐츠 관리가 복잡해진다.
| 구분 | 적합한 용도 | 예시 |
|---|---|---|
| Group | 번들·캐시·원격 배포 정책 | LocalCharacters, RemoteSeasonalContent |
| Address | 단일 에셋 식별 | prefab/enemy/goblin |
| Label | 여러 에셋 일괄 선택 | chapter-2, character-skin |
예를 들어 챕터 2에서 사용할 프리팹과 오디오에 chapter-2 라벨을 붙이면 챕터 진입 전에 관련 번들을 미리 다운로드할 수 있다.
3. 콘텐츠를 빌드하고 실제 번들 환경에서 확인한다
에셋 등록 뒤 Addressables Groups > Build > New Build > Default Build Script를 실행한다. 개발 중에는 Play Mode Script를 Use Asset Database (fastest)로 설정해 빠르게 확인할 수 있다.
단, 이 모드는 실제 AssetBundle 의존성, 다운로드, 캐시 문제를 재현하지 못한다. 출시 전에는 Use Existing Build 또는 배포 환경과 가까운 설정으로 씬 전환과 메모리 해제를 검증해야 한다.
Addressables 동적 로딩은 어떻게 구현할까?
프리팹을 생성할 때는 Addressables.InstantiateAsync를 사용한다. 반환된 AsyncOperationHandle<GameObject>를 보관하고 해당 인스턴스의 수명이 끝날 때 ReleaseInstance로 해제한다.
using System.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public sealed class EnemySpawner : MonoBehaviour
{
[SerializeField] private string enemyAddress = "prefab/enemy/goblin";
private AsyncOperationHandle<GameObject> enemyHandle;
private IEnumerator Start()
{
enemyHandle = Addressables.InstantiateAsync(
enemyAddress,
new Vector3(0f, 0f, 5f),
Quaternion.identity);
yield return enemyHandle;
if (enemyHandle.Status == AsyncOperationStatus.Succeeded)
{
Debug.Log($"Loaded: {enemyHandle.Result.name}");
}
else
{
Debug.LogError($"Failed to load: {enemyAddress}");
Addressables.Release(enemyHandle);
}
}
private void OnDestroy()
{
if (enemyHandle.IsValid())
{
Addressables.ReleaseInstance(enemyHandle);
}
}
}
InstantiateAsync로 생성한 인스턴스는 Destroy(enemyHandle.Result)만 호출하지 않는 것이 좋다. ReleaseInstance(enemyHandle)가 인스턴스를 제거하고 Addressables 참조도 반환한다. 로딩 실패 시에는 인스턴스가 없으므로 Release로 핸들만 정리한다.
Addressables Release와 UnloadUnusedAssets는 무엇이 다른가?
Addressables는 핸들 단위로 참조 카운트를 관리한다. 같은 에셋을 여러 곳에서 로드하면 의존 번들이 공유될 수 있으며 각 로드 요청에 대응하는 해제가 필요하다.
flowchart LR
A[LoadAssetAsync 또는 InstantiateAsync] --> B[AsyncOperationHandle 획득]
B --> C[에셋 또는 인스턴스 사용]
C --> D[Release 또는 ReleaseInstance 호출]
D --> E{참조 카운트가 0인가?}
E -- 아니오 --> C
E -- 예 --> F[에셋과 의존 번들이 언로드 대상이 됨]
Release를 호출했다고 네이티브 메모리가 즉시 반환되는 것은 아니다. 실제 회수 시점은 공유 의존성, 번들 상태, Unity의 언로드 정책에 따라 달라진다. 그러나 Addressables 관점에서 참조를 반환하는 첫 단계는 반드시 Release다.
| 로드 방식 | 권장 해제 API | 주의점 |
|---|---|---|
LoadAssetAsync<T> | Addressables.Release(handle) | 로드 호출마다 핸들 수명 관리 필요 |
InstantiateAsync | Addressables.ReleaseInstance(handle) | Destroy만 호출하지 않기 |
LoadSceneAsync | Addressables.UnloadSceneAsync(handle) | 씬 핸들을 유지해야 함 |
LoadAssetsAsync<T> | Addressables.Release(handle) | 결과를 쓰는 동안 해제하지 않기 |
안전한 언로드는 어떻게 설계할까?
핸들의 소유자(owner)를 정하고 소유자가 기능 종료 시점에 해제하도록 설계하는 것이 가장 중요하다. 상점 UI가 아이콘을 로드했다면 상점 UI 컨트롤러가 핸들을 보관하고 UI를 닫을 때 해제해야 한다.
using System.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.UI;
public sealed class ShopIconView : MonoBehaviour
{
[SerializeField] private Image iconImage;
[SerializeField] private string iconAddress = "sprite/ui/shop-icon";
private AsyncOperationHandle<Sprite> iconHandle;
private bool isOpen;
private int generation;
public IEnumerator Open()
{
if (iconHandle.IsValid())
{
yield break;
}
isOpen = true;
int openGeneration = ++generation;
AsyncOperationHandle<Sprite> loadHandle =
Addressables.LoadAssetAsync<Sprite>(iconAddress);
iconHandle = loadHandle;
yield return loadHandle;
// Close가 이전 핸들을 해제했으면 이 코루틴은 소유권을 잃었습니다.
// 같은 에셋의 재오픈이 내부 작업을 공유해도 새 핸들을 해제하지 않습니다.
if (openGeneration != generation)
yield break;
if (!isOpen || !loadHandle.IsValid() ||
loadHandle.Status != AsyncOperationStatus.Succeeded)
{
if (loadHandle.IsValid())
{
Addressables.Release(loadHandle);
}
if (iconHandle.Equals(loadHandle))
{
iconHandle = default;
}
yield break;
}
iconImage.sprite = loadHandle.Result;
}
public void Close()
{
++generation;
isOpen = false;
iconImage.sprite = null;
ReleaseIcon();
}
private void OnDestroy()
{
++generation;
isOpen = false;
ReleaseIcon();
}
private void ReleaseIcon()
{
if (!iconHandle.IsValid())
{
return;
}
Addressables.Release(iconHandle);
iconHandle = default;
}
}
로딩 중 UI가 닫힐 수 있으므로 Close와 OnDestroy가 핸들을 해제하고 세대 번호를 변경한다. 이전 Open 코루틴은 완료 후 세대가 달라졌다면 UI를 갱신하거나 핸들을 다시 해제하지 않는다. 같은 에셋을 재오픈할 때 내부 로드 작업이 공유되더라도, 핸들의 유효성만으로 자신에게 해제 소유권이 남아 있다고 판단해서는 안 된다.

라벨 기반 사전 로딩은 언제 사용할까?
다음 구간에 필요한 콘텐츠가 예측 가능하다면 라벨 기반 사전 다운로드를 사용한다. 전투 시작 직전에 캐릭터 프리팹과 오디오 번들을 내려받으면 플레이 중 발생하는 로딩 지연을 줄일 수 있다.
- 스테이지 또는 챕터에 필요한 에셋에
chapter-2라벨을 부여한다. - 로딩 화면에서
Addressables.DownloadDependenciesAsync("chapter-2")를 실행한다. - 완료 상태를 확인한 뒤 다운로드 핸들은
Addressables.Release로 반환한다. - 실제 사용 시점에는 개별 에셋을 로드하고 사용 종료 시 각각 해제한다.
using System.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public sealed class ChapterPreloader : MonoBehaviour
{
public IEnumerator DownloadChapter2()
{
AsyncOperationHandle downloadHandle =
Addressables.DownloadDependenciesAsync("chapter-2");
yield return downloadHandle;
if (downloadHandle.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError("Chapter 2 download failed.");
}
Addressables.Release(downloadHandle);
}
}
DownloadDependenciesAsync는 주로 번들을 로컬 캐시에 확보하는 작업이다. 다운로드가 끝났다고 프리팹 인스턴스나 Sprite가 계속 메모리에 유지되는 것은 아니다. 실제 메모리 점유는 이후 LoadAssetAsync와 InstantiateAsync로 얻은 핸들의 수명에 따라 관리한다.
메모리 누수와 로딩 지연은 왜 발생하는가?
가장 흔한 원인은 로드 API와 해제 API가 맞지 않는 경우다. LoadAssetAsync로 얻은 핸들을 반환하지 않으면 참조 카운트가 남는다. 반대로 여러 화면이 공유하는 에셋을 한 화면이 임의로 해제하면 다른 화면에서 예상하지 못한 문제가 발생할 수 있다.
다음 항목을 확인하자.
LoadAssetAsync,LoadAssetsAsync,InstantiateAsync,LoadSceneAsync마다 해제 책임자가 있는가?- Addressables로 생성한 프리팹을
ReleaseInstance로 해제하는가? - 화면을 연속으로 열 때 같은 주소에 대한 중복 요청을 막았는가?
- 라벨로 대량 로드한 에셋을 필요 이상으로 붙잡고 있지 않은가?
- Unity Profiler의 Memory 모듈에서 씬 이탈 후에도 텍스처, 메시, 번들이 예상보다 오래 남는가?
씬 진입과 이탈을 반복하면서 메모리 변화량을 비교하면 이상 징후를 찾기 쉽다.
반복 실행 후 가 계속 증가한다면 누락된 핸들 해제, 정적 참조, 이벤트 구독, 공유 의존 번들을 우선 조사한다. 단, 가비지 컬렉션과 네이티브 메모리 회수 시점은 다르므로 한 번의 측정만으로 누수라고 단정해서는 안 된다.
자주 묻는 질문 (FAQ)
Addressables.Release만 호출하면 메모리가 즉시 줄어드나요?
아니다. Release는 Addressables의 참조 카운트를 반환한다. 실제 에셋과 번들의 회수 시점은 공유 의존성, 캐시, Unity의 메모리 상태에 따라 달라질 수 있다.
Destroy와 ReleaseInstance를 함께 호출해야 하나요?
Addressables.InstantiateAsync로 생성했다면 일반적으로 ReleaseInstance만 호출한다. 이 API가 인스턴스 제거와 Addressables 참조 반환을 함께 처리한다.
로드한 에셋을 캐시해도 되나요?
가능하다. 다만 캐시의 소유자와 만료 시점을 정해야 하며 캐시를 비울 때 연결된 핸들을 Release해야 한다. 주소 문자열을 캐시하는 것과 로드된 핸들을 캐시하는 것은 메모리 영향이 다르다.
정리
Unity Addressables 언로드 최적화의 핵심은 복잡한 호출 순서가 아니라 명확한 소유권이다. 주소와 라벨로 콘텐츠를 구분하고 로드 API가 반환한 AsyncOperationHandle을 수명 단위로 보관한 뒤 같은 단위에서 Release, ReleaseInstance, UnloadSceneAsync를 호출하자.
개발 중에는 빠른 Play Mode로 작업하되 배포 전에는 실제 번들 설정에서 씬 진입과 이탈을 반복하며 Unity Profiler로 피크 메모리와 잔존 에셋을 확인하는 것이 좋다.



