[UE] 언리얼 엔진의 메모리 관리 (Memory Management in UE)
언리얼 엔진의 메모리 관리 방식을 파악하고, 언리얼 오브젝트의 메모리를 관리하는 예제 실습을 한다.
- 언리얼 엔진의 메모리 관리 시스템의 이해
- 안정적으로 언리얼 오브젝트 포인터를 관리하는 방법의 학습
인프런 이득우님의 '언리얼 프로그래밍 Part1 - 언리얼 C++의 이해' 강의를 참고하였습니다.
😎 [이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해] 강의 들으러 가기!
목차
언리얼 엔진의 자동 메모리 관리
C++ 언어 메모리 관리의 문제점
- C++은 low 레벨 언어로, 메모리 주소에 직접 접근하는 포인터를 사용해서 오브젝트를 관리한다.
- 그러다보니 프로그래머가 직접 할당(new), 해지(delete) 짝 맞추기를 해야 한다.
- 이를 잘 지키지 못하는 경우 다양한 문제가 발생할 수 있다.
- 잘못된 포인터 사용 예시
- 메모리 누수(Leak) : new를 했는데 delete 짝을 맞추지 못함. 힙에 메모리가 그대로 남아있음.
- 허상(Dangling) 포인터 : (다른 곳에서) 이미 해제해 무효화된 오브젝트의 주소를 가리키는 포인터
- 와일드(Wild) 포인터 : 값이 초기화되지 않아 엉뚱한 주소를 가리키는 포인터.
- 잘못된 포인터 값은 다양한 문제를 일으키며, 한 번의 실수는 프로그램을 종료시킨다.
- 게임 규모가 커지고 구조가 복잡해질수록 프로그래머가 실수할 확률은 크게 증가한다.
C++ 이후에 나온 언어 Java/C# 은 이런 고질적인 문제를 해결하기 위해 포인터를 버리고
대신 가비지 컬렉션 시스템(Garbage Collection)을 도입했다.
가비지 컬렉션 시스템
- 프로그램에서 더 이상 사용하지 않는 오브젝트를 자동으로 감지해 메모리를 회수하는 시스템
- 동적으로 생성된 모든 오브젝트 정보를 모아둔 저장소를 사용해 사용되지 않는 메모리를 추적
- 마크-스윕(Mark-Sweep) 방식의 가비지 컬렉션
- 저장소에서 최초 검색을 시작하는 루트 오브젝트 표기한다.
- 루트 오브젝트가 참조하는 객체를 찾아 마크(Mark)한다.
- 마크된 객체로부터 다시 참조하는 객체를 찾아 마크하고 이를 계속 반복한다.
- 이제 저장소에는 마크된 객체와 마크되지 않은 객체의 두 그룹으로 나뉜다.
- 가비지 컬렉터가 저장소에서 마크되지 않은 객체(가비지)들의 메모리를 회수한다. (Sweep)
언리얼 엔진의 가비지 컬렉션 시스템
- 마크-스윕 방식의 가비지 컬렉션 시스템을 자체적으로 구축함.
- 지정된 주기마다 몰아서 없애도록 설정되어 있다. ( GCCycle, 기본 값 60초 )
- 성능 향상을 위해 병렬 처리, 클러스터링과 같은 기능을 탑재함. Project Setting 에서 관련 옵션 설정이 가능하다.
가비지 컬렉션을 위한 객체 저장소
- GUObjectArray: 관리되는 모든 언리얼 오브젝트의 정보를 저장하는 전역 변수
- 언리얼 엔진에서 관리되는 모든 언리얼 오브젝트의 정보를 저장하는 전역 변수다. G 접두사는 전역을 의미한다. 이는 언리얼 엔진이 활성화된 순간에 누구나 접근 가능하다.
- GUObjectArray의 각 요소에는 플래그(Flag) 가 설정되어 있음.
- Garbage 플래그 : 다른 언리얼 오브젝트로부터의 참조가 없어 회수 예정인 오브젝트. GC 는 이 플래그로 설정된 오브젝트를 파악 후 안전하게 회수한다. 시스템이 자동 회수하므로 수동 회수할 필요가 없다.
- RootSet 플래그 : 다른 언리얼 오브젝트로부터 참조가 없어도 회수하지 않는 특별한 오브젝트
가비지 컬렉터는 GUObjectArray 에 있는 플래그를 확인해
빠르게 회수해야 할 오브젝트를 파악하고 메모리에서 제거한다.
가비지 컬렉터의 메모리 회수
- 가비지 컬렉터는 지정된 시간에 따라 주기적으로 메모리를 회수한다. (기본 값 60초)
- Garbage 플래그로 설정된 오브젝트를 파악하고 메모리를 안전하게 회수함.
- Garbage 플래그는 수동으로 설정하는 것이 아닌, 시스템이 알아서 설정함.
한 번 생성된 언리얼 오브젝트는 바로 삭제가 불가능함
레퍼런스 정보를 없앰으로써 언리얼 GC가 자동으로 메모리를 회수하도록 설정하는 것이다.
루트셋 플래그의 설
- 만약 시스템이 실행되는 동안 제거되지 않아야 하는 오브젝트라면, GUObjectArray 에서 제공하는 AddToRoot 함수를 호출해 루트셋 플래그를 설정하면 최초 탐색 목록으로 설정됨. (그럼으로 메모리 회수가 되지 않음)
- 루트셋으로 설정된 언리얼 오브젝트는 메모리 회수로부터 보호받음.
- RemoveFromRoot 함수를 호출해 루트셋 플래그를 제거할 수 있음
이렇게 콘텐츠 관련 오브젝트에 루트셋을 설정하는 방법은 권장되지는 않음
언리얼 오브젝트를 통한 포인터 문제의 해결
- 메모리 누수 방지
- 언리얼 오브젝트는 GC를 통해 자동으로 해결.
- C++ 오브젝트는 직접 신경써야 함. (스마트 포인터 라이브러리를 활용하거나 직접 관리하는 방법이 있음)
- 댕글링 포인터 문제
- 어떤 오브젝트의 포인터가 외부로부터 해지되어 유효하지 않은지를 파악할 수 있어야 한다.
언리얼 오브젝트는 이를 탐지하기 위한 함수로 IsValid() 를 제공한다. - C++ 오브젝트는 직접 신경써야 함. (스마트 포인터 라이브러리를 활용하거나 직접 관리하는 방법이 있음)
- 어떤 오브젝트의 포인터가 외부로부터 해지되어 유효하지 않은지를 파악할 수 있어야 한다.
- 와일드 포인터 문제
- 언리얼 오브젝트에 UPROPERTY 속성을 지정하면 자동으로 nullptr 로 초기화 해 줌.
- C++ 오브젝트의 포인터는 직접 nullptr 로 초기화 할 것 (또는 스마트 포인터 라이브러리를 활용)
회수되지 않는 언리얼 오브젝트
언리얼 엔진은 어떤 형태로 언리얼 오브젝트의 회수를 결정할까?
- 언리얼 엔진 방식으로 참조를 설정한 언리얼 오브젝트 ( 대부분의 경우 이를 사용 )
- UPROPERTY 로 참조된 언리얼 오브젝트 포인터는 GC로부터 회수 되지 않음.
- UPROPERTY 를 사용할 수 없는 상황이라면 AddReferencedObject 함수를 통해 참조를 설정한 언리얼 오브젝트
- 루트셋(RootSet)으로 지정된 언리얼 오브젝트
- 이 방식은 해당 오브젝트가 굉장히 중요하다는 것을 의미하기 때문에 많이 사용하지는 않는다.
(오브젝트 선언의 기본 원칙을 따르자)
오브젝트 포인터는 가급적 UPROPERTY 로 선언하고,
메모리는 가비지컬렉터가 자동으로 관리하도록 위임한다.
일반 클래스에서 언리얼 오브젝트를 관리하는 경우
- UPROPERTY 를 사용하지 못하는 일반 C++ 클래스가 언리얼 오브젝트를 관리해야 하는 경우
- FGCObject 클래스를 상속받은 후 AddReferenceObjects 함수를 구현한다.
- 함수 구현 부에서 관리할 언리얼 오브젝트를 추가해 줌.
- 콘텐츠 제작에서 자주 사용되지는 않는다. 아래의 예제코드에서 사용법을 익혀두자.
언리얼 오브젝트의 관리 원칙
- 생성된 언리얼 오브젝트를 유지하기 위해서 레퍼런스 참조 방법을 설계할 것.
- 언리얼 오브젝트 내의 언리얼 오브젝트: UPROPERTY 사용
- 일반 C++ 오브젝트 내의 언리얼 오브젝트: FGCObject 상속받은 후 AddReferencedObjects 함수를 사용.
- 생성된 언리얼 오브젝트는 강제로 지우지 말 것 ⇒ 생성하고 GC에게 맡긴다.
- 참조를 끊는다 생각으로 설계할 것
- 가비지 컬렉터에게 회수를 재촉할 수는 있음 ( ForceGarbageCollection 함수 )
- 콘텐츠 제작에서 액터 오브젝트 소멸을 위해 Destroy 함수를 사용할 수 있으나, 바로 삭제는 안되고 플래그가 설정되는 것으로 내부 동작은 동일함. ( 가비지컬렉터에 위임 )
코드 실습
가비지 컬렉션 테스트 환경 제작
- 프로젝트 설정에서 가비지 컬렉션 GCCycle 시간을 3초로 단축 설정
- 새로운 GameInstance 의 두 함수를 오버라이드
- Init : 어플리케이션이 초기화될 때 호출
- Shutdown : 어플리케이션이 종료될 때 호출
- 테스트 시나리오
- 플레이 버튼을 누를 때 Init 함수에서 오브젝트를 생성하고
- 3초 이상 대기해 가비지 컬렉션을 발동
- 플레이 중지를 눌러 Shutdown 함수에서 생성한 오브젝트의 유효성을 확인
가비지 콜렉션 주기 변경
예제 코드
StudentManager.h
#pragma once
#include "CoreMinimal.h"
class UNREALMEMORY_API FStudentManager : public FGCObject // GCObject 상소
{
public:
FStudentManager(class UStudent* InStudent) : SafeStudent(InStudent) {}
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
virtual FString GetReferencerName() const override
{
return TEXT("FStudentManager");
}
const class UStudent* GetStudent() const { return SafeStudent; }
private:
class UStudent* SafeStudent = nullptr;
};
StudentManager.cpp
#include "StudentManager.h"
#include "Student.h"
void FStudentManager::AddReferencedObjects(FReferenceCollector& Collector)
{
if (SafeStudent->IsValidLowLevel())
{
Collector.AddReferencedObject(SafeStudent); // 등록
}
}
예제 코드
MyGameInstance.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"
UCLASS()
class UNREALMEMORY_API UMyGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
virtual void Init() override;
virtual void Shutdown() override;
private:
TObjectPtr<class UStudent> NonPropStudent;
UPROPERTY() // 댕글링 포인터 문제에서 벗어나기 위해 UPROPERTY를 붙임
TObjectPtr<class UStudent> PropStudent;
TArray<TObjectPtr<class UStudent>> NonPropStudents;
// TArray, TSet, TMap의 타입 인자로 언리얼 오브젝트 포인터가 들어갈 때, 반드시 UPROPERTY를 붙여줘야 안전하게 언리얼 오브젝트를 관리할 수 있다.
UPROPERTY()
TArray<TObjectPtr<class UStudent>> PropStudents;
class FStudentManager* StudentManager = nullptr;
};
MyGameInstance.cpp
#include "MyGameInstance.h"
#include "Student.h"
#include "StudentManager.h"
void CheckUObjectIsValid(const UObject* InObject, const FString& InTag)
{
if (InObject->IsValidLowLevel())
{
UE_LOG(LogTemp, Log, TEXT("[%s] 유효한 언리얼 오브젝트"), *InTag);
}
else
{
UE_LOG(LogTemp, Log, TEXT("[%s] 유효하지 않은 언리얼 오브젝트"), *InTag);
}
}
void CheckUObjectIsNull(const UObject* InObject, const FString& InTag)
{
if (nullptr == InObject)
{
UE_LOG(LogTemp, Log, TEXT("[%s] 널 포인터 언리얼 오브젝트"), *InTag);
}
else
{
UE_LOG(LogTemp, Log, TEXT("[%s] 널 포인터가 아닌 언리얼 오브젝트"), *InTag);
}
}
void UMyGameInstance::Init()
{
Super::Init();
NonPropStudent = NewObject<UStudent>();
PropStudent = NewObject<UStudent>();
NonPropStudents.Add(NewObject<UStudent>());
PropStudents.Add(NewObject<UStudent>());
StudentManager = new FStudentManager(NewObject<UStudent>());
}
void UMyGameInstance::Shutdown()
{
Super::Shutdown();
const UStudent* StudentInManager = StudentManager->GetStudent();
delete StudentManager;
StudentManager = nullptr;
CheckUObjectIsNull(StudentInManager, TEXT("StudentInManager"));
CheckUObjectIsValid(StudentInManager, TEXT("StudentInManager"));
CheckUObjectIsNull(NonPropStudent, TEXT("NonPropStudent"));
CheckUObjectIsValid(NonPropStudent, TEXT("NonPropStudent"));
CheckUObjectIsNull(PropStudent, TEXT("PropStudent"));
CheckUObjectIsValid(PropStudent, TEXT("PropStudent"));
CheckUObjectIsNull(NonPropStudents[0], TEXT("NonPropStudents"));
CheckUObjectIsValid(NonPropStudents[0], TEXT("NonPropStudents"));
CheckUObjectIsNull(PropStudents[0], TEXT("PropStudents"));
CheckUObjectIsValid(PropStudents[0], TEXT("PropStudents"));
}
실행화면
결론: UPROPERTY를 붙여줘야만 댕글링 포인터 문제로부터 벗어날 수 있다.
'⭐ Unreal Engine > UE 개념정리 - 언리얼의 이해' 카테고리의 다른 글
[UE] 언리얼 오브젝트 관리 II - 패키지 (Package) (0) | 2024.07.09 |
---|---|
[UE] 언리얼 오브젝트 관리 I - 직렬화 (UE Object Management I - Serialization) (0) | 2024.07.03 |
[UE] 언리얼 구조체와 TMap (1) | 2024.07.01 |
[UE] 언리얼 C++의 TArray, TSet, TMap 자료구조 라이브러리와 활용방법 (0) | 2024.04.20 |
[UE] 언리얼 C++ 설계 3 - 델리게이트 Delegate (0) | 2024.04.05 |
댓글
이 글 공유하기
다른 글
-
[UE] 언리얼 오브젝트 관리 II - 패키지 (Package)
[UE] 언리얼 오브젝트 관리 II - 패키지 (Package)
2024.07.09 -
[UE] 언리얼 오브젝트 관리 I - 직렬화 (UE Object Management I - Serialization)
[UE] 언리얼 오브젝트 관리 I - 직렬화 (UE Object Management I - Serialization)
2024.07.03 -
[UE] 언리얼 구조체와 TMap
[UE] 언리얼 구조체와 TMap
2024.07.01 -
[UE] 언리얼 C++의 TArray, TSet, TMap 자료구조 라이브러리와 활용방법
[UE] 언리얼 C++의 TArray, TSet, TMap 자료구조 라이브러리와 활용방법
2024.04.20