[TIL] 13일 차 - Json을 활용한 Save 및 Load

ChangBeom·2025년 2월 12일

TIL

목록 보기
14/53
post-thumbnail

오늘은 팀프로젝트 과정에서 Save와 Load를 구현했던 부분을 기록해보려고 한다.

직렬화역직렬화에 대해 알아야 이해할 수 있는 내용들이니 앞서 작성했던 글을 참고하자!

링크 : [TIL] 7일차 - C# 직렬화를 활용한 게임 저장 및 불러오기

[Json 라이브러리 추가]

내가 작업하고 있는 환경인 Visual Studio 2022 에선 Json 라이브러리를 직접 추가해줘야 사용할 수 있다.

도구 -> NuGet 패키지 관리자 -> 솔루션용 NuGet 패키지 관리

위의 경로로 들어가면 NuGet 패키지 관리 창으로 넘어 가는데, 왼쪽 위에 있는 찾아보기 탭을 클릭하면 다음과 같은 화면이 보인다.

그 이후 검색창에 json이라고 입력하고, Newtonsoft.Json을 찾아 클릭한다. 그러면 오른쪽에 현재 프로젝트가 나오는데 Newtonsoft.Json을 설치할 프로젝트를 선택하고 아래의 설치 버튼을 눌러서 설치하면 된다. (이미지는 현재 설치되어 있는 모습)

이제 라이브러리가 추가 되었으니

using Newtonsoft.Json;

같이 선언해서 사용할 수 있다!


[Json을 이용해서 데이터를 저장하고 불러오기]

시작하기 앞서, 이전에 구현했던 Xml을 활용하면 쉽게 구현할 수 있는 거 아닌가? 라고 생각할 수 있는데, 이번에 만든 Player 클래스에는 인터페이스를 저장하는 List가 존재하는데 인터페이스는 Xml파일로 직렬화 할 수 없다. 이 부분을 기억하고 설명을 들으면 좋을 것 같다.

먼저 기본적인 Json을 활용한 저장 방법이다.

프로젝트에서 사용하고 있는 inventory 클래스를 예시로 들어보겠다.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;

namespace TextRPG
{
    public class Inventory
    {
        public Dictionary<string, int> itemQuantities;

        public Inventory()
        {
            itemQuantities = new Dictionary<string, int>();
      
            AddItem("HealthPotion", 3);
            AddItem("ManaPotion", 3);
            AddItem("SlimeArmor", 1);
        }

        public void AddItem(string itemName, int quantity = 1)
        {
			//	...생략
        }

        public bool UseItem(string itemName)
        {
            //	...생략
        }
       
        /*
       
       
        			...생략
           
           
        */

        //  Inventory 클래스의 데이터를 저장하는 함수
        public void Save(string filePath)
        {
            string json = JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented);
            File.WriteAllText(filePath, json);
        }

        //  Inventory 클래스의 데이터를 불러오는 함수
        public static Inventory Load(string filePath)
        {
            string json = File.ReadAllText(filePath);
            return JsonConvert.DeserializeObject<Inventory>(json);
        }
    }
}

위 코드를 보면 멤버변수인 딕셔너리를 Save() 함수를 통해 매개변수로 받은 filePath 라는 주소에 저장된다.

public void DataSave (Inventory inventory)
{
    inventory.Save("Inventory.json");
}

이런식으로 함수를 만들어서 호출하면 아래와 같이 잘 저장되는 것을 볼 수 있다.

불러오기도 마찬가지로 아래와 같이 만들어서 호출하면 잘 적용된다.

public void DataLoad(ref Inventory inventory)
{
    inventory = Inventory.Load("inventory.json");
}


이제 마지막으로 Player 클래스를 저장하고 불러와보자. 사실 내가 이 글을 쓰고 있는 목적이 이 부분이라고 봐도 무방하다.


[JsonConverter를 이용한 인터페이스 직렬화 및 역직렬화]

앞에서 살짝 언급했었는데 Xml을 활용해서 interface를 직렬화하면 역직렬화할 수 없다. 그래서 Json을 활용해서 직렬화와 역직렬화를 해야하는데, JsonConverter를 활용해서 사용자 지정 변환기를 작성해야한다. 왜냐하면 인터페이스는 직접적으로 인스턴스를 생성할 수 없는 추상적인 타입이기 때문이다.

그래서 Player 클래스를 직렬화하는데 가장 큰 문제는 ISkill 인터페이스를 상속받아 만든 스킬들이라고 할 수 있다. 아래는 Skill 클래스와 사용자 지정 변환기를 작성한 코드이다. 사용자 지정 변환기란 JsonConverter를 상속받아 Json 파일에 데이터를 쓰거나 Json 파일의 데이터를 읽어올 때 원하는대로 형식을 지정할 수 있다고 생각하면 된다. 이를 활용해서 ISkill interface가 무엇인지 명시해줄 예정이다. 전체 코드를 확인한 후 사용자 지정 변환기 부분을 리뷰해보겠다.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace TextRPG
{
    public class SkillConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return typeof(ISkill).IsAssignableFrom(objectType);
        }

        public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
        {
            if (value is not ISkill skill) return;

            writer.WriteStartObject();
            writer.WritePropertyName("$type");
            writer.WriteValue(skill.GetType().FullName);

            writer.WritePropertyName("name");
            writer.WriteValue(skill.name);

            writer.WritePropertyName("mpValue");
            writer.WriteValue(skill.mpValue);

            writer.WritePropertyName("damageValue");
            writer.WriteValue(skill.damageValue);

            writer.WritePropertyName("description");
            writer.WriteValue(skill.description);

            writer.WritePropertyName("isRandom");
            writer.WriteValue(skill.isRandom);

            writer.WriteEndObject();
        }

        public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
        {
            reader.Read();
            string? type = null;
            ISkill? skill = null;

            while (reader.TokenType != JsonToken.EndObject)
            {
                if (reader.TokenType == JsonToken.PropertyName)
                {
                    string? propertyName = reader.Value as string;
                    reader.Read();

                    if (propertyName == "$type")
                    {
                        type = reader.Value as string;
                    }
                    else if (propertyName == "name" && skill != null)
                    {
                        skill.name = reader.Value as string;
                    }
                    else if (propertyName == "mpValue" && skill != null)
                    {
                        skill.mpValue = reader.Value is int ? (int)reader.Value : Convert.ToInt32(reader.Value);
                    }
                    else if (propertyName == "damageValue" && skill != null)
                    {
                        skill.damageValue = reader.Value is float ? (float)reader.Value : Convert.ToSingle(reader.Value);
                    }
                    else if (propertyName == "description" && skill != null)
                    {
                        skill.description = reader.Value as string;
                    }
                    else if (propertyName == "isRandom" && skill != null)
                    {
                        skill.isRandom = (bool)reader.Value;
                    }
                }
                reader.Read();
            }

            if (type != null)
            {
                // type에 따라 skill 객체 생성
                if (type == typeof(AlphaStrike).FullName)
                {
                    skill = new AlphaStrike();
                }
                else if (type == typeof(DoubleStrike).FullName)
                {
                    skill = new DoubleStrike();
                }
            }

            return skill;
        }
    }

    //  스킬 기본 구성 인터페이스
    public interface ISkill
    {
        public string type { get; set; }
        public string name { get; set; }
        public int mpValue { get; set; }
        public float damageValue { get; set; }
        public string description { get; set; }
        public bool isRandom { get; set; }

        //  스킬의 정보를 보여주는 함수
        public void SkillInfo();

        //  스킬 사용 함수
        public void Use(Player player, Battle battle, int input);
    }

    //  알파 스트라이크 스킬 클래스
    public class AlphaStrike : ISkill
    {
        public string type { get; set; }
        public string name { get; set; }
        public int mpValue { get; set; }
        public float damageValue { get; set; }
        public string description { get; set; }
        public bool isRandom { get; set; }

        public AlphaStrike()
        {
            type = "AlphaStrike";
            name = "알파 스트라이크";
            mpValue = 10;
            damageValue = 2;
            description = $"공격력 * {damageValue} 로 하나의 적을 공격합니다.";
            isRandom = false;
        }

        public void SkillInfo()
        {
              //	...생략
        }

        public void Use(Player player, Battle battle, int input)
        {
             //		...생략
        }
    }

    //  더블 스크라이크 스킬 클래스
    public class DoubleStrike : ISkill
    {
        public string type { get; set; }
        public string name { get; set; }
        public int mpValue { get; set; }
        public float damageValue { get; set; }
        public string description { get; set; }
        public bool isRandom { get; set; }

        public DoubleStrike()
        {
            type = "DoubleStrike";
            name = "더블 스트라이크";
            mpValue = 15;
            damageValue = 1.5f;
            description = $"공격력 * {damageValue} 로 2명의 적을 랜덤으로 공격합니다.";
            isRandom = true;
        }

        public void SkillInfo()
        {
            //	...생략
        }

        public void Use(Player player, Battle battle, int input)
        {
			//	...생략
        }
    }
}

이제부터 가장 중요한 부분인 SkillConverter 클래스를 리뷰해보겠다.

  • CanConvert() 함수 : 주어진 objectType이 ISkill 인터페이스를 구현하는지 확인하는 함수이다. 간단하게 설명하면 직렬화 또는 역직렬화가 가능한 타입인지 판단하는데 사용된다.
  • WriteJson() 함수 : 객체를 Json으로 직렬화하는 기능을 수행한다. writer는 Json을 작성하는 데 사용되는 객체이며, value는 직렬화할 객체이다.
  • ReadJson() 함수 : Json을 읽어 ISkill 객체로 변환하는 함수이다. reader는 Json을 읽는 데 사용되는 객체이다. 이 때 중요한 부분이 $type속성을 읽으면, 해당 값을 type 변수에 저장하는 것이다. 이를 통해 인터페이스로 구현된 클래스임을 판단하고 ISKill 객체를 생성하는 것이다.

이렇게 만든 SkillConverter를 활용해서 Player 클래스에서

//  Player 클래스의 데이터를 저장하는 함수
public void Save(string filePath)
{
    var settings = new JsonSerializerSettings();
    settings.Converters.Add(new SkillConverter());
    string json = JsonConvert.SerializeObject(this, settings);
    File.WriteAllText(filePath, json);
}

//  Player 클래스의 데이터를 불러오는 함수
public static Player Load(string filePath)
{
    string json = File.ReadAllText(filePath);
    var settings = new JsonSerializerSettings();
    settings.Converters.Add(new SkillConverter());
    return JsonConvert.DeserializeObject<Player>(json, settings);
}

이런식으로 Save(), Load() 함수를 구현해주고

public void DataSave(Player player, Inventory inventory)
{
	player.Save("player.json");
	inventory.Save("Inventory.json");
}

public void DataLoad(ref Player player, ref Inventory inventory)
{
    player = Player.Load("player.json");
    inventory = Inventory.Load("inventory.json");
}

이렇게 함수를 만들어서 호출해주면 Player와 Inventory 모두 저장되는 모습을 볼 수 있다!

0개의 댓글