3_3_언리얼 메모리 관리 시스템(가비지 컬렉션)
강의 내용
언리얼 엔진의 메모리 관리 방식을 파악하고, 언리얼 오브젝트의 메모리를 관리하는 예제 실습
강의 목표
언리얼 엔진의 메모리 관리 시스템의 이해
안정적으로 언리얼 오브젝트 포인터를 관리하는 방법의 학습
언리얼 엔진의 자동 메모리 관리
C++ 언어 메모리 관리의 문제점 : 직접 관리해야 하기 때문에 메모리 누수, Dangling, Wild등 문제 발생 가능성 있음. 이런 실수 방지 위해 가비지 컬렉션 시스템 도입
가비지 컬렉션 시스템 : 동적 생성된 오브젝트 정보 모아둔 저장소를 사용해 메모리 추적하고 참조되지 않는 건 메모리 회수
언리얼 엔진의 가비지 컬렉션 시스템 : 마크-스윕 방식, 주기 설정 가능, 병렬처리, 클러스터링 같은 기능 탑제
가비지 컬렉션을 위한 객체 저장소 : GUObjectArray 전역 변수에 Flag가 설정되어 있어. Garbage, RootSet
가비지 컬렉터의 메모리 회수 : 지정된 주기 마다 회수, 플래그로 설정된 오브젝트 파악 하고 회수, 플래그는 시스템이 설정
루트셋 플래그의 설정 : AddToRoot 함수를 호출해 플래그 설정. 메모리 회수로부터 보호. RemoveFromRoot함수로 플래그 제거 가능. 컨텐츠 관련 오브젝트에 설정하는 건 비권장.
언리얼 오브젝트를 통한 포인터 문제의 해결 : 메모리 누수 자동 해결, 댕글링 포인터 확인 함수 ::IsValid(), 와일드 포인터는 UPROPERTY 지정하면 nullptr 자동 초기화
회수되지 않은 언리얼 오브젝트 : 언리얼 엔진 방식으로 참조를 선언한 언리얼 오브젝트. UPROPERTY, AddReferencedObject, RootSet
일반 클래스에서 언리얼 오브젝트를 관리하는 경우 : FGCObject클래스 상속 받은 후 AddReferencedObjects함수를 구현한다. 구현부에서 관리할 언리얼 오브젝트를 추가한다.
언리얼 오브젝트의 관리 원칙 : 생성된 언리얼 오브젝트를 유지하기 위해 레퍼런스 참조 방법 설계. 언리얼 오브젝트 내의 언리얼 오브젝트는 UPROPERTY, 일반 C++ 오브젝트 내의 언리얼 오브젝트는 FGCObject 상속후 구현, 생성된 언리얼 오브젝트는 강제로 지우려 하지 말것.
실습
UnrealMemory라는 이름으로 프로젝트를 생성하자.
하던대로 GameInstance 클래스를 추가한다.
그리고 Object를 상속받은 Student라는 클래스를 생성한다.
그리고 Project setting으로 가서 맵을 None으로 하고, GameInstance도 만든 것으로 세팅한다.
그리고 언리얼 에디터를 끄고 비쥬얼 스튜디오로 가서 리로드를 해준다.
테스트 프리뷰
GCCycle 3초로 설정
GameInstance의 두 함수 오버라이드
Init: 초기화될 때 호출
Shutdown: 종료될 때 호출
테스트 시나리오
플레이 버튼 누를 때 Init함수에서 오브젝트 생성
3초 가비지컬렉터 발동
중지 버튼 눌러서 Shutdown 함수에서 오브젝트의 유효성 확인
1. 언리얼 오브젝트의 클래스 멤버변수를 선언할 때 UPROPERTY() 쓰냐 안쓰냐
일단 MyGameInstance.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()
TObjectPtr<class UStudent> PropStudent;
};
이렇게 Init과 Shutdown을 선언하고 재정의 한다.
#include "MyGameInstance.h"
#include "Student.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>();
}
void UMyGameInstance::Shutdown()
{
Super::Shutdown();
CheckUObjectIsNull(NonPropStudent, TEXT("NonPropStudent"));
CheckUObjectIsValid(NonPropStudent, TEXT("NonPropStudent"));
CheckUObjectIsNull(PropStudent, TEXT("PropStudent"));
CheckUObjectIsValid(PropStudent, TEXT("PropStudent"));
}
한글이 들어가 있으니 다른 이름으로 저장하기로 인코딩 설정을 해준다.
컴파일을 해준다.
문제가 없다면 언리얼 엔진을 실행하연 GC 사이클 주기를 바꿔보자.
project setting의 Garbage Collection으로 가서 61초로 되어 있는 걸 3초로 변경을 해본다.
변경 후 에디터를 끄고 디버깅 없이 플레이로 다시 실행을 해본다.
언리얼이 켜지면 플레이 버튼을 눌러서 언리얼 오브젝트를 Init 함수에서 생성해본다.
로그 필터에 LogTemp를 입력하고, 3초 뒤 중지 버튼을 눌러서 Shutdown함수를 실행시켜 본다.
이걸 봐서 nullptr인지 아닌지만 보고 진행하게 되면, 댕글링 포인터 문제가 발생할 수 있게 된다.
즉 MyGameInstance.h에서 처럼 언리얼 오브젝트의 클래스 멤버변수를 선언할 때에는 반드시 UPROPERTY()를 붙여줘야지 이 댕글링 포인터 문제에서 벗어날 수 있다.
2. 자료구조 컨테이너 안의 언리얼 오브젝트에 UPROPERTY() 쓰냐 안쓰냐
이제는 유사하게 자료구조 컨테이너 안의 언리얼 오브젝트도 안전하게 관리하는 방법에 대해 살펴본다. MyGameInstance.h에서
TArray<TObjectPtr<class UStudent>> NonPropStudents;
UPROPERTY()
TArray<TObjectPtr<class UStudent>> PropStudents;
};
그리고 .cpp에서
void UMyGameInstance::Init()
{
Super::Init();
NonPropStudent = NewObject<UStudent>();
PropStudent = NewObject<UStudent>();
NonPropStudents.Add(NewObject<UStudent>());
PropStudents.Add(NewObject<UStudent>());
}
void UMyGameInstance::Shutdown()
{
Super::Shutdown();
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"));
}
이렇게 하고 언리얼 에디터를 끄고, 빌드를 한다.
디어깅 없이 실행을 하고, 플레이 버튼을 누르고 3초 뒤 중지 버튼을 누른다.
UPROPERTY()를 붙이지 않은 언리얼 컨테이너의 오브젝트의 경우는 nullptr이 아니지만 유용하지 않다는 걸 알 수 있고,
UPROPERTY()를 붙인 컨테이너의 오브젝트는 유용한 것을 알 수 있다.
TArray나 TSet, TMap 같은 자료 구조에 템플릿 인자로 언리얼 오브젝트 포인터가 들어가는 경우에는 반드시 UPROPERTY 매크로를 붙여줘야 안전하게 언리얼 오브젝트를 관리할 수 있게 된다.
3. 일반 C++ 오브젝트에서 언리얼 오브젝트 관리 할 때 FGCObject 상속
이번엔 일반 C++ 오브젝트에서 언리얼 오브젝트를 관리할 때 어떻게 관리해야 하는지에 대해 살펴보자.
언리얼 에디터로 가서 None을 상속한 일반 C++ 클래스를 만들어 본다. StudentManager라고 이름을 짖는다.
일단 StudentManager의 생성자와 소멸자 코드를 없앤다.
그리고 StudentManager.h에서 일반 C++ 클래스 오브젝트는 F라는 접두사를 붙이면 좋다.
class UNREALMEMORY_API FStudentManager
{
public:
FStudentManager(class UStudent* InStudent) : SafeStudent(InStudent) {}
const class UStudent* GetStudent() const { return SafeStudent; }
private:
class UStudent* SafeStudent = nullptr;
};
이렇게 UStudnet를 인자로 받는 생성자를 만들어 준다.
리렇게 기본적인 StudentManager클래스 객체를 선언했고, 이걸 MyGameInstance에서 사용해 볼거야.
class FStudentManager* StudentManager = nullptr;
};
이렇게 만든 포인터를 cpp에서 인스턴스를 만들어 준다.
#include "StudentManager.h”를 추가해주고,
Init함수에서
StudentManager = new FStudentManager(NewObject<UStudent>());
}
인스턴스를 만들어 주고,
void UMyGameInstance::Shutdown()
{
Super::Shutdown();
delete StudentManager;
StudentManager = nullptr;
Shutdown에서 삭제를 해준다.
빌드를 해준다.
이번에는 StudentManager가 가지고 있는 UnrealObject가 여전히 유효한지를 살펴 볼거야.
void UMyGameInstance::Shutdown()
{
Super::Shutdown();
const UStudent* StudnetInManager = StudentManager->GetStudent();
delete StudentManager;
StudentManager = nullptr;
CheckUObjectIsNull(StudnetInManager, TEXT("StudnetInManager"));
CheckUObjectIsValid(StudnetInManager, TEXT("StudnetInManager"));
내용물을 가져온 다음에 출력해 보도록 하자.
매니저가 지워졌을 때도 여전히 유효한지 살펴 보자.
여기서 Manager는 일반 C++ 객체이고 얘는 안에 있는 UObject를 관리할 수 있는 능력이 없다. UPROPERTY같은 걸 쓸 수 없기 떄문에 생성자로 UObject가 들어 왔을 떄 이것을 지킬 방법이 없다. 그래서 가비지 컬렉션이 발동되면 FStudentManager안에 있는 오브젝트는 회수가 될 것이다.
플레이를 눌러서 확인해 보자.
플레이 누르고 3초뒤 중지를 눌러 보면
널 포인터가 아니지만 유효하지 않은 언리얼 오브젝트라고 나온다.
댕글링 포인터 문제가 발생한 거 .
그렇다면 이걸 안정적으로 유지시킬 수 있는 방법에 대해 알아보자.
언리얼 가비지 컬렉터에게 일반 C++ 객체가 언리얼 오브젝트를 관리하겠다고 알려줘야 한다.
그러려면 FGCObject라는 특수한 클래스를 상속 받아서 관련 함수를 구현해줘야 한다.
두가지 함수를 구현해줘야 하는데 virtual void =0 추상 abstract로 구현된 AddReferencedObjects와 GetReferencerName두가지다.
virtual void AddReferencedObjects( FReferenceCollector& Collector ) = 0;
/** Overload this method to report a name for your referencer */
virtual FString GetReferencerName() const = 0;
FGCObject클래스의 이것을 복사해서
class UNREALMEMORY_API FStudentManager : public FGCObject
{
public:
FStudentManager(class UStudent* InStudent) : SafeStudent(InStudent) {}
virtual void AddReferencedObjects(FReferenceCollector& Collector)override;
virtual FString GetReferencerName() const override
{
return TEXT("FStudentManager");
}
이렇게 해주고, AddReferencedObjects는 cpp에서 구현을 해준다.
#include "StudentManager.h"
#include "Student.h"
void FStudentManager::AddReferencedObjects(FReferenceCollector& Collector)
{
if (SafeStudent->IsValidLowLevel())
{
Collector.AddReferencedObject(SafeStudent);
}
}
이렇게 구현하고 언리얼 에디터를 끄고 빌드를 한다.
디버깅 없이 실행을 한다.
결과를 보면 아까는 유용하지 않은 언리얼 오브젝트로 나왔던게 이제 유용하다고 나오는 걸 볼 수 있다.
이렇게 일반 C++클래스에서 언리얼 오브젝트를 관리할 때 어떤 형태로 선언을 하고, 어떤 함수를 구현해야 하는지 살펴 봤다.
정리
언리얼 메모리 관리 시스템
1. C++ 언어의 문제인 포인터 문제
2. 이를 해결하기 위한 가비지 콜렉션 원리와 이해, 설정 방법
3. 다양한 상황에서 언리얼 오브젝트를 생성하고 메모리에 유지하는 방법
4. 언리얼 오브젝트 포인터를 선언하는 코딩 규칙