---
title: "Addressables 는 AssetBundle 위에 주소를 얹는다"
description: "Addressables 는 에셋에 주소를 붙여 그 주소로 로드하게 하고, 어느 번들에 있는지와 의존 번들을 여는 일을 대신합니다. 다만 에디터 기본 모드는 그룹 설정을 보지 않아서, 에디터에서만 되는 로드가 생깁니다."
author: "유니얼"
pubDate: 2026-10-19
category: "game-dev/unity/runtime"
tags: ["Unity", "메모리"]
canonical: https://blog.unialgames.com/posts/unity-addressables-why/
---
<x-repo>
<script type="application/json">
{
  "repo": "SeoBYP/AssetBundleStudy",
  "text": "AssetBundle 과 Addressables 의 문서 내용을 Windows Player 에서 직접 재서 확인한 기록입니다.",
  "chips": ["Unity 6000.6.3f1", "Addressables 4.1.1", "Windows Player"],
  "links": [
    { "text": "결과 AD1", "path": "Measure/results/AD1.txt" },
    { "text": "에디터 Play Mode 측정", "path": "Assets/Measure/Tests/AD1EditorPlayModeTest.cs" }
  ]
}
</script>
</x-repo>

AssetBundle 을 직접 쓰면 일이 계속 늘어납니다. 어느 에셋이 어느 번들에 있는지 기억해야 하고, 의존 번들을 순서대로 열어야 하고, 누가 아직 쓰는지 세다가 내려야 하고, 서버에 올린 번들의 경로와 버전도 관리해야 합니다.

**Addressables** 는 **에셋에 주소를 붙이고, 그 주소로 로드하면 나머지를 대신 해 주는 패키지**입니다. 밑에서는 여전히 AssetBundle 이 돌고, Addressables 는 그 위에 "주소 → 위치 → 번들" 을 찾아 주는 층을 얹은 것입니다. 매뉴얼도 AssetBundle 을 에디터에서 정의하고 빌드하는 쉬운 방법이라며 저수준 API 대신 이것을 권합니다.

## 주소로 부르면 번들은 Addressables 가 연다

에셋을 Addressable 로 표시하면 주소가 붙습니다. 기본값은 프로젝트 경로이고 원하는 이름으로 바꿀 수 있습니다. 라벨을 붙이면 여러 에셋을 한꺼번에 부를 수도 있습니다. 코드는 주소만 압니다.

```csharp
AsyncOperationHandle<GameObject> handle = Addressables.LoadAssetAsync<GameObject>("Cube");
yield return handle;
GameObject instance = Instantiate(handle.Result);

// 다 쓰면 로드한 횟수만큼 놓는다
Destroy(instance);
Addressables.Release(handle);
```

이 한 줄 뒤에서 일어나는 일입니다.

<x-wiring>
<script type="application/json">
{
  "label": "LoadAssetAsync(\"Cube\") 가 하는 일",
  "width": 560,
  "height": 560,
  "nodes": [
    { "id": "code", "title": "LoadAssetAsync(\"Cube\")", "x": 170, "y": 10, "w": 220, "h": 46, "aside": true },
    { "id": "cat", "title": "카탈로그", "sub": "주소 → 위치와 의존 목록", "x": 170, "y": 115, "w": 220 },
    { "id": "dep", "title": "의존 번들", "sub": "머티리얼, 셰이더가 든 번들", "x": 170, "y": 225, "w": 220 },
    { "id": "own", "title": "Cube 가 든 번들", "x": 170, "y": 335, "w": 220 },
    { "id": "asset", "title": "Cube 프리팹", "sub": "핸들 참조 1", "x": 170, "y": 450, "w": 220 }
  ],
  "edges": [
    { "from": "code", "to": "cat", "label": "주소로 찾는다" },
    { "from": "cat", "to": "dep", "label": "의존 번들을 먼저 연다" },
    { "from": "dep", "to": "own", "label": "그다음 본 번들" },
    { "from": "own", "to": "asset", "label": "에셋을 꺼낸다" }
  ],
  "steps": [
    { "edge": 0, "why": "카탈로그에서 주소 Cube 의 위치를 찾습니다. 어느 번들이고 어느 번들에 기대는지가 적혀 있습니다." },
    { "edge": 1, "why": "기대는 번들을 먼저 엽니다. 직접 쓸 때 손으로 하던 일입니다." },
    { "edge": 2, "why": "Cube 가 든 번들을 엽니다. 이미 열려 있으면 다시 열지 않습니다." },
    { "edge": 3, "why": "에셋을 꺼내 핸들에 담아 줍니다. 핸들마다 참조를 세서, Release 로 0 이 되면 번들을 내립니다." }
  ],
  "note": "번들이 로컬에 있든 서버에 있든 코드는 같습니다. 위치는 카탈로그가 압니다."
}
</script>
</x-wiring>

**카탈로그**는 빌드할 때 함께 나오는 표입니다. 주소마다 어느 번들의 어느 에셋인지, 그 번들이 어느 번들에 기대는지, 번들 파일이 로컬 경로에 있는지 서버 URL 에 있는지가 들어 있습니다. 그래서 같은 주소가 개발 중에는 로컬 번들을, 출시 뒤에는 서버의 번들을 가리킬 수 있습니다.

번들 구성은 **그룹**으로 정합니다. 에셋을 그룹에 넣으면 빌드가 그룹 설정대로 번들을 만듭니다. 번들 이름을 에셋마다 적던 일이 그룹 단위 설정으로 바뀐 것입니다. `AssetReference` 필드를 쓰면 문자열 대신 인스펙터에서 고른 에셋으로 참조할 수 있어서 주소 오타를 컴파일 단계 가까이에서 막을 수 있습니다.

## 에디터 기본 모드는 그룹 설정을 보지 않는다

편해진 대가로 함정이 하나 생깁니다. 에디터의 Play Mode Script 기본값인 **Use Asset Database** 는 번들을 만들지 않고 프로젝트의 에셋을 바로 읽습니다. 빌드 없이 바로 돌릴 수 있어 빠른 대신, 빌드된 번들과 다르게 동작할 수 있습니다.

그룹 하나의 Include in Build 를 끄고, 그 그룹의 에셋을 세 환경에서 로드했습니다.

| 실행 | 빌드에 넣은 그룹 | Include in Build 를 끈 그룹 |
| --- | --- | --- |
| 에디터, Use Asset Database | 성공 | **성공** |
| 에디터, Use Existing Build | 성공 | `InvalidKeyException` |
| Player | 성공 | **`InvalidKeyException`** |

에디터에서는 되는 로드가 Player 에서는 `No Location found for Key=…` 로 실패했습니다. Addressables 4.1.1 소스를 보면 이유가 나옵니다. Use Asset Database 모드는 `Editor/Build/FastModeInitializationOperation.cs` 에서 `AddressableAssetSettingsLocator` 를 등록하는데, 이 로케이터는 **모든 그룹의 모든 엔트리**를 키 표에 넣고 `IncludeInBuild` 를 확인하지 않습니다(`Editor/Build/AddressableAssetSettingsLocator.cs`).

**Use Existing Build 는 Player 와 같은 결과**를 냈습니다. 실제로 빌드한 카탈로그와 번들을 읽기 때문입니다.

콘텐츠 빌드를 아예 빠뜨리면 Player 는 초기화부터 실패합니다. 이 프로젝트는 Player 빌드 때 Addressables 를 같이 빌드하지 않게 설정했는데, 콘텐츠를 빌드하지 않고 Player 만 만들면 `RuntimeData is null. Please ensure you have built the correct Player Content.` 이 찍히고 모든 키가 실패했습니다.

## 직접 확인하는 테스트 코드

Include in Build 를 끈 그룹의 에셋을 Play Mode 테스트와 Player 에서 같은 코드로 로드합니다. Play Mode Script 를 바꿔 가며 테스트를 돌리고, 같은 코드를 Player 에서도 실행합니다.

```csharp
public class AddressablesKeyTest
{
    [UnityTest]
    public IEnumerator ExcludedGroupIsNotInBuild()
    {
        yield return Addressables.InitializeAsync();

        foreach (string key in new[] { "AD1/Included", "AD1/Excluded" })
        {
            var handle = Addressables.LoadAssetAsync<Texture2D>(key);
            yield return handle;
            Debug.Log($"{key}: {handle.Status} {handle.OperationException?.Message}");
            if (handle.IsValid()) Addressables.Release(handle);
        }
    }
}
```

Use Asset Database 에서는 둘 다 `Succeeded`, Use Existing Build 와 Player 에서는 `AD1/Excluded` 만 `Failed` 와 `InvalidKeyException` 이 나오면 같은 결과입니다.

## 주의할 점

### 1. 출시 전 확인은 Use Existing Build 로 한다

**안 지키면 생기는 문제.** Use Asset Database 는 그룹 설정을 무시하고 프로젝트 에셋을 바로 읽어서, 빌드에서 빠진 에셋도 에디터에서는 로드됩니다. 에디터 테스트를 다 통과한 기능이 Player 에서 키를 못 찾습니다.

**이렇게 쓴다.** 개발 중에는 Use Asset Database 로 빠르게 돌리고, 확인 단계에서는 콘텐츠를 빌드한 뒤 Use Existing Build 로 돌립니다.

### 2. Player 빌드에 콘텐츠 빌드를 묶어 둔다

**안 지키면 생기는 문제.** 콘텐츠를 빌드하지 않거나 옛 콘텐츠로 Player 를 만들면 초기화가 실패하거나 새 키를 못 찾습니다. 빌드 자체는 성공합니다.

**이렇게 쓴다.** Addressables 설정의 Player 빌드 때 콘텐츠도 빌드하는 옵션을 켜 두거나, 빌드 스크립트에서 `AddressableAssetSettings.BuildPlayerContent()` 를 Player 빌드 앞에 부릅니다.

### 3. 주소 문자열은 한 곳에서만 쓴다

**안 지키면 생기는 문제.** 주소는 문자열이라 오타나 주소 변경이 컴파일에서 안 잡히고, 런타임에 `InvalidKeyException` 으로만 드러납니다.

**이렇게 쓴다.** 인스펙터에서 연결하는 `AssetReference` 필드를 쓰거나, 주소를 상수로 모아 둔 클래스 하나에서만 가져다 씁니다.

## 정리

Addressables 는 **AssetBundle 위에 주소와 카탈로그를 얹어, 위치 찾기와 의존 번들 열기를 대신합니다.**

- 코드는 주소로 `LoadAssetAsync` 하고 `Release` 로 짝을 맞추면 됩니다. 어느 번들인지, 무엇을 먼저 열지는 카탈로그가 압니다.
- 번들 구성은 에셋마다 번들 이름을 적는 대신 그룹 설정으로 정합니다.
- 에디터 기본 모드 Use Asset Database 는 Include in Build 를 보지 않아서, 에디터에서만 되는 로드가 생깁니다. 출시 전에는 Use Existing Build 로 확인합니다.