XML은 시스템 간 계약을 강하게 유지하고, 스키마 검증과 호환성을 갖추기 좋습니다. 여기서는 XmlSerializer를 중심으로 성능, 버전 호환, 다형성, 보안, 커스텀 직렬화 등의 고급 기법을 간략히 정리합니다.
1. 성능 최적화와 안전한 리더/라이터 설정
XmlSerializer는 최초 생성 시 동적 어셈블리를 빌드하므로, 인스턴스를 재사용(캐싱)하는 것이 중요합니다. 또한 XML 파서 보안 설정을 명시해 XXE 같은 취약점을 차단합니다.
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
public static class XmlSer
{
private static readonly ConcurrentDictionary<string, XmlSerializer> Cache = new();
public static XmlSerializer Get<T>(XmlRootAttribute? root = null)
{
var key = typeof(T).AssemblyQualifiedName + "|" + (root?.ElementName ?? "") + "|" + (root?.Namespace ?? "");
return Cache.GetOrAdd(key, _ => root == null ? new XmlSerializer(typeof(T)) : new XmlSerializer(typeof(T), root));
}
public static string ToXml<T>(T obj, XmlSerializerNamespaces? ns = null)
{
var settings = new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
NewLineHandling = NewLineHandling.Replace
};
using var sw = new StringWriter();
using var xw = XmlWriter.Create(sw, settings);
Get<T>().Serialize(xw, obj, ns);
return sw.ToString();
}
public static T FromXml<T>(string xml, XmlReaderSettings? readerSettings = null)
{
readerSettings ??= new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null
};
using var sr = new StringReader(xml);
using var xr = XmlReader.Create(sr, readerSettings);
return (T)Get<T>().Deserialize(xr)!;
}
}팁: 빌드 타임 프리컴파일이 필요하면 sgen을 사용합니다. 단, 타입+오버라이드 조합마다 하나의 XmlSerializer를 재사용해야 메모리 누수를 방지합니다.
2. 네임스페이스/루트 제어
스키마 호환과 버전 네임스페이스를 명확히 관리합니다. XmlSerializerNamespaces로 프리픽스를 부여하고 [XmlRoot], [XmlType]으로 네임스페이스를 고정합니다.
using System.Collections.Generic;
using System.Xml.Serialization;
[XmlRoot("Order", Namespace = "urn:sample:v1")]
[XmlType(Namespace = "urn:sample:v1")]
public class Order
{
[XmlAttribute] public string Id { get; set; } = "";
[XmlElement("Item")] public List<OrderItem> Items { get; set; } = new();
[XmlAnyElement] public System.Xml.XmlElement[]? Extras { get; set; }
public bool ShouldSerializeExtras() => Extras != null && Extras.Length > 0;
}
public static class Ns
{
public static XmlSerializerNamespaces App()
{
var ns = new XmlSerializerNamespaces();
ns.Add("app", "urn:sample:v1");
return ns;
}
}
// 사용
// var xml = XmlSer.ToXml(new Order { Id = "A-100" }, Ns.App());런타임에서 루트를 바꾸고 싶다면 XmlRootAttribute로 오버라이드합니다.
var rootV2 = new XmlRootAttribute("purchaseOrder") { Namespace = "urn:sample:v2" };
var ser = XmlSer.Get<Order>(rootV2);
// ser.Serialize(...)3. 다형성(상속)과 컬렉션
상속형 데이터를 직렬화하려면 [XmlInclude] 또는 [XmlElement(Type=...)]를 사용합니다.
[XmlInclude(typeof(BookItem))]
public class OrderItem
{
[XmlAttribute] public string Sku { get; set; } = "";
[XmlAttribute] public int Qty { get; set; }
}
public class BookItem : OrderItem
{
[XmlAttribute] public string Isbn { get; set; } = "";
}
// 예시
var order = new Order
{
Id = "A-101",
Items = new List<OrderItem> { new BookItem { Sku = "B-1", Qty = 2, Isbn = "9781234567890" } }
};
var xml = XmlSer.ToXml(order, Ns.App());4. 버전 호환: 확장 필드, 알 수 없는 노드, 선택적 필드
미래 확장과 뒤로 호환을 위해 여유 필드를 수용합니다.
- [XmlAnyElement]/[XmlAnyAttribute]로 알 수 없는 요소를 수집
- UnknownElement/UnknownAttribute 이벤트로 로깅
- ShouldSerializeXxx, XxxSpecified 패턴으로 선택적 필드 제어
// 이벤트 사용(단발성 사용 시 별도 인스턴스 권장)
var local = new XmlSerializer(typeof(Order));
local.UnknownElement += (s, e) => System.Diagnostics.Debug.WriteLine($"Unknown: {e.Element.Name}");
using var sr = new StringReader(xml);
var loaded = (Order)local.Deserialize(sr)!;
// ShouldSerialize / Specified 패턴
public class Customer
{
public int Age { get; set; }
public bool ShouldSerializeAge() => Age > 0;
public bool Active { get; set; }
[XmlIgnore] public bool ActiveSpecified { get; set; } // true일 때만 Active 요소 출력
}5. XmlAttributeOverrides로 런타임 매핑 변경
스키마가 바뀌었거나 외부 XML과 이름을 맞춰야 할 때 유용합니다.
var overrides = new XmlAttributeOverrides();
// Sku -> ProductCode로 요소 이름 변경
var skuAttrs = new XmlAttributes();
skuAttrs.XmlElements.Add(new XmlElementAttribute("ProductCode"));
overrides.Add(typeof(OrderItem), nameof(OrderItem.Sku), skuAttrs);
// Qty 무시
var qtyAttrs = new XmlAttributes { XmlIgnore = true };
overrides.Add(typeof(OrderItem), nameof(OrderItem.Qty), qtyAttrs);
var ser2 = new XmlSerializer(typeof(Order), overrides);
// ser2.Serialize(...)주의: XmlAttributeOverrides를 자주 생성하면 내부 동적 어셈블리가 누적될 수 있으므로 조합별로 캐싱하십시오.
6. IXmlSerializable로 완전 커스텀
표준 속성/요소 매핑으로 표현하기 어려운 경우 직접 읽기/쓰기를 구현합니다. 스키마는 GetSchema에서 null을 반환합니다.
using System.Globalization;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
public struct Point : IXmlSerializable
{
public int X { get; set; }
public int Y { get; set; }
public XmlSchema? GetSchema() => null;
public void ReadXml(XmlReader reader)
{
X = int.Parse(reader.GetAttribute("x") ?? "0", CultureInfo.InvariantCulture);
Y = int.Parse(reader.GetAttribute("y") ?? "0", CultureInfo.InvariantCulture);
reader.Read(); // <Point ... /> 소비
}
public void WriteXml(XmlWriter writer)
{
writer.WriteAttributeString("x", X.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("y", Y.ToString(CultureInfo.InvariantCulture));
}
}주의: ReadXml/WriteXml에서 요소 경계 제어를 명확히 하십시오. 컬처 독립 처리를 위해 XmlConvert 또는 InvariantCulture를 사용합니다.
7. 날짜/시간, 사전(Dictionary) 등 특수 타입
DateTime은 Kind/타임존 혼동을 피해야 합니다. 선택형 날짜는 문자열 프록시로 라운드트립합니다.
using System;
using System.Xml;
using System.Xml.Serialization;
public class EventStamp
{
[XmlElement(DataType = "dateTime")]
public DateTime CreatedUtc { get; set; } = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc);
[XmlIgnore]
public DateTime? OptionalWhen { get; set; }
[XmlElement("OptionalWhen")]
public string? OptionalWhenString
{
get => OptionalWhen.HasValue ? XmlConvert.ToString(OptionalWhen.Value, XmlDateTimeSerializationMode.RoundtripKind) : null;
set => OptionalWhen = string.IsNullOrEmpty(value)
? null
: XmlConvert.ToDateTime(value, XmlDateTimeSerializationMode.RoundtripKind);
}
}Dictionary는 기본적으로 바로 직렬화되지 않습니다. 프록시 리스트로 우회합니다.
using System.Collections.Generic;
using System.Linq;
using System.Xml.Serialization;
public class MapItem
{
[XmlAttribute] public string Key { get; set; } = "";
[XmlElement] public string Value { get; set; } = "";
}
public class Settings
{
[XmlIgnore] public Dictionary<string, string> Map { get; set; } = new();
[XmlArray("Map"), XmlArrayItem("Item")]
public List<MapItem> MapItems
{
get => Map.Select(kv => new MapItem { Key = kv.Key, Value = kv.Value }).ToList();
set => Map = value?.ToDictionary(i => i.Key, i => i.Value) ?? new Dictionary<string, string>();
}
}8. 스키마(XSD) 검증과 안정적 역직렬화
읽는 단계에서 XSD로 검증하면 조기 실패와 품질 보증에 유리합니다.
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
public static class XmlValidation
{
public static T DeserializeWithValidation<T>(string xml, XmlSchemaSet schemas)
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
ValidationType = ValidationType.Schema,
Schemas = schemas
};
settings.ValidationEventHandler += (s, e) => throw new XmlSchemaValidationException(e.Message, e.Exception);
var ser = new XmlSerializer(typeof(T));
using var sr = new StringReader(xml);
using var xr = XmlReader.Create(sr, settings);
return (T)ser.Deserialize(xr)!;
}
}9. 체크리스트
- Serializer 재사용(캐싱)으로 성능 최적화
- DtdProcessing.Prohibit, XmlResolver = null로 보안 강화
- 네임스페이스/루트/요소명 명시 및 XmlAttributeOverrides로 대응
- [XmlInclude], [XmlAnyElement], ShouldSerialize/XxxSpecified로 확장성 확보
- 필요 시 IXmlSerializable로 커스텀 직렬화 구현
- DateTime은 XmlConvert와 UTC/라운드트립 준수
- 사전/복잡 타입은 프록시로 변환
- 스키마 검증과 라운드트립 테스트로 안정성 확보
위 패턴을 템플릿으로 삼아 프로젝트의 XML 계약을 견고하고 유지보수 가능하게 설계하시길 바랍니다.
'C#' 카테고리의 다른 글
| C# Stack<T>와 Queue<T> 성능 비교 및 활용 사례 (0) | 2026.07.21 |
|---|---|
| C# Mutex를 이용한 프로세스 간 동기화 (0) | 2026.07.16 |
| C# async/await에서 ConfigureAwait(false) 사용 이유 (0) | 2026.07.15 |
| C# 리플렉션으로 애셈블리 의존성 분석하기 (0) | 2026.07.15 |
| C# 고정(Fixed) 키워드로 네이티브 메모리 접근 (0) | 2026.07.15 |