C# 6.0 완벽 가이드/ LINQ to XML

  • .NET Famework는 XML 자료를 다루는 여러 API를 제공한다. .NET Framework 3.5부터 범용 XML 문서 처리의 주된 수단은 LINQ to XML 이다. LINQ to XML은 가볍고 LINQ 친화적인 DOM과 이를 보충하는 일단의 질의 연산자들로 구성되어 있다.
  • LINQ to XML의 모든 형식은 System.Xml.Linq 이름공간에 있다.

전체적인 구조

DOM이란 무엇인가?

  • 다음과 같은 XML 파일을 생각해 보자
<?xml version="1.0" encoding="utf-8"?>
<customer id="123" status="archived">
  <firstname>Joe</firstname>
  <lastname>Bloggs<lastname>
</customer>
  • 다른 모든 XML 파이렃럼 이 파일은 하나의 XML 선언(declaration)으로 시작한다.
    • 그 다음은 XML 문서 전체의 뿌리(루트)에 해당하는 요소(element)로 그 이름은 customer이다.
    • 이 customer 요소에는 2개의 특성(attribute)이 있다. 각 특성은 이름(id와 status)과 값(“123”, “archived”)으로 구성된다.
    • customer 요소 안에는 두 자식 요소 firstname과 lastname이 있다. 이 요소들은 각자 단순 텍스트 내용(“Joe”와 “Bloggs”)을 담고 있다.
  • 이러한 구성요소들(선언, 요소, 특성, 값, 텍스트 내용)을 각각 클래스로 나타낼 수 있다.
    • 그리고 그런 클래스에 자식 내용을 저장할 수 있는 컬렉션 속성들을 부여한다면, 문서 전체를 나타내는 객체들의 트리를 형성할 수 있다.
    • 그러한 트리가 바로 흔히 DOM이라고 줄여서 표기하는 문서 객체 모형(document object model)이다.

LINQ to XML DOM

  • LINQ to XML은 다음 두 가지로 이루어진다.
    • XML DOM. 이 책에서는 이를 X-DOM이라 부른다.
    • 약 10개의 추가적인 질의 연산자들
  • 짐작했겠지만 X-DOM은 XDocument나 XElement, XAttribute 같은 형식들로 구성된다.
    • 흥미롭게도 X-DOM의 형식들이 LINQ에 묶여 있지는 않다. 즉, LINQ 질의를 작성하지 않고도 X-DOM을 적재, 생성, 갱신, 저장할 수 있다.
  • 반대로 LINQ 역시 X-DOM과 무관하게 사용할 수 있다. 즉, 구식의 W3C 표준 준수 형식들로 만든 DOM을 질의하는데 LINQ를 사용할 수 있다. 그러나 그런 접근 방식은 짜증스럽고 제한적이다.
  • X-DOM의 특징은 LINQ 친화적이라는 점이다. 좀 더 구체적으로 말하면
    • X-DOM에는 유용한(추가적인 질의가 가능하다는 점에서) IEnumerable 순차열을 출력하는 메서드들이 있다.
    • X-DOM은 LINQ 투영을 통해서 X-DOM 트리를 구축할 수 있는 생성자들을 제공한다.

X-DOM의 개요

  • 아래 그림은 핵심 X-DOM 형식들과 그 관계를 나타낸 것이다. 이 중 가장 흔히 쓰이는 형식은 XElement이다. XObject는 상속 계통구조의 뿌리이고, XElement와 XDocument는 포함 관계(containership) 계통구조의 뿌리이다.
  • 아래 그림은 아래 코드로부터 생성된 X-DOM 트리를 보여준다.
string xml = @"<customer id='123' status='archived'>
<firstname>Joe</firstname>
<lastname>Bloggs<!--멋진 이름--></lastname>
</customer>";

XElement customer = XElement.Parst(xml);
  • XObject는 모든 XML 내용을 위한 추상 기반 클래스이다. 이 클래스에는 포함 관계 트리에서 이 객체의 부모 요소를 가리키는 Parent 속성이 있으며, 생략 가능한 XDocument 형식의 속성도 있다.
  • XNode는 특성을 제외한 대부분의 XML 내용을 위한 기반 클래스이다. XNode의 특징은 서로 다른 XNode 파생 형식 객체들로 순서 있는 컬렉션을 구성할 수 있다는 점이다. 예컨대 다음 XML을 생각해 보자.
<data>
Hello world
<subelement1/>
<!--주석-->
  <subelement2/>
</data>
  • 부모 요소 <data> 안에는 제일 먼저 하나의 XText 노드(Hello world)가 있고, 그 다음에 XElement 노드와 XCommnet 노드가 있고, 그 다음에 또 다른 XElement 노드가 있다. 반면 XAttribute 형식은 오직 XAttribute 형식의 객체들로만 목록을 구성할 수 있다.
  • XNode는 자신의 부모 요소(XElement)에 접근할 수 있을 뿐, 자식 노드들을 포함하지는 못한다. 자식 노드들을 가질 수 있는 것은 XNode의 파생 클래스인 XContainer이다. XContainer에는 자식들을 다루는 멤버들이 정의되어 있다. 그리고 XContainer는 XElement와 XDocument를 위한 추상 기반 클래스이다.
  • XElement에는 특성들을 관리하는 멤버들이 추가되었다. 또한 이 클래스에는 Name 속성과 Value 속성이 있다. XText 형식의 자식 노드 하나만 있는 요소의 경우(이런 경우가 상당히 흔하다), XElement의 Value 속성은 그 자식 노드의 내용을 설정하고 조회하는 연산을 캡슐화한다. 이 덕분에 번거롭게 XText 노드를 거치지 않고도 요소의 텍스트 내용에 접근할 수 있다.
  • XDocument는 XML 트리의 뿌리 노드를 나타낸다. 좀 더 정확하게 말하면, 이 클래스는 뿌리 요소에 해당하는 XElement 뿐만 아니라 문서의 XML 선언을 나타내는 XDeclaration과 처리 명령(processing instruction) 등등 루트 수준 ‘잡다한 추가 요소들’을 모두 감싼다. W3C DOM과 달리 X-DOM에서는 이 노드를 생략할 수 있다.
    • 즉, XDocument 객체를 생성하지 않고도 X-DOM을 적재, 조작, 저장 할 수 있다. XDocument에 의존하지 않는다는 것은 하나의 부분 트리(subtree)를 다른 X-DOM 계통구조에 쉽고 효율적으로 옮길 수 있다는 뜻이기도 하다.

적재와 파싱

  • XElement과 XDocumnet 둘 다 기존 XML 자료로부터 X-DOM 트리를 구축하는 정적 Load 메서드와 Parse 메서드를 제공한다.
    • Load는 파일이나 URI, Stream, TextReader, XmlReader로부터 X-DOM을 구축한다.
    • Parse는 문자열로부터 X-DOM을 구축한다.
  • 다음은 이들을 사용하는 예이다.
XDocument fromWeb = XDocument.Load("http://albahari.com/sample.xml");
XElement fromFile = XElement.Load(@"e:\media\somefile.xml");
XElement config = XElement.Parse(
@"<configuration>
<client enabled = 'true'>
<timeout>30</timeout>
</cient>
</configuration>");
  • 다음은 위의 xml을 조작하는 예이다.
foreach(XElement child in config.Elements())
Console.WriteLine(child.Name); // client

XElement client = config.Element("client");

bool enabled = (bool)client.Attribute("enabled");  // 특성을 조회
Console.WriteLine(enabled); // true
client.Attribute("enabled").SetValue(!enabled); // 특성을 갱신

int timeout = (int)client.Element("timeout"); // 요소를 조회
Console.WriteLine(timeout); // 30
client.Element("timeout").SetValue(timeout*2); // 요소를 갱신

client.Add(new XElement("retries", 3)); // 새 요소 추가
Console.WriteLine(config); // 암묵적으로 config.ToString() 호출

// 결과
// <configuration>
// <client enabled = "false">
// <timeout>60</timeout>
// <retries>3</retries>
// </cient>
// </configuration>
  • XNode는 XmlReader에서 읽은 자료로 임의의 형식의 노드를 생성, 설정하는 정적 메서드 ReadFrom도 제공한다. Load와는 달리 이 메서드는 (완전한) 노드 하나만 읽고 멈추기 때문에 같은 XmlReader를 계속 읽어서 노드들을 차례로 생성할 수 있다.
    • 반대로 XNode로부터 XmlReader 객체나 XmlWriter 객체를 생성하는 것도 가능하다. XNode의 CreateReader 메서드나 CreateWriter 메서드를 사용하면 된다.

저장과 직렬화

  • 임의의 노드에 대해 ToString을 호출하면 노드의 내용을 나타내느 XML 문자열을 얻게 된다. 이때 ToString은 줄 바꿈과 들여쓰기까지 적용된 XML 문자열을 돌려준다.
    • 줄 바꿈과 들여쓰기를 원하지 않는다면 ToString 호출 시 SaveOptions.DisableFormatting을 지정하면 된다)
  • XElement와 XDocument는 X-DOM을 파일이나 Stream, TextWriter, XmlWriter에 기록하는 Save 메서드도 제공한다. 대상이 파일인 경우에는 XML 선언도 자동으로 기록된다. 또한 XNode 클래스에는 WriteTo라는 메서드도 있는데, 이 메서드는 XmlWriter 객체 하나만 받는다.

X-DOM의 인스턴스화

  • Load나 Parse 메서드를 사용하는 대신, 개별 노드 객체들을 직접 인스턴스화 하고 XContainer의 Add 메서드를 통해서 부모 요소에 배정하는 식으로 X-DOM 트리를 손수 구축하는 것도 가능하다.
  • XElement나 XAttribute 객체는 이름과 값만 지정하면 생성할 수 있다.
XElement lastName = new XElement("lastname", "Bloggs");
lastName.Add(new XCommet("멋진 이름"));

XElement customer = new XElement("customer");
customer.Add(new XAttribute("id", 123));
customer.Add(new XElement("firstname", "Joe"));
customer.Add(lastName);

Console.WriteLine(customer.ToString());

// 결과
// <customer id="123">
// <firstname>Joe</firstname>
// <lastname>Bloggs<!--멋진 이름--></lastname>
// </customer>
  • XElement를 생성할 때는 값을 생략할 수 있다. 그냥 요소 이름만 지정하고 내용은 나중에 추가해도 된다. 값을 지정하는 경우에는 그냥 문자열만 제공하면 된다.
    • 명시적으로 XText 형식의 자식 노드를 생성해서 추가할 필요가 없다. X-DOM이 문자열을 자동으로 XText 노드로 바꾸어서 추가해 주므로 그냥 ‘값’만 제공하면 된다.

함수적 생성

  • 앞의 예제에서는 코드만 봐서는 XML의 구조를 짐작하기 어렵다. X-DOM은 함수적 생성(functional construction; functional programming을 흉내낸 용어이다)이라고 하는 또 다른 인스턴스화 방식을 지원한다. 함수적 생성 방법으로는 XML 트리 전체를 하나의 표현식으로 구축할 수 있다.
XElement customer = 
new XElement("customer", new XAttribute("id", 123),
new XElement("firstname", "joe"),
new XElement("lastname", "bloggs",
new XCommnet("nice name")
)
);
  • 이런 방식에는 두 가지 장점이 있다. 첫째로 코드의 형태가 XML의 구조와 비슷하다. 둘째로 LINQ 질의의 select 절을 포함할 수 있다. 예컨대 다음 코드는 LINQ to SQL 질의를 X-DOM 안으로 직접 투영한다.
XElement query = 
new XElement ("customers",
from c in dataContext.Customers
select new XElement("customer", new XAttribute("id", c.ID),
new XElement("firstname", c.FirstName),
new XElement("lastname", c.LastName,
new XCommnet("nice name")
)
)
);

내용 지정

  • 함수적 생성이 가능한 것은 params 객체 배열을 받도록 중복적재된 생성자가 XElement에(그리고 XDocument에) 있기 때문이다.
public XElement(XName name, params object[] content)
  • XContainer의 Add 메서드도 마찬가지이다.
public void Add(params object[] content)
  • 따라서 하나의 X-DOM 노드를 생성 또는 추가할 때 임의의 형식의, 임의의 개수의 자식 객체들을 지정할 수 있다. 그래도 되는 이유는 XContainer가 그 어떤 형식의 객체도 유효한 내용으로 간주해서 적절히 처리해 주기 때문이다. 구체적으로 XContainer는 주어진 객체를 다음과 같은 순서로 처리한다.
    1. 객체가 null이면 무시한다.
    2. 객체가 XNode나 XStreamingElement 파생 형식이면 객체를 그대로 Nodes 컬렉션에 추가한다.
    3. 객체가 XAttribute이면 Attributes 컬렉션에 추가한다.
    4. 객체가 string이면 XText 노드로 감싸서 Nodes에 추가한다.
    5. 객체가 IEnumerable을 구현하는 형식이면 객체(순차열)를 열거해서 각 요소를 지금과 같은 규칙들에 따라 처리한다.
    6. 그 외의 경우이면 객체를 문자열로 변환 후 XText 노드로 감싸서 Nodes에 추가한다.
  • 결과적으로 널이 아닌 모든 객체는 결국에는 Nodes 아니면 Attributes에 들어간다. 또한 널이 아닌 객체는 어떤 것이든 궁극적으로 ToString을 호출해서 XText 노드로 감쌀 수 있으므로 유효한 내용이 된다.
  • 임의의 객체에 대해 ToString을 호출하기 전에 XContainer는 먼저 객체가 다음 형식 중 하나 인지 점검한다.
    • float, double, decimal, bool, DateTime, DateTimeOffset, TimeSpan
    • 만일 객체가 이 형식 중 하나이면, XContainer는 객체 자체에 대해 ToString을 호출하는 대신 보조 클래스 XmlConvert에 있는 적절한 형식의 ToString 메서드를 호출한다. 이는 자료를 왕복 통신이 가능한, 그리고 표준 XML 서식화 규칙들을 만족하는 형태로 변환하기 위한 것이다.

자동적인 깊은 복제

  • 노드나 특성을 어떤 요소에 추가하려면(함수적 생성을 통해서든 Add 메서드를 통해서든) 그 노드나 특성의 Parent 속성이 그 요소로 설정된다. 한 노드의 부모 요소는 많아야 하나이다. 만일 이미 부모가 있는 노드를 또 다른 부모에 추가하면 그 노드는 자동으로 깊게 복제 된다.
    • 다음 예제에서 두 고객 요소는 각자 개별적인 address 복사본을 가지게 된다.
var address = new XElement("address", 
new XElement("street", Lawley St"),
new XElement("town", "North Beach"));

var customer1 = new XElement("customer1", address);
var customer2 = new XElement("customer2", address);

customer1.Element("address").Element("street").Value = "Another St";
Console.WriteLine(customer2.Element("address").Element("street").Value); // Lawley St
  • 이러한 자동 깊은 복제(deep cloning) 덕분에 X-DOM 객체 인스턴스화에는 부수 효과(side effect)가 없다. 이는 함수적 프로그래밍의 또 다른 중요한 특징이다.

내비게이션과 질의

  • XNode 클래스와 XContainer 클래스에는 X-DOM 트리의 운행을 위한 메서드들과 속성들이 정의되어 있다. 그런데 통상적인 DOM과는 달리 이 함수들은 IList<T>를 구현한 컬렉션을 돌려주지 않는다. 대신 이들은 하나의 값 또는 IEnumerable<T>를 구현하는 순차열을 돌려준다.
    • 그러한 순차열에 대해 LINQ 질의를 수행할 수 있으며 foreach 문을 이용한 열거도 수행할 수 있다. 덕분에 간단한 내비게이션 작업은 물론이고 고급 질의도 익숙한 LINQ 질의 구문을 이용해서 처리할 수 있다.

자식 노드 내비게이션

반환 형식 멤버 적용대상
XNode FirstNode { get; } XContainer
  LastNode { get; } XContainer
IEnumerable<XNode> Nods() XContainer*
  DescendantNodes() XContainer*
  DescendantNodesAndSelf() XElement*
XElement Element(XName) XContainer
IEnumerable<XElement> Elements() XContainer*
  Elements(XName) XContainer*
  Descendants() XContainer*
  Descendants(XName) XContainer*
  DescendantsAndSelf() XElement*
  DescendantsAndSelf(XName) XElement*
bool HasElements { get; } XElement

FirstNode, LastNode, Nodes

  • FirstNode 속성과 LastNode 속성을 통해서 첫째 자식 노드와 마지막 자식 노드에 직접 접근할 수 있다. Nodes 메서드는 모든 자식 노드를 담은 순차열을 돌려준다. 이 세 함수 모두 오직 직접적인 자식 노드들만 고려한다. 다음은 이 점을 보여주는 예이다.
var bench = new XElement("bench",
new XElement("toolbox",
new XElement("handtool", "Hammer"),
new XElement("handtool", "Rasp")
),
new XElement("toolbox",
new XElement("handtool", "Saw"),
new XElement("powertool", "Nailgun")
),
new XComment("못총 사용시 주의 필요")
);

foreach(XNode node in bench.Nodes())
Console.WriteLine(node.ToString(SaveOptions.DisableFormatting) + ".");

// 결과
// <toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>.<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>.<!--못총 사용시 주의 필요-->.

요소 조회

  • Elements 메서드는 XElement 형식의 자식 노드들만 돌려준다.
foreach (XElement e in bench.Elements())
Console.WriteLine(e.Name + "=" + e.Value);

// 결과
// toolbox=HammerRasp
// toolbox=SawNailgun
  • 다음의 LINQ 질의는 못총이 있는 공구함들ㅇ르 찾는다.
IEnumerable<string> query = 
from toolbox in bench.Elements()
where toolbox.Elements().Any(tool => tool.Value == "Nailgun")
select toolbox.Value;

// result: { "SwaNailgun" }
  • Elements로 특정 이름의 요소들만 얻을 수도 있다.
int x = bench.Elements("toolbox").Count();  // 2

// 위 질의는 아래와 동등하다.
int x = bench.Element().Where(e => e.Name == "toolbox").Count(); // 2
  • 또한 IEnumerable<XContainer>를 받는 확장 메서드로서의 Elements도 있다. 좀 더 정확히 말하면, 이 확장 메서드는 다음과 같은 형식의 인수를 받는다.
IEnumerable<T> where T : XContainer
  • 이 덕분에 X-DOM 요소들의 순차열에 대해 Elements를 호출할 수 있다. 다음은 앞에 나온 모든 공구함의 수지공구를 찾는 질의를 이 메서드를 이용해서 다시 작성한 것이다.
from tool in bench.Elements("toolbox").Elements("handtool")
select tool.Value.ToUpper();

단일 요소 조회

  • Element(이름이 단수형) 메서드는 주어진 이름에 부합하는 첫 요소를 돌려준다. Element는 다음 예처럼 간단한 내비게이션에 유용하다.
XElement setting = XElement.Load("databaseSettings.xml");
string cx = setting.Element("database").Element("connectString").Value;
  • Element는 Elements를 호출한 후 LINQ의 FirstOrDefault 질의 연산자를 적용하는 것과 같다. 만일 그런 이름의 요소가 하나도 없으면 Element는 null을 돌려준다.
  • Element(“xyz”).Value는 만일 xyz라는 요소가 존재하지 않으면 NullReference Exception을 던진다. 예외보다는 null을 선호한다면, Value 속성을 사용하지 말고 XElement 자체를 string으로 캐스팅하는 것이 낫다.
string xyz = (string)settings.Element("xyz");
  • 이는 XElement가 명시적 string 변환을 정의하고 있기 때문에 가능한 일인데, 사실 그 명시적 string 변환은 딱 이런 용도를 위한 것이다.
  • C# 6부터는 널 조건부 연산자를 사용하는 해법도 가능하다. 즉 Element {“xyz”}?.Value를 사용하면 된다.

후손 노드 조회

  • XContainer는 직접적인 자식 노드들은 물론이고 그 노드들로부터 시작해서 트리 전체에 대해 재귀적으로 이어지는 모든 자식 노드, 즉 후손(descendant) 노드들을 돌려주는 Descendants 메서드와 DescendantNodes 메서드를 제공한다.
    • Descendants 메서드는 선택적 인수를 하나 받는데, 이를 통해서 특정 이름의 자식 요소들만 얻을 수 있다. 다음은 이전의 모든 수지공구를 찾는 예제를 Descendants를 이용해서 다시 작성한 것이다.
Console.WriteLine(bench.Descendants("handtool").Count());  // 3
  • 다음 예에서 보듯이, 이 메서드들의 결과에는 더 이상 자식 노드가 없는 노드, 즉 잎(leaf) 노드 또는 말단 노드들에 이르는 경로의 모든 후손 노드가 포함된다.
    • (이하 예시 생략)

부모 내비게이션

  • 모든 XNode에는 부모 쪽으로의 내비게이션을 위한 Parent 속성과 AncestorXXX 메서드들이 있다. 모든 부모는 항상 XElement이다.
반환 형식 멤버 적용 대상
XElement Parent { get; } XNode*
Enumerable<XElement> Ancestors() XNode*
  Ancestors(XName) XNode*
  AncestorsAndSelf() XElement*
  AncestorsAndSelf(XName) XElement*
  • XDocument는 좀 이상하다. 이 노드는 자식 노드들을 가질 수 있짐만, 그 누구의 부모도 되지 못한다. XDocument에 접근하려면 Document 속성을 사용해야 한다. X-DOM 트리의 그 어떤 객체에서도 Document 속성으로 XDocument 노드에 접근할 수 있다.
  • Ancestors는 첫 요소가 Parent이고 그 다음 요소가 Parent.Parent인 식으로 뿌리 요소에 이르기까지의 모든 선조(ancestor) 요소를 담은 순차열을 돌려준다.
  • 뿌리 요소에는 LINQ 질의 AncestorsAndSelf().Last()를 통해서 접근할 수 있다. 또 다른 방법은 Document.Root 속성을 사용하는 것이다. 단 이 방법은 X-DOM에 실제로 XDocument 노드가 있는 경우에만 유효하다.

동기 노드(부모가 같은 노드) 내비게이션

반환 형식 멤버 적용대상
bool IsBefore (XNode node) XNode
  IsAfter(XNode node) XNode
XNode PreviousNode { get; } XNode
  NextNode { get; } XNode
IEnumerable<XNode> NodesBeforeSelf() XNode
  NodesAfterSelf() XNode
IEnumerable<XElement> ElementsBeforeSelf() XNode
  ElementsBeforeSelf(XName name) XNode
  ElementsAfterSelf() XNode
  ElementsAfterSelf(XName name) XNode
  • PreviousNode와 NextNode(그리고 FIrstNode/LastNode)를 이용하면 같은 부모 요소의 자식 노드들을 마치 연결 목록에서처럼 운행할 수 있다. 이는 우연이 아니다 실제로 자식 노드들은 내부적으로 연결목록에 저장되어 있다.
  • XNode는 내부적으로 단일 연결 목록을 사용하므로, PreviousNode는 그리 성능이 좋지 않다.

특성 내비게이션XNode

반환 형식 멤버 적용대상
bool HasAttributes { get; } XElement
XAttribute Attribute (XName name) XElement
  FirstAttribute { get; } XElement
  LastAttribute { get; } XElement
IEnumerable<XAttribute> Attributes() XElement
  Attributes(XName name) XElement
  • 이들 외에 XAttribute는 Parent 속성 뿐만 아니라 PreviousAttribute 속성과 NextAttribute 속성도 정의한다.
  • 이름 하나를 인수로 받는 Attributes 메서드는 빈 순차열 또는 하나의 요소를 담은 순차열을 돌려준다. XML에서 한 요소에 같은 이름의 특성이 여러 개 있을 수는 없다.

X-DOM 갱신

  • X-DOM의 요소들과 특성들을 갱신하는 방법은 다음과 같이 여러가지이다.
    • SetValue를 호출하거나 Vlue 속성을 다시 배정한다.
    • SetElementValue나 SetAttributeValue를 호출한다.
    • RemoveXXX 메서드들을 호출한다.
    • 새로운 내용으로 AddXXX 메서드들이나 ReplaceXXX 메서드들을 호출한다.

간단한 값 갱신

멤버 적용대상
SetValue (object value) XElement, XAttribute
Value { get; set; } XElement, XAttribute

 

  • SetValue 메서드는 요소나 특성의 내용을 단순한 값으로 대체한다. Value 속성을 설정하는 것으로도 같은 효과가 나지만, 이 경우에는 문자열 값만 사용할 수 있다.
  • SetValue를 호출하면(또는 Value 속성을 배정하면) 모든 자식 노드가 대체되는 효과가 난다.

자식 노드와 특성의 갱신

범주 멤버 적용대상
추가 Add (params object[] content) XContainer
추가 AddFirst (params object[] content) XContainer
제거 RemoveNodes() XContainer
제겨 RemoveAttributes() XElement
제거 RemoveAll() XElement
갱신 ReplaceNodes(params object[] content) XContainer
갱신 ReplaceAttributes(params object[] content) XElement
갱신 ReplaceAll(params object[] content) XElement
갱신 SetElementValue(XName name, object value) XElement
갱신 SetAttributeValue(XName name, object value)

XElement

 

  • 이 부류의 메서드 중 가장 편리한 것은 마지막 2개, 즉 SetElementValue와 SetAttributeValue이다. 이 두 메서드는 XElement나 XAttribute 인스턴스를 생성해서 추가하되, 같은 이름의 기존 요소나 특성이 있으면 새 값으로 대체하는 작업을 한 번의 호출로 수행하는 효과를 낸다. 다음은 SetElementValue를 사용하는 예이다.
XElement settings = new XElement("settings");
settings.SetElementValue("timeout", 30); // 자식 노드를 추가한다.
settings.SetElementValue("timeout", 60);  // 그 노드의 값을 60으로 갱신한다.
  • Add 메서드는 자식 노드를 요소나 문서의 끝에 추가한다. AddFirst도 같은 일을 하되, 컬렉션의 끝이 아니라 시작에 삽입한다.
  • RemoveNodes나 RemoveAttributes를 이용하면 모든 자식 노드나 특성을 단번에 제거할 수 있다. RemoveAll은 그 둘을 모두 호출한 것과 같은 효과를 낸다.
  • ReplaceXXX 메서드들은 기존 자식 노드들이나 특성들을 제거하고 새로운 노드들이나 특성들을 추가하는 것과 같다. 이들은 내부적으로 입력의 복사본을 유지하므로 e.ReplaceNodes(e.Nodes()) 같은 호출도 의도한 대로 작동한다.

부모를 거치는 갱신

멤버 적용대상
AddBeforeSelf(params object[] content) XNode
AddAfterSelf(params object[] content) XNode
Remove() XNode*, XAttribute*
ReplaceWith(params object[] content)

XNode

 

  • AddBeforeSelf, AddAfterSelf, Remove, ReplaceWith 메서드는 현재 노드의 자식 노드 컬렉션이 아니라 현재 노드가 속한 자식 노드 컬렉션에 즉 현재 노드의 부모의 자식 노드들에 작용한다.
    • 따라서 현재 노드에 반드시 부모 요소가 존재해야 한다. 그렇지 않으면 예외가 발생한다.
    • AddBeforeSelf와 AddAfterSelf는 노드를 임의의 위치에 삽입할 때 유용하다.
XElement items = new XElement("items", new XElement("one"), new XElement("three"));
items.FirstNode.AddAfterSelf(new XElement("two"));

// 결과
// <items><one/><two/><three/></items>
  • 노드들이 아주 많이 있는 순차열의 안의 임의의 위치에 노드를 삽입해도 효율이 떨어지지는 않는다. 내부적으로 노드들이 연결된 목록에 정의되어 있기 때문이다.
  • Remove 메서드는 현재 노드를 부모에서 제거한다. ReplaceWith는 현재 노드를 제거하고 그 위치에 다른 내용을 삽입한다. 다음은 그 예이다.
XElement items = XElement.Parse("<items><one/><two/><three/></items>");
items.FirstNode.ReplaceWith(new XComment("여기에 one이 있었음"));

// 결과
// <items><!--여기에 one이 있었음--><two/><three/></items>

노드 순차열 또는 특성 순차열 제거

  • System.Xml.Linq에 있는 확장 메서드들 덕분에, 노드들이나 특성들의 순차열에도 Remove를 호출할 수 있다. 다음 X-DOM을 생각해 보자.
XElement contacts = XElement.Parse(
@"<contacts>
<customer name='Mary'/>
<customer name='Chris' archived='true'/>
<supplier name='Susan'>
<phone archived='true'>012345678<!--기밀--></phone>
</supplier>
</contacts>");
  • 다음은 모든 고객(customer 요소)을 제거한다.
contacts.Elements("customer").Remove();
  • 다음 문장은 연락처 항목(contacts의 자식 요소) 중 보관된(즉, archived 특성이 true인) 항목을 모두 제거한다.
contacts.Elements().Where(e => (bool?) e.Attribute("archived") == true).Remove();
  • Elements()를 Descendants()로 대체하면 X-DOM 전체에서 보관된 요소들이 제거된다. 결과적으로 XML은 다음과 같은 모습이 된다.
<contacts>
<customer name="Mary"/>
<supplier name="Susan"/>
</contacts>
  • 다음 예는 후손 중 ‘기밀’이라는 단어가 포함된 주석 노드가 있는 모든 연락처 항목을 제거한다.
contacts.Elements().Where(e => e.DescendantNodes().OfType<XComment>().Any(c => c.Value == "기밀")).Remove();
  • 결과는 다음과 같다.
<contacts>
<customer name="Mary"/>
  <customer name="Chris" archived="true"/>
</contacts>
  • 이를 다음과 같이 트리에서 모든 주석 노드를 제거하는 좀 더 간단한 질의와 비교해 보기 바란다.
contacts.DescendantNodes().OfType<XComment>().Remove();
  • Remove 메서드들은 내부적으로 조건에 부합하는 모든 요소를 임시 목록에 복사한 후 그 임시 목록을 훑으면서 삭제를 수행한다. 이 덕분에 삭제와 질의를 동시에 수행할 때 발생할 수 있는 오류들이 방지된다.

값 다루기

  • XElement와 XAttribute에는 string 형식의 Value 속성이 있다. 요소에 XText 형식의 자식 노드 하나만 있는 경우, XElement의 Value 속성은 그 노드의 내용에 직접 접근하는 지름길 역할을 한다. XAttribute의 Value 속성은 그냥 해당 특성의 값이다.
  • 요소들과 특성들은 비록 저장되는 장소가 다르지만, X-DOM에서는 같은 이름의 메서드와 속성을 이용해서 이들의 값에 접근할 수 있다.

값 설정

  • 요소나 특성의 값을 설정하는 방법은 2가지이다. 하나는 SetValue를 호출하는 것이고 하나는 Value 속성에 배정하는 것이다. 문자열 뿐만 아니라 다른 단순 자료 형식들도 받아들인다는 점에서 SetValue가 유연하다.
var e = new XElement("date", DateTime.Now);
e.SetValue(DateTime.Now.AddDays(1));
Console.Write(e.Value);
  • 이렇게 하는 대신 요소의 Value 속성에 값을 배정하려면 해당 DateTime을 손수 문자열로 변환해야 하는데, 그냥 ToString을 호출하는 것으로는 해결되지 않는다.
    • XML 서식화 규칙들을 만족하는 문자열 표현을 얻으려면 XmlConvert를 사용해야 한다. 그래야 DateTime이 제대로 서식화되며, 부울 값 true가 True가 아니라 소문자 true로 그리고 double.NegativeInfinity가 “-INF”로 기록된다.
  • XElement나 XAttribute의 생성자에 문자열 형식이 아닌 값을 전달하는 경우에도 XmlConvert로 하는 것과 동일한 변환이 자동으로 일어난다.

값 조회

  • Value 속성에 담긴 문자열 형식의 값을 다시 적절한 형식의 값으로 복원하려면 그냥 XElement나 XAttribute를 해당 형식으로 캐스팅하기만 하면 된다. 왠지 안 될 것 같지만 실제로 된다.
XElement e = new XElement("now", DateTime.Now);
DateTime dt = (DateTime) e;

XAttribute a = new XAttribute("resolution", 1.234);
double res = (double) a;
  • 요소나 특성이 DateTime이나 수치를 그 자체로 저장하지는 않는다. 항상 텍스트로 저장하고 필요하면 파싱한다.
    • 또한 원래의 형식을 ‘기억’하지도 않는다. 따라서 실행시점 오류를 피하려면 정확한 형식으로 캐스팅해야 한다.
    • 튼튼한 코드를 위해서는 그러한 캐스팅을 try 블록 안에 두고 catch 절에서 FormatException을 잡아야 한다.
  • XElement나 XAttribute로의 명시적 캐스팅으로 파싱할 수 있는 형식들은 다음과 같다.
    • 모든 표준 수치 형식
    • string, bool, DateTime, DateTimeOffset, TimeSpan, Guid
    • 위의 형식들의 널가능 버전
  • 널 가능 형식으로의 캐스팅은 Element나 Attribute 메서드와 함께 사용할 때 유용하다. 지정된 이름의 요소나 특성이 없어도 캐스팅이 여전히 작동하기 때문이다. 예컨대 x에 timeout 요소가 하나도 없으면 다음 예제의 첫 줄은 실행시점 오류를 발생하지만 둘째 줄은 발생하지 않는다.
int timeout = (int) x.Element("timeout");  // 오류
int? timeout = (int?) x.Element("timeout"); // OK. timeout은 null
  • 그러나 널 가능 형식으로의 캐스팅에서 예외가 전혀 발생하지 않는 것은 아니다. 지정된 이름의 요소나 특성이 존재하긴 하지만 그 값이 빈 문자열(또는 서식이 잘못된 문자열)이면 예외가 발생한다. 이 때문에 반드시 FormatException을 잡아야 한다.
  • LINQ 질의에서도 캐스팅을 사용할 수 있다. 다음 질의는 “John”을 돌려준다.
var data = XElement.Parse(
@"<data>
<customer id='1' name='Mary' credit='100' /> <customer id='2' name='John' credit='150' /> <customer id='3' name='Anne' />
</data>");

IEnumerable<string> query =
from cust in data.Elements()
where (int?) cust.Attribute("credit") > 100
select cust.Attribute("name").Value;
  • 널 가능 int로의 캐스팅은 Anne 처럼 credit 특성이 없는 요소에서도 Null ReferenceException이 발생하지 않게 하기 위한 것이다. 또는 다음 처럼 where 절에 적절한 술어를 추가해도 같은 결과를 얻을 수 있다.
where cust.Attributes("credit").Any() && (int) cust.Attribute..

값과 혼합 내용 노드

  • Value라는 속성이 있으니 XText 노드를 직접 다룰 필요는 없을 것이라는 생각이 들 수도 있다. 그러나 하나의 요소에 다음처럼 여러 형식의 노드들이 섞여 있는 ‘혼합 내용(mixed content)’이 들어 있을 수도 있다.
<summary>An XAttribute is <bold>not</bold> an XNode</summary>
  • 이 경유 그냥 Value 속성을 조회하는 것으로는 summary의 내용을 제대로 얻지 못한다.
    • summary 요소에는 3개의 자식 노드가 있는데, 첫째는 XText 노드이고 그 다음은 XElement 노드, 마지막은 또 다른 XText 노드이다. 다음은 summary 요소를 구축하는 코드이다.
XElement summary = 
new XElement("summary",
new XText("An XAttribute is "),
new XElement("bold", "not"),
new XText(" an XNode"));
  • 흥미롭게도 이런 경우에도 여전히 summary의 Value를 조회할 수 있다. 그래도 예외는 발생하지 않는다. 다만 모든 자식 노드의 값이 연결된 하나의 문자열을 얻게 된다.
    • summary의 Value에 다른 값을 배정하는 것도 적법하다. 단, 그러면 기존의 모든 자식 노드를 새로운 XText 형식의 자식 노드 하나가 대신하게 된다.

자동적인 XText 연결

  • XElement에 단순 내용(혼합 내용이 아닌)을 추가하면, X-DOM은 새 XText 노드를 생성해서 추가하는 대신 그 내용의 문자열 표현을 기존의 XText 자식 노드의 내용에 연결한다.
    • 다음 예에서 e1과 e2는 그 값이 HelloWorld인 자식 XText 노드 하나만 갖게 된다.
var e1 = new XElement("test", "Hello"); e1.Add("World");
var e2 = new XElement("test", "Hello", "World");
  • 그러나 명시적으로 XText 노드를 생성해서 추가하면 여러 개의 자식 노드가 남게 된다.
var e = new XElement("test", new XText("Hello"), new XText("World"));
Console.WriteLine(e.Value); // HelloWorld
Console.WriteLine(e.Nodes().Count()); // 2
  • XElement는 두 XText 노드를 연결하지 않으므로 노드들은 구별되는 객체로 남게 된다.

문서와 선언

XDocument

  • XDocument는 하나의 뿌리(루트) XElement와 생략 가능한 XDeclaration, 처리 명령, 문서 유형, 뿌리 수준 주석들을 감싸는 역할을 한다. X-DOM에서 XDocument 노드는 선택적이다. 즉, X-DOM 트리에 XDocument 노드가 없어도 된다.
    • W3C DOM의 Document 노드는 모든 것을 한데 묶는 접착제 역할을 하지만, XDocument는 그렇지 않다.
  • XDocument도 XElement처럼 함수적 생성 방식의 생성자를 제공한다. 그리고 XDocument는 XContainer의 파생 형식이므로 AddXXX, RemoveXXX, ReplaceXX 메서드들도 지원한다. 그러나 XElement와는 달리 XDocument가 받아들이는 내용은 제한적이다. XDocument는 다음과 같은 내용만 받아 들인다.
    • 하나의 XElement 객체(뿌리 요소)
    • 하나의 XDeclaration 객체
    • 하나의 XDocumentType 객체(DTD 참조용)
    • 임의의 개수의 XProcessingInstruction 객체들
    • 임의의 개수의 XComment 객체들
  • (이하 기본적인 XDocument 예시 생략)
  • XDocument에는 Root라는 속성이 있다. 이 속성은 문서의 뿌리 요소에 해당하는 XElement에 바로 접근하는 용도로 쓰인다. 반대로 XObject에는 문서(XDocument)로의 접근을 위한 Document 속성이 있다. 트리의 어떤 객체에서도 이 속성을 사용할 수 있다.
Console.WriteLine(doc.Root.Name.LocalName);  // html
XElement bodyNode = doc.Root.Element(ns + "body");
Console.WriteLine(bodyNode.Document == doc); // True
  • XDocument는 부모 요소가 될 수 없다. 즉, XDocument의 자식 노드에는 Parent가 없다.
Console.WriteLine(doc.Root.Parent == null);  // True
foreach (XNode node in doc.Nodes())
Console.Write(node.Parent == null); // TrueTrueTrueTrue

XML 선언

  • 표준적인 XML 파일은 하나의 XML 선언으로 시작한다. 다음은 XML 선언의 예이다.
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
  • XML 선언에는 XML 파일을 읽는 코드가 제대로 파싱하는데 필요한 정보가 포함되어 있다. 저장과 직렬화 과정에서 XElement와 XDocument는 다음과 같은 규칙에 따라 XML 선언을 출력한다.
    • 파일 이름을 지정해서 Save를 호출하면 항상 XML 선언이 출력된다.
    • XmlWriter를 지정해서 Save를 호출하면 XmlWriter에는 특별한 설정이 없는 한 XML 선언이 출력된다.
    • ToString 메서드는 절대로 XML 선언을 출력하지 않는다.
  • XmlWriter가 XML 선언을 출력하지 않게 하려면, XmlWriter 객체를 생성할 때 지정하는 XmlWriterSettings 객체의 OmitXmlDelcaration 속성과 ConformanceLevel 속성을 적절히 설정해야 한다.
  • X-DOM에 XDeclaration 객체가 있는지 없는지는 XML 선언 출력 여부에 영향을 미치지 않는다. X-DOM에서 XDelcaration의 목적은 XML 직렬화시 힌트를 제공하는 것이다. 특히 다음의 두 가지 정보를 제공한다.
    • 사용할 텍스트 부호화(encoding) 방식
    • XML 선언의 encoding 특성과 standalone 특성의 값(XML 선언을 출력하는 경우)
  • XDeclaration의 생성자는 세 개의 인수를 받는데, 순서대로 version, encoding, standalone 특성의 값으로 쓰인다. 다음 예에서 test.xml은 UTF-16으로 부호화된다.
var doc = new XDocument(
new XDelcaration("1,0", "utf-16", "yes"),
new XElement("test", "data"));

doc.Save("test.xml");

선언을 문자열로 기록

  • 하나의 XDocument 객체를 string으로 직렬화하는데, XML 선언이 결과에 포함되어야 한다고 하자. ToString은 XML 선언을 출력하지 않으므로 XmlWriter를 사용해야 한다.
var doc = new XDocument(
new XDeclaration("1,0", "utf-8", "yes"),
new XElement("test", "data"));

var output = new StringBuilder();
var settings = new XmlWriterSettings { Indent = true };

using (XmlWriter xw = XmlWriter.Create(output, settings))
doc.Save(xw);

Console.WriteLine(output.ToString());

// 결과
// <?xml version="1.0" encoding="utf-16" standalone="yes"?>
// <test>data</test>
  • 그런데 XDeclaration을 생성할 때 UTF-8을 지정했지만 최종 출력에는 UTF-16이 있음을 주목하기 바란다. 버그 같아 보이지만 XmlWriter가 아주 똑똑하게 대처한 결과이다.
    • 지금 예제는 XDocument 객체를 파일이나 스트림이 아니라 하나의 string에 기록하는 것이며, string 형식이 내부적으로 텍스트를 저장하는데 사용하는 부호화는 UTF-16이므로, 애초에 UTF-16 이외의 부호화는 적용할 수 없다. 그래서 XmlWriter는 현실을 올바로 반영한 “utf-16″을 기록한 것이다.
  • 이는 ToString 메서드가 XML 선언을 출력하지 않는 이유이기도 하다. Save를 호출하는 대신 다음과 같이 좀 더 직접적인 방식으로 XDocument를 파일에 기록한다고 하자.
File.WriteAllText("data.xml", doc.ToString());
  • 이 경우 data.xml에는 XML 선언이 포함되지 않는다. 따라서 XML 표준을 준수하는 파일은 아니다. 그래도 파싱은 가능하다. (텍스트 부호화 방식을 추론하는 것이 가능하므로)
    • 만일 ToString이 XML 선언을 출력한다면, 그 선언에는 encoding=”utf-16″이 포함될 것이다. 그런데 이는 부정확한 선언이다. WriteAllText는 주어진 문자열을 UTF-8로 부호화해서 저장하기 때문이다. 결과적으로 이후 data.xml을 읽는 코드는 오작동 하게 된다.

이름과 이름 공간

  • .NET의 형식들을 이름공간으로 조직화하는 것과 비슷하게, XML의 요소들과 특성들도 이름공간으로 조직화 할 수 있다.
  • XML의 이름공간은 크게 두 가지 용도로 쓰인다.
    • 첫째로 C#의 이름공간과 비슷하게 XML 이름공간은 이름 충돌을 피하는데 도움이 된다. 이름 충돌은 서로 다른 두 XML  파일의 자료를 합칠 때 문제가 될 수 있다.
    • 둘째로 이름 공간은 이름에 절대적인 의미를 부여한다. 예컨대 ‘nil’이라는 이름의 의미는 얼마든지 다양한다. 그러나 http://www.w3.org/2001/xmlschema-instance 이름 공간 안에서 ‘nil’은 C#의 null에 해당하는 것을 의미하며, 그 적용 방법에 관한 규칙들이 정해져 있다.

XML의 이름공간

  • 예컨대 customer이라는 요소가 OReilly.Nutshell.CSharp이라는 이름공간에 속하게 하고 싶다고 하자. 방법은 두 가지인데, 첫째는 다음과 같이 xmlns 특성을 이용하는 것이다.
<customer xmlns="OReilly.Nutshell.CSharp"/>
  • xmlns는 특별한 의미로 쓰이는 예약된 특성이다. 지금처럼 사용했을 떄 이 특성은 다음 두 가지 역할을 한다.
    • 현재 요소가 속하는 이름공간을 지정한다.
    • 현재 요소의 모든 후손 요소가 속하는 기본 이름공간을 지정한다.
  • 따라서 다음 예에서 address와 postcode도 암묵적으로 OReilly.Nutshell.CSharp 이름공간에 속하게 된다.
<customer xmlns="OReilly.Nutshell.CSharp">
<address>
<postcode>02138</postcode>
</address>
</customer>
  • 만일 address와 postcode가 아무 이름공간에도 속하지 않게 하려면 다음처럼 빈값을 지정하면 된다.
<customer xmlns="OReilly.Nutshell.CSharp">
<address xmlns="">
<postcode>02138</postcode> <!--postcode는 빈 이름공간을 상속한다.-->
</address>
</customer>

이름공간 접두사

  • 이름공간을 지정하는 두 번째 방법은 접두사(prefix)를 이용하는 것이다. 접두사는 이름공간에 배정하는 하나의 별칭으로 기본적으로는 타자량을 줄이기 위한 것이다.
    • 접두사를 사용하는 단계는 두 개인데, 첫 단계는 정의이고 그 다음은 사용이다. 다음 예처럼 하나의 요소에서 두 단계를 모두 수행할 수도 있다.
<nut:customer xmlns:nut="OReilly.Nutshell.CSharp"/>
  • 이 요소에서는 두 가지 일이 일어난다. 오른쪽의 xmlns:nut=”…”는 nut이라는 접두사를 정의한다. 이 접두사는 현재 요소와 현재 요소의 모든 후손 요소에서 사용할 수 있다. 왼쪽의 nut:customer는 새로 할당된 접두사를 customer 요소에 적용한다.
  • 요소에 적용된 접두사에 해당하는 이름공간이 그 후손 요소들의 기본 이름공간이 되는 것이 아니다. 다음 XML에서 firstname에는 이름공간이 부여되지 않는다.
<nut:customer xmlns:nut="OReilly.Nutshell.CSharp">
<firstname>Joe</firstname>
</customer>
  • firstname이 OReilly.Nutshell.CSharp에 속하게 하려면 다음처럼 명시적으로 접두사를 지정해야 한다.
<nut:customer xmlns:nut="OReilly.Nutshell.CSharp">
<nut:firstname>Joe</firstname>
</customer>
  • 부모 요소 자체에는 접두사를 지정하지 않고, 후손 요소들이 사용할 접두사들만 정의하는 것도 가능하다. 다음은 i와 z라는 두 개의 접두사를 정의하되, customer 자체에는 아무 이름공간도 부여하지 않는다.
<customer xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
...
</customer>
  • 만일 customer가 뿌리 노드라면 문서 전체에서 i와 z를 바로 사용할 수 있다. 접두사는 여러 이름공간에서 요소들을 가져와서 사용하려 할 때 유용하다.
  • 이 예에서 두 이름공간의 이름이 모두 URI라는 점을 주목하기 바란다. 이처럼 이름공간 이름에 URI(자신이 소유한)를 사용하는 것이 표준적인 관행이다. 따라서 실제 코드라면 customer 요소를 다음과 같은 형태로 정의했을 것이다.
<customer xmlns="http://oreilly.com/schemas/nutshell/csharp"/>
<nut:customer xmlns:nut="http://oreilly.com/schemas/nutshell/csharp"/>

특성

  • 특성에도 이름공간을 부여할 수 있다. 주된 차이는, 항상 접두사를 사용해야 한다는 것이다.
<customer xmlns:nut="OReilly.Nutshell.CSharp" nut:id="123"/>
  • 또 다른 차이는 접두사가 없는 특성에는 항상 빈 이름공간이 부여된다는 점이다. 즉 특성은 부모 요소의 기본 이름공간을 물려받지 않는다.
  • 대체로 특성은 해당 요소에 국한된 의미로 쓰이므로 특성에 이름공간을 부여하는 경우는 많지 않다. 예외는 W3C가 정의한 nil 특성 같은 범용 특성 또는 메타자료 특성들이다.
<customer xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<firstname>Joe</firstname>
<lastname xsi:nil="true"/>
</customer>
  • 여기서 nil 특성은 lastname이 빈 문자열이 아니라 nil임을 명시적으로 밝히는 용도로 쓰였다. 표준 이름공간을 사용했기 때문에, 범용 파싱 유틸리티는 이러한 의도를 인식할 가능성이 크다.

X-DOM의 이름공간 지정 방법

  • 이번 장에서 지금까지는 XElement나 XAttribute에 단순한 형태의 문자열 이름을 부여했다. 단순한 문자열 이름은 이름공간이 없는 XML 이름에 해당한다. 그런 이름은 전역 이름공간에 정의된 .NET 형식과 다소 비슷하다.
  • XML 이름공간을 지정하는 방법은 두 가지이다. 첫째는 지역 이름 앞에 이름공간 이름을 중괄호로 감싸는 것이다.
var e = new XElement("{http://domain.com/xmlspace}customer", "Bloggs");
Console.WriteLine(e.ToString());

// 결과
// <customer xmlns="http://domain.com/xmlspace">Bloggs</customer>
  • 둘째 방법은 XNamespace 형식과 XName 형식을 사용하는 것이다. (이쪽이 성능이 더 좋다.)
public sealed class XNamespace
{
public string NameSpaceName { get; }
}

public sealed class XName // 지역 이름과 이름공간(생략 가능)
{
  public string LocalName { get; }
public XNamespace Namespace { get; } // 생략 가능
}
  • 두 형식 모두 string으로부터의 암묵적 캐스팅을 정의한다. 따라서 다음과 같은 코드가 유효하다.
XNamespace ns = "http://domain.com/xmlspace";
XName localName = "customer";
XName fullName = "{http://domain.com/xmlspace}customer";
  • 또한 XNamepace는 중괄호 없이도 이름공간 이름과 노드 이름을 하나의 XName으로 합칠 수 있도록 + 연산자를 적절히 중복적재한다. 예컨대 다음과 같은 코드가 가능하다.
XNamespace ns = "http://domain.com/xmlspace";
XName fullName = ns + "customer";
Console.WriteLine(fullName); // {http://domain.com/xmlspace}customer
  • X-DOM에서 요소 이름이나 특성 이름을 받는 모든 생성자와 메서드는 실제로는 string이 아니라 XName 객체를 받는다. 지금까지의 예제들처럼 XName 객체 대신 그냥 문자열을 사용할 수 있는 것은 다름 아닌 암묵적 캐스팅 덕분이다.
  • 이름공간을 부여하는 방법은 요소와 특성이 동일하다.
XNamespace ns = "http://domain.com/xmlspace";
var data = new XElement(ns + "data", new XAttribute(ns + "id", 123));

X-DOM과 기본 이름공간

  • X-DOM에서 기본 이름공간이라는 개념은 실제로 XML을 출력할 때만 적용될 뿐 그 전에는 아무 효과도 내지 않는다. 따라서 자식 요소가 어떤 이름공간에 속하게 하고 싶다면 해당 XElement를 생성할 때 반드시 이름공간을 명시적으로 지정해야 한다. 부모로부터 기본 이름공간을 물려받지는 않는다.
XNamespace ns = "http://domain.com/xmlspace";
var data = new XElement(ns + "data",
  new XElement(ns + "customer", "Bloggs"),
new XElement(ns + "purchase", "Bicycle")
);
  • XML을 읽거나 출력할 때는 X-DOM이 기본 이름공간을 적용한다.
Console.WriteLine(data.ToString());

// 결과
// <data xmlns="http://domain.com/xmlspace>
// <customer>Bloggs</customer>
// <purchase>Bicycle</purchase>
// </data>


Console.WriteLine(data.Element(ns+"customer").ToString());

// 결과
// <customer xmlns="http://domain.com/xmlspace">Bloggs</customer>
  • 이름공간을 지정하지 않고 자식 요소 XElement 객체를 생성하면 다시 말해 코드를 다음과 같이 바꾸면,
XNamespace ns = "http://domain.com/xmlspace";
var data = new XElement(ns + "data",
  new XElement("customer", "Bloggs"),
new XElement("purchase", "Bicycle")
);
  • 다음과 같은 XML이 출력된다.
Console.WriteLine(data.ToString());

// 결과
// <data xmlns="http://domain.com/xmlspace>
// <customer xmlns="">Bloggs</customer>
// <purchase xmlns="">Bicycle</purchase>
// </data>
  • 또 다른 함정은 X-DOM을 운행할 때 이름공간을 빼먹는 것이다.
XNamespace ns = "http://domain.com/xmlspace";
var data = new XElement(ns + "data",
  new XElement(ns + "customer", "Bloggs"),
new XElement(ns + "purchase", "Bicycle")
);

XElement x = data.Element(ns + "customer"); // ok
XElement y = data.Element("customer"); // null
  • 이름공간을 지정하지 않고 X-DOM 트리를 구축한 후 나중에 이름공간을 지정할 수도 있다. 다음은 기존 트리의 모든 요소에 하나의 이름공간을 부여하는 예이다.
foreach(XElement e in data.DescendantsAndSelf())
if (e.Name.Namespace == "")
e.Name = ns + e.Name.LocalName;

접두사

  • X-DOM은 접두사 역시 이름공간과 마찬가지로 취급한다. 즉, 접두사는 전적으로 직렬화에만 관여한다. 따라서 X-DOM을 다룰 때는 접두사 문제를 아예 무시해도 된다. X-DOM에서 접두사를 사용하는 유일한 이유는 XML 파일 출력의 효율성이다. 예컨대 다음과 같은 코드를 생각해 보자
XNamespace ns1 = "http://domain.com/space1";
XNamespace ns2 = "http://domain.com/space2";

var mix = new XElement(ns1 + "data",
new XElement(ns2 + "element", "value"), new XElement(ns2 + "element", "value"), new XElement(ns2 + "element", "value")
);
  • 기본적으로 XElement는 다음과 같이 직렬화된다.
<data xmlns="http://domain.com/space1">
<element xmlns="http://domain.com/space2">value</element> <element xmlns="http://domain.com/space2">value</element> <element xmlns="http://domain.com/space2">value</element>
</data>
  • 이러한 출력에 불필요한 중복이 많다는 점이 눈에 띌 것이다. 이에 대한 해결책은 X-DOM 트리의 구축 방식을 바꾸는 것이 아니라, 구축된 트리를 좀 더 효율적으로 XML로 직렬화하도록 X-DOM에 힌트를 주는 것이다.
    • 구체적으로 말하면 다음처럼 적용하고자 하는 접두사를 정의하는 특성들을 추가하면 된다. 흔히 문서의 뿌리 요소에 대해 이런 특성들을 추가한다.
mix.SetAttributeValue(XNamespace.Xmlns + "ns1", ns1);
mix.SetAttributeValue(XNamespace.Xmlns + "ns2", ns2);
  • 이 코드는 XNamepace 형식의 변수 ns1에 “ns1″이라는 접두사를 부여하고 변수 ns2에는 “ns2″를 부여한다. X-DOM은 직렬화시 자동으로 이 특성들을 적용해서 좀 더 간결한 XML을 출력한다. 다음은 이 mix에 대해 ToString을 호출했을 때 나오는 결과이다.
<ns1:data xmlns:ns1="http://domain.com/space1"
xmlns:ns2="http://domain.com/space2">
<ns2:element>value</ns2:element> <ns2:element>value</ns2:element> <ns2:element>value</ns2:element>
</data>
  • 접두사가 X-DOM의 구축, 질의, 갱신 방식을 바꾸지는 않는다. 그런 작업에서는 접두사의 존재를 아예 무시하고 그냥 전체 이름을 사용하면 된다. 접두사는 XML 파일이나 스트림과의 변환에서만 효과를 발휘한다.
  • 특성을 직렬화할 때도 접두사가 적용된다. 다음 예는 dob(date of birth, 생년월일) 특성과 credit 특성을 W3C 표준 특성을 이용해서 “nil”로 설정한 요소를 생성한다.
XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";
var nil = new XAttribute(xsi + "nil", true);

var cust = new XElement("customers",
new XAttribute(XNamespace.Xmlns + "xsi", xsi),
new XElement("customer",
new XElement("lastname", "Bloggs"),
new XElement("dob", nil),
new XElement("credit", nil)
)
);

// 결과
// <customers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
// <customer>
// <lastname>Bloggs</lastname>
// <dob xsi:nil="true"/>
// <credit xsi:nil="true"/>
// </customer>
// </customers>

주해

  • 임의의 XObject에 커스텀 자료를 붙일 수 있다. 그러한 자료를 주해(annotation, 사용자 주석)라고 부른다. 주해는 전적으로 프로그래머 자신을 위한 것으로 X-DOM은 주해를 블랙박스로 취급한다.
    • Windows Forms나 WPF 컨트롤의 Tag 속성을 사용해 본 사람이라면 이런 개념에 익숙할 것이다. Tag와의 차이는 여러 개의 주해를 붙일 수 있다는 점과 주해들을 특정 형식의 전용 범위에 둘 수 있다는 점이다. 즉, 다른 형식들을 덮어 쓰기는 커녕 볼 수도 없는 주해를 만들 수 있다.
  • 다음은 XObject에 주해를 추가하거나 제거하는 메서드들이다.
public void AddAnnotation(object annotation)
public void RemoveAnnotations<T>() where T : class
  • 다음은 부착된 주해들을 조회하는 메서드들이다.
public T Annotations<T>() where T : class
public IEnuemrable<T> Annotations<T> where T : class
  • 각 주해는 해당 형식을 키로 해서 조회된다. 주해의 형식은 반드시 참조 형식이어야 한다. 다음은 string 형식의 주해를 추가하고 조회하는 예이다.
XElement e = new XElement("test");
e.AddAnnotation("Hello");
Console.WriteLine(e.Annotation<string>()); // Hello
  • 한 요소에 같은 형식의 주해를 여러 개 추가할 수 있다. Annotations 메서드는 지정된 형식의 모든 주해를 담은 순차열을 돌려준다.
  • 그런데 string 같은 공용 형식을 키로 사용하는 것은 그리 바람직하지 않다. 그런 주해는 다른 형식들이 얼마든지 접근해서 변경할 수 있기 때문이다. 더 나은 접근 방식은 내부 또는 전용 클래스를 사용하는 것이다.
class X
{
class CustomData { internal string Message; } // 전용의 내포된 클래스

  static void Test()
{
XElement e = new XElement("test");
e.AddAnnotation(new CustomData { Message = "Hello" } );
Console.WriteLine(e.Annotations<CustomeData>().First().Message); // Hello
}
}
  • 주해들을 삭제하려면 반드시 해당 키의 형식에 접근할 수 있어야 한다.
e.RemoveAnnotations<CustomData>();

질의를 X-DOM으로 투영

  • 지금까지는 X-DOM에서 자료를 바깥으로 빼내는 용도로 LINQ를 사용했다. 그와는 반대로 LINQ 질의를 X-DOM 안으로 투영하는 것도 가능하다. LINQ로 질의를 할 수 있는 것이면, 어떤 자료원(data source)이라도 그런 용도로 사용할 수 있다. 이를테면 다음을 X-DOM으로 투영할 수 있다.
    • LINQ to SQL나 Entity Framework 질의
    • 지역 컬렉션
    • 또 다른 X-DOM
  • 자료원이 어떻든 LINQ 질의로부터 X-DOM을 얻는 전략은 동일하다. 우선 원하는 형태의 X-DOM을 산출하느 함수적 생성 표현식을 작성해 본다. 그런 다음 표현식을 감싸는 LINQ 질의를 구축한다.
    • 예컨대 데이터베이스에서 고객 레코드들을 조회해서 다음과 같은 형태의 XML로 출력한다고 하자.
<customers>
<customer id="1">
<name>Sue</name>
<buys>3</buys>
</customer>
...
</customers>
  • 우선 이에 해당하는 X-DOM을 구축하는 함수적 생성 표현식을 작성한다. 일단은 단순한 리터럴들로 고객의 정보를 지정한다.
var customers = 
new XElement("customers",
new XElement("customer", new XAttribute("id", 1),
new XElement("name", "Sue"),
new XElement("buys", 3)
)
);
  • 이제 이를 투영 형태로 바꾸고 LINQ 질의로 감싼다.
var customers = 
new XElement("customers",
  from c in dataContext.Customers
select
new XElement("customer", new XAttribute("id", c.ID),
new XElement("name", c.Name),
new XElement("buys", c.Purchases.Count)
)
);
  • 같은 질의를 두 단계로 구축해 보면 이 전략을 좀 더 잘 이해할 수 있다. 첫 단계로 다음과 같은 질의를 구축한다.
IEnumerable<XElement> sqlQuery =
  from c in dataContext.Customers
select
new XElement("customer", new XAttribute("id", c.ID),
new XElement("name", c.Name),
new XElement("buys", c.Purchases.Count)
);
  • 앞의 질의의 안쪽 부분에 해당하는 이 질의는 그냥 겨로가를 커스텀 형식으로 투영하는 보통의 LINQ to SQL 질의일 뿐이다. 둘째 단계로 이 질의를 인수로 해서 XElement를 생성한다.
var customers = new XElement("customers", sqlQuery);
  • 이 코드는 전체 X-DOM의 뿌리에 해당하는 XElement를 생성한다. 여기서 특기할 점은 sqlQuery가 하나의 XElement 객체가 아니라 IQueryable<XElement>를 구현하는 형식의 객체라는 점 뿐이다. 앞서 언급했듯이 XML 내용을 처리할 때 컬렉션이 자동으로 열거되므로 결과적으로 컬렉션의 모든 XElement가 뿌리 요소의 자식 노드로 추가된다.
  • 이 바깥쪽 질의는 데이터베이스 대상 질의에서 지역의 열거 가능 객체에 대한 LINQ 질의로 전이하는 경계선도 정의한다. XElement의 생성자는 IQeuryable<>에 관해 알지 못하므로 이 시점에서 데이터베이스 질의가 실제로 열거되어서 해당 SQL 질의문이 실행된다.

빈 요소 제거

  • 앞의 예에 이어서 고객의 가장 최근 고가 구매의 세부사항도 결과에 포함한다고 하자. 다음은 앞의 질의를 수정한 버전이다.
var customers = 
new XElement("customers",
  from c in dataContext.Customers
    let lastBigBuy = (from p in c.Purchases
where p.Price > 1000
orderby p.Date descending
select p).FirstOrDefault()
select
new XElement("customer", new XAttribute("id", c.ID),
new XElement("name", c.Name),
new XElement("buys", c.Purchases.Count),
new XElement("lastBigBuy",
new XElement("description", lastBigBuy?.Description,
new XElement("price", lastBigBuy?.Price ?? 0m)
)
);
  • 그런데 고가 구매가 없는 고객의 경우에는 빈 요소가 출력된다. 그런 경우에는 lastBigBuy 노드를 아예 생략하는 것이 더 낫다. 한가지 방법은 lastBigBuy 요소를 생성하는 구문을 다음처럼 조건 연산자로 감싸는 것이다.
select 
new XElement("customer", new XAttribute("id", c.ID),
new XElement("name", c.Name),
new XElement("buys", c.Purchases.Count),
      lastBigBuy == null ? null :
new XElement("lastBigBuy",
new XElement("description", lastBigBuy.Description,
new XElement("price", lastBigBuy.Price)
  • lastBigBuy 요소가 없는 고객의 경우에는 빈 XElement 요소 대신 null이 출력되며, null인 내용은 그냥 무시되므로 원하는 결과가 나온다.

투영의 스트리밍

  • 투영으로 얻은 X-DOM에 대해 그냥 Save 또는 ToString만 호출할 것이라면 XStreamingElement를 이용해서 메모리 효율성을 향상할 수 있다.
    • XElement의 축소 버전인 XStremingElement는 자식 내용에 지연된 실행 의미론을 적용한다는 점이 특징이다.
    • 이 객체를 사용하려면 그냥 바깥 쪽 XElement만 XStreamingElement로 대체하면 된다.
var customers = 
new XStreamingElement("customers",
  from c in dataContext.Customers
select
new XStreamingElement("customer", new XAttribute("id", c.ID),
new XElement("name", c.Name),
new XElement("buys", c.Purchases.Count)
)
);

customers.Save("data.xml");
  • XStreamingElement의 생성자에 전달되는 질의는 XStreamingElement 객체에 대해 Save나 ToString, WriteTo를 호출해야 비로소 열거된다. 즉, 생성시 X-DOM 전체를 메모리에 적재해야 하는 부담이 사라지는 것이다.
    • 단점은 Save를 다시 실행하면 질의도 다시 열거된다는 점이다. 또한 XStreamingElement의 자식 노드들은 운행할 수 없다. XStreamingElement는 Elements나 Attributes 같은 메서드를 노출하지 않기 때문이다.
  • XStreamingElement는 XObject는 물론이고 그 어떤 형식도 상속하지않는다. 그런 만큼 멤버가 아주 적다. 이 형식의 멤버는 Save, ToString, WriteTo 외에 다음 둘 뿐이다.
    • 생성자처럼 내용을 받아서 자식으로 추가하는 Add 메서드
    • Name 속성
  • 내용을 스트리밍 방식으로 읽는 연산은 XStreamingElement가 지원하지 않는다. 그런 작업이 필요하다면 반드시 XmlReader를 X-DOM과 함께 사용해야 한다.

X-DOM의 변환

  • X-DOM을 재투영함으로써 다른 형태의 X-DOM으로 변혼할 수 있다. 예컨대 C# 컴파일러와 Visual Studio가 프로젝트 설정ㅇ르 담는데 사용하는 msbuild XML 파일을 보고서 생성에 적합한 좀 더 단순한 서식으로 바꾼다고 하자.
    • (원본과 변환 예시 생략. 파일 정보만 남긴 형태로 수정하는 예시)
  • 다음은 이러한 변환을 수행하는 질의이다.
XElement project = XElement.Load("myProjectFile.csproj");
XNamespace ns = project.Name.Namespace;

var query =
new XElement("ProjectReport",
from compileItem in
project.Elements(ns + "ItemGroup").Elements(ns + "Compile")
let include = compileItem.Attribute("Include")
where include != null
select new XElement("File", include.Value)
);
  • 이 질의는 우선 모든 ItemGroup 요소를 추출하고 확장 메서드 Elements를 이용해서 자식 Compile 요소들의 평평한 순차열을 얻는다.
    • 이때 XML 이름공간을 지정해야 함을 주의하기 바란다. 원래의 파일에서 모든 요소는 Project 요소에 정의된 이름공간을 상속한다. 그래서 그냥 ItemGroup처럼 지역 이름만 지정하면 원하는 요소들을 얻을 수 없다.
    • 다음으로 Include 특성 값들을 추출해서 각 값을 File 요소로 투영한다.

고급 변환

  • X-DOM 같은 지역 컬렉션에 대한 질의에서는 커스텀 질의 연산자의 작성과 활용에 별 제약이 없다. 커스텀 질의 연산자는 복잡한 질의를 좀 더 효과적으로 표현하는데 도움이 된다.
  • 다음처럼 파일들이 폴더별로 나열된 계통구조 형태의 출력을 원한다고 하자.
<Project>
<File>ObjectGraph.cs</File>
<File>Program.cs</File>
<Folder name="Properties">
<File>AssemblyInfo.cs</File>
</Foder>
<Folder name="Tests">
<File>Aggregation.cs</File>
<Folder name="Advanced">
<File>RecursiveXml.cs</File>
</Folder>
</Folder>
</Project>
  • 이런 결과를 산출하려면 Tests\Advanced\RecursiveXml.cs 같은 문자열을 재귀적으로 처리해야 한다. 다음 메서드가 바로 그러한 작업을 수행한다. 이 메서드는 경로 문자열들의 순차열을 받아서 위의 출력과 같은 계통구조 형태의 X-DOM을 산출한다.
static IEnumerable<XElement> ExpandPaths(IEnumerable<string> paths)
{
var brokenUp =
from path in paths
let split = path.Split(new char[] { '\\' }, 2)
orderby split[0]
select new
{
name = split[0],
remainder = split.ElementAtOrDefault(1)
}

IEnumerable<XElement> files =
from b in brokenUp
where b.remainder == null
select new XElement("file", b.name);

IEnumerable<XElement> folders =
from b in brokenUp
where b.remainder != null
group b.remainder by b.name ito grp
select new XElement("folder",
new XAttribute("name", grp.Key),
ExpandPaths(grp)
);

return files.Concat(folders);
}
  • 첫 질의는 각 경로 문자열을 첫 번째 역슬래시에서 분할해서 앞부분과 뒷부분을 name과 reminder에 배정한다.
Tests\Advanced\RecursiveXml.cs -> Tests + Advanced\RecursiveXml.cs
  • 만일 remainder가 null이면 현재 경로는 보통의 파일 이름이다. files 질의가 이 경우들을 추출한다.
  • 만일 remainder가 null이 아니면 현재 경로는 폴더이다. 이 경우들은 folders가 처리한다. 한 폴더에 여러 개의 파일이 있을 수 있으므로 group by 절을 이용해서 파일들을 폴더 이름별로 묶어야 한다. 그런 다음 각 그룹의 하위 요소들에 대해 동일한 함수를 실행한다.
  • 마지막으로 files의 결과와 folders의 결과를 하나의 순차열로 연결한다. Concat 연산자는 순서를 보존하므로 먼저 모든 파일이 알파벳순으로 나열되고 그 다음에 모든 폴더가 나열된다.
  • 이러한 메서드를 이용해서 실제 질의를 두 단계로 구축해 보자. 우선 경로 문자열들을 하나의 순차열로 추출한다.
IEnuemrable<string> paths =
from compileItem in
project.Elements(ns + "ItemGroup").Elements(ns + "Compile")
let include = compileItem.Attribute("Include")
where include != null
select include.Value;
  • 그런 다음 이 순차열을 ExpandPaths 메서드에 넘겨주어서 최종 결과를 얻는다.
var query = new XElement("Projec", ExpandPaths(paths));

 

[ssba]

The author

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

댓글 남기기

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