콘텐츠로 이동

Unity Save System — JSON vs Binary

게임 세이브 시스템은 “데이터를 어떤 형식으로, 어디에 저장하느냐”로 결정됩니다. Unity에서 가장 많이 쓰는 두 방식은 JSON(텍스트, 디버그 용이)과 Binary(바이너리, 크기·속도 우위)입니다. 두 방식의 특성을 정확히 이해하고 프로젝트 요구사항에 맞게 선택해야 합니다.


// Application.persistentDataPath: 플랫폼별 쓰기 가능 경로
// PC: C:/Users/<user>/AppData/LocalLow/<company>/<product>/
// Android: /data/data/<package>/files/
// iOS: /var/mobile/Applications/<UUID>/Documents/
string SavePath => Path.Combine(Application.persistentDataPath, "save.dat");

PlayerPrefs는 소량의 설정값(볼륨, 언어)에만 씁니다. 실제 게임 세이브는 파일 시스템에 직접 저장하는 것이 안전합니다.


[System.Serializable]
public class SaveData
{
public string playerName;
public int level;
public float health;
public Vector3Serializable position; // Vector3은 직렬화 직접 지원 안 됨
public List<string> unlockedItems = new();
public long savedAt; // UTC ticks
}
[System.Serializable]
public struct Vector3Serializable
{
public float x, y, z;
public static implicit operator Vector3(Vector3Serializable v)
=> new(v.x, v.y, v.z);
public static implicit operator Vector3Serializable(Vector3 v)
=> new() { x = v.x, y = v.y, z = v.z };
}
using System.IO;
using UnityEngine;
public static class JsonSaveSystem
{
private static readonly string Path =
System.IO.Path.Combine(Application.persistentDataPath, "save.json");
public static void Save(SaveData data)
{
data.savedAt = System.DateTime.UtcNow.Ticks;
string json = JsonUtility.ToJson(data, prettyPrint: false);
File.WriteAllText(Path, json);
}
public static SaveData Load()
{
if (!File.Exists(Path))
return new SaveData();
string json = File.ReadAllText(Path);
return JsonUtility.FromJson<SaveData>(json);
}
public static void Delete() => File.Delete(Path);
}

JsonUtility는 Unity 내장 직렬화기로 가볍고 빠릅니다. 단, Dictionary, private 필드, 다형성 타입은 지원하지 않습니다. 이 경우 Newtonsoft.Json (Json.NET) 패키지를 사용합니다.


BinaryFormatter는 .NET의 내장 바이너리 직렬화기지만 보안 취약점 때문에 Unity 2022+에서 obsolete 처리되었습니다. 대신 BinaryWriter/Reader를 직접 사용합니다.

using System.IO;
using UnityEngine;
public static class BinarySaveSystem
{
private static readonly string Path =
System.IO.Path.Combine(Application.persistentDataPath, "save.dat");
public static void Save(SaveData data)
{
using var fs = new FileStream(Path, FileMode.Create);
using var writer = new BinaryWriter(fs);
writer.Write(data.playerName ?? string.Empty);
writer.Write(data.level);
writer.Write(data.health);
writer.Write(data.position.x);
writer.Write(data.position.y);
writer.Write(data.position.z);
writer.Write(data.unlockedItems.Count);
foreach (var item in data.unlockedItems)
writer.Write(item);
writer.Write(data.savedAt);
}
public static SaveData Load()
{
if (!File.Exists(Path)) return new SaveData();
using var fs = new FileStream(Path, FileMode.Open);
using var reader = new BinaryReader(fs);
var data = new SaveData
{
playerName = reader.ReadString(),
level = reader.ReadInt32(),
health = reader.ReadSingle(),
position = new Vector3Serializable
{
x = reader.ReadSingle(),
y = reader.ReadSingle(),
z = reader.ReadSingle()
}
};
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
data.unlockedItems.Add(reader.ReadString());
data.savedAt = reader.ReadInt64();
return data;
}
}

모바일 게임에서 세이브 파일 위변조를 막으려면 AES 암호화를 적용합니다.

using System.Security.Cryptography;
using System.Text;
public static class SaveEncryption
{
// 실제 프로덕션에서는 키를 소스코드에 하드코딩하지 말고
// 서버 검증 + 난독화를 함께 사용할 것
private static readonly byte[] Key =
Encoding.UTF8.GetBytes("MyGame16ByteKey!"); // 16 bytes = AES-128
private static readonly byte[] IV =
Encoding.UTF8.GetBytes("InitVector1234!!");
public static byte[] Encrypt(string plainText)
{
using var aes = Aes.Create();
aes.Key = Key;
aes.IV = IV;
using var encryptor = aes.CreateEncryptor();
byte[] inputBytes = Encoding.UTF8.GetBytes(plainText);
return encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length);
}
public static string Decrypt(byte[] cipherBytes)
{
using var aes = Aes.Create();
aes.Key = Key;
aes.IV = IV;
using var decryptor = aes.CreateDecryptor();
byte[] outputBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
return Encoding.UTF8.GetString(outputBytes);
}
}

항목JSONBinary
가독성텍스트, 수동 편집 가능불가능
파일 크기크다 (키 이름 포함)작다
직렬화 속도보통빠름
스키마 변경 대응쉬움 (필드 추가 무시)어려움 (순서 의존)
위변조 난이도쉬움상대적으로 어려움
권장 용도설정, 개발 단계프로덕션 세이브

  • 개발 초기에는 JSON으로 데이터 구조를 빠르게 검증하고, 출시 전에 Binary + 암호화로 전환하는 것이 일반적인 패턴이다.
  • BinaryFormatter는 보안 이슈로 사용하지 말고 BinaryWriter/Reader를 직접 구현한다.
  • Vector3, Quaternion 등 Unity 타입은 직렬화 래퍼 구조체를 만들어 처리한다.
  • 세이브 파일 경로는 Application.persistentDataPath를 사용하고 플랫폼별 차이를 확인한다.