오늘은 개인 과제의 마지막 별 6개짜리 챌린지인 게임 저장/불러오기 기능을 추가하는데 성공했다.
개발 과정에서 겪였던 에러, 고민, 해결한 방법 등등을 써보려고 한다.
public abstract class Item
{
public int ID { get; protected set; }
public string Name { get; protected set; }
public int Price { get; protected set; }
public string Descript { get; protected set; }
public int Atk { get; protected set; }
public int Def { get; protected set; }
}
오늘은 자리에 앉자마자 어제 나를 괴롭게 했던 abstract class인 Item
클래스를 먼저 역직렬화 하려고 해봤다.
JsonConvert.DeserializeObject<T>()
을 이용해서 Item
클래스로 역직렬화 하면, Item
클래스는 추상 클래스라 인스턴스를 가질 수 없다며 오류가 나는 문제가 있었다.
그래서 그냥 에라 모르겠다~ Item
에서 abstract를 지워봤다. 그런데 이번엔 에러는 안나는데 property에 값이 전부 기본값으로 들어가 있는 거다.
이제와서 생각해보면 너무 당연한 얘기인데, Item
의 property들은 외부에서 변조할 수 없게 setter를 protected
로 선언해놔서 그런거였다. 이 문제도 그냥 setter의 protected
를 떼어버리면 간단하게 해결할 수는 있겠지만, Item
의 옵션이나 가격등을 외부에서 건드릴 수 있게 되는건 원치 않아서, 다른 방법을 찾아보기로 했다.
그래서, Item
대신 JObject
라는 형태로 역직렬화 하기로 했다. JObject
와 그 친구들에 대해 잠깐 정리해보겠다.
JObject
는 string
자료형의 key
와 JToken
자료형의 value
한 쌍으로 이루어져 있는 오브젝트이다. key
값을 이용해 value
에 접근한다.JProperty
는 key
, value
둘 다 필드로 제공해준다.JArray
는 간단하게 말하면, JObject
의 배열이라고 생각하자.JToken
을 상속받는다.item.Name = (string)itemJObject["Name"];
JObject
의 value로 JArray
가 있을 수 있고, JArray
의 value로 JObject
가 있을 수 있다.public static void InitItemDictionary()
{
var jsonStr = AESManager.Decrypt(File.ReadAllText(@"ItemDictionary.json"));
var dict = JsonConvert.DeserializeObject<Dictionary<int, JObject>>(jsonStr);
foreach (var e in dict)
instance.Add(e.Key, Item.JsonParse(e.Value));
}
ItemDictionary
에 모든 아이템의 정보를 불러오는 코드.
public static Item JsonParse(JObject obj)
{
var id = (int)obj["ID"];
var name = (string)obj["Name"];
var price = (int)obj["Price"];
var desc = (string)obj["Descript"];
var atk = (int)obj["Atk"];
var def = (int)obj["Def"];
if (id < 10)
return new Weapon(id, name, price, atk, desc);
else
return new Armor(id, name, price, def, desc);
}
JObject
를 받아서 Weapon
이나 Armor
의 객체를 만들어 반환해주는 Item
클래스의 static 메서드.
아이템 데이터의 저장과 불러오기는 성공했으니, 이제 캐릭터의 데이터만 작업하면 된다.
내가 짠 Character
클래스는 자신의 Inventory
클래스를 필드로 가지고 있고, Invetory
클래스는 자신의 주인인 Character
의 클래스를 필드로 가지고 있다. 따라서 Character
클래스를 그대로 JsonConvert.SerializeObject<T>()
를 이용해 직렬화를 시도하면, 다음과 같은 오류가 발생했다.
Character
클래스를 직렬화 하다가 Invetory
가 나와서 Inventory
를 직렬화 했더니 Character
가 또 나왔다.. 이런 무한 루프가 발견이 됐다고 예외가 발생한 것이다.
이것은 그냥 Character
와 Inventory
에서 필요한 정보만 따로 빼서 처리해주는게 맞는 방향인 것 같았다.
public JArray ToJArray()
{
JArray res = new JArray(items.Select(x => x.ID).ToList());
return res;
}
Inventory
클래스가 가지고 있는 ItemList를 JArray
로 만들어 반환하는 메서드.
public JObject ToJObject()
{
JObject res = new JObject
{
{ "Name", Name },
{ "Job", Job },
{ "Level", Level },
{ "Atk", _atk },
{ "Def", _def },
{ "HP", HP },
{ "Gold", Gold },
{ "Exp", Exp },
{ "equipWeapon", equipWeapon.ID },
{ "equipArmor", equipArmor.ID },
{ "Inventory", inventory.ToJArray() }
};
return res;
}
Character
클래스 중에서 필요한 정보만 JObject
로 만들어 반환하는 메서드.
아이템 관련해선 Item
의 ID만 따로 저장해두고, 초기화단계에서 ItemDictionary
를 이용해 ID로 객체를 만들어내기로 했다.
이렇게, 게임의 데이터를 저장하고 불러오는 기능을 추가하는게 끝이 났다.
json 파일을 뽑고 보니, 그냥 메모장으로 열어서 수치만 바꿔줘도 캐릭터가 무식하게 강해지거나 부자가 될 수 있다. 그래서 구글링으로 AES
암호화 알고리즘을 이용해서 Json txt 파일을 암호화 해봤다.
DqDJyke/yFzLUNwCL/GjOqrZq5URqzKSZ1u9CsVV18mT+YssG5cXCESJz020Bz8t+mWYqZeNxUTyCrFp3ZEP+s7LIx7xc7+PvDmvfCcnyWZgBIpW9VYpnJw4qlfQkA71s8ny7bE/AzI31bpoUfyMxISqIrOa7t5kfq/mP0mvA/dfZpAhKvVA6TMJH2ue0CrA
이게 AES
알고리즘을 이용해 암호화 한 내 캐릭터 정보이다. 원래는 이렇게 생겼다.
{"Name":"JSON LOVER","Job":"전사","Level":3,"Atk":14,"Def":9,"HP":100,"Gold":3500,"Exp":0,"equipWeapon":0,"equipArmor":10,"Inventory":[10,0]}
AES
는 대칭키 암호화 알고리즘으로, 하나의 키로 암호화와 복호화가 가능하다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.IO;
namespace source
{
public class AESManager
{
static string aesKey = "Y2Tc3un1H7RRLMJ0lhTvii0mS4v4q6XjPT5+96PT8O0=";
static string aesIv = "NBCChapter2SpartaDunge==";
public static string Encrypt(string originText)
{
byte[] encrypted;
using (AesCryptoServiceProvider aes = new AesCryptoServiceProvider())
{
aes.KeySize = 256;
aes.BlockSize = 128;
aes.Key = Convert.FromBase64String(aesKey);
aes.IV = Convert.FromBase64String(aesIv);
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
ICryptoTransform enc = aes.CreateEncryptor(aes.Key, aes.IV);
using (MemoryStream ms = new MemoryStream())
{
using (CryptoStream cs = new CryptoStream(ms, enc, CryptoStreamMode.Write))
{
using (StreamWriter sw = new StreamWriter(cs))
{
sw.Write(originText);
}
encrypted = ms.ToArray();
}
}
}
return Convert.ToBase64String(encrypted);
}
public static string Decrypt(string encryptedText)
{
string decryted = null;
byte[] cipher = Convert.FromBase64String(encryptedText);
using (AesCryptoServiceProvider aes = new AesCryptoServiceProvider())
{
aes.KeySize = 256;
aes.BlockSize = 128;
aes.Key = Convert.FromBase64String(aesKey);
aes.IV = Convert.FromBase64String(aesIv);
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
ICryptoTransform dec = aes.CreateDecryptor(aes.Key, aes.IV);
using (MemoryStream ms = new MemoryStream(cipher))
{
using (CryptoStream cs = new CryptoStream(ms, dec, CryptoStreamMode.Read))
{
using (StreamReader sr = new StreamReader(cs))
{
decryted = sr.ReadToEnd();
}
}
}
}
return decryted;
}
}
}
구글링으로 찾아보니, C#으로 구현한 AES
알고리즘 코드가 git에 있어서, 그대로 가져다 써 봤다.
Key
: 대칭키로, 암호화 복호화 모두 이 키를 가지고 진행한다. AES-128 / AES-256 등등 알고리즘 뒤에 오는 숫자들은 이 키의 bit 수를 의미한다고 한다.IV
: 초기화 벡터 (Initialize Vector)라고 한다. 이후 소개할 CBC
방식에 사용할 벡터라고 이해하면 될 것 같다. 이 IV를 매번 다르게 만들어주면 같은 키를 사용하더라도 매번 다른 암호문을 만들어 낼 수 있다고 한다.key
와는 다르게 굳이 비밀일 필요도 없다고 함.Cipher Mode
: 암호화 과정에서 데이터 처리 방법을 지정한다. CBC
, ECB
, CTR
, OFB
, CFB
등이 있는데, 가장 대표적으로 많이 사용되는 방식은 CBC
(Cipher Block Chaining)라고 한다. CBC
방식은 암호화 과정에서 이전에 암호화된 byte와 계속해서 XOR연산을 진행하는데, 첫 암호화 단계에선 XOR연산을 할 byte가 아직 만들어지지 않았으니, 이 때 IV
를 이용한다.Padding Mode
: padding은 특정 공간 안에 부족한 부분을 메워주는 것을 말한다. 암호화에서 padding은 부족한 부분을 메울 때 의미없는 문자들로 채운다고 하는데, PKCS5
, PKCS7
등이 있다.https://gist.github.com/RichardHan/0848a25d9466a21f1f38
https://ts2ree.tistory.com/333
https://generate-random.org/encryption-key-generator?count=1&bytes=16&cipher=aes-256-cbc&string=&password=
https://aspdotnet.tistory.com/2836