직렬화
- 오브젝트나 연결된 오브젝트의 묶음(오브젝트 그래프)을 바이트 스트림으로 변환하는 과정
복잡한 데이터를 일렬로 세우기 때문에 직렬화 라고 불린다. - 거꾸로 복구시키는 과정도 포함해서 의미
시리얼라이제이션(Serializaiton) : 오브젝트 그래프에서 바이트 스트림으로
디시리얼라이제이션(Deserialization) : 바이트 스트림에서 오브젝트 그래프로 - 직렬화가 가지는 장점
1. 현재 프로그램의 상태를 저장하고 필요할 때 복원할 수 있다. (게임의 저장)
2. 현재 객체의 정보를 클립보드에 복사해서 다른 프로그램에 전송할 수 있다.
3. 네트워크를 통해 현재 프로그램의 상태를 다른 컴퓨터에 복원할 수 있다. ( 멀티플레이어 게임)
4. 데이터 압축, 암호화를 통해 데이터를 효율적이고 안전하게 보관할 수도 있음.
직렬화 구현 시 고려할 점
- 데이터 레이아웃 : 오브젝트가 소유한 다양한 데이터를 변환할 것인가?
- 이식성 : 서로 다른 시스템에 전송해도 이식될 수 있는가?
- 버전 관리 : 새로운 기능이 추가될 때 이를 어떻게 확장하고 처리할 것인가?
- 성능: 네트웍 비용을 줄이기 위해 어떤 데이터 형식을 사용할 것인가?
- 보안 : 데이터를 어떻게 안전하게 보호할 것인가?
- 에러 처리 : 전송 과정에서 문제가 발생할 경우 이를 어떻게 인식하고 처리할 것인가?
이러한 상황을 모두 감안해 직렬화 모델을 만드는 것은 쉬운일이 아님
언리얼 엔진의 직렬화 시스템
- 언리얼 엔진은 이러한 상황을 모두 고려한 직렬화 시스템을 자체적으로 제공하고 있음
- 직렬화 시스템을 위해 제공하는 클래스 FArchive와 연산자
아카이브 클래스 (FArchive)
Shift(<<) operator (언리얼 엔진에서 shift 연산은 바이트 쉬프트가 아닌 데이터 스트림에 데이터를 보내는 역할을 한다.) - 다양한 아카이브 클래스의 제공
1. 메모리 아카이브 (FMemoryReader, FMemoryWriter)
2. 파일 아카이브 (FArchiveFileReaderGeneric, FArchiveFileWirterGeneric)
3. 기타 언리얼 오브젝트와 관련된 아카이브 클래스 (FArchiveUObject) - Json 직렬화 기능 : 별도의 라이브러리를 통해 제공하고 있음
직렬화 실습
강의에서는 한 쌍의 raw데이터만 넣는 것을 실습했다. 여기에 추가로 TArrary<FStudentData> 형태의 raw데이터를 쓰고 읽는 것을 직접 해보았다.
void UMyGameInstance::Init()
{
Super::Init();
TArray<FStudentData> StudentData;
StudentData.Add(FStudentData(16, TEXT("박아무개")));
StudentData.Add(FStudentData(17, TEXT("김아무개")));
StudentData.Add(FStudentData(18, TEXT("오아무개")));
const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));
UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더 : %s"), *SavedDir);
//언리얼 엔진 자체 직렬화
{
const FString RawDataFileName(TEXT("RawData.bin"));
FString RawDataAbosolutePath = FPaths::Combine(*SavedDir, *RawDataFileName);
UE_LOG(LogTemp, Log, TEXT("저장할 파일 전체 경로 : %s"), *RawDataAbosolutePath);
FPaths::MakeStandardFilename(RawDataAbosolutePath); //상대경로를 절대경로로 변경
UE_LOG(LogTemp, Log, TEXT("변경할 파일 전체 경로 : %s"), *RawDataAbosolutePath);
//버퍼에 순회하면서 배열의 데이터 직렬화
FBufferArchive Buffer;
for (FStudentData& it : StudentData)
{
Buffer << it;
}
//파일에 저장 후 Flush 및 버퍼 해제
if (FFileHelper::SaveArrayToFile(Buffer, *RawDataAbosolutePath))
{
Buffer.FlushCache();
Buffer.Empty();
}
// 파일에서 직렬화된 데이터 읽어오기
// uint8 형태의 직렬화된 데이터를 읽어들여 다시 FStudent 데이터로 변환해준 후 출력한다.
TArray<uint8> SerializedData;
TArray<FStudentData> ReadStudentData;
if (FFileHelper::LoadFileToArray(SerializedData, *RawDataAbosolutePath))
{
FMemoryReader Reader(SerializedData, true); // 스트리밍 모드로 메모리 읽기
Reader.Seek(0); //0번부터 읽기
while (!Reader.AtEnd())
{
FStudentData Student;
Reader << Student;
ReadStudentData.Add(Student);
}
for (const FStudentData& Student : ReadStudentData)
{
UE_LOG(LogTemp, Log, TEXT("[RawData] 이름 %s 순번 %d"), *Student.Name, Student.Order);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("파일에서 데이터를 읽어오는 데 실패했습니다."));
}
Buffer.FlushCache() 에 대한 설명
Buffer.FlushCache() 함수는 버퍼의 내용을 파일 시스템에 플러시하여 버퍼에 있는 데이터를 실제 파일로 기록하는 역할을 합니다. 이것은 주로 파일에 대한 변경사항을 디스크에 반영하는 데 사용됩니다.
버퍼는 일반적으로 메모리 내에 위치하며, 데이터가 쓰여지기 전에 임시로 버퍼에 모아둡니다. 이렇게 함으로써 작은 데이터를 여러 번 쓰는 대신 한 번에 큰 덩어리로 쓰는 등의 성능 향상을 이룰 수 있습니다. 그러나 이 데이터는 버퍼에만 있기 때문에 실제로 파일에 쓰여지지는 않습니다.
FlushCache() 호출은 버퍼의 내용을 강제로 디스크에 쓰도록 합니다. 이 작업은 파일 시스템에게 "이 버퍼에 있는 모든 내용을 디스크에 써야 한다"고 명령하는 것과 같습니다. 이렇게 함으로써 버퍼에 있는 변경사항이 실제 파일에 반영됩니다.
이것은 일반적으로 프로그램이 데이터를 쓰는 과정에서 중요한 단계입니다. 버퍼를 플러시하지 않고 데이터를 쓰기만 한다면, 시스템이 데이터를 디스크에 실제로 기록하기 전에 프로그램이 종료되거나 다른 이유로 인해 데이터가 손실될 수 있습니다. 따라서 중요한 데이터를 다룰 때는 데이터를 버퍼에 쓴 후에 FlushCache()를 호출하여 변경사항을 디스크에 반영하는 것이 좋습니다.
언리얼 오브젝트 직렬화
강의에서는 한 쌍의 언리얼 오브젝트만 직렬화해 넣는 것을 실습했다. 여기에 추가로 여러 오브젝트를 담은 배열을 한번에 직렬화 해보았다.
기존의 포인터
UPROPERTY()
TObjectPtr<class UStudent> StudentSrc;
배열 포인터
UPROPERTY()
TArray<TObjectPtr<class UStudent>> StudentArr;
//오브젝트 배열 추가
StudentSrc = NewObject<UStudent>();
StudentSrc->SetName(TEXT("김아무개"));
StudentSrc->SetOrder(59);
StudentArr.Add(StudentSrc);
StudentSrc = NewObject<UStudent>();
StudentSrc->SetName(TEXT("이아무개"));
StudentSrc->SetOrder(60);
StudentArr.Add(StudentSrc);
StudentSrc = NewObject<UStudent>();
StudentSrc->SetName(TEXT("박아무개"));
StudentSrc->SetOrder(61);
StudentArr.Add(StudentSrc);
{
const FString ObjectDataFileName(TEXT("ObjectData.bin"));
FString ObjectDataAbsolutePath = FPaths::Combine(*SavedDir, *ObjectDataFileName);
FPaths::MakeStandardFilename(ObjectDataAbsolutePath);
TArray<uint8> BufferArray;
FMemoryWriter MemoryWriterAr(BufferArray);
//StudentSrc->Serialize(MemoryWriterAr);
for (auto& it : StudentArr)
{
it->Serialize(MemoryWriterAr);
}
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);
for (auto& it : StudentArr)
{
it->Serialize(MemoryReaderAr);
PrintStudentInfo(it, TEXT("ObjectData"));
}
}
Json 직렬화
- Json(JavaScript Object Notation) 의 약자
- 웹 환경에서 서버와 클라이언트 사이에 데이터를 주고받을 떄 사용하는 테긋트 기반 데이터 포맷
- Json 장점
텍스트임에도 데이터 크기가 가벼움.
읽기 편해서 데이터를 보고 이해할 수 있음.
사실 상 웹 통신의 표준으로 널리 사용됨. - Json 단점
지원하는 타입이 몇가지 안됨. (문자, 숫자, 불리언, 널, 배열, 오브젝트만 사용 가능)
텍스트 형식으로만 사용할 수 있음.
Unresolved External Symbol 에러
Json 라이브러리 사용을 위해서는 라이브러리를 연동해서 구현부를 제공해 줘야 한다.
Build.cs 에서 Json 관련 라이브러리를 연동 시켜주자. (Json, JsonUtilites)
{
const FString JsonDataFileName(TEXT("StudentJsonData.txt"));
FString JsonDataAbsolutePath = FPaths::Combine(*SavedDir, *JsonDataFileName);
FPaths::MakeStandardFilename(JsonDataAbsolutePath);
TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>();
FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc);
FString JsonOutString;
TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString);
if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
{
FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);
}
FString JsonInString;
FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);
TSharedRef<TJsonReader<TCHAR>> JsonReaderAr = TJsonReaderFactory<TCHAR>::Create(JsonInString);
TSharedPtr<FJsonObject> JsonObjectDest;
if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
{
UStudent* JsonStudentDest = NewObject<UStudent>();
if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudentDest))
{
PrintStudentInfo(JsonStudentDest, TEXT("JsonData"));
}
}
}
언리얼 스마트 포인터 라이브러리 개요
- 일반 C++ 오브젝트의 포인터 문제를 해결해주는 언리얼 엔진의 라이브러리
- TUniquePtr(유니크포인터) : 지정한 곳에서만 메모리를 관리하는 포인터
특정 오브젝트에게 명확하게 포인터 해지 권한을 주고 싶은 경우.
delete 구문 없이 함수 실행 후 자동으로 소멸시키고 싶을 때 - TSharedPtr(공유포인터) : 더 이상 사용되지 않으면 자동으로 메모리를 해지하는 포인터
여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우
다른 함수로부터 할당된 오브젝트를 Out 으로 받는 경우
Null 일 수 있음 - TSharedRef(공유레퍼런스) : 공유포인터와 동일하지만, 유효한 객체를 항상 보장받는 레퍼런스여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우Not Null을 보장받으며 오브젝트를 편리하게 사용하고 싶은 경우
'언리얼엔진5 > [Part1] 이득우의 언리얼 프로그래밍' 카테고리의 다른 글
[이득우의 언리얼 프로그래밍 Part1 필기] 15. 언리얼 빌드 시스템 (0) | 2024.02.28 |
---|---|
[이득우의 언리얼 프로그래밍 Part1 필기] 14. 오브젝트 관리 II 패키지 (0) | 2024.02.27 |
[이득우의 언리얼 프로그래밍 Part1 필기] 12 . 언리얼 엔진의 메모리 관리 (0) | 2024.02.21 |
[이득우의 언리얼 프로그래밍 Part1 필기] 10 - 11. TArray, TSet, 구조체, TMap (0) | 2024.02.20 |
[이득우의 언리얼 프로그래밍 Part1 필기] 9. 설계 : 델리게이트 (0) | 2024.02.19 |