---
title: "AssetBundle 의 Unload 는 어느 쪽이든 사고가 난다"
description: "AssetBundle.Unload(false) 뒤 같은 번들을 다시 열면 옛 객체와 새 객체가 메모리에 같이 있고, 옛 것은 UnloadUnusedAssets 로만 치워집니다. Unload(true) 를 쓰는 중에 부르면 인스턴스의 머티리얼이 경고 없이 사라집니다."
author: "유니얼"
pubDate: 2026-10-19
category: "game-dev/unity/runtime"
tags: ["Unity", "메모리"]
canonical: https://blog.unialgames.com/posts/unity-assetbundle-unload/
---
<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": "결과 AB8 · AB9", "path": "Measure/results" },
    { "text": "측정 코드", "path": "Assets/Measure/MeasureRunner.cs" }
  ],
  "note": "화면 결과는 GPU Resident Drawer 가 켜진 URP 기본 설정에서 잰 것이고, 끈 상태도 같이 쟀습니다."
}
</script>
</x-repo>

번들을 다 썼으면 `AssetBundle.Unload` 로 내립니다. 인자는 `bool` 하나뿐인데, `false` 를 고르면 메모리가 안 줄거나 오히려 늘고, `true` 를 고르면 화면에서 물건이 사라집니다.

`Unload` 의 인자는 **번들에서 꺼낸 에셋까지 같이 지울지**를 정합니다. `false` 는 번들과 에셋 사이의 연결만 끊고 에셋은 남겨서 다시 열면 사본이 생기고, `true` 는 에셋을 쓰는 곳이 있든 없든 지웁니다. 어느 쪽도 "아무도 안 쓰면 내린다" 를 해 주지 않습니다.

## Unload(false) 는 연결만 끊어서 다시 열면 사본이 생긴다

`Unload(false)` 는 번들 파일과 그 헤더는 내리지만, 번들에서 꺼낸 머티리얼이나 텍스처는 메모리에 남겨 둡니다. 쓰고 있는 오브젝트가 깨지지 않으니 안전해 보입니다. 문제는 그 뒤에 **같은 번들을 다시 열 때**입니다.

<x-wiring>
<script type="application/json">
{
  "label": "Unload(false) 뒤 같은 번들을 다시 열면",
  "width": 560,
  "height": 600,
  "nodes": [
    { "id": "b1", "title": "mat 번들", "sub": "처음 연 것", "x": 170, "y": 10, "w": 220 },
    { "id": "m1", "title": "Lit (ID 2683)", "sub": "go1 이 쓰는 중", "x": 170, "y": 125, "w": 220 },
    { "id": "cut", "title": "Unload(false)", "sub": "번들만 내려가고 Lit 은 남음", "x": 170, "y": 240, "w": 220 },
    { "id": "b2", "title": "mat 번들", "sub": "다시 연 것", "x": 170, "y": 355, "w": 220 },
    { "id": "m2", "title": "Lit (ID 2687)", "sub": "go2 가 쓰는 새 사본", "x": 170, "y": 475, "w": 220 }
  ],
  "edges": [
    { "from": "b1", "to": "m1", "label": "LoadAsset" },
    { "from": "m1", "to": "cut", "label": "연결이 끊긴다" },
    { "from": "cut", "to": "b2", "label": "같은 파일을 다시 연다" },
    { "from": "b2", "to": "m2", "label": "LoadAsset" }
  ],
  "steps": [
    { "edge": 0, "why": "번들을 열고 Cube 를 만들었습니다. Lit 머티리얼 하나가 올라와 있습니다." },
    { "edge": 1, "why": "Unload(false) 로 번들을 내립니다. Lit 은 남아 있고, 이제 어느 번들의 것도 아닙니다." },
    { "edge": 2, "why": "같은 번들을 다시 엽니다. 번들은 남아 있는 Lit 을 모릅니다." },
    { "edge": 3, "why": "새 Cube 를 만들면 Lit 을 번들에서 새로 읽습니다. 같은 에셋이 ID 가 다른 두 객체가 됩니다." }
  ],
  "note": "옛 Lit 은 go1 이 사라지고 UnloadUnusedAssets 를 불러야 내려갑니다."
}
</script>
</x-wiring>

| 단계 | 메모리의 `Lit` 머티리얼 | native 증가 |
| --- | --- | --- |
| 열고 go1 생성 | 1개 (2683) | +5.54 MiB |
| 두 번들 `Unload(false)` | 1개 (2683), go1 정상 | |
| 다시 열고 go2 생성 | **2개 (2683, 2687)**, 셰이더도 2개 | +7.10 MiB |
| go1 파괴 후 `UnloadUnusedAssets` | 1개 (2687) | |

2017.4 매뉴얼은 이 경우를 그대로 적어 두었습니다. `Unload(false)` 뒤 다시 로드하면 기존 사본에 다시 연결하지 않고 사본이 두 개가 된다는 것입니다. 지금 매뉴얼에는 "중복을 막으려면 `Unload(true)` 를 써라" 까지만 남아 있습니다.

남은 옛 사본은 번들 쪽에서는 더 이상 내릴 방법이 없습니다. 쓰는 오브젝트를 없앤 뒤 `Resources.UnloadUnusedAssets()` 를 부르거나, 씬을 Single 로 바꿔서 그 안에서 불리게 해야 정리됩니다.

## Unload(true) 는 쓰는 중이어도 지운다

`Unload(true)` 는 번들에서 꺼낸 에셋까지 지웁니다. 사본이 생길 일이 없으니 매뉴얼이 권하는 쪽입니다. 대신 그 에셋을 쓰는 오브젝트가 아직 있어도 묻지 않고 지웁니다. Cube 인스턴스를 그대로 둔 채 머티리얼이 든 `mat` 번들을 `Unload(true)` 했습니다.

<x-compare>
<script type="application/json">
{
  "label": "쓰는 중에 mat 번들을 Unload(true) 한 뒤 (GPU Resident Drawer)",
  "tabs": [
    { "label": "켬 (기본)", "text": "큐브가 그려지지 않습니다. 그 자리에 배경이 보입니다.", "stats": [["인스턴스", "살아 있음"], ["sharedMaterial", "null"], ["가운데 픽셀", "rgb(26,26,26) 배경"]], "tone": "bad" },
    { "label": "끔", "text": "머티리얼이 없을 때의 마젠타로 그려집니다.", "stats": [["인스턴스", "살아 있음"], ["sharedMaterial", "null"], ["가운데 픽셀", "rgb(209,0,209) 마젠타"]], "tone": "bad" }
  ]
}
</script>
</x-compare>

머티리얼은 메모리에서 0개가 됐고, 인스턴스의 머티리얼 슬롯은 `null` 이 됐습니다. 이어서 프리팹이 든 `cubea` 번들까지 `Unload(true)` 해도 인스턴스와 메시는 살아 있었습니다. 큐브 메시는 엔진 내장 리소스라 번들에 없기 때문입니다. **사라진 원인은 머티리얼입니다.**

그리는 결과는 URP 의 GPU Resident Drawer 설정에 따라 달랐습니다. 꺼져 있으면 마젠타로 그리고, 켜져 있으면 아예 그리지 않았습니다. 내부 이유는 확인하지 못했습니다. 어느 쪽이든 **경고와 에러 로그는 하나도 없었습니다.** 측정 코드가 Warning 이상의 로그를 전부 받도록 걸어 두었고, Player 로그에도 없었습니다.

## UnloadUnusedAssets 는 씬 전체를 훑는다

두 경우 모두 마지막 정리는 `Resources.UnloadUnusedAssets()` 입니다. 이 함수는 메모리의 에셋을 누가 쓰는지 전부 따라가 보고, 아무도 안 쓰는 것을 내립니다. 그래서 비용이 씬의 오브젝트 수에 따라 늡니다. 비활성 GameObject 를 늘려 가며 호출부터 완료까지 쟀습니다.

| 씬 GameObject | 세 번 잰 시간 |
| --- | --- |
| 1 | 2.47 / 1.68 / 1.71 ms |
| 1,000 | 1.75 / 1.57 / 1.60 ms |
| 10,000 | 5.20 / 3.92 / 3.73 ms |
| 50,000 | 32.96 / 24.66 / 21.97 ms |

1,000개까지는 차이가 없고 1만 개부터 늘어서, 5만 개에서는 한 프레임 예산(16.7 ms)을 넘었습니다. 게임 도중 아무 때나 부를 함수가 아닙니다.

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

`Unload(false)` 뒤 사본이 생기는 것과, `Unload(true)` 가 쓰는 중인 머티리얼을 지우는 것을 차례로 봅니다. `cubea` 번들은 `mat` 번들에 기댑니다.

```csharp
IEnumerator UnloadFalseThenReload(string root)
{
    AssetBundle mat = AssetBundle.LoadFromFile(Path.Combine(root, "mat"));
    AssetBundle cube = AssetBundle.LoadFromFile(Path.Combine(root, "cubea"));
    GameObject go1 = Instantiate(cube.LoadAsset<GameObject>("Cube"));
    yield return null;

    mat.Unload(false);
    cube.Unload(false);

    mat = AssetBundle.LoadFromFile(Path.Combine(root, "mat"));
    cube = AssetBundle.LoadFromFile(Path.Combine(root, "cubea"));
    GameObject go2 = Instantiate(cube.LoadAsset<GameObject>("Cube"));
    yield return null;

    var lits = Resources.FindObjectsOfTypeAll<Material>().Where(m => m.name == "Lit").ToArray();
    Debug.Log($"Lit {lits.Length}개: {string.Join(", ", lits.Select(m => m.GetInstanceID()))}");   // 2개
}

IEnumerator UnloadTrueWhileInUse(string root)
{
    AssetBundle mat = AssetBundle.LoadFromFile(Path.Combine(root, "mat"));
    AssetBundle cube = AssetBundle.LoadFromFile(Path.Combine(root, "cubea"));
    GameObject go = Instantiate(cube.LoadAsset<GameObject>("Cube"));
    yield return null;

    mat.Unload(true);
    yield return null;
    yield return null;
    Debug.Log($"살아있음:{go != null}, 머티리얼:{go.GetComponent<Renderer>().sharedMaterial}");   // True, null
}
```

두 함수는 각자 새로 실행한 Player 에서 돌립니다. 앞 함수가 남긴 사본이 뒤 함수의 결과에 섞이지 않게 하려는 것입니다.

## 주의할 점

### 1. Unload(false) 한 번들을 다시 열지 않는다

**안 지키면 생기는 문제.** 남겨 둔 에셋과 새로 읽은 에셋이 따로 올라와 같은 머티리얼과 셰이더가 두 벌이 됩니다. 옛 것은 번들로는 못 내리고 `UnloadUnusedAssets` 를 기다려야 합니다.

**이렇게 쓴다.** `Unload(false)` 는 그 번들을 다시 열지 않을 때만 씁니다. 다시 열 수 있는 번들은 쓰는 오브젝트를 다 치운 뒤 `Unload(true)` 로 내립니다.

### 2. Unload(true) 전에 그 번들의 에셋을 쓰는 오브젝트를 치운다

**안 지키면 생기는 문제.** 인스턴스가 남아 있으면 머티리얼만 지워져, 화면에서 사라지거나 마젠타가 됩니다. 로그가 없어서 어디서 지웠는지 찾기 어렵습니다.

**이렇게 쓴다.** 번들마다 그 번들에서 만든 인스턴스를 기록해 두고, 전부 `Destroy` 한 뒤 `Unload(true)` 합니다. 다른 번들이 기대는 공용 번들은 그 번들들이 다 내려간 뒤에 내립니다.

### 3. UnloadUnusedAssets 는 로딩 화면에서 부른다

**안 지키면 생기는 문제.** 씬에 오브젝트가 많으면 한 번에 수십 ms 가 걸려 게임 중에 프레임이 튑니다.

**이렇게 쓴다.** 씬 전환이나 로딩 화면처럼 멈춰도 되는 자리에서 부르고, 끝날 때까지 기다린 뒤 다음으로 넘어갑니다.

## 정리

AssetBundle 의 `Unload` 는 **에셋을 같이 지울지만 정하고, 아무도 안 쓰는지는 확인해 주지 않습니다.**

- `Unload(false)` 뒤 같은 번들을 다시 열면 옛 객체와 새 객체가 같이 있고, 옛 것은 `UnloadUnusedAssets` 로만 내려갑니다.
- `Unload(true)` 는 쓰는 중이어도 지워서, 인스턴스의 머티리얼이 경고 없이 `null` 이 됩니다.
- 정리 수단인 `UnloadUnusedAssets` 는 씬 전체를 훑어서, 오브젝트 5만 개에서 한 프레임을 넘겼습니다.