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

데이터 직렬화와 역직렬화


이전 절에서 우리는 언리얼 엔진의 SaveGame 시스템을 사용하여 게임 데이터를 디스크에 저장하고 불러오는 기본 과정을 살펴보았습니다. 이 과정의 핵심에는 직렬화(Serialization)역직렬화(Deserialization) 라는 개념이 있습니다. 이들은 단순히 데이터를 파일로 만들고 다시 읽어들이는 것을 넘어, 게임 객체의 복잡한 상태를 안정적으로 보존하고 복원하는 데 필수적인 과정입니다.

이번 절에서는 데이터 직렬화와 역직렬화가 무엇인지, 언리얼 엔진에서 어떻게 작동하는지, 그리고 개발자가 이를 어떻게 활용하고 주의해야 하는지에 대해 깊이 있게 다뤄보겠습니다.


직렬화 (Serialization)란?

직렬화메모리에 있는 복잡한 데이터 구조(객체, 변수, 배열, 맵 등)를 파일이나 네트워크를 통해 전송하거나 저장할 수 있는 순차적인 바이트 스트림(연속된 데이터 형식)으로 변환하는 과정을 말합니다. 게임에서는 주로 게임의 현재 상태를 .sav 파일과 같은 영구 저장소에 기록할 때 사용됩니다.

왜 직렬화가 필요한가요?

  • 영구 저장: 게임이 종료된 후에도 데이터를 유지하기 위해 하드 드라이브 같은 비휘발성 저장소에 기록해야 합니다. 메모리의 데이터는 프로그램이 종료되면 사라집니다.
  • 네트워크 전송: 멀티플레이어 게임에서 다른 클라이언트나 서버로 게임 상태를 전송할 때, 데이터는 네트워크를 통해 순차적으로 전송될 수 있는 형태로 변환되어야 합니다.
  • 데이터 일관성: 복잡한 객체 그래프를 재구성할 때 모든 참조와 값이 올바르게 연결되도록 보장합니다.

역직렬화 (Deserialization)란?

역직렬화직렬화된 바이트 스트림을 읽어들여 메모리에서 원래의 복잡한 데이터 구조(객체)로 재구성하는 과정입니다. 게임에서는 저장된 .sav 파일을 불러와 게임의 상태를 이전 시점으로 되돌릴 때 사용됩니다.

왜 역직렬화가 필요한가요?

  • 게임 상태 복원: 저장된 게임 파일을 불러와 캐릭터의 위치, 인벤토리, 퀘스트 진행 상황 등을 이전 상태로 정확하게 복원하기 위함입니다.
  • 네트워크 데이터 수신: 네트워크를 통해 수신된 순차적인 데이터를 다시 게임 로직에서 처리할 수 있는 객체 형태로 변환합니다.

언리얼 엔진의 직렬화 메커니즘

언리얼 엔진은 자체적인 강력한 직렬화 시스템을 가지고 있으며, 이는 UObject 시스템과 밀접하게 통합되어 있습니다.

UPROPERTY()의 중요성

언리얼 엔진의 직렬화 시스템에서 가장 중요한 부분은 UPROPERTY() 매크로입니다. UPROPERTY()로 선언된 모든 멤버 변수는 언리얼 엔진의 리플렉션(Reflection) 시스템에 노출되며, 이로 인해 자동적으로 직렬화 대상이 됩니다.

이것이 바로 USaveGame 클래스에서 우리가 저장하려는 모든 변수에 UPROPERTY()를 붙인 이유입니다. FString, int32, float, FVector, FRotator, TArray, TMap과 같은 기본 타입과 컨테이너들은 UPROPERTY()만 붙이면 자동으로 직렬화/역직렬화됩니다.

USaveGame 클래스의 직렬화

USaveGame은 언리얼 엔진의 직렬화 시스템을 활용하도록 특별히 설계된 UObject의 자식 클래스입니다. UGameplayStatics::SaveGameToSlot() 함수를 호출하면, 내부적으로 언리얼 엔진의 직렬화 메커니즘이 USaveGame 인스턴스의 모든 UPROPERTY() 변수를 읽어들여 파일로 기록합니다. 마찬가지로 LoadGameFromSlot()은 파일에서 데이터를 읽어들여 새로운 USaveGame 인스턴스에 채워 넣습니다.

UObjectAActor 참조 직렬화 (고급)

단순한 값 타입과 컨테이너는 UPROPERTY()로 충분하지만, UObject*AActor*와 같은 다른 UObject 또는 AActor에 대한 포인터 참조는 직접적으로 저장되지 않습니다. 왜냐하면 이들은 메모리의 특정 주소를 가리키는데, 게임을 종료했다가 다시 시작하면 해당 오브젝트가 다른 메모리 주소에 할당되거나 아예 존재하지 않을 수 있기 때문입니다.

이러한 참조를 직렬화하려면 다음 전략 중 하나를 사용해야 합니다.

  • 이름 또는 ID 저장: 참조하려는 액터/오브젝트의 고유한 이름(FString)이나 ID(예: FGuid)를 저장합니다. 게임을 불러올 때, 이 이름/ID를 사용하여 현재 월드에서 해당 액터/오브젝트를 찾아서 다시 참조를 연결합니다.
    • 예시: UPROPERTY() FString TargetActorName;
  • FSoftObjectPtr / FSoftClassPtr: 에셋(Assets)에 대한 참조를 저장할 때 사용합니다. 이들은 메모리에 로드되지 않은 에셋에 대한 "약한" 참조를 저장하며, 필요할 때 에셋을 비동기적으로 로드할 수 있습니다.
    • 예시: UPROPERTY() FSoftObjectPtr<UTexture2D> PlayerIconAsset;
  • TWeakObjectPtr: 런타임에 생성된 UObject에 대한 "약한" 참조를 저장합니다. 참조된 오브젝트가 가비지 컬렉션될 수 있으므로, 사용 시에는 항상 IsValid() 검사를 해야 합니다.
  • Custom Serialization (커스텀 직렬화): Serialize() 함수를 오버라이드하여 FArchive를 통해 데이터를 수동으로 읽고 쓰는 방식입니다. 매우 복잡하고 특정 상황에서만 사용됩니다.

대부분의 게임 저장 시스템에서는 플레이어의 인벤토리 아이템처럼 FString (아이템 ID) 또는 FDataTableRowHandle (데이터 테이블 행 참조)과 같은 고유 식별자를 저장하여 해당 아이템의 클래스를 재구성하는 방식으로 사용됩니다.


직렬화 과정에서의 주의사항

  • 데이터 버전 관리: 게임이 업데이트되면서 USaveGame 클래스의 구조가 변경될 수 있습니다 (변수 추가/제거, 타입 변경). 이 경우 이전 버전의 저장 파일이 새로운 버전의 게임에서 로드될 때 문제가 발생할 수 있습니다. 언리얼 엔진은 기본적인 버전 관리 메커니즘을 제공하지만, 복잡한 변경에는 개발자의 추가적인 처리가 필요할 수 있습니다 (VER_ADDED_SOME_FEATURE).
  • 민감한 정보 저장 금지: 보안상 민감한 정보(예: 비밀번호, 치트 코드)는 SaveGame 파일에 직접 저장해서는 안 됩니다. SaveGame 파일은 쉽게 변조될 수 있기 때문입니다.
  • 데이터 용량: 너무 많은 데이터를 SaveGame에 저장하면 파일 크기가 커지고 저장/로드 시간이 길어질 수 있습니다. 필요한 데이터만 효율적으로 저장하는 것이 중요합니다.
  • 가비지 컬렉션: SaveGame 오브젝트를 로드한 후, 해당 데이터를 게임 상태에 적용했다면 SaveGame 오브젝트는 더 이상 필요하지 않을 수 있습니다. SaveGameToSlot()이나 LoadGameFromSlot()UObject를 반환하므로, 명시적으로 nullptr로 설정하거나 참조가 끊어지면 가비지 컬렉션의 대상이 됩니다.

블루프린트에서의 직렬화

블루프린트에서 SaveGame 오브젝트를 만들고 저장/로드할 때도 C++와 동일한 직렬화 원리가 적용됩니다. 블루프린트에서 생성된 SaveGame 블루프린트 클래스 내에 Variable로 추가된 모든 변수는 자동으로 직렬화 대상이 됩니다.

Create Save Game Object, Save Game to Slot, Load Game from Slot, Does Save Game Exist 노드들은 내부적으로 C++의 UGameplayStatics 함수들을 호출하며, 동일한 직렬화 메커니즘을 사용합니다.


데이터 직렬화와 역직렬화는 게임의 지속성을 보장하는 근본적인 메커니즘입니다. UPROPERTY() 매크로의 중요성을 이해하고, USaveGame 클래스를 효과적으로 설계하며, 필요에 따라 복잡한 참조를 처리하는 방법을 아는 것은 견고하고 확장 가능한 저장 시스템을 구축하는 데 필수적입니다. 이러한 이해를 바탕으로 플레이어가 언제든지 게임 진행 상황을 이어서 즐길 수 있도록 할 수 있습니다.