데이터 직렬화와 역직렬화
UPROPERTY 데이터가 직렬화·역직렬화되는 원리를 이해하고 객체 참조와 버전 변경 시 주의점을 익힙니다.
이전 절에서 SaveGame 시스템으로 저장/불러오기의 기본 흐름을 확인했다면, 이제 내부 원리를 알아둘 차례입니다.
핵심은 직렬화(Serialization)와 역직렬화(Deserialization)입니다.
이 개념은 단순 파일 저장을 넘어, 복잡한 게임 상태를 안정적으로 보존하고 복원하는 기반이 됩니다.
이번 절에서는 데이터 직렬화와 역직렬화가 무엇인지, 언리얼 엔진에서 어떻게 작동하는지, 그리고 개발자가 이를 어떻게 활용하며 무엇에 주의해야 하는지에 대해 깊이 있게 다뤄 보겠습니다.
직렬화 (Serialization)란?
직렬화는 메모리에 있는 복잡한 데이터 구조(객체, 변수, 배열, 맵 등)를 파일이나 네트워크를 통해 전송하거나 저장할 수 있는 순차적인 바이트 스트림(연속된 데이터 형식)으로 변환하는 과정을 말합니다.
게임에서는 주로 게임의 현재 상태를 .sav 파일과 같은 영구 저장소에 기록할 때 사용됩니다.
왜 직렬화가 필요한가요?
- 영구 저장: 게임이 종료된 후에도 데이터를 유지하기 위해 하드 드라이브 같은 비휘발성 저장소에 기록해야 합니다. 메모리의 데이터는 프로그램이 종료되면 사라집니다.
- 네트워크 전송: 멀티플레이어 게임에서 다른 클라이언트나 서버로 게임 상태를 전송할 때, 데이터는 네트워크를 통해 순차적으로 전송될 수 있는 형태로 변환되어야 합니다.
- 재구성 규칙: 값과 참조를 다시 연결할 형식을 정합니다. 형식 호환성, 참조 대상 존재 여부와 게임 규칙 검증은 별도로 필요합니다.
역직렬화 (Deserialization)란?
역직렬화는 직렬화된 바이트 스트림을 읽어 들여 메모리에서 원래의 복잡한 데이터 구조(객체)로 재구성하는 과정입니다.
게임에서는 저장된 .sav 파일을 불러와 게임의 상태를 이전 시점으로 되돌릴 때 사용됩니다.
왜 역직렬화가 필요한가요?
- 게임 상태 복원: 저장된 게임 파일을 불러와 캐릭터의 위치, 인벤토리, 퀘스트 진행 상황 등을 이전 상태로 정확하게 복원하기 위함입니다.
- 네트워크 데이터 수신: 네트워크를 통해 수신된 순차적인 데이터를 다시 게임 로직에서 처리할 수 있는 객체 형태로 변환합니다.
언리얼 엔진의 직렬화 메커니즘
언리얼 엔진은 자체적인 강력한 직렬화 시스템을 가지고 있으며, 이는 UObject 시스템과 밀접하게 통합되어 있습니다.
UPROPERTY()의 중요성
언리얼 엔진의 직렬화 시스템에서 가장 중요한 부분은 UPROPERTY() 매크로입니다.
UPROPERTY()는 멤버를 리플렉션에 노출합니다. 실제 저장 대상은 속성 지정자와 사용하는 아카이브의 정책에 따라 결정됩니다.
이것이 바로 USaveGame 클래스에서 우리가 저장하려는 모든 변수에 UPROPERTY()를 붙인 이유입니다.
FString, 숫자, 벡터 및 지원되는 컨테이너 속성은 엔진의 기본 직렬화를 사용할 수 있습니다. Transient 속성처럼 저장에서 제외되는 경우도 있습니다.
USaveGame 클래스의 직렬화
USaveGame은 언리얼 직렬화 시스템을 사용하도록 설계된 UObject 파생 클래스입니다.
기본 UGameplayStatics::SaveGameToSlot()은 non-transient 속성을 기록하며 SaveGame 지정자 유무를 검사하지 않습니다. 별도의 SaveGame 아카이브를 구성한 경우와 구분해야 합니다.
반대로 LoadGameFromSlot()은 파일 데이터를 읽어 새 USaveGame 인스턴스를 복원합니다.
UObject 및 AActor 참조 직렬화 (고급)
객체 참조가 직렬화되더라도 포인터의 메모리 주소나 대상 객체의 전체 상태가 다음 실행으로 그대로 옮겨지는 것은 아닙니다. 복원할 대상에 맞는 식별 방식을 선택합니다.
현재 실행의 참조와 다음 실행의 식별자는 다릅니다
door-F/open=true 저장 레코드→현재 월드 ID 조회→대상 존재 검사→해당 문에 상태 적용 또는 복원 정책의 상태 흐름이다. 원 코드 구현 완료가 아닌 설명 정책으로 명시한다.
UNREAL C++ · CH7-2
현재 실행의 참조와 다음 실행의 식별자는 다릅니다
객체 수명을 추적하는 약한 참조, 에셋을 찾는 소프트 참조, 프로젝트가 유지하는 개체 ID를 구분합니다. 다음 실행에서 대상 찾기·생성·상태 적용은 복원 정책에 따라 수행합니다.
이 흐름은 원 코드에 이미 구현된 ID 등록표가 아니라 설명용 복원 정책입니다. 같은 클래스의 다른 문에 상태를 적용하거나 과거 포인터를 재사용하지 않으며, 생성 정책도 자동으로 제공되지 않습니다.
| 대상 | 표현 | 복원할 때의 책임 |
|---|---|---|
| 현재 실행의 객체 | TWeakObjectPtr는 객체를 살려 두지 않는 약한 참조입니다. | IsValid를 확인해 사용합니다. 실행 간 영구 식별자로 사용할 수는 없습니다. |
| 에셋 | TSoftObjectPtr<UTexture2D>와 TSoftClassPtr<T>는 타입을 지정한 소프트 참조입니다. | 경로가 가리키는 에셋의 존재와 로드 결과를 확인합니다. 참조 보관만으로 메모리 로드가 완료되지는 않습니다. |
| 게임 속 개체 | 프로젝트가 유지하는 아이템 ID나 FGuid 등 안정적인 식별자를 저장합니다. | 현재 월드에서 찾거나 생성하고 상태를 적용합니다. 액터 이름의 실행 간 안정성은 별도 정책입니다. |
- 현재 실행의 객체
- 표현: TWeakObjectPtr는 객체를 살려 두지 않는 약한 참조입니다.복원할 때의 책임: IsValid를 확인해 사용합니다. 실행 간 영구 식별자로 사용할 수는 없습니다.
- 에셋
- 표현: TSoftObjectPtr<UTexture2D>와 TSoftClassPtr<T>는 타입을 지정한 소프트 참조입니다.복원할 때의 책임: 경로가 가리키는 에셋의 존재와 로드 결과를 확인합니다. 참조 보관만으로 메모리 로드가 완료되지는 않습니다.
- 게임 속 개체
- 표현: 프로젝트가 유지하는 아이템 ID나 FGuid 등 안정적인 식별자를 저장합니다.복원할 때의 책임: 현재 월드에서 찾거나 생성하고 상태를 적용합니다. 액터 이름의 실행 간 안정성은 별도 정책입니다.
FSoftObjectPtr·FSoftClassPtr는 비템플릿이고 TSoftObjectPtr·TSoftClassPtr가 타입 지정 형태입니다. 소프트 참조는 대상의 전체 런타임 상태를 저장하지 않습니다. 저장 버전·필수 값·대상 존재를 함께 검사합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 프로젝트가 실행 간 보존한 door-F와 open=true 레코드를 현재 월드 등록표에 대응시키는 설계 예다. 현재 대상이 있으면 저장 상태를 적용하고, 없으면 프로젝트의 생성/오류/건너뛰기 정책을 택한다. 이 그림에서 약한 참조는 대상을 생성하거나 수명을 연장하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 저장 레코드 설명 정책의 문 ID와 상태를 읽는다. |
|
|
| 현재 월드 조회 현재 월드에서 같은 ID의 대상을 찾는다. |
|
|
| 상태 적용·정책 대상이 있으면 문 상태를 반영한다. |
|
|
현재 실행의 참조와 다음 실행의 식별자는 다릅니다
door-F/open=true 저장 레코드→현재 월드 ID 조회→대상 존재 검사→해당 문에 상태 적용 또는 복원 정책의 상태 흐름이다. 원 코드 구현 완료가 아닌 설명 정책으로 명시한다.
UNREAL C++ · CH7-2
현재 실행의 참조와 다음 실행의 식별자는 다릅니다
객체 수명을 추적하는 약한 참조, 에셋을 찾는 소프트 참조, 프로젝트가 유지하는 개체 ID를 구분합니다. 다음 실행에서 대상 찾기·생성·상태 적용은 복원 정책에 따라 수행합니다.
이 흐름은 원 코드에 이미 구현된 ID 등록표가 아니라 설명용 복원 정책입니다. 같은 클래스의 다른 문에 상태를 적용하거나 과거 포인터를 재사용하지 않으며, 생성 정책도 자동으로 제공되지 않습니다.
| 대상 | 표현 | 복원할 때의 책임 |
|---|---|---|
| 현재 실행의 객체 | TWeakObjectPtr는 객체를 살려 두지 않는 약한 참조입니다. | IsValid를 확인해 사용합니다. 실행 간 영구 식별자로 사용할 수는 없습니다. |
| 에셋 | TSoftObjectPtr<UTexture2D>와 TSoftClassPtr<T>는 타입을 지정한 소프트 참조입니다. | 경로가 가리키는 에셋의 존재와 로드 결과를 확인합니다. 참조 보관만으로 메모리 로드가 완료되지는 않습니다. |
| 게임 속 개체 | 프로젝트가 유지하는 아이템 ID나 FGuid 등 안정적인 식별자를 저장합니다. | 현재 월드에서 찾거나 생성하고 상태를 적용합니다. 액터 이름의 실행 간 안정성은 별도 정책입니다. |
- 현재 실행의 객체
- 표현: TWeakObjectPtr는 객체를 살려 두지 않는 약한 참조입니다.복원할 때의 책임: IsValid를 확인해 사용합니다. 실행 간 영구 식별자로 사용할 수는 없습니다.
- 에셋
- 표현: TSoftObjectPtr<UTexture2D>와 TSoftClassPtr<T>는 타입을 지정한 소프트 참조입니다.복원할 때의 책임: 경로가 가리키는 에셋의 존재와 로드 결과를 확인합니다. 참조 보관만으로 메모리 로드가 완료되지는 않습니다.
- 게임 속 개체
- 표현: 프로젝트가 유지하는 아이템 ID나 FGuid 등 안정적인 식별자를 저장합니다.복원할 때의 책임: 현재 월드에서 찾거나 생성하고 상태를 적용합니다. 액터 이름의 실행 간 안정성은 별도 정책입니다.
FSoftObjectPtr·FSoftClassPtr는 비템플릿이고 TSoftObjectPtr·TSoftClassPtr가 타입 지정 형태입니다. 소프트 참조는 대상의 전체 런타임 상태를 저장하지 않습니다. 저장 버전·필수 값·대상 존재를 함께 검사합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 프로젝트가 실행 간 보존한 door-F와 open=true 레코드를 현재 월드 등록표에 대응시키는 설계 예다. 현재 대상이 있으면 저장 상태를 적용하고, 없으면 프로젝트의 생성/오류/건너뛰기 정책을 택한다. 이 그림에서 약한 참조는 대상을 생성하거나 수명을 연장하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 저장 레코드 설명 정책의 문 ID와 상태를 읽는다. |
|
|
| 현재 월드 조회 현재 월드에서 같은 ID의 대상을 찾는다. |
|
|
| 상태 적용·정책 대상이 있으면 문 상태를 반영한다. |
|
|
복잡한 형식은 Serialize(FArchive&)에서 직접 읽고 쓸 수 있지만, 참조 해석과 버전 정책도 함께 책임져야 합니다.
대부분의 게임 저장 시스템에서는 플레이어의 인벤토리 아이템처럼 FString (아이템 ID) 또는 FDataTableRowHandle (데이터 테이블 행 참조)과 같은 고유 식별자를 저장하여 해당 아이템의 클래스를 재구성하는 방식을 사용합니다.
직렬화 과정에서의 주의사항
- 데이터 버전 관리: 업데이트 과정에서
USaveGame구조(변수 추가/제거, 타입 변경)가 바뀌면 구버전 저장 파일 로드 시 문제가 생길 수 있습니다. 언리얼 엔진의 기본 버전 관리 메커니즘만으로 충분하지 않은 경우가 있으므로, 복잡한 변경에는 개발자가 버전 분기 처리(VER_ADDED_SOME_FEATURE등)를 직접 넣어야 합니다. - 민감한 정보 저장 금지: 보안상 민감한 정보(예: 비밀번호, 치트 코드)는 SaveGame 파일에 직접 저장해서는 안 됩니다. SaveGame 파일은 쉽게 변조될 수 있기 때문입니다.
- 데이터 용량: 너무 많은 데이터를 SaveGame에 저장하면 파일 크기가 커지고 저장/로드 시간이 길어질 수 있습니다. 필요한 데이터만 효율적으로 저장하는 것이 중요합니다.
- 가비지 컬렉션:
SaveGameToSlot()은 성공 여부bool,LoadGameFromSlot()은USaveGame*를 반환합니다. 로드한 객체를 계속 사용할 때는 GC가 추적하는 강한 참조로 유지합니다. 한 포인터를nullptr로 바꾸는 것만으로 다른 참조까지 없어지지는 않습니다.
저장 데이터는 단순히 값을 파일에 넣는 것이 아니라, 버전과 참조 복원 규칙을 포함한 작은 프로토콜로 설계하는 편이 안전합니다.
블루프린트에서의 직렬화
블루프린트에서 SaveGame 오브젝트를 만들고 저장/로드할 때도 C++와 동일한 직렬화 원리가 적용됩니다.
블루프린트 변수도 속성으로 직렬화되며, 실제 저장 범위는 동일한 함수·속성 정책을 따릅니다.
Create Save Game Object, Save Game to Slot, Load Game from Slot, Does Save Game Exist 노드들은 내부적으로 C++의 UGameplayStatics 함수들을 호출하며, 동일한 직렬화 메커니즘을 사용합니다.
예시: 다음 실행에서 문 상태를 다시 연결하기
프로젝트가 문 F에 실행 간 유지되는 ID door-F를 부여하고, 저장 레코드에 그 ID와 open=true를 기록하는 정책을 정했다고 가정합니다. 다음 실행에서는 현재 월드의 ID 등록표에서 문 F를 찾은 뒤 저장 상태를 적용합니다. 지난 실행의 포인터 주소를 현재 액터처럼 사용하지 않습니다. 클래스가 같은 다른 문 C에 open=true를 적용하는 것도 같은 복원이 아닙니다. ID의 고유성·보존과 찾지 못했을 때 생성하거나 건너뛸 정책까지 정해야 합니다.
버전 변경에도 같은 원리가 필요합니다. 예를 들어 구버전의 Health=75를 새 버전의 Health와 MaxHealth로 나눈다면, 이전 형식에서는 MaxHealth가 없다는 사실을 먼저 판별하고 합의한 기본값 또는 변환 규칙을 적용합니다. 직렬화가 자동으로 게임 설계의 새 의미까지 추론해 주지는 않습니다.
수명 추적과 복원 정책 판단하기
문 F를 가리키던 약한 참조가 무효가 되었고, 현재 등록표에도 door-F가 없습니다. 저장 파일에는 open=true가 남아 있습니다. 약한 참조가 문을 살려 두었거나 자동으로 새 문을 만들었다고 판단해도 될까요? 별도로 MaxHealth가 없는 구버전을 MaxHealth=100으로 옮긴다는 명시적 규칙이 있다면 Health=75의 표시 비율은 얼마인지 계산하세요.
대상 재연결과 버전 변환을 나누기
약한 참조는 수명을 유지하거나 다음 실행의 액터를 생성하는 기능이 아닙니다. 대상이 없으면 현재 프로젝트의 생성·실패·건너뛰기 정책을 따르고, 무효 참조로 상태를 적용하지 않습니다. 주어진 버전 변환 규칙에서는 75/100 = 0.75로 75%입니다. 이 기본값은 문제에서 정한 정책이며 모든 저장 형식이 자동으로 사용하는 값이 아닙니다.
파일을 읽은 뒤에는 버전과 필수 값, 참조 대상의 존재 여부를 확인한 후 게임 상태에 적용합니다.