icon안동민 개발노트

가비지 컬렉션 (Garbage Collection) 이해와 활용


 언리얼 엔진의 가비지 컬렉션(GC) 시스템은 메모리 관리를 자동화하여 개발자의 부담을 줄이고 메모리 누수를 방지합니다.

 이 절에서는 GC의 원리, 작동 방식, 그리고 C++ 프로그래밍에서의 고려사항을 살펴보겠습니다.

가비지 컬렉션의 기본 개념

 가비지 컬렉션은 더 이상 사용되지 않는 메모리를 자동으로 식별하고 해제하는 메모리 관리 기법입니다.

 주요 특징은 다음과 같습니다.

  1. 자동 메모리 관리
  2. 순환 참조 문제 해결
  3. 댕글링 포인터 방지

언리얼 엔진의 가비지 컬렉션 구현

 언리얼 엔진은 'Mark and Sweep' 알고리즘을 기반으로 한 가비지 컬렉션을 구현합니다.

  1. Mark 단계: 루트 세트에서 시작하여 도달 가능한 모든 객체를 표시
  2. Sweep 단계: 표시되지 않은 객체를 메모리에서 제거

가비지 컬렉션 대상 객체

 언리얼 엔진에서 가비지 컬렉션 대상이 되는 객체는 다음과 같습니다.

  1. UObject를 상속받은 클래스의 인스턴스
  2. 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는 자동화된 메모리 관리의 편리함을 제공하지만 성능 오버헤드를 동반할 수 있습니다.

  1. GC 사이클 동안 게임 스레드 일시 정지
  2. 메모리 단편화
  3. 예측 불가능한 타이밍의 성능 저하

가비지 컬렉션을 고려한 최적화 전략

  1. 객체 풀링 사용
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);
    }
};
  1. 가능한 경우 UObject 대신 일반 C++ 클래스 사용
  2. 필요한 경우에만 UPROPERTY 사용
  3. 대량의 객체 생성 및 소멸 최소화

일반적인 문제 상황과 해결 방법

 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의 부담을 줄일 수 있습니다.