저장/불러오기
크래시에도 안전한 원자적 쓰기와 버전 마이그레이션을 갖춘 슬롯 기반 JSON 저장.
크래시에도 안전한 원자적 쓰기와 버전 마이그레이션을 갖춘 슬롯 기반 JSON 저장 · 한 줄 오프 스위치:
NullSaveService
무엇을 하는가
저장 서비스는 게임 상태를 디스크에 기록하고 나중에 복원합니다 — 저장 도중 게임이 크래시해도 손상 없이. "slot0"이나 "autosave" 같은 이름 있는 슬롯(슬롯은 그저 확장자 없는 파일 이름입니다)으로 Save<T>(slot, data)나 Load<T>(slot)을 호출하면 나머지는 서비스가 처리합니다: JSON 직렬화, 원자적 파일 쓰기(임시 파일에 쓴 뒤 교체), 그리고 데이터 포맷이 바뀔 때의 스키마 마이그레이션까지. 저장 파일이 손상되었거나 그 버전을 더는 마이그레이션할 수 없으면, 예외 대신 명시적인 결과 코드를 받습니다 — 복구 경험의 주도권은 여러분에게 있습니다.
빠른 예제
using CommonGameSystem.Core;
using UnityEngine;
using Logger = CommonGameSystem.Core.Logger; // Unity has its own Logger type
[DefaultExecutionOrder(100)]
public class SaveLoadDemo : MonoBehaviour
{
private ISaveService _save;
void Awake()
{
_save = ServiceLocator.Resolve<ISaveService>();
}
public void SaveGame(string slot, PlayerData data)
{
var result = _save.Save(slot, data);
if (result.Status != SaveStatus.Ok)
Logger.Error(Logger.Categories.Save, $"Save failed: {result.Message}");
}
public void LoadGame(string slot)
{
var result = _save.Load<PlayerData>(slot);
if (result.Status == SaveStatus.Ok)
ApplyPlayerData(result.Value);
else
Logger.Warning(Logger.Categories.Save, $"Load failed: {result.Message}");
}
private void ApplyPlayerData(PlayerData data)
{
// Apply the loaded values to your game state here.
}
}
[System.Serializable]
public class PlayerData
{
public int level;
public float health;
}
저장 파일은 기본적으로 {Application.persistentDataPath}/saves/{slot}.json에 놓입니다.
전체 API
쓰기
-
SaveResult Save<T>(string slot, T data) where T : class—data를 직렬화해 이름 있는 슬롯에 원자적으로 씁니다. 성공 시SaveStatus.Ok를, 디스크 오류(디스크 가득 참, 권한) 시IOFailure를 반환합니다 — 그 경우 기존 파일은 전혀 건드리지 않습니다. 잘못된 슬롯 이름에는ArgumentNullException/ArgumentException을 던집니다 — 그것은 디스크 상황이 아니라 호출 코드의 버그입니다. -
SaveResult Delete(string slot)— 슬롯 파일을 삭제합니다. 성공 시Ok, 슬롯(또는 saves 폴더 자체)이 없으면NotFound— 두 번 호출해도 안전하고 예외 없음 — 파일 시스템이 삭제를 거부하면IOFailure를 반환합니다. 동반되는.bak백업 파일은 의도적으로 남겨 둡니다; 그 수명은 해당 슬롯에 대한 다음Save가 관리합니다. -
void RegisterMigration(int fromVersion, ISaveMigration migration)— 버전v에서v + 1로 가는 스키마 마이그레이션 한 단계를 순수 JSON 텍스트 변환으로 등록합니다.fromVersion은 1 이상이어야 하고 마이그레이션은 null이면 안 됩니다. 마이그레이션은 시작 시, 첫Load전에 한 번 등록하십시오. 두 마이그레이션이 같은fromVersion을 공유하면 마지막에 등록된 쪽이 이깁니다.
읽기
-
SaveLoadResult<T> Load<T>(string slot) where T : class— 슬롯을 읽어 역직렬화합니다. 성공 시 값과 함께Ok를 반환합니다. 슬롯이 없으면NotFound, 파일을 파싱할 수 없으면(또는 마이그레이션 단계가 예외를 던졌으면)Corrupt, 파일을 현재 버전으로 끌어올릴 수 없으면VersionUnsupported를 반환합니다. 디스크나 데이터 상황으로는 결코 예외를 던지지 않습니다 — 잘못된 슬롯 이름에만 던집니다. -
bool Exists(string slot)— 슬롯 파일이 디스크에 존재하면true를 반환합니다. 파일이 읽을 수 있는지, 형식이 올바른지는 확인하지 않습니다 — 그것은Load로 확인하십시오. -
IReadOnlyList<string> ListSlots()— 모든 슬롯 이름(확장자 없는 파일 이름)을 알파벳순으로 나열합니다. 저장이 아직 없으면 빈 리스트를 반환하며, 부작용으로 saves 폴더를 만드는 일은 결코 없습니다.
프로퍼티
int CurrentSchemaVersion { get; }— 모든 새 저장에 찍히는 스키마 버전. 이미 이 버전인 파일은 마이그레이션 없이 로드됩니다. 기본값 1; 데이터 포맷이 바뀌면SaveServiceOptions를 통해 올리십시오.
결과 타입
두 결과 타입 모두 public 읽기 전용 필드를 가진 가벼운 struct입니다:
SaveResult—Status(SaveStatus)와Message(진단 문자열,Ok이면null; 플레이어용이 아니라 로그용).SaveLoadResult<T>—Status,Value(Status == Ok일 때만 null 아님), 그리고Message.
SaveStatus 값:
| 상태 | 의미 |
|---|---|
Ok | 작업이 성공적으로 완료되었습니다. |
NotFound | 슬롯 파일이 존재하지 않습니다. Load와 Delete가 반환합니다. |
Corrupt | 파일은 있지만 파싱하거나 역직렬화할 수 없었습니다 — 잘못된 형식의 JSON, 누락된 필드, 타입 불일치, 또는 예외를 던진 마이그레이션 단계. 파일과 그 .bak은 바이트 하나 바뀌지 않고 남으므로 수작업으로 여전히 복구할 수 있습니다. |
VersionUnsupported | 저장된 버전이 CurrentSchemaVersion보다 최신이거나(저장은 결코 다운그레이드하지 않습니다), 마이그레이션 체인에 공백이 있어 파일을 끌어올릴 수 없습니다. 파일은 그대로 보존됩니다. |
IOFailure | 쓰기 또는 삭제 중 파일 시스템 오류가 발생했습니다. 대상 파일과 그 .bak은 그대로이며, 기저 예외는 던져지는 대신 로그에 남습니다. |
스키마 마이그레이션
저장 데이터 포맷을 바꿀 때는 CurrentSchemaVersion을 올리고 버전 단계마다 ISaveMigration을 하나씩 등록하십시오. 각 단계는 원시 페이로드 JSON 텍스트를 버전 v에서 v + 1로 변환합니다 — 파일 접근도, 부작용도 없이 그저 string이 들어가고 string이 나옵니다. Load 시 서비스는 데이터가 현재 버전에 도달할 때까지 단계를 순서대로 적용한 뒤 역직렬화합니다.
using CommonGameSystem.Core;
// v1 stored "oldField"; v2 renamed it to "newField".
public sealed class V1ToV2Migration : ISaveMigration
{
public string Migrate(string payloadJson)
=> payloadJson.Replace("\"oldField\"", "\"newField\"");
}
// Once at startup, before the first Load:
_save.RegisterMigration(1, new V1ToV2Migration());
마이그레이션 단계가 예외를 던지면 Load는 Corrupt를 반환하고 디스크의 파일은 그대로 남습니다. 하향 마이그레이션은 지원하지 않습니다: 더 새로운 버전의 게임이 쓴 파일은 VersionUnsupported를 반환합니다.
커스텀 위치, 확장자, 시리얼라이저
기본 서비스는 부트스트랩이 표준 옵션으로 등록합니다. 폴더 이름, 파일 확장자, 백업 동작, 스키마 버전을 바꾸려면 인스턴스를 직접 생성해 등록을 교체하십시오:
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class CustomSaveInstaller : MonoBehaviour
{
void Awake()
{
var options = new SaveServiceOptions(
saveDirectoryName: "mygame_saves", // folder inside persistentDataPath (default "saves")
fileExtension: ".sav", // default ".json"
backupExtension: ".sav.bak", // default ".bak"; must differ from fileExtension
currentSchemaVersion: 2, // default 1; must be >= 1
createBackup: true); // default true — keep a backup of the prior save
ServiceLocator.Replace<ISaveService>(
new SaveService(new JsonUtilitySaveSerializer(), options));
}
}
생성자는 모든 인자를 검증하며 잘못된 값(경로 구분자가 든 디렉터리 이름, 점으로 시작하지 않는 확장자, 파일 확장자와 백업 확장자가 같은 경우, 1 미만의 버전)에는 ArgumentException을 던집니다.
Newtonsoft JSON, System.Text.Json, 또는 암호화를 쓰려면 ISaveSerializer를 구현해 JsonUtilitySaveSerializer 자리에 SaveService 생성자로 넘기십시오.
끄는 방법
ServiceLocator.Replace<ISaveService>(new NullSaveService());
이 한 줄이 호출자 코드를 전혀 건드리지 않고 영속화를 끕니다. Save는 (아무것도 하지 않고) Ok를, Load는 NotFound를, Exists는 false를, ListSlots()는 빈 리스트를 반환합니다. 디스크 접근 0, 할당 0. 개발 중이거나 헤드리스 서버 빌드에 유용합니다. 설정 서비스가 기본적으로 저장 서비스를 통해 설정을 라우팅한다는 점에 유의하십시오 — 그 라우팅은 시작 시 한 번 결정되므로, 세션 초반에 기록된 설정은 이후의 NullSaveService 교체에 영향받지 않습니다.
동작 및 엣지 케이스
-
메인 스레드 전용. 모든 메서드는 Unity 메인 스레드에서 실행되는지 확인합니다(검사는 Release 빌드에서 제거됩니다). 다른 스레드에서 호출하면
InvalidOperationException이 발생합니다. -
잘못된 슬롯 이름은 즉시 예외를 던집니다. 슬롯은 순수한 파일 이름 스템이어야 합니다:
null금지, 빈 문자열이나 공백 금지, 경로 구분자 금지,..금지,:금지, 그리고 파일 시스템이 파일 이름에 금지하는 문자도 금지입니다. 위반은 디스크 접근 전에ArgumentNullException이나ArgumentException을 던지며 — 결과 코드로 감싸지지 않습니다. 실제 디스크 상황(파일 없음, 손상, 버전 공백)은 대신 결과 코드로 돌아옵니다. -
원자적 쓰기. 서비스는 먼저
.tmp파일에 쓴 뒤File.Replace로 교체합니다. 교체가 중단되어도(예: 정전) 원래 슬롯은 온전하게 남습니다. 다음Save가 남은.tmp파일을 그냥 덮어씁니다. 이전 저장의.bak백업도 자동으로 생깁니다; 디스크 부담을 줄이고 싶다면SaveServiceOptions.CreateBackup으로 끄십시오(스키마 버전을 올리는 중에는 권장하지 않습니다 — 실패한 마이그레이션 쓰기를 복구할.bak이 없게 됩니다). -
실패는 결코 데이터를 파괴하지 않습니다.
Ok가 아닌 모든 결과 —Corrupt,VersionUnsupported,IOFailure— 는 슬롯 파일과 그.bak을 바이트 하나 바뀌지 않은 채 남깁니다. 서비스는 읽지 못한 파일을 결코 "정리"하지 않습니다. -
IL2CPP / link.xml. 서비스 자체의 타입은 이미 IL2CPP 코드 스트리핑에서 보호됩니다. 여러분의 저장 데이터 클래스(위의
PlayerData같은)는 **여러분 프로젝트의link.xml**에서 보존해야 하며, 그러지 않으면 스트리핑된 빌드에서 역직렬화에 실패합니다. -
직렬화 한계. 기본 시리얼라이저는 Unity의
JsonUtility를 사용합니다: public 필드와[SerializeField]필드만 지원하며, 자동 프로퍼티 금지,UnityEngine.Object참조 금지, 참조 순환 금지입니다. 필드가 소리 없이 저장에 실패한다면 대개 자동 프로퍼티가 범인입니다 — public 필드로 바꾸십시오.
관련 페이지
- 부트스트랩 — 서비스가 자동으로 시작되는 방식
- 설정 — 이 서비스 위에 구축된 설정 영속화
- 로거 —
Logger.Categories.Save아래의 저장 진단 - 서비스 로케이터 — 서비스를 직접 만든 구현으로 교체하기
- 매뉴얼: 문제 해결 —
Corrupt와VersionUnsupported결과에서 복구하기