TOOLS GAMES DEVLOG DOCS ABOUT CONTACT
HOME > DOCS > Unity / C# > Unity C# 제네릭 오브젝트 풀링 (UnityEngine.Pool 최적화)
Unity / C# Beginner Unity 2021.1+ / 2022 / Unity 6

Unity C# 제네릭 오브젝트 풀링 (UnityEngine.Pool 최적화)

Instantiate/Destroy 반복으로 인한 GC 가비지와 프레임 드랍을 원천 차단하는 UnityEngine.Pool.IObjectPool 기반 제네릭 풀 구현 및 실전 컴포넌트 래퍼.

#Unity #C# #ObjectPool #Performance #GC-Optimization
왜 써야 하는가?탄막 슈팅의 총알, 파티클 이펙트, 몬스터 스폰 등 자주 생성/소멸하는 오브젝트에 Instantiate()와 Destroy()를 호출하면 C# 가비지 컬렉터(GC)에 심각한 힙 할당이 누적되어 주기적인 화면 멈춤(Spike) 현상이 발생합니다. Unity 2021부터 기본 제공되는 내장 UnityEngine.Pool을 사용하면 추가 에셋 없이도 안전하고 빠른 풀링 시스템을 구축할 수 있습니다.

1. 제네릭 컴포넌트 풀 (ComponentPool<T>)

프리팹을 인스턴스화하고 풀 반환/대여 시 상태를 초기화해 주는 MonoBehaviour용 래퍼 클래스입니다.

ComponentPool.cs
using System;
using UnityEngine;
using UnityEngine.Pool;

namespace LyscStudio.Patterns
{
    /// <summary>
    /// UnityEngine.Pool.ObjectPool 기반의 컴포넌트 풀 래퍼
    /// </summary>
    public class ComponentPool<T> : IDisposable where T : Component
    {
        private readonly T _prefab;
        private readonly Transform _parent;
        private readonly IObjectPool<T> _pool;

        public int ActiveCount => _pool.CountActive;
        public int InactiveCount => _pool.CountInactive;

        public ComponentPool(
            T prefab, 
            Transform parent = null, 
            int defaultCapacity = 20, 
            int maxCapacity = 200,
            bool collectionCheck = true)
        {
            _prefab = prefab;
            _parent = parent;

            _pool = new ObjectPool<T>(
                createFunc: OnCreateItem,
                actionOnGet: OnGetItem,
                actionOnRelease: OnReleaseItem,
                actionOnDestroy: OnDestroyItem,
                collectionCheck: collectionCheck, // 이미 반환된 객체의 중복 반환 검사
                defaultCapacity: defaultCapacity,
                maxSize: maxCapacity
            );
        }

        private T OnCreateItem()
        {
            T instance = UnityEngine.Object.Instantiate(_prefab, _parent);
            instance.gameObject.SetActive(false);
            return instance;
        }

        private void OnGetItem(T item)
        {
            item.gameObject.SetActive(true);
        }

        private void OnReleaseItem(T item)
        {
            if (item != null && item.gameObject.activeSelf)
            {
                item.gameObject.SetActive(false);
            }
        }

        private void OnDestroyItem(T item)
        {
            if (item != null)
            {
                UnityEngine.Object.Destroy(item.gameObject);
            }
        }

        public T Get() => _pool.Get();
        public void Release(T item) => _pool.Release(item);
        public void Clear() => _pool.Clear();

        public void Dispose()
        {
            Clear();
        }
    }
}

2. 실제 총알(Bullet) 오브젝트 적용 예시

풀에서 꺼낸 오브젝트가 수명을 다했을 때 스스로 풀로 돌아가도록 콜백을 전달하는 패턴입니다.

PooledBullet.cs
using System;
using UnityEngine;

public class PooledBullet : MonoBehaviour
{
    [SerializeField] private float _speed = 20f;
    [SerializeField] private float _lifeTime = 2.5f;

    private Action<PooledBullet> _returnToPool;
    private float _timer;

    public void Initialize(Action<PooledBullet> returnAction)
    {
        _returnToPool = returnAction;
        _timer = 0f;
    }

    private void Update()
    {
        transform.position += transform.forward * (_speed * Time.deltaTime);
        _timer += Time.deltaTime;

        if (_timer >= _lifeTime)
        {
            Return();
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        // 충돌 로직 처리 후 풀로 반환
        Return();
    }

    private void Return()
    {
        if (_returnToPool != null)
        {
            _returnToPool(this);
            _returnToPool = null; // 중복 반환 방지
        }
    }
}

3. 주요 옵션 및 성능 비교

항목일반 Instantiate/DestroyUnityEngine.Pool
프레임 타임 (1000개 연타)12ms ~ 28ms (GC Spike 발생)0.8ms ~ 1.5ms (극도로 안정적)
GC Alloc (Update 매 프레임)초당 수십 KB ~ MB 누적0 B (최초 풀 생성 이후 제로 할당)
collectionCheck 옵션해당 없음true 시 중복 반환 시 예외를 던져 안정성 보장 (빌드 시 조건부 권장)
maxSize 초과 시계속 힙 메모리 증가초과분은 풀에 쌓아두지 않고 자동으로 Destroy 호출

4. 주의점 및 메모리 누수 방지 팁

  • 이중 반환(Double Release) 버그: 이미 풀에 들어있는 오브젝트를 다른 곳에서 다시 Release()하면 풀 큐에 같은 인스턴스가 2번 들어가서 다음 Get() 시 예측할 수 없는 버그가 생깁니다. collectionCheck: true를 유지하여 개발 단계에서 즉시 적발하십시오.
  • 컴포넌트 잔여 상태 리셋: Rigidbody.velocity = Vector3.zero, TrailRenderer.Clear(), 각종 애니메이터 파라미터는 OnGetItem 또는 풀에서 꺼낼 때 반드시 리셋해 주어야 이전 상태가 튀는 현상을 막을 수 있습니다.
  • 씬 전환 시 풀 정리: 씬이 바뀔 때 파괴된(Destroyed) 오브젝트의 참조가 풀에 남아있지 않도록 OnDestroy에서 풀의 Dispose() 또는 Clear()를 명시적으로 호출하십시오.