08/26 Addressables 기초 - 에셋 비동기 로딩과 메모리 관리

댓글 0
댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요.

댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요.
Addressables를 사용하는 이유
프로젝트 초반에는 프리팹이나 에셋을 Inspector에 직접 드래그해서 참조하는 방식만으로도 충분합니다. 하지만 프로젝트 규모가 커져 스테이지, 캐릭터, 이펙트, 사운드 등의 종류가 많아지면 에셋 관리가 복잡해지고, 씬에서 직접 참조하고 있는 에셋들이 한꺼번에 메모리에 올라갈 수 있습니다.
Addressables는 이러한 문제를 해결하기 위한 Unity의 에셋 관리 시스템입니다. 각 에셋에 Address(주소) 를 부여하고, 필요한 순간에 해당 주소를 이용해 에셋을 비동기 방식으로 로드합니다. 사용이 끝난 에셋은 다시 해제하여 메모리를 관리할 수 있습니다.
쉽게 생각하면 Addressables는 도서관 시스템과 비슷합니다.
필요한 책을 처음부터 전부 책상에 올려두는 것이 아니라,
필요한 순간에 청구기호(Address)로 찾아 빌리고, 사용이 끝나면 반납하는 방식입니다.
Addressables 사용 시 기대할 수 있는 효과
| 효과 | 내용 |
|---|---|
| 메모리 절약 | 모든 에셋을 처음부터 메모리에 올리지 않고 필요한 에셋만 로드할 수 있습니다. |
| 씬 경량화 | 씬이 많은 에셋을 직접 참조하지 않아 씬의 의존성과 복잡도를 줄일 수 있습니다. |
| 에셋 관리 용이 | 캐릭터, 스테이지, 이펙트 등 에셋 종류가 많아져도 주소를 기준으로 체계적으로 관리할 수 있습니다. |
| 비동기 로딩 | 로딩 중 게임 전체가 멈추는 현상을 줄이고 자연스럽게 에셋을 불러올 수 있습니다. |
| 동적 콘텐츠 구성 | 필요한 시점에 콘텐츠를 불러오므로 대규모 게임이나 DLC, 추가 콘텐츠 관리에 유리합니다. |
| 메모리 해제 관리 | 사용이 끝난 인스턴스를 ReleaseInstance로 반환하여 참조 카운트를 관리할 수 있습니다. |
Addressables 핵심 용어
| 용어 | 의미 |
|---|---|
| Address | Addressable 에셋을 찾기 위한 문자열 주소입니다. 도서관의 청구기호와 같은 역할을 합니다. |
| AsyncOperationHandle | 비동기 작업의 진행 상태, 성공/실패 여부, 결과값 등을 가지고 있는 핸들입니다. |
| InstantiateAsync | Address를 이용해 프리팹을 비동기로 로드하고 생성하는 API입니다. |
| async / await | 시간이 걸리는 작업을 기다릴 때 사용하는 C# 문법입니다. 기다리는 동안 게임 전체를 멈추지 않고 작업 완료 후 다음 코드를 실행할 수 있습니다. |
| ReleaseInstance | InstantiateAsync로 생성한 인스턴스를 Addressables 시스템에 반납하는 API입니다. 단순한 Destroy와 다릅니다. |
| 참조 카운트 | 특정 에셋을 현재 몇 곳에서 사용하고 있는지 나타내는 값입니다. 참조 카운트가 0이 되어야 메모리에서 실제로 해제될 수 있습니다. |
| AssetReference | 문자열 Address를 직접 입력하는 대신 Inspector에서 에셋을 드래그하여 연결할 수 있는 Addressables 전용 참조 타입입니다. |
기본 사용 흐름
Addressable 등록 → Address 지정 → 비동기 로드 → 사용 → Release
예를 들어 캐릭터 프리팹이 필요하면,
캐릭터 Address 확인 → InstantiateAsync() → await로 로딩 대기 → 캐릭터 사용 → ReleaseInstance()
순서로 처리합니다.
Addressables의 목적은 단순히 에셋을 불러오는 것이 아니라,
"필요할 때 로드하고 필요 없을 때 해제하는 구조"를 만드는 것입니다.
따라서 프로젝트의 에셋 종류와 규모가 커질수록 메모리 관리, 로딩 관리, 에셋 관리 측면에서 효과가 커지는 시스템이라고 정리할 수 있습니다.
Resources로 에셋 로드하기
Resources.Load<T>()는 Resources 폴더 안의 에셋을 런타임에 문자열 경로로 불러오는 방법입니다.
예를 들어 에셋 위치가
Assets/Resources/VFX/HitEffect.prefab
이라면 다음과 같이 불러옵니다.
ParticleSystem hitEffect = Resources.Load<ParticleSystem>("VFX/HitEffect");
경로를 작성할 때는 Assets/Resources/ 부분과 파일 확장자는 제외합니다.
Resources 폴더 안의 에셋은 씬이나 코드에서 직접 참조하지 않아도 빌드에 포함됩니다.
즉, 실제로 Resources.Load()로 사용하지 않는 에셋이라도 Resources 폴더 안에 있다면 빌드에 포함됩니다.
또한 해당 에셋이 Texture, Material 등 다른 에셋을 참조하고 있다면 그 의존 에셋도 함께 포함될 수 있습니다.
따라서 Resources에 많은 에셋을 넣으면 게임의 빌드 용량이 커질 수 있습니다.
Resources 안의 모든 에셋이 게임 시작과 동시에 메모리에 올라가는 것은 아닙니다.
하지만 Unity는 앱 시작 시 Resources의 에셋을 찾기 위한 에셋 정보와 인덱스를 초기화해야 합니다.
따라서 Resources에 에셋이 많아질수록 초기화 비용이 증가하여 앱 시작 시간이 길어질 수 있습니다.
Resources의 에셋은 게임 빌드 안에 포함되는 데이터입니다.
따라서 배포 후 Resources의 프리팹, 이미지, 사운드 등을 수정하면 변경 내용을 적용하기 위해 게임 빌드를 다시 빌드하고 배포해야 합니다.
Addressables처럼 에셋만 따로 업데이트하기에는 적합하지 않습니다.
Resources.Load()는 문자열 경로로 에셋을 찾습니다.
Resources.Load<ParticleSystem>("VFX/HitEffect");
따라서 HitEffect의 이름을 변경하거나 다른 폴더로 이동하면 코드의 문자열도 직접 수정해야 합니다.
문자열 경로가 잘못되어도 컴파일 에러가 발생하지 않고 실행 시 null이 반환될 수 있기 때문에 프로젝트가 커질수록 관리가 어려워집니다.
| 단점 | 이유 |
|---|---|
| 빌드 사이즈 증가 | Resources 내부 에셋은 사용하지 않아도 빌드에 포함됨 |
| 앱 시작 시간 증가 | Resources 에셋 정보의 초기화 비용이 증가함 |
| 업데이트 어려움 | 에셋 변경 시 Player를 다시 빌드해야 함 |
| 이름 변경 어려움 | 문자열 경로를 사용하기 때문에 이름/위치 변경에 취약함 |
Resources.Load()는 간단하고 편리하지만, 에셋의 종류와 수가 많아지면 관리가 어려워지기 때문에 규모가 큰 프로젝트에서는 Addressables를 사용하는 것이 더 적합합니다.
Addressables 설치 및 등록
Package Manager에서 Addressables를 설치합니다.Addressables > Groups에서 Create Addressables Settings로 초기화합니다.Addressable을 체크합니다.enemy/basic, vfx/hit처럼 짧고 일관된 규칙으로 수정합니다.핵심: 설치 → 초기화 → 에셋 등록 → Address 정리
Addressables에서는 에셋의 Address를 실제 파일 경로와 분리해서 사용할 수 있습니다.
예를 들어 에셋의 Address를 enemy/basic으로 지정해두면, 프리팹을 Assets/Prefabs/Enemy에서 다른 폴더로 옮겨도 Address를 그대로 유지할 수 있습니다. 즉, 코드에서는 계속 enemy/basic이라는 같은 키로 에셋을 불러올 수 있어 폴더 구조 변경에 덜 영향을 받고 관리가 쉬워집니다.
비동기란?

비동기(Asynchronous)란 어떤 작업이 끝날 때까지 프로그램 전체가 멈춰서 기다리지 않고, 다른 작업을 계속 진행하면서 결과가 준비되면 나중에 처리하는 방식입니다.
게임에서는 파일 읽기, 네트워크 통신, 대용량 에셋 로딩처럼 시간이 오래 걸릴 수 있는 작업에 비동기 처리를 많이 사용합니다.
예를 들어 Addressables에서 Material을 로드한다고 해보겠습니다.
Addressables.LoadAssetAsync<Material>("Monster.mat");
Monster.mat을 불러오는 작업은 즉시 끝난다고 보장할 수 없습니다. 에셋이 로컬 파일에 있을 수도 있고, 아직 다운로드하지 않은 Remote Addressable이라면 서버에서 다운로드해야 할 수도 있습니다.
만약 이런 작업을 동기적으로 처리한다면, 로딩이 끝날 때까지 게임의 메인 스레드가 기다려야 할 수 있습니다.
게임 실행
↓
Monster.mat 로딩 시작
↓
[로딩이 끝날 때까지 대기]
↓
Monster.mat 로딩 완료
↓
게임 계속 진행
이렇게 메인 스레드가 멈추면 그동안 화면 갱신이나 사용자 입력 처리도 원활하게 이루어지지 않아 게임이 멈춘 것처럼 보이는 현상이 발생할 수 있습니다.
반면 비동기 방식에서는 로딩 작업을 요청한 뒤 게임은 계속 실행됩니다.
Monster.mat 로딩 시작 ───────────────→ 로딩 완료
↓
게임 로직 계속 실행
↓
프레임 렌더링
↓
사용자 입력 처리
↓
기타 게임 로직 실행
그리고 에셋 로딩이 완료되면 그때 결과를 받아서 사용합니다.
Addressables의 LoadAssetAsync에서 Async가 바로 Asynchronous(비동기)를 의미합니다.
var handle =
Addressables.LoadAssetAsync<Material>("Monster.mat");
Material material = await handle.Task;
위 코드에서는 Monster.mat의 로딩을 요청한 뒤 await를 통해 에셋의 로딩이 완료될 때까지 기다립니다.
여기서 중요한 점은 await가 있다고 해서 프로그램 전체를 멈춰 세우는 것은 아니라는 것입니다.
await = "여기서 결과가 필요하니,
완료되면 이 코드의 다음 부분을 계속 실행해 주세요."
즉,
Material material = await handle.Task;
Debug.Log("Material 로딩 완료!");
라고 작성하면 Monster.mat의 로딩이 끝난 후 Debug.Log() 이후의 코드가 실행됩니다. 그동안 Unity 자체의 프레임 처리 등 다른 작업은 계속 진행될 수 있습니다.
동기(Synchronous)
작업 A 시작
↓
작업 A가 끝날 때까지 기다림
↓
작업 B 실행
비동기(Asynchronous)
작업 A 시작 ─────────→ 완료
↓
다른 작업 진행
↓
작업 A가 완료되면 결과 처리
따라서 Addressables에서 비동기 로딩을 사용하는 가장 큰 이유는 에셋을 불러오는 동안 게임 전체가 멈추는 것을 방지하기 위해서라고 이해할 수 있습니다.
Addressables.LoadAssetAsync<Material>("Monster.mat");
이 코드는
"Monster.mat을 불러와 줘. 하지만 불러오는 동안 게임 전체를 멈추고 기다리지는 않을게. 로딩이 완료되면 그 결과를 사용하겠다."
라는 의미로 이해하면 됩니다.
즉, 비동기란 시간이 걸리는 작업을 요청해 놓고, 그 작업 때문에 프로그램 전체를 멈추지 않는 처리 방식입니다.
순서가 중요한 경우 → 동기 처리 방식 사용
속도가 중요한 경우 → 비동기 처리 방식 사용
Addressables를 통한 비동기 에셋 로드
Addressables.LoadAssetAsync<Material>("Monster.mat").Completed += x => monsterMaterial = x.Result;
비동기 에셋 로드가 완료되면(Completed), 로드된 Monster.mat의 결과값(x.Result)을 monsterMaterial 변수에 할당합니다. 가장 정석적인 방법
monsterMaterial = Addressables.LoadAssetAsync<Material>("Monster.mat").WaitForCompletion();
WaitForCompletion()을 사용해 Monster.mat의 로드가 끝날 때까지 기다린 뒤 결과를 monsterMaterial에 할당하는 동기식 처리 방법이지만, 메인 스레드를 멈출 수 있어 일반적으로 권장되지는 않습니다.
private IEnumerator Start()
{
var handle = Addressables.LoadAssetAsync<Material>("Monster.mat");
yield return handle;
monsterMaterial = handle.Result;
}
Monster.mat의 비동기 로드를 시작하고, 로드가 완료될 때까지 코루틴을 중단한 뒤 결과를 monsterMaterial에 할당합니다.
Release
Addressables.ReleaseInstance();
Addressables.Release();
Release를 해야 하는 이유는 Addressables가 로드한 에셋의 참조 카운트(Reference Count)를 관리하기 때문입니다.
더 이상 사용하지 않는 에셋을 Release하지 않으면 참조가 계속 유지되어 메모리에서 해제되지 않을 수 있고, 결국 불필요한 메모리 점유나 메모리 누수처럼 보이는 문제가 생길 수 있습니다.
Addressables.ReleaseInstance()는 InstantiateAsync()로 생성한 인스턴스를 제거하고, 해당 Addressable 에셋에 대한 참조도 함께 해제할 때 사용합니다.
Addressables.Release()는 LoadAssetAsync() 등으로 로드한 에셋의 참조를 해제하며, 더 이상 사용하지 않는 에셋이 메모리에 계속 남는 것을 방지하기 위해 필요합니다.

우리들의 게임 발매 이야기

안녕하세요. 플밍 4기 입니다. 게임 개발을 배우기 전 네트워크 엔지니어 도메인에서 익히고 배웠던 네트워크 이론에 대한 기초 입니다. 학습에 도움이 되길 바라며 공유 드립니다.
XR을 활용한 게임 개발 3기(유니티) 수강생입니다. 곧 수료 하지만 앞으로 이곳에 가끔 저의 개발 경험이 나 지식 기록할까 합니다. 더 나아가 이 사이트가 제 개인위키의 역할을 할 수 있으면 좋겠습니다. 한국 게임 시장을 흔들겠습니다


안녕하세요. 플밍 4기 입니다. 게임 개발을 배우기 전 네트워크 엔지니어 도메인에서 익히고 배웠던 네트워크 이론에 대한 기초 입니다. 학습에 도움이 되길 바라며 공유 드립니다.
XR을 활용한 게임 개발 3기(유니티) 수강생입니다. 곧 수료 하지만 앞으로 이곳에 가끔 저의 개발 경험이 나 지식 기록할까 합니다. 더 나아가 이 사이트가 제 개인위키의 역할을 할 수 있으면 좋겠습니다. 한국 게임 시장을 흔들겠습니다
