본문으로 건너뛰기

안동민 개발노트

본문 시작

데이터 직렬화와 역직렬화

UPROPERTY 데이터가 직렬화·역직렬화되는 원리를 이해하고 객체 참조와 버전 변경 시 주의점을 익힙니다.

이전 절에서 SaveGame 시스템으로 저장/불러오기의 기본 흐름을 확인했다면, 이제 내부 원리를 알아둘 차례입니다.

핵심은 직렬화(Serialization)역직렬화(Deserialization)입니다.

이 개념은 단순 파일 저장을 넘어, 복잡한 게임 상태를 안정적으로 보존하고 복원하는 기반이 됩니다.

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

데이터 직렬화와 역직렬화

직렬화는 런타임 객체 상태를 저장 가능한 형식으로 바꾸고, 역직렬화는 버전이 달라진 저장값을 다시 안전한 객체 상태로 복원하는 경계입니다.

  1. 1
    직렬화 (Serialization) 개념

    직렬화는 객체와 배열을 저장 가능한 바이트 흐름으로 바꿉니다.

  2. 2
    역직렬화 (Deserialization) 개념

    역직렬화는 직렬화된 바이트 스트림을 읽어들여 메모리에서 원래의 복잡한 데이터 구조(객체)로 재구성하는 과정입니다.

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

    언리얼 엔진의 직렬화는 UPROPERTY 메타데이터와 UObject 리플렉션을 기준으로 저장/로드 대상을 추적합니다.

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

    저장 구조가 바뀌면 버전 필드로 구버전 로드를 분기합니다.

  5. 5
    데이터 직렬화와 역직렬화 기준

    저장 형식 저장 슬롯에 넣을 값과 런타임에서 다시 계산할 값을 먼저 나눕니다. 변환 흐름 UObject 참조, 배열, 맵은 저장 형식에서 식별자와 버전 필드를 함께 남겨야 복원이 안정적입니다. 호환 확인 역직렬화 (Deserialization)란? 직렬화 호환성 언리얼 직렬화는 UObject와 SaveGame 규칙 위에서 동작합니다.

  6. 6
    직렬화(Serialization)

    저장 형식 저장 슬롯에 넣을 값과 런타임에서 다시 계산할 값을 먼저 나눕니다.

  7. 7
    직렬화 (Serialization) 개념

    변환 흐름 UObject 참조, 배열, 맵은 저장 형식에서 식별자와 버전 필드를 함께 남겨야 복원이 안정적입니다.

  8. 역직렬화 (Deserialization)란?
    역직렬화 (Deserialization) 개념

    호환 확인 역직렬화 (Deserialization)란?

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

    직렬화 호환성 언리얼 직렬화는 UObject와 SaveGame 규칙 위에서 동작합니다.


직렬화 (Serialization)란?

직렬화는 다시 만들 상태만 SaveGame 슬롯에 안정적으로 계약화한다

메모리의 모든 것을 덤프하는 것이 아니라 저장과 로드 후 복원할 값을 고르는 일이다.

해야 할 일주의할 점
SerializeUPROPERTY 값 중 저장해야 할 상태를 표시한다불필요한 런타임 객체까지 저장하려 한다
Slot저장 파일 위치와 이름을 정한다사용자별/프로필별 구분이 없다
Deserialize로드 시 게임 상태로 다시 적용한다객체 포인터 존재 여부를 확인하지 않는다
Version필드가 바뀌면 이전 저장 데이터 호환 경로를 둔다업데이트 후 저장 파일을 못 읽는다

직렬화메모리에 있는 복잡한 데이터 구조(객체, 변수, 배열, 맵 등)를 파일이나 네트워크를 통해 전송하거나 저장할 수 있는 순차적인 바이트 스트림(연속된 데이터 형식)으로 변환하는 과정을 말합니다.

게임에서는 주로 게임의 현재 상태를 .sav 파일과 같은 영구 저장소에 기록할 때 사용됩니다.

왜 직렬화가 필요한가요?

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

역직렬화 (Deserialization)란?

역직렬화직렬화된 바이트 스트림을 읽어들여 메모리에서 원래의 복잡한 데이터 구조(객체)로 재구성하는 과정입니다.

게임에서는 저장된 .sav 파일을 불러와 게임의 상태를 이전 시점으로 되돌릴 때 사용됩니다.

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

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

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

UObject Serialization 저장·복원 경로

UPROPERTY, SaveGame 객체, 참조 처리, 역직렬화 복원 단계가 저장 데이터의 안정성을 어떻게 만드는지 정리한다.

  1. 1
    저장 대상 표시

    보존할 값은 UPROPERTY와 SaveGame 규칙에 맞게 관리한다. mark

  2. 2
    데이터 패킹

    USaveGame 객체나 Archive에 primitive, struct, soft reference를 기록한다. serialize

  3. 3
    버전 확인

    드 시 저장 버전과 현재 구조를 비교해 누락 필드를 기본값으로 채운다. version

  4. 4
    참조 복원

    Actor 직접 포인터보다 ID, SoftObjectPath, GameplayTag로 다시 찾는 구조가 안전하다. resolve

  5. 5
    테스트

    전 버전 세이브 파일을 현재 코드에서 로드해 본다.

  6. 6
    Null 처리

    참조 복원 실패 시 기본 동작과 오류 로그를 둔다.

  7. 7
    원자성

    저장 중 실패해도 기존 세이브가 깨지지 않도록 임시 파일 전략을 검토한다.

언리얼 엔진은 자체적인 강력한 직렬화 시스템을 가지고 있으며, 이는 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* 같은 포인터 참조는 직접 저장되지 않습니다.

이 포인터는 메모리 주소를 가리키므로 게임 재시작 후 동일 주소를 보장할 수 없기 때문입니다.

따라서 참조 복원이 필요하면 별도 식별자 전략을 함께 설계해야 합니다.

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

  • 이름 또는 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 직렬화 안전 계약

SaveGame은 UPROPERTY 값을 파일에 쓰는 기능이지만, 실제 안정성은 버전, 식별자, 복원 순서, 검증 규칙을 함께 둘 때 확보됩니다.

  1. 1
    저장 대상

    schema 값 타입 FString, int32, FVector 같은 값은 UPROPERTY로 직접 직렬화합니다. 참조 타입 Actor 포인터 대신 ID, 이름, 데이터 테이블 행으로 복원합니다.

  2. 2
    복원 흐름

    load path Load Slot 슬롯 존재 여부와 파일 읽기 성공을 먼저 확인합니다. 버전 점검 구버전 저장 구조는 마이그레이션 경로를 통과시킵니다. Resolve IDs 아이템, 퀘스트, 액터 참조는 현재 월드 데이터와 다시 연결합니다. Apply State 플레이어, 월드, UI에 순서대로 반영해 중간 불일치를 줄입니다.

  3. 3
    민감 정보

    risk 민감 정보 치트 토큰이나 비밀 값은 로컬 SaveGame에 그대로 저장하지 않습니다. 용량 증가 매 프레임 상태나 큰 배열 대신 진행에 필요한 핵심 값만 남깁니다. GC 수명 로드 오브젝트에서 게임 상태로 복사한 뒤 참조 유지 여부를 명확히 합니다.


블루프린트에서의 직렬화

블루프린트에서 SaveGame 오브젝트를 만들고 저장/로드할 때도 C++와 동일한 직렬화 원리가 적용됩니다.

블루프린트에서 생성된 SaveGame 블루프린트 클래스 내에 Variable로 추가된 모든 변수는 자동으로 직렬화 대상이 됩니다.

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


직렬화는 단순히 값을 바이트로 바꾸는 작업을 넘어, 저장 데이터가 다음 버전과 다음 실행에서도 의미를 유지하도록 만드는 약속입니다.

UPROPERTY 대상, 참조 식별자, 버전 분기, 로드 후 검증을 한 묶음으로 설계하면 SaveGame과 JSON, 네트워크 데이터 모두에서 같은 안정성 기준을 적용할 수 있습니다.

저장 데이터 복원 설계

직렬화는 메모리 값을 파일이나 네트워크에 맞게 바꾸는 과정이지만, 장기적으로 안전하려면 스키마, 버전, 참조 복원, 검증 규칙이 함께 있어야 합니다.

  1. Schema
    UPROPERTY 대상

    Schema 저장할 값 타입과 컨테이너를 명시하고 누락 필드를 줄입니다.

  2. Identity
    참조는 ID로

    Identity Actor 포인터 대신 이름, 경로, 행 키를 저장합니다.

  3. Version
    구조 변경 분기

    Version 필드 추가와 타입 변경은 로드 시 변환 경로를 둡니다.

  4. Resolve
    현재 월드 연결

    Resolve 저장된 ID를 레벨, 데이터 테이블, 에셋으로 다시 매칭합니다.

  5. Validate
    로드 후 검증

    Validate 누락, 범위 초과, 오래된 ID를 정상 분기로 처리합니다.

  6. write
    직렬화 전에 확인

    write 매 프레임 캐시나 임시 효과보다 플레이어 진행에 필요한 값을 우선합니다. 로컬 SaveGame이나 JSON 파일에 비밀 값과 치트 토큰을 그대로 두지 않습니다.

  7. 7
    필요한 데이터만 남김

    매 프레임 캐시나 임시 효과보다 플레이어 진행에 필요한 값을 우선합니다.

  8. 8
    민감 정보 제외

    컬 SaveGame이나 JSON 파일에 비밀 값과 치트 토큰을 그대로 두지 않습니다.

  9. read
    역직렬화 후 적용

    read 레벨과 데이터 정의가 준비된 뒤 플레이어 상태와 UI 값을 적용합니다. 슬롯 없음, JSON 파싱 실패, 오래된 버전은 로그와 기본값으로 회복합니다.

  10. 10
    참조 복원 순서 고정

    레벨과 데이터 정의가 준비된 뒤 플레이어 상태와 UI 값을 적용합니다.

  11. 11
    실패를 정상 분기로 처리

    슬롯 없음, JSON 파싱 실패, 오래된 버전은 로그와 기본값으로 회복합니다.

마지막으로 저장 형식이 SaveGame이든 JSON이든 버전, 참조, 검증 기준을 같은 체크리스트로 묶어 두면 복원 안정성이 높아집니다.

저장 데이터는 값, 식별자, 버전, 검증 규칙을 함께 담는 계약이다

SaveGame 파일은 메모리 주소를 보존하지 않으므로, 다시 찾을 수 있는 값으로 복원 경로를 남겨야 합니다.

  1. 대상 선택 저장할 값

    다시 찾을 참조를 분리합니다.

  2. 형식 결정 SaveGame, JSON

    DataTable 핸들을 고릅니다.

  3. 버전 기록 구조 변경에 대비해 저장 버전

    남깁니다.

  4. 참조 재연결 로드 시 현재 월드나 에셋

    다시 찾습니다.

  5. 검증 적용 없어진 에셋, 잘못된 ID, 누락 필드

    보정합니다.

직렬화와 역직렬화는 게임 지속성을 보장하는 핵심 메커니즘입니다.

UPROPERTY() 활용, USaveGame 구조 설계, 참조 복원 전략을 함께 고려하면 저장 시스템의 안정성과 확장성이 크게 올라갑니다.

이 기반이 갖춰져야 플레이어가 언제든 게임 진행 상황을 안전하게 이어갈 수 있습니다.