Unity

[Unity] 디자인패턴 : Object Pool

sunlight-dby 2025. 6. 16. 14:43

 

 

 

이 글은 해당 유튜브를 보고 진행한 것에 대해 공부한 것을 정리한 글입니다.

 


 

오브젝트 풀 (Object Pool)

오브젝트 풀링은 반복적으로 생성과 제거가 필요한 게임 오브젝트를 미리 생성해 비활성화 상태로 보관해두고, 필요할 때 재사용하는 기법입니다.


엄밀히 말하면 GoF 디자인 패턴에는 포함되지 않지만, 게임 개발에서는 사실상 필수적인 최적화 패턴으로 널리 쓰입니다.

 

풀링된 오브젝트 비활성화 및 재사용


주요 특징

  • 빠른 재사용
    • 매번 오브젝트를 생성하지 않고, "Pool"에서 준비된 상태로 대기 중인 초기화된 오브젝트를 사용하므로 속도가 빠릅니다.
  • GC (가비지 콜렉션) 절감
    • 반복적인 생성 / 파괴로 인한 메모리 파편화 및 GC 스파이크를 방지할 수 있습니다.
  • 사전 로딩 가능
    • 로딩 화면 중 필요한 수만큼 미리 생성해두면, 런타임 성능 향상에 기여합니다.

장점

  • 성능 향상
    • 메모리 할당 및 해제를 줄이므로 게임의 성능을 향상할 수 있습니다.
  • 예측 가능한 동작
    • 오브젝트 생성과 제거가 일정하므로 디버깅과 관리가 용이합니다.
  • 재사용성
    • 오브젝트가 파괴되지 않고 계속 사용되므로 자원 낭비를 줄일 수 있습니다.

장점

  • 초기 메모리 비용
    • 풀을 미리 생성하므로 초기 메모리를 선점하게 됩니다.
  • 복잡도 증가
    • 객체의 상태 초기화, 반환 처리 등 로직이 추가로 필요합니다.
  • 메모리 낭비 가능성
    • 너무 많은 오브젝트가 풀에 남아있으면 오히려 불필요한 메모리 소모로 이어질 수 있습니다.

개선 방안

정적 또는 싱글톤 패턴 적용

  • 다양한 소스에서 풀링된 오브젝트를 생성해야 하는 경우에 Singleton 또는 static 풀 매니저를 사용하여 어디서든 접근하는 것이 가능합니다.
  • 단, static 클래스를 사용하면 Unity Inspector를 통한 조작이 불가능하므로 주의가 필요합니다.

Dictionary 기반 다중 풀 관리

Dictionary<int, Queue<GameObject>> objectPools;
  • 다양한 프리팹을 풀링할 경우, Dictionary<Key, Object Pool> 형태로 프리팹별 풀을 관리하면 효율적입니다.
    • 프리팹의 InstanceID 또는 고유 이름을 Key 값으로 사용하면 됩니다.

잘못된 반환 방지

  • 이미 풀에 있는 오브젝트를 다시 반환하려 하면 런타임 오류가 발생할 수 있습니다.
  • 반환 전 오브젝트 상태를 체크하거나, 풀 내부에서 중복 여부를 검사해야 합니다.

최대 개수 제한 설정

  • 풀에 들어갈 수 있는 최대 수량을 제한해 메모리 폭증을 방지합니다.
  • 초과할 경우 Destroy( )하거나 가장 오래된 항목을 제거하는 등의 전략을 사용합니다.

구현 예시 : 기본 Object Pool

Object Pool 클래스

 public class ObjectPool : MonoBehaviour
 {
    [SerializeField] private uint initPoolSize;
    [SerializeField] private PooledObject objectToPool;
    
    // 풀링된 오브젝트를 컬렉션에 저장
    private Stack<PooledObject> stack;
    
    private void Start()
    {
        SetupPool();
    }
    
    // 풀 생성(지연을 인지할 수 없을 때 호출)
    private void SetupPool()
    {
        stack = new Stack<PooledObject>();
        PooledObject instance = null;
    
        for (int i = 0; i < initPoolSize; i++)
        {
            instance = Instantiate(objectToPool);
            instance.Pool = this;
            instance.gameObject.SetActive(false);
            stack.Push(instance);
        }
    }
    
     // 풀에서 첫 번째 액티브 게임 오브젝트를 반환합니다.
    public PooledObject GetPooledObject()
    {
        // 풀이 충분히 크지 않으면 새로운 PooledObjects를 인스턴스화합니다.
        if (stack.Count == 0)
        {
            PooledObject newInstance = Instantiate(objectToPool);
            newInstance.Pool = this;
            return newInstance;
        }
        
        // 그렇지 않으면 목록에서 다음 항목을 가져옵니다.
        PooledObject nextInstance = stack.Pop();
        nextInstance.gameObject.SetActive(true);
        
        return nextInstance;
    }
    
    public void ReturnToPool(PooledObject pooledObject)
    {
        stack.Push(pooledObject);
        pooledObject.gameObject.SetActive(false);
    }
 }

PoolObject 클래스

 public class PooledObject : MonoBehaviour
 {
    private ObjectPool pool;
    public ObjectPool Pool { get => pool; set => pool = value; }
    
    public void Release()
    {
        pool.ReturnToPool(this);
    }
 }

구현 예시

오브제트 풀 쌤플 씬


Bullet Object Pool

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DesignPatterns.ObjectPool

// This is an example client that uses our simple object pool.
public class ExampleGun : MonoBehaviour
{
    [Tooltip("Prefab to shoot")]
    [SerializeField] private GameObject projectile;
    [Tooltip("Projectile force")]
    [SerializeField] float muzzleVelocity = 700f;
    [Tooltip("End point of gun where shots appear")]
    [SerializeField] private Transform muzzlePosition;
    [Tooltip("Time between shots / smaller = higher rate of fire")]
    [SerializeField] float cooldownWindow = 0.1f;
    [Tooltip("Reference to Object Pool")]
    [SerializeField] ObjectPool objectPool;
    
    private float nextTimeToShoot;

    private void FixedUpdate()
    {
        // shoot if we have exceeded delay
        if (Input.GetButton("Fire1") && Time.time > nextTimeToShoot && objectPool != null)
        {
            // get a pooled object instead of instantiating
            GameObject bulletObject = objectPool.GetPooledObject().gameObject;

            if (bulletObject == null)
                return;

            bulletObject.SetActive(true);

        // align to gun barrel/muzzle position
        bulletObject.transform.SetPositionAndRotation(muzzlePosition.position, muzzlePosition.rotation);

        // move projectile forward
        bulletObject.GetComponent<Rigidbody>().AddForce(bulletObject.transform.forward * muzzleVelocity, ForceMode.Acceleration);

        // turn off after a few seconds
        ExampleProjectile projectile = bulletObject.GetComponent<ExampleProjectile>();
        projectile ?. Deactivate();

        // set cooldown delay
        nextTimeToShoot = Time.time + cooldownWindow;
        
        // ...
    }
    
    // ...
    
}

UnityEngine.Pool

Unity 2021 이상 버전부터는 UnityEngine.Pool 네임스페이스가 도입되어, 직접 Object Pool을 만들 필요 없이 빌트인하여 사용할 수 있습니다.


장점

  • 별도의 관리 코드 없이 효율적인 풀 관리가 가능합니다.
  • ObjectPool<T>, CollectionPool<TCollection, TItem> 등의 제네릭 클래스로 확장성 높습니다.

예시

RevisedGun.cs

using UnityEngine.Pool;
 public class RevisedGun : MonoBehaviour
 {
    // …
    
    // Unity 2021 이상 버전에서 사용 가능한 스택 기반 ObjectPool
    private IObjectPool<RevisedProjectile> objectPool;
    
    // 이미 풀에 있는 기존 항목을 반환하려 할 때 예외를 반환
    [SerializeField] private bool collectionCheck = true;
    // 풀의 용량과 최대 크기를 제어하는 추가 옵션
    [SerializeField] private int defaultCapacity = 20;
    [SerializeField] private int maxSize = 100;
    
    private void Awake()
    {
        objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile,
            OnGetFromPool, OnReleaseToPool, OnDestroyPooledObject,
            collectionCheck, defaultCapacity, maxSize);
    }
    
    // 오브젝트 풀을 채울 항목을 만들 때 호출됨
    private RevisedProjectile CreateProjectile()
    {
        RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
        projectileInstance.ObjectPool = objectPool;
        return projectileInstance;
    }
    
    // 오브젝트 풀로 항목을 반환할 때 호출됨
    private void OnReleaseToPool(RevisedProjectile pooledObject)
    {
        pooledObject.gameObject.SetActive(false);
    }
    
    // 오브젝트 풀에서 다음 항목을 검색할 때 호출됨
    private void OnGetFromPool(RevisedProjectile pooledObject)
    {
        pooledObject.gameObject.SetActive(true);
    }
    
    // 풀링된 항목의 최대 개수를 초과할 때 호출됨(풀링된 오브젝트 파괴)
    private void OnDestroyPooledObject(RevisedProjectile pooledObject)
    {
        Destroy(pooledObject.gameObject);
    }
    
    private void FixedUpdate()
    {
        // …
    }
 }

 

RevisedGun 클래스는 오브젝트 풀을 생성하고 관리하며, 풀에서 발사체를 꺼내거나 반환하는 주체입니다.


RevisedProjectile.cs

public class RevisedProjectile : MonoBehaviour
 {
    // …
    
    private IObjectPool<RevisedProjectile> objectPool;
    
    // 발사체에 ObjectPool에 대한 레퍼런스를 제공하는 공용 프로퍼티
    public IObjectPool<RevisedProjectile> ObjectPool { set => objectPool = value; }
    
    // …
 }

 

RevisedProjectile 클래스는 발사체의 개별 동작을 담당하며, 자신이 속한 오브젝트 풀에 대한 레퍼런스를 보관합니다.


ObjectPool<T>  클래스

 

public class ObjectPool<T>: IDisposable, IObjectPool<T> where T : class 
{
    internal readonly List<T> m_List;
    private readonly Func<T> m_CreateFunc;
    private readonly Action<T> m_ActionOnGet;
    private readonly Action<T> m_ActionOnRelease;
    private readonly Action<T> m_ActionOnDestroy;
        
    // ...
}

 

UnityEngine.Pool 내부 구현된 ObjectPool<T> 클래스입니다.