파일 저장 구조
목차
1. 시스템 요구 사항
건축 시스템에서 저장 기능은 단순히 현재 상태를 기록한다는 수준으로 끝나지 않는다.
현재 프로젝트의 배치 데이터는 런타임에서 PlacementManager, GridData, PlacementGridData를 중심으로 관리되며, 내부적으로는 Grid 점유 상태와 구조물 참조를 빠르게 처리하기 위해 참조 기반 구조와 Dictionary 중심 구조를 사용한다.
하지만 이 구조는 게임 플레이에서는 효율적이지만, 그대로 저장하기에는 적합하지 않다.
저장 단계에서는 현재 상태를 외부 저장소에 남길 수 있는 값 구조로 재구성해야 하며, 이후 다시 읽어 왔을 때 동일한 상태로 복원할 수 있어야 한다.
따라서 저장 시스템은 런타임 상태를 직접 저장하는 것이 아니라, 먼저 GridSaveData 형태의 저장용 데이터로 정리한 뒤 이를 직렬화하여 보관하는 구조를 가져야 한다.
또한 이 프로젝트는 단일 세이브가 아니라 파일명 기반의 저장 슬롯 개념을 사용하고 있다.
따라서 저장 시스템은 데이터를 저장하는 기능과 함께, 현재 사용 중인 저장 파일 이름을 기억하고, 저장된 파일 목록을 따로 관리해야 한다.
PlayerPrefs는 키 하나의 존재 여부는 확인할 수 있지만, 저장된 전체 키 목록을 직접 제공하지 않기 때문에, 저장된 파일 이름들을 별도의 리스트 형태로 관리하는 보조 구조가 필요하다.
결국 저장 시스템은 단순 Save 함수 하나로 끝나지 않는다.
현재 게임 상태를 저장용 데이터로 변환하고, 이를 문자열로 직렬화하여 슬롯별로 저장하며, 저장 파일 목록까지 함께 유지하는 시스템이어야 한다.
여기서 중요한 부분은 어떤 저장 방식과 직렬화 방법을 선택하느냐이다.
현재 구조에서는 JsonUtility를 통해 GridSaveData를 JSON 문자열로 변환하고, 그 결과를 PlayerPrefs에 저장한다.
이는 대규모 파일 저장 시스템에 비해 단순하고 구현 부담이 적으며, 비교적 작은 규모의 건축 상태 데이터를 다루는 현재 프로젝트에 적합하다.
따라서 이 시스템은 범용 저장 프레임워크가 아니라, 현재 건축 시스템의 상태를 빠르고 안전하게 저장하는 경량 저장 구조에 가깝다.
2. 흐름도
현재 건축 상태 유지
↓
PlacementManager.GetDataToSave()
↓
GridSaveData 생성
↓
JsonUtility.ToJson(data)
↓
JSON 문자열 생성
↓
PlayerPrefs.SetString(fileName, json)
↓
PlayerPrefs.Save()
↓
AddFileToList(fileName)
↓
SaveFileListToPrefs(savedFileList)
↓
저장 완료 + 현재 파일명 갱신
이 흐름에서 가장 중요한 점은 SaveManager가 직접 Grid를 순회하거나 구조물을 분석하지 않는다는 것이다.
저장 데이터 생성은 PlacementManager.GetDataToSave()를 통해 이루어지고, SaveManager는 그 결과를 받아 저장 가능한 문자열 형태로 바꾸고, 저장 슬롯 목록을 갱신하는 역할을 담당한다.
즉, 이 시스템은 저장 로직 전체를 한 곳에 몰아넣지 않고, 데이터 생성 책임과 저장 실행 책임을 분리한 구조를 가진다.
또한 흐름의 마지막에 AddFileToList와 SaveFileListToPrefs가 이어진다는 점도 중요하다.
저장은 JSON을 키 하나에 기록하는 것으로 끝나지 않는다.
이후 UI에서 어떤 파일들이 저장되어 있는지를 보여 주기 위해서는 저장 슬롯 목록이 반드시 필요하다.
PlayerPrefs는 파일 시스템처럼 목록을 직접 제공하지 않기 때문에, 이 시스템은 저장 직후 파일명을 리스트에 반영하고 그 리스트 자체도 다시 저장한다.
즉, 저장 흐름은 '데이터 저장 + 슬롯 메타데이터 갱신' 까지 포함하는 전체 과정이다.
3. 구현
3.1. 저장 시스템의 진입점
public class SaveManager : MonoBehaviour
{
private string currentFileName;
private const string SaveFileListKey = "SaveFileList";
[SerializeField]
private PlacementManager _placementManager;
[SerializeField]
private ItemDataBaseSO _database;
...
}먼저, currentFileName은 현재 활성 저장 파일 이름을 보관하는 필드다.
이 값은 단순 문자열이 아니라, 현재 어떤 슬롯을 기준으로 저장/덮어쓰기/불러오기를 하고 있는가를 나타내는 상태값이다.
저장 UI에서는 이 값을 기준으로 새 저장인지 덮어쓰기인지 판단할 수 있고, 저장 후에는 이 값을 갱신함으로써 다음 저장 동작이 같은 슬롯을 기준으로 이루어지도록 만든다.
SaveFileListKey는 저장 파일 목록을 PlayerPrefs 안에 보관하기 위한 키다.
PlayerPrefs는 키-값 저장 구조이기 때문에, 저장된 모든 키를 시스템 차원에서 편하게 열거하는 기능이 없다.
그래서 별도의 메타데이터 키를 하나 두고, 저장된 파일 이름 목록을 문자열로 직접 관리한다.
상수로 선언한 이유는 문자열 키를 여러 곳에서 반복 작성할 때 생길 수 있는 오타와 유지보수 문제를 줄이기 위해서다.
_placementManager는 현재 배치 상태를 저장용 데이터로 변환하거나, 저장 데이터를 다시 런타임 배치로 복원하는 데 필요한 핵심 참조다.
[SerializeField]를 쓴 이유는 Unity Inspector에서 직접 연결하기 위해서다.
이 방식의 장점은 코드에서 런타임 탐색을 하지 않아도 된다는 점이고, 단점은 인스펙터 연결이 누락되면 저장 시스템이 바로 동작하지 않는다는 점이다.
하지만 저장 시스템은 반드시 배치 시스템과 연결되어야 하므로, 명시적 참조 연결이 더 적절하다.
_database 역시 구조물 ID를 실제 ItemData로 복원하는 데 필요하므로 같은 방식으로 연결된다.
3.2. 저장 데이터 생성과 JSON 변환
public void SaveData(string fileName)
{
GridSaveData data = _placementManager.GetDataToSave();
string dataToSave = JsonUtility.ToJson(data);
PlayerPrefs.SetString(fileName, dataToSave);
PlayerPrefs.Save();
AddFileToList(fileName);
currentFileName = fileName;
Debug.Log("Data saved with file name : " + fileName);
}SaveData는 저장 시스템의 핵심 엔트리 포인트로 동작한다.
가장 먼저 _placementManager.GetDataToSave()를 호출하는데, 여기서 중요한 점은 SaveManager가 직접 Grid를 순회하지 않는다는 것이다.
그 책임은 PlacementManager와 하위 데이터 구조에 맡기고, SaveManager는 이미 정리된 데이터를 받아 저장만 수행한다.
이렇게 저장 관리자와 저장 데이터 생성기를 분리해 두면, 저장 시스템이 배치 시스템 내부 구조를 과도하게 알 필요가 없고, 변경 영향도 줄어든다.
다음 줄의 JsonUtility.ToJson(data)는 Unity 내장 JSON 직렬화 함수다.
JsonUtility는 [Serializable] 기반의 구조체나 클래스 데이터를 JSON 문자열로 변환할 수 있게 해 준다.
여기서 JsonUtility를 선택한 이유는 현재 저장 데이터 구조가 이미 GridSaveData, PlacementGridSaveData, CellObjectSaveData처럼 Unity 직렬화 규칙에 맞춰 설계되어 있기 때문이다.
장점은 Unity 내장 기능이라 별도 라이브러리 없이 바로 사용할 수 있고, 코드가 단순하다는 점이다.
단점은 일반 .NET JSON 라이브러리보다 기능이 제한적이고 Dictionary 같은 구조를 직접 처리하기 어렵다는 점이다.
하지만 현재 프로젝트는 이미 Dictionary를 저장용 List 구조로 변환한 뒤 저장하는 방식이므로, JsonUtility와 매우 잘 맞는다.
이 과정은 Unity 직렬화 규칙에 맞춰 구성된 데이터를 실제 저장 가능한 문자열 형태로 변환하는 단계다.
그 다음 PlayerPrefs.SetString(fileName, dataToSave)가 실행된다.
PlayerPrefs는 Unity가 제공하는 간단한 키-값 저장소로, 문자열, 정수, 실수 같은 소규모 데이터를 쉽게 저장할 수 있다.
여기서는 fileName을 키로 사용하고 JSON 문자열을 값으로 저장한다.
이 구조의 장점은 구현이 매우 단순하고, 별도의 파일 경로나 스트림 처리가 필요 없다는 점이다.
또한 슬롯 이름만 다르면 여러 저장 데이터를 동시에 관리할 수 있다.
단점은 대용량 세이브 파일, 버전 관리, 복잡한 바이너리 저장에는 적합하지 않다는 점이다.
하지만 현재 프로젝트처럼 건축 상태를 문자열 기반으로 저장하고, 저장 슬롯 개념을 간단히 구현하려는 경우에는 충분히 실용적이다.
PlayerPrefs.Save()는 지금까지 메모리에 반영된 값을 실제 저장소에 즉시 기록하도록 요청하는 함수다.
그 뒤 AddFileToList(fileName)를 호출해 저장된 파일 목록을 갱신하고, currentFileName에 현재 파일명을 기록한다.
즉, 이 함수는 데이터 저장과 저장 슬롯 관리 상태를 동시에 갱신하는 함수다.
마지막 Debug.Log는 저장 성공 시점과 저장 파일 이름을 확인하기 위한 디버그 출력이다.
개발 과정에서는 저장 동작이 제대로 호출되었는지 추적하는 데 도움을 준다.
3.3. 저장 파일 목록 관리
private void AddFileToList(string fileName)
{
List<string> savedFileList = GetSavedFileList();
if (!savedFileList.Contains(fileName))
{
savedFileList.Add(fileName);
SaveFileListToPrefs(savedFileList);
}
}이 함수는 저장 슬롯 이름 목록을 관리하는 함수다.
여기서 핵심은 PlayerPrefs가 파일 시스템처럼 저장된 모든 키 목록을 자동으로 제공하지 않는다는 점이다.
따라서 저장 데이터를 넣는 것과 별개로, 어떤 파일들이 저장되어 있는지를 추적하는 별도의 목록이 필요하다.
GetSavedFileList()는 현재 저장된 파일 이름 목록을 읽어오고, Contains로 같은 이름이 이미 존재하는지 검사한다.
List<T>.Contains는 리스트를 순회하면서 동일한 문자열이 있는지 확인하는 함수다.
이건 O(n) 선형 탐색이지만, 저장 파일 목록 수가 많지 않은 현재 구조에서는 충분히 단순하고 실용적이다.
중복이 없으면 savedFileList.Add(fileName)으로 새 파일명을 추가하고, SaveFileListToPrefs(savedFileList)를 호출해 다시 저장한다.
현재 시스템은 저장 데이터 본문과 저장 슬롯 목록을 별도로 관리하기 때문에, 저장 기능이 완전히 동작하려면 이 목록 갱신이 반드시 필요하다.
저장은 되었지만 목록에 반영되지 않으면 UI에서 파일이 보이지 않을 수 있기 때문이다.
3.4. 파일 목록 직렬화와 저장
public void SaveFileListToPrefs(List<string> fileList)
{
string saveListString = string.Join(",", fileList);
PlayerPrefs.SetString(SaveFileListKey, saveListString);
PlayerPrefs.Save();
}이 함수는 파일 이름 리스트를 PlayerPrefs에 저장하는 역할을 한다.
PlayerPrefs는 List를 직접 저장할 수 없기 때문에, 리스트를 문자열로 바꾸는 과정이 필요하다.
여기서 string.Join(",", fileList)를 사용한다.
string.Join은 C#에서 컬렉션 요소들을 특정 구분자로 이어 붙여 하나의 문자열로 만드는 함수다.
이 코드는 예를 들어 ["Save1","Save2","Save3"]를 'Save1,Save2,Save3' 처럼 변환한다.
이는 리스트를 저장 가능한 문자열 형식으로 평탄화하는 과정이다.
그 다음 PlayerPrefs.SetString(SaveFileListKey, saveListString)로 파일 목록 문자열을 저장한다.
이때 저장 데이터 본문은 각 파일명 키로 따로 저장되고, 파일 목록은 SaveFileListKey라는 별도 키에 저장된다.
이 구조는 약간 수동적이지만, PlayerPrefs의 제약 안에서 저장 슬롯 목록을 유지하는 데 적합하다.
장점은 구조가 단순하고 구현이 명확하다는 점이다.
단점은 파일명에 쉼표가 들어가면 파싱 문제가 발생할 수 있다는 점인데, 현재 프로젝트에서는 일반적인 파일명만 입력한다고 가정하면 충분히 감당 가능한 제약이다.
이렇게 변환된 문자열은 PlayerPrefs에 저장되며, 이후 UI에서 저장 슬롯 목록을 구성할 때 사용된다.
4. 개발 의도
이 저장 시스템의 핵심 의도는 저장 로직을 데이터 생성과 저장 실행으로 분리하는 것이다.
현재 배치 상태를 GridSaveData로 정리하는 일은 PlacementManager가 담당하고, SaveManager는 그 결과를 받아 JSON 직렬화와 저장소 기록을 담당한다.
이렇게 나누면 저장 시스템이 배치 시스템 내부 구조를 직접 다루지 않아도 되고, 각 계층의 책임이 명확해진다.
저장 시스템은 무엇을 저장할지보다 어떻게 저장할지에 집중할 수 있게 된다.
또한 JsonUtility와 PlayerPrefs를 선택한 것도 현재 프로젝트 요구사항에 맞춘 설계다.
복잡한 파일 기반 세이브 시스템 대신, 직렬화 가능한 값 구조를 JSON 문자열로 변환해 간단한 키-값 저장소에 보관하는 방식은 구현 복잡도를 줄이고, 저장 기능을 빠르게 안정화하는 데 유리하다.
동시에 파일 목록을 별도 문자열로 관리하는 구조를 추가함으로써, PlayerPrefs의 단순한 저장소 구조를 저장 슬롯 시스템처럼 사용할 수 있게 만들었다.
