숏컷 코드
spawn 요청
-> 풀에서 비어 있는 객체 확보
-> 상태 초기화
-> 사용 종료 시 반환문법과 예시
매 프레임 할당과 해제는 GC와 메모리 단편화를 야기한다
총알, 파티클, 데미지 숫자, 짧은 UI 토스트처럼 수명이 짧고 개수가 많은 오브젝트를 매번 Instantiate/Destroy 또는 new/delete로 처리하면 두 가지 문제가 생깁니다. 첫째, 관리형 언어(C#, Java)에서는 Garbage Collector(GC)가 주기적으로 heap을 정리하는데, 이 시점에 frame time이 급격히 치솟는 GC spike가 발생합니다. 둘째, 비관리 언어(C, C++)에서는 메모리 단편화로 연속 메모리 확보가 어려워지고 캐시 miss가 늘어납니다. 오브젝트 풀은 초기에 대상 객체를 미리 할당해 두고 재사용하므로 이 두 문제를 모두 피합니다.
// 나쁜 패턴: 매 발사마다 할당·해제
Bullet b = Instantiate(bulletPrefab);
// ... 사용 후
Destroy(b);
// 좋은 패턴: 풀에서 꺼내고 돌려놓기
Bullet b = pool.Acquire();
b.Init(position, direction);
// ... 사용 후
pool.Release(b);풀의 핵심은 재사용 그 자체가 아니라 reset 규칙이다
풀링에서 버그가 가장 많이 나는 곳은 객체를 재사용하기 전 상태 초기화입니다. 위치와 속도는 쉽게 초기화하지만, 이벤트 구독, trail renderer의 이전 경로, target reference 캐시, 오디오 소스의 재생 상태가 그대로 남아 있으면 이전 사용의 흔적이 다음 사용에 영향을 줍니다. OnAcquire와 OnRelease 훅을 명시적으로 정의해 "어떤 상태를 어디서 초기화/정리하는가"를 한 곳에서 관리하는 것이 가장 안전한 패턴입니다.
class Bullet : IPoolable {
void OnAcquire(Vector3 pos, Vector3 dir) {
transform.position = pos;
velocity = dir * speed;
trail.Clear(); // trail 기록 초기화
target = null; // 이전 target 캐시 제거
// 이벤트 구독은 OnAcquire에서 연결
}
void OnRelease() {
// 이벤트 구독 해제
gameObject.SetActive(false);
}
}풀 크기 전략은 "부족할 때 어떻게 할 것인가"가 핵심이다
풀이 비어 있을 때 대응 전략은 세 가지입니다. 첫째, 새로 할당해서 풀 크기를 늘립니다(grow). 초기 할당 비용을 런타임에 지불하지만 유연합니다. 둘째, spawn 요청을 무시하거나 효과를 생략합니다(drop). 성능 한계를 명시적으로 두되 UX 영향을 감수합니다. 셋째, 가장 오래된 객체를 강제 회수합니다(recycle). 파티클, 사운드처럼 오래된 것을 교체해도 무방한 경우에 적합합니다. 어떤 전략도 정답이 아니며, 게임 특성과 대상 객체 종류에 따라 다릅니다.
모든 것을 풀링할 필요는 없다
드물게 생성되는 보스, 씬 전체에서 하나만 존재하는 HUD, 복잡한 초기화가 거의 없는 객체는 일반 생성이 더 단순합니다. 풀링은 "자주 생성되고 빠르게 파괴되는, 동시에 개수가 많은" 객체에 가장 효과적입니다. 잘못된 대상에 풀링을 적용하면 복잡한 reset 로직만 늘어나고 얻는 이점은 거의 없습니다. Unity 2021+의 ObjectPool<T> 내장 API는 이런 판단과 구현을 표준화하는 데 유용합니다.
풀과 수명 관리를 설계할 때 핵심
| 상황 | 적합한 선택 |
|---|---|
| 투사체, 파티클, 짧은 UI 토스트 | 오브젝트 풀 적용 |
| 드문 보스, 씬 전체에서 하나인 객체 | 일반 생성이 더 단순 |
| 풀 부족 시 유연성 우선 | grow 전략 |
| 성능 한계를 명시적으로 두고 싶을 때 | drop 또는 recycle 전략 |
| 상태 초기화 관리 | OnAcquire / OnRelease 훅으로 명시적 분리 |
| 이전 사용 흔적이 자주 남는 객체 | 초기화 목록을 코드로 고정하고 테스트 추가 |
특이 케이스와 주의할 점
흔한 실패는 "GC가 무섭다"는 이유로 모든 객체를 풀링하는 것입니다. 수명이 길고 드물게 생성되는 객체까지 풀로 돌리면 reset 코드만 늘고 디버깅이 어려워집니다. 풀링은 생성/파괴 빈도와 동시 개수가 높은 대상에만 쓰고, 반환 누락과 상태 잔존을 테스트로 잡는 쪽이 훨씬 중요합니다.
참고 링크
1 sources