C# 6.0 완벽 가이드/ 직렬화

직렬화 개념

  • 직렬화(serialization)는 메모리 안의 객체 하나 또는 객체 그래프(서로 참조하는 객체들의 집합)를 저장 또는 전송에 적합한 바이트 스트림 또는 XML 노드들로 평탄화(flattening)하는 연산을 뜻한다.
    • 그 반대는 역직렬화(deserialization), 즉 자료 스트림을 해석해서 메모리 안의 객체 또는 객체 그래프를 재추축하는 연산이다.
  • 직렬화와 역직렬화의 주된 용도는 다음 두 가지이다.
    • 객체를 네트워크나 응용 프로그램 경계 너머로 전송한다.
    • 객체를 파일이나 데이터베이스에 저장한다.
  • 그보다 덜 자주 쓰이는 용도는 객체들을 깊게 복제하는 것이다. 또한 직렬화 및 역직렬화 기능을 제공하는 자료 계약(data contract) 직렬화 엔진과 XML 직렬화 엔진을 XML 파일(구조가 알려진)의 저장과 적재를 위한 범용 도구로 사용할수도 있다.
  • .NET Framework는 직렬화 및 역직렬화를, 객체들의 직렬화/역직렬화를 원하는 클라이언트의 관점에서는 물론 자신의 직렬화 방식을 어느 정도 제어하고자 하는 형식의 관점에서도 지원한다.

직렬화 엔진

  • .NET Framework는 다음과 같은 네 가지 직렬화 메커니즘을 제공한다.
    • 자료 계약 직렬화기
    • 이진 직렬화기(데스크톱 응용 프로그램에서)
    • (특성 기반) XML 직렬화기(XmlSerializer)
    • IXmlSerializable 인터페이스
  • 이 중 처음 셋은 직렬화 작업의 대부분 또는 전부를 실제로 수행하는 직렬화 ‘엔진’이다. 마지막은 XmlReader와 XmlWriter를 이용해서 직렬화를 독자가 직접 구현하는데 필요한 틀을 제공할 뿐이다.
    • IXmlSerializable은 자료 계약 직렬화기(data contract serializer)와 함께 사용할 수도 있고, XmlSerializer와 함께 사용할 수도 있다 (좀 더 복잡한 XML 직렬화 과제를 처리하려는 경우)
  • 아래 표는 네 메커니즘을 비교한 것이다 별표가 많을수록 점수가 높다.
자료 계약 직렬화기 이진 직렬화기 XmlSerializer IXmlSerializable
자동화 수준 *** ***** **** *
형식 결합도 선택 강함 느슨함 느슨함
버전 내구성 ***** *** ***** *****
객체 참조 유지 선택 아니요 선택
비공용 필드 직렬화 가능 아니요
상호운용적 메시지 적합성 ***** ** **** ****
XML 파일 읽기/쓰기의 유연성 ** **** *****
축약된 출력 ** **** ** **
성능 *** **** * ~ *** ***

 

  • IXmlSerializable의 점수들은 독자가 XmlReader와 XmlWriter를 이용해서 코드를 최적으로 작성했다고 가정한 것이다. XML 직렬화 엔진에서 좋은 성능을 내려면 같은 XmlSerializer 객체를 재사용해야 한다.

엔진이 세 개인 이유

  • 엔진이 세 개나 되는 것은 어느 정도 역사적인 이유 떄문이다. 초창기 .NET Framework에서 직렬화 기능은 다음과 같은 서로 다른 두 목표를 위해 만들어졌다.
    • .NET 객체 그래프를 형식과 참조를 충실히 보존할 수 있는 방식으로 직렬화한다.
    • XML과 SOAP 메시징 표준들과의 상호운용성을 보장한다
  • 첫 목표는 Remoting Framework의 요구에서 비롯된 것이고, 둘째 목표는 Web Services의 요구 때문이다. 두 목표를 모두 만족하는 하나의 작렬화 엔진을 작성하는 것은 너무 벅찬 일이라서 Microsoft는 엔진을 두 개 작성했다. 그것이 바로 이진 직렬화기와 XML 직렬화기이다.
  • 이후 WCF(Windows Communication Foundation)가 .NET Framework 3.0에 도입되면서 Remoting과 Web Services를 통합하는 것이 목표의 일부가 되었다. 이를 위해서는 새로운 직렬화 엔진이 필요했다. 자료 계약 직렬화기가 바로 그것이다.
    • 자료 계약 직렬화기는 (상호운용적) 메시징에 관련된 두 기존 엔진의 기능들을 통합한다. 그러나 이러한 문맥 바깥에서는 기존의 두 엔진도 여전히 중요하다.

자료 계약 직렬화기

  • WCF에 쓰이는 자료 계약 직렬화기는 세 직렬화 엔진 중 최시느이 그리고 가장 다재다능한 엔진이다. 이 직렬화기는 다음 두 상황에서 특히나 강력하다.
    • 표준을 준수하는 메시징 프로토콜들을 통해서 정보를 교환할 때
    • 버전 내구성(version tolerance)이 좋아야 하고 객체 참조들을 유지할 수 있으면 더욱 좋을 때
  • 자료 계약 직렬화기는 자료 계약(data contract) 모형을 지원한다. 이 모형은 직렬화하려는 형식의 저수준 세부사항을 직렬화된 자료의 구조로부터 분리하는데 도움이 된다.
    • 이 모형을 따르면 버전 내구성이 아주 좋아진다. 버전 내구성이 좋다는 것은 객체를 직렬화할 때 쓰인 형식의 버전이 객체를 역직렬화할 떄 쓰이는 형식의 버전과 달라도 객체를 최대한 잘 복원할 수 있음을 뜻한다. 심지어 이름이 바뀌거나 다른 어셈블리로 이동한 형식도 역직렬화할 수 있다.
  • 자료 계약 직렬화기는 대부분의 객체 그래프를 지원하지만, 이진 직렬화기보다 프로그래머의 손이 많이 가는 떄도 있다. 다루고자 하는 XML의 구조를 어느 정도 임의로 제어할 수 있는 경우에는 자료 계약 직렬화기를 XML 파일을 읽고 쓰는 범용 도구로 사용할 수도 있다. (자료를 XML 요소의 특성들에 저장해야 하거나 XML 요소들이 임의의 순서로 등장하는 경우에는 자료 계약 직렬화기를 사용할 수 없다.)

이진 직렬화기

  • 이진 직렬화 엔진은 사용하기 쉽고, 고도로 자동화 되어 있으며, .NET Framwork 전반에서 잘 지원된다. Remoting은 이진 직렬화를 사용한다(이를테면 같은 프로세스의 두 응용 프로그램 도메인 사이의 통신에서도)
  • 이진 직렬화기는 고도로 자동화되어 있다. 그냥 특성 하나만 지정하면 복잡한 형식이 완전히 직렬화 가능한 형식으로 변하는 경우가 많다. 또한 완전한 형식 충실도가 필요한 경우 이진 직렬화기가 자료 계약 직렬화기보다 빠르다.
    • 대신 형식의 내부 구조와 직렬화된 자료의 구조가 밀접히 결합하기 때문에 버전 내구성이 낮다는 단점이 있다. (.NET Framework 2.0 이전에는 필드 하나만 추가해도 버전 간 직렬화 호환성이 깨졌다)
    • 그리고 이진 직렬화 엔진은 XML 출력에 맞게 설계된 것이 아니다. 그러나 SOAP 기반 메시징을 위한 포매터는 제공한다. 이를 통해서 제한적이나마 단순 형식들에 대한 상호운용성은 얻을 수 있다.

XML 직렬화기

  • XML 직렬화 엔진(XmlSerializer)은 XML만 산출할 수 있으며, 복잡한 객체 그래프를 저장, 복원하는 능력은 다른 엔진들보다 못하다(예컨대 공유된 객체 참조들은 복원하지 못한다) 그러나 임의의 XML 구조에 대응하는 능력 덕분에 유연성은 세 엔진 중 가장 뛰어나다.
    • 예컨대 객체의 어떤 속성을 XML 요소로 직렬화할 지 아니면 요소의 한 특성으로 직렬화할지 선택할 수 있으며, 컬렉션의 외곽 요소를 처리하는 방식도 선택할 수 있다.
    • XML 직렬화 엔진은 또한 버전 내구성도 훌륭하다.
  • XmlSerializer는 ASMX Web Services가 사용한다.

IXmlSerializable 인터페이스

  • IXmlSerializable을 구현한다는 것은 XmlReader와 XmlWriter를 이용해서 직렬화를 수행하는 코드를 독자가 직접 작성한다는 뜻이다.
    • XmlSerializer와 자료 계약 직렬화기 모두 IXmlSerializable 인터페이스를 인식하므로, 기본적으로 XmlSerializer 또는 자료 계약 직렬화기를 사용해도 좀 더 복잡한 형식에 대해서는 IXmlSeiralizable 구현을 함께 사용할 수 있다.(물론 IXmlSerializable 구현을 단독으로 직접 사용할 수도 있다. WCF와 ASMX Web Services가 그런 접근 방식을 사용한다)

포매터

  • 자료 계약 직렬화기와 이진 직렬화기가 출력하는 직렬화 결과의 형태를 교체 가능한 포매터(fomatter)를 이용해서 변형할 수 있다. 두 직렬화 엔진 모두 포매터를 같은 목적으로 사용하지만, 구체적인 클래스는 다르다.
  • 포매터는 직렬화의 최종 결과를 특정 매체나 직렬화 문맥에 맞는 형태로 만든다. 흔히 XML 포매터와 이진 포매터 중 하나를 사용한다.
    • XML 포매터는 XML 기록자/판독자, 텍스트 파일/스트림, SOAP 메시징 패킷과 함께 사용하도록 만들어진 것이다.
    • 반면 이진 포매터는 임의의 바이트 스트림을 염두에 두고 설계된 것으로, 흔히 파일/스트림이나 커스텀 메시징 패킷에 쓰인다. 대체로 이진 출력이 XML보다 작다. 훨씬 작은 경우도 종종 있다.
  • 포매터와 관련해서 ‘이진’이라는 용어는 ‘이진’ 직렬화 엔진과는 무관하다. 이진 직렬화 엔진은 이진 포매터와 XML 포매터를 모두 제공하며, XML 직렬화 엔진 역시 마찬가지다.
  • 이론적으로 엔진과 그 포매터는 분리되어 있다. 그러나 현실적으로 각 엔진은 특정 종류의 포매터에 맞게 설계되어 있다.
    • 자료 계약 직렬화기는 XML 메시징의 상호운용성 요구사항들에 맞추어져 있기 때문에 XML 포매터와 잘 맞지만, 이진 포매터를 사용하면 기대한 만큼의 이득이 없다.
    • 반면 이진 직렬화 인젠은 비교적 좋은 이진 포매터를 제공하지만, XML 포매터는 조악한 SOAp 상호운용성을 제공하는 수준으로 아주 제한적이다.

명시적 직렬화 대 암묵적 직렬화

  • 직렬화/역직렬화를 진행하는 방식은 크게 두 가지로 나뉜다.하나는 구체적인 객체에 대해 직렬화(또는 역직렬화)를 명시적으로 시작하는 것이다. 직렬화 또는 역직렬화를 명시적으로 진행할 때는 직렬화 엔진과 포매터도 직접 선택한다.
  • 다른 하나는 .NET Framework가 내부적으로 진행하는 암묵적 직렬화이다. 암묵적 직렬화는 다음과 같은 경우에 일어난다.
    • 직렬화기가 자식 객체를 재귀적으로 직렬화할 때
    • WCF나 Remoting, Web Services처럼 직렬화에 의존하는 기능을 사용할 때
  • WCF는 항상 자료 계약 직렬화기를 사용하지만, 다른 엔진들의 특성이나 인터페이스와의 연동도 가능하다.
    • Remoting은 항상 이진 직렬화기를 사용한다.
    • Web Services는 항상 XmlSerializer를 사용한다.

자료 계약 직렬화기

  • 다음은 자료 계약 직렬화기를 사용하는 기본적인 단계들이다.
    1. DataContractSerializer를 사용할지, 아니면 NetDataContractSerializer를 사용할지 결정한다.
    2. 직렬화할 형식들과 멤버들에 각각 [DataContract] 특성과 [DataMember] 특성을 적용한다
    3. 직렬화기 인스턴스를 생성해서 WriteObject나 ReadObject를 호출한다.
  • DataContractSerializer를 사용하기로 했다면 ‘알려진 형식들(함께 직렬화할 하위 형식들)’도 등록해야 하며, 객체 참조들을 유지할 것인지도 결정해야 한다.
  • 또한 컬렉션이 제대로 직렬화되게 하려면 특별한 처리를 해주어야 할 수도 있다.

DataContractSerializer 대 NetDataContractSerialize

  • 자료 계약 직렬화기 클래스는 다음 두 가지이다.
    • DataContractSerializer
      • .NET 형식들과 자료 계약 형식들 사이의 결합도가 낮다.
    • NetDataContractSerializer
      • .NET 형식들과 자료 계약 형식들 사이의 결합도가 높다.
  • DataContractSerializer는 상호운용성이 좋은, 표준을 준수하는 XML을 산출한다. 다음은 그러한 XML의 예이다.
<Person xmls="...">
  ....
</Person>
  • DataContractSerializer의 단점은 직렬화 가능한 형식들을 미리 등록해야한다는 것이다. 그래야 역직렬화시 ‘Person’ 같은 자료 계약 이름을 적절한 .NET 형식에 대응시킬 수 있기 때문이다.
    • 반면 NetDataContractSerializer는 이진 직렬화 엔진처럼 형식 이름 전체와 어셈블리 이름을 출력 자체에 포함하므로 그런 전처리가 필요하지 않다. 예컨대 다음과 같다.
<Person z:Type="SerialTest.Person" z;Assembly="SerialTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
  ....
</Person>
  • 그러나 이런 서식은 .NET 프레임워크에 고유한 것일 뿐 널리 쓰이는 표준과는 무관하므로 상호운용성이 떨어진다.
    • 또한 역직렬화할 때 특정 이름공간과 어셈블리의 특정 .NET 형식이 있어야 형식이 제대로 복원된다.
  • 객체 그래프를 일종의 ‘블랙박스’에 저장할 때는 어떤 직렬화기 클래스를 사용해도 좋다.
    • 해당 프로그램의 목적에 중요한 이점을 제공하는 클래스를 선택하면 된다.
    • 그러나 WCF를 통해서 통신하거나 XML 파일을 읽고 써야 할 때는 DataContractSerializer가 더 나은 선택인 경우가 많다.
  • 두 직렬화기 클래스의 또 다른 차이는 NetDataContractSerializer는 항상 참조상등성(referential equality)을 보존하지만 DataContractSerializer는 요청 시에만 보존한다는 것이다.

직렬화기 사용

  • 직렬화기를 선택했다면 다음으로 할 일은 직렬화할 형식들과 멤버들에 특성을 부여하는 것이다. 적어도 다음 두 가지는 해야 한다.
    • 각 형식에 [DataContract] 특성을 부여한다.
    • 출력에 포함할 각 멤버에 [DataMember] 특성을 부여한다.
  • 다음은 클래스 하나와 그 멤버들에 직렬화 특성들을 부여한 예이다.
namespace SerialTest
{
  [DataContract] public class Person
  {
    [DataMember] public string Name;
    [DataMember] public int Age;
  }
}
  • 이 특성들을 부여함으로써 이 형식은 자료 계약 엔진이 암묵적으로 직렬화할 수 있는 형식이 된다.
  • 이런 식으로 정의된 형식의 객체를 명시적으로 직렬화 또는 역직렬화할 때는 DataContractSerializer나 NetDataContractSerializer의 인스턴스를 생성한 후 WriteObject, ReadObject를 호출하면 된다.
Person p = new Person { Name = "Stacey", Age = 30 };
var ds = new DataContractSerializer (typeof (Person));

using (Stream s = File.Create("person.xml"))
  ds.WriteObject(s, p);  // 직렬화

Pserson p2;
using (Stream s = File.OpenRead("person.xml"))
  p2 = (Person) ds.ReadObject(s);  // 역직렬화

Console.WriteLine(p2.Name + " " + p2.Age);  // Stacey 30
  • DataContractSerializer의 생성자는 직렬화할 객체 그래프의 뿌리(root) 노드에 해당하는 객체(명시적으로 직렬화를 시작할 객체)의 형식을 요구한다. 반면 NetDataContractSerializer는 그런 정보를 요구하지 않는다.
var ns = new NetDataContractSerializer();

// NetDataContractSerializer에서도 나머지 과정은 DataContractSerializer를 사용할 때와 같다.
...
  • 두 직렬화기 클래스 모두 기본적으로 XML 포매터를 사요한다. XmlWriter와 XmlWriterSettings를  이용하면 가독성을 위한 들여쓰기도 설정할 수 있다.
Person p = new Person { Name = "Stacey", Age = 30 };
var ds = new DataContractSerializer (typeof (Person));

XmlWriterSettings settings = new XmlWriterSettings() { Indent = true };

using (XmlWriter w = new XmlWriter.Create("person.xml", settings))
  ds.WriteObject(w, p);

System.Diagnostics.Process.Start("person.xml");
  • 직렬화 결과는 다음과 같다.
<Person xmlns="http://schemas.datacontract.org/2004/07/SerialTest" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Age>30</Age>
  <Name>Stacey</Name>
</Person>
  • XML 요소 <Person>의 ‘Person’이라는 이름은 형식의 특성으로 지정된 자료 계약 이름에서 비롯된 것이다. 기본적으로 자료 계약 이름은 해당 .NET 형식의 이름과 같지만, 필요하다면 다음과 같이 다른 이름을 지정할 수도 있다.
[DataContract(Name="Candidate")]
public class Person { ... }
  • 비슷하게 XML 이름공간은 자료 계약 이름공간에서 비롯된 것이다. 자료 계약 이름공간은 기본적으로 http://schemas.datacontract.org/2004/07/에 .NET 형식의 이름공간을 붙인 것인데, 역시 다른 이름공간을 지정할 수 있다.
[DataContract(Namespace="http://orilly.com/nutshell")]
public class Person { ... }
  • 이처럼 자료 계약의 이름과 이름공간을 명시적으로 지정하면 자료 계약과 .NET 형식 사이의 결합이 끊어진다. 그러면 나중에 형식의 이름이나 이름 공간을 변경해도 직렬화/역직렬화에 영향을 미치지 않는 ‘버전 내구성’이 생긴다.
  • 자료 멤버의 이름도 지정할 수 있다.
[DataContract(Name="Candidate", Namespace="http://orilly.com/nutshell")]
public class Person 
{ 
  [DataMember (Name="FirstName")] public string Name;
  [DataMember (Name="ClaimedAge")] public int Age;
}
  • 출력은 다음과 같다.
<?xml version="1.0" encoding="utf-8"?>
<Candidate xmlns="http://oreilly.com/nutshell" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <ClaimedAge>30</ClaimedAge>
  <FirstName>Stacey</FirstName>
</Candidate>
  • [DataMember] 특성은 필드뿐만 아니라 속성에도 그리고 공용(public) 멤버뿐만 아니라 전용(private) 멤버에도 붙일 수 있다. 단 필드나 속성의 자료 형식이 다음 중 하나이어야 한다.
    • 모든 기본 형식
    • DateTime, TimeSpan, Guid, Uri 또는 모든 Enum 파생 형식
    • 위의 널 가능 버전들
    • byte[] (XML에서는 Base64로 부호화됨)
    • DataContract 특성이 부여된 ‘알려진’ 형식
    • IEnumerable을 구현하는 모든 형식
    • [Serializable] 특성이 부여되었거나 ISerializable을 구현하는 모든 형식
    • IXmlSerializable을 구현하는 모든 형식

이진 포매터 지정

  • DataContractSerializer와 NetDataContractSerializer 둘 다 이진 포매터를 지원한다. 이진 포매터를 사용하는 과정은 두 클래스가 동일하다.
Person p = new Person { Name = "Stacey", Age = 30 };
var ds = new DataContractSerializer (typeof (Person));

var s = new MemoryStream();
using (XmlDictionaryWriter w = XmlDictionaryWriter.CreateBinaryWriter(s))
  ds.WriteObject(w, p); 

var s2 = new MemoryStream(s.ToArray());
Person p2;
using (XmlDictionaryReader r = XmlDictionaryReader.CreateBinaryReader(s2, XmlDictionaryReaderQuotas.Max))
  p2 = (Person) ds.ReadObject(r);
  • 둘의 출력은 조금 다른데, XML 포매터의 결과보다 크기가 약간 작다는 점은 동일하다. 형식에 큰 배열이 포함되어 있으면 훨씬 더 작다.

파생 클래스 직렬화

  • NetDataContractSerializer를 사용할 때는 특별한 처리 없이도 파생 클래스들이 직렬화된다. 그냥 파생 클래스에 DataContract 특성만 부여하면된다.
    • 다음 예에서 보듯이 NetDataContractSerializer는 실제 파생 형식과 그 어셈블리의 완전 한정 이름들을 기록한다.
<Person ... z:Type="SerialTest.Person" z:Assembly=SerialTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
  • 그러나 DataContractSerializer를 사용할 때는 직렬화/역직렬화할 파생 형식들을 구체적으로 지정해야 한다. 예컨대 Person 클래스와 그 파생 클래스들을 다음과 같이 정의한다고 하자.
[DataContract] public class Person
{
  [DataMember] public string Name;
  [DataMember] public int Age;
}
[DataContract] public class Student : Person { }
[DataContract] public class Teacher : Person { }
  • 그리고 Person을 복제하는 다음과 같은 메서드를 작성한다고 하자.
static Person DeepClone (Person p)
{
  var ds = new DataContractSerializer (typeof(Person));
  MemoryStream stream = new MemoryStream();
  ds.WriteObject(stream, p);
  stream.Position = 0;
  return (Person) ds.ReadObject(stream);
}
  • 다음은 이 메서드를 호출하는 예이다.
Person person = new Person { Name = "Stacey", Age = 30 };
Student student = new Student { Name = "Stacey", Age = 30 };
Teacher teacher = new Teacher { Name = "Stacey", Age = 30 };

Person p2 = DeepClone(person);  // OK
Student s2 = (Student) DeepClone(student);  // SerializationException
Teacher t2 = (Teacher) DeepClone(teacher);  // SerializationException
  • Person으로 DeepClone을 호출하면 잘 작동하지만, Student나 Teacher로 호출하면 예외가 발생한다. 이는 역직렬화기가 ‘Student’나 ‘Teacher’를 어떤 .NET 형식 (그리고 어셈블리)으로 복원해야 할지 알지 못하기 때문이다. 예기치 못한 형식이 역직렬화되는 일을 방지한다는 점에서 이는 보안에 도움이 되는 측면이기도 하다.
  • 위의 코드가 작동하려면 역직렬화가 허용되는 파생 형식들, 즉 ‘알려진’ 파생 형식들을 명시적으로 지정해야 한다.
    • 한 가지 방법은 다음처런 DataContractSerializer를 생성할 때 해당 형식들을 지정하는 것이다.
var ds = new DataContractSerializer(typeof(Person), new Type[] { typeof(Student), typeof(Teacher) });
  • 또는 다음처럼 형식 자체에 대해 KnownType 특성을 이용해서 지정할 수도 있다.
[DataContract, KnownType(typeof(Student)), KnownType(typeof(Teacher))]
public class Person
  ...
  • 다음은 Student를 직렬화한 예이다.
<Person xmlns="..." xmlns:i="http://www.w3.org/2001/XMLSchema-instance" i:type="Student">
  ...
</Person>
  • Person을 뿌리 형식으로 지정했으므로, 뿌리 요소의 이름은 여전히 ‘Person’이다. 실제 파생 클래스는 그 요소의 type 특성에 따로 서술되어 있다.
  • 파생 형식들과 직렬화할 때는 NetDataContractSerializer의 성능이 몹시 나쁘다. 마치 파생 형식과 마주치면 NetDataContractSerializer가 손을 놓고 한참 고민하는 것처럼 보일 정도이다.
    • 직렬화 성능은 이를테면 다수의 요청을 동시에 처리해야 하는 응용 프로그램 서버에서 중요하다.

객체 참조

  • 다른 객체에 대한 참조도 직렬화된다. 다음과 같은 클래스를 생각해 보자.
[DataContract] public class Person 
{ 
  [DataMember] public string Name;
  [DataMember] public int Age;
  [DataMember] public Address HomeAddress;
}

[DataContract] public class Address
{
  [DataMember] public string Street, Postcode;
}
  • DataContractSerializer를 이용해서 이를 XML로 직렬화하면 다음과 같은 형태의 XML이 기록된다.
<Person ...>
  <Age>...</Age>
  <HomeAddress>
    <Street>...</Street>
    <Postcode>...</Postcode>
  </HodeAddress>
  <Name>...</Name>
</Person>
  • 앞에서 작성한 DeepClone 메서드는 HomeAddress도 복제할 것이다. 이는 단순 복사를 수행하는 MemberwiseClone 메서드와는 구별되는 특징이다.
  • DataContractSerializer를 사용할 떄는 Address의 파생에도 뿌리 형식의 파생에서와 같은 규칙들이 적용된다. 예컨대 USAddress라는 클래스를 정의해서
[DataContract] public class USAddress : Address { }
  • 그 인스턴스를 Person의 해당 속성에 배정한다고 하자.
Person p = new Person { Name = "John", Age = 30 };
p.HomeAddress = new USAddress { Street="Fawcett St", Postcode="02138" };
  • 이렇게 하면 p를 직렬화할 수 없다. 해결책은 이전과 같다. 즉, Address에 KnownType 특성을 부여해서 USAddress를 알려주거나
[DataContract, KnownType(typeof(USAddress)] 
public class Address
{
  [DataMember] public string Street, Postcode;
}
  • DataContractSerializer 인스턴스 생성시 USAddress를 알려주면 된다.
var ds = new DataContractSerializer(typeof(Person), new Type[] { typeof(USAddress) });
  • Address는 이미 HomeAddress 자료 멤버의 형식으로 선언되어 있으므로 따로 알려 줄 필요가 없다.

객체 참조 유지

  • NetDataContractSerializer는 항상 참조 상등성을 유지한다. 그러나 DataContractSerializer는 명시적으로 지정한 경우에만 유지한다.
  • 따라서 만일 두 곳에서 같은 객체를 참조한다면 DataContractSerializer는 그 객체를 두 번 기록한다. 예컨대 Person 클래스에 직장 주소를 위한 필드를 추가한다고 하자.
[DataContract] public class Person 
{ 
  ...
  [DataMember] public Address HomeAddress, WorkAddress;
}
  • 그리고 다음처럼 집 주소와 직장 주소가 같은 사람이 있다고 하자.
Person p = new Person { Name = "Stacey", Age = 30 };
p.HomeAddress = new Address { Street="Odo St", Postcode="6020" };
p.WorkAddress = p.HomeAddress;
  • 이 인스턴스를 DataContractSerializer로 직렬화하면, 다음처럼 XML에 같은 주소가 두 번 등장한다.
...
<HomeAddress>
  <Street>Odo St</Street>
  <Postcode>6020</Postcode>
</HodeAddress>
...
<HomeAddress>
  <Street>Odo St</Street>
  <Postcode>6020</Postcode>
</HodeAddress>
  • 나중에 이를 역직렬화하면 WorkAddress와 HomeAddress는 다른 객체가 될 것이다. 이런 방식의 장점은 XML이 간단하다는 점과 표준을 준수한다는 점이다. 단점은 XML이 더 커지고 참조 무결성(referential integrity; 참조의 일관성)이 사라지고, 순환 참조 문제를 처리할 수 없다는 점이다.
  • 참조의 무결성을 원한다면 DataContractSerializer를 인스턴스화 할 때 다음처럼 preserveObjectReferences 매개변수에 true를 지정하면 된다.
var ds = new DataContractSerializer(typeof(Person), null, 1000, false, true, null);
  • preserveObjectReferences를 true로 할 때는 셋째 인수도 반드시 지정해야 한다. 이 인수는 직렬화기가 유지할 객체 참조들의 최대 개수를 뜻한다. 만일 객체 참조가 그보다 많으면 직렬화기는 예외를 던진다(이러한 제한은 악의적으로 만들어진 스트림을 이용한 DoS(서비스 거부) 공격을 방지하기 위한 것이다)
  • 이제 집 주소와 직장 주소가 같은 Person 인스턴스를 직렬화하면 다음과 같은 XML이 만들어진다.
<Person xmlns="http://schemas.datacontract.org/2004/07/SerialTest" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" z:Id="1">
  <Age>30</Age>
  <HomeAddress z:Id="2">
    <Postcode z:Id="3">6020</Postcode>
    <Street z:Id="4">Odo St</Street>
  </HodeAddress> 
  <Name z:Id="5">Stacey</Name> 
  <WorkAddress z:Ref="2" i:nil="true"/>
</Person>
  • 이렇게 하면 참조 무결성이 보존되지만, 대신 상호운용성이 떨어진다(Id와 Ref 특성에 Microsoft 고유의 이름공간이 쓰였음을 주목할 것)

버전 내구성

  • 직렬화 자료의 상휘 호환성이나 하위 호환성을 깨지 않고도 형식에 자료 멤버를 추가하거나 제거할 수 있다. 기본적으로 자료 계약 직렬화기는 역직렬화시 다음과 같이 행동한다.
    • 인식되지 않은 자료, 즉 [DataMember]가 붙은 멤버에 대응되지 않는 자료는 그냥 건너뛴다.
    • [DataMember]가 붙은 멤버가 해당하는 자료가 직렬화 스트림에 없어도 불평하지 않는다.
  • 인식되지 않은 자료를 그냥 건너뛰는 것이 마음에 들지 않다면, 그런 자료를 일단 블랙박스에 넣어 두고 나중에 객체를 다시 직렬화할 때 블랙박스의 자료를 직렬화에 포함하게 만드는 것도 가능하다. 그러면 해당 객체를 형식의 이후 버전에서 제대로 복원할 가능성이 생긴다.
    • 이 기능을 활성화하려면 IEntensibleDataObject를 구현해야 한다. 사실 이 인터페이스에는 “IBlackBoxProvider(블랙박스 공급자 인터페이스)”라는 이름이 더 어울린다. 이 인터페이스를 구현하렴녀 블랙박스를 조회/설정하는 속성 하나만 구현하면 된다.
[DataContract] public class Person : IExtensibleDataObject
{ 
  [DataMember] public string Name;
  [DataMember] public int Age;
  ExtensionDataObject IExtensibleDataObject.ExtensionData { get; set; }
}

필수 멤버

  • 주어진 형식에 꼭 필요한 멤버의 경우, 직렬화 스트림에 반드시 그 멤버의 자료가 존재해야 한다는 점을 DataMember 특성의 IsRequired 매개변수로 지정할 수 있다.
[DataMember (IsRequired=true)] public int ID;
  • 역직렬화시 직렬화 스트림에 이 멤버가 존재하지 않으면 예외가 발생한다.

멤버 순서

  • 자료 계약 직렬화기는 자료 멤버의 순서에 관해 극도로 까다롭다. 실제로 역직렬화 시 이 직렬화기는 제 순서가 아니라고 판단한 멤버들을 모두 건너뛴다.
  • 직렬화시 멤버들은 다음과 같은 순서로 기록된다.
    1. 기반 클래스에서 파생 클래스 순으로
    2. Order가 낮은 것에서 높은 것 순으로 (Order가 설정된 자료 멤버들의 경우)
    3. 알파벳순으로(서수적 문자열 비교를 이용)
  • 예컨대 앞의 예제에서는 Age가 Name보다 먼저 기록되고 다음 예에서는 Name이 Age 보다 먼저 기록된다.
[DataContract] public class Person
{ 
  [DataMember (Order=0)] public string Name;
  [DataMember (Order=1)] public int Age;
}
  • 만일 Person에 기반 클래스가 있다면 그 기반 클래스의 자료 멤버들이 제일 먼저 기록된다.
  • 멤버들의 순서를 지정하는 주된 이유는 특정 XML 스키마를 만족하기 위한 것이다. XML 요소 순서는 자료 멤버 순서와 같다.
  • 다른 무언가와의 상호운용성이 필요하지 않다면, 가장 쉬운 접근방식은 자료 멤버의 Order를 지정하지 않고 그냥 알파벳순에만 의존하는 것이다. 그러면 형식에 멤버들을 추가하거나 제거해도 직렬화나 역직렬화 시 멤버 순서 불일치 때문에 문제가 생길 일이 없다. 기반 클래스에서 파생 클래스로(또는 그 반대로) 멤버를 이동하는 경우만 신경 쓰면 된다.

널 또는 빈 값 처리

  • 값이 널이거나 비어 있는 자료 멤버를 다루는 방식을 크게 두 가지이다.
    1. 널이나 빈 값을 명시적으로 기록한다(기본 방식)
    2. 그런 값을 가진 자료 멤버를 직렬화 출력에서 제외한다.
  • XML의 경우 , 값이 널인 멤버를 명시적으로 기록하면 다음과 같은 모습이 된다.
<Person xmlns="..." xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Name i:nil="true"/>
</Person>
  • 그런데 널 또는 빈 값을 가진 멤버를 명시적으로 기록하면 공간 낭비가 될 수 있다. 빈 값인 필드나 속성이 많이 있는 형식의 경우에는 특히 낭비가 크다. 더욱 중요하게는 nil 값이 아니라 선택적 (생략 가능) 요소 (이를테면 minOccures=”0″)를 사용하는 XML 스키마를 따라야 할 때도 있다.
  • 널 값이나 빈 값을 가진 자료 멤버를 직렬화기가 기록하지 않게 하려면 다음과 같이 하면 된다.
[DataContract] public class Person
{ 
  [DataMember (EmitDefaultValue=false)] public string Name;
  [DataMember (EmitDefaultValue=false)] public int Age;
}
  • 이렇게 하면 값이 null이면 Name 속성은 기록되지 않으며, 값이 0(int 형식의 기본값)인 Age 속성도 기록되지 않는다. Age가 널 가능 int 형식인 경우, 만일 Age의 값이 널이면(그리고 오직 그럴 때만) Age는 기록되지 않는다.
  • 자료 계약 직렬화기는 객체를 역직렬화할 때 해당 형식의 생성자와 필드 초기치(initializer) 절을 건너뛴다. 이는 행당 형식의 기본값 이외의 값이 필드 초기치나 생성자를 통해서 배정되는 자료 멤버를 직렬화에서 제외시켜도 문제가 생기지 않게 하기 위한 것이다. 예컨대 Person 클래스의 Age를 기본적으로 30으로 둔다고 하자.
[DataMember (EmitDefaultValue=false)] public int Age = 30;
  • Person 인스턴스의 Age를 의도적으로 0으로 설정한 후 직렬화하면 Age는 직렬화에서 제외된다(0이 int 형식의 기본값이므로). 이를 다시 역직렬화하면 Age에는 해당 형식의 기본값이 배정되는데, 그 값은 바로 애초에 설정한 0이다. 만일 필드 초기치 절이나 생성자를 건너뛰지 않았다면 엉뚱한 값인 30이 설정되었을 것이다.

자료 계약 직렬화기와 컬렉션

  • 자료 계약 직렬화기는 그 어떤 열거 가능 컬렉션도 저장하고 복원할 수 있다. 예컨대 Person에 주소 목록을 담는 필드를, 구체적으로 말해서 List<Address> 형식의 Addresses라는 필드를 추가한다고 하자.
[DataContract] public class Person
{ 
  ...
  [DataMember] public List<Address> Addresses;
}

[DataContract] public class Address
{ 
  [DataMember] public string Street, Postcode;
}
  • Person 인스턴스에 주소 두 개를 추가해서 직렬화하면 다음과 같은 형태의 결과가 나온다.
<Person ...>
  ...
  <Addresses>
    <Address>
      <Postcode>6020</Posecode>
      <Street>Odo St</Street>
    </Address>
    <Address>
      <Postcode>6152</Posecode>
      <Street>Comer St</Street>
    </Address>
  </Addresses>
  ...
</Person>
  • 직렬화기가 컬렉션을 직렬화할 때 컬렉션의 구체적인 형식에 관한 정보는 전혀 포함시키지 않았음을 주목하기 바란다. Addresses 필드의 형식을 Address[]로 했어도 같은 결과가 나왔을 것이다. 이런 방식에서는 직렬화와 역직렬화 사이에 컬렉션 형식을 바꾸어도 오류가 발생하지 않는다.
  • 그러나 컬렉션 형식을 구체적으로 명시하고 싶을 떄도 있다. 극단적인 예는 다음처럼 인터페이스 형식의 컬렉션을 사용하는 것이다.
[DataMember] public IList<Address> Addresses;
  • 직렬화에서는 컬렉션이 이전과 마찬가지로 잘 직렬화된다. 그러나 역직렬화를 수행하면 문제가 발생한다. 역직렬화기는 이를 인스턴스화할 구체적인 형식을 알지 못하므로 가장 간단한 옵션인 배열을 선택하는데, 이는 프로그래머의 의도와는 동떨어진 선택이다. 해당 필드를 다음처럼 다른 구체 형식으로 초기화했을 때도 역직렬화기는 같은 전략을 적용한다.
[DataMember] public IList<Address> Addresses = new List<Address>();
  • 역직렬화 시 직렬화기가 필드 초기치 절을 건너뜀을 기억하기 바란다. 해결책은 자료 멤버를 전용 필드로 만들고 그것에 접근하는 공용 속성을 추가하는 것이다.
[DataMember (Name="Addresses")] List<Address> _addresses;
public IList<Address> Addresses { get { return _addresses; } }
  • 어차피 본격적인 응용 프로그램에서는 이런 방식을 사용할 것이다. 여기서 어색한 부분은 공용 속성이 아니라 전용 필드를 자료 멤버로 둔다는 점뿐이다.

파생 형식 컬렉션 요소

  • 직렬화기는 컬렉션의 파생 형식 요소들을 투명하게 처리한다. 단, 파생 형식이 관여하는 다른 경우에서처럼 해당 파생 형식을 명시해 주어야 한다.
[DataContract, KnownType(typeof(USAddress))] 
public class Address
{ 
  [DataMember] public string Street, Postcode;
}

public class USAddress : Address { }
  • USAddress 인스턴스를 Person의 주소 목록에 추가해서 직렬화하면 다음과 같은 형태의 XML이 나온다.
...
<Addresses>
  <Address i:type="USAddress">
    <Postcode>6020</Posecode>
    <Street>Odo St</Street>
  </Address>
</Addresses>

컬렉션과 요소 이름의 커스텀화

  • 컬렉션 클래스 자체를 파생한 경우, 컬렉션의 요소를 서술하는 XML 요소의 이름을 CollectionDataContract라는 특성을 이용해서 커스텀화할 수 있다.
[CollectionDataContract(ItemName="Residence")]
public class AddressList : Collection<Address> { }

[DataContract] public class Person
{ 
  ...
  [DataMember] public AddressList Addresses;
}
  • 이 클래스를 직렬화한 결과는 다음과 같은 모습이다.
...
<Addresses>
  <Residence>
    <Postcode>6020</Posecode>
    <Street>Odo St</Street>
  </Residence>
  ...
  • CollectionDataContract로 컬렉션 자체의 이름공간과 이름을 지정할 수도 있다. (각각 Namespace 매개변수와 Name 매개변수). 컬렉션 이름은 다른 객체의 한 속성으로 존재하는 컬렉션(지금 에제에서처럼)을 직렬화할 떄에는 적용되지 않고, 컬렉션을 뿌리 객체로서 직렬화할 때 적용된다.
  • 사전(dictionary)의 직렬화 방식을 제어하는데도 CollectionDataContract를 사용할 수 있다.
[CollectionDataContract(ItemName="Entry", KeyName="Kind", ValueName="Number")]
public class PhoneNumberList : Dictionary<string, string> { }

[DataContract] public class Person
{ 
  ...
  [DataMember] public PhoneNumberList PhoneNumbers;
}
  • 이를 직렬화하면 다음과 같은 형태의 결과가 나온다.
...
<PhoneNumbers>
  <Entry>
    <Kind>Home</Kind>
    <Number>08 1234 5678</Number>
  </Entry>
  <Entry>
    <Kind>Mobile</Kind>
    <Number>040 8675 4321</Number>
  </Entry>
</PhoneNumbers>

자료 계약의 확장

직렬화 및 역직렬화 확장점

  • 다음 특성들을 이용하면 직려로하 직전 또는 직후에 커스텀 메서드가 실행되게 할 수 있다.
    • [OnSerializing]
      • 직렬화 직전에 호출될 메서드를 지정한다.
    • [OnSerialized]
      • 직렬화 직후에 호출될 메서드를 지정한다.
  • 역직렬화 역시 비슷한 특성들을 지원한다.
    • [OnDeserializing]
      • 역직렬화 직전에 호출될 메서드를 지정한다.
    • [OnDeserialized]
      • 역직렬화 직후에 호출될 메서드를 지정한다.
  • 이러한 커스텀 메서드들은 반드시 StreamingContext 형식의 매개변수 하나를 받는 함수이어야 한다. 이 매개변수는 이진 엔진과의 일관성에 필요한 것으로 자료 계약 직렬화기는 이를 사용하지 않는다.
  • [OnSerializing]과 [OnDeserialized]는 자료 계약 엔진의 능력 밖에 있는 멤버들을 다룰 때 이를테면 추가적인 자료를 가진 컬렉션을 처리하거나 표준 인터페이스를 구현하는 것이 아닌 컬렉션을 처리할 떄 유용하다. 그런 용도로 이 특성들을 활용할 때의 틀은 다음과 같다.
[DataContract] public class Person
{
  public 직렬화와_잘_맞지_않는_형식 Addresses;

  [DataMember (Name="Addresses")]
  직렬화에_잘_맞는_형식 _serializationFriendlyAddresses;

  [OnSerializing]
  void PrepareForSerialization(StreamingContext sc)
  {
    // 여기서 Addresses를 _serializationFriendlyAddresses에 복사
    // ...
  }

  [OnDeserialized]
  void CompleteDeserialization(StreamingContext sc)
  {
    // 여기서 _serializationFriendlyAddresses를 Addresses에 복사
    // ...
  }
}
  • [OnSerializing] 특성의 또 다른 용도는 필드들을 선택적으로 직렬화하는 것이다.
public DateTime DateOfBirth;

[DataMember] public bool Confidential;

[DataMember (Name="DateOfBirth", EmitDefaultValue=false)]
DateTime? _tempDateOfBirth;

[OnSerializing]
void PrepareForSerialization(StreamingContext sc)
{
  if (Confidential)
    _tempDateOfBirth = DateOfBirth;
  else
    _tempDateOfBirth = null;
}
  • 역직렬화시 자료 계약 직렬화기가 필드 초기치 절과 생성자를 건너뛴다는 점을 기억할 것이다. [OnDeserializing]은 역직렬화를 위한 유사 생성자(pseudoconstructor)로 작용하며 따라서 직렬화에서 제외된 필드들의 초기화에 유용하다.
[DataContract] public class Test
{
  bool _editable = true;
  public Test() { _editable = true; }

  [OnDeserializing]
  void Init(StreamingContext sc)
  {
    _editable = true;
  }
}
  • Test 클래스는 _editable을 true로 초기화하기 위해 두 가지 방법(생성자와 필드 초기화)를 동원하지만 둘 다 역직렬화 시에는 작동하지 않는다. 역직렬화 시 _editable을 true로 만드는 것은 전적으로 Init 메서드의 몫이다.
  • 이 네 특성은 전용 메서드에도 적용할 수 있다. 파생 형식이 관여하는 경우 파생 형식 고유의 커스텀 메서드들에도 이 특성들을 부여할 수 있으며, 직렬화/역직렬화시 그 메서드들도 호출된다.

[Serializable] 지원

  • 자료 계약 직렬화기는 이진 직렬화 엔진의 특성들과 인터페이스들이 지정된 형식도 직렬화할 수 있다. .NET 프레임워크 3.0 이전에 작성된 상당한 분량의 구성요소들(.NET 프레임워크 자체도 포함해서)에 이진 엔진 지원 코드가 포함되어 있다는 점에서 이는 중요한 능력이다.
  • 주어진 형식이 이진 엔진으로 직렬화할 수 있는 형식임을 나타내는 수단은 다음 두 가지이다.
    • [Serializable] 특성 부여
    • ISerializable 인터페이스 구현
  • 이러한 이진 엔진과의 상호운용성은 기존 형식을 직렬화할 때는 물론이고 두 엔진은 모두 지원해야 하는 새 형식을 만들 때에도 유용하다.
    • 또한 이진 엔진의 ISerializable이 자료 계약 직렬화기 특성들보다 유연하다는 점에서, 이는 자료 계약 직렬화기의 능력을 확장하는 또 다른 수단이다.
    • 안타까운 점은 ISerializable을 통해서 추가된 자료를 서식화하는 자료 계약 직렬화기의 기능이 그리 효율적이지 않다는 것이다.
  • 두 엔진의 장점을 모두 취하기 위해 한 형식에 두 엔진의 특성들을 모두 지정하는 것은 해결책이 아니다.
    • string이나 DateTime 같은 형식은 역사적인 이유로 이진 직렬화 엔진의 특성들을 떼어낼 수 없기 때문에 문제가 생긴다.
    • 자료 계약 직렬화기는 그런 기본 형식들에 특별한 처리를 적용함으로써 이 문제를 피해 간다.
  • 그 외의 이진 직렬화 대상 형식들에 대해 자료 계약 직렬화기는 이진 엔진이 사용했을 규칙과 비슷한 규칙을 적용한다.
    • 예컨대 NonSerialized 같은 특성들을 반영하며, ISerializable 구현의 경우 해당 메서드들도 적절히 호출한다. 그렇다고 자료 계약 직렬화기가 이진 엔진을 그대로 흉내 내는 것은 아니다. 즉, 직렬화 결과는 자료 계약 특성들을 사용했을 때와 같은 스타일로 서식화된다.
  • 이진 엔진으로 직렬화되도록 설계된 형식들은 기본적으로 객체 참조가 유지된다고 가정한다. DataContractSerializer에서는 생성자의 선택적 매개변수를 통해서 객체 참조를 유지할 수 있다(또는 NetDataContractSerializer를 사용할 수도 있다)
  • 이진 인터페이스를 통해서 직렬화되는 객체와 파생 형식 객체들에도 알려진 형식의 등록에 관한 규칙들이 적용된다.
  • 다음은 [Serializable] 자료 멤버가 있는 클래스의 예이다.
[DataContract] public class Person
{
  ...
  [DataMember] public Address MailingAddress;
}

[Serializable] public class Address
{
  public string Postcode, Street;
}
  • 다음은 이를 직렬화한 예이다.
<Person ...>
  ...
  <MailingAddress>
      <Postcode>6020</Posecode>
      <Street>Odo St</Street>
  </MailingAddress>
  ...
  • Address가 ISerializable을 구현했다면 다음처럼 덜 효율적인 형태의 결과가 나올 것이다.
<MailingAddress>
  <Street xmlns:d3p1="http://www.w3.org/2001/XMLSchema" i:type="d3p1:string" xmlns="">str</Street>
  <Postcode xmlns:32p1="http://www.w3.org/2001/XMLSchema" i:type="d3p1:string" xmlns="">pcdoe</Postcode>
</MailingAddress>

IXmlSerializable 지원

  • 자료 계약 직려로하기의 한 가지 한계는 출력 XML의 구조를 제어할 수 있는 여지가 거의 없다는 점이다. WCF 응용 프로그램에서는 표준 메시징 프로토콜들의 요구를 준수하기 쉽다는 점에서 이것이 장점일 수 있다.
  • XML을 정교하게 제어해야 한다면, 한 가지 방법은 IXmlSerializable을 구현하고 XmlReader와 XmlWriter를 이용해서 XML을 직접 읽고 쓰는 것이다. 자료 계약 직렬화기에서는 그럴 필요가 있는 개별 형식 수준에서 그런 방법을 적용할 수 있다.

이진 직렬화기

  • 이진 직렬화 엔진은 Remoting이 내부적으로 사용한다. 또한 일반 응용 프로그램에서 객체들은 디스크에 저장하고 복원하는 용도로 이진 직렬화기를 직접 사용하는 것도 가능하다.
    • 이진 직렬화는 고도로 자동화되어 있어서 프로그래머가 거의 개입하지 않아도 복잡한 객체 그래프를 처리할 수 있다.
    • 단 Windows 스토어 앱에서는 이진 직렬화기를 사용할 수 없다.
  • 어떤 형식이 이진 직렬화를 지원하게 만든느 방법은 두 가지이다. 하나는 적절한 특성들을 적용하는 것이고 다른 하나는 ISerializable을 구현하는 것이다.
    • 특성 적용은 간단하다는 장점이 있고, ISerializable 구현은 유연하다는 장점이 있다.
  • ISerializable을 구현하는 목적은 크게 다음 두 가지이다.
    • 직렬화 대상들을 동적으로 선택한다.
    • 직렬화 가능 형식을 다른 사람들이 상속해서 사용하기 쉽게 만든다.

간단한 방법

  • 기존 형식에 특성 하나만 추가하면 직렬화 가능 형식이 된다.
[Serializable] public sealed class Person
{ 
  public string Name;
  public int Age;
}
  • 이진 직렬화기는 [Serializable] 특성이 부여된 형식의 모든 필드를 직렬화한다. 공용 필드 뿐만 아니라 전용 필드들도 직렬화에 포함된다. (속성들은 포함되지 않는다.)
    • 단 모든 필드가 반드시 직렬화가 가능한 형식이어야 하며, 그렇지 않으면 예외가 발생한다.
    • string과 int 같은 기본 .NET 형식들은 직렬화를 지원한다.
  • [Serializable] 특성은 상속되지 않으므로, [Serializable]이 적용된 클래스를 상속하는 파생 클래스가 자동으로 직렬화를 지원하지는 않는다. 직렬화가 가능하려면 파생 형식에도 명시적으로 [Serializable]을 추가해야 한다.
    • 클래스에 자동 속성이 있는 경우, 이진 직렬화 엔진은 컴파일러가 그런 속성을 위해 내부적으로 생성한 필드도 직렬화한다.
    • 안타깝게도 형식을 다시 컴파일하면 그런 필드의 이름이 바뀔 수 있으며, 그러면 이전에 직렬화된 자료와의 호환성이 깨진다.
    • 우회책은 [Serializable] 특성이 있는 형식에는 자동 속성을 사용하지 않거나, 특성을 사용하는 대신 ISerializable을 구현하는 것이다.
  • Person 인스턴스를 직렬화하는 방법은 간단하다. 적절한 포매터를 인스턴스화 해서 Serialize를 호출하면 된다. 이진 직렬화 엔진에 사용할 수 있는 포매터는 기본적으로 다음 두 가지이다.
    • BinaryFormatter
      • 이 포매터는 더 작은 크기의 출력을 더 짧은 시간에 산출한다는 점에서 다음의 포매터보다 더 효율적이다.
    • SoapFormatter
      • Remoting과 함께 사용하는 경우 이 포매터는 기본 SOAP 스타일 메시징을 지원한다.
  • SoapFormatter는 BinaryFormatter 보다 기능이 적다. SoapFOrmatter는 제네릭 형식을 지원하지 않으며 여분의 자료를 건너 뛰는 기능이 없어서 버전 내구성을 갖추기가 어렵다.
  • 방금 말한 차이점들 외에 사용법 자체는 두 포매터가 동일하다. 다음은 BinaryFormatter를 이용해서 Person을 직렬화하는 예이다.
Person p = new Person() { Name = "George", Age = 25 };

IFormatter formatter = new BinaryFormatter();

using (FileStream s = FIle.Create("serialized.bin"))
  formatter.Serialize(s, p);
  • serialized.bin 파일에는 나중에 Person 객체를 재구축하는데 필요한 모든 자료가 기록된다. 객체를 복원하는 메서드는 Deserialize이다.
using (FileStream s = FIle.OpenRead("serialized.bin"))
{
  Person p2 = (Person)formatter.Deserialize(s);
  Console.WriteLine(p2.Name + " "  + p2.Age);  // George 25
}
  • 역직렬화 메서드는 객체를 재국축할 때 모든 생성자를 건너뛴다. 내부적으로 역직렬화 메서드는 FormatterServices.GetUninitializedObject라는 메서드를 호출해서 객체를 생성한다. 아주 지저분한 설계 패턴을 구현하는 경우에는 이 메서드를 직접 호출해야 할 수도 있다.
  • 직렬화된 자료에는 형식과 어셈블리에 관한 완전한 정보가 포함되므로, 역직렬화의 결과를 다른 어셈블리에 있는 같은 구조의 Person 형식으로 캐스팅하려 하면 예외가 발생한다.
    • 역직렬화시 직렬화기는 객체 참조들도 원래 상태로 완전히 복원한다. 여기에는 컬렉션들도 포함되는데, 직렬화기는 컬렉션도 다른 모든 직려로하 가능 객체들과 동일하게 취급한다. (System.Collections.*의 모든 컬렉션 형식은 직렬화 가능 형식이다)
  • 이진 직렬화 엔진은 프로그래머의 특별한 도움 없이도 크고 복잡한 객체 그래프를 처리할 수 있다(관여하는 모든 멤버가 직렬화 가능 형식이어야 한다는 점만 보장해 주면 된다)
    • 한 가지 주의할 점은 이 직렬화기의 성능은 객체 그래프에 있는 참조의 개수에 비례해서 떨어진다는 점이다. 다수의 요청을 동시에 처리해야 하는 Remoting 서버에서는 이것이 문제가 될 수 있다.

이진 직렬화 특성들

[NonSerialized] 특성

  • 자료 계약 직렬화기는 직렬화를 명시적으로 요청한 필드들만 직렬화하는 명시적 선택(opt-in) 정책을 사용하지만, 이진 직렬화 엔진은 그 반대인 암묵적 선택(opt-out) 정책을 사용한다.
    • 즉, 이진 직렬화에서는 기본적으로 모든 필드가 직렬화되며, 직렬화하지 않을 필드들은 명시적으로 지정해야 한다.
    • 예컨대 임시 계산에 쓰이는 필드나 파일 핸들 또는 창(윈도) 핸들을 저장하는 필드 등 직렬화할 필요가 없는 필드에는 명시적으로 [NonSerialized] 특성을 붙여야 한다.
[Serializable] public sealed class Person
{ 
  public string Name;
  public DateTime DateOfBirth;

  // 나이는 생년월일로 계산할 수 있으므로, Age 필드는 직렬화할 필요가 없다.
  [NonSerialized] public int Age;
}
  • 직렬화되지 않은 멤버는 역직렬화시 항상 (다른 값을 설정하는 필드 초기치나 생성자가 있다고 해도) 빈 값 또는 null로 초기화 된다.

[OnDeserializing] 특성과 [OnDeserialized] 특성

  • 역직렬화 시 이진 직렬화기는 모든 보통의 생성자와 필드 초기치 절을 무시한다. 모든 필드를 직렬화하는 경우에는 이것이 문제가 되지 않지만, 일부를 [NonSerialized]로 제외할 때는 문제의 여지가 있다. 예컨대 Person에 Valid라는 bool 필드를 추가한다고 하자.
[Serializable] public sealed class Person
{ 
  public string Name;
  public DateTime DateOfBirth;

  [NonSerialized] public int Age;
  [NonSerialized] public bool Valid = true;

  public Person() { Valid = true; }
}
  • 그런데 Person 인스턴스를 직렬화했다가 역직렬화하면 생성자나 필드 초기치 절이 작동하지 않아서 Valid가 true로 설정되지 않는다.
    • 해결책은 자료 계약 직렬화기에서와 같다. 즉, [OnDeserializing] 특성을 이용해서 역직렬화를 위한 특별한 ‘유사 생성자’를 지정하는 것이다. 이 특성으로 지정하는 메서드는 역직렬화 직전에 호출된다.
[OnDeserializing]
void OnDeserializing(StreamingContext context)
{
  Valid = true;
}
  • 또 다른 예로 [OnDeserialized] 특성을 다음과 같은 메서드에 부여해서 역직렬화 시 Age 필드가 계산되게 할 수도 있다. (이 특성을 부여한 메서드는 역직렬화 직후에 호출된다.)
[OnDeserialized]
void OnDeserialized(StreamingContext context)
{
  TimeSpan ts = DateTime.Now = DateOfBirth;
  Age = ts.Days / 365;
}

[OnSerializing] 특성과 [OnSerialized] 특성

  • 이진 직렬화 엔진은 [OnSerializing]과 [OnSerialized] 특성도 지원한다. 이 특성들은 직렬화 직전 또는 직후에 호출될 메서드를 지정하는데 쓰인다. 이들의 용도를 보여주는 예로 선수들의 제네릭 List가 있는 Team이라는 클래스를 생각해 보자.
[Serializable] public class Team
{ 
  public string Name;
  public List<Person> Players = new List<Person>();
}
  • 이 클래스는 이진 포매터로는 정확히 직렬화/역직렬화되지만 SOAP 포매터로는 잘 안된다. 이는 SOAP 포매터가 제네릭 형식의 직렬화를 거분한다는 다소 애매한 한계 때문이다.
    • 한 가지 간단한 해결책은 직렬화 직전에 Players를 배열로 변환하고, 역직렬화 직후에는 다시 제네릭 List로 변환하는 것이다.
    • 이러한 과정을 원활하게 진행하기 위해, 배열을 저장하는 필들르 하나 추가하고, 원래의 Players 필드에 [NonSerialized] 특성을 적용한다. 또한 변환을 위한 메서드들도 추가한다.
[Serializable] public sealed class Team
{ 
  public string Name;
  Person[] _playersToSerialize;

  [NonSerialized]  public List<Person> Players = new List<Person>();

  [OnSerializing]
  void OnSerializing(StreamingContext context)
  {
    _playersToSerialize = Players.ToArray();
  }

  [OnSerialized]
  void OnSerialized(StreamingContext context)
  {
    _playersToSerialize = null;  // 메모리가 해제될 수 있게 한다.
  }

  [OnDeserialized]
  void OnDeserialized(StreamingContext context)
  {
    Players = new List<Person> (_playersToSerialize);
  }
}

[OptionalField] 특성과 버전 관리

  • 기본적으로 이진 직렬화기의 경우 형식에 필드를 추가하면 이미 직렬화된 자료와의 호환성이 깨진다. 이를 피하려면 새 필드에 [OptionalField] 특성을 추가해야 한다.
  • Person 클래스를 예로 들겠다. 우선 처음에는 이 클래스가 다음과 같이 필드 하나로만 되어 있었다고 하자. 이를 버전 1이라고 부르기로 한다.
[Serializable] public sealed class Person  // 버전 1
{ 
  public string Name;
}
  • 나중에 생년월일을 저장해야 할 일이 생겨서 또 다른 필드를 추가했다. 이를 버전2라고 하자.
[Serializable] public sealed class Person  // 버전 2
{ 
  public string Name;
  public DateTime DateOfBirth;
}
  • 만일 두 컴퓨터가 Remoting을 통해서 Person 객체를 교환한다면, 두 컴퓨터 모두 정확히 같은 시기에 버전 2로 갱신한다는 엄격한 조건을 만족하지 않는 한 역직렬화가 제대로 일어나지 않을 것이다. [OptionalField] 특성을 이용하면 그런 조건을 느슨하게 만들 수 있다.
[Serializable] public sealed class Person  // 버전 2 (안정적)
{ 
  public string Name;
  [OptionalField (VersionAdded = 2)] public DateTime DateOfBirth;
}
  • 이렇게 하면 역직렬화시 이진 직렬화기는 자료 스트림에 DateOfBirth가 없어도 당황하지 않고 그것을 그냥 직려로하되지 않은 필드로 간주한다.
    • 결과적으로 DateOfBirth 필드는 빈 DateTime 인스턴스로 초기화된다(다른 값을 원한다면 [OnDeserializing] 특성을 사용하면 된다.)
  • VersionAdded 매개변수는 해당 필드가 추가된 버전의 번호를 뜻한다. 이것은 일종의 문서화 역할을 하며, 직렬화 방식에는 아무런 영향을 미치지 않는다.
  • 버전 내구성이 중요하다면 필드의 이름을 바꾸거나 필드를 삭제하는 일은 피해야 하며, 뒤늦게 Nonserialized 특성을 추가하지도 말아야 한다. 또한 필드의 형식을 바꾸는 일도 반드시 피해야 한다.
  • 지금까지의 논의는 하위 호환성 문제, 즉 직렬화된 스트림으로부터 객체를 복원할 때 객체의 특정 필드에 해당하는 값이 스트림에 없어서 생기는 문제에 초점을 두었다.
    • 그런데 직렬화는 기본적으로 양방향 통신이므로, 상위 호환성 문제, 즉 역직렬화 과정에서 직렬화 스트림에 있는 자료를 어디에 써먹어야 할지 알 수 없는 문제도 발생하 ㄹ수 있다.
    • 이진 포매터는 기대치 않은 여분의 자료를 그냥 폐기함으로써 이 문제를 자동으로 해소한다.
    • 반면 SOAP 포매터는 그런 경우 예외를 던진다.
    • 따라서 만일 양방향 버전의 내구성이 필요하다면 반드시 이진 포매터를 사용해야 한다. 그것이 여의치 않담녀 ISerializable을 구현해서 직렬화를 직접 제어하는 방법도 있다.

ISerializable 구현을 통한 이진 직렬화

  • ISerializable을 구현하는 접근방식의 장점은 주어진 형식의 이진 직렬화/ 역직렬화 방식을 완전히 제어할 수 있다는 것이다.
  • 다음은 ISerializable 인터페이스의 정의이다.
public interface ISerializable
{
  void GetObjectData(SerializtionInfo info, StreamingContext context);
}
  • GetObjectData는 직렬화시 호출된다. 이 메서드에서 할 일은 직렬화하고자 하는 모든 필드를 SerializationInfo 객체에 채우는 것이다.
  • 다음은 Name과 DateOfBirth라는 두 필드를 직렬화하도록 구현된 GetObjectData 메서드의 예이다.
public virtual void GetObjectData(SerializtionInfo info, StreamingContext context)
{
  info.AddValue("Name", Name);
  info.AddValue("DateOfBirth", DateOfBirth);
}
  • 이 예에서는 각 항목의 이름을 해당 필드의 이름과 동일하게 지정했다. 그런데 꼭 그렇게 해야 하는 것은 아니다. 역직렬화에 쓰이는 이름과 일치하기만 한다면 그 어떤 이름을 사용해도 된다.
    • 그리고 각 사전 항목의 값은 반드시 직렬화가 가능한 형식이어야 하며, 직렬화가 가능한 형식이면 어떤 것이라도 값으로 사용할 수 있다. 필요하면 .NET Framework가 그 값을 재귀적으로 직렬화한다.
    • 널 값도 가능하다. 사전에 널 값을 저장하는 것은 위법이 아니다.
  • 해당 클래스가 sealed가 아닌 한, GetObjectData를 virtual로 만드는 것이 바람직하다. 그렇게 하면 파생 클래스들이 인터페이스를 다시 구현하지 않고도 직렬화 기능을 확장할 수 있다.
  • SerializationInfo 매개변수에는 역직렬화 시 객체에 적용할 형식과 어셈블리를 제어하는데 사용할 수 있는 속성들도 있다.
    • StreamingContext 매개변수는 여러 필드로 이루어진 구조체인데, 특히 직렬화된 인스턴스들이 기록 또는 전송될 장소(디스크, Remoting 등)를 가리키는 열거형 필드가 있다(단, 이 필드가 항상 채워지는 것은 아니다)
  • 주어진 형식의 직렬화 방식을 세세하게 제어하려면 ISerializable을 구현하는 것 말고도 할 일이 있다. 바로 GetObjectData와 동일한 두 매개변수를 받는 역직렬화용 생성자를 정의하는 것이다.
    • 이 생성자의 접근성(private, public 등)은 어떤 것으로 선언해도 된다. 어떻게 지정하든 런타임이 이 생성자를 찾아내서 호출한다.
    • 그러나 일반적으로는 protected로 선언하는 것이 바람직하다. 그러면 파생 클래스에서 그 생성자를 호출할 수 있기 때문이다.
  • 다음은 Team 클래스에서 ISerializable을 구현한 예이다.
    • 선수 목록을 제네릭 List가 아니라 배열로 취급해서 직렬화한다는 점을 주목하기 바란다. 이렇게 하면 SOAP 포매터도 사용할 수 있다.
[Serializable] public class Team : ISerializable
{
  public string Name;
  public List<Person> Players;

  public virtual void GetObjectData(SerializationInfo si, StreamingContext sc)
  {
    si.AddValue("Name", Name);
    si.AddValue("PlayerData", Players.ToArray());
  }

  public Team() { }

  protected Team (SerializationInfo si, StreamingContext sc)
  {
    Name = si.GetString("Name");

    // 직렬화와 부합하도록, Players를 배열로 역직렬화한다.
    Person[] a = (Person[])si.GetValue("PlayerData", typeof(Person[]));

    // 배열을 이용해서 새 List를 생성한다.
    Players = new List<Person>(a);
  }
}
  • SerializationInfo 클래스에는 흔히 쓰이는 형식들에 특화된 ‘Get’ 메서드들이 있다. 이를테면 GetString이 그런 메서드이다. 이들을 이용하면 역직렬화용 생성자를 좀 더 쉽게 작성할 수 있다.
    • 만일 주어진 이름에 해당하는 자료가 존재하지 않으면 그런 ‘Get’ 메서드들은 예외를 던진다.
    • 직렬화와 역직렬화를 수행하는 코드들의 버전이 일치하지 않을 때 흔히 그런 일이 발생한다. 예컨대 형식에 필드를 하나 추가하고는 역직렬화 코드에서 새 필드에 맞게 갱신하는 것을 잊어 버릴 수 있다.
    • 이런 문제를 피하는 방법을 두 가지 들자면 다음과 같다.
      • 이후 버전에서 추가된 자료 멤버를 가져오는 코드를 예외 처리 블록으로 감싼다.
      • 자신만의 버전 관리 시스템을 구현한다. 이를테면 다음과 같다.
public string MyNewField;

public virtual void GetObjectData(SerializationInfo si, StreamingContext sc)
{
  si.AddValue("_version", 2);
  si.AddValue("MyNewField", MyNewField);
  ...
}

protected Team(SerializationInfo si, StreamingContext sc)
{
  int version = si.GetInt32("_version");
  if (version >= 2) MyNewField = si.GetString("MyNewField);
  ...
}

직렬화 가능 클래스의 파생

  • 앞의 예제들에서는 특성들을 지정해서 직렬화 가능 클래스를 만들 때 클래스에 sealed 적용해서 이후의 상속을 금지했다. 그 이유를 설명하기 위해 다음과 같은 클래스 계통 구조를 생각해 보자.
[Serializable] public class Person
{
  public string Name;
  public int Age;
}

[Serializable] public class Student : Person
{
  public string Course;
}
  • 이 예의 Person과 Student는 둘 다 직렬화 가능 형식이다. 그리고 둘 다 ISerializable을 구현하지 않으므로 둘 다 기본 런타임 직렬화 행동방식이 적용된다.
  • 그런데 Person의 작성자가 어떻나 이유로 마음을 바꾸어서 ISerializable을 구현했으며, Person의 직렬화/역직렬화를 제어하기 위해 역직렬화용 생성자를 추가했다고 하자. Pserson의 새 버전은 다음과 같은 모습이다.
[Serializable] public class Person : ISerializable
{
  public string Name;
  public int Age;

  public virtual void GetObjectData(SerializationInfo si, StreamingContext sc)
  {
    si.AddValue("Name", Name);
    si.AddValue("Age", Age);
  }

  protected Person (SerializationInfo si, StreamingContext sc)
  {
    Name = si.GetString("Name");
    Age = si.GetInt32("Age");
  }

  public Person() { }
}
  • Person 인스턴스들은 여전히 잘 직렬화/역직렬화되지만 Student 인스턴스들은 그렇지 않다. 구체적으로 말하면, Student 인스턴스를 직렬화하면 Course 필드가 스트림에 기록되지 않는다.
    • Person의 ISerializable.GetObjectData 구현은 Person의 멤버들만 알 뿐 그로부터 파생된 Student의 멤버들은 알지 못하기 때문이다.
    • 게다가 Student 인스턴스를 역직렬화할 때 예외가 발생한다. 역직렬화 과정에서 런타임은 Student의 역직렬화용 생성자를 호출하려 하지만 그런 생성자를 찾지 못하기 때문이다.
  • 이 문제를 해결하려면 애초에 직렬화 가능 클래스 계통 구조를 만들기 시작할 때부터 ISerializable을 구현해야 한다. 그리고 그 클래스들을 public으로 선언하되 sealed로 선언하면 안 된다.(internal 클래스에서는 이 문제가 그리 중요하지 않다. 필요하다면 나중에라도 파생 클래스들을 쉽게 수정할 수 있기 때문이다)
  • 처음부터 Person을 앞의 예처럼 작성했다면 Student는 다음과 같이 작성했을 것이다.
[Serializable] public class Student : Person
{
  public string Course;

  public override void GetObjectData(SerializationInfo si, StreamingContext sc)
  {
    base.GetObjectData(si, sc);
    si.AddValue("Course", Course);
  }

  protected Student (SerializationInfo si, StreamingContext sc) : base (si, sc)
  {
    Course = si.GetString("Course");
  }

  public Student() { }
}

XML 직렬화

  • .NET Framework는 XML 전용 직렬화 엔진인 XmlSerializer를 제공한다. 이 클래스는 .NET 형식들을 XML 파일로 직렬화하는데 적합하다. 그리고 ASMX Web Services가 내부적으로 이 엔진을 사용한다.
  • 이진 직렬화 엔진처럼, 이 엔진을 사용하는 접근방식은 두 가지이다.
    • 형식들과 멤버들에 XML 직렬화 특성들을 부여한다.
    • IXmlSerializable 인터페이스를 구현한다.
  • 그러나 이진 직렬화 엔진과 다른 점도 있다. IXmlSerializable 인터페이스를 구현하는 경우에는 직렬화/역직렬화 과정에서 엔진이 완전히 제외된다는 점이다.
    • 즉, 그런 경우에는 XmlReader와 XmlWriter를 이용해서 독자가 직접 직렬화/역직렬화 기능을 구현해야 한다.

특성 기반 직렬화의 기초

  • XmlSerializer를 사용하는 방법은 간단하다. XmlSerializer 인스턴스를 생성한 후 Stream 객체와 직렬화할 객체로 Serialize와 Deserialize를 호출하면 된다.
    • 예컨대 다음과 같은 클래스가 있다고 하자.
public class Person
{
  public string Name;
  public int Age;
}
  • 다음은 이 Person의 한 인스턴스를 XML 파일에 저장했다 복원하는 예이다.
Person p = new Person();
p.Name = "Stacey";
p.Age = 30;

XmlSerializer xs = new XmlSerializer(typeof(Person));

using (Stream s = File.Create("person.xml"))
  xs.Serialize(s, p);

Person p2;
using (Stream s = File.OpenRead("person.xml"))
  p2 = (Person)xs.Deserialize(s);

Console.WriteLine(p2.Name + " " + p2.Age);  // Stacey 30
  • Serialize와 Deserialize는 Stream 뿐만 아니라 XmlWriter/XmlReader나 TextWriter/TextReader도 지원한다. 다음은 위의 코드가 산출한 XML이다.
<?xml version="1.0"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>Stacey</Name>
  <Age>30</Age>
</Person>
  • 앞의 예에서 Person 형식에 그 어떤 직렬화 특성도 부여하지 않았음을 주목하기 바란다. 이처럼 XmlSerializer는 특성이 전혀 없는 형식도 직렬화할 수 있다.
  • 기본적으로 XmlSerializer는 주어진 형식의 모든 공용 필드와 속성을 직렬화한다.
    • 직렬화하고 싶지 않은 멤버가 있으면 다음 예처럼 XmlIgnore 특성을 지정해 주면 된다.
public class Person
{
  ...
  [XmlIgnore] public DateTime DateOfBirth;
}
  • 다른 두 엔진과는 달리 XmlSerializer는 [OnDeserializing] 특성을 인식하지 않는다. 대신 역직렬화를 시작하기 전에 XmlSerializer는 매개변수 없는 생성자를 호출해서 필드들의 초기화를 위임한다. 만일 그런 생성자가 없으면 예외를 던진다.
    • 앞의 예에서 Person에는 매개변수 없는 생성자가 암묵적으로 존재한다.
    • 또한 XmlSerializer는 역직렬화 전에 필드 초기치들도 적용한다.
public class Person
{
  public bool Valid = true;  // 역직렬화 전에 실행된다.
}
  • XmlSerializer는 거의 모든 형식을 직렬화할 수 있으나, 모든 형식을 동일하게 직렬화하는 것은 아니다. XmlSerializer는 다음과 같은 형식들을 인식해서 특별하게 취급한다.
    • 기본 형식들과 DateTime, TimeSpan, Guid 그리고 이들의 널 가능 버전들
    • byte[] (Base64로 부호화됨)
    • XmlAttribute 또는 XmlElement(그 내용을 스트림에 주입함)
    • IXmlSerializable을 구현하는 모든 형식
    • 모든 컬렉션 형식
  • XML 직렬화기의 역직렬화 기능은 버전 내구성을 갖추고 있다. 즉, 특정 필드나 속성에 해당하는 자료가 없어도 그리고 기대치 않은 여분의 자료가 있어도 불평하지 않는다.

XML 특성, 이름, 이름공간

  • 기본적으로 필드와 속성은 XML 요소로 직렬화 된다. 만일 특정 필드나 속성을 요소가 아니라 XML 특성으로 직렬화하고 싶으면 다음과 같이 하면 된다.
[XmlAttribute] public int Age;
  • 또한 XML 요소나 특성의 이름을 명시적으로 지정할 수도 있다.
public class Person
{
  [XmlElement("FirstName")] public string Name;
  [XmlAttribute("RoughAge")] public int Age;
}
  • 이러한 클래스의 인스턴스를 직렬화하면 다음과 같은 형태의 XML이 출력된다.
<Person RoughAge="30"...>
  <FirstName>Stacey</FirstName>
</Person>
  • 기본 XML  이름공간은 비어 있다. (형식의 이름공간을 기본 XML 이름공간으로 사용하는 자료 계약 직렬화기와는 다른 점이다) 요소나 특성의 XML 이름공간을 지정하려면 [XmlElement]나 [XmlAttribute]의 Namespace 매개변수를 사용하면 된다.
    • 또한 형식 자체의 이름과 이름 공간을 지정하려면 [XmlRoot] 특성을 사용하면 된다.
[XmlRoot("Cadidate", Namespace="http://mynamespace/test/")]
public class Person { ... }
  • 이렇게 하면 person 인스턴스는 ‘Candidate’라는 이름의 XML 요소로 직렬화되며 그 요소와 자식 요소들에 해당 이름공간(http://mynamespace/test/)이 적용된다.

XML 요소의 순서

  • XmlSerializer는 XML 요소들을 해당 필드가 클래스에 정의된 순서로 기록한다. 그와는 다른 순서를 원한다면 XmlElement 특성의 Order 매개변수를 사용하면 된다.
public class Person
{
  [XmlElement(Order = 2)] public string Name;
  [XmlElement(Order = 1)] public int Age;
}
  • 한 멤버에 Order 매개변수를 사용했다면, 다른 모든 멤버에도 Order를 사용해야 한다.
  • 역직렬화시 XML 직렬화기는 요소들의 순서를 까다롭게 따지지 않는다. 요소들이 어떤 순서로 나타나든 객체가 적절히 복원된다.

파생 클래스와 자식 객체

뿌리 형식의 파생

  • Person 클래스가 뿌리 형식(직렬화/역직렬화가 시작되는 형식)이고, 그것을 상속하는 파생 클래스가 두 개 있다고 하자.
public class Person { public string Name; }
public class Student : Person { }
public class Teacher : Person { }
  • 그리고 뿌리 형식의 직렬화를 위해 다음과 같은 재사용 가능한 메서드를 작성했다고 하자.
public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer(typeof(Person));
  using (Stream s = File.Create(path))
    xs.Serialize(s, p);
}
  • 이 메서드가 Student나 Teacher 인스턴스에 대해서도 잘 작동하려면 파생 클래스들을 XmlSerializer에게 알려 주어야 한다. 방법은 두 가지 인데, 하나는 각 파생 클래스를 XmlInclude 특성을 이용해서 등록하는 것이다.
[XmlInclude (typeof (Student))]
[XmlInclude (typeof (Teacher))]
public class Person { public string Name; }
  • 또 다른 방법은 XmlSerializer 인스턴스를 생성할 때 두 팟행 형식을 지정하는 것이다.
XmlSerializer xs = new XmlSerializer(typeof(Person), new Type[] { typeof(Student), typeof(Teacher) });
  • 어떤 경우이든 XML 직렬화기는 해당 파생 형식을 XML 요소의 type 특성으로 기록한다(자료 계약 직렬화기와 같은 방식이다)
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Student">
  <Name>Stacey</Name>
</Person>
  • 역직렬화시 XML 직렬화기는 이 특성을 보고 Person이 아니라 Student 객체를 생성한다.
  • XML 요소의 type 특성에 배정되는 이름을 명시적으로 지정하고 싶다면 다음 예처럼 파생 형식에 [XmlType] 특성을 지정하면 된다.
[XmlType ("Candidate")]
public class Student : Person { }
  • 결과는 다음과 같다.
<Person xmlns:xsi="..." xsi:type="Candidate">

자식 객체의 직렬화

  • 프로그래머가 특별한 처리를 하지 않아도, XmlSerializer는 Person의 HomeAddress 같은 객체 참조들을 재귀적으로 처리한다.
public class Person
{
  public string Name;
  public Address HomeAddress = new Address();
}

public class Address { public string Street, PostCode; }
  • 예컨대 다음과 같은 Person 인스턴스를 직렬화 하면
Person p = new Person();
p.Name = "Stacey";
p.HomeAddress.Street = "Odo St";
p.HomeAddress.PostCode = "6020";
  • 다음과 같은 XML이 나온다.
<Person ...>
  <Name>Stacey</Name>
  <HomeAddress>
    <Street>Odo St</Street>
    <PostCode>6020</PostCode>
  </HomeAddress>
</Person>
  • 같은 객체를 참조하는 필드 또는 속성이 두 개이면 그 객체는 두 번 직렬화 된다. 참조 상등성을 유지하려면 다른 직렬화 엔진을 사용해야 한다.

자식 객체 파생

  • Person의 HomeAddress 속성이 Address의 파생 클래스를 참조할 수도 있다고 하자.
public class Address { public string Street, PostCode; }
public class USAddress : Address { }
public class AUAddress : Address { }

public class Person 
{ 
  public string Name; 
  public Address HomeAddress = new USAddress();
}
  • 이런 인스턴스를 직렬화하는 방법은 어떤 형태의 XML을 원하느냐에 따라 두 가지로 나뉜다. 만일 요소 이름은 속성 또는 필드의 이름과 같게 하고 구체적인 파생 형식의 이름은 type 특성으로 기록되게 하고 싶다면, 다시 말해 다음과 같은 형태의 XML을 원한다면
<Person ...>
  ...
  <HomeAddress xsi:type="USAddress">
    ...
  </HomeAddress>
</Person>
  • 다음처럼 Address의 각 파생 클래스를 [XmlInclude]로 등록해야 한다.
[XmlInclude(typeof(AUAddress))]
[XmlInclude(typeof(USAddress))]
public class Address { public string Street, PostCode; }
  • 그렇지 않고 요소 이름에 파생 형식 이름 자체가 반영되게 하고 싶다면, 다시 말해 다음과 같은 형태의 XML을 원한다면
<Person ...>
  ...
  <USAddress>
    ...
  </USAddress>
</Person>
  • 부모 형식의 필드나 속성에 다음처럼 여러 개의 [XmlElement] 특성을 함께 지정하면 된다.
public class Person 
{ 
  public string Name; 

  [XmlElement("Address", typeof(Address))]
  [XmlElement("AUAddress", typeof(AUAddress))]
  [XmlElement("USAddress", typeof(USAddress))]
  public Address HomeAddress = new USAddress();
}
  • 각 [XmlElement]는 하나의 요소 이름을 하나의 형식에 대응시킨다. 이 접근방식을 따르는 경우에는 Address 형식에 [XmlInclude] 특성을 부여할 필요가 없다(부여해도 직려로하에 문제가 생기지는 않는다)
  • [XmlElement]에 형식만 지정하고 요소 이름을 지정하지 않으면 형식의 기본 이름이 요소 이름으로 쓰인다(형식의 기본 이름은 [XmlType]의 영향을 받지만 [XmlRoot]의 영향은 받지 않는다.

컬렉션 직렬화

  • XmlSerializer는 구체적인 컬렉션 형식들을 자도응로 인식해서 직렬화한다. 다음과 같은 클래스들이 있다고 하자.
public class Person 
{ 
  public string Name; 
  public List<Address> Addresses = new List<Address>();
}

public class Address { public string Street, PostCode; }
  • 이를 직렬화하면 다음과 같은 구조의 XML이 나온다.
<Person ...>
  <Name>...</Name>
  <Addresses>
    <Address>
      <Street>...</Street>
      <PostCode>...</PostCode>
    </Address>
    <Address>
      <Street>...</Street>
      <PostCode>...</PostCode>
    </Address>
    ...
  </Addresses>
</Person>
  • 바깥 요소(지금 예의 Addresses)의 이름을 바꾸고 싶으면 [XmlArray] 특성을 사용하면 된다. 한편 안쪽 요소(지금 예의 Address)의 이름은 [XmlArrayItem] 특성으로 바꿀 수 있다. 예컨대 다음과 같은 클래스를 직렬화하면
public class Person 
{ 
  public string Name; 

  [XmlArray("PerviousAddresses")]
  [XmlArrayItem("Location")]
  public List<Address> Addresses = new List<Address>();
}
  • 다음과 같은 구조의 XML이 나온다.
<Person ...>
  <Name>...</Name>
  <PreviousAddresses>
    <Location>
      <Street>...</Street>
      <PostCode>...</PostCode>
    </Location>
    <Location>
      <Street>...</Street>
      <PostCode>...</PostCode>
    </Location>
    ...
  </PreviousAddresses>
</Person>
  • XmlArray 특성과 XmlArrayItem 특성으로 XML 이름공간들을 지정할 수 있다.
  • 컬렉션을 바깥쪽 요소 없이 직렬화할 수도 있다. 예컨대 다음과 같은 구조의 XML을 얻는 것이 가능하다.
<Person ...>
  <Name>...</Name>
  <Address>
    <Street>...</Street>
    <PostCode>...</PostCode>
  </Address>
  <Address>
    <Street>...</Street>
    <PostCode>...</PostCode>
  </Address>
</Person>
  • 이런 XML을 원한다면 다음처럼 명시적으로 [XmlElement]를 컬렉션 필드나 속성에 부여하면 된다.
public class Person 
{ 
  public string Name; 

  [XmlElement("Address")]
  public List<Address> Addresses = new List<Address>();
}
  • 파생 형식의 요소들을 담는 컬렉션 다루기

  • 파생 형식의 요소들을 담는 컬렉션과 관련된 규칙은 보통의 필드나 속성의 파생 형식 관련 규칙과 동일하다.
    • 즉, 파생 형식을 type 특성에 기록하고 싶으면 다시 말해 다음과 같은 형태의 XML을 원한다면
<Person ...>
  <Name>...</Name>
  <Addresses>
    <Address xsi:type="AUAddress"?
    ...
  • 이전에 했던 것처럼 기반 형식(Address)에 [XmlInclude] 특성들을 부여하면 된다. 이는 바깥쪽 요소의 직렬화 여부와는 무관하게 작동한다.
  • 파생 형식을 해당 요소 이름으로 기록하고 싶으면, 다시 말해 다음과 같은 형태의 XML을 원한다면
<Person ...>
  <Name>...</Name>
  <!--바깥쪽 요소를 포함하는 경우 여기에 해당 시작 요소가 온다-->
  <AUAddress>
    <Street>...</Street>
    <PostCode>...</PostCode>
  </AUAddress>
  <USAddress>
    <Street>...</Street>
    <PostCode>...</PostCode>
  </USAddress>
  <!--바깥쪽 요소를 포함하는 경우 여기에 해당 종료 요소가 온다-->
</Person>
  • 컬렉션 필드나 속성에 여러 개의 [XmlArrayItem] 또는 [XmlElement] 특성을 함께 부여하면 된다.
  • 이 접근 방식은 두 가지로 나뉜다. 바깥쪽 컬렉션 요소를 포함하고 싶다면 [XmlArrayItem] 특성들을 부여해야 한다.
[XmlArrayItem("Address", typeof(Address))]
[XmlArrayItem("AUAddress", typeof(AUAddress))]
[XmlArrayItem("USAddress", typeof(USAddress))]
public List<Address> Addresses = new List<Address>();
  • 반대로 바깥쪽 컬렉션 요소를 제외하고 싶다면 [XmlElement] 특성들을 부여해야 한다.
[XmlElement("Address", typeof(Address))]
[XmlElement("AUAddress", typeof(AUAddress))]
[XmlElement("USAddress", typeof(USAddress))]
public List<Address> Addresses = new List<Address>();

IXmlSerializable 인터페이스

  • 특성 기반 XML 직렬화가 유연하긴 하지만 한계들이 있다. 예컨대 직렬화 확장점을 추가할 수 없으며, 공용(public)이 아닌 멤버를 직렬화 할 수도 없다. 또한 같은 요소나 특성이 한 XML에 서로 다른 여러 방식으로 나타날 수도 있는 경우에는 코드가 복잡해진다.
  • 후자의 문제는 XmlSerializer 생성자에 적절한 XmlAttributeOverrides 객체를 지정하는 방법을 이용해서 ‘어느 정도까지는’ 해결할 수 있다. 그러나 언젠가는 그냥 명령식(imperative) 접근방식을 취하는 것이 더 쉬운 방법인 지경에 도달한다. 그럴 때 필요한 것이 IXmlSerializable 인터페이스이다.
public interface IXmlSerializable
{ 
  XmlSchema GetSchema();
  void ReadXml(XmlReader reader);
  void WriteXml(XmlWriter writer);
}
  • 이 인터페이스를 구현하면 직렬화/역직렬화시 XML을 쓰고 읽는 방식을 완전히 제어할 수 있다.
  • IXmlSerializable을 구현하는 컬렉션 클래스에는 컬렉션 직렬화에 관한 XmlSerializer의 규칙들이 적용되지 않는다.
    • 이 점은 추가 자료가 있는 컬렉션을 직렬화할 때, 다시 말해 보통의 규칙 하에서는 무시될 추가적인 필드나 속성이 있는 컬렉션을 직렬화 할 때 유용할 수 있다.
  • IXmlSerializable 구현시 따라야 할 규칙들은 다음과 같다.
    • ReadXml 메서드는 반드시 바깥쪽 시작 요소를 제일 먼저 읽고, 그 다음에 요소의 내용을 읽고, 그 다음에 바깥쪽 종료 요소를 읽어야 한다.
    • WriteXml 메서드는 내용만 기록해야 한다.
  • 다음 예를 보자.
public class Address : IXmlSerializable
{
  public string Street, PostCode;
  public XmlSchema GetSchema() { return null; }

  public void ReadXml(XmlReader reader)
  {
    reader.ReadStartElement();
    Street = reader.ReadElementContentAsString("Street", "");
    PostCode = reader.ReadElementContentAsString("PostCode", "");
    reader.ReadEndElement();
  }

  public void WriteXml(XmlWriter writer)
  {
    writer.WriteElementString("Street", Street);
    writer.WriteElementString("PostCode", PostCode);
  }
}
  • Address의 한 인스턴스를 XmlSerializer로 직렬화/역직렬화하면 자동으로 WriteXml 메서드와 ReadXml 메서드가 호출된다. 더 나아가서, 다음과 같이 정의된 Person의 인스턴스를 직렬화하면
public class Person 
{ 
  public string Name; 
  public Address HomeAddress;
}
  • XmlSerializer는 HomeAddress 필드를 직렬화할 때 선택적으로 IXmlSerializable의 메서드들을 호출한다.
[ssba]

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.