---
title: "AssetBundle 의 의존성은 직접 열어야 따라온다"
description: "AssetBundle 은 프리팹의 머티리얼이 다른 번들에 있으면 그 번들을 직접 열기 전까지 머티리얼이 비어 마젠타로 그립니다. 나중에 열면 끊긴 참조가 이어지지만, AssetBundleManifest 로 의존 번들을 먼저 여는 순서를 지킵니다."
author: "유니얼"
pubDate: 2026-10-19
category: "game-dev/unity/runtime"
tags: ["Unity", "메모리"]
canonical: https://blog.unialgames.com/posts/unity-assetbundle-dependency/
---
<x-repo>
<script type="application/json">
{
  "repo": "SeoBYP/AssetBundleStudy",
  "text": "AssetBundle 과 Addressables 의 문서 내용을 Windows Player 에서 직접 재서 확인한 기록입니다.",
  "chips": ["Unity 6000.6.3f1", "URP 17.6.0", "Windows Player"],
  "links": [
    { "text": "결과 AB6", "path": "Measure/results/AB6.txt" },
    { "text": "스크린샷", "path": "Measure/results" }
  ]
}
</script>
</x-repo>

프리팹 번들을 열어 `Instantiate` 했더니 오브젝트가 분홍색으로 나옵니다. 에러 로그는 없습니다. 에디터에서는 멀쩡하던 것이 번들에서 꺼내니 머티리얼이 빠진 것입니다.

AssetBundle 에서 **다른 번들에 있는 에셋은 그 번들을 직접 열어야 따라옵니다.** 엔진은 의존 관계를 알지만 대신 열어 주지 않습니다. 그 목록은 빌드가 남긴 `AssetBundleManifest` 에 있습니다.

## 다른 번들의 에셋은 그 번들이 열려야 이어진다

Cube 프리팹은 `cubea` 번들에, Cube 가 쓰는 머티리얼과 셰이더는 `mat` 번들에 나눠 넣었습니다. `cubea` 의 프리팹은 머티리얼을 직접 담지 않고 "`mat` 번들 안의 그 오브젝트" 를 가리키기만 합니다. 그 상태에서 `cubea` 만 열고 Cube 를 만든 뒤, 2프레임 뒤에 `mat` 을 열었습니다.

<x-wiring>
<script type="application/json">
{
  "label": "의존 번들을 나중에 열 때",
  "width": 560,
  "height": 470,
  "nodes": [
    { "id": "cubea", "title": "cubea 번들", "sub": "Cube.prefab", "x": 170, "y": 10, "w": 220 },
    { "id": "inst", "title": "Cube 인스턴스", "sub": "sharedMaterial = null, 마젠타", "x": 170, "y": 130, "w": 220 },
    { "id": "mat", "title": "mat 번들을 연다", "sub": "Lit.mat, Lit.shader", "x": 170, "y": 250, "w": 220 },
    { "id": "fixed", "title": "같은 인스턴스", "sub": "Lit 으로 그려진다", "x": 170, "y": 375, "w": 220 }
  ],
  "edges": [
    { "from": "cubea", "to": "inst", "label": "LoadAsset + Instantiate" },
    { "from": "inst", "to": "mat", "label": "2프레임 뒤" },
    { "from": "mat", "to": "fixed", "label": "참조가 이어진다" }
  ],
  "steps": [
    { "edge": 0, "why": "cubea 만 열고 Cube 를 만듭니다. 머티리얼이 가리키는 mat 번들은 아직 안 열렸습니다." },
    { "node": "inst", "why": "머티리얼 슬롯이 비어 있어 마젠타로 그려집니다. 경고는 없습니다." },
    { "edge": 1, "why": "mat 번들을 엽니다." },
    { "edge": 2, "why": "새로 만들지 않았는데, 이미 있던 인스턴스의 머티리얼 슬롯이 Lit 을 가리키고 정상으로 그려집니다." }
  ],
  "note": "끊긴 참조는 빈 채로 굳지 않습니다. 대상 번들이 열리는 순간 이어집니다."
}
</script>
</x-wiring>

| 단계 | 인스턴스의 머티리얼 | 화면 가운데 픽셀 |
| --- | --- | --- |
| `cubea` 만 열고 `Instantiate` | `null` | rgb(209,0,209) 마젠타 |
| `mat` 번들 연 뒤 | `Lit` | rgb(140,133,129) 정상 |
| 그 뒤 새로 `Instantiate` | 같은 `Lit` | 정상 |

**이미 만든 인스턴스도 나중에 이어졌습니다.** 프리팹을 다시 `LoadAsset` 하면 처음과 같은 객체가 돌아왔고, 그 프리팹의 머티리얼도 채워져 있었습니다. 끊긴 참조는 "대상이 없음" 으로 굳는 게 아니라 "아직 안 열린 번들의 그 오브젝트" 를 계속 가리키고 있다가, 번들이 열리면 이어집니다. 화면 결과는 GPU Resident Drawer 를 켜든 끄든 같았습니다.

그래도 `mat` 이 열리기 전 프레임에는 마젠타가 화면에 나갑니다. 고쳐진다고 순서를 무시해도 되는 것은 아닙니다.

## 의존 목록은 AssetBundleManifest 에 있다

번들을 빌드하면 출력 폴더 이름과 같은 **매니페스트 번들**이 하나 더 나옵니다. 그 안의 `AssetBundleManifest` 가 번들마다 기대는 번들 목록을 갖고 있습니다. 이 프로젝트에서 `cubea` 의 의존 목록은 `mat` 하나였습니다.

```csharp
// 매니페스트 번들은 출력 폴더 이름과 같은 이름으로 나온다
AssetBundle manifestBundle = AssetBundle.LoadFromFile(Path.Combine(root, "Bundles"));
var manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

// 기대는 번들을 먼저, 끝까지 따라가서 연다
foreach (string dep in manifest.GetAllDependencies("cubea"))
    Open(dep);
AssetBundle cube = Open("cubea");
GameObject prefab = cube.LoadAsset<GameObject>("Cube");
```

`GetAllDependencies` 는 의존의 의존까지 펼친 목록이고, `GetDirectDependencies` 는 바로 기대는 것만 줍니다. 열린 번들을 다시 `LoadFromFile` 하면 에러가 나니, 이미 연 번들은 이름으로 기억해 두고 다시 쓰는 도우미를 하나 둡니다.

```csharp
readonly Dictionary<string, AssetBundle> m_Opened = new Dictionary<string, AssetBundle>();

AssetBundle Open(string name)
{
    if (!m_Opened.TryGetValue(name, out AssetBundle bundle))
    {
        bundle = AssetBundle.LoadFromFile(Path.Combine(root, name));
        m_Opened[name] = bundle;
    }
    return bundle;
}
```

여기서 AssetBundle 의 단위가 드러납니다. 의존은 **번들 → 번들**입니다. Cube 하나가 머티리얼 하나를 쓰더라도 `mat` 번들 전체를 열고, 내릴 때도 번들째입니다. 이 도우미에 "누가 이 번들을 쓰는지" 를 세는 기능까지 붙이면, 그것이 Addressables 가 대신 해 주는 일입니다.

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

`cubea` 와 `mat` 을 나눠 빌드하고, `mat` 을 늦게 열어 이미 만든 인스턴스가 바뀌는지 봅니다.

```csharp
// 에디터: Cube 와 그 머티리얼을 다른 번들에 넣는다
var builds = new[]
{
    new AssetBundleBuild { assetBundleName = "cubea", assetNames = new[] { "Assets/Prefab/Cube.prefab" } },
    new AssetBundleBuild { assetBundleName = "mat", assetNames = new[] { "Packages/com.unity.render-pipelines.universal/Runtime/Materials/Lit.mat" } },
};
var manifest = BuildPipeline.BuildAssetBundles("Builds/Bundles", builds,
    BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.StandaloneWindows64);
Debug.Log(string.Join(", ", manifest.GetAllDependencies("cubea")));   // mat
```

```csharp
// Player: 의존 번들 없이 만든 뒤, 늦게 연다
IEnumerator LateDependency(string root)
{
    AssetBundle cube = AssetBundle.LoadFromFile(Path.Combine(root, "cubea"));
    GameObject go = Instantiate(cube.LoadAsset<GameObject>("Cube"));
    yield return null;
    Debug.Log($"mat 전: {go.GetComponent<Renderer>().sharedMaterial}");    // null

    AssetBundle mat = AssetBundle.LoadFromFile(Path.Combine(root, "mat"));
    yield return null;
    yield return null;
    Debug.Log($"mat 후: {go.GetComponent<Renderer>().sharedMaterial}");    // Lit
}
```

Cube 가 쓰는 머티리얼이 프로젝트의 어느 파일인지는 프리팹의 머티리얼 GUID 로 확인합니다. URP 기본 큐브는 `Assets` 안의 머티리얼이 아니라 URP 패키지의 `Lit.mat` 을 쓰는 경우가 있어서, 다른 파일을 `mat` 번들에 넣으면 의존이 생기지 않습니다.

## 주의할 점

### 1. 의존 번들을 먼저 연다

**안 지키면 생기는 문제.** 의존 번들이 열리기 전에 만든 오브젝트는 그 사이 마젠타로 그려집니다. 나중에 이어지긴 하지만 한두 프레임이라도 화면에 나갑니다.

**이렇게 쓴다.** `GetAllDependencies` 로 받은 번들을 전부 연 뒤에 본 번들에서 에셋을 꺼냅니다.

### 2. 같은 번들을 두 번 열지 않게 기억해 둔다

**안 지키면 생기는 문제.** 이미 열린 번들을 `LoadFromFile` 로 다시 열면 실패합니다. 여러 번들이 같은 의존 번들을 기대면 이 일이 쉽게 생깁니다.

**이렇게 쓴다.** 연 번들을 이름으로 담아 두는 사전을 두고, 열기 전에 먼저 찾아봅니다. 내릴 때도 그 사전을 거쳐서, 다른 번들이 아직 기대고 있는지 확인합니다.

## 정리

AssetBundle 의 의존성은 **번들 → 번들이고, 쓰는 쪽이 직접 열어야 따라옵니다.**

- 의존 번들 없이 만든 오브젝트는 머티리얼이 비어 마젠타로 그려지고, 경고는 없습니다.
- 나중에 의존 번들을 열면 이미 만든 인스턴스의 참조도 이어집니다. 그래도 그 전 프레임은 화면에 나갑니다.
- `AssetBundleManifest.GetAllDependencies` 로 기대는 번들을 먼저 열고, 연 번들은 이름으로 기억해 둡니다.