Let's Girin!

[Unity] AssetBundle과 Addressable 본문

Unity

[Unity] AssetBundle과 Addressable

window= 2025. 6. 23. 20:29

유니티로 라이브 서비스 게임을 개발하다 보면 반드시 마주치게 되는 주제가 있다. 바로 에셋을 어떻게 빌드하고, 배포하고, 관리할 것인가라는 문제다. 앱 스토어 심사 없이 콘텐츠를 업데이트하고 싶고, 앱 초기 다운로드 용량은 줄이고 싶고, 유저마다 필요한 리소스만 받게 하고 싶다. 이 모든 요구사항을 해결하기 위해 유니티는 AssetBundle이라는 시스템을 오랫동안 제공해왔고, 최근에는 이를 한 단계 발전시킨 Addressables를 표준으로 밀고 있다.

 

1. 왜 에셋을 따로 관리해야 할까?

유니티 프로젝트를 빌드하면 기본적으로 Assets 폴더 안의 모든 리소스가 실행 파일(APK, IPA 등) 안에 통째로 패키징된다. 이 방식의 한계는 명확하다.

  • 콘텐츠 업데이트마다 앱스토어 재심사가 필요함 — 새 건물 하나 추가하려고 며칠씩 걸리는 심사를 매번 기다릴 순 없다.
  • 초기 다운로드 용량이 계속 커짐 — 라이브 서비스가 길어질수록 앱 용량이 불어나서 신규 유저의 설치 장벽이 높아진다.
  • 모든 유저가 모든 리소스를 다 받아야 함 — 언어별 음성이나 고사양 텍스처처럼 일부 유저에게만 필요한 리소스도 전부 받게 된다.

이 문제를 해결하기 위해, 리소스를 앱 바이너리와 분리해서 런타임에 필요한 것만 다운로드하는 구조가 필요해졌다. 그게 바로 AssetBundle의 존재 이유다.


2. AssetBundle이란?

AssetBundle은 텍스처, 프리팹, 사운드, 씬 등 여러 에셋을 압축된 하나의 배포 가능한 바이너리 패키지로 묶은 것이다. BuildPipeline.BuildAssetBundles() API를 호출하면, 지정해둔 에셋들을 모아 플랫폼별 바이너리 파일로 만들어낸다.

 

/// 결과물이 생성될 로컬 경로, 압축 방식, 대상 플랫폼.
BuildPipeline.BuildAssetBundles(outputPath, BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.Android);

이 API를 실행하면 지정된 경로에 번들 파일들과 함께 매니페스트(Manifest)파일이 생성된다.

 

🔹매니페스트

매니페스트는 이번 빌드에서 만들어진 모든 번들의 목록, 각 번들의 해시값(내용이 바뀌었는지 판별하는 지문), 그리고 번들 간 의존관계를 기록한 색인 파일이다. 예를 들어 캐릭터 프리팹이 공용 머티리얼을 참조하고 있다면, 매니페스트에는 "이 번들을 로드하려면 저 번들 먼저 로드해야 한다"는 정보가 담긴다. 이 정보가 있어야 런타임에 유니티가 의존 번들을 자동으로 함께 로드할 수 있다.

 

🔹압축 방식: LZMA vs LZ4

번들을 빌드할 때 압축 방식을 선택할 수 있는데, 이게 로딩 성능에 꽤 큰 영향을 준다.

항목 LZMA (BuildAssetBundleOptions.None) LZ4 (ChunkBasedCompression)
압축률 높음 (파일 용량 작음) 낮음 (파일 용량 큼)
압축 해제 방식 번들 전체를 한 번에 풀어야 함 청크 단위로 부분 압축 해제 가능
로드 속도 느림 빠름
주 사용처 다운로드 용량이 절대적으로 중요할 때 런타임 로딩 속도가 중요한 대부분의 경우

LZMA는 파일 하나를 통째로 압축 해제해야만 그 안의 에셋에 접근할 수 있다는 제약이 있어서, 번들이 크거나 자주 로드/언로드된다면 LZ4가 실무에서 훨씬 더 선호된다.


3. AssetBundle 배포 파이프라인

빌드가 끝났다고 끝이 아니다. 실제 서비스에서는 대략 이런 흐름을 거친다.

1. 로컬/CI 서버에서 BuildAssetBundles() 실행
   → 로컬 디스크에 번들 파일 + 매니페스트 생성

2. 결과물을 CDN/서버에 업로드
   → S3, CloudFront 등으로 배포

3. 매니페스트에서 추출한 해시값을 버전 관리 시스템에 등록
   → "지금 최신 버전 = 이 해시" 라고 서버 DB에 기록

4. 클라이언트가 접속 시 서버에 최신 해시를 조회
   → 로컬에 갖고 있는 해시와 비교

5. 다르면 변경된 번들만 선별해서 다운로드
   → 매니페스트의 의존관계 정보를 활용해 필요한 것만 받음

 

이 파이프라인에서 중요한 개념이 몇 가지 있다.

  • 해시 기반 버전 관리: 번들 내용이 조금이라도 바뀌면 해시값도 바뀐다. 이 특성을 이용해서, 클라이언트는 "내가 가진 파일의 해시"와 "서버가 알려주는 최신 해시"를 비교해서 무엇이 바뀌었는지 판단한다. 즉 전체를 다시 받는 게 아니라 바뀐 번들만 골라서 받을 수 있다.
  • 네트워크 요청 방식: 번들을 다운로드할 때 순차적으로 하나씩 받을지, 병렬로 여러 개를 동시에 받을지도 성능에 큰 영향을 준다. 특히 번들 개수가 수백 개에 달하는 프로젝트(콘텐츠가 세분화된 장르일수록 흔함)에서는, 순차 다운로드 시 요청 하나당 발생하는 네트워크 왕복 지연(RTT)이 번들 개수만큼 그대로 누적되어 심각한 로딩 지연으로 이어질 수 있다. 이런 경우 동시 요청 개수를 제한한 병렬 다운로드 방식으로 전환하는 게 일반적인 해결책이다.

4. AssetBundle 실무적 한계

AssetBundle 시스템은 강력하지만, 실무에서 쓰다 보면 여러 불편함이 드러난다.

  • 문자열 기반 관리: 번들명, 경로를 전부 문자열로 직접 관리해야 해서 오탈자에 취약하고, 경로가 바뀌면 코드 곳곳을 다 찾아 고쳐야 한다.
  • 의존관계 관리의 번거로움: 어떤 에셋을 어느 번들에 넣을지, 중복은 어떻게 처리할지를 개발자가 수동으로 설계해야 한다. 잘못 설계하면 같은 에셋이 여러 번들에 중복 포함되어 용량이 낭비되거나, 반대로 공용 번들 하나가 너무 커져서 비효율적인 로딩이 발생한다.
  • 참조 카운트 관리의 부재: 번들을 로드/언로드하는 타이밍을 개발자가 직접 추적해야 해서, 실수하면 필요한 시점에 이미 언로드된 번들에 접근하려다 오류가 나거나, 반대로 다 쓴 번들을 안 내려서 메모리를 계속 차지하는 문제가 생기기 쉽다.

이런 문제들을 해결하기 위해 유니티가 내놓은 답이 Addressables다.


5. Addressable이란?

Addressable는 AssetBundle을 완전히 대체하는 새로운 시스템이라기보다, AssetBundle 위에 훨씬 다루기 쉬운 추상화 레이어를 얹은 시스템이라고 보는 게 정확하다. 내부적으로는 여전히 AssetBundle 기술을 사용하지만, 개발자가 직접 다뤄야 했던 복잡한 부분들을 자동화해준다.

🔹주소(Address) 기반 참조

AssetBundle에서는 번들명과 에셋 경로를 문자열로 직접 관리했다면, Addressable에서는 각 에셋에 고유한 주소(키)를 부여하고 이 키로 접근한다.

/// AssetBundle 방식 (문자열 경로를 직접 관리)
AssetBundle bundle = AssetBundle.LoadFromFile(path);
GameObject prefab = bundle.LoadAsset<GameObject>("Hero");

/// Addressable 방식 (주소 기반, 참조 카운트 자동 관리)
AsyncOperationHandle<GameObject> handle = Addressables.LoadAssetAsync<GameObject>("Hero_Address");
handle.Completed += (op) => 
{
   GameObject prefab = op.Result;
};

에디터의 Addressable Groups 창에서 에셋을 그룹/라벨 단위로 관리할 수 있어서, 어떤 에셋이 어느 번들에 포함될지를 시각적으로 설정할 수 있다는 것도 큰 장점이다.

 

🔹참조 카운트 기반 자동 메모리 관리

Addressable는 AsyncOperationHandle을 통해 로드된 에셋의 참조 카운트를 자동으로 추적한다. 같은 에셋을 여러 곳에서 로드해도 내부적으로 참조 카운트만 증가하고, 모든 참조가 Release()되면 그제서야 실제 메모리에서 해제된다. AssetBundle 시절에는 이런 관리를 전부 개발자가 수동으로 해야 했던 것과 대비된다.

/// 사용이 끝나면 반드시 Release 해줘야 참조 카운트가 감소함
Addressables.Release(handle);

여기서 실무적으로 자주 하는 실수가 있다. LoadAssetAsync로 로드한 핸들을 Release하지 않고 방치하면, 참조 카운트가 계속 남아있어서 실제로는 다 쓴 리소스인데도 메모리에서 내려가지 않는 누수가 발생한다. 그래서 핸들 관리를 IDisposable 패턴으로 감싸서 스코프를 벗어나면 자동으로 Release되도록 만드는 방식도 실무에서 종종 쓰인다.

 

🔹원격 배포 설정

Addressable는 Remote Build Path와 Remote Load Path라는 설정을 통해, 로컬 빌드 경로와 실제 서비스에서 참조할 CDN 경로를 분리해서 관리할 수 있다. 이 덕분에 AssetBundle 시절보다 원격 배포 파이프라인을 훨씬 표준화된 방식으로 구성할 수 있다.


 

6. AssetBundle vs Addressable 비교 정리

항목 AssetBundle Addressable
에셋 참조 방식 번들명 + 경로 문자열 고유 주소(키)
의존관계 관리 매니페스트를 개발자가 직접 조회/처리 내부적으로 자동 처리
메모리 관리 로드/언로드를 수동으로 관리 참조 카운트 기반 자동 관리
그룹/번들 구성 코드로 직접 번들명 지정 에디터에서 그룹/라벨로 시각적 관리
원격 배포 설정 별도 파이프라인 직접 구축 Remote Build/Load Path 표준 제공
러닝커브 상대적으로 단순하지만 관리 부담이 큼 개념은 더 많지만 장기적으로 관리가 쉬움

 

결론적으로, 신규 프로젝트라면 Addressable로 시작하는 것이 유니티 공식 권장 방향이고, 실제로도 업계에서 점점 표준으로 자리잡고 있다. 다만 기존에 AssetBundle로 구축된 대규모 라이브 서비스 프로젝트는 마이그레이션 비용이 상당해서, 점진적으로 전환하는 경우가 많다.


 

AssetBundle과 Addressables는 결국 같은 문제(리소스를 앱 바이너리와 분리해서 유연하게 관리하고 싶다)를 해결하기 위한 기술이지만, Addressables는 그 과정에서 개발자가 직접 신경 써야 했던 복잡한 디테일(문자열 관리, 의존관계 추적, 메모리 참조 카운트)을 시스템 차원에서 흡수해준다.

다만 어느 시스템을 쓰든 근본적으로 신경 써야 하는 지점은 동일하다.

 

1. 번들(또는 그룹)을 어떻게 나눌 것인가?

너무 잘게 쪼개면 요청 횟수가 늘어 네트워크 오버헤드가 커지고, 너무 크게 뭉치면 불필요한 리소스까지 함께 받게 된다. 

2. 압축 방식을 어떻게 정할 것인가?

다운로드 용량과 런타임 로딩 속도 사이의 트레이드 오프.

3. 언제 무엇을 로드하고 언제 해제할 것인가?

메모리와 로딩 성능에 직결되는 설계 문제.

반응형