Unreal GC(Garbage Collection)
- 일반 c++은 메모리를 동적 할당 시에 직접 해제를 해줘야 하기 때문에, 같은 메모리를 두 번 지우거나, 지워진 메모리에 접근하는 등의 문제가 발생할 수 있다.
- 언리얼에서는 이와 같은 문제를 해결하기 위해 GC를 통해 메모리 관리를 한다.
- 일정시간마다 GC가 동작하면서 해당 객체를 더 이상 아무도 참조하지 않는다면 삭제하면서 메모리를 해제시켜주는 방식으로 동작한다.
GC 규칙
- UObject 상속 클래스만 관리
- UPROPERTY로 참조 표시 필수
- 절대 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"에 넣는 방법이다.
올바른 사용법
- 싱글톤
- 비동기 작업 보호
- 임시 보호 - GC 유발 가능한 작업들을 임시로 보호
주의사항
- RemoveFromRoot를 통해 루트에서 해제시켜야 함 -> 아니면 메모리에 끝까지 남아있음
- 불필요한 Root 추가
더 좋은 대안
- UPROPERTY 사용
- Outer 관계 활용
- 게임 인스턴스나 월드에 등록
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가 제공하는 핵심 기능
- 블루프린트 통합
- 에디터 통합
- 자동 저장/불러오기
UPROPERTY(SaveGame)
int32 PlayerLevel; // 자동으로 세이브 파일에 포함
- 리플렉션
- 메모리 자동 관리
언리얼 클래스 계층
UObject (모든 것의 시작)
├── AActor (3D 월드에 존재하는 것들)
│ ├── APawn (조종 가능한 객체)
│ │ └── ACharacter (걸어다니는 캐릭터)
│ ├── AController (두뇌 역할)
│ └── AGameMode (게임 규칙 관리)
├── UActorComponent (액터에 붙이는 부품들)
│ ├── UMeshComponent (모델 표시)
│ └── UMovementComponent (이동 처리)
└── UObject (순수 데이터/로직)
├── UGameInstance (게임 전체 관리)
└── UUserWidget (UI 요소)
- 장점
1. 통일성 - 모든 객체가 같은 방식으로 동작
2. 확장성 - 새 기능 추가 시 모든 객체가 자동으로 적용
3. 상호 운용성 - 서로 다른 시스템끼리 쉽게 연동
Outer 체인
- 객체 간 연결 관계를 소유 관계로 표시
- 주인이 사라지면 소유물도 자동 삭제
특징
- 자동 메모리 정리의 핵심 - 모든 객체를 일일이 정리할 필요없이 가장 상위의 것을 정리하면 연쇄 정리됨
- GC가 Outer 체인을 따라 작동
- 논리적 구조를 코드로 표현
자동 정리 과정
- 레벨 바뀔 때
(1) 현재 Level의 모든 Actor들에게 곧 사라진다고 알림
(2) 각 Actor의 모든 Component들 정리
(3) 각 Component의 모든 SubObject들 정리
(4) Level 자체 삭제
- 객체가 파괴될 때
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 참조로 저장
};
출처: 팀스파르타 내일배움캠프