객체 풀링과 GC
Object Pooling으로 GC 부하를 줄이고 객체 생성/파괴 비용을 제거하는 전략을 학습합니다
Object Pooling 기본 개념
객체를 재사용하여 GC 오버헤드를 제거하는 기법
풀링의 GC 이점
Object Pooling은 객체를 파괴하지 않고 비활성화한 채 보관했다가 필요 시 재활성화합니다. GC 관점에서 객체가 파괴되지 않으므로 GC Sweep 비용이 발생하지 않습니다.
// 풀링 없이: 매번 생성/파괴
SpawnProjectile() → NewObject + GUObjectArray 등록
DestroyProjectile() → MarkAsGarbage → BeginDestroy → GC
// 초당 100발 = 초당 100번 GC 작업
// 풀링 사용: 재활성화/비활성화
GetFromPool() → SetActorHiddenInGame(false) + 위치 설정
ReturnToPool() → SetActorHiddenInGame(true) + 비활성화
// GC 작업 = 0
Actor 풀링 구현
AActor 기반 객체 풀의 실전 구현
기본 Actor Pool
UCLASS()
class UActorPool : public UObject
{
GENERATED_BODY()
public:
void InitPool(UWorld* World, TSubclassOf<AActor> Class,
int32 PoolSize)
{
for (int32 i = 0; i < PoolSize; i++)
{
AActor* Actor = World->SpawnActor<AActor>(Class);
DeactivateActor(Actor);
InactivePool.Add(Actor);
}
}
AActor* GetFromPool(FVector Location, FRotator Rotation)
{
if (InactivePool.Num() == 0) return nullptr;
AActor* Actor = InactivePool.Pop();
ActivateActor(Actor, Location, Rotation);
ActivePool.Add(Actor);
return Actor;
}
void ReturnToPool(AActor* Actor)
{
ActivePool.Remove(Actor);
DeactivateActor(Actor);
InactivePool.Add(Actor);
}
private:
void DeactivateActor(AActor* Actor)
{
Actor->SetActorHiddenInGame(true);
Actor->SetActorEnableCollision(false);
Actor->SetActorTickEnabled(false);
Actor->SetActorLocation(FVector(0, 0, -10000));
}
void ActivateActor(AActor* A, FVector Loc, FRotator Rot)
{
A->SetActorLocationAndRotation(Loc, Rot);
A->SetActorHiddenInGame(false);
A->SetActorEnableCollision(true);
A->SetActorTickEnabled(true);
}
UPROPERTY()
TArray<AActor*> ActivePool;
UPROPERTY()
TArray<AActor*> InactivePool;
};
풀 내의 비활성 Actor들은 UPROPERTY TArray에 의해 강한 참조되므로 GC에 의해 수집되지 않습니다. 이것이 풀링의 핵심 - GC 사이클에서 완전히 제외되며, 객체 수는 일정하게 유지됩니다.
UObject 풀링 패턴
Actor가 아닌 일반 UObject의 풀링 기법
UObject Pool 구현
template<typename T>
class TUObjectPool
{
public:
T* Acquire(UObject* Outer)
{
if (FreeList.Num() > 0)
{
T* Obj = FreeList.Pop();
Obj->OnAcquiredFromPool(); // 커스텀 재활성화
return Obj;
}
// 풀이 비면 새로 생성
return NewObject<T>(Outer);
}
void Release(T* Obj)
{
Obj->OnReturnedToPool(); // 커스텀 비활성화
FreeList.Add(Obj);
}
// GC 참조 유지 - FGCObject 또는 UPROPERTY 필요
TArray<T*>& GetFreeList() { return FreeList; }
private:
TArray<T*> FreeList;
};
풀에서 꺼낸 객체는 이전 사용의 상태가 남아 있습니다. OnAcquiredFromPool()에서 모든 상태를 초기화해야 합니다. 특히 UPROPERTY 참조, 타이머, 델리게이트 바인딩을 반드시 정리하세요.
풀링 성능 효과 측정
풀링 전후의 GC 성능 비교
성능 비교
| 시나리오 (초당 100회 생성/파괴) | 풀링 없음 | 풀링 적용 |
|---|---|---|
| GC Mark 시간 | 2.5ms | 1.2ms |
| GC Sweep 시간 | 1.8ms | 0.1ms |
| GC 사이클당 파괴 객체 | ~6000 | ~0 |
| UObject 총 수 변동 | 큰 변동 | 일정 |
| GC 히치 빈도 | 높음 | 거의 없음 |
모든 객체를 풀링할 필요는 없습니다. 다음 기준으로 판단하세요:
- 초당 10회 이상 생성/파괴되는 객체 (투사체, 파티클, 피격 효과)
- 짧은 수명의 반복 생성 Actor (탄환, 파편)
- 생성 비용이 높은 객체 (복잡한 컴포넌트 구성)
핵심 요약
- Object Pooling은 객체를 비활성화하고 재사용하여 GC 비용을 제거합니다
- 풀 내 객체는 UPROPERTY 참조로 GC에서 보호되어야 합니다
- 풀에서 꺼낸 객체는 상태 초기화가 필수입니다
- 초당 10회 이상 생성/파괴되는 객체가 풀링의 주요 대상입니다
- 풀링 적용 시 GC Sweep 비용이 거의 0으로 감소합니다
도전 과제
배운 내용을 직접 실습해보세요
TArray 기반의 간단한 UObject 풀을 구현하세요. Acquire/Release 인터페이스를 만들고, 풀 사용 시 vs 매번 NewObject 생성 시의 GC 부하를 stat gc로 비교하세요. 풀 크기에 따른 메모리 트레이드오프도 분석하세요.
프로젝타일이나 이펙트 Actor를 풀링하는 시스템을 구현하세요. SetActorHiddenInGame, SetActorEnableCollision, SetActorTickEnabled를 활용하여 비활성화/활성화 패턴을 만들고, Destroy 대신 풀 반환을 사용하세요.
사용 패턴을 모니터링하여 자동으로 풀 크기를 확장/축소하는 적응형 풀링 시스템을 구현하세요. 최소/최대 풀 크기 제한, 미사용 객체의 시간 기반 정리, GC 부하에 따른 풀 크기 조절 등의 기능을 포함하세요.