언리얼의 메모리 관리

김혁·2025년 8월 18일

챌린지

목록 보기
7/14

Unreal GC(Garbage Collection)

  • 일반 c++은 메모리를 동적 할당 시에 직접 해제를 해줘야 하기 때문에, 같은 메모리를 두 번 지우거나, 지워진 메모리에 접근하는 등의 문제가 발생할 수 있다.
  • 언리얼에서는 이와 같은 문제를 해결하기 위해 GC를 통해 메모리 관리를 한다.
  • 일정시간마다 GC가 동작하면서 해당 객체를 더 이상 아무도 참조하지 않는다면 삭제하면서 메모리를 해제시켜주는 방식으로 동작한다.

GC 규칙

  1. UObject 상속 클래스만 관리
  2. UPROPERTY로 참조 표시 필수
  3. 절대 delete하면 안 됨 -> 직접적인 메모리 해제는 금지

GC 동작 원리 : Mark & Sweep

(1) Mark 단계

  • GC가 "Root Set"에서 시작해서 살아있는 객체를 탐색
    • Root Set 예시
      • UObject를 참조하는 글로벌 변수
      • 현재 로드된 Level의 Actor 및 컴포넌트
      • PlayerController, GameInstance 같은 엔진 주요 객체
      • C++에서 UPROPERTY()로 선언된 포인터들
  • 탐색 가능한 객체들을 "Marked(살아있음)"으로 표시
  • 참조 그래프를 따라가면서 연쇄적으로 객체들을 마킹

(2) Sweep 단계

  • Heap에 있는 모든 UObject를 순회
  • Mark되지 않은 객체는 사용되지 않는 객체로 판단하고, 메모리 해제

UE의 GC 특징

  • UObject 기반의 클래스만 추적하기 때문에 일반 c++ 클래스는 직접 관리해야 함
  • 객체 간 연결 관계를 "참조 체인(Reference Chain)"으로 분석해서 판단
  • World 단위로 동작 -> 레벨 변경 시 해당 World 관련 오브젝트들의 참조 체인을 분석
  • 성능 비용이 존재 -> 실행 중 잠깐 멈춤
  • 객체가 많으면 Mark 단계가 길어져 프레임 드랍 가능

수동으로 GC 실행

  • 특수한 상황에서만 강제로 실행
  • 너무 많이 실행하면 게임이 끊김
GEngine->ForceGarbageCollection(true);

AddToRoot

  • Mark 단계에서 무조건 살아있는 객체로 하기 위해서 "Root Set"에 넣는 방법이다.

올바른 사용법

  1. 싱글톤
  2. 비동기 작업 보호
  3. 임시 보호 - GC 유발 가능한 작업들을 임시로 보호

주의사항

  1. RemoveFromRoot를 통해 루트에서 해제시켜야 함 -> 아니면 메모리에 끝까지 남아있음
  2. 불필요한 Root 추가

더 좋은 대안

  1. UPROPERTY 사용
  2. Outer 관계 활용
  3. 게임 인스턴스나 월드에 등록

UPROPERTY

GC가 UPROPERTY만 보는 이유

1. 성능

  • 객체에는 수많은 변수들이 있기 때문에 모든 것을 검사하면 성능이 너무 떨어짐

2. 타입 안전성

  • GC가 볼 수 있는 타입에만 UPROPERTY를 부착함으로 런타임 에러를 방지하여 의도하지 않은 클래스 사용을 막음

3. 의도 파악

  • UObject 기반이어도 중요한 참조와 임시로 쓸 참조 같이 구분을 할 수 있어야 한다.
  • UPROPERTY 있음 -> 이건 중요한 참조, GC가 봐야 함
  • UPROPERTY 없음 -> 이건 임시로 쓸 것, GC가 안 봐도 됨

UPROPERTY 없는 참조 안전하게 다루기

  • UPROPERTY를 부착할 수 없는 것들은 직접 모든 포인터를 정리해줘야 함
class UAdvancedUsage : public UObject
{
    // 이런 것들은 UPROPERTY가 안 됨
    TSharedPtr<FMyData> SharedData;           // UE 스마트 포인터
    std::unique_ptr<SomeClass> UniquePtr;     // C++ 스마트 포인터

public:
    // 방법 1: 수명을 명확히 관리
    void SafeUsagePattern()
    {
        auto Data = MakeShared<FMyData>();
        SharedData = Data;

        ProcessData(SharedData.Get());  // 이 함수 안에서만 사용

        SharedData.Reset();  // 명시적 해제
    }

    // 방법 2: BeginDestroy에서 정리
    virtual void BeginDestroy() override
    {
        SharedData.Reset();     // 죽기 전에 모든 포인터 정리
        UniquePtr.reset();

        Super::BeginDestroy();
    }
};

UObject

UObject가 제공하는 핵심 기능

  1. 블루프린트 통합
  2. 에디터 통합
  3. 자동 저장/불러오기
UPROPERTY(SaveGame)
int32 PlayerLevel;	// 자동으로 세이브 파일에 포함
  1. 리플렉션
  2. 메모리 자동 관리

언리얼 클래스 계층

UObject (모든 것의 시작)
├── AActor (3D 월드에 존재하는 것들)
│   ├── APawn (조종 가능한 객체)
│   │   └── ACharacter (걸어다니는 캐릭터)
│   ├── AController (두뇌 역할)
│   └── AGameMode (게임 규칙 관리)
├── UActorComponent (액터에 붙이는 부품들)
│   ├── UMeshComponent (모델 표시)
│   └── UMovementComponent (이동 처리)
└── UObject (순수 데이터/로직)
    ├── UGameInstance (게임 전체 관리)
    └── UUserWidget (UI 요소)
  • 장점
    1. 통일성 - 모든 객체가 같은 방식으로 동작
    2. 확장성 - 새 기능 추가 시 모든 객체가 자동으로 적용
    3. 상호 운용성 - 서로 다른 시스템끼리 쉽게 연동

Outer 체인

  • 객체 간 연결 관계를 소유 관계로 표시
  • 주인이 사라지면 소유물도 자동 삭제

특징

  1. 자동 메모리 정리의 핵심 - 모든 객체를 일일이 정리할 필요없이 가장 상위의 것을 정리하면 연쇄 정리됨
  2. GC가 Outer 체인을 따라 작동
  3. 논리적 구조를 코드로 표현

자동 정리 과정

  1. 레벨 바뀔 때
    (1) 현재 Level의 모든 Actor들에게 곧 사라진다고 알림
    (2) 각 Actor의 모든 Component들 정리
    (3) 각 Component의 모든 SubObject들 정리
    (4) Level 자체 삭제
  2. 객체가 파괴될 때

NewObject에서 Outer 지정

// NewObject의 기본 형태
UObject* NewObj = NewObject<UMyClass>(
    this,                    // ← 이 부분이 Outer (주인)
    UMyClass::StaticClass(), // 클래스 타입
    TEXT("ObjectName")       // 이름 (선택)
);

// 다양한 Outer 선택 예시
UObject* TempObj = NewObject<UObject>(GetTransientPackage());     // 임시 객체
UObject* WorldObj = NewObject<UObject>(GetWorld());              // 월드 레벨
UObject* GameObj = NewObject<UObject>(GetGameInstance());        // 게임 전체
UActorComponent* Comp = NewObject<UActorComponent>(MyActor);     // 액터 소유

CreateDefaultSubobject vs NewObject

CreateDefaultSubobject

  • 생성자에서 미리 생성
  • 필수적으로 필요한 기본 구성품만
  • 에디터에서 보이고, 항상 같기 때문에 예측 가능함
  • 메시, 카메라, 기본 컴포넌트 등에서 사용
class AMyActor : public AActor
{
	AMyActor()
    {
    	MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
    }
    
private:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Component")
    UStaticMeshComponent* MeshComp;
}

NewObject

  • 동적으로 생성할 때 사용
  • 런타임 전용이기 때문에 에디터에서 보이지 않음
  • 아이템, 능력, 임시 객체 등에서 사용
void AMyActor::OnLevelUp()
{
	if (PlayerLevel >= 10)
    {
    	USpecialAbility* NewAbility = NewObject<USpecialAbility>(
        	this, USpecialAbility::StaticClass(), TEXT("SpecialAbil") 
        );
        
        Abilities.Add(NewAbility);
    }
}

순환 참조

순환 참조의 문제

  • 두 개의 객체가 서로 강한 참조를 통해 참조하고 있어서, 영원히 메모리에 안 사라지는 문제
class UParent : public UObject
{
    UPROPERTY()
    UChild* Child;  // Parent가 Child를 Hard 참조
};

class UChild : public UObject
{
    UPROPERTY()
    UParent* Parent;  // Child도 Parent를 Hard 참조
};

해결법

1. 한 쪽을 Weak(약한 참조)로 수정

UCLASS()
class UParent : public UObject
{
    GENERATED_BODY()
public:
    UPROPERTY()
    UChild* Child;  // Parent → Child는 Hard (소유)

    void CreateChild()
    {
        Child = NewObject<UChild>(this);
        Child->SetParent(this);  // 자식에게 부모 알려주기
    }
};

UCLASS()
class UChild : public UObject
{
    GENERATED_BODY()
public:
    UPROPERTY()
    TWeakObjectPtr<UParent> Parent;  // Child → Parent는 Weak (역참조)

    void SetParent(UParent* NewParent)
    {
        Parent = NewParent;
    }
};

2. 인터페이스로 의존성 줄이기

// 1. 인터페이스 정의
UINTERFACE()
class UHealthOwner : public UInterface
{
    GENERATED_BODY()
};

class IHealthOwner
{
    GENERATED_BODY()
public:
    virtual float GetHealth() const = 0;
    virtual void TakeDamage(float Damage) = 0;
};

// 2. 캐릭터가 인터페이스 구현
UCLASS()
class AMyCharacter : public AActor, public IHealthOwner
{
    GENERATED_BODY()

    float Health = 100.0f;

public:
    virtual float GetHealth() const override { return Health; }
    virtual void TakeDamage(float Damage) override { Health -= Damage; }
};

// 3. UI가 인터페이스로 소통
UCLASS()
class UHealthBar : public UUserWidget
{
    GENERATED_BODY()

    UPROPERTY()
    TWeakObjectPtr<UObject> Target;  // 구체적인 타입을 모름!

public:
    void UpdateHealthBar()
    {
        if (!Target.IsValid()) return;

        UObject* Obj = Target.Get();

        // 인터페이스를 구현했는지 확인
        if (Obj->GetClass()->ImplementsInterface(UHealthOwner::StaticClass()))
        {
            // 인터페이스를 통해 소통
            IHealthOwner* HealthOwner = Cast<IHealthOwner>(Obj);
            float CurrentHealth = HealthOwner->GetHealth();
            
            /** UI 업데이트 **/
        }
    }
};

3. 델리게이트로 이벤트 기반 소통

class UEventSender : public UObject
{
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealth);

    UPROPERTY(BlueprintAssignable)
    FOnHealthChanged OnHealthChanged;

    void ChangeHealth(float NewHealth)
    {
        OnHealthChanged.Broadcast(NewHealth);  // 이벤트 발생
    }
};

class UEventReceiver : public UObject
{
public:
    void StartListening(UEventSender* Sender)
    {
        if (Sender)
        {
            // 이벤트 바인딩
            Sender->OnHealthChanged.AddDynamic(this, &UEventReceiver::OnHealthChanged);
        }
    }

    virtual void BeginDestroy() override
    {
        // 중요: 죽기 전에 반드시 언바인딩!
        if (EventSender && IsValid(EventSender))
        {
            EventSender->OnHealthChanged.RemoveDynamic(this, &UEventReceiver::OnHealthChanged);
        }

        Super::BeginDestroy();
    }

    UFUNCTION()
    void OnHealthChanged(float NewHealth)
    {
        // 이벤트 처리
    }

private:
    UPROPERTY()
    TWeakObjectPtr<UEventSender> EventSender;  // Weak 참조로 저장
};


출처: 팀스파르타 내일배움캠프
profile
게임 개발자를 향해..

0개의 댓글