Priv's Blog
세션 간의 데이터 지속성 (JsonUtility & NewtownSoft Json) 본문
1. JsonUtility
Unity는 기본적으로 Json을 이용해 데이터를 외부에 저장할 수 있는 기능을 지원합니다.
Unity Learn에서는 세션 간의 데이터 지속성으로 설명하고 있으며, 직렬화/역직렬화 개념을 기반으로 합니다.
다만 Unity에서 제공하는 JsonUtility는 가장 기본적인 기능만 제공하고 있어 직렬화 가능한 범위가 매우 제한적입니다.
Unity Learn에서는 아래와 같이 서술하고 있습니다.
- JsonUtility의 한계
Unity 엔진의 JsonUtility는 성능과 단순함에 초점을 맞추어 설계되었기 때문에 몇 가지 한계점을 가지고 있습니다.
JsonUtility는 원시 자료형(Primitive Types), 배열(Array), 리스트(List), 딕셔너리(Dictionary)와 함께 사용할 수 없습니다.
2. XML
JsonUtility를 이용한 직렬화/역직렬화 기능이 마음에 들지 않는다면, XML을 사용하는 방법도 있습니다.
다만 XML을 사용하는 방식은 가독성이 매우 떨어지며, Json보다 데이터를 넣고 꺼내는, 파싱(Parsing) 과정이 까다롭습니다.
Json의 뛰어난 가독성, 편리하고 직관적인 문법, 간단한 파싱 과정을 모두 포기하기에는 부담이 클 수밖에 없습니다.
3. Newtonsoft JSON
Unity에서 제공하는 JsonUtility, XML도 마음에 들지 않는다면, 타사에서 제공하는 Json 라이브러리를 적용해 사용하면 됩니다.
라이브러리의 종류에 따라 성능과 특징이 모두 다르겠지만, 그중에서 가장 많이 사용하는 라이브러리는 Newtonsoft Json 라이브러리입니다.
이 라이브러리는 JsonUtility에서 지원하지 않던 자료형에 대한 직렬화/역직렬화도 지원합니다.
특히 딕셔너리의 직렬화/역직렬화도 지원하기 때문에 상당히 유용합니다.
사용법은 JsonUtility와 크게 다르지 않지만, 라이브러리가 다르기 때문에 사용되는 키워드에서 차이가 존재합니다.
using System;
using System.IO;
using System.Text;
using UnityEngine;
using Newtonsoft.Json;
using UnityEditor;
public class GameControlSaveLoad : GameControlSingleton<GameControlSaveLoad> {
private string fileName;
private string filePath;
private void Init() {
this.fileName = DateTime.Now.ToString("yyyy-MM-dd");
this.filePath = Application.persistentDataPath;
}
private void Awake() {
Init();
}
// Obj -> Json
public string ObjectToJson(object obj) {
return JsonConvert.SerializeObject(obj);
}
// Json -> Obj
public T JsonToObject<T>(string jsonData) {
return JsonConvert.DeserializeObject<T>(jsonData);
}
// Json -> File
public void CreateJsonFile(string jsonData) {
FileStream fileStream = new FileStream($"{this.filePath}/{this.fileName}.json", FileMode.Create);
byte[] data = Encoding.UTF8.GetBytes(jsonData);
fileStream.Write(data, 0, data.Length);
fileStream.Close();
}
// Json -> Obj
public T LoadJsonFile<T>() {
if (!File.Exists($"{this.filePath}/{this.fileName}.json")) {
throw new FileNotFoundException();
}
FileStream fileStream = new FileStream($"{this.filePath}/{this.fileName}.json", FileMode.Open);
byte[] data = new byte[fileStream.Length];
fileStream.Read(data, 0, data.Length);
fileStream.Close();
var jsonData = Encoding.UTF8.GetString(data);
return JsonConvert.DeserializeObject<T>(jsonData);
}
}
제공되는 메서드들은 JsonConvert 클래스를 통해 접근할 수 있으며, 직렬화/역직렬화를 위해서는 아래와 같이 작성해 사용하면 됩니다.
JsonConvert.SerializeObject(obj);
JsonConvert.DeserializeObject<T>(jsonData);
역직렬화의 경우, Json 데이터를 객체로 전환해야 하므로, 제네릭 형태로 구성되어 있습니다.
4. SerializableDictionary
Newtonsoft의 JSON 라이브러리와 함께 사용하기 좋은 에셋을 하나 뽑으라면, SerializableDictionary를 뽑을 수 있습니다.
일반적으로 Unity 상에서 List는 직렬화가 가능해도, Dictionary는 직렬화가 불가능하다고 알려져 있습니다.
하지만 제네릭의 특성을 사용하면 간접적으로나마 Dictionary도 직렬화가 가능합니다.
<Tkey, Tvalue> 형식을 사용하는 제네릭 클래스를 생성하고 Key와 Value 데이터를 각각의 List로 저장한 뒤, 제네릭 클래스 타입의 객체 형태로 묶어서 직렬화하는 것입니다.
물론, 아이디어를 직접 구현하여 개발에 적용하는 것도 좋겠지만, 아쉽게도(?) 이를 벌써 구현해 둔 에셋이 있습니다.
위 에셋을 사용하여 Unity 에디터의 Inspector 창에서 직접 딕셔너리의 값에 접근할 수 있도록 응용하는 방법도 존재합니다.
5. JsonSerializationException: Self referencing loop detected
다만 Newtonsoft의 JSON도 완벽하지는 않습니다.
Unity 엔진으로 게임을 개발할 때, 대부분의 클래스는 Monobehaviour 클래스를 상속합니다.
Start( ), Update( ) 메서드처럼 우리에게 친숙한 개념들을 사용하기 위해서, 게임 오브젝트에 스크립트 파일을 컴포넌트로 연결하기 위해서는 Monobehaviour를 상속해야 합니다.
이 때문에 Unity 엔진 상에서 스크립트를 새로 생성하면 기본적으로 이 클래스를 상속한 상태로 클래스가 생성됩니다.
하지만 Newtonsoft의 JSON 라이브러리를 이용하여 Monobehaviour를 상속한 클래스를 직렬화/역직렬화하려고 시도하면 아래와 같은 에러가 발생합니다.
JsonSerializationException: Self referencing loop detected for property ~
이는 gameObject가 gameObject를 호출할 수 있는 순환 구조 때문에 발생하는 것이며, 알려져 있는 해결책들도 사실상 임시방편책에 가깝습니다.
이 때문에 Newtonsoft의 JSON 라이브러리로 직렬화/역직렬화하고 싶은 클래스는 Monobehaviour를 상속하지 않는 상태로 만들거나, JSON에 저장하고 싶은 프로퍼티만 별도의 클래스로 묶어서 직렬화해야 합니다.
다만 프로퍼티만 묶어서 직렬화하는 방법은 접근성 문제 때문에 다루기가 까다롭거나 클래스 안에 클래스가 존재하는 방식이므로 가독성을 해칠 수 있어서 가급적이면 JsonUtility와 함께 사용하거나, Monobehaviour 상속을 포기하는 것을 권장합니다.
수고하셨습니다!
'Dev. Study Note > Unity' 카테고리의 다른 글
Unity Event vs Delegate Event (0) | 2024.10.06 |
---|---|
SerializeField 속성과 변수 초기화, Null 참조 에러 (0) | 2024.07.28 |
OOP의 다형성, Overloading과 Overriding 그리고 global:: (0) | 2024.05.13 |
ScriptableObject는 빌드 버전과 에디터 버전에서 다르게 동작한다. (0) | 2024.02.11 |
Mathf.Lerp( )를 사용해 부드럽게 충전되는 로딩 게이지 만들기 (0) | 2023.11.11 |