---
title: "AssetBundle 은 배정하지 않은 공용 에셋을 번들마다 복사한다"
description: "두 번들이 같은 머티리얼을 쓰는데 그 머티리얼을 어느 번들에도 넣지 않으면, AssetBundle 은 번들마다 따로 복사하고 둘을 열면 메모리에도 두 벌이 올라옵니다. 공용 에셋을 공용 번들에 명시하면 한 벌로 줄어듭니다."
author: "유니얼"
pubDate: 2026-10-19
category: "game-dev/unity/runtime"
tags: ["Unity", "메모리", "최적화"]
canonical: https://blog.unialgames.com/posts/unity-assetbundle-duplicate/
---
<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": "결과 AB7", "path": "Measure/results/AB7.txt" },
    { "text": "번들 크기", "path": "Measure/results/bundles.txt" }
  ],
  "note": "메모리 증가는 한 번 잰 값입니다."
}
</script>
</x-repo>

적 프리팹 두 종류를 각자 번들로 만들었더니, 프리팹은 몇 KB 짜리인데 번들이 수백 KB 씩 나옵니다. 두 적을 같이 띄우면 같은 머티리얼이 메모리에 두 개 있습니다. 어디에도 경고는 없습니다.

AssetBundle 은 **번들에 넣으라고 적은 에셋만 그 번들의 것으로 칩니다.** 적지 않은 에셋이 참조로 딸려 오면 그 번들 안에 복사해 넣고, 같은 에셋을 쓰는 번들이 여럿이면 번들마다 따로 복사합니다.

## 적지 않은 에셋은 번들마다 따라 들어간다

매뉴얼은 이렇게 적습니다. 같은 에셋을 여러 번들이 참조하면 "그 내용을 참조하는 각 번들 안에 복사한다". 복사된 것들은 서로 다른 오브젝트로 취급되고, 두 번들을 다 열면 **메모리에도 두 사본**이 올라옵니다.

Cube 와 CubeB 는 같은 URP `Lit.mat` 을 씁니다. 머티리얼을 어디에도 적지 않은 경우와, 머티리얼만 `mat` 번들로 따로 적은 경우를 같이 빌드했습니다.

<x-tabs>
<x-wiring data-tab="머티리얼을 안 적었을 때">
<script type="application/json">
{
  "label": "두 번들이 같은 머티리얼을 쓸 때",
  "width": 560,
  "height": 320,
  "nodes": [
    { "id": "a", "title": "cubea 번들", "sub": "265.6 KiB", "x": 20, "y": 10, "w": 220 },
    { "id": "b", "title": "cubeb 번들", "sub": "265.6 KiB", "x": 320, "y": 10, "w": 220 },
    { "id": "ma", "title": "Lit.mat + Lit.shader", "sub": "사본 1", "x": 20, "y": 140, "w": 220 },
    { "id": "mb", "title": "Lit.mat + Lit.shader", "sub": "사본 2", "x": 320, "y": 140, "w": 220 },
    { "id": "mem", "title": "메모리", "sub": "머티리얼 2개, 셰이더 2개", "x": 170, "y": 255, "w": 220 }
  ],
  "edges": [
    { "from": "a", "to": "ma", "label": "안에 복사" },
    { "from": "b", "to": "mb", "label": "안에 복사" },
    { "from": "ma", "to": "mem" },
    { "from": "mb", "to": "mem" }
  ],
  "steps": [
    { "edge": 0, "why": "cubea 를 빌드하면서 Cube 가 쓰는 머티리얼과 셰이더를 번들 안에 넣습니다." },
    { "edge": 1, "why": "cubeb 도 같은 머티리얼과 셰이더를 자기 안에 따로 넣습니다." },
    { "edge": 2, "why": "cubea 를 열면 사본 1 이 올라옵니다." },
    { "edge": 3, "why": "cubeb 를 열면 사본 2 가 따로 올라옵니다. 같은 에셋인데 다른 객체입니다." }
  ],
  "note": "두 인스턴스의 sharedMaterial 을 비교하면 false 입니다."
}
</script>
</x-wiring>
<x-wiring data-tab="mat 번들로 적었을 때">
<script type="application/json">
{
  "label": "두 번들이 같은 머티리얼을 쓸 때",
  "width": 560,
  "height": 320,
  "nodes": [
    { "id": "a", "title": "cubea 번들", "sub": "3.9 KiB", "x": 20, "y": 10, "w": 220 },
    { "id": "b", "title": "cubeb 번들", "sub": "4.0 KiB", "x": 320, "y": 10, "w": 220 },
    { "id": "m", "title": "mat 번들", "sub": "Lit.mat + Lit.shader, 262.6 KiB", "x": 170, "y": 140, "w": 220 },
    { "id": "mem", "title": "메모리", "sub": "머티리얼 1개, 셰이더 1개", "x": 170, "y": 255, "w": 220 }
  ],
  "edges": [
    { "from": "a", "to": "m", "label": "가리킨다" },
    { "from": "b", "to": "m", "label": "가리킨다" },
    { "from": "m", "to": "mem" }
  ],
  "steps": [
    { "edge": 0, "why": "cubea 는 머티리얼을 담지 않고 mat 번들의 것을 가리키기만 합니다." },
    { "edge": 1, "why": "cubeb 도 같은 것을 가리킵니다." },
    { "edge": 2, "why": "mat 번들을 한 번 열면 두 큐브가 같은 객체를 씁니다." }
  ],
  "note": "두 인스턴스의 sharedMaterial 을 비교하면 true 입니다."
}
</script>
</x-wiring>
</x-tabs>

| 구성 | 프리팹 번들 크기 | 메모리의 머티리얼 · 셰이더 | native 증가 |
| --- | --- | --- | --- |
| 안 적음 | 265.6 KiB 씩 | 2개 · 2개 | +7.70 MiB |
| `mat` 번들로 적음 | 3.9 · 4.0 KiB | 1개 · 1개 | +5.56 MiB |

번들 크기의 대부분은 머티리얼이 아니라 같이 끌려온 **셰이더**였습니다. `mat` 번들 하나가 262.6 KiB 인데 프리팹 번들은 4 KiB 안쪽이 됐습니다. 셰이더처럼 패키지에 든 에셋도 이렇게 복사 대상입니다.

## 번들 API 로는 복사본이 안 보인다

이 중복은 번들을 열어 봐서는 알아채기 어렵습니다. `GetAllAssetNames()` 는 번들에 넣으라고 적은 에셋만 돌려줍니다. 안 적었을 때의 `cubea` 도 목록에는 `cube.prefab` 하나뿐이었고, 그 안에 복사된 머티리얼과 셰이더는 이름이 안 나옵니다.

그래서 중복은 빌드 쪽에서 찾습니다. 매뉴얼은 Build Report Inspector 로 번들의 중복을 찾을 수 있다고 안내하고, Addressables 를 쓰면 Analyze 창이 같은 일을 합니다. 번들 크기가 프리팹 크기와 안 맞게 크면 그것부터가 신호입니다.

## 공용 에셋을 공용 번들에 적는다

막는 방법은 하나입니다. **여러 번들이 같이 쓰는 에셋을 따로 번들에 적어 줍니다.** 그러면 그 에셋은 그 번들의 것이 되고, 쓰는 쪽 번들은 가리키기만 합니다.

```csharp
var builds = new[]
{
    new AssetBundleBuild { assetBundleName = "cubea", assetNames = new[] { "Assets/Prefab/Cube.prefab" } },
    new AssetBundleBuild { assetBundleName = "cubeb", assetNames = new[] { "Assets/Prefab/CubeB.prefab" } },
    // 두 큐브가 같이 쓰는 머티리얼을 따로 적는다. 셰이더도 이 번들로 따라온다
    new AssetBundleBuild { assetBundleName = "mat", assetNames = new[] { "Packages/com.unity.render-pipelines.universal/Runtime/Materials/Lit.mat" } },
};
```

대가는 의존 번들이 하나 생긴다는 것입니다. 이제 `cubea` 를 쓰려면 `mat` 을 먼저 열어야 하고, `mat` 은 두 큐브가 다 내려갈 때까지 열어 둬야 합니다. 매뉴얼이 "중복을 피하려고만 존재하는 번들" 이라고 부르는 것이 이 `mat` 번들입니다.

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

두 구성을 각자 폴더로 빌드하고, 두 큐브를 띄운 뒤 메모리에 있는 같은 이름의 머티리얼 수를 셉니다.

```csharp
IEnumerator CountCopies(string dir)
{
    AssetBundle a = AssetBundle.LoadFromFile(Path.Combine(dir, "cubea"));
    AssetBundle b = AssetBundle.LoadFromFile(Path.Combine(dir, "cubeb"));
    string matPath = Path.Combine(dir, "mat");
    AssetBundle mat = File.Exists(matPath) ? AssetBundle.LoadFromFile(matPath) : null;

    GameObject ga = Instantiate(a.LoadAsset<GameObject>("Cube"));
    GameObject gb = Instantiate(b.LoadAsset<GameObject>("CubeB"));
    yield return null;

    int count = Resources.FindObjectsOfTypeAll<Material>().Count(m => m.name == "Lit");
    bool same = ga.GetComponent<Renderer>().sharedMaterial == gb.GetComponent<Renderer>().sharedMaterial;
    Debug.Log($"{dir}: Lit 머티리얼 {count}개, 같은 객체 {same}");
    Debug.Log($"cubea 목록: {string.Join(", ", a.GetAllAssetNames())}");
}
```

안 적은 쪽에서 `Lit` 2개와 `false`, 적은 쪽에서 1개와 `true` 가 나오면 같은 결과입니다. 목록에는 두 경우 모두 프리팹만 나옵니다.

## 주의할 점

### 1. 여러 번들이 같이 쓰는 에셋은 따로 번들에 적는다

**안 지키면 생기는 문제.** 그 에셋을 쓰는 번들마다 사본이 들어가 다운로드 크기가 늘고, 여러 번들을 같이 열면 메모리에도 같은 에셋이 여러 벌 올라옵니다.

**이렇게 쓴다.** 셰이더, 공용 머티리얼, 공용 텍스처처럼 여러 곳에서 쓰는 것은 공용 번들에 명시합니다. 공용 번들은 그것을 쓰는 번들보다 먼저 열고 나중에 내립니다.

### 2. 패키지와 엔진 쪽 에셋도 확인한다

**안 지키면 생기는 문제.** URP 의 `Lit.shader` 처럼 내가 만들지 않은 에셋도 참조로 딸려 와 복사됩니다. 프로젝트 폴더만 보고 공용 번들을 짜면 가장 큰 중복을 놓칩니다.

**이렇게 쓴다.** 프리팹이 실제로 가리키는 머티리얼과 셰이더의 경로를 확인하고, `Packages/` 아래 것도 공용 번들 대상에 넣습니다.

### 3. 번들 목록이 아니라 빌드 결과로 중복을 찾는다

**안 지키면 생기는 문제.** `GetAllAssetNames()` 에는 복사된 에셋이 안 나와서, 런타임 코드로 확인하면 중복이 없는 것처럼 보입니다.

**이렇게 쓴다.** 빌드 뒤 Build Report Inspector 나 번들 크기로 확인합니다. 프리팹 하나짜리 번들이 수백 KiB 면 무엇이 딸려 왔는지부터 봅니다.

## 정리

AssetBundle 은 **번들에 적지 않은 공용 에셋을 그 에셋을 쓰는 번들마다 복사합니다.**

- 두 큐브가 같은 머티리얼을 쓸 때 안 적으면 번들 하나가 265.6 KiB, 메모리에 머티리얼과 셰이더가 2개씩 올라왔습니다.
- `mat` 번들로 적으면 프리팹 번들은 4 KiB 안쪽, 메모리에 1개씩이고 두 큐브가 같은 객체를 씁니다.
- 복사본은 `GetAllAssetNames()` 에 안 보입니다. 빌드 리포트로 찾고, 셰이더 같은 패키지 에셋도 공용 번들에 적습니다.