가비지 컬렉션 (Garbage Collection) 이해와 활용
언리얼 엔진의 가비지 컬렉션(GC) 시스템은 메모리 관리를 자동화하여 개발자의 부담을 줄이고 메모리 누수를 방지합니다.
이 절에서는 GC의 원리, 작동 방식, 그리고 C++ 프로그래밍에서의 고려사항을 살펴보겠습니다.
가비지 컬렉션의 기본 개념
가비지 컬렉션은 더 이상 사용되지 않는 메모리를 자동으로 식별하고 해제하는 메모리 관리 기법입니다.
주요 특징은 다음과 같습니다.
- 자동 메모리 관리
- 순환 참조 문제 해결
- 댕글링 포인터 방지
언리얼 엔진의 가비지 컬렉션 구현
언리얼 엔진은 'Mark and Sweep' 알고리즘을 기반으로 한 가비지 컬렉션을 구현합니다.
- Mark 단계: 루트 세트에서 시작하여 도달 가능한 모든 객체를 표시
- Sweep 단계: 표시되지 않은 객체를 메모리에서 제거
가비지 컬렉션 대상 객체
언리얼 엔진에서 가비지 컬렉션 대상이 되는 객체는 다음과 같습니다.
- UObject를 상속받은 클래스의 인스턴스
- UPROPERTY 매크로로 선언된 포인터가 참조하는 객체
예시
UCLASS()
class MYGAME_API AMyActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY()
UMyObject* MyObject; // GC 대상
};
UPROPERTY 매크로 기반 GC 대상 지정
UPROPERTY 매크로는 객체를 GC 시스템에 등록합니다.
UCLASS()
class MYGAME_API UMyClass : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
UObject* ManagedObject; // GC에 의해 관리됨
UObject* UnmanagedObject; // GC에 의해 관리되지 않음
};
수동 가비지 컬렉션 제어
개발자가 직접 GC를 제어할 수 있는 방법들이 있습니다.
1. AddToRoot() 및 RemoveFromRoot()
- 객체를 루트 세트에 추가하거나 제거하여 GC를 제어할 수 있습니다.
UMyObject* MyObject = NewObject<UMyObject>();
MyObject->AddToRoot(); // GC 방지
// 사용 완료 후
MyObject->RemoveFromRoot(); // GC 허용
2. MarkPendingKill()
- 객체를 다음 GC 사이클에 삭제되도록 표시합니다.
MyObject->MarkPendingKill();
3. ConditionalBeginDestroy()
- 객체의 즉시 삭제를 요청합니다.
MyObject->ConditionalBeginDestroy();
가비지 컬렉션이 성능에 미치는 영향
GC는 자동화된 메모리 관리의 편리함을 제공하지만 성능 오버헤드를 동반할 수 있습니다.
- GC 사이클 동안 게임 스레드 일시 정지
- 메모리 단편화
- 예측 불가능한 타이밍의 성능 저하
가비지 컬렉션을 고려한 최적화 전략
- 객체 풀링 사용
UCLASS()
class MYGAME_API UMyObjectPool : public UObject
{
GENERATED_BODY()
private:
TArray<UMyObject*> Pool;
public:
UMyObject* GetObject()
{
if (Pool.Num() > 0)
return Pool.Pop();
return NewObject<UMyObject>();
}
void ReturnObject(UMyObject* Obj)
{
Pool.Push(Obj);
}
};
- 가능한 경우 UObject 대신 일반 C++ 클래스 사용
- 필요한 경우에만 UPROPERTY 사용
- 대량의 객체 생성 및 소멸 최소화
일반적인 문제 상황과 해결 방법
1. 순환 참조
문제
UCLASS()
class MYGAME_API UMyObjectA : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
UMyObjectB* B;
};
UCLASS()
class MYGAME_API UMyObjectB : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
UMyObjectA* A;
};
해결
- 약한 참조 사용 (TWeakObjectPtr)
- 순환 구조 재설계
2. 의도치 않은 객체 소멸
문제 : AddToRoot()를 사용했지만 RemoveFromRoot()를 호출하지 않음.
해결 : RAII 패턴 사용
class FScopedRootObject
{
public:
FScopedRootObject(UObject* InObject) : Object(InObject)
{
if (Object)
Object->AddToRoot();
}
~FScopedRootObject()
{
if (Object)
Object->RemoveFromRoot();
}
private:
UObject* Object;
};
3. 성능 저하
문제 : 빈번한 GC 호출로 인한 성능 저하
해결
- GC 빈도 조절 (엔진 설정 조정)
- 객체 풀링 사용
- 크리티컬 섹션에서 GC 비활성화
FGCScopeGuard GCGuard; // GC 일시 중지
// 크리티컬 코드 실행
// GCGuard 소멸 시 GC 재개
Best Practices
1. 명확한 소유권 정의
UPROPERTY(VisibleAnywhere, Category = "Components")
UStaticMeshComponent* MeshComponent;
2. 수명주기 관리 주의
void AMyActor::BeginPlay()
{
Super::BeginPlay();
MyObject = NewObject<UMyObject>(this); // 액터가 소유하므로 별도의 GC 관리 불필요
}
3. 디버깅 도구 활용
- 언리얼 엔진의 메모리 프로파일러 사용
- GC 로깅 활성화
UE_LOG(LogGarbage, Log, TEXT("GC Log: %s"), *ObjectName);
4. 대량 객체 처리 최적화
UCLASS()
class MYGAME_API UMySubsystem : public UWorldSubsystem
{
GENERATED_BODY()
private:
TArray<UMyObject*> ManagedObjects;
public:
void RegisterObject(UMyObject* Obj)
{
ManagedObjects.Add(Obj);
}
void UnregisterObject(UMyObject* Obj)
{
ManagedObjects.Remove(Obj);
}
virtual void Deinitialize() override
{
for (UMyObject* Obj : ManagedObjects)
{
Obj->MarkPendingKill();
}
ManagedObjects.Empty();
}
};
5. 가비지 컬렉션 친화적인 설계
- 객체 간 강한 결합 최소화
- 컴포넌트 기반 설계 활용
- 리소스의 동적 로딩/언로딩 고려
언리얼 엔진의 가비지 컬렉션 시스템은 메모리 관리를 크게 단순화하지만 효과적으로 활용하기 위해서는 그 작동 원리와 영향을 이해해야 합니다.
GC를 고려한 설계와 최적화를 통해 메모리 누수를 방지하고 성능을 향상시킬 수 있습니다.
필요에 따라 수동 메모리 관리 기법을 병행하는 것이 중요합니다. 객체 풀링, 적절한 UPROPERTY 사용, 그리고 순환 참조 방지 등의 기법을 통해 GC의 부담을 줄일 수 있습니다.