언리얼 엔진이 제공하는 직렬화 기능을 이해하고 언리얼 오브젝트의 데이터를 저장하고 불러들이기

  • 언리얼 엔진이 제공하는 직렬화 기능을 이해
  • 언리얼 오브젝트를 직렬화하고 이 데이터를 저장하고 불러들이는 방법을 이



인프런 이득우님의 '언리얼 프로그래밍 Part1 - 언리얼 C++의 이해' 강의를 참고하였습니다. 

😎 [이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해] 강의 들으러 가기!

 

 

 

목차

     

     


     

     

    언리얼 엔진의 직렬화


     

     

    직렬화(Serialization)란?

     

    • 오브젝트나 연결된 오브젝트의 묶음(오브젝트 그래프)을 바이트 스트림으로 변경하는 과정
      • 복잡한 데이터를 일렬로 세우므로 직렬화라 부름.
    • 거꾸로 복구시키는 과정도 포함해서 의미
      • Serialization : 오브젝트 그래프에서 바이트 스트림으로 (Object → Byte Stream)
      • Deserialization : 바이트 스트림에서 오브젝트 그래프로 (Byte Stream → Object)
    • 직렬화가 가지는 장점
      • 현재 프로그램의 상태를 저장하고 필요한 때 복원할 수 있다 (게임의 저장)
      • 현재 프로그램에서 사용하고 있는 객체의 정보를 클립보드에 복사해서 다른 프로그램으로 전송할 수 있다.
      • 네트워크를 통해 현재 프로그램의 상태를 다른 컴퓨터에 복원할 수 있다.
      • 직렬화된 데이터는 압축, 암호화를 통해 데이터를 효율적이고 안전하게 보관할 수 있다.


     

     

    직렬화 구현 시 고려해야 할 점

     

    이러한 직렬화를 직접 구현할 경우, 다양한 상황을 고려해야 한다.

    • 데이터 레이아웃 : 오브젝트가 가지고 있는 데이터를 어떻게 일렬로 변환할 것인가?
    • 이식성 : 서로 다른 시스템에 전송해도 이식될 수 있는가? (운영체제 별 데이터를 저장하는 방법인 엔디안 기법이 다를 수 있다)
    • 버전 관리 : 새로운 기능이 추가될 때 이를 어떻게 확장하고 처리할 것인가?
    • 성능 : 네트웍 비용을 줄이기 위해서 어떤 데이터 형식을 사용할 것인가?
      • ex. 회전의 경우에는 4개의 플롯(16바이트)을 사용. 해당 경우, 모두 보낼 필요가 없으므로 최대한 줄여서 보내는 방법이 있는데 이를 '양자화라 한다.
    • 보안 : 데이터를 어떻게 안전하게 보호할 것인가?
    • 에러 처리 : 전송 과정에서 문제가 발생할 경우 어떻게 인식하고 처리할 것인가?

     

    이러한 상황을 모두 감안해 직렬화 모델을 만드는 것은 쉬운일이 아님


     

     

    언리얼 엔진의 직렬화 시스템

     

    • 언리얼 엔진은 이런 상황을 모두 고려한 직렬화 시스템을 자체적으로 제공하고 있음
    • 직렬화 시스템을 위해 제공하는 클래스 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();
    
    	int32 GetOrder() const { return Order; }
    	void SetOrder(int32 InOrder) { Order = InOrder; }
    
    	const FString& GetName() const { return Name; }
    	void SetName(const FString& InName) { Name = InName; }
    
    	// 언리얼 오브젝트의 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(Ar);
    
    	Ar << Order; // Order 정보 넣기
    	Ar << Name;  // Name 정보 넣기
    }

     

     


     

     

    GameInstance.h

    #pragma once
    #include "CoreMinimal.h"
    #include "Engine/GameInstance.h"
    #include "Engine/StreamableManager.h"
    #include "MyGameInstance.generated.h"
    
    struct FStudentData // C++ 구조체
    {
    	FStudentData() {}
    	FStudentData(int32 InOrder, const FString& InName) : Order(InOrder), Name(InName) {}
    
    	friend FArchive& operator<<(FArchive& Ar, FStudentData& InStudentData)
    	{
    		Ar << InStudentData.Order;  // Order 넣기
    		Ar << InStudentData.Name;	// 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; // 저장할 대상인 소스 오브젝트
    };

     


    GameInstance.cpp

    #include "MyGameInstance.h"
    #include "Student.h"
    #include "UObject/SavePackage.h"
    
    const FString UMyGameInstance::PackageName = TEXT("/Game/Student");
    const FString UMyGameInstance::AssetName = TEXT("TopStudent");
    
    void PrintStudentInfo(const UStudent* InStudent, const FString& InTag)
    {
    	UE_LOG(LogTemp, Log, TEXT("[%s] 이름 %s 순번 %d"), *InTag, *InStudent->GetName(), InStudent->GetOrder());
    }
    
    UMyGameInstance::UMyGameInstance()
    {
    	const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
    	static ConstructorHelpers::FObjectFinder<UStudent> UASSET_TopStudent(*TopSoftObjectPath);
    	if (UASSET_TopStudent.Succeeded())
    	{
    		PrintStudentInfo(UASSET_TopStudent.Object, TEXT("Constructor"));
    	}
    }
    
    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);
    
    	{
    		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);
    
    		FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbosolutePath);
    		if (nullptr != RawFileWriterAr) // 데이터가 있다면
    		{
    			*RawFileWriterAr << RawDataSrc; // 데이터 전송
    			RawFileWriterAr->Close(); 		// 파일 닫기
    			delete RawFileWriterAr;			// 파일 삭제
    			RawFileWriterAr = nullptr;
    		}
    
    		FStudentData RawDataDest; // 구조체 선언
    		FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbosolutePath); 
    		if (nullptr != RawFileReaderAr) // 데이터가 있다면
    		{
    			*RawFileReaderAr << RawDataDest; // 데이터 부르기
    			RawFileReaderAr->Close();		 // 파일 닫기
    			delete RawFileReaderAr;			 // 파일 삭제
    			RawFileReaderAr = nullptr;
    
    			UE_LOG(LogTemp, Log, TEXT("[RawData] 이름 %s 순번 %d"), *RawDataDest.Name, RawDataDest.Order);
    		}
    	}
    
    	StudentSrc = NewObject<UStudent>(); // 생성
    	StudentSrc->SetOrder(59);			// 데이터 넣기
    	StudentSrc->SetName(TEXT("이득우")); // 데이터 넣기
    
    	//** 언리얼 오브젝트를 저장하고 불러들인다.
    	{
    		const FString ObjectDataFileName(TEXT("ObjectData.bin")); // 파일 이름
    		FString ObjectDataAbsolutePath = FPaths::Combine(*SavedDir, *ObjectDataFileName); // 파일 절대 경로값(파일경로 + 파일 이름)
    		FPaths::MakeStandardFilename(ObjectDataAbsolutePath); // 경로 보기좋게 바꿈
    
    		TArray<uint8> BufferArray; 				   // 바이트 스트림을 위한 버퍼(Buffer)
    		FMemoryWriter MemoryWriterAr(BufferArray); // 메모리 Writer에 버퍼 연동
    		StudentSrc->Serialize(MemoryWriterAr); // Serialize 함수 실행시키고 메모리에 쓸 Archive를 지정
    		
            // 쓰기
    		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); // Archive에 버퍼 연동
    		UStudent* StudentDest = NewObject<UStudent>();	   // 새로운 Student 객체 생성
    		StudentDest->Serialize(MemoryReaderAr);			   // 새로운 객체에 BufferArrayFromFile에 있는 내용 덮어쓰기
    		PrintStudentInfo(StudentDest, TEXT("ObjectData")); // 출력
    	}
    
    }

     


     

     

    실행화면

     

     


     

     

     

     

    Json 직렬화


     

     

    Json 직렬화

     

    • Json은 JavaScript Object Notation의 약자
    • 웹 환경에서 서버와 클라이언트 사이에 데이터를 주고받을 때 사용하는 텍스트 기반 데이터 포맷 
    • 데이터를 표현하기 위한 텍스트 기반 경량의 데이터 교환 포맷이다.
    • Json 장점
      • 텍스트임에도 데이터 크기가 가볍움.
      • 읽기 편해서 데이터를 보고 이해할 수 있음.
      • 사실 상 웹 통신의 표준으로 널리 사용됨.
    • Json 단점
      • 지원하는 타입이 몇 가지 안됨. (문자, 숫자, 불리언, 널, 배열, 오브젝트만 사용 가능)
      • 텍스트 형식으로만 사용할 수 있음. (네트워크 상에서 극도의 효율을 추구하고 싶을 때는 조금 불리할 수 있다.)

     

    언리얼은 Json, JosnUtilities 라이브러리 활용


     



    Json 데이터 예시

     

    Json 데이터 유형

    • 오브젝트:  { }
      • 오브젝트 내 데이터는 Key, Value 조합으로 구성됨.
      • ex. { "key" : 10 }
    • 배열:  [ ]
      • 배열 내 데이터는 Value로만 구성됨.
      • ex. [ "value1", "value2", "value3" ]
    • 이외 데이터
      • 문자열 ( "string" ), 숫자 ( 10 또는 3.14 ), 불리언 ( true 또는 false ), 널 ( null )로 구성

     


     

     

    언리얼 스마트 포인터 라이브러리 개요

     

    일반 C++오브젝트의 포인터 문제를 해결해주는 언리얼 엔진의 라이브러리이다.

    • 일반 C++ 스마트 포인터와 비교
      • TUniquePtr : std::unique_ptr
      • TSharedPtr : std::shared_ptr, nullptr 가능
      • TSharedRef : 공유 참조, Not NULL 보장,
      • TWeakPtr : std::weak_ptr. 독립 사용 불가
    • TUniquePtr: 지정한 곳에서만 메모리를 관리하는 포인터
      • 특정 오브젝트에게 명확하게 해지 권한을 주고 싶은 경우
      • delete 구문 없이 함수 실행 후 자동으로 소멸시키고 싶을 때
    • TSharedPtr: 더 이상 사용되지 않으면 자동으로 메모리를 해지하는 포인터
      • 여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우
      • 다른 함수로부터 할당된 오브젝트를 Out으로 받는 경우
      • Null 일 수 있음
    • TShardRef: 공유 포인터와 동일하지만, 유효한 객체를 항상 보장받는 레퍼런스
      • 여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우
      • Not Null을 보장받으며 오브젝트를 편리하게 사용하고 싶은 경우

     

     

    예제 코드

     

    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();
    
    	int32 GetOrder() const { return Order; }
    	void SetOrder(int32 InOrder) { Order = InOrder; }
    
    	const FString& GetName() const { return Name; }
    	void SetName(const FString& InName) { Name = InName; }
    
    	// 언리얼 오브젝트의 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(Ar);
    
    	Ar << Order; // Order 정보 넣기
    	Ar << Name;  // Name 정보 넣기
    }

     

     


     

     

    GameInstance.h

    #pragma once
    #include "CoreMinimal.h"
    #include "Engine/GameInstance.h"
    #include "Engine/StreamableManager.h"
    #include "MyGameInstance.generated.h"
    
    struct FStudentData // C++ 구조체
    {
    	FStudentData() {}
    	FStudentData(int32 InOrder, const FString& InName) : Order(InOrder), Name(InName) {}
    
    	friend FArchive& operator<<(FArchive& Ar, FStudentData& InStudentData)
    	{
    		Ar << InStudentData.Order;  // Order 넣기
    		Ar << InStudentData.Name;	// Name 넣기
    		return Ar;
    	}
    
    	int32 Order = -1;
    	FString Name = TEXT("홍길동");
    };
    
    UCLASS()
    class UNREALSERIALIZATION_API UMyGameInstance : public UGameInstance
    {
    	GENERATED_BODY()
    	
    public:
    	UMyGameInstance();
    
    	virtual void Init() override;
    
    	void SaveStudentPackage() const;
    	void LoadStudentPackage() const;
    	void LoadStudentObject() const;
    
    private:
    	static const FString PackageName;
    	static const FString AssetName;
    
    	UPROPERTY()
    	TObjectPtr<class UStudent> StudentSrc;
    
    	FStreamableManager StreamableManager;
    	TSharedPtr<FStreamableHandle> Handle;
    };

     


    GameInstance.cpp

    #include "MyGameInstance.h"
    #include "Student.h"
    #include "JsonObjectConverter.h"
    #include "UObject/SavePackage.h"
    
    const FString UMyGameInstance::PackageName = TEXT("/Game/Student");
    const FString UMyGameInstance::AssetName = TEXT("TopStudent");
    
    void PrintStudentInfo(const UStudent* InStudent, const FString& InTag)
    {
    	UE_LOG(LogTemp, Log, TEXT("[%s] 이름 %s 순번 %d"), *InTag, *InStudent->GetName(), InStudent->GetOrder());
    }
    
    UMyGameInstance::UMyGameInstance()
    {
    	const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
    	static ConstructorHelpers::FObjectFinder<UStudent> UASSET_TopStudent(*TopSoftObjectPath);
    	if (UASSET_TopStudent.Succeeded())
    	{
    		PrintStudentInfo(UASSET_TopStudent.Object, TEXT("Constructor"));
    	}
    }
    
    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);
    
    	{
    		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);
    
    		FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbosolutePath);
    		if (nullptr != RawFileWriterAr) // 데이터가 있다면
    		{
    			*RawFileWriterAr << RawDataSrc; // 데이터 전송
    			RawFileWriterAr->Close(); 		// 파일 닫기
    			delete RawFileWriterAr;			// 파일 삭제
    			RawFileWriterAr = nullptr;
    		}
    
    		FStudentData RawDataDest; // 구조체 선언
    		FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbosolutePath); 
    		if (nullptr != RawFileReaderAr) // 데이터가 있다면
    		{
    			*RawFileReaderAr << RawDataDest; // 데이터 부르기
    			RawFileReaderAr->Close();		 // 파일 닫기
    			delete RawFileReaderAr;			 // 파일 삭제
    			RawFileReaderAr = nullptr;
    
    			UE_LOG(LogTemp, Log, TEXT("[RawData] 이름 %s 순번 %d"), *RawDataDest.Name, RawDataDest.Order);
    		}
    	}
    
    	StudentSrc = NewObject<UStudent>(); // 생성
    	StudentSrc->SetOrder(59);			// 데이터 넣기
    	StudentSrc->SetName(TEXT("이득우")); // 데이터 넣기
    
    	//** 언리얼 오브젝트를 저장하고 불러들인다.
    	{
    		const FString ObjectDataFileName(TEXT("ObjectData.bin")); // 파일 이름
    		FString ObjectDataAbsolutePath = FPaths::Combine(*SavedDir, *ObjectDataFileName); // 파일 절대 경로값(파일경로 + 파일 이름)
    		FPaths::MakeStandardFilename(ObjectDataAbsolutePath); // 경로 보기좋게 바꿈
    
    		TArray<uint8> BufferArray; 				   // 바이트 스트림을 위한 버퍼(Buffer)
    		FMemoryWriter MemoryWriterAr(BufferArray); // 메모리 Writer에 버퍼 연동
    		StudentSrc->Serialize(MemoryWriterAr); // Serialize 함수 실행시키고 메모리에 쓸 Archive를 지정
    		
            // 쓰기
    		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); // Archive에 버퍼 연동
    		UStudent* StudentDest = NewObject<UStudent>();	   // 새로운 Student 객체 생성
    		StudentDest->Serialize(MemoryReaderAr);			   // 새로운 객체에 BufferArrayFromFile에 있는 내용 덮어쓰기
    		PrintStudentInfo(StudentDest, TEXT("ObjectData")); // 출력
    	}
    
    	{
    		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"));
    			}
    		}
    	}
    
    	SaveStudentPackage();
    	//LoadStudentPackage();
    	//LoadStudentObject();
    
    	const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
    	Handle = StreamableManager.RequestAsyncLoad(TopSoftObjectPath,
    		[&]()
    		{
    			if (Handle.IsValid() && Handle->HasLoadCompleted())
    			{
    				UStudent* TopStudent = Cast<UStudent>(Handle->GetLoadedAsset());
    				if (TopStudent)
    				{
    					PrintStudentInfo(TopStudent, TEXT("AsyncLoad"));
    
    					Handle->ReleaseHandle();
    					Handle.Reset();
    				}
    			}
    		}
    	);
    }
    
    void UMyGameInstance::SaveStudentPackage() const
    {
    	UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
    	if (StudentPackage)
    	{
    		StudentPackage->FullyLoad();
    	}
    
    	StudentPackage = CreatePackage(*PackageName);
    	EObjectFlags ObjectFlag = RF_Public | RF_Standalone;
    
    	UStudent* TopStudent = NewObject<UStudent>(StudentPackage, UStudent::StaticClass(), *AssetName, ObjectFlag);
    	TopStudent->SetName(TEXT("이득우"));
    	TopStudent->SetOrder(36);
    
    	const int32 NumofSubs = 10;
    	for (int32 ix = 1; ix <= NumofSubs; ++ix)
    	{
    		FString SubObjectName = FString::Printf(TEXT("Student%d"), ix);
    		UStudent* SubStudent = NewObject<UStudent>(TopStudent, UStudent::StaticClass(), *SubObjectName, ObjectFlag);
    		SubStudent->SetName(FString::Printf(TEXT("학생%d"), ix));
    		SubStudent->SetOrder(ix);
    	}
    
    	const FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
    	FSavePackageArgs SaveArgs;
    	SaveArgs.TopLevelFlags = ObjectFlag;
    
    	if (UPackage::SavePackage(StudentPackage, nullptr, *PackageFileName, SaveArgs))
    	{
    		UE_LOG(LogTemp, Log, TEXT("패키지가 성공적으로 저장되었습니다."));
    	}
    
    }
    
    void UMyGameInstance::LoadStudentPackage() const
    {
    	UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
    	if (nullptr == StudentPackage)
    	{
    		UE_LOG(LogTemp, Warning, TEXT("패키지를 찾을 수 없습니다."));
    		return;
    	}
    
    	StudentPackage->FullyLoad();
    
    	UStudent* TopStudent = FindObject<UStudent>(StudentPackage, *AssetName);
    	PrintStudentInfo(TopStudent, TEXT("FindObject Asset"));
    }
    
    void UMyGameInstance::LoadStudentObject() const
    {
    	const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
    
    	UStudent* TopStudent = LoadObject<UStudent>(nullptr, *TopSoftObjectPath);
    	PrintStudentInfo(TopStudent, TEXT("LoadObject Asset"));
    }

     

     


     

     

    실행화면