C# 6.0 완벽 가이드/ 그 밖의 XML 기술들

XmlReader 클래스

  • XmlReader는 XML 스트림을 저수준, 전진 전용 방식으로 읽어들이는 고성능 XML 판독기를 나타내는 클래스이다.
  • XML 판독기는 XmlReader.Create를 호출해서 생성하는데, 이 메서드는 Stream이나 TextReader 또는 파일 이름을 뜻하는 URI 문자열을 인수로 받는다.
using (XmlReader reader = XmlReader.Create("customer.xml"))
  • Stream과 URI에서 XML 자료를 가져오는 속도가 느릴 수도 있기 때문에, XmlReader의 메서드들에는 비차단(nonblocking) 코드를 작성하는데 적합한 비동기 버전들이 존재한다.
  • 다음은 문자열로부터 XML을 읽어 들이는 XmlReader 인스턴스를 생성하는 예이다.
XmlReader reader = XmlReader.Create(new System.IO.StringReader(myString));

  • Create의 둘째 인수로 XmlReaderSettings 객체를 지정할 수도 있다. 이 객체는 파싱과 유효성 점검 방식을 결정한다.
    • XmlReaderSettings의 여러 속성 가운데 다음 세 속성은 필요하지 않은 내용을 건너뛰는데 유용하다.
bool IgnoreCommnets  // 주석 노드 무시 여부
bool IgnoreProcessingInstructions  // 처리 명령 무시 여부
bool IgnoreWhitespace  // 공백 문자 무시 여부
  • 다음은 공백(whitespace) 노드들을 읽어 들이지 않도록 하는 예이다. 전형적ㅇ니 XML 활용 상황에서 공백들은 주의를 흐트러뜨릴 뿐 별 도움이 되지 않는다.
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;

using (XmlReader reader = XmlReader.Create("customer.xml", settings))
...
  • XmlReaderSettings의 또 다른 유용한 속성은 ConformanceLevel이다. 이 속성의 기본값은 Document인데, 이 값으로 생성된 XmlReader는 뿌리 노드가 단 하나인 적격의 XML 문서만 받아들인다.
    • 그런 판독기로 다음처럼 XML 문서 중 여러 개의 노드가 있는 부분을 읽어들이면 예외가 발생한다.
<firstname>Jim</firstname>
<lastname>Bo</lastname>
  • 이를 예외 발생 없이 읽으려면 ConformanceLevel을 Fragment로 설정해서 XmlReader를 생성해야 한다.
  • XmlReaderSettings에는 또한 CloseInput이라는 속성도 있다. 이 속성은 판독기를 닫을 때 바탕 스트림도 닫을 것인지의 여부를 뜻한다. 이에 상응해서 XmlWriterSettings에는 CloseOutput이라는 속성이 있다.
    • CloseInput와 CloseOutput의 기본값은 false이다

노드 읽기

  • XML 스트림의 최소 단위는 XML 노드이다. 판독기는 스트림의 노드들을 XML 텍스트에 나온 순서대로 즉 깊이 우선(depth-first) 순서로 운행한다. 판독기의 Depth 속성은 커서의 현재 깊이를 돌려준다.
  • XmlReader로 XML 자료를 읽는 가장 기본적인 방법은 Read를 호출하는 것이다. 이 메서드는 커서를 XML 스트림의 다음 노드로 전진시켜서 그 노드를 읽어들인다. IEnumerator의 MoveNext와 비슷한 방식이다.
    • 단, 판독기에 대해 처음으로 Read를 호출하면 첫 노드를 읽게 된다. Read가 false를 돌려주었다면 커서가 마지막 노드를 지나친 위치로 전진한 것이다. 그러면 XmlReader를 닫고 폐기해야 한다.
    • 다음 예는 XML의 모든 노드를 차례로 읽으면서 노드의 종류를 출력한다.
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;

using (XmlReader reader = XmlReader.Create("customer.xml", settings))
  while(reader.Read())
  {
    Console.Write(new string(' ', reader.Depth * 2));  // 들여쓰기 출력
    Console.WriteLine(reader.NodeType);
  }

// 결과
// XmlDelcaration
// Element
//   Element
//     Text
//   EndElement
//   Element
//     Text
//   EndElement
// EndElement
  • NodeType은 XmlNodeType 형식의 속성으로, XmlNodeType 자체는 다음과 같은 멤버들이 있는 열거형이다.
    • None/ XmlDeclaration/ Element/ EndElement/ Text/ Attribute/ Comment/ Entity/ EndEntity/ EntityReference/ ProcessingInstruction/ CDATA/ Document/ DocumentType/ DocumentFragment/ Notation/ Whitespace/ SignificantWhitespace
  • XmlReader에는 노드의 내용에 접근하기 위한 string 속성이 두 개 있다. 바로 Name과 Value이다. Name 속성과 Value 속성은 노드의 종류에 따라서 둘 다 채워지기도 하고 둘 중 하나만 채워지기도 한다.
  • (예시 코드 생략)
  • XML 개체는 매크로 같은 것이도 CDATA는 C#의 축자(verbatim) 문자열(@”…”) 같은 것이다.
  • XmlReader는 XML 개체를 자동으로 환원해준다.

요소 읽기

  • 읽고자 하는 XML 문서의 구조를 미리 알고 있는 경우도 많다. 그런 경우를 위해 XmlReader는 각 노드의 종류나 내용의 형식에 특화된 여러 메서드를 제공한다. 이들을 이용하면 코드가 간단해질 뿐만 아니라 어느 정도의 유효성 점검 효과도 생긴다.
  • 유효성 점검에 실패하면 XmlReader는 XmlException 예외를 던진다. XmlException에는 문제가 발생한 위치를 가리키는 LineNumber 속성과 LinePosition 속성이 있다. 큰 XML 파일을 읽을 때는 이런 정보를 기록하는 것이 필수이다.
  • ReadStartElement는 NodeType(현재 노드의 종류)이 ELement(요소)인지 점검한 후 Read를 호출한다. 이름을 지정하면현재 요소의 이름이 그 이름과 부합하는지도 점검한다.
  • ReadEndElement는 NodeType이 EndElement인지 점검한 후 Read를 호출한다.
  • 예컨대 다음과 같은 XML 조각을
<firstname>Jim</firstname>
  • 다음과 같이 읽을 수 있따.
reader.ReadStartElement("firstname");
Console.WriteLine(reader.Value);
reader.Read();
reader.ReadEndElement();
  • ReadElementContentAsString 메서드는 이 모든 것을 한번에 수행한다. 이 메서드는 시작 요소와 텍스트 노드, 종료 요소를 읽고 텍스트 노드의 내용(문자열)을 돌려준다.
string firstName = reader.ReadElementContentAsString("firstname", "");
  • 둘째 인수는 해당 요소의 이름공간인데, 지금 예에서는 비워두었다. ReadElementContentAsInt처럼 특정 형식으로의 파싱까지 수행하는 메서드들도 있다.
  • (예시 생략)
  • MoveToContent 메서드는 아주 유용하다. 이 메서드는 군더더기들, 즉 XML 선언과 공백, 주석, 처리 명령들을 건너뛰고 실질적인 내용 노드로 이동한다. XmlReaderSettings의 속성들을 적절히 설정하면, 대부분의 작업에서 XmlReader가 자동으로 이를 적용하게 만들 수도 있다.

선택적 요소

  • 앞선 예에서 <lastname>의 요소가 없다고 할 때, 해결책은 다음과 같다.
r.ReadStartElement("customer");
string firstName = r.ReadElementContentAsString("firstname", "");
string lastNaem = r.Name == "lastname" ? r.ReadElementContentAsString() : null;
decimal creditLimit = r.ReadElementContentAsDecimal("creditlimit", "");

무작위 순 요소

  • 이번 절의 예제들은 XML 파일 안에서 요소들이 특정한 순서로 나타난다고 가정한다. 요소들이 임의의 순서로 나타날 수 있는 XML 파일들을 다루는 가장 간단한 방법은 XML 문서(의 해당 부분)를 X-DOM으로 읽어 들이는 것이다.

빈 요소

  • XmlReader가 빈 요소를 다루는 방식에는 끔찍한 함정이 숨어 있다. 다음과 같은 요소를 생각해 보자.
<customerLIst></customerList>
  • XML에서 이 요소는 다음과 동등하다.
<customerLIst/>
  • 그러나 XmlReader는 이들을 다르게 취급한다. 첫 예에 대해서는 다음과 같은 코드가 예상대로 작동한다.
reader.ReadStartElement("customerList");
reader.ReadEndElement();
  • 하지만 둘째 예에 대해서는 ReadEndElement가 예외를 던진다. XmlReader의 관점에서 둘째 예는 ‘종료 요소’가 없기 때문이다. 이 문제에 대한 우회책은 다음처럼 미리 빈 요소 여부를 점검하는 것이다.
bool isEmpty = reader.IsEmptyElement;
reader.ReadStartElement("customerList");
if (!isEmpty) reader.ReadEndElement();
  • 실제 응용에서 이는 해당 요소에 자식 요소들이 있는 경우에만(예컨대 고객 목록 요소) 문제가 된다. 그냥 단순한 텍스트를 감싸는 요소라면 ReadElementContentAsString 같은 메서드를 호출하면 그만이다. ReadElementXXX 메서드들은 두 종류의 빈 요소를 문제 없이 처리한다.

그 밖의 ReadXXX 메서드들

  • 아래 표는 XmlReader의 모든 ReadXXX 메서드를 정리한 것이다. 이들 대부분은 요소에 대해 작동한다. 예제 XML 조각 열에서 굵게 표시된 부분은 해당 메서드가 읽어들이는 내용이다.
멤버 대상 노드 종류 예제 XML 조각 입력 매개변수 반환값
ReadContentAsXXX Text <a>x</a> x
ReadString Text <a>x</a> x
ReadElementString Element <a>x</a> x
ReadELementContentAsXXX Element <a>x</a> x
ReadInnerXml Element <a>x</a> x
ReadOuterXml Element <a>x</a> <a>x</a>
ReadStartElement Element <a>x</a>
ReadEndElement Element <a>x</a>
ReadSubtree Element <a>x</a> <a>x</a>
ReadToDescendant Element <a>x<b></b></a> “b”
ReadToFollowing Element <a>x<b></b></a> “b”
ReadToNextsibling Element <a>x</a><b></b> “b”
ReadAttributeValue Attribute

 

  • ReadContentAsXXX 메서드들은 텍스트 노드를 XXX 형식으로 파싱한다. 이 메서드들은 내부적으로 XmlConvert 클래스를 이용해서 문자열을 해당 형식으로 변환한다. 이때 텍스트 노드는 요소 노드에 속한 것일 수도 있고 특성 노드에 속한 것일 수도 있다.
  • ReadElementContenAsXXX 메서드들은 해당 ReadContentAsXXX 메서드를 감싼 메서드들로 이들은 요소에 속한 텍스트 노드가 아니라 그런 요소 노드 자체에 적용된다.
  • 형식 있는 ReadXXX 메서드 중에는 Base64나 BinHex로 부호화된 자료를 바이트 배열로 읽어들이는 것들도 있다.
  • ReadInnerXml은 주로 요소에 적용된다. 이 메서드는 하나의 요소와 그 요소의 모든 후손을 읽어서 돌려준다. 특성에 적용한 경우에는 특성의 값을 돌려준다.
  • ReadOuterXml은 ReadInnerXml과 같되 커서 위치의 요소를 제외하는 것이 아니라 포함한다는 점이 다르다.
  • ReadSubtree는 현재 요소(와 그 후손들)만으로 이루어진 부분 트리를 읽는 하나의 프록시 판독기를 돌려준다. 원래의 판독기를 안전하게 다시 읽으려면 이 프록시 판독기를 먼저 닫아 주어야 한다. 프록시 판독기를 닫으면 원래의 판독기의 커서는 부분 트리의 끝으로 이동한다.
  • ReadToDescendant는 지정된 이름/이름공간에 부합하는 첫 후손 노드의 시작 위치로 커서를 옮긴다.
  • ReadToFollowing은 지정된 이름/이름공간에 부합하는 첫 노드(깊이와 무관하게)의 시작 위치로 커서를 옮긴다.
  • ReadToNextSibling은 지정된 이름/이름공간에 부합하는 첫 동기 노드(sibling node; 현재 노드와 부모가 같은 자식 노드)의 시작 위치로 커서를 옮긴다.
  • ReadString과 ReadElementString은 ReadContentAsString, ReadElementContentAsString과 비슷하되, 요소 안에 텍스트 노드 하나만 있어야 작동한다. 만일 여러 개의 노드가 있으면 예외를 던진다.
    • 이들은 요소에 텍스트 노드 외에 주석이 들어 있어도 예외를 던지므로 가능하면 사용하지 않는 것이 좋다.

특성 읽기

  • XmlReader는 요소의 특성들에 직접 접근할 수 있는 임의 접근 인덱서를 제공한다. 이 인덱서를 이용해서 이름이나 위치로 특정 특성에 접근할 수 있다. 이 인텍서를 사용하는 것은 GetAttribute를 호출하는 것과 동등하다.
  • 다음과 같은 XML 조각이 있다고 하자.
<customer id="123" status="archived"/>
  • 다음은 이 요소의 특성들을 읽는 예이다.
Console.WriteLine(reader["id"]);  // 123
Console.WriteLine(reader["status"]);  // archived
Console.WriteLine(reader["bogus"] == null);  // True
  • 특성을 읽으려면 XmlReader의 커서가 시작 요소에 있어야 한다. ReadStartElement를 호출하고 나면 그 특성들에는 더 이상 접근할 수 없게 된다.
  • 의미론적인 관점에서는 특성들의 순서가 중요하지 않지만, 필요하다면 특성의 순서(위치)를 이용해서 특정 특성을 읽을 수 있다.
Console.WriteLine(reader[0]);  // 123
Console.WriteLine(reader[1]);  // archived
  • 특성에 이름공간이 있는 경우, 인덱서로 이름공간을 지정할 수도 있다.
  • AttributeCount 속성은 현재 노드의 특성 개수를 돌려준다.

특성 노드

  • 특성 노드를 명시적으로 운행하려면 그냥 Read 메서드를 거듭 호출하는 통상적인 경로를 벗어나서 가상의 ‘특성 운행 모드’로 진입해야 한다.
    • 예컨대 특성 값을 어떤 형식으로 파싱하고 싶다면, 한가지 방법은 특성 노드들을 명시적으로 운행하면서 ReadContentAsXXX 메서드를 호출하는 것이다.
  • 특성 운행 모드의 진입은 현재 커서가 시작 요소에 있을 때만 가능하다. 일단 특성 운행 모드로 들어가면, 운행의 편의를 위해 전진 전용 규칙이 완화된다. MoveToAttribute를 호출해서 임의의 특성으로(앞이든 뒤든) 즉시 건너뛸 수 있다.
  • 현재 커서가 다음 요소를 가리킨다고 가정하자.tA
<customer id="123" status="archived"/>
  • 다음은 이 요소의 특성들을 읽는 예이다.
reader.MoveToAttribute("status");
string status = reader.ReadContentAsString();

reader.MoveToAttribue("id");
int id = reader.ReadContentAsInt();
  • MoveToAttribute는 만일 지정된 특성이 존재하지 않으면 false를 돌려준다.
  • MoveToFirstAttribute를 호출한 후 MoveToNextAttribute 메서드를 되풀이해 호출하는 식으로 모든 특성을 차례로 운행하는 것도 가능하다.
if (reader.MoveToFirstAttribute())
  do
  {
    Console.WriteLine(reader.Name + "=" + reader.Value);
  } while(reader.MoveToNextAttribute());

이름공간과 접두사

  • XmlReader에서 요소와 특성 이름을 지정하는 방식은 다음 두 가지이다.
    • Name을 사용
    • NamespaceURI와 LocalName의 조합을 사용
  • 어떤 요소의 Name 속성을 읽거나 하나의 name 인수를 받는 메서드를 호출하는 것은 첫 방식에 해당한다. 이 방식은 그 어떤 이름공간이나 접두사도 적용되지 않은 문맥에서 잘 작동한다.
    • 이름공간이나 접두사가 존재하는 경우 이 방식은 거칠고 융통성 없는 방식으로 작동한다. 그런 경우 이 방식은 이름공간을 무시하고 그냥 이름 자체의 일부로 간주한다.
예제 XML 조각 이름
<customer …> customer
<customer xmlns=’blah’ …> customer
<x:customer …> x:customer

 

  • 다음 코드는 첫 두 예제 XML 조각에 대해 잘 작동한다.
reader.ReadStartElement("customer");
  • 세 번째 XML 조각을 읽으려면 다음과 같은 코드가 필요하다.
reader.ReadStartElement("x:customer");
  • 둘째 방식은 두 개의 이름공간 인식 속성, 즉 NamepaceURI 속성과 LocalName 속성의 조합을 사용한다. 이 속성들을 부모 요소들에 정의된 접두사들과 기본 이름 공간들을 고려해서 작동한다.
    • 이 속성들은 접두사들을 자동으로 확장한다. 결과적으로 NamespaceURI에는 항상 현재 요소에 댛나 의미론적으로 정확한 이름공간이 반영되며, LocalName에는 항상 접두사가 제거된 이름이 설정된다.
  • ReadStartElement 같은 메서드를 두 개의 이름 인수를 지정해서 호출한다면 이 둘째 방식을 사용하는 것이다.
<customer xmlns="DefaultNamespace" xmlns:other="OtherNamespace">
  <address>
    <other:city>
      ...
  • 다음은 이 XML을 읽는 예이다.
reader.ReadStartElement("customer", "DefaultNamespace");
reader.ReadStartElement("address", "DefaultNamespace");
reader.ReadStartElement("city "DefaultNamespace");
  • 이 방식을 사용하는 주된 이유는 접두사들을 추상화해서 없애 버리는 것이다. 필요하다면 Prefix 속성을 이용해서 접수사를 확인할 수 있으며 LookupNamespace를 호출해서 그 접두사에 해당하는 이름공간 이름을 얻을 수 있다.

XmlWriter

  • XmlWriter는 내용을 XML 스트림에 기록하기 위한 전진 적용 기록자(writer)이다. XmlWriter의 설계는 XmlReader와 대칭을 이룬다.
  • XmlTextReader처럼, XmlWriter 인스턴스를 생성할 때는 Create 메서드를 호출한다. 이때 선택적인 둘째 인수로 XmlWriterSettings 인스턴스를 지정할 수도 있다. 다음은 들여쓰기를 적용한 간단한 XML 파일을 기록하는 예이다.
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;

using (XmlWriter writer = XmlWriter.Create("..\\..\foo.xml", settrings))
{
  writer.WriteStartElement("customer");
  writer.WriteElementString("firstname", "Jim");
  writer.WriteElementString("lastname", "Bo");
  writer.WriteEndElement();
}

// 결과
// <?xml version="1.0" encoding="utf-8" ?>
// <customer>
//   <firstname>Jim</firstname>
//   <lastname>Bo</lastname>
// </customer>
  • 기본적으로 XmlWriter는 제일 먼저 XML 선언을 기록한다. 단, OmitXmlDeclaration을 true 로 설정하거나 ConformanceLevel을 Fragment로 설정한 XmlWriterSettings를 지정하면 XML 선언이 기록되지 않는다.
    • ConformanceLevel을 Fragement로 설정하면 여러 개의 뿌리 노드를 기록하는 것도 허용된다. 해당 설정 없이 여러 개의 뿌리 노드를 기록하면 예외가 발생한다.
  • WriteValue 메서드는 하나의 텍스트 노드를 기록한다. 이 메서드는 문자열뿐만 아니라 bool이나 DateTime처럼 문자열이 아닌 형식의 값도 받는데, 그런 경우 내부적으로 XmlConvert를 이용해서 해당 값을 XML 규칙을 준수하는 문자열로 변환한다.
writer.WriteStartElement("birthdate");
writer.WriteValue(DateTime.Now);
writer.WriteEndElement()l
  • 반면 만일 다음과 같이 호출하면
WriteElementString("birthdate", DateTime.Now.ToString());
  • XML 규칙을 준수하지 않는 문자열이 기록되어서 나중에 해당 파일을 파싱할 때 문제가 생길 수 있다.
  • WriteString 문자열로 WriteValue를 호출하는 것과 같다. XmlWriter는 특성이나 요소 안에 사용하면 안 되는 문자들(&, <, >나 확장 유니코드 문자 등)을 자동으로 적절히 변환해서 기록한다.

특성 기록

  • 시작 요소를 기록한 직후에는 특성들을 기록할 수 있다.
writer.WriteStartElement("customer");
writer.WriteAttributeString("id", "1");
writer.WriteAttributeString("status", "archived");
  • 문자열이 아닌 값을 기록하려면 WriteStartAttribute와 WriteValue를 호출한 후 WriteEndAttribute를 호출하면 된다.

다른 종류의 노드 기록

  • XmlWriter는 다른 종류의 노드들을 기록하기 위해 다음과 같은 메서드들을 제공한다.
    • WriteBase64/ WriteBinHex/ WriteCData/ WriteCommnet/ WriteDocType/ WriteEntityRef/ WriteProcessingInstruction/ WriteRaw/ WriteWhitespace
  • WriteRaw는 주어진 문자열을 그대로 출력 스트림에 주입한다. 또한 XmlReader 인스턴스를 받아서 그 안에 있는 모든 것을 XML 형태로 기록하는 WriteNode 메서드도 있다.

이름공간과 접두사

  • Write* 메서드들에는 요소나 특성 이름에 이름공간을 부여하는 중복적재 버전들이 갖추어져 있다.
    • 아래 예는 모든 요소를 http://oreilly.com 이름공간에 연관시키고, customer 요소에서 접두사 o를 선언한다.
writer.WriteStartElement("o", "customer", "http://oreilly.com");
writer.WriteElementString("o", "firstname", "http://oreilly.com", "Jim");
writer.WriteElementString("o", "lastname", "http://oreilly.com", "Bo");
writer.WriteEndElement();

// 결과
// <?xml version="1.0" encoding="utf-8" standalone="yes"?>
// <o:customer xmlns:o='http://oreilly.com'>
//   <o:firstname>Jim</o:firstname>
//  <o:lastname>Bo</o:lastname>
// </o:customer>

XmlReader/XmlWriter 사용 패턴

계통적 자료 다루기

  • 다음과 같은 클래스들이 있다고 하자.
public class Contacts
{
  public IList<Customer> Customers = new List<Customer>();
  public IList<Supplier> Suppliers = new List<Supplier>();
}

public class Customer { public string FirstName, LastName; }
public class Supplier { public string Name; }
  • XmlReader와 XmlWriter로 Contacts 객체를 XML로 직렬화해서 다음과 같은 XML 문서를 생성한다고 하자.
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<contacts>
  <customer id="1">
    <firstname>Jay</firstname>
    <lastname>Dee</lastname>
  </customer>
  <customer>
    <firstname>Kay</firstname>
    <lastname>Gee</lastname>
  </customer>
  <supplier>
    <name>X Technologies Ltd</name>
  </supplier>
</contacts>
  • 가장 좋은 접근방식은 그러한 직렬화를 수행하는 하나의 커다란 메서드를 작성하는 것이 아니라, XML을 읽고 쓰는 기능성을 Customer 형식과 Supplier 형식 자체에 캡슐화하는 것이다. 두 형식에 ReadXml과 WriteXml이라는 메서드를 추가한다고 할 때, 따라야 할 바람직한 패턴은 다음과 같다.
    • 반환시 ReadXml과 WriteXml은 판독기/기록기 커서를 계통구조의 동일한 깊이에 남겨둔다.
    • ReadXml은 바깥쪽 요소를 읽고, WriteXml은 그 요소의 안쪽 내용만 기록한다.
  • 다음은 이 패턴에 따라 Customer 형식에 두 메서드를 추가한 예이다.
public class Customer
{
  public const string XmlName = "customer";
  public int? ID;
  public string FirstName, LastName;

  public Customer () { }
  public Customer (XmlReader r) { ReadXml(r); }

  public void ReadXml(XmlReader r)
  {
    if (r.MoveToAttribute("id")) ID = r.ReadContentAsInt();
    r.ReadStartElement();
    FirstName = r.ReadElementContentAsString("firstname", "");
    LastName = r.ReadElementContentAsString("lastname", "");
    r.ReadEndElement();
  }

  public void WriteXml(XmlWriter w)
  {
    if (ID.HasValue) w.WriteAttributeString("id", "", ID.Tostring());
    w.WriteElementString("firstname", FirstName);
    w.WriteElementString("lastname", LastName);
  }
}
  • 코드에서 보듯이 ReadXml 메서드는 바깥쪽 시작, 종료 요소들을 읽어들인다. 만일 호출자가 이미 바깥쪽 요소들을 읽어들였다면 Customer는 자신의 특성들을 읽지 못하게 된다. 이와는 비대칭적으로 WriteXml은 바깥쪽 요소를 기록하지 않는데, 그 이유는 두가지 이다.
    • 호출자가 바깥쪽 요소의 이름ㅇ르 선택해야 할 수도 있다.
    • 호출자가 추가적인 XML 특성들을 기록해야 할 수도 있다. 예컨대 요소를 다시 읽어 들여서 객체를 인스턴스화 할 때 구체적인 파생 형식을 파악하기 위한 subtype 같은 특성이 필요할 수도 있다.
  • 이 패턴을 따르는 것의 또 다른 장점은 구현이 IXmlSerializable과 호환된다는 것이다.
  • Supplier 클래스도 비슷한 모습이다.
public class Supplier
{
  public const string XmlName = "supplier";
  public string Name;

  public Supplier () { }
  public Supplier (XmlReader r) { ReadXml(r); }

  public void ReadXml(XmlReader r)
  {
    r.ReadStartElement();
    Name = r.ReadElementContentAsString("name", "");
    r.ReadEndElement();
  }

  public void WriteXml(XmlWriter w)
  {
    w.WriteElementString("name", Name);
  }
}
  • Contacts 클래스의 경우에는 ReadXml에서 customers 요소를 열거해서 각 자식 요소가 고객(customer)이냐 공급업체(supplier)이냐에 따라 다른 처리를 수행해야 한다. 또한 빈 요소의 함정을 피하는 처리도 필요하다.
public void ReadXml(XmlReader r)
{
  bool isEmpty = r.IsEmptyElement;
  r.ReadStartElement();
  if (isEmpty) return;
  while(r.NodeType == XmlNodeType.Element)
  {
    if (r.Name == Customer.XmlName) Customer.Add(new Customer(r));
    else if (r.Name == Supplier.XmlName) Supplier.Add(new Supplier(r));
    else throw new XmlException ("Unexpected node: " + r.Name);
  }
  r.ReadEndElement();
}

public void WriteXml(XmlWriter w)
{
  foreach (Customer c in Customers)
  {
    w.WriteStartElement(Customer.XmlName);
    c.WriteXml(w);
    w.WriteEndElement();
  }

  foreach (Supplier s in Suppliers)
  {
    w.WriteStartElement(Supplier.XmlName);
    c.WriteXml(w);
    w.WriteEndElement();
  }
}

XmlReader/XmlWriter와 X-DOM을 함께 사용

  • XmlReader나 XmlWriter를 사용하기가 번거로운 지점에서는 X-DOM으로 전환하는 것도 좋은 방법이다. 특히 안쪽 요소들만 X-DOM을 이용해서 처리하는 것은 X-DOM의 장점인 사용 편의성과 XmlReader/XmlWriter의 장점인 메모리 효율성을 모두 취하는 아주 좋은 방법이다.

XmlReader와 XElement의 조합

  • 현재 요소를 X-DOM으로 읽어 들이려면 XmlReader를 인수로 해서 XNode.ReadFrom을 호출하면 된다. 문서 전체를 읽으려 하는 XElement.Load 메서드와는 달리 이 메서드는 ‘탐욕’스럽지 않다. 이 메서드는 그냥 현재 부분 트리의 끝까지만 읽어들인다.
  • 로그 정보를 다음과 같은 구조로 담은 XML 파일이 있다고 하자.
<log>
  <logentry id="1">
    <date>...</date>
    <source>...</source>
    ...
  </logentry>
  ...
</log>
  • 만일 logentry 요소가 수백만 개라면 파일 전체를 하나의 X-DOM으로 읽어 들이는 것은 메모리 낭비일 수 있다. 더 나은 해법은 XmlReader로 logentry 요소들을 훑으면서 각 요소를 XElement를 이용해서 처리하는 것이다.
XmlReaderSettrings settings = new XmlReaderSettings();
settrings.IgnoreWhitespace = true;

using (XmlReader r = XmlReader.Create("logfile.xml", settrings))
{
  r.ReadStartElement("log");
  while (r.Name == "logentry)
  {
    int id = (int) logEntry.Attribute("id");
    DateTime date = (DateTime) logEntry.Element("date");
    string source = (string) logEntry.Element("source");
    ...
  }
  r.ReadEndElement();
}
  • 이전 절의 패턴을 따른다면 XElement 관련 코드를 커스텀 형식의 ReadXml 메서드나 WriteXml 메서드에 끼워 넣어서 구현 세부사항을 커스텀 형식의 사용자로부터 숨기는 것도 좋은 방법이다. 다음은 Customer의 ReadXml 메서드를 그런 식으로 구현한 예이다.
public void ReadXml(XmlReader r)
{
  XElement x = (XElement) XNode.ReadFrom(r);
  FirstName = (string) x.Element("firstname");
  LastName = (string) x.Element("lastname");
}
  • XElement와 XmlReader의 이러한 조합에서는 이름공간들이 유지되고 접두사들이 적절히 확장된다. 더 바깥 수준에서 정의된 이름공간들과 접두사들도 그런 식으로 잘 처리된다. 예컨대 XML 파일이 이런 형태라면
<log xmlns="http://loggingspace">
  <logentry id="1">
    ...
  • logentry 수준에서 생성된 XElement들은 바깥쪽 이름공간들을 제대로 물려받는다.

XmlWriter와 XElement의 조합

  • 안쪽 요소들만 XmlWriter에 기록하는 용도로 XElement를 사용할 수 있다. 다음 코드는 백만 개의 logentry 요소를 XElement를 이용해서 XML 파일에 기록한다. XElement를 활용한 덕분에, 모든 logentry 요소를 한꺼번에 메모리에 담아두지 않아도 된다.
using (XmlWriter w = XmlWriter.Create("logfile.xml"))
{
  w.WriteStartElement("log");
  for (int i = 0; i < 1000000; i++)
  {
    XElement e = new XElement("logentry",
                             new XAttribute("id", i),
                             new XElement("date", DateTime.Today.AddDays(-1)),
                             new XElement("source", "test"));
    e.WriteTo(w);
  }
  w.WriteEndElement();
}
  • XElement를 사용해도 실행상의 추가부담은 미미한 정도이다. 이 예제를 XmlWriter만 사용하도록 고친다고 해도 측정 가능한 수준의 실행 시간 차이는 없다.

XSD와 스키마 유효성 점검

  • 구체적인 실제 XML 문서의 내용과 구조는 거의 항상 특정 응용 영역(domain)에 국한된다. 이를테면 MS Word 문서가 그렇고, 특정 응용 프로그램 또는 웹서비스의 구성(configuration) 파일들도 그렇다.
    • 그런 응용 영역 국한적 XML 파일들은 어떤 특정한 패턴을 따른다. 그런 패턴을 좀 더 공식화한 것을 XML 스키마(schema)라고 부른다.
    • XML 문서의 해석과 유효성 점검(validation)을 표준화하고 자동화하기 위해, 그런 스키마를 서술하는 표준들이 여럿 제정되었다. 가장 널리 쓰이는 표준은 흔히 XSD로 줄여쓰는 XML Schema Definition이다.
    • XSD 이전에는 DTD와 XDR이 있었는데, System.Xml은 이 표준들도 지원한다.
  • (예시 생략)
  • 이 예에서 보듯이 XSD 문서 자체도 XML이다. 게다가 XSD 문서의 스키마를 XSD로 서술할 수 있다.
    • http://www.w3.org/2001/xmlschema.xsd에 그 정의가 있다.

스키마를 이용한 유효성 점검

  • XML 파일이나 문서를 읽을 때 그 문서가 특정한 스키마를 기준으로 유효한 문서인지, 다시 말해 문서가 스키마들에 서술된 패턴을 정확히 따르는지를 점검할 수 있다. 이러한 유효성 점검을 수행하는 이유를 몇 가지 들자면 다음과 같다.
    • 오류 점검과 예외 처리를 위한 코드를 줄일 수 있다.
    • 스키마 유효성 점검을 수행해 보면 미처 생각지 못한 오류를 발견할 수 있다.
    • 오류 메시지가 상세하고 유익한 정보를 담고 있다.
  • 유효성을 점검하려면 XmlReader나 XmlDocument, X-DOM 객체에 적절한 스키마를 부착해야 한다. 이후 평소대로 XML을 읽거나 적재하면 자동으로 유효성이 점검된다.
    • 유효성 점검은 읽기/적재 작업과 함께 일어난다. 즉, 유효성 점검 때문에 입력 스트림이 두 번 읽히지는 않는다.

XmlReader를 이용한 유효성 점검

  • 다음은 customers.xsd 팡리에 담긴 스키마를 XmlReader에 부착하는 예이다.
XmlReaderSettings settings = new XmlReaderSettings();
settings.ValidationType = ValidationType.Schema;
settings.Schemas.Add(null, "customers.xsd");

using (XmlReader r = XmlReader.Create("customers.xml", settings))
...
  • 스키마가 인라인이면 즉, XML 문서 자체에 스키마 서술이 포함되어 있으면 위의 예처럼 Schemas에 XSD 문서를 추가하는 대신 다음과 같은 플래그를 설정하면 된다.
settings.ValidationFlags |= XmlSchemaValidationFlags.ProcessInlineSchema;
  • 이제 평소대로 Read를 호출하면 읽기 작업과 함께 스키마를 기준으로 한 유효성 점검이 일어난다. 만일 어떤 지점에서 스키마 유효성 점검이 실패하면 XmlSchemaValidationException 예외가 발생한다.
  • Read를 호출하면 요소들과 특성들 모두에 대해 유효성이 점검된다. 특성들을 일일히 운행하면서 유효성을 점검할 필요가 없다.
  • XML 문서의 내용에는 관심이 없고 문서의 유효성만 점검하고 싶다면 다음과 같이 하면 된다.
using (XmlReader r = XmlReader.Create("customers.xml", settings))
  try { while (r.Read()) ; }
  catch (XmlSchemaValidationException ex) { ... }
  • XmlSchemaValidationException에는 오류 메시지와 행 번호, 그 행 안에서의 구체적인 오류 위치를 알려주는 Message, LineNumber, LinePosition 속성이 있다.
    • 그런데 이 예외는 문서의 첫 오류에 관한 정보만 제공할 뿐이다. 문서의 모든 오류를 알고 싶으면 이 예외를 잡는 대신 ValidationEventHandler 이벤트를 처리해야 한다.
XmlReaderSettings settings = new XmlReaderSettings();
settings.ValidationType = ValidationType.Schema;
settings.Schemas.Add(null, "customers.xsd");
settings.ValidationEventHandler += ValidationHandler;

using (XmlReader r = XmlReader.Create("customers.xml", settings))
  whie (r.Read()) ;
  • 이 이벤트를 처리하도록 설정하면 문서에 스키마 위반 오류가 있어도 예외가 발생하지 않는다. 대신 설정된 이벤트 처리부가 호출된다.

X-DOM의 유효성 점검

  • XML  파일이나 스트림을 X-DOM으로 읽어들일 때 유효성을 점검하려면 XmlReader 인스턴스를 생성해서 스키마들을 부착하고 그것을 이용해서 문서를 X-DOM에 적재하면 된다.
XmlReaderSettings settings = new XmlReaderSettings();
settings.ValidationType = ValidationType.Schema;
settings.Schemas.Add(null, "customers.xsd");

XDocument doc;
using (XmlReader r = XmlReader.Create("customers.xml", settings))
  try { doc = XDocument.Load(r); }
  catch (XmlSchemaValidationException ex) { ... }
  • 또한 이미 메모리에 있는 XDocument나 XElement의 유효성도 점검할 수 있다. System.Xml.Schema의 해당 확장 메서드 Validate를 호출하면 된다. 이 확장 메서드들은 XmlSchemaSet 인스턴스와 유효성 점검 이벤트 처리부를 받는다.
XDocument doc = XDocument.Load(@"customers.xml");
XmlSchemaSet set = new XmlSchemaSet();
set.Add(null, @"customers.xsd");

StringBuilder errors = new StringBuilder();
doc.Validate (set, (sender, args) => { errors.AppendLine(args.Exception.Message); } );

XSLT

  • XSLT는 Extensible Stylesheet Language Transformation(확장성 스타일시트 언어 변환)의 약자이다. XSLT는 한 XML 언어를 다른 XML 언어로 변환하는 방법을 서술하는 하나의 XML 언어이다. 그런 변환의 좋은 예는 XML를 XHTML 문서로 바꾸는 것이다.
  • (예시 생략)
  • System.Xml.Xsl.XslCompiledTransform 클래스는 이러한 XSLT 변환을 효율적으로 수행한다. 이 클래스가 있으므로 XmlTransform은 더 이상 필요하지 않다. XslCompiledTransform의 사용법은 아주 간단하다.
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load("test.xslt");
transform.Transform("input.xml", "output.xml");
  • Transform 메서드에는 여러 중복적재 버전이 있는데, 일반적으로 위의 예처럼 출력 파일을 받는 버전보다는 XmlWriter를 받는 버전이 더 유용하다(서식화 방식을 제어할 수 있다는 점에서)
[ssba]

The author

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

댓글 남기기

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