1. 도입
LINQ가 등장하면서 .NET에서 XML 작업은 사실상 혁명이었어. 예전엔 XML을 수정하려면 NodeList나 XmlElement 같은 복잡한 구조를 잔뜩 써야 했지만, 이제는 모든 작업을 LINQ 스타일로 표현할 수 있어. 긴 자갈길을 헤매다 고속도로로 진입한 느낌이랄까.
LINQ to XML은 세 가지 주요 문제를 해결해:
- XML을 생성하고 읽는 문법을 단순화해.
- 문서 탐색을 컬렉션 다루듯이 만들어줘.
- XML을 어떤 LINQ 쿼리와도 통합할 수 있게 해.
핵심 클래스: XDocument, XElement, XAttribute
본격적으로 LINQ to XML을 다루기 전에 세 가지 핵심을 알아보자:
- XDocument — 문서 전체의 루트야, 모든 게 들어있는 표지(문서의 루트) 같은 존재야.
- XElement — XML의 개별 노드(요소). 창고의 박스처럼 다른 박스나 상품(요소, 속성, 텍스트)을 담을 수 있어.
- XAttribute — 요소의 속성(예: <User id="42" />).
시각적 구조
+-----------------+
| XDocument |---------------------+
+-----------------+ |
| (Root XElement)
v |
+-------------+ +-----------------+
| XElement |<-------------| XElement (root) |
+-------------+ +-----------------+
| |
(XAttribute) (nested XElements)
2. LINQ to XML로 XML 읽기
우리 학습용 앱(예: 주소록)에서 XML 파일에서 연락처를 읽는다고 하자. 파일 구조는 대략 이렇게 생겼어:
<Contacts>
<Person id="1">
<Name>이반 이바노프</Name>
<Email>ivan@example.com</Email>
<Phones>
<Phone type="mobile">+12 999 123-45-67</Phone>
<Phone type="home">+12 812 123-45-67</Phone>
</Phones>
</Person>
<Person id="2">
<Name>마리야 페트로바</Name>
<Email>maria@example.com</Email>
<Phones>
<Phone type="mobile">+12 921 123-45-67</Phone>
</Phones>
</Person>
</Contacts>
XML을 메모리로 읽기
먼저 프로젝트에 using 지시문을 추가하자:
using System.Xml.Linq;
이제 파일이나 문자열을 읽어보자:
// xmlString이 XML 문서를 포함한다고 가정하거나 XDocument.Load("contacts.xml")를 사용할 수 있어
var doc = XDocument.Parse(xmlString);
// Person 목록을 얻기
var persons = doc.Root.Elements("Person");
foreach (var person in persons)
{
string name = person.Element("Name")?.Value ?? "이름 없음";
string email = person.Element("Email")?.Value ?? "이메일 없음";
string id = person.Attribute("id")?.Value ?? "id 없음";
Console.WriteLine($"이름: {name}, 이메일: {email}, id: {id}");
}
- doc.Root는 루트 요소(Contacts)를 준다.
- Elements("Person")는 모든 <Person>을 반환해.
- 필요한 요소/속성은 .Element("...")나 .Attribute("...")로 가져와.
- 널 안전 연산자 ?.는 요소나 속성이 없을 때 발생하는 오류를 방지해.
비유
XElement로 작업하는 건 마트료시카를 분해하는 것 같아: 하나 꺼내면 그 안에 또 있고, 또 있고. 예전처럼 복잡하게 순회하지 않아도 간단한 쿼리로 끝낼 수 있어.
3. XML에 대해 LINQ 쿼리 작성하기
모든 사람의 모바일 전화번호 목록을 얻고 싶어? LINQ 스타일로 이렇게 하면 돼!
var mobilePhones = doc
.Descendants("Phone") // 문서의 모든 수준에서 Phone을 찾음
.Where(p => (string)p.Attribute("type") == "mobile")
.Select(p => p.Value);
foreach (string phone in mobilePhones)
{
Console.WriteLine($"모바일: {phone}");
}
- Descendants("Phone")는 문서 내 모든 <Phone> 태그를 찾아.
- Attribute("type") — 속성을 가져와.
- (string)로 캐스트하면 속성에서 문자열 또는 null을 자동으로 얻어.
꿀팁
여러 단계의 중첩을 신경 쓰지 않고 바로 검색할 수 있어. 단순한 XmlDocument로는 이런 걸 깔끔하게 하기가 귀찮지.
4. XML을 "즉석에서" 생성 및 수정하기
XElement와 XDocument 생성자를 이용하면 메모리에서 바로 XML을 만들기 편해. 새 사람을 문서에 추가하는 예시(우리의 '주소록 앱'을 키운다고 생각해봐):
// 새 Person 생성
var newPerson = new XElement("Person",
new XAttribute("id", "3"),
new XElement("Name", "세르게이 노비코프"),
new XElement("Email", "sergey@example.com"),
new XElement("Phones",
new XElement("Phone", new XAttribute("type", "work"), "+12 812 987-65-43")
)
);
// 루트에 추가
doc.Root.Add(newPerson);
// 파일(또는 문자열)에 다시 저장
doc.Save("contacts.xml");
설명:
- 중첩된 생성자를 이용해 요소를 넣을 수 있어: 첫 번째 인자는 태그, 나머지는 중첩된 내용(요소, 속성, 텍스트 등)이야.
- 속성은 항상 직접 내용 앞에 와 (예: <Phone type="work">...).
5. C# 컬렉션과 상호작용 — LINQ의 마법
일반적인 객체 컬렉션(예: List<Person>)을 XML로 직렬화하거나 반대로 하는 경우가 많아. 거의 한 줄의 LINQ로 끝낼 수 있어:
var people = new List<Person>
{
new Person { Id = 1, Name = "이반 이바노프", Email = "ivan@example.com" },
// 등등
};
var doc = new XDocument(
new XElement("Contacts",
people.Select(p =>
new XElement("Person",
new XAttribute("id", p.Id),
new XElement("Name", p.Name),
new XElement("Email", p.Email)
)
)
)
);
Person 클래스는 다음과 같아 (전체 그림을 위해):
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
이제 C# 컬렉션을 XML로 변환하는 간단하고 명확한 방법을 알게 되었어!
6. XElement와 XDocument의 유용한 메서드와 팁
탐색
- .Elements("TagName") — 모든 직접적인 자식 요소(직계 자식).
- .Descendants("TagName") — 현재 요소 안의 모든 수준에 있는 요소들.
- .Parent — 부모 요소.
- .Ancestors() — 요소의 모든 조상(경로 찾을 때 유용).
- .FirstNode, .LastNode, .NextNode, .PreviousNode — 이웃 노드로 이동.
조건으로 검색하기
var personsWithEmail = doc.Root
.Elements("Person")
.Where(p => !string.IsNullOrEmpty((string)p.Element("Email")));
추가 및 삭제
// 이미 있는 사용자에게 전화번호 추가
var maria = doc.Root.Elements("Person")
.FirstOrDefault(p => (string)p.Element("Name") == "마리야 페트로바");
maria?.Element("Phones")?.Add(
new XElement("Phone", new XAttribute("type", "work"), "+12 812 111-22-33")
);
// id로 사용자 삭제
doc.Root.Elements("Person")
.Where(p => (string)p.Attribute("id") == "3")
.Remove();
문자열/파일로 다시 변환하기
string xmlText = doc.ToString(); // 들여쓰기 포함
doc.Save("contacts.xml");
7. 알아두면 좋은 요점
LINQ to XML과 다른 접근 방식(.NET) 비교
| 접근 방식 | 사용할 때 | 장점 | 단점 |
|---|---|---|---|
|
구식 코드 유지보수 | XPath와 통합 가능 | 코드가 많고 API가 복잡함 |
|
객체 직렬화용 | C# ↔ XML 직렬화가 간단함 | 유연성이 낮음 |
|
그 외 대부분: 파싱, 수정 | 선형적이고 간결하며 강력함, LINQ와 자연스럽게 결합 | 메모리를 약간 더 사용함 |
실전 프로젝트와 인터뷰에서의 활용
- 명확한 C# 클래스 매핑이 없는 설정 파일 파싱.
- 데이터 임포트/익스포트(예: 사용자 데이터 덤프/로드).
- 마이그레이션, "더럽거나" 복잡한 XML 구조 처리.
- 빠른 변환(예: 한 XML 포맷을 다른 포맷으로 변환).
"왜 이걸 알아야 하지?" — 인터뷰에서 복잡한 XML을 변환하거나 파싱해보라고 요구할 때가 있어. LINQ to XML을 알고 있으면 예전 API보다 훨씬 깔끔하고 빠르게 해결할 수 있어.
8. 흔한 실수
오류 №1: 존재하지 않는 요소에 검사 없이 접근하는 것.
요소가 없을 수 있다면 ?. 연산자나 사전 검사를 사용해. 그렇지 않으면 NullReferenceException가 발생해서 실행 중에 뻗을 수 있어.
오류 №2: 속성(attributes)과 요소(elements)를 혼동하는 것.
속성과 요소는 XML에서 다른 엔티티야. 데이터가 둘 중 하나에 분산될 수 있어. 예를 들어:
<User login="admin"><Status>active</Status></User>
속성 login 값을 얻으려면 .Attribute("login")을 사용하고, 태그 <Status>의 내용을 얻으려면 .Element("Status")을 사용해.
오류 №3: .Remove()의 동작을 잘못 이해하는 것.
.Remove()는 메모리상의 XML 구조를 즉시 변경해. 아무것도 반환하지 않고, 삭제를 "되돌릴" 수 없어. 초보자들은 삭제된 요소를 반환하거나 변경을 알리는 값을 기대하는데, 그런 동작은 하지 않아.
GO TO FULL VERSION