콘텐츠로 이동

Unity Object Pooling 심화

오브젝트 풀링은 GameObject의 반복적인 Instantiate/Destroy 비용을 제거하는 가장 기본적인 최적화 기법입니다. Unity 2021부터는 UnityEngine.Pool.ObjectPool<T>가 내장되어 별도 구현 없이 바로 사용할 수 있습니다.


using UnityEngine;
using UnityEngine.Pool;
public class BulletPool : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
[SerializeField] private int defaultCapacity = 20;
[SerializeField] private int maxSize = 100;
private IObjectPool<Bullet> _pool;
void Awake()
{
_pool = new ObjectPool<Bullet>(
createFunc: CreateBullet,
actionOnGet: OnGetFromPool,
actionOnRelease: OnReleaseToPool,
actionOnDestroy: OnDestroyPoolObject,
collectionCheck: true, // 중복 반환 감지 (개발 시 true)
defaultCapacity: defaultCapacity,
maxSize: maxSize
);
}
private Bullet CreateBullet()
{
var bullet = Instantiate(bulletPrefab);
bullet.Pool = _pool; // 총알이 스스로 반환할 수 있도록 참조 전달
return bullet;
}
private void OnGetFromPool(Bullet bullet) => bullet.gameObject.SetActive(true);
private void OnReleaseToPool(Bullet bullet) => bullet.gameObject.SetActive(false);
private void OnDestroyPoolObject(Bullet bullet) => Destroy(bullet.gameObject);
public Bullet Get() => _pool.Get();
}
public class Bullet : MonoBehaviour
{
public IObjectPool<Bullet> Pool { get; set; }
[SerializeField] private float lifetime = 3f;
void OnEnable()
{
CancelInvoke();
Invoke(nameof(ReturnToPool), lifetime);
}
private void ReturnToPool() => Pool?.Release(this);
}

레벨 시작 시 풀을 미리 채워두면 첫 번째 스파이크를 제거할 수 있습니다.

public class PoolPrewarmer : MonoBehaviour
{
[SerializeField] private BulletPool bulletPool;
[SerializeField] private int prewarmCount = 20;
IEnumerator Start()
{
// 프레임 분산: 한 프레임에 몰아서 생성하면 히치 발생
var list = new List<Bullet>(prewarmCount);
for (int i = 0; i < prewarmCount; i++)
{
list.Add(bulletPool.Get());
if (i % 5 == 0) yield return null; // 5개마다 프레임 양보
}
// 전부 반환
foreach (var b in list) b.Pool.Release(b);
}
}

여러 프리팹 타입을 하나의 매니저로 관리하는 패턴입니다.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
public class PoolManager : MonoBehaviour
{
public static PoolManager Instance { get; private set; }
private readonly Dictionary<GameObject, ObjectPool<GameObject>> _pools = new();
void Awake()
{
if (Instance != null) { Destroy(gameObject); return; }
Instance = this;
DontDestroyOnLoad(gameObject);
}
private ObjectPool<GameObject> GetOrCreatePool(GameObject prefab)
{
if (!_pools.TryGetValue(prefab, out var pool))
{
pool = new ObjectPool<GameObject>(
createFunc: () => Instantiate(prefab),
actionOnGet: obj => obj.SetActive(true),
actionOnRelease: obj => obj.SetActive(false),
actionOnDestroy: Destroy,
defaultCapacity: 10,
maxSize: 200
);
_pools[prefab] = pool;
}
return pool;
}
public GameObject Get(GameObject prefab) => GetOrCreatePool(prefab).Get();
public void Release(GameObject prefab, GameObject instance)
=> GetOrCreatePool(prefab).Release(instance);
}

4. LinkedPool — 스택 기반 경량 풀

섹션 제목: “4. LinkedPool — 스택 기반 경량 풀”

LinkedPool<T>ObjectPool<T>보다 메모리 오버헤드가 적은 연결 리스트 기반 풀입니다. 풀 크기를 동적으로 늘려야 하는 경우에 적합합니다.

// LinkedPool은 maxSize 초과 시 오브젝트를 파괴하지 않고 계속 보유
var linkedPool = new LinkedPool<Bullet>(
createFunc: () => Instantiate(bulletPrefab).GetComponent<Bullet>(),
actionOnGet: b => b.gameObject.SetActive(true),
actionOnRelease: b => b.gameObject.SetActive(false),
actionOnDestroy: b => Destroy(b.gameObject)
);

실제 게임에서는 부하가 예측 불가능합니다. 풀 사용량을 모니터링하고 동적으로 확장하는 로직을 추가합니다.

public class AdaptivePool<T> where T : Component
{
private readonly ObjectPool<T> _pool;
private int _peakCount = 0;
private int _currentCount = 0;
public int PeakCount => _peakCount;
public AdaptivePool(System.Func<T> factory, int initial = 10, int max = 500)
{
_pool = new ObjectPool<T>(
factory,
actionOnGet: _ => _currentCount++,
actionOnRelease: _ => _currentCount--,
maxSize: max,
defaultCapacity: initial
);
}
public T Get()
{
var obj = _pool.Get();
_peakCount = Mathf.Max(_peakCount, _currentCount);
return obj;
}
public void Release(T obj) => _pool.Release(obj);
}

방식Instantiate 비용GC 압력코드 복잡도
매번 Instantiate높음높음 (Destroy)낮음
ObjectPool없음 (재사용)낮음보통
LinkedPool없음매우 낮음보통
커스텀 배열 풀없음없음높음

  • Unity 내장 ObjectPool<T>collectionCheck: true로 개발 중 중복 반환 버그를 조기에 발견한다.
  • 레벨 로드 시 Prewarm을 프레임 분산하여 실행하면 로딩 직후 히치를 방지할 수 있다.
  • 여러 프리팹을 관리할 때는 Dictionary<GameObject, ObjectPool<GameObject>> 패턴을 사용한다.
  • 반환된 오브젝트의 상태(위치, 속도, 이벤트 구독 등)를 OnGetFromPool에서 반드시 초기화해야 한다.