[c#] 클래쓰를 직렬화하여 파일로 저장하기
밸류 타입 데이터를 메모리나 파일에 저장할 경우에는 그냥 순서대로 write하거나 시작 메모리 주소와 전체 크기를 지정하여 한꺼번에 write하면 되지만 레퍼런쓰 타입은 이렇게 하면 주소만 저장된다. 이런 문제를 피하기 이해 저장할 데이터를 바이트의 형태로 나열하는 걸 serialize한다고 한다. 델파이와 달리 .네트에는 씨리얼라이즈하기에 편한 클래쓰들이 준비되어 있다.
최근까지는 BinaryFormatter가 많이 이용되었지만 마이크로쏘프트는 이걸 이제는 이용하지 말고 XmlSerializer나 DataContractSerializer를 이용하라고 한다. 전자는 예전부터 이용되던 거고 후자는 나중에 만들어진 거다. 이용 방법이 조금 다르고 성능은 당연히 최신 클래쓰인 후자가 더 좋다.
The BinaryFormatter type is dangerous and is not recommended for data processing.
– Deserialization risks in use of BinaryFormatter and related types
XmlSerializer는 퍼블릭 엘리먼트들만 씨리얼라이즈하지만 DataContractSerializer는 DataMember라는 애트리뷰트가 마크된 것들만 직렬화한다. 프라이비트도 직렬화된다. 저장하지 않을 것에 DataMember 애트리뷰트를 마크하지 않아서 제외해도 디씨리얼라이즈 과정에서 문제가 생기지는 않는다. 제외된 것만 default로 처리된다.
name과 namespace를 설정하지 않으면 저장하는 폼의 이름과 네임스페이쓰가 저장되므로 이것들이 다르게 저장된 애플리케이션에서는 읽을 수 없다.
아래와 같이 하면 xml 파일이 만들어지는데 용량이 많이 커진다. 배보다 배꼽이 더 큰 거 같다. 클래쓰는 그렇다 치고 구조체는 델파이나 c++의 경우 바로 바이너리로 저장할 수 있는데 .네트에서는 역시 직렬화를 해야 한다. 우회할 수 있는 방법들이 몇 개 있긴 하지만 앓느니 죽는 셈이다.
// write
[DataContract(Name = "TextDataContract", Namespace = "shinjguk")]
class Class1
{
[DataMember]
public int Int1;
}
private void Form1_Load(object sender, EventArgs e)
{
Class1 class1 = new();
class1.Int1 = 1;
FileStream fileStream = new(@"c:\test.txt", FileMode.Create);
DataContractSerializer dataContractSerializer = new(typeof(Class1));
dataContractSerializer.WriteObject(fileStream, class1);
fileStream.Close();
}
// read
[DataContract(Name = "TextDataContract", Namespace = "shinjguk")]
class Class1
{
[DataMember]
public int Int1;
}
private void TestForm_Load(object sender, EventArgs e)
{
Class1 class1;
FileStream fileStream = new(@"c:\test.txt", FileMode.Open);
DataContractSerializer dataContractSerializer = new(typeof(Class1));
class1 = (Class1)dataContractSerializer.ReadObject(fileStream);
Text = class1.Int1.ToString();
fileStream.Close();
}
IntPtr은 직렬화해 봐야 주소만 정수로 저장되므로 해당 주소의 밸류로 인스턴쓰를 만든 뒤 씨리얼라이즈해야 한다.
클래쓰 안에 또 클래쓰가 있을 때에는 그 안의 것들에도 애트리뷰트를 표시해야 한다. 리스트의 경우에도 마찬가지다. 클래스 안에 리스트가 있으면 이것에도 데이터 멤버 마크를 해야 한고 그 리스트 안에 클래쓰가 들어간다면 그것들에도 애트리뷰트를 표시해야 한다.
[DataContract(Name = "ChildDataContract1", Namespace = "Namespace1")]
class ChildClass1
{
[DataMember]
public int Int1;
}
[DataContract(Name = "ChildDataContract2", Namespace = "Namespace1")]
class ChildClass2
{
[DataMember]
public string String1;
}
[DataContract(Name = "ParentDataContract", Namespace = "Namespace1")]
class ParentClass
{
[DataMember]
public ChildClass1 ChildClass1_1 = new();
[DataMember]
public ChildClass2 ChildClass2_1 = new();
}
private void button1_Click(object sender, EventArgs e)
{
ParentClass parentClass = new();
ChildClass1 childClass1 = new();
ChildClass2 childClass2 = new();
childClass1.Int1 = 1;
cildClass2.String1 = "abcde";
parentClass.ChildClass1_1 = childClass1;
parentClass.ChildClass2_1 = childClass2;
FileStream fileStream = new(@"c:\test.txt", FileMode.Create);
DataContractSerializer dataContractSerializer = new(typeof(ParentClass));
dataContractSerializer.WriteObject(fileStream, parentClass);
fileStream.Close();
}
private void button2_Click(object sender, EventArgs e)
{
ParentClass parentClass = new();
FileStream fileStream = new(@"c:\test.txt", FileMode.Open);
DataContractSerializer dataContractSerializer = new(typeof(ParentClass));
parentClass = (ParentClass)dataContractSerializer.ReadObject(fileStream);
Text = parentClass.ChildClass1_1.Int1.ToString() + " " + parentClass.ChildClass2_1.String1; // 1 abcde
fileStream.Close();
}
여러 클래쓰들을 연달아 직렬화하는 게 가능하기는 한데 이러면 디씨리얼라이즈할 때 multiple root elements가 있다며 오류로 처리된다. xml에서는 루트 엘리먼트가 하나만 있어야 한다. 이런 경우에는 여럿을 하나로 묶어서 계층 구조를 바로잡은 뒤 이걸 씨리얼라이즈해야 한다.