본문 바로가기

유니티 공부

Object Pooling

프로젝트를 진행하면서 오브젝트 풀링을 활용했지만 정리한 내용이 없어서 정리를 해볼려고 합니다.

 

• Object Pool이란?

 

오브젝트 풀(Object Pool)은 게임에서 자주 생성되고 파괴되는 오브젝트들을 효율적으로 관리하기 위한 디자인 패턴입니다.


쉽게 말해, 오브젝트를 매번 새로 만들고 없애는 대신, '웅덩이(Pool)'처럼 미리 만들어 둔 오브젝트들을 필요할 때 꺼내 쓰고 다시 넣는 방식입니다.

 

Unity에서 흔히 사용하는 Instantiate와 Destroy는 오브젝트를 반복해서 생성하고 제거할 때 가비지 컬렉션(GC)를 유발하며, 이는 성능 저하로 이어질 수 있습니다.


이런 문제를 줄이기 위해 오브젝트 풀을 사용하면, 오브젝트를 생성할 때 한 번만 만들고, 필요할 때 활성화/비활성화만 하여 재사용할 수 있습니다.

 

이 방식은 불필요한 메모리 할당과 해제를 줄여 성능을 최적화하는 데 큰 도움이 됩니다.

 

• 구현하는 방법은?

오브젝트 풀을 구현하는 방식은 개발자마다 조금씩 다르지만, 일반적으로 Stack이나 Queue 자료구조를 활용해 관리합니다.
이러한 방식은 재사용할 오브젝트를 효율적으로 보관하고 꺼내 쓰기 쉽기 때문입니다.

 

Unity에서는 이러한 오브젝트 풀링 패턴을 보다 편리하게 사용할 수 있도록, UnityEngine.Pool 네임스페이스의 ObjectPool<T> 클래스를 제공합니다.


이 클래스를 사용하면 오브젝트 생성, 반환, 초기화, 해제 등의 과정을 보다 구조적으로 처리할 수 있어, 직접 자료구조를 구현하는 수고를 줄일 수 있습니다.

 

이제부터는 Unity의 ObjectPool<T>를 활용한 오브젝트 풀링 구현 방법을 설명드리겠습니다.

 

• ObjectPool<T>

Unity의 ObjectPool<T>는 객체 풀링을 쉽게 구현할 수 있도록 도와주는 제네릭 클래스입니다. 생성자에서 다양한 콜백 델리게이트와 설정값을 전달받아 풀을 구성할 수 있습니다. 주요 매개변수는 다음과 같습니다:

ObjectPool<T> pool = new ObjectPool<T>(
    createFunc,        // 새 인스턴스를 생성하는 함수
    actionOnGet,       // Get할 때 실행할 동작
    actionOnRelease,   // Release할 때 실행할 동작
    actionOnDestroy,   // Destroy할 때 실행할 동작
    collectionCheck,   // 중복 반환 검사 여부
    defaultCapacity,   // 초기 풀 용량
    maxSize            // 최대 용량
);

 

  • createFunc :
    • 오브젝트를 최초로 생성할 때 호출되는 델리게이트입니다.
    • 일반적으로 Instantiate() 메서드를 람다로 넘겨 사용합니다.
  • actionOnGet :
    • 풀에서 오브젝트를 가져올 때 호출되는 델리게이트입니다.
    • 주로 SetActive(true) 등 오브젝트를 활성화하는 동작을 연결합니다.
  • actionOnRelease :
    • 오브젝트를 다 사용한 후 풀에 반환할 때 호출되는 델리게이트입니다.
    • 일반적으로 SetActive(false) 등 비활성화 처리를 합니다.
  • actionOnDestroy :
    • 풀에서 오브젝트를 완전히 파괴해야 할 때 호출되는 델리게이트입니다.
    • 예를 들어, 더 이상 필요 없는 오브젝트를 제거하고 싶을 때 Destroy()를 구독시킵니다.
    • ObjectPool.Clear() 메서드를 호출하면 이 델리게이트가 모든 오브젝트에 대해 실행되며, 내부 리스트도 초기화됩니다.
  • collectionCheck :
    • 중복 반환을 검사할지 여부를 설정합니다.
    • true로 설정하면 디버그 모드에서 같은 오브젝트가 여러 번 풀에 반환되는 경우 예외를 발생시켜 버그를 쉽게 잡을 수 있습니다.
    • 디버깅용으로는 유용하지만, 성능에 영향을 줄 수 있으므로 최종 빌드에서는 false로 설정하는 것이 일반적입니다.
  • defaultCapcity :
    • 풀 생성 시 초기 오브젝트 개수입니다.
    • 사전에 미리 풀링할 개체 수를 설정해 성능을 최적화할 수 있습니다.
  • maxSize :
    • 풀의 최대 보관 개수입니다.
    • 이 값을 초과하면 반환된 오브젝트는 파괴(actionOnDestroy)되어 풀에 저장되지 않습니다.

 

•  사용 예시

아래는 Unity의 ObjectPool<T>를 래핑한 제네릭 오브젝트 풀 클래스입니다.


Prefab을 기반으로 오브젝트 풀을 생성하고, 필요한 오브젝트를 꺼내서 사용하거나 다시 풀에 반환할 수 있도록 구성되어 있습니다.

using UnityEngine;
using UnityEngine.Pool;


public class Pool<T> where T : Component
{
    private ObjectPool<T> pool;

    public Pool(T _prefab, Transform _parent = null)
    {
        pool = new ObjectPool<T>(
            createFunc: () => GameObject.Instantiate(_prefab, _parent),
            actionOnGet: obj => obj.gameObject.SetActive(true),
            actionOnRelease: obj => obj.gameObject.SetActive(false),
            actionOnDestroy: obj => GameObject.Destroy(obj.gameObject)
            );
    }

    public T Get() => pool.Get();
    
    public void Release(T obj) => pool.Release(obj);

}

 

좀 더 확장성을 주기 위해서 다음과 같이 PoolRegistry를 만들어서 사용하면 좋습니다.

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

public class PoolRegistry
{
    // 관리할 오브젝트풀을 Type에 따라 Dictionary로 관리
    public Dictionary<Type, object> _typedPools = new();


    // 풀 등록하기
    public void Register<T>(T prefab, Transform parent = null) where T : Component
    {
        Type type = typeof(T);
        if (_typedPools.ContainsKey(type))
        {
            Debug.LogWarning($"Pool for {type} already registered.");
            return;
        }

        var pool = new Pool<T>(prefab, parent ?? Managers.Pool.transform);
        _typedPools[type] = pool;
    }

    // 타입에 맞는 오브젝트 풀에서 가져오기
    public T Get<T>() where T : Component
    {
        if (_typedPools.TryGetValue(typeof(T), out var obj))
        {
            return ((Pool<T>)obj).Get();
        }

        Debug.LogError($"No pool registered for type {typeof(T)}");
        return null;
    }

    // 다 사용한 오브젝트 비활성화 하기
    public void Release<T>(T obj) where T : Component
    {
        if (_typedPools.TryGetValue(typeof(T), out var poolObj))
        {
            ((Pool<T>)poolObj).Release(obj);
        }
        else
        {
            Debug.LogWarning($"Tried to release unregistered object type: {typeof(T)}");
        }
    }
}

 

•  Addressable와 같이 사용하기

이제 Pool을 만들었으니 그 Pool 안에 오브젝트를 넣어보겠습니다. Unity에 Resource 폴더에서 가져오는 간단한 방법도 있지만 여기서는 Addressable을 이용해 확장성 있게 코드를 작성했습니다.

 

ResourceManager 클래스를 만들고 여기서 Addressable에서 데이터를 가져와 자동으로 Pool에 등록 시키는 코드를 아래와 같이 작성했습니다.

using System;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class ResourceManager
{
    [SerializeField] private string prefabLabel = "PooledPrefab";

    public void Init()
    {
        LoadAssetAsync();

    }
    public async void LoadAssetAsync()
    {
        // Addressables에서 Label 기준으로 프리팹 자동 로드
        var handle = Addressables.LoadAssetsAsync<GameObject>(prefabLabel, null);
        await handle.Task;

        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            foreach (var go in handle.Result)
            {
                if(!go.TryGetComponent(out IPoolable component))
                {
                    Debug.LogWarning($"{go.name}에 Component가 없습니다.");
                    continue;
                }

                Type type = component.GetType();
                if (Managers.Pool.PoolRegistry._typedPools.ContainsKey(type))
                {
                    Debug.LogWarning($"중복 등록된 타입: {type}");
                    continue;
                }

                // Pool 묶음 생성
                Transform transform = new GameObject(type + "_Pool").transform;
                transform.SetParent(Managers.Pool.transform);

                var poolType = typeof(Pool<>).MakeGenericType(type);
                var pool = Activator.CreateInstance(poolType, component,transform);

                Managers.Pool.PoolRegistry._typedPools[type] = pool;
            }
        }
        else
        {
            Debug.LogError("Addressables 로딩 실패");
        }
    }

}

 

추가로 Pool에 들어갈 클래스는 IPoolable 인터페이스를 상속 시켜주면, 이제 IPoolable을 상속 받은 클래스는 Pool에 자동 등록됩니다.

public interface IPoolable
{
    public void Realease();
}

 

 

'유니티 공부' 카테고리의 다른 글

UIToolkit 튜토리얼  (0) 2025.07.04
UI Toolkit  (0) 2025.06.29
유니티 게임 WebGL 빌드 하기  (0) 2025.06.06
Update, FixedUpdate, LateUpdate의 차이점  (1) 2025.04.24
Addressable  (0) 2025.04.07