Unreal5/Unreal C++
[U C++] 직렬화(Serialization)
taene_
2024. 7. 12. 00:40
직렬화의 정의
- 오브젝트나 오브젝트끼리 연결된 오브젝트 묶음(오브젝트 그래프)을 디스크나 네트워크로 전송할 수 있는 바이트 스트림으로 변환하는 과정
- 복잡한 데이터를 일렬로 세우기 때문에 직렬화라고 한다.
- 직렬화는 어떤 복잡한 데이터를 한 줄로 변환하는 작업뿐만 아니라 거꾸로 복구시키는 과정도 포함해서 의미한다.
- Serialization: 오브젝트 그래프에서 바이트 스트림으로
- Deserialization: 바이트 스트림에서 오브젝트 그래프로
- 직렬화가 가지는 장점
- 현재 프로그램의 상태를 저장하고 필요한 때 복원할 수 있다. ( 게임의 현재 상태 저장 )
- 현재 객체의 정보를 클립보드에 복사해서 다른 프로그램에 전송할 수 있다.
- 네트워크를 통해 현재 프로그램의 상태를 다른 컴퓨터에 복원할 수 있다. ( 멀티플레이어 게임 )
- 데이터 압축, 암호화를 통해 데이터를 보다 효율적이고 안전하게 보관할 수 있다.
직렬화 구현 시 고려할 점
- 데이터 레이아웃: 오브젝트가 소유한 다양한 데이터를 변환할 것인가?
- 이식성: 서로 다른 시스템에 전송해도 이식될 수 있는가?
- 버전 관리: 새로운 기능이 추가될 때 이를 어떻게 확장하고 처리할 것인가?
- 성능: 네트워크 비용을 줄이기 위해 어떤 데이터 형식을 사용할 것인가?
- 보안: 데이터를 어떻게 안전하게 보호할 것인가?
- 에러 처리: 전송 과정에서 문제가 발생할 경우 이를 어떻게 인식하고 처리할 것인가?
언리얼 엔진의 직렬화 시스템
- 언리얼 엔진은 위의 상황을 모두 고려한 직렬화 시스템을 자체적으로 제공한다.
- 직렬화 시스템을 위해 제공하는 클래스 FArchive와 연산자
- 아카이브 클래스 ( FArchive )
- Shift(<<) operator
- 다양한 아카이브 클래스의 제공
- 메모리 아카이브 ( FMemoryReader, FMemoryWriter )
- 파일 아카이브 ( FArchiveFileReaderGeneric, FArchiveFileWriterGeneric )
- 기타 언리얼 오브젝트와 관련된 아카이브 클래스 ( FArchiveUObject )
- Json 직렬화 기능: 별도의 라이브러리를 통해 제공한다.
직렬화 시스템의 활용
// Student.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Student.generated.h"
/**
*
*/
UCLASS()
class UNREALSERIALIZATION_API UStudent : public UObject
{
GENERATED_BODY()
public:
UStudent();
const int32 GetOrder() const { return Order; }
void SetOrder(const int32 InOrder) { Order = InOrder; }
const FString& GetName() const { return Name; }
void SetName(const FString& InName) { Name = InName; }
// 저장하거나 불러들일 때(직렬화를 진행할 때) Serialize()함수를 override해서 구현해주면 된다.
// 언리얼 오브젝트(UObject)에 Serialize() 함수가 기본적으로 구현되어있다.
virtual void Serialize(FArchive& Ar) override;
private:
UPROPERTY()
int32 Order;
UPROPERTY()
FString Name;
};
// Student.cpp
#include "Student.h"
UStudent::UStudent()
{
Order = -1;
Name = TEXT("홍길동");
}
void UStudent::Serialize(FArchive& Ar)
{
// 언리얼 오브젝트가 가져야 하는 기본적인 정보들을 Super::Serialize()가 해주기 때문에
Super::Serialize(Ar);
// 프로퍼티의 순번만 지정해주면 된다. Order/Name
Ar << Order;
Ar << Name;
}
// MyGameInstance.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"
struct FStudentData
{
FStudentData() {}
FStudentData(int32 InOrder, const FString& InName) :Order(InOrder), Name(InName) {}
friend FArchive& operator<<(FArchive& Ar, FStudentData& InStudentData)
{
Ar << InStudentData.Order;
Ar << InStudentData.Name;
return Ar;
}
int32 Order = -1;
FString Name = TEXT("홍길동");
};
/**
*
*/
UCLASS()
class UNREALSERIALIZATION_API UMyGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
UMyGameInstance();
virtual void Init() override;
private:
UPROPERTY()
TObjectPtr<class UStudent> StudentSrc;
};
// MyGameInstance.cpp
#include "MyGameInstance.h"
#include "Student.h"
void PrintStudentInfo(const UStudent* InStudent, const FString& InTag) // UE_LOG 출력함수
{
UE_LOG(LogTemp, Log, TEXT("[%s] 이름 %s 순번 %d"), *InTag, *InStudent->GetName(), InStudent->GetOrder());
}
UMyGameInstance::UMyGameInstance()
{
}
void UMyGameInstance::Init()
{
Super::Init();
FStudentData RawDataSrc(16, TEXT("김태연"));
const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved")); // 현 프로젝트의 폴더에서 Saved 폴더의 경로를 얻어옴
UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더: %s"), *SavedDir);
// ======RawData.bin 파일을 직접 생성하고 구조체 내에 있는 데이터를 파일에 쓰고 다시 읽는 방법======
{
const FString RawDataFileName(TEXT("RawData.bin"));
FString RawDataAbsolutePath = FPaths::Combine(*SavedDir, *RawDataFileName); // AbsolutePath: 절대 경로
UE_LOG(LogTemp, Log, TEXT("저장할 파일 전체 경로: %s"), *RawDataAbsolutePath);
FPaths::MakeStandardFilename(RawDataAbsolutePath); // 바로 알아볼 수 있는 깔끔하게 나오는 전체 경로 값
UE_LOG(LogTemp, Log, TEXT("변경할 파일 전체 경로: %s"), *RawDataAbsolutePath);
/*
* 실행결과
* LogTemp: 저장할 파일 폴더 : .. / .. / .. / .. / .. / .. / UE5Part1 / UnrealSerialization / Saved
* LogTemp : 저장할 파일 전체 경로 : .. / .. / .. / .. / .. / .. / UE5Part1 / UnrealSerialization / Saved / RawData.bin
* LogTemp : 변경할 파일 전체 경로 : C: / UE5Part1 / UnrealSerialization / Saved / RawData.bin
*/
// 파일에 쓸 수 있는 아카이브 클래스 생성, 아카이브 값을
FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbsolutePath);
if (nullptr != RawFileWriterAr)
{
/*
* *RawFIleWriterAr << RawDataSrc.Order;
* *RawFIleWriterAr << RawDataSrc.Name;
* 매번 이렇게 Order Name 따로 넣어주기 귀찮으므로 구조체에 새 함수를 만든다.
*/
*RawFileWriterAr << RawDataSrc; // 데이터 전송
RawFileWriterAr->Close(); // 전송 후 파일을 닫고 지우고 삭제까지 한다.
delete RawFileWriterAr;
RawFileWriterAr = nullptr;
}
// 저장되어있는 데이터 읽어오기
FStudentData RawDataDest;
FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbsolutePath);
if (nullptr != RawFileReaderAr)
{
*RawFileReaderAr << RawDataDest; // 데이터 불러오기
RawFileReaderAr->Close(); // 불러온 후 RawFileReaderAr 포인터 비우기
delete RawFileReaderAr;
RawFileReaderAr = nullptr;
UE_LOG(LogTemp, Log, TEXT("[RawData] 이름 %s 순번 %d"), *RawDataDest.Name, RawDataDest.Order);
}
/*
* 실행결과
* LogTemp: [RawData] 이름 김태연 순번 16
*/
}
// ======언리얼 오브젝트를 저장하고 불러들이는 코드======
StudentSrc = NewObject<UStudent>();
StudentSrc->SetName(TEXT("김태연"));
StudentSrc->SetOrder(59);
{
const FString ObjectDataFileName(TEXT("ObjectData.bin"));
FString ObjectDataAbsolutePath = FPaths::Combine(*SavedDir, *ObjectDataFileName);
FPaths::MakeStandardFilename(ObjectDataAbsolutePath);
// 언리얼 오브젝트를 저장할 때 메모리에 언리얼 오브젝트의 내용을 저장해보기
TArray<uint8> BufferArray; // 직렬화를 위한 버퍼
FMemoryWriter MemoryWriterAr(BufferArray); // 선언한 버퍼와 연동되는 메모리 Writer 만들기
StudentSrc->Serialize(MemoryWriterAr); // Serialize함수를 실행시키고 메모리에 쓸 아카이브를 지정해주기
// (이러면 메모리의 BufferArray에 Student 언리얼 오브젝트에 대한 내용이 쫙 써져있을 것이다.)
// 이것을 파일로 저장하기(처음 파일을 읽고 저장할 때 delete를 진행했는데, 이것을 한번에 처리할 수 있는 스마트 포인터 라이브러리를 사용해보기)
// 스마트 포인터 라이브러리 - 바로 쓰고 지울 용도
if(TUniquePtr<FArchive> FileWriterAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*ObjectDataAbsolutePath)))
{
*FileWriterAr << BufferArray;
FileWriterAr->Close();
}
// 파일 읽어오기
TArray<uint8> BufferArrayFromFile;
if (TUniquePtr<FArchive> FileReaderAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileReader(*ObjectDataAbsolutePath)))
{
// 버퍼에 파일의 내용들이 담김
*FileReaderAr << BufferArrayFromFile;
FileReaderAr->Close();
}
// 버퍼의 데이터를 다시 메모리에 전송
FMemoryReader MemoryReaderAr(BufferArrayFromFile); // 아카이브에 버퍼를 연동시킨다.
UStudent* StudentDest = NewObject<UStudent>(); // 새 객체를 생성하고
StudentDest->Serialize(MemoryReaderAr); // Serialize를 사용하면 알아서 새로 만들어진 StudentDest 객체에 BufferArrayFromFile에 있는 내용이 덮어씌워진다.
PrintStudentInfo(StudentDest, TEXT("ObjectData"));
/*
* 실행결과
* LogTemp : [ObjectData] 이름 김태연 순번 59
*/
}
}
Json 직렬화
- Json(JavaScript Object Notation)의 약자
- 웹 환경에서 서버와 클라이언트 사이에 데이터를 주고 받을 때 사용하는 텍스트 기반 데이터 포맷
- Json 장점
- 텍스트임에도 데이터 크기가 가볍다.
- 읽기 편해서 데이터를 보고 이해할 수 있다.
- 사실 상 웹 통신의 표준으로 널리 사용된다.
- Json 단점
- 지원하는 타입이 몇 가지 안된다. (문자, 숫자, 불리언, 널, 배열, 오브젝트만 사용 가능)
- 텍스트 형식으로만 사용할 수 있다.
- 언리얼 엔진은 언리얼 엔진의 Json, JsonUtilities 라이브러리를 활용한다.
- 언리얼 엔진의 Json 데이터 유형
- 오브젝트: {}
- 오브젝트 내 데이터는 key, value 조합으로 구성된다. 예) { "key" : 10 }
- 배열: []
- 배열 내 데이터는 value로만 구성된다. 예) [ "value1", "value2", "value3" ]
- 이외 데이터
- 문자열 ( "string" ), 숫자 ( 10 또는 3.14 ), 불리언 ( true 또는 false ), 널 ( null )로 구성된다.
- 오브젝트: {}
- 언리얼 엔진의 Json 데이터 유형
언리얼 스마트 포인터 라이브러리 개요
- 일반 C++ 오브젝트의 포인터 문제를 해결해주는 언리얼 엔진의 라이브러리이다.
- TUniquePtr(유니크포인터): 지정한 곳에서만 메모리를 관리하는 포인터
- 특정 오브젝트에게 명확하게 포인터 해지 권한을 주고 싶은 경우
- delete 구문 없이 함수 실행 후 자동으로 소멸시키고 싶을 때
- TSharedPtr(공유포인터): 더 이상 사용되지 않으면 자동으로 메로리를 해지하는 포인터
- 여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우
- 다른 함수로부터 할당된 오브젝트를 Out으로 받는 경우
- Null 일 수 있다.
- TSharedRef(공유레퍼런스): 공유포인터와 동일하지만, 유효한 객체를 항상 보장받는 레퍼런스
- 여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우
- Not Null을 보장받으며 오브젝트를 편리하게 사용하고 싶은 경우
Json 직렬화의 활용
// MyGameInstance.cpp
#include "MyGameInstance.h"
#include "Student.h"
#include "JsonObjectConverter.h" // 언리얼 오브젝트를 Json 오브젝트로 편리하게 변환해 주는 헬퍼 라이브러리들이 들어간 헤더
void PrintStudentInfo(const UStudent* InStudent, const FString& InTag) // UE_LOG 출력함수
{
UE_LOG(LogTemp, Log, TEXT("[%s] 이름 %s 순번 %d"), *InTag, *InStudent->GetName(), InStudent->GetOrder());
}
UMyGameInstance::UMyGameInstance()
{
}
void UMyGameInstance::Init()
{
Super::Init();
FStudentData RawDataSrc(16, TEXT("김태연"));
const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved")); // 현 프로젝트의 폴더에서 Saved 폴더의 경로를 얻어옴
UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더: %s"), *SavedDir);
// ======RawData.bin 파일을 직접 생성하고 구조체 내에 있는 데이터를 파일에 쓰고 다시 읽는 방법======
{. . .}
// ======언리얼 오브젝트를 저장하고 불러들이는 코드======
StudentSrc = NewObject<UStudent>();
StudentSrc->SetName(TEXT("김태연"));
StudentSrc->SetOrder(59);
{. . .}
// Json 직렬화
{
const FString JsonDataFileName(TEXT("StudentJsonData.txt"));
FString JsonDataAbsolutePath = FPaths::Combine(*SavedDir, *JsonDataFileName);
FPaths::MakeStandardFilename(JsonDataAbsolutePath);
// Json의 경우, FJson 오브젝트라는 객체를 생성해줘야 한다. (#include "JsonObjectConverter.h" 추가)
TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>();
// 언리얼 오브젝트는 결국 UStruct로부터 상속받는다. 프로퍼티들을 Json으로 변환하기 위해 UStruct 정보를 Json에 넘긴다.
// UStructToJsonObject(클래스 정보, 오브젝트 포인터, Json 오브젝트에 대한 공유레퍼런스)
FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc);
// 언리얼 오브젝트가 Json 오브젝트로 변환됐다.
/*
* unresolved external symbol(헤더로는 선언이 되어있는데 구현부가 없는 상황) 에러 발생!!!!!
* => 우리가 사용하는 Json 라이브러리에 대해서는 별도의 라이브러리들을 연동시켜줘서 구현부를 제공해줘야한다.
* => 구현부 제공하는 방법: UnrealSerialization.Build.cs 파일에서 UnrealSerialization 프로젝트의 모듈에 Json에 관련된 라이브러리를 연동시켜줘야 한다.
* => UnrealSerialization.Build.cs -> PublicDependencyModuleNames.AddRange(..., "Json", "JsonUtilities"); < 처럼 "Json" 추가!
*/
// 문자열로 Json 오브젝트를 변환
FString JsonOutString;
TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString); // Factory에 의해 Json으로 써주는 아카이브 생성
if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
{
//파일 경로에 Json 값을 쓰게 한다.
FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);
}
// 불러들이기
FString JsonInString;
FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath); // 파일로부터 문자열 불러오기
TSharedRef<TJsonReader<TCHAR>> JsonReaderAr = TJsonReaderFactory<TCHAR>::Create(JsonInString); // 블러온 스트링으로 reader 아카이브 생성
// Json 오브젝트를 언리얼 오브젝트로 변환
TSharedPtr<FJsonObject> JsonObjectDest;
if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
{
// 객체 만들기
UStudent* JsonStudentDest = NewObject<UStudent>();
// JsonObjectToUStruct(공유레퍼런스, 클래스 정보, 객체 정보)
if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudentDest))
{
PrintStudentInfo(JsonStudentDest, TEXT("JsonData")); // UE_LOG 사용자 지정 함수
}
}
}
}