WN_인생기록

[UE_5] 직렬화 본문

언리얼 개발/탐구(이론)

[UE_5] 직렬화

WhNi 2024. 6. 5. 14:53

직렬화 : 오브젝트나 연결된 오브젝트의 묶음을 바이트 스트림으로 변환하는 과정

- 복잡한 데이터를 일렬로 세우기 때문에 직렬화 라고 함

 

시리얼라이제이션(직렬화) : 오브젝트 그래프에서 바이트 스트림으로

디시리얼라이제이션 : 바이트 스트림에서 오브젝트 그래프로 복구

 

왜 직렬화가 중요한가

프로그램의 상태를 저장하고 필요할 때 복원할 수 있다 (세이브)

객체 정보를 클립보드에 복사해서 다른 프로그램에 전송가능

네트워크를 통해 다른 컴퓨터에 복원 ( 멀티플레이)

데이터 압축, 암호화를 통해서 데이터를 효율적으로 보관 가능 

 

구현할때 고려할 점 

1. 데이터 레이아웃 : 오브젝트가 소유한 데이터를 변환할 것인가? (어떻게 직렬화 될 것인가?)

2. 이식성 : 다른 시스템에 전송해도 그대로 사용 가능한가?

3. 버전관리 : 새로운 기능이 추가될때 어떻게 확장할 것인가?

4. 성능 : 비용을 줄이기 위해 어떤 데이터 형식을 사용할 것인가?

5. 보안 : 데이터를 어떻게 안전하게 보호할 것인가?

6. 에러 처리 : 전송 과정에서 문제가 발생할 경우 어떻게 인식하고 처리할 것인가? 

 

언리얼에서는 자체적으로 시스템 제공

클래스는 FArchive

메모리 아카이브 (FMemoryReader,FMemoryWriter)가 있음

 

먼저, 프로젝트의 디렉토리 내의 폴더 경로를 알아야 한다. 

 

const FString SaveDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));

const FString RawDataFileName(TEXT("RowData.bin"));
FString RawDataAbsolutePath = FPaths::Combine(*SaveDir, *RawDataFileName);
FPaths::MakeStandardFilename(RawDataAbsolutePath);

FPaths를 통해 Saved에 대한 경로를 얻을 수 있고, 

이를 파일 이름과, 경로를 합쳐서 최종 경로를 만들어내야 한다. 

마지막에는 MakeStandardFilename() 기능을 통해서 표준경로로 바꿔줘야 한다. 이렇게 하면 프로젝트 내의 폴더 경로를 코드로 저장할 수 있다. 

 

// 파일 쓰기를 위한 파일 스트림 생성
FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbsolutePath);
if (RawFileWriterAr)
{
    // 데이터 객체를 파일에 직렬화
    *RawFileWriterAr << RawDataSrc;
    // 파일 스트림 닫기
    RawFileWriterAr->Close();
    // 메모리 해제
    delete RawFileWriterAr;
    RawFileWriterAr = nullptr;
}

// 파일 읽기를 위한 파일 스트림 생성
FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbsolutePath);
if (RawFileReaderAr)
{
    // 파일로부터 객체 데이터 역직렬화
    *RawFileReaderAr << RawDataDest;
    // 파일 스트림 닫기
    RawFileReaderAr->Close();
    // 메모리 해제
    delete RawFileReaderAr;
    RawFileReaderAr = nullptr;
   
}

 

IFileManager의 ::Get().CreateFileWriter((경로))를 통해서 파일 스트림을 생성하고, 

해당 파일에 데이터를 직렬화 시킨다. << 라는 기능을 통해서 직렬화 시킬 수 있다. 

스트림을 닫고, 메모리를 해제하고, 확실하게 nullptr 까지 만들어줘야 메모리 누수가 없다. 

 

IFileManager의 ::Get().CreateFileReader((경로))를 통해서 파일 스트림을 생성하고, 

해당 파일에 데이터를 역직렬화 시킨다. 

마찬가지로 스트림을 닫고, 메모리를 해제하고, 확실하게 nullptr 까지 만들어줘야 메모리 누수가 없다. 

 

이렇게 하는것이 가장 기본적이나, 언리얼에서는 객체를 직렬화 할때, 좀 더 준비된 시스템이 있다. 

 

const FString ObjectDataFileName(TEXT("ObjectData.bin"));
FString ObjectDataAbsolutePath = FPaths::Combine(*SaveDir, *ObjectDataFileName);
// 표준 경로 형식으로 변환
FPaths::MakeStandardFilename(ObjectDataAbsolutePath);

// 버퍼 배열 생성 및 메모리 스트림을 통한 객체 직렬화
TArray<uint8> BufferArray;
FMemoryWriter MemoryWriterAr(BufferArray);
StudentSrc->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);
UStudent* StudentDest = NewObject<UStudent>();
StudentDest->Serialize(MemoryReaderAr);

 

TArray<uint8> 을 통해서, 데이터를 직렬화 시키는데, 이를 FMemoryWriter 이라는 클래스에서 위의 과정을 줄여준다.

StudentSrc 라는 클래스에서 직렬화에 대한 오버라이딩으로 변수의 순번만 정해주면 된다. 

	virtual void Serialize(FArchive& Ar) override
    {
    	Super::Serialize(Ar);

		Ar << (변수1);
		Ar << (변수2);
    }

FArchive 데이터형에 FMemoryWriter 를 지정해주면 자동으로 객체에 대한 정보를 저장한다.

 

이후에 TUniquePtr을 통해 delete와 nullptr을 선언하는 로직을 줄여줄 수 있다. 

 

이후에 FMemoryReader를 통해서 역직렬화를 할 수 있다. 

 

마지막으로, Json이라는 파일명으로도 직렬화가 가능한데 이를 위해서는 Module에 

 

"Json","JsonUtilities" 

을 추가해줘야 하며, 

 

#include "JsonObjectConverter.h"

 

를 cpp에 추가해줘야 한다. 

// JSON 파일 이름 설정 및 경로 조합
const FString JsonDataFileName(TEXT("StudentJsonData.txt"));
FString JsonDataAbsolutePath = FPaths::Combine(*SaveDir, *JsonDataFileName);
// 표준 경로 형식으로 변환
FPaths::MakeStandardFilename(JsonDataAbsolutePath);

// JSON 객체 생성 및 구조체 데이터를 JSON으로 변환
TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>();
FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc);

// JSON 문자열 생성 및 파일에 저장
FString JsonOutString;
TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString);
if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
{
    FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);
}

// JSON 파일 로드
FString JsonInString;
FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);

// JSON 문자열을 읽어들이고 객체로 역직렬화
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))
    {
        // 역직렬화된 JSON 객체 정보 로깅
        UE_LOG(LogTemp, Log, TEXT("이름 %s, 순번 %d"), *JsonStudentDest->GetName(), JsonStudentDest->GetOrder());
    }
}

 

FJsonObjectConverter 를 통해서 UStructToJsonObject 으로 변환 시킬 수 있다.

 

이후에 데이터를 직렬화 하고, 역직렬화한 데이터를 JsonObjectToUStruct으로 언리얼에 적용 시킬 수 있다.