1. 오브젝트 풀링이란?
오브젝트 풀링은 필요할 때마다 오브젝트를 새로 생성하고 파괴하는 대신, 미리 일정량의 오브젝트를 생성하여 '풀(Pool)'에 저장해두고 재사용하는 디자인 패턴입니다. 오브젝트가 필요하면 풀에서 가져와 사용하고, 사용이 끝나면 파괴하는 대신 풀에 다시 반납하여 비활성화 상태로 보관합니다.
특히 총알, 파티클, 적 유닛 등과 같이 생성과 소멸이 빈번하게 일어나는 오브젝트에 매우 효과적입니다. 메모리 할당 및 해제에 드는 비용과 가비지 컬렉션(GC)으로 인한 성능 저하를 크게 줄일 수 있기 때문입니다.
2. 오브젝트 풀링의 필요성
2-1. 성능 문제: 동적 할당과 해제의 비용
게임 실행 중 new/malloc (생성)과 delete/free (소멸) 연산은 생각보다 큰 비용을 수반합니다.
- 메모리 할당 (Allocation):
- 운영체제는 프로그램에 적절한 크기의 연속된 메모리 블록을 찾아 할당해야 합니다. 이 과정에서 메모리 단편화(Memory Fragmentation)가 발생할 수 있으며, 가용 메모리를 찾는 탐색 시간 자체가 오버헤드가 됩니다.
- 언리얼 엔진의 경우
UObject기반의 액터를 생성(SpawnActor)할 때, 메모리 할당뿐만 아니라 리플렉션 시스템 등록, 초기화 등 복잡한 과정을 거치므로 비용이 더욱 큽니다.
- 메모리 해제 (Deallocation):
- 할당된 메모리를 시스템에 반환하는 과정 역시 즉각적으로 이루어지지 않을 수 있습니다.
- 특히 언리얼 엔진의 가비지 컬렉션(GC)은 더 이상 참조되지 않는
UObject를 찾아 메모리에서 해제하는 방식으로 동작합니다. GC는 특정 주기마다 실행되는데, 이 과정에서 게임 스레드가 잠시 멈추는 'GC 스파이크(Spike)' 현상이 발생하여 프레임 드랍의 주된 원인이 됩니다.
2-2. 문제 상황 예시
수백 발의 총알이 발사되는 슈팅 게임을 가정해 보겠습니다.
- 풀링 미적용 시:
- 총알이 발사될 때마다
SpawnActor로 총알 액터를 생성합니다. (메모리 할당 및 초기화 비용 발생) - 총알이 목표에 맞거나 화면 밖으로 사라지면
Destroy함수를 호출합니다. Destroy된 액터는 당장 메모리에서 해제되지 않고, 다음 GC가 실행될 때까지 'Pending Kill' 상태로 대기합니다.- GC가 실행되면 수많은 총알 액터 객체들을 탐색하고 메모리에서 해제하면서 큰 폭의 성능 저하(Hitch)를 유발합니다.
- 총알이 발사될 때마다
- 풀링 적용 시:
- 게임 시작 시점에 100개의 총알 액터를 미리 생성하여 비활성화 상태로 풀에 넣어둡니다.
- 총알이 발사되면 풀에서 비활성화된 총알 하나를 가져와 위치, 방향 등을 설정하고 활성화합니다. (매우 빠른 연산)
- 총알의 역할이 끝나면
Destroy대신, 다시 비활성화하여 풀에 반납합니다. - 이 과정에서는 새로운 메모리 할당/해제가 전혀 없으므로 GC 부담이 사라지고, 성능 스파이크 없이 부드러운 게임 플레이가 가능해집니다.
3. 내부 구조 및 구현 방식
3-1. 핵심 구성 요소
- 풀 (Pool):
- 재사용할 오브젝트들을 담아두는 컨테이너입니다.
- 주로
TArray<APoolableActor*>또는TQueue<APoolableActor*>를 사용합니다.TArray: 인덱스를 통한 접근이 필요하거나, 풀의 모든 오브젝트를 순회해야 할 때 유용합니다.TQueue: 선입선출(FIFO) 구조로, 오브젝트를 가져오고 반납하는 연산이O(1)으로 매우 빠르기 때문에 풀링 시스템에 더 적합한 경우가 많습니다.
- 풀링 매니저 (Pooling Manager):
- 오브젝트 풀을 소유하고 관리하는 주체입니다.
- 주로
UGameInstanceSubsystem이나UActorComponent로 만들어져 게임 월드 내에서 싱글톤처럼 동작하게 합니다. - 주요 기능:
- 초기화: 게임 시작 시점에 정해진 수의 오브젝트를 미리 생성하여 풀에 채워 넣습니다.
- 오브젝트 제공 (Get): 외부에서 오브젝트를 요청하면, 풀에서 사용 가능한 오브젝트를 찾아 반환합니다. 만약 풀이 비어있다면, 설정에 따라 새로운 오브젝트를 동적으로 생성하거나 null을 반환할 수 있습니다.
- 오브젝트 반납 (Return): 사용이 끝난 오브젝트를 다시 풀로 돌려받아 비활성화 상태로 만듭니다.
- 풀링 대상 오브젝트 (Poolable Object):
- 풀에서 관리될 액터 또는 오브젝트입니다.
- 재사용을 위해 상태를 초기화하고, 활성화/비활성화하는 기능이 필요합니다.
- 보통 인터페이스(
IPoolable)를 구현하여 다음과 같은 함수를 정의합니다.OnActivated(): 풀에서 꺼내져 사용될 때 호출됩니다. (예: 위치 설정, 물리 시뮬레이션 켜기, 렌더링 활성화)OnDeactivated(): 풀로 반납될 때 호출됩니다. (예: 물리 시뮬레이션 끄기, 렌더링 비활성화, 타이머 초기화)
3-2. 간단한 구현 예시 (TQueue 사용)
// 1. 풀링 가능한 오브젝트를 위한 인터페이스
UINTERFACE(MinimalAPI)
class UPoolable : public UInterface
{
GENERATED_BODY()
};
class IPoolable
{
GENERATED_BODY()
public:
virtual void OnActivated() = 0;
virtual void OnDeactivated() = 0;
};
// 2. 풀링될 액터 (예: 총알)
class AProjectile : public AActor, public IPoolable
{
GENERATED_BODY()
public:
// ... 액터 기본 설정 ...
virtual void OnActivated() override
{
SetActorHiddenInGame(false);
SetActorTickEnabled(true);
ProjectileMovement->Activate();
// ... 초기 위치, 속도 등 설정 ...
}
virtual void OnDeactivated() override
{
SetActorHiddenInGame(true);
SetActorTickEnabled(false);
ProjectileMovement->Deactivate();
// 월드 밖으로 이동시켜 충돌 및 렌더링 방지
SetActorLocation(FVector(0, 0, -10000.0f));
}
// 사용이 끝나면 호출될 함수
void ReturnToPool()
{
// 풀 매니저에게 자신을 반납
UObjectPoolManager* PoolManager = GetGameInstance()->GetSubsystem<UObjectPoolManager>();
if (PoolManager)
{
PoolManager->ReturnObject(this);
}
}
};
// 3. 오브젝트 풀 매니저
UCLASS()
class UObjectPoolManager : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
// 오브젝트 가져오기
AProjectile* GetObject()
{
AProjectile* PooledObject = nullptr;
if (ObjectPool.IsEmpty())
{
// 풀이 비어있으면 새로 생성 (혹은 null 반환)
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
PooledObject = GetWorld()->SpawnActor<AProjectile>(ProjectileClass, SpawnParams);
}
else
{
ObjectPool.Dequeue(PooledObject);
}
if (PooledObject)
{
PooledObject->OnActivated();
}
return PooledObject;
}
// 오브젝트 반납하기
void ReturnObject(AProjectile* ObjectToReturn)
{
if (ObjectToReturn)
{
ObjectToReturn->OnDeactivated();
ObjectPool.Enqueue(ObjectToReturn);
}
}
protected:
virtual void Initialize(FSubsystemCollectionBase& Collection) override
{
Super::Initialize(Collection);
// 게임 시작 시 100개 미리 생성
if (ProjectileClass)
{
for (int32 i = 0; i < InitialPoolSize; ++i)
{
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AProjectile* NewObject = GetWorld()->SpawnActor<AProjectile>(ProjectileClass, SpawnParams);
if (NewObject)
{
NewObject->OnDeactivated();
ObjectPool.Enqueue(NewObject);
}
}
}
}
private:
// 풀 컨테이너
TQueue<AProjectile*> ObjectPool;
// 생성할 액터 클래스 (블루프린트에서 설정)
UPROPERTY(EditDefaultsOnly, Category = "Object Pool")
TSubclassOf<AProjectile> ProjectileClass;
UPROPERTY(EditDefaultsOnly, Category = "Object Pool")
int32 InitialPoolSize = 100;
};
4. 동작 계층 구조
오브젝트 풀링의 동작은 사용자 코드 레벨에서부터 엔진 내부 시스템까지 여러 계층에 걸쳐 상호작용합니다.
- 사용자 영역 (Game Logic Layer):
- 플레이어가 총을 발사하는 등 특정 이벤트가 발생합니다.
- 게임 로직 코드(예:
ACharacter클래스)는UObjectPoolManager에게 "총알 오브젝트 하나 주세요"라고 요청합니다 (GetObject()).
- 풀링 매니저 (Pooling System Layer):
GetObject()함수가 호출됩니다.- 매니저는 내부의
TQueue(풀)를 확인합니다. - (Case A: 풀에 오브젝트가 있을 경우)
TQueue::Dequeue()를 통해 오브젝트의 포인터를 꺼냅니다.- 해당 오브젝트의
OnActivated()인터페이스 함수를 호출합니다. - 오브젝트의 포인터를 사용자 영역으로 반환합니다.
- (Case B: 풀이 비어있을 경우 - 초기화 단계)
UWorld::SpawnActor()를 호출하여 새로운 액터를 생성합니다. 이 과정은 엔진 내부로 이어집니다.- 생성된 액터의
OnDeactivated()를 호출하여 비활성화하고 풀에Enqueue합니다.
- 언리얼 엔진 코어 (Engine Core Layer):
UWorld::SpawnActor()가 호출되면 엔진은 다음의 복잡한 과정을 수행합니다.- 메모리 할당:
UObject시스템이 객체를 위한 메모리를 할당합니다. - 생성자 호출: 해당 액터의 C++ 생성자를 호출합니다.
- 리플렉션 등록: 생성된 객체를 언리얼의 리플렉션 시스템(UObject 체계)에 등록합니다.
- 컴포넌트 초기화 및 등록: 액터에 속한 모든 컴포넌트(
UActorComponent)를 생성하고 초기화합니다. BeginPlay()호출: 액터와 컴포넌트들의BeginPlay함수를 호출합니다.
- 메모리 할당:
- 이 모든 과정은 풀링 시스템에서는 게임 시작 초기에만 수행되고, 게임 플레이 중에는 거의 발생하지 않습니다.
- 오브젝트 상태 변경 (Object State Layer):
OnActivated()가 호출된 오브젝트는 다시 게임 월드에서 상호작용을 시작합니다.SetActorHiddenInGame(false): 렌더링 스레드가 이 액터를 렌더링 큐에 포함시킵니다.SetActorTickEnabled(true): 매 프레임Tick()함수가 호출되도록 스케줄링합니다.ProjectileMovement->Activate(): 무브먼트 컴포넌트가 물리 계산을 다시 시작합니다.
- 반대로
OnDeactivated()가 호출되면 이러한 기능들이 모두 비활성화되어, CPU와 GPU 자원을 거의 소모하지 않는 '휴면' 상태가 됩니다.
5. 동적 생성 vs 오브젝트 풀링
| 항목 | 동적 생성 (Spawn/Destroy) | 오브젝트 풀링 (Object Pooling) |
|---|---|---|
| 성능 | - 생성/소멸 시 높은 CPU 비용 발생 - GC 스파이크로 인한 프레임 드랍 유발 |
- 거의 0에 가까운 획득/반납 비용 - GC 부담이 없어 안정적인 프레임 유지 |
| 메모리 | - 잦은 할당/해제로 메모리 단편화 유발 가능성 - GC 전까지 메모리에 객체가 남아있음 |
- 시작 시점에 정해진 메모리를 점유 - 메모리 사용량이 예측 가능하고 안정적 |
| 구현 복잡도 | - 매우 간단 (SpawnActor, Destroy) |
- 초기 설계 및 구현 필요 (매니저, 인터페이스 등) - 오브젝트 상태 초기화 로직을 꼼꼼히 작성해야 함 |
| 주요 사용처 | - 생성/소멸 빈도가 낮은 오브젝트 - 수명이 긴 배경 액터, 플레이어 캐릭터 등 |
- 생성/소멸이 매우 빈번한 오브젝트 - 총알, 파티클, 이펙트, 재사용되는 적 유닛 등 |
6. 요약
오브젝트 풀링은 게임에서 총알처럼 자주 만들어지고 없어지는 오브젝트들을 미리 한꺼번에 만들어놓고, 필요할 때마다 '빌려 쓰고 반납하는' 재활용 시스템입니다. 매번 새로 만들고 파괴하면 컴퓨터(CPU)가 힘들어하고, 특히 언리얼 엔진에서는 가비지 컬렉션(GC)이라는 청소 시간이 길어져서 게임이 순간적으로 뚝 끊기는 현상(프레임 드랍)이 생길 수 있습니다. 풀링을 쓰면 이런 성능 문제를 예방하고 아주 부드러운 게임 환경을 만들 수 있습니다.
언리얼 엔진에서 오브젝트 풀링은 어떻게 구현하나요?
보통 '풀 매니저'라는 관리자를 하나 만듭니다. 이 관리자가 게임이 시작될 때 필요한 오브젝트들을 왕창 생성해서 창고(TQueue 같은 곳)에 넣어둡니다. 그리고 다른 캐릭터가 "총알 하나 줘!" 하고 요청하면, 창고에서 잠자고 있던 총알 하나를 꺼내서 깨운 다음에(OnActivated) 빌려줍니다. 총알이 벽에 부딪히는 등 자기 역할을 다하면, 파괴하는 대신 다시 창고로 돌려보내서 재웁니다(OnDeactivated). 이렇게 하면 새로 만드는 과정 없이 계속 재활용할 수 있습니다.
오브젝트 풀링을 사용하면 항상 좋은 건가요? 언제 쓰는 게 가장 효과적인가요?
항상 좋은 것은 아닙니다. 구현이 조금 더 복잡해지고, 처음부터 메모리를 차지하는 단점이 있습니다. 따라서 플레이어나 게임 매니저처럼 게임 내내 몇 개 없는 오브젝트에는 굳이 쓸 필요가 없습니다. 하지만 총알, 미사일, 폭발 이펙트, 혈흔 효과, 주기적으로 나타나는 몬스터 등 짧은 시간 동안 대량으로 생성과 소멸이 반복되는 오브젝트에 사용하면 성능 향상 효과가 매우 큽니다. 즉, '다회용품'처럼 계속 재사용할 수 있는 것들에 적용하는 것이 핵심입니다.
'Unreal' 카테고리의 다른 글
| Voxel Terrain (0) | 2026.01.06 |
|---|---|
| EOS 보이스 채팅 구현 (0) | 2026.01.05 |
| EOS 보이스 인터페이스 (0) | 2025.12.26 |
| Unreal Engine 패키징 (0) | 2025.12.24 |
| Installed Engine vs Source Build Engine (0) | 2025.12.23 |
