본문으로 건너뛰기

안동민 개발노트

본문 시작

간단한 데이터 직렬화와 역직렬화 기법

게임 상태를 저장 가능한 형태로 직렬화하고 다시 객체 상태로 복원하는 흐름과 데이터 변경 시 안정성을 익힙니다.

지난 절에서 Save Game Object를 통해 데이터를 저장하고 불러오는 방법을 익혔다면, 이번에는 그 내부 원리를 짚어볼 차례입니다.

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

이 개념을 이해하면 저장 시스템에서 어떤 데이터가 안전하게 보존되는지, 어떤 데이터는 별도 처리가 필요한지 훨씬 명확해집니다.

데이터 직렬화와 역직렬화 기법

Save Game Object로 데이터를 저장하고 불러온 뒤에는 저장 형식, 복원 시점, 버전 호환성을 기준으로 직렬화(Serialization)와 역직렬화(Deserialization)를 구분합니다.

  1. Runtime State

    체력, 위치, 진행도에서 저장 경계 안의 값만 선택합니다.

  2. Serialize

    선택한 값을 슬롯에 쓸 수 있는 형식과 버전으로 변환합니다.

  3. Slot Data

    저장 형식은 런타임 객체와 분리되며 다음 실행까지 유지됩니다.

  4. Deserialize & Restore

    버전을 확인한 뒤 값을 복원하고 파생 상태는 다시 계산합니다.

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

    BOUNDARY Actor 포인터와 임시 위젯처럼 다음 실행에서 유효하지 않은 참조는 저장하지 않습니다. VERSION 필드 추가·이름 변경에 대비해 버전과 마이그레이션 경로를 둡니다. VALIDATE 복원 전에 enum, 수량, 좌표가 현재 게임 규칙의 범위 안인지 확인합니다. DERIVED 합계, UI 표시, 캐시처럼 원본에서 만들 수 있는 값은 로드 뒤 다시 계산합니다.

  6. 런타임 참조 제외

    BOUNDARY Actor 포인터와 임시 위젯처럼 다음 실행에서 유효하지 않은 참조는 저장하지 않습니다.

  7. 스키마 버전

    VERSION 필드 추가·이름 변경에 대비해 버전과 마이그레이션 경로를 둡니다.

  8. 값 범위 검증

    VALIDATE 복원 전에 enum, 수량, 좌표가 현재 게임 규칙의 범위 안인지 확인합니다.

  9. 파생 상태 재계산

    DERIVED 합계, UI 표시, 캐시처럼 원본에서 만들 수 있는 값은 로드 뒤 다시 계산합니다.


직렬화(Serialization)란 무엇인가?

직렬화는 메모리(RAM) 상에 존재하는 복잡한 객체(Object)의 데이터를 디스크에 저장하거나 네트워크를 통해 전송할 수 있는 형태로 변환하는 과정입니다.

메모리 상의 객체는 여러 변수가 서로 다른 메모리 주소에 흩어져 있거나, 복잡한 포인터 관계로 얽혀 있을 수 있습니다.

이러한 구조를 그대로 디스크에 저장하거나 다른 컴퓨터로 보내는 것은 불가능합니다.

직렬화는 이러한 복잡한 메모리 구조를 연속적인 바이트 스트림(Byte Stream), 즉 하나의 긴 일렬 데이터로 변환합니다.

이렇게 변환된 데이터는 파일로 저장되거나 네트워크를 통해 전송될 수 있습니다.

직렬화는 런타임 객체의 상태를 저장 가능한 바이트 또는 텍스트 형식으로 변환하는 과정입니다.

박제된 생물이나 표준화된 설계도는 보관하거나 이동하기 쉽습니다.

언리얼 엔진의 저장 게임 오브젝트는 바로 이 직렬화 과정을 거쳐 디스크에 파일 형태로 저장됩니다.

블루프린트 개발자는 Save Game to Slot 노드를 호출함으로써 이 복잡한 직렬화 과정을 언리얼 엔진에게 맡기는 것입니다.


역직렬화(Deserialization)란 무엇인가?

역직렬화는 직렬화된 바이트 스트림 데이터를 다시 메모리 상의 원래 객체 형태로 복원하는 과정입니다.

디스크에 저장된 파일이나 네트워크를 통해 수신된 바이트 스트림을 해석하여, 게임 내에서 다시 사용할 수 있는 객체로 재구성하는 것이죠.

역직렬화는 저장된 데이터를 다시 런타임 객체 상태로 복원하는 과정입니다.

언리얼 엔진의 Load Game From Slot 노드를 호출하면, 디스크의 저장 파일이 역직렬화 과정을 거쳐 SaveGame Object 형태로 메모리에 복원됩니다.


블루프린트와 직렬화/역직렬화

아래 다이어그램은 저장할 변수를 고른 뒤 SaveGame 객체에 담고, 슬롯 저장과 로드 검증으로 이어지는 기본 흐름을 정리한 것입니다.

Blueprint SaveGame은 값 복사와 슬롯 기록을 분리한다

SaveGame 클래스는 저장할 필드의 그릇이고, 실제 기록은 SaveGameToSlot 노드가 맡는다.

  1. SaveGame Class

    저장할 변수 정의

  2. Create Object

    현재 저장 객체 생성

  3. Set Variables

    점수, 위치, 옵션 복사

  4. Slot Name

    저장 위치 이름 지정

  5. Save To Slot

    쓰기 결과 확인

점검기준
중요SaveGame 객체를 만들었다고 자동 저장되는 것은 아니다.
검수Set한 값이 SaveGameToSlot 전에 모두 채워지는지 본다.

블루프린트를 사용할 때 대부분의 직렬화/역직렬화 과정은 언리얼 엔진 내부적으로 자동으로 처리됩니다.

개발자가 직접 바이트 스트림을 다룰 필요는 거의 없습니다.

하지만 어떤 데이터가 직렬화될 수 있고 없는지 이해하는 것은 중요합니다.

  • 저장 게임 오브젝트의 변수: BP_MySaveGame 같은 저장 게임 오브젝트 블루프린트에 선언된 변수(Integer, Float, Vector, Text, Array, Map, Structure, Object Reference 등)는 기본적으로 직렬화 대상입니다. Save Game to Slot을 호출하면 이 변수 값들이 자동으로 저장 파일에 기록됩니다.
  • 복잡한 오브젝트 참조: 저장 게임 오브젝트에 특정 액터(Actor Reference)를 직접 넣을 때는 주의해야 합니다. 일반적인 Actor Reference는 레벨 로드 시 메모리 주소가 바뀔 수 있으므로, 액터 자체보다 고유 ID(Get Actor Unique ID)나 이름(Get Name)을 저장하고 로드 시 다시 조회해 연결하는 방식이 더 안정적입니다.
    • 예외: 언리얼 엔진 5.0부터 도입된 Soft Object ReferenceSoft Class Reference는 직렬화가 가능하며, 필요한 시점에 로드하여 사용할 수 있도록 설계된 참조 타입입니다. 이는 에셋(Static Mesh, Material 등)에 대한 참조를 저장할 때 유용합니다.
  • 컴포넌트의 변수: 액터 블루프린트 내 컴포넌트의 변수는 기본적으로 해당 액터가 저장될 때 함께 직렬화되지 않습니다. 컴포넌트의 특정 상태를 저장하고 싶다면, 그 상태를 액터 블루프린트의 변수에 복사하여 저장 게임 오브젝트에 담거나, 저장/로드 로직을 직접 구현해야 합니다.

저장 대상 타입을 고를 때는 값 자체, 참조, 복원 방법을 함께 판단해야 합니다.

슬롯에 남길 값과 로드 후 다시 찾을 단서를 분리한다

SaveGame은 실행 중 객체를 그대로 보존하지 않으므로 값은 저장하고 참조는 복원 키로 바꾼다.

질문판정
점수, 옵션, 진행도인가?그대로 SaveGame 필드에 저장한다.
Actor 인스턴스인가?고유 ID나 위치를 저장하고 로드 후 다시 찾는다.
에셋 참조인가?Soft Object Path를 저장하고 필요할 때 로드한다.
구조가 바뀔 수 있나?Version 필드를 두고 로드 후 보정한다.

직렬화와 역직렬화의 활용 (고급 개념)

언리얼 엔진의 내장 저장 시스템 외에도, 직렬화/역직렬화는 다음과 같은 상황에서 간접적으로 활용되거나 이해가 필요합니다.

  • 네트워크 통신: 멀티플레이어 게임에서 클라이언트와 서버 간에 데이터를 주고받을 때, 메모리상의 게임 상태는 네트워크를 통해 전송 가능한 바이트 스트림으로 직렬화되어야 합니다.
  • 에셋 관리: 언리얼 엔진의 모든 에셋(Static Mesh, Texture, Material, Blueprint 등)은 디스크에 파일 형태로 저장될 때 직렬화됩니다. 엔진이 이 파일을 로드할 때 역직렬화하여 메모리 상의 에셋 객체로 만듭니다.
  • 커스텀 저장 형식: SaveGame 시스템 외에 완전히 커스텀된 저장 형식을 만들고자 할 때, 파일을 직접 읽고 쓰는 로직을 구현해야 하며, 이때 어떤 데이터를 어떤 순서와 형태로 바이트로 변환하고 다시 복원할지 직렬화/역직렬화의 개념이 필요합니다.

블루프린트 개발에서는 대부분 SaveGame Object를 통해 직렬화/역직렬화의 복잡성을 추상화하여 사용하지만, 이 개념을 이해하고 있으면 왜 특정 타입의 데이터가 저장될 수 있고 없는지, 그리고 고급 저장 기능을 구현할 때 어떤 문제가 발생할 수 있는지에 대한 통찰력을 얻을 수 있습니다.

이는 데이터를 다루는 게임 개발의 근간이 되는 중요한 원리입니다.


저장 안정성 검증 루틴

직렬화/역직렬화 로직을 적용한 뒤에는 다음 순서로 검증하면 안정성이 높아집니다.

  1. 저장 직후 즉시 로드하여 동일 값 재현 여부 확인
  2. 게임 재시작 후 로드 테스트로 슬롯 유효성 확인
  3. 필드 추가/삭제 시 기본값 처리와 버전 호환 정책 점검
  4. 참조형 데이터는 ID 기반 복원으로 null 참조 여부 확인

아래 다이어그램처럼 같은 슬롯을 기준으로 저장, 재시작, 구조 변경, 복원 검증을 반복하면 직렬화 오류를 더 빨리 찾을 수 있습니다.

저장 구조가 바뀔 때 같은 슬롯 검증

직렬화는 성공했지만 복원 값이 깨지는 경우가 있으므로, 저장 직후와 재시작 후를 같은 기준으로 비교합니다.

  1. Save

    현재 변수 값을 슬롯에 쓰고 성공 반환값을 확인합니다.

  2. Restart

    에디터나 게임을 다시 실행해 메모리 잔상을 제거합니다.

  3. Migrate

    필드 추가, 삭제, 이름 변경 후 기본값 보정을 통과시킵니다.

  4. Restore

    캐스팅 결과와 실제 게임 오브젝트 적용 값을 비교합니다.

이 루틴을 배포 전 체크 항목으로 고정하면 저장 데이터 회귀를 줄일 수 있습니다.


이번 절에서는 데이터 직렬화와 역직렬화의 기본 개념, 그리고 언리얼 엔진 블루프린트에서 이들이 어떻게 작동하는지에 대해 알아보았습니다.

블루프린트 저장 흐름 점검 메모

저장 기능 실습은 "저장 성공" 여부만 확인하면 놓치는 부분이 많습니다.

아래 기준을 추가로 확인하면 복구 실패를 줄일 수 있습니다.

  • 저장 직후와 에디터 재시작 후의 로드 결과가 동일한지 비교합니다.
  • 버전 변경 시 기본값 보정 로직이 있는지 점검합니다.
  • 참조형 데이터는 ID 기준으로 직렬화/역직렬화하는지 확인합니다.

마지막으로 직렬화 경계를 값, 참조, 복원 로직으로 나누어 보면 저장 가능한 데이터와 다시 찾아야 하는 대상을 구분하기 쉽습니다.

값은 저장하고, 참조는 다시 찾는다

직렬화는 메모리 상태를 파일로 옮기는 과정이지만, 실행 중 주소나 임시 연결은 로드 후 재구성해야 합니다.

  1. 1
    게임 중 상태

    Memory Primitive 점수, 이름, bool, Vector 값 Struct 인벤토리 행, 퀘스트 진행 묶음 Actor Ref 현재 레벨의 액터 인스턴스 연결

  2. 2
    저장 파일에 남길 것

    Slot Value 그대로 복원 가능한 숫자와 텍스트 ID 액터를 다시 찾기 위한 이름이나 고유 키 경로 에셋은 Soft Object Reference로 보관

  3. 3
    로드 후 다시 적용

    Restore Cast SaveGame 클래스로 캐스팅해 변수 읽기 Lookup ID로 레벨 액터나 데이터 행 다시 찾기 Set 현재 오브젝트 변수와 컴포넌트 상태에 반영

  4. 4
    Actor

    인스턴스 자체보다 이름, 태그, 고유 ID를 저장하고 로드 시 다시 조회합니다.

  5. 5
    Asset

    메시와 머티리얼은 Soft Reference로 경로를 보관하고 필요할 때 로드합니다.

  6. 6
    Component

    컴포넌트 내부 상태는 소유 액터 변수로 옮겨 SaveGame에 담습니다.

마지막으로 직렬화와 역직렬화에서 저장 가능한 값과 복구 가능한 값을 구분하는 기준을 정리합니다.

직렬화 데이터 경계 정하기

블루프린트 저장 데이터는 즉시 쓰는 런타임 값과 파일에 남길 안정 값의 경계를 나눌 때 안전해집니다.

  1. 저장 가능한 값만 구조체로 묶기

    액터 참조나 위젯 참조 대신 ID, Transform, Row Name, Gameplay Tag처럼 다시 찾을 수 있는 값을 저장합니다. 01 현재 상태를 F_PlayerSaveData 같은 구조체로 모아 SaveGame 오브젝트에 넣고, 임시 참조는 제외합니다. 02 로드한 Struct Version, Level Name, Row Name이 현재 프로젝트 데이터와 맞는지 확인한 후 역직렬화합니다. 03 사라진 아이템 ID나 유효하지 않은 Transform은 기본 위치, 기본 장비, 빈 배열로 되돌리는 분기를 둡니다.

  2. 저장 가능한 값만 구조체로 묶기

    액터 참조나 위젯 참조 대신 ID, Transform, Row Name, Gameplay Tag처럼 다시 찾을 수 있는 값을 저장합니다.

  3. 쓰기 전 변환

    현재 상태를 F_PlayerSaveData 같은 구조체로 모아 SaveGame 오브젝트에 넣고, 임시 참조는 제외합니다.

  4. 읽은 뒤 검증

    드한 Struct Version, Level Name, Row Name이 현재 프로젝트 데이터와 맞는지 확인한 후 역직렬화합니다.

  5. 복구 경로

    사라진 아이템 ID나 유효하지 않은 Transform은 기본 위치, 기본 장비, 빈 배열로 되돌리는 분기를 둡니다.

직렬화와 역직렬화에서 안전하게 저장할 값을 가르는 기준을 추가합니다.

직렬화는 복원 가능한 값만 통과시킨다

저장 가능한 데이터는 파일로 쓰고 다시 읽어도 같은 의미를 가져야 한다.

질문판정
원시 값 또는 Struct인가?안전하다. 숫자, 문자열, bool, 작은 배열을 우선한다.
월드 Actor 참조인가?직렬화하지 말고 ID, 태그, 위치 같은 단서를 남긴다.
런타임 전용 객체인가?저장 대상에서 제외하고 로드 후 재생성한다.
버전 변경 가능성이 있나?SaveVersion과 기본값 보정 경로를 둔다.

직렬화와 역직렬화를 설계할 때는 값 자체로 저장 가능한 데이터와 로드 후 다시 찾아야 하는 참조를 분리해야 합니다.

직렬화 가능 데이터는 값, 식별자, 버전으로 선별한다

저장 전에 “값 자체인가, 다시 찾을 단서인가, 호환성 정보인가”로 나누면 위험한 참조를 줄일 수 있다.

  1. Value

    Score, Level, Options 즉시 저장

  2. Identifier

    ActorId, RowName, AssetPath 로드 후 재연결

  3. Version

    SaveVersion, SchemaVersion 구조 변경 보정

선택 기준의미
저장 금지Widget, Controller, Timer, Dynamic Material Instance
검수로드 직후 None이 될 수 있는 값은 식별자로 바꾼다.