---
title: "오브젝트 풀은 언제 손해인가"
description: "풀링이 항상 이득이라는 통념을 실측으로 확인해봤고, 손해가 나는 구간이 분명히 있었다."
author: "유니얼"
pubDate: 2026-09-07
updatedDate: 2026-09-09
category: "game-dev/design-patterns"
tags: ["Unity", "최적화", "GC"]
canonical: https://blog.unialgames.com/posts/object-pool-cost/
---
총알을 초당 30발 쏘면 1분에 1800개를 만들고 1800개를 버립니다.
버린 것은 그 자리에서 사라지지 않고 쌓여 있다가, 어느 순간 한꺼번에 정리되면서
화면이 한 번 멈춥니다. 오브젝트 풀은 그 만들고 버리는 일을 안 하는 방법입니다.

다만 결론부터 쓰면, 생성 비용이 싼 객체를 풀링하면 오히려 느려집니다. 풀은 공짜가 아니라서
가져오고 돌려주는 비용이 매번 붙고, 미리 채워두는 만큼 초기 비용도 먼저 냅니다.

## 풀이 바꾸는 것

풀링은 "빨라지는 기술"이 아니라 **객체를 버리지 않는 기술**입니다.

![매번 생성과 풀 사용의 흐름 비교](./images/pool-flow.png)
*버리지 않으니 GC 가 수거할 것도 없습니다.*

버려진 객체는 그 자리에서 사라지지 않고 GC 가 돌 때까지 메모리에 남습니다.
그래서 "매번 생성"의 대가는 생성 비용이 아니라 **나중에 몰아서 오는 정지**입니다.

![메모리가 쌓이는 모양 개념도](./images/memory-pattern.png)
*실측 그래프가 아니라 패턴을 그린 것입니다.*

말로는 잘 안 와닿아서 움직이는 걸로 만들어 뒀습니다. 방식을 바꿔가며 보세요.
**"매번 생성"에서만 만든 객체 수가 계속 오르고 GC 막대가 차오릅니다.**

<x-pool>
<script type="application/json">
{
  "label": "풀이 도는 모습",
  "poolSize": 8,
  "spawnRate": 2.6,
  "lifetime": 2.4,
  "gcEvery": 12,
  "note": "네모 하나가 객체 하나입니다. 아래 줄의 뜻은 방식에 따라 바뀝니다. 풀 사용이면 풀 슬롯, 매번 생성이면 버려져서 GC 를 기다리는 객체입니다."
}
</script>
</x-pool>

## 직접 굴려보기

말보다 숫자가 빠릅니다. 재사용 횟수와 객체 하나의 생성 비용을 바꿔보면 풀링이 손해가
되는 구간이 어디인지 바로 보입니다. **재사용 10회 + 가벼움**으로 두면 풀이 집니다.

<x-calc>
<script type="application/json">
{
  "label": "직접 해보기",
  "inputs": [
    { "id": "n", "label": "객체 하나를 몇 번 재사용하나", "options": [1, 10, 100, 1000], "default": 100, "suffix": "회" },
    { "id": "c", "label": "객체 하나를 만드는 비용", "options": [["가벼움", 0.4], ["보통", 2], ["무거움", 9]], "default": 2 }
  ],
  "bars": [
    { "label": "매번 생성", "expr": "c * n", "unit": "μs" },
    { "label": "풀 사용", "expr": "c * Math.min(n, 32) + 0.15 * n", "unit": "μs", "accent": true }
  ],
  "verdict": {
    "expr": "bars[1] < bars[0]",
    "yes": "풀링이 이득, 재사용이 생성 비용을 덮습니다",
    "no": "풀링이 손해, 가져오고 돌려주는 비용이 더 큽니다"
  },
  "note": "풀 크기 32개, 가져오고 돌려주는 비용 0.15μs 기준. 실제 수치는 프로젝트마다 다릅니다."
}
</script>
</x-calc>

재사용이 열 번 이하이고 객체가 가벼우면 풀이 지는 구간이 나옵니다. 이때는 풀을 걷어내는
게 최적화입니다.

## 할당을 걷어낸 쪽은 확실히 이득

풀링과 별개로, 매 프레임 새 리스트를 만드는 건 거의 항상 손해입니다.

<x-compare>
<script type="application/json">
{
  "label": "개선 전후",
  "tabs": [
    {
      "label": "개선 전",
      "code": "void Update() {\n    var hits = new List<Hit>();   // 매 프레임 할당\n    Scan(hits);\n}",
      "stats": [["프레임당 할당", "1.8 KB"], ["GC 발생 간격", "3.4초"]],
      "tone": "bad"
    },
    {
      "label": "개선 후",
      "code": "readonly List<Hit> _hits = new();\n\nvoid Update() {\n    _hits.Clear();                // 할당 없음\n    Scan(_hits);\n}",
      "stats": [["프레임당 할당", "0 B"], ["GC 발생 간격", "없음"]],
      "tone": "good"
    }
  ]
}
</script>
</x-compare>

## 판단 순서

무엇부터 볼지 헷갈리면 이 순서로 봅니다.

<x-steps>
<script type="application/json">
{
  "label": "판단 순서",
  "steps": [
    { "tag": "STEP 1", "title": "측정", "text": "프로파일러로 실제 할당량과 GC 간격을 먼저 봅니다. 여기서 문제가 안 보이면 아무것도 하지 않는 게 맞습니다." },
    { "tag": "STEP 2", "title": "재사용 횟수", "text": "객체 하나가 몇 번 재사용되는지 셉니다. 열 번 이하라면 풀링은 후보에서 빼세요." },
    { "tag": "STEP 3", "title": "생성 비용", "text": "생성자가 무거운지 봅니다. 컴포넌트 조회나 문자열 파싱이 들어 있으면 풀링이 이깁니다. 필드 몇 개 채우는 정도면 집니다." }
  ]
}
</script>
</x-steps>

> 측정하지 않은 최적화는 최적화가 아닙니다. 풀링도 예외가 아닙니다.