icon
7장 : 저장 및 데이터 관리

파일 입출력 및 JSON 처리


이전 절에서 우리는 언리얼 엔진의 SaveGame 시스템을 통해 게임 데이터를 쉽고 편리하게 저장하고 불러오는 방법을 알아보았습니다. SaveGame 시스템은 대부분의 게임 저장 요구 사항을 충족하지만, 때로는 게임 데이터가 아닌 다른 종류의 파일(예: 사용자 로그, 커스텀 설정 파일, 외부 데이터)을 직접 다루거나, 특정 형식(JSON, XML 등)으로 데이터를 저장해야 할 필요가 생깁니다.

이번 절에서는 언리얼 엔진에서 제공하는 기본적인 파일 입출력(File I/O) 기능과 함께, 웹 서비스 통신이나 설정 파일에 널리 사용되는 JSON(JavaScript Object Notation) 데이터 형식을 C++에서 처리하는 방법에 대해 알아보겠습니다.


언리얼 엔진의 파일 입출력

언리얼 엔진은 플랫폼 독립적인 파일 입출력을 위해 여러 유틸리티 클래스를 제공합니다. 주로 IPlatformFile 인터페이스와 FFileHelper 클래스가 사용됩니다.

IPlatformFile

IPlatformFile은 저수준의 파일 시스템 접근을 위한 인터페이스입니다. 파일을 열고, 읽고, 쓰고, 닫는 등의 기본적인 파일 작업을 수행할 수 있습니다. 이는 특정 플랫폼의 파일 시스템 API를 추상화하여 개발자가 플랫폼별 코드를 작성할 필요 없게 합니다.

IPlatformFile 사용 예제
#include "HAL/PlatformFilemanager.h" // IPlatformFile을 위해 포함
#include "Misc/FileHelper.h"        // FFileHelper를 위해 포함
#include "Misc/Paths.h"             // FPaths를 위해 포함

void MyCustomFileWriter()
{
    // 1. 파일 경로 설정
    // FPaths::ProjectSavedDir() : 프로젝트의 Saved 디렉토리 (C:\Users\[User]\AppData\Local\[ProjectName]\Saved\)
    // FPaths::ProjectContentDir() : 프로젝트의 Content 디렉토리
    // FPaths::ProjectDir() : 프로젝트의 루트 디렉토리
    FString FilePath = FPaths::ProjectSavedDir() + TEXT("MyCustomData.txt");

    // 2. 파일 쓰기 (텍스트)
    FString ContentToWrite = TEXT("Hello, Unreal Engine File I/O!");
    // FFileHelper::SaveStringToFile: 가장 쉬운 방법으로 문자열을 파일에 저장
    if (FFileHelper::SaveStringToFile(ContentToWrite, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8))
    {
        UE_LOG(LogTemp, Warning, TEXT("Successfully wrote to file: %s"), *FilePath);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to write to file: %s"), *FilePath);
    }

    // 3. 파일 읽기 (텍스트)
    FString LoadedContent;
    // FFileHelper::LoadFileToString: 파일에서 문자열을 읽어옴
    if (FFileHelper::LoadFileToString(LoadedContent, *FilePath))
    {
        UE_LOG(LogTemp, Warning, TEXT("Successfully read from file: %s"), *LoadedContent);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to read from file: %s"), *FilePath);
    }

    // 4. (고급) IPlatformFile을 직접 사용하여 파일 스트림 제어
    IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
    
    // 파일 생성 및 쓰기 (바이트 단위)
    TArray<uint8> BytesToWrite;
    FString BinaryContent = TEXT("Binary Data Example");
    BytesToWrite.SetNum(BinaryContent.Len() * sizeof(TCHAR)); // TCHAR 크기에 맞게 배열 크기 설정
    FMemory::Memcpy(BytesToWrite.GetData(), *BinaryContent, BytesToWrite.Num());

    IFileHandle* WriteHandle = PlatformFile.OpenWrite(*FilePath + TEXT(".bin"));
    if (WriteHandle)
    {
        WriteHandle->Write(BytesToWrite.GetData(), BytesToWrite.Num());
        delete WriteHandle; // 핸들 닫기
        UE_LOG(LogTemp, Warning, TEXT("Successfully wrote binary data to: %s"), *(FilePath + TEXT(".bin")));
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to open binary file for writing: %s"), *(FilePath + TEXT(".bin")));
    }

    // 파일 읽기 (바이트 단위)
    TArray<uint8> BytesRead;
    IFileHandle* ReadHandle = PlatformFile.OpenRead(*FilePath + TEXT(".bin"));
    if (ReadHandle)
    {
        BytesRead.SetNum(ReadHandle->Size()); // 파일 크기만큼 배열 설정
        ReadHandle->Read(BytesRead.GetData(), BytesRead.Num());
        delete ReadHandle; // 핸들 닫기

        FString ReadBinaryContent;
        ReadBinaryContent.Append((TCHAR*)BytesRead.GetData(), BytesRead.Num() / sizeof(TCHAR)); // TCHAR 개수만큼
        UE_LOG(LogTemp, Warning, TEXT("Successfully read binary data: %s"), *ReadBinaryContent);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to open binary file for reading: %s"), *(FilePath + TEXT(".bin")));
    }
}
  • FFileHelper: FFileHelperIPlatformFile 위에 구축된 고수준 유틸리티 클래스로, 문자열이나 바이트 배열을 파일로 저장하거나 파일에서 로드하는 것을 매우 간단하게 만듭니다. 대부분의 간단한 파일 입출력 시나리오에서는 FFileHelper를 사용하는 것이 좋습니다.
  • FPaths: 파일 경로를 구성할 때 프로젝트의 표준 디렉토리 경로를 얻는 데 사용됩니다. 이는 플랫폼 간 호환성을 보장합니다.
  • IPlatformFile 직접 사용: 더 정교한 제어(예: 파일 포인터 이동, 부분 읽기/쓰기, 비동기 I/O)가 필요할 때 IPlatformFile을 직접 사용할 수 있습니다. 그러나 이는 복잡하며, 대부분의 경우 FFileHelper로 충분합니다.

JSON (JavaScript Object Notation) 처리

JSON은 데이터를 구조화하여 표현하는 경량의 데이터 교환 형식입니다. 인간이 읽고 쓰기 쉬우며, 기계가 파싱하고 생성하기도 용이하여 웹 서비스 API, 설정 파일 등에 널리 사용됩니다.

언리얼 엔진은 Json 모듈을 통해 JSON 데이터를 C++에서 파싱하고 생성할 수 있는 기능을 제공합니다.

JSON 모듈 활성화

프로젝트의 .Build.cs 파일에 JsonJsonUtilities 모듈을 추가해야 합니다.

MyProject.Build.cs
// ...
PublicDependencyModuleNames.AddRange(
    new string[] {
        "Core",
        "CoreUObject",
        "Engine",
        "InputCore",
        "Json",         // JSON 모듈 추가
        "JsonUtilities" // JSON 유틸리티 모듈 추가
        // ...
    });
// ...

JSON 데이터 읽기 (파싱)

JSON 문자열을 파싱하여 C++에서 사용할 수 있는 데이터 구조로 변환합니다.

JSON 데이터 읽기 예제
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"

void ParseJsonData()
{
    FString JsonString = TEXT(R"({"name": "Unreal Guy", "health": 100, "inventory": ["Sword", "Shield"], "stats": {"strength": 10, "dexterity": 8}})");

    // 1. JSON 리더 생성
    TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);

    // 2. JSON 객체 파싱
    TSharedPtr<FJsonObject> JsonObject;
    if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
    {
        UE_LOG(LogTemp, Warning, TEXT("Successfully parsed JSON."));

        // 3. 데이터 접근
        // 문자열 가져오기
        FString Name;
        if (JsonObject->TryGetStringField(TEXT("name"), Name))
        {
            UE_LOG(LogTemp, Warning, TEXT("Name: %s"), *Name);
        }

        // 숫자 가져오기 (정수 또는 부동소수점)
        double Health;
        if (JsonObject->TryGetNumberField(TEXT("health"), Health))
        {
            UE_LOG(LogTemp, Warning, TEXT("Health: %f"), Health);
        }

        // 배열 가져오기
        TArray<TSharedPtr<FJsonValue>> InventoryArray;
        if (JsonObject->TryGetArrayField(TEXT("inventory"), InventoryArray))
        {
            UE_LOG(LogTemp, Warning, TEXT("Inventory Items:"));
            for (TSharedPtr<FJsonValue> ItemValue : InventoryArray)
            {
                UE_LOG(LogTemp, Warning, TEXT("- %s"), *ItemValue->AsString());
            }
        }

        // 중첩된 JSON 객체 가져오기
        TSharedPtr<FJsonObject> StatsObject = JsonObject->GetObjectField(TEXT("stats"));
        if (StatsObject.IsValid())
        {
            double Strength;
            if (StatsObject->TryGetNumberField(TEXT("strength"), Strength))
            {
                UE_LOG(LogTemp, Warning, TEXT("Stats - Strength: %f"), Strength);
            }
        }
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to parse JSON."));
    }
}
  • TJsonReaderFactory: FString로부터 TJsonReader를 생성합니다.
  • FJsonSerializer::Deserialize: TJsonReaderTSharedPtr<FJsonObject>를 사용하여 JSON 문자열을 실제 FJsonObject로 파싱합니다.
  • FJsonObject: JSON 객체({})를 나타내는 핵심 클래스입니다. TryGetStringField, TryGetNumberField, TryGetArrayField, GetObjectField 등의 함수를 사용하여 필드에 접근합니다.

JSON 데이터 쓰기 (생성)

C++ 데이터를 JSON 문자열로 변환합니다.

JSON 데이터 쓰기 예제
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"

void CreateJsonData()
{
    // 1. FJsonObject 생성
    TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject());

    // 2. 필드 추가
    JsonObject->SetStringField(TEXT("gameTitle"), TEXT("My Awesome Game"));
    JsonObject->SetNumberField(TEXT("version"), 1.0);
    JsonObject->SetBoolField(TEXT("isDebugMode"), true);

    // 배열 필드 추가
    TArray<TSharedPtr<FJsonValue>> LevelNamesArray;
    LevelNamesArray.Add(MakeShareable(new FJsonValueString(TEXT("Level_01"))));
    LevelNamesArray.Add(MakeShareable(new FJsonValueString(TEXT("Level_02"))));
    LevelNamesArray.Add(MakeShareable(new FJsonValueString(TEXT("Level_Boss"))));
    JsonObject->SetArrayField(TEXT("levels"), LevelNamesArray);

    // 중첩된 JSON 객체 추가
    TSharedPtr<FJsonObject> SettingsObject = MakeShareable(new FJsonObject());
    SettingsObject->SetNumberField(TEXT("volume"), 0.7);
    SettingsObject->SetStringField(TEXT("resolution"), TEXT("1920x1080"));
    JsonObject->SetObjectField(TEXT("gameSettings"), SettingsObject);

    // 3. JSON 문자열로 직렬화
    FString OutputString;
    TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
    if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer, true)) // true는 Pretty Print (들여쓰기)
    {
        UE_LOG(LogTemp, Warning, TEXT("Generated JSON:\n%s"), *OutputString);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to serialize JSON."));
    }

    // 4. 생성된 JSON 문자열을 파일로 저장 (FFileHelper 사용)
    FString FilePath = FPaths::ProjectSavedDir() + TEXT("GameConfig.json");
    if (FFileHelper::SaveStringToFile(OutputString, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8))
    {
        UE_LOG(LogTemp, Warning, TEXT("JSON saved to: %s"), *FilePath);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to save JSON file."));
    }
}
  • MakeShareable(new FJsonObject()): TSharedPtr를 사용하여 FJsonObject를 생성합니다. 언리얼 엔진은 스마트 포인터(TSharedPtr, TSharedRef, TWeakPtr)를 사용하여 메모리 관리를 효율적으로 수행합니다.
  • SetStringField, SetNumberField, SetBoolField, SetArrayField, SetObjectField: FJsonObject에 다양한 타입의 필드를 추가합니다.
  • FJsonSerializer::Serialize: FJsonObjectFString으로 직렬화합니다. 세 번째 인자를 true로 설정하면 가독성을 위해 들여쓰기가 적용됩니다.

UStruct를 JSON으로 직렬화/역직렬화

JsonUtilities 모듈은 UStruct와 JSON 간의 변환을 더욱 편리하게 해주는 헬퍼 함수를 제공합니다. 이는 복잡한 데이터를 UStruct로 정의하고 이를 JSON 파일로 저장하거나 불러올 때 매우 유용합니다.

UStruct를 JSON으로 직렬화/역직렬화 예제
#include "Serialization/Json/JsonArchiveOutputFormat.h" // 추가 필요
#include "Serialization/Json/JsonArchiveInputFormat.h"  // 추가 필요
#include "Serialization/Json/JsonSerializer.h"          // 추가 필요
#include "UObject/TextProperty.h" // FText 처리를 위해 필요

// USTRUCT로 정의하여 JSON으로 직렬화할 데이터 구조
USTRUCT(BlueprintType)
struct FGameConfigData
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
    FString LastPlayedPlayerName;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
    float MasterVolume;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
    int32 MaxFps;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
    TArray<FString> EnabledFeatures;

    FGameConfigData()
        : LastPlayedPlayerName(TEXT("Default")), MasterVolume(0.8f), MaxFps(60)
    {}
};

void SerializeUStructToJson()
{
    FGameConfigData ConfigData;
    ConfigData.LastPlayedPlayerName = TEXT("AwesomeGamer");
    ConfigData.MasterVolume = 0.65f;
    ConfigData.MaxFps = 120;
    ConfigData.EnabledFeatures.Add(TEXT("HighResTextures"));
    ConfigData.EnabledFeatures.Add(TEXT("RayTracing"));

    FString OutputString;
    // FJsonObject를 통한 직렬화 (JsonUtilities 사용)
    TSharedPtr<FJsonObject> JsonObject = FJsonObjectConverter::UStructToJsonObject(FGameConfigData::StaticStruct(), &ConfigData);
    if (JsonObject.IsValid())
    {
        TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
        FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer, true);
        UE_LOG(LogTemp, Warning, TEXT("UStruct to JSON:\n%s"), *OutputString);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to convert UStruct to FJsonObject."));
    }

    // 파일로 저장
    FString FilePath = FPaths::ProjectSavedDir() + TEXT("GameConfigStruct.json");
    if (FFileHelper::SaveStringToFile(OutputString, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8))
    {
    UE_LOG(LogTemp, Warning, TEXT("UStruct JSON saved to: %s"), *FilePath);
    }
}

void DeserializeJsonToUStruct()
{
    FString FilePath = FPaths::ProjectSavedDir() + TEXT("GameConfigStruct.json");
    FString InputString;
    if (!FFileHelper::LoadFileToString(InputString, *FilePath))
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to load JSON file: %s"), *FilePath);
        return;
    }

    FGameConfigData LoadedConfigData;
    // JSON 문자열을 FJsonObject로 파싱
    TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(InputString);
    TSharedPtr<FJsonObject> JsonObject;
    if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
    {
        // FJsonObject를 UStruct로 역직렬화 (JsonUtilities 사용)
        if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), FGameConfigData::StaticStruct(), &LoadedConfigData))
        {
            UE_LOG(LogTemp, Warning, TEXT("JSON to UStruct Loaded:"));
            UE_LOG(LogTemp, Warning, TEXT("  LastPlayedPlayerName: %s"), *LoadedConfigData.LastPlayedPlayerName);
            UE_LOG(LogTemp, Warning, TEXT("  MasterVolume: %f"), LoadedConfigData.MasterVolume);
            UE_LOG(LogTemp, Warning, TEXT("  MaxFps: %d"), LoadedConfigData.MaxFps);
            UE_LOG(LogTemp, Warning, TEXT("  EnabledFeatures: %s"), *FString::Join(LoadedConfigData.EnabledFeatures, TEXT(", ")));
        }
        else
        {
            UE_LOG(LogTemp, Error, TEXT("Failed to convert FJsonObject to UStruct."));
        }
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to parse JSON for UStruct deserialization."));
    }
}
  • FJsonObjectConverter::UStructToJsonObject: USTRUCT의 인스턴스를 FJsonObject로 변환합니다.
  • FJsonObjectConverter::JsonObjectToUStruct: FJsonObjectUSTRUCT의 인스턴스로 변환합니다.

이 방법은 특히 게임 설정, 로컬 캐시 데이터, 또는 웹 API와 통신할 때 매우 유용합니다.


마치며

언리얼 엔진의 파일 입출력 및 JSON 처리 기능은 SaveGame 시스템으로 해결할 수 없는 더 넓은 범위의 데이터 저장 및 교환 요구 사항을 충족시킵니다. FFileHelper를 사용하여 간단한 파일 작업을 수행하고, JsonJsonUtilities 모듈을 통해 JSON 데이터를 효율적으로 파싱하고 생성하는 방법을 익히면 게임 내외부의 다양한 데이터를 유연하게 관리할 수 있게 됩니다. 이는 특히 설정 파일 관리, 외부 데이터 연동, 그리고 향후 웹 서비스와의 통신을 구현하는 데 중요한 기반이 됩니다.