---
title: "Addressables 는 번들 단위로 참조를 센다"
description: "Addressables 는 에셋마다 참조를 세지만 실제로 내리는 단위는 번들이라, 같은 번들의 다른 에셋이 살아 있으면 Release 한 에셋도 남습니다. Object.Instantiate 인스턴스는 세지 않고, InstantiateAsync 를 Destroy 만 하면 씬이 내려갈 때까지 번들이 열려 있습니다."
author: "유니얼"
pubDate: 2026-10-19
category: "game-dev/unity/runtime"
tags: ["Unity", "메모리"]
canonical: https://blog.unialgames.com/posts/unity-addressables-refcount/
---
<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": "결과 AD4 · AD6", "path": "Measure/results" },
    { "text": "측정 코드", "path": "Assets/Measure/MeasureRunner.cs" }
  ],
  "note": "한 번 잰 값입니다. 화면 결과는 GPU Resident Drawer 가 켜진 URP 기본 설정 기준이고 끈 상태도 같이 쟀습니다."
}
</script>
</x-repo>

`LoadAssetAsync` 와 `Release` 짝을 꼼꼼히 맞췄는데 메모리 프로파일러에 놓은 텍스처가 그대로 있습니다. 반대로 핸들을 놓자 아직 화면에 있던 오브젝트가 사라지기도 합니다.

Addressables 는 **핸들마다 참조를 세지만, 실제로 메모리에서 내리는 단위는 번들**입니다. 그리고 셈에 들어가는 것은 Addressables 를 거친 로드와 인스턴스뿐입니다.

## Release 한 에셋도 번들이 열려 있으면 남는다

Addressables 매뉴얼은 이렇게 적습니다. 에셋을 놓아도 바로 메모리에서 빠지지 않고, **"AssetBundle 의 일부만 내릴 수는 없다"**. 번들의 참조가 0 이 되어 번들이 내려갈 때까지 그 번들의 에셋은 남습니다.

Pack Together 그룹 하나에 큰 텍스처 A(2048², GPU 에서 16 MiB)와 작은 텍스처 B 를 넣고, 둘을 로드한 뒤 A 만 놓았습니다.

<x-wiring>
<script type="application/json">
{
  "label": "같은 번들의 A 만 Release 할 때",
  "width": 560,
  "height": 340,
  "nodes": [
    { "id": "ha", "title": "A 핸들", "x": 20, "y": 10, "w": 220 },
    { "id": "hb", "title": "B 핸들", "x": 320, "y": 10, "w": 220 },
    { "id": "bundle", "title": "AD4 번들", "sub": "A + B, 하나로 묶임", "x": 170, "y": 135, "w": 220 },
    { "id": "gpu", "title": "GPU 메모리", "sub": "A 의 16 MiB", "x": 170, "y": 265, "w": 220 }
  ],
  "edges": [
    { "from": "ha", "to": "bundle", "label": "참조 1" },
    { "from": "hb", "to": "bundle", "label": "참조 1" },
    { "from": "bundle", "to": "gpu" }
  ],
  "steps": [
    { "edge": 0, "why": "A 를 로드합니다. 번들이 열리고 A 가 GPU 에 올라갑니다." },
    { "edge": 1, "why": "B 를 로드합니다. 같은 번들이라 번들 참조만 하나 늘어납니다." },
    { "node": "ha", "why": "A 를 Release 합니다. A 의 참조는 0 이 됐지만 번들은 B 가 쥐고 있습니다." },
    { "edge": 2, "why": "번들이 열려 있으니 A 의 16 MiB 는 그대로입니다. B 를 놓아 번들이 내려가야 같이 빠집니다." }
  ],
  "note": "에셋의 참조가 0 이어도, 그 에셋이 든 번들의 참조가 남아 있으면 메모리에서 안 빠집니다."
}
</script>
</x-wiring>

| 단계 | A | GPU 증가 | 열린 번들 |
| --- | --- | --- | --- |
| A, B 로드 | 있음 | +16.02 MiB | 1 |
| A `Release` | **있음** | **+16.02 MiB** | 1 |
| `UnloadUnusedAssets` | 없음 | +0.02 MiB | 1 |
| B `Release` | 없음 | 0 | 0 |

A 를 놓아도 B 를 놓기 전까지 16 MiB 가 남았습니다. 이때 `Resources.UnloadUnusedAssets()` 를 부르면 **번들은 열린 채 A 만 내려갔습니다.** 메모리 페이지에는 없는 동작입니다. 번들이 열려 있으니 A 를 다시 로드하면 번들에서 다시 읽어 왔습니다(8.9 ms).

그래서 놓은 에셋을 바로 빼려면 같은 번들의 다른 에셋까지 놓거나, `UnloadUnusedAssets` 를 불러야 합니다. 뒤쪽은 씬 전체를 훑는 비용이 있어 아무 때나 부를 수는 없습니다. 근본적인 답은 **수명이 다른 에셋을 같은 번들에 넣지 않는 것**입니다. 매뉴얼은 이것과 반대 방향의 함정도 적어 둡니다. 번들의 마지막 에셋을 놓자마자 같은 번들의 에셋을 다시 로드하면, 번들을 내렸다가 다시 여는 일이 반복됩니다. Addressables 의 Profiler 모듈에서 번들이 언제 열리고 닫히는지 볼 수 있습니다.

## Instantiate 를 누가 했느냐에 따라 셈이 다르다

프리팹을 로드해 인스턴스를 만드는 길이 둘입니다. `LoadAssetAsync` 로 프리팹을 받아 직접 `Object.Instantiate` 하거나, `Addressables.InstantiateAsync` 로 인스턴스를 받습니다. 두 길에서 핸들을 놓거나 인스턴스를 지우는 방식을 바꿔 가며, 직후, 2초 뒤, `UnloadUnusedAssets` 뒤, 씬을 Single 로 다시 올린 뒤에 열린 번들 수를 셌습니다.

| 만든 방식 | 정리 | 인스턴스 | 열린 번들 (직후, 2초, UUA, 씬 재로드) |
| --- | --- | --- | --- |
| `Object.Instantiate` | 로드 핸들 `Release` | **살아 있음, 머티리얼 null** | 0, 0, 0, 0 |
| `InstantiateAsync` | 핸들 `Release` | 파괴됨 | 0, 0, 0, 0 |
| `InstantiateAsync` | `Destroy` 만 | 파괴됨 | **3, 3, 3, 0** |
| `InstantiateAsync` | `ReleaseInstance` | 파괴됨 | 0, 0, 0, 0 |

**`Object.Instantiate` 한 인스턴스는 Addressables 가 모릅니다.** 프리팹 핸들을 놓으면 셈이 0 이 되어 번들이 내려가고, 인스턴스가 쓰던 머티리얼이 파괴됩니다. 인스턴스는 남아 있는데 GPU Resident Drawer 가 켜져 있으면 화면에서 사라지고, 끄면 마젠타로 그려집니다. 경고나 에러 로그는 없었습니다.

**`InstantiateAsync` 는 인스턴스 수명까지 셉니다.** 핸들을 놓거나 `ReleaseInstance` 하면 인스턴스를 지우고 셈을 줄입니다. 그런데 `Destroy` 로 지우기만 하면 핸들은 유효한 채로 남아 셈이 줄지 않습니다. 2초 뒤에도, `UnloadUnusedAssets` 뒤에도 번들 셋이 열려 있었고, **씬을 다시 올리자 0 이 됐습니다.** 매뉴얼의 "추적되는 `InstantiateAsync` 인스턴스는 씬이 내려갈 때 해제된다" 는 설명과 맞습니다. 영원히 새는 것은 아니지만, 그 씬에 머무는 동안은 안 내려갑니다.

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

같은 번들의 A 만 놓는 경우와, `InstantiateAsync` 인스턴스를 `Destroy` 만 하는 경우를 봅니다. 열린 번들은 `AssetBundle.GetAllLoadedAssetBundles()` 로 셉니다.

```csharp
IEnumerator ReleaseOneOfTwo()
{
    var hA = Addressables.LoadAssetAsync<Texture2D>("AD4/A");
    var hB = Addressables.LoadAssetAsync<Texture2D>("AD4/B");
    yield return hA;
    yield return hB;

    Addressables.Release(hA);
    yield return null;
    bool aAlive = Resources.FindObjectsOfTypeAll<Texture2D>().Any(t => t.name == "AD4_A");
    Debug.Log($"A 를 놓은 뒤 A 가 메모리에 있나: {aAlive}");   // True

    yield return Resources.UnloadUnusedAssets();
    aAlive = Resources.FindObjectsOfTypeAll<Texture2D>().Any(t => t.name == "AD4_A");
    Debug.Log($"UnloadUnusedAssets 뒤: {aAlive}, 열린 번들 {AssetBundle.GetAllLoadedAssetBundles().Count()}");   // False, 1

    Addressables.Release(hB);
}

IEnumerator DestroyOnly()
{
    var handle = Addressables.InstantiateAsync("AD6/Cube");
    yield return handle;

    Destroy(handle.Result);
    yield return new WaitForSecondsRealtime(2);
    Debug.Log($"Destroy 2초 뒤 핸들 유효: {handle.IsValid()}, 열린 번들 {AssetBundle.GetAllLoadedAssetBundles().Count()}");
}
```

첫 함수에서 `True` 다음 `False, 1`, 둘째 함수에서 핸들이 유효하고 번들이 열려 있으면 같은 결과입니다.

## 주의할 점

### 1. 놓은 에셋이 빠졌는지는 번들 단위로 본다

**안 지키면 생기는 문제.** 에셋 핸들만 보고 "다 놓았다" 고 판단하면, 같은 번들의 다른 에셋 때문에 남아 있는 메모리를 놓칩니다. 반대로 번들의 마지막 에셋을 놓자마자 같은 번들을 다시 로드하면 번들을 닫았다 여는 일이 반복됩니다.

**이렇게 쓴다.** Addressables Profiler 모듈에서 번들이 언제 열리고 닫히는지 같이 봅니다. 곧 다시 쓸 번들은 에셋 하나를 쥐고 있어 닫히지 않게 둡니다.

### 2. Object.Instantiate 로 만들면 핸들을 인스턴스보다 오래 쥔다

**안 지키면 생기는 문제.** 인스턴스는 셈에 안 들어가서, 프리팹 핸들을 놓는 순간 번들이 내려가고 인스턴스의 머티리얼이 경고 없이 사라집니다.

**이렇게 쓴다.** 프리팹 핸들을 쥔 쪽이 인스턴스를 전부 지운 뒤에 `Release` 합니다. 풀이나 스포너가 프리팹을 가지면 핸들도 그 풀이 가집니다.

### 3. InstantiateAsync 로 만든 것은 ReleaseInstance 로 지운다

**안 지키면 생기는 문제.** `Destroy` 로 지우면 핸들이 남아 그 씬이 내려갈 때까지 번들이 열려 있습니다. 한 씬에서 오래 도는 게임에서는 그동안 계속 쌓입니다.

**이렇게 쓴다.** `InstantiateAsync` 로 만든 인스턴스는 `Addressables.ReleaseInstance(go)` 로 지웁니다. 이 인스턴스를 받는 쪽 코드가 `Destroy` 를 부르지 않게 한 곳으로 모읍니다.

## 정리

Addressables 는 **핸들마다 세지만, 메모리에서 내리는 단위는 번들입니다.**

- 같은 번들의 다른 에셋이 살아 있으면 `Release` 한 에셋도 남습니다. `UnloadUnusedAssets` 를 부르면 번들은 열린 채 그 에셋만 내려갑니다.
- `Object.Instantiate` 한 인스턴스는 셈에 안 들어가서, 핸들을 놓으면 머티리얼이 경고 없이 사라집니다.
- `InstantiateAsync` 는 인스턴스까지 세지만, `Destroy` 만 하면 씬이 내려갈 때까지 번들이 열려 있습니다. `ReleaseInstance` 로 지웁니다.