1. 소개
이미 알다시피 .NET은 기본적으로 공개 필드와 속성을 기반으로 객체를 XML로 직렬화/역직렬화할 수 있어. 그런데 예쁘고 남의 프로그램이나 표준과 호환되는 XML을 만들고 싶거나, XML에 포함하고 싶지 않은 내부 정보를 제외하고 싶을 때는 좀 더 세밀한 설정이 필요하지.
C#에는 이를 위한 직렬화 어트리뷰트가 있어 — 클래스나 속성, 필드에 붙이는 특수한 '태그'야. 이 어트리뷰트들 덕분에 어떤 필드가 XML에 들어갈지, 어떤 건 무시할지, 요소 이름을 뭐로 할지, 속성으로 만들지, 복잡한 구조를 가질지, 심지어 요소들의 순서까지 완전히 제어할 수 있어!
기본 직렬화는 자동으로 양식 채우는 것과 같아: 이름-성-부모 이름이 각 칸에 자동으로 들어가고 나머진 알아서 하는 수준. 근데 어트리뷰트를 쓰면 너는 마치 슈퍼 공무원처럼 각 줄을 어디에 쓸지, 뭐는 건너뛸지, 뭐는 빨갛게 표시할지, 뭐는 비밀 구역에 숨길지 전부 결정할 수 있어.
객체와 XML의 매핑
| 클래스/속성 | 어트리뷰트 | XML 결과 |
|---|---|---|
|
클래스 UserList | |
|
|
|
|
|
id="..." (어트리뷰트) |
|
|
|
|
|
|
|
[XmlIgnore] (RegisteredAtString 참조) | - |
|
|
|
|
|
|
2. 주요 XML 직렬화 어트리뷰트 개요
가장 많이 쓰이는 어트리뷰트들을 쭉 훑어볼게 — .NET 개발자들이 오랜 기간 클래스에 붙여온 것들이야!
어트리뷰트 [XmlElement] — 요소 이름
XML에서 요소 이름을 바꾸거나 속성/필드를 XML 요소로 표시할 때 써.
public class Person
{
[XmlElement("FullName")]
public string Name { get; set; }
}
XML:
<Person>
<FullName>바실리 페트로프</FullName>
</Person>
어트리뷰트를 안 쓰면 기본적으로 요소 이름은 속성/필드 이름과 같아.
어트리뷰트 [XmlAttribute] — XML 속성으로 직렬화
속성/필드를 XML 요소가 아니라 XML 속성으로 직렬화하고 싶을 때 사용해.
public class Person
{
[XmlAttribute("id")]
public int Id { get; set; }
}
XML:
<Person id="123"></Person>
실제 XML API에서는 식별자, 날짜, 플래그 같은 데에 속성을 자주 쓰지.
어트리뷰트 [XmlIgnore] — 필드/속성 건너뛰기
완전한 파라노이아용: 직렬화/역직렬화 과정에서 특정 속성을 완전히 제외하고 싶을 때! 이 어트리뷰트가 붙은 건 최종 XML에 전혀 들어가지 않아.
public class Person
{
[XmlIgnore]
public string InternalNote { get; set; }
}
XML: (InternalNote 속성은 없음)
어트리뷰트들 [XmlArray] 와 [XmlArrayItem] — 컬렉션 제어
배열과 컬렉션 전용 어트리뷰트야. [XmlArray]는 바깥 래퍼 배열 태그 이름을, [XmlArrayItem]은 각 아이템의 태그 이름을 지정해.
public class Person
{
[XmlArray("Phones")]
[XmlArrayItem("Phone")]
public string[] PhoneNumbers { get; set; }
}
XML:
<Person>
<Phones>
<Phone>+12951234567</Phone>
<Phone>+12876543210</Phone>
</Phones>
</Person>
어트리뷰트 [XmlRoot] — 루트 요소 이름
클래스 자체에 붙여서 XML의 루트 요소 이름을 바꿀 수 있어.
[XmlRoot("User")]
public class Person
{
public string Name { get; set; }
}
XML:
<User>
<Name>안나</Name>
</User>
어트리뷰트 [XmlText] — 요소의 텍스트 내용으로 직렬화
때로는 속성 값이 요소 안의 단순 텍스트로 들어가야 할 때가 있어 — 하위 요소나 속성이 아니라 요소의 텍스트 내용으로.
public class Note
{
[XmlText]
public string Content { get; set; }
}
XML:
<Note>할머니에게 전화해!</Note>
어트리뷰트들 [XmlNamespaceDeclarations], [XmlElement(Type = ...)]
네임스페이스를 다루거나 상속을 지원하는 등 좀 더 고급 시나리오를 위해서는 더 세부적인 어트리뷰트들이 있어. 전체 목록은 공식 XML 직렬화 문서를 참고해.
3. 실습: 실제 예제로 직렬화 개선하기
한 단계씩 우리 학습용 앱을 'XML 직렬화 프리미엄'으로 다듬어 보자 — 엄격한 XML 검사자도 만족하게 만들 거야.
어트리뷰트 없는 기본 클래스
예를 들어 이런 사용자 클래스가 있다고 해보자:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string[] Emails { get; set; }
public DateTime RegisteredAt { get; set; }
public string Password { get; set; }
}
기본 직렬화는 대략 이런 XML을 만들어 (날짜는 자체 포맷으로 들어감):
<User>
<Id>42</Id>
<Name>Ivan Ivanov</Name>
<Emails>
<string>user@mail.com</string>
<string>admin@site.org</string>
</Emails>
<RegisteredAt>2024-07-01T00:00:00</RegisteredAt>
<Password>qwerty</Password>
</User>
문제점:
- 비밀번호는 차라리 직렬화하지 않는 게 안전해.
- 날짜는 속성으로 사람 친화적인 포맷을 원해.
- Emails를 더 예쁘게 표현하고 싶어.
- 루트 요소를 <Person>로 바꾸고 싶어.
어트리뷰트로 설정 추가하기
[XmlRoot("Person")]
public class User
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlElement("FullName")]
public string Name { get; set; }
[XmlArray("EMails")]
[XmlArrayItem("Email")]
public string[] Emails { get; set; }
[XmlAttribute("registered")]
public string RegisteredAtString
{
get => RegisteredAt.ToString("yyyy-MM-dd");
set => RegisteredAt = DateTime.Parse(value);
}
[XmlIgnore]
public string Password { get; set; }
[XmlIgnore]
public DateTime RegisteredAt { get; set; }
}
세부 설명:
- 날짜를 속성으로 만들기 위해 [XmlAttribute("registered")]를 사용했어.
- DateTime 자체를 직렬화하는 대신 문자열 표현을 직렬화하려고, 별도의 속성 RegisteredAtString을 추가하고 실제 필드는 [XmlIgnore]로 숨겼어.
- 비밀번호는 [XmlIgnore]로 숨김.
- 이메일 배열은 바깥 태그를 <EMails>로, 각 이메일을 <Email>로 지정했어.
결과:
<Person id="42" registered="2024-07-01">
<FullName>Ivan Ivanov</FullName>
<EMails>
<Email>user@mail.com</Email>
<Email>admin@site.org</Email>
</EMails>
</Person>
4. 중첩 객체 다루기
사용자와 주소 같은 중첩 객체가 있으면 XML 표현이 더 진가를 발휘해.
public class Address
{
[XmlAttribute]
public string City { get; set; }
[XmlText]
public string Details { get; set; }
}
public class User
{
// ...다른 속성들, 위를 참고...
public Address[] Addresses { get; set; }
}
주소 배열에 대한 설정을 추가해보자:
[XmlArray("UserAddresses")]
[XmlArrayItem("Address")]
public Address[] Addresses { get; set; }
그럼 XML은 이렇게 될 거야:
<Person id="42" registered="2024-07-01">
<FullName>Ivan Ivanov</FullName>
<EMails>
<Email>user@mail.com</Email>
</EMails>
<UserAddresses>
<Address City="Neon city">잠코바야 거리, 1번지</Address>
<Address City="North Cave">크라이냐야 거리, 12번지</Address>
</UserAddresses>
</Person>
각 <Address>에서 city는 어트리뷰트로 들어가고, 주소 본문은 요소의 텍스트로 들어간다는 걸 볼 수 있어.
5. 자주 하는 실수와 주의점
복잡한 객체를 직렬화했는데 기대한 대로 XML이 안 바뀌었다면 — 어트리뷰트를 제대로 붙이는 걸 깜빡했을 가능성이 높아. 어떤 속성들이 마킹되지 않으면 .NET은 그냥 기본 동작을 선택해.
또 한 가지 함정은 컬렉션 이름 문제야. 예를 들어 문자열 배열에 [XmlArray]나 [XmlArrayItem]를 안 붙이면 요소들이 string 태그로 기록될 수 있어 (위의 초기 예 참고). 원하지 않으면 항상 명시적으로 태그 이름을 지정해줘.
getter-only(읽기 전용) 속성은 기본적으로 역직렬화되지 않으니 주의해. 가능하면 public set을 만들어 줘.
사전(Dictionary)이나 인터페이스 같은 일부 타입은 기본 XmlSerializer로 직렬화되지 않거나 별도 처리가 필요해.
GO TO FULL VERSION