게임을 만들다 보면 총알, 이펙트, 몬스터처럼 짧은 시간 동안 반복적으로 생성되고 파괴되는 오브젝트를 자주 다룬다.
이런 경우 단순히 Instantiate()와 Destroy()를 반복하면 성능 저하와 GC(Garbage Collection) 부하가 발생한다.
이 문제를 해결하기 위한 대표적인 패턴이 바로 오브젝트 풀(Object Pool) 이다.
Unity 2021 버전 이후부터는 UnityEngine.Pool 네임스페이스에
내장 풀 시스템이 추가되어, 직접 구현하지 않아도 효율적인 풀을 쉽게 사용할 수 있다.
💡 오브젝트 풀(Object Pool) 이란?
“필요할 때마다 새로 만들지 말고, 미리 만들어둔 오브젝트를 재사용하자.”
오브젝트 풀은 자주 사용되는 오브젝트를 미리 생성해두고 필요할 때 꺼내서 쓰고, 다 쓰면 비활성화 후 다시 보관하는 구조다.
예를 들어, 슈팅 게임에서 총알을 Instantiate()로 계속 만들면
- 생성 시 메모리 할당
- 파괴 시 GC 수집
이 반복되며 프레임 드랍이 발생할 수 있다.
하지만 오브젝트 풀을 사용하면 한 번 만들어진 총알을 재활용하므로 성능이 일정하게 유지된다.
🧩 Unity 내장 풀 시스템 소개
Unity는 UnityEngine.Pool 네임스페이스 아래에 제너릭 기반의 ObjectPool<T> 클래스를 제공한다.
이 클래스를 사용하면 오브젝트 생성, 회수, 삭제 등의 과정을 콜백 기반으로 손쉽게 관리할 수 있다.
🧱 기본 사용 예시
아래는 ObjectPool<GameObject>를 이용해 Enemy를 관리하는 간단한 예시다.
using UnityEngine;
using UnityEngine.Pool;
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private GameObject enemyPrefab;
private ObjectPool<GameObject> enemyPool;
[SerializeField] private int initialPoolSize = 10;
[SerializeField] private int maxPoolSize = 50;
void Awake()
{
// 오브젝트 풀 초기화
enemyPool = new ObjectPool<GameObject>(
createFunc: CreateEnemy,
actionOnGet: OnTakeFromPool,
actionOnRelease: OnReturnedToPool,
actionOnDestroy: OnDestroyPoolObject,
collectionCheck: false,
defaultCapacity: initialPoolSize,
maxSize: maxPoolSize
);
}
// Enemy 오브젝트 생성
private GameObject CreateEnemy()
{
var enemy = Instantiate(enemyPrefab);
enemy.GetComponent<Enemy>().SetPool(enemyPool);
return enemy;
}
// 풀에서 꺼낼 때 호출
private void OnTakeFromPool(GameObject enemy)
{
enemy.SetActive(true);
}
// 풀에 반환될 때 호출
private void OnReturnedToPool(GameObject enemy)
{
enemy.SetActive(false);
}
// 풀 크기를 초과해 제거될 때 호출
private void OnDestroyPoolObject(GameObject enemy)
{
Destroy(enemy);
}
// Enemy 스폰 및 초기화
public void SpawnEnemy(Vector3 position)
{
var enemy = enemyPool.Get();
enemy.GetComponent<Enemy>().Init(position);
}
}
👾 Enemy 스크립트 예시
Enemy가 사라질 때 스스로 풀로 돌아가도록 구성한다.
using UnityEngine;
using UnityEngine.Pool;
public class Enemy : MonoBehaviour
{
private ObjectPool<GameObject> pool;
private int maxHp = 100;
private int currentHp;
public void SetPool(ObjectPool<GameObject> objectPool)
{
pool = objectPool;
}
// 위치와 상태를 초기화하는 함수
public void Init(Vector3 spawnPosition)
{
currentHp = maxHp;
transform.position = spawnPosition;
// 추가 초기화: 애니메이션, AI 상태 등
// Rigidbody 속도 초기화 필요 시 여기서
var rb = GetComponent<Rigidbody>();
if (rb != null) rb.velocity = Vector3.zero;
}
public void TakeDamage(int damage)
{
currentHp -= damage;
if (currentHp <= 0)
{
Die();
}
}
private void Die()
{
pool.Release(gameObject);
}
}
🧠 주요 파라미터 설명
파라미터 | 설명 |
createFunc | 오브젝트를 새로 생성할 때 호출되는 함수 |
actionOnGet | 풀에서 오브젝트를 꺼낼 때 호출 (예: SetActive(true)) |
actionOnRelease | 오브젝트를 다시 반환할 때 호출 (예: SetActive(false)) |
actionOnDestroy | 풀 크기를 초과해 제거될 때 호출 |
collectionCheck | 중복 반환 감지 여부 (디버깅용, 성능 저하 가능) |
defaultCapacity | 초기 생성 개수 |
maxSize | 최대 풀 크기 (초과 시 actionOnDestroy 호출) |
🚀 장점
✅ 직접 구현 불필요
기존에 직접 Queue나 Stack으로 풀을 만들 필요가 없다.
✅ 일관된 인터페이스
Get(), Release() 메서드만으로 간단히 제어 가능.
✅ 성능 최적화
내부적으로 C++ 레벨에서 관리되어
직접 구현한 C# 풀보다 메모리 관리가 효율적이다.
✅ 자동 메모리 관리
최대 크기 초과 시 자동으로 파괴(OnDestroy)가 호출된다.
⚡ 실무 적용 팁
- 공통 풀 관리 클래스 작성하기
여러 종류의 오브젝트를 관리한다면,
풀 초기화를 통합 관리하는 PoolManager 클래스를 두는 것이 좋다. - 비활성화 시 상태 초기화하기
오브젝트를 반환할 때(actionOnRelease)
위치, 체력, 속도 등 상태를 초기화하면 다음 재사용 시 안정적이다. - maxSize 적정선 정하기
너무 크게 잡으면 메모리 낭비,
너무 작으면 빈번한 생성/파괴가 다시 발생한다.
플레이 환경에 맞는 균형 잡힌 크기를 실험적으로 정하자.
🧾 정리
직접 구현한 풀 | Unity 내장 풀 | |
구현 난이도 | 높음 | 매우 쉬움 |
제너릭 지원 | 직접 구현 필요 | 기본 지원 |
자동 관리 | ❌ | ✅ |
성능 | 좋음 | 더 안정적 |
유지보수 | 어려움 | 용이함 |
🧩 결론
- Unity 2021 이후 버전에서는 내장 오브젝트 풀(ObjectPool) 을 적극 활용하자.
- Instantiate() / Destroy()의 반복을 줄이면 프레임 안정성이 크게 향상된다.
- 적, 총알, 이펙트 등 자주 생성·파괴되는 오브젝트에 필수적이다.
- ObjectPool<T>는 가독성, 안정성, 성능을 모두 잡은 Unity의 정답 같은 기능이다.
'개발 > Unity' 카테고리의 다른 글
Unity에서 tag 비교할 때 CompareTag()를 사용해야 하는 이유 (0) | 2025.10.12 |
---|---|
Unity에서 커맨드(Command) 패턴 활용하기 (0) | 2025.10.11 |
Unity에서 이벤트버스(Event Bus) 패턴 활용하기 (0) | 2025.10.08 |
Unity에서 상태패턴으로 캐릭터 움직임과 공격 구현하기 (0) | 2025.10.07 |
Unity에서 추상 클래스 기반 Singleton 패턴 구현하기 (0) | 2025.10.06 |