수많은 오브젝트가 화면에 생성되었다 삭제되면 불필요한 G.C가 발생합니다. 오늘은 다량의 오브젝트에 있어서 게임의 퍼포먼스를 유지하기 위한 최적화 기법인 오브젝트 풀링(Object Pooling)에 대해 정리해보겠습니다.
오브젝트 풀링은 오브젝트를 담아두는 Pool을 미리 만들어두는 기법입니다.
새로운 오브젝트가 필요할 때마다 Instantiate대신 미리 만들어둔 Pool에서 비활성화한 객체를 꺼내와 재사용하고 다 쓰면 Destroy하는 것이 아닌 다시 Pool로 반납하여 비활성화하는 방식입니다.
생성 및 파괴가 반복적으로 발생하는 오브젝트의 규모가 작다면 오브젝트 풀링을 사용하지 않아도 됩니다.
하지만 반대로 규모가 크다면 다음의 이유로 반드시 사용해야 합니다.
CPU 오버헤드 감소: 런타임 중 빈번한 Instantiate()와 Destroy() 호출은 메모리 할당과 엔진 내부 연산을 유발하여 CPU 성능을 심각하게 저하시킵니다.
가비지 컬렉터(G.C) 방지: 메모리의 할당과 해제가 반복되면 '가비지'가 발생합니다. 유니티가 이를 청소하는 과정에서 GC Spikes가 발생하며, 이는 플레이어가 체감하는 치명적인 프레임 드랍(렉)의 주범이 됩니다.
메모리 단편화 해결: 메모리 공간을 썼다 지웠다 반복하면 메모리 사이에 빈 구멍이 생기는 파편화가 발생합니다. 풀링은 한 번 할당한 공간을 계속 재활용하므로 이를 방지할 수 있습니다.
오브젝트 풀링은 선입선출(FIFO) 구조의 Queue를 컨테이너로 사용하며 다음과 같이 사용할 수 있습니다.
<style>
using System.Collections.Generic;
using UnityEngine;
public class ManualObjectPool : MonoBehaviour
{
[SerializeField] private GameObject prefab;
// 1. Container: 오브젝트를 담아둘 큐
private Queue<GameObject> pool = new Queue<GameObject>();
// 2. 초기화 (Initialze)
private void Start()
{
for (int i = 0; i < 20; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
pool.Enqueue(obj);
}
}
// 3. 대여 (Rent)
public GameObject Get()
{
if (pool.Count > 0)
{
GameObject obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
return Instantiate(prefab); // 풀이 비었을 때 예외 처리
}
// 4. 반납 (Return)
public void Return(GameObject obj)
{
obj.SetActive(false);
pool.Enqueue(obj);
}
}
</style>
Unity 2021.1 버전부터 공식 제공되는 빌트인 오브젝트 풀링 내장 API로 다음과 같은 이점이 있습니다.
안정성 보장: Collection Checks 기능을 통해, 이미 반납된 오브젝트를 다시 반납하는 중복 에러를 방지합니다.
다양한 컬렉션 풀: 기본 ObjectPool 뿐만 아니라 ListPool, DictionaryPool 등 C#자료구조 풀도 제공합니다.
메모리 누수 방지: 최대 용량을 설정해, 제한을 넘어가면 자동으로 메모리에서 해제하는 기능을 지원합니다.
핵심 콜백은 다음과 같습니다.
Create: 풀에 오브젝트가 없을 경우 생성하는 로직
OnGet: 풀에서 오브젝트를 대여할 때 실행할 로직
OnRelease: 사용이 끝난 오브젝트를 풀에 반납할 때 실행할 로직
OnDestroy: 풀의 크기가 한도를 초과하여 삭제가 필요할 때 로직
<style>
using UnityEngine;
using UnityEngine.Pool; // API 사용을 위해 추가
public class BulletManager : MonoBehaviour
{
public GameObject bulletPrefab;
private IObjectPool<GameObject> bulletPool;
private void Awake()
{
// 풀 선언 및 초기화
bulletPool = new ObjectPool<GameObject>(
createFunc: () => Instantiate(bulletPrefab), // 생성
actionOnGet: (obj) => obj.SetActive(true), // 대여
actionOnRelease: (obj) => obj.SetActive(false), // 반납
actionOnDestroy: (obj) => Destroy(obj), // 파괴
maxSize: 100 // 최대 용량 설정
);
}
public void Fire()
{
// 오브젝트 대여
GameObject bullet = bulletPool.Get();
// ... 발사 로직
}
public void OnBulletHit(GameObject bullet)
{
// 오브젝트 반납
bulletPool.Release(bullet);
}
}
</style>
오늘은 오브젝트 풀링에 대해 학습해보았습니다.
단순히 Instantiate와 Destroy의 반복 사용을 억제하는 것이 아닌 CPU 부담을 줄이고 G.C의 간섭을 최소화하여 플레이어에게 끈김 없는 경험을 제공하는 것을 위해 사용하는 것이 핵심이라는 것을 알게되었습니다.