c# system.text.json polymorphic serialization 실무 가이드
c# system.text.json polymorphic serialization의 개념과 실무 적용 방법을 코드 예제와 체크리스트로 정리합니다. 상속 구조 직렬화, 커스텀 컨버터, 보안 및 운영 주의점을 다룹니다.
c# system.text.json polymorphic serialization이 필요한 상황
c# system.text.json polymorphic serialization을 통해 상속(인터페이스/추상 클래스 포함) 기반 객체를 JSON으로 직렬화/역직렬화할 때 발생하는 문제를 해결하는 방법을 배우게 됩니다. 이 글에서는 개념 설명, JsonSerializerOptions와 JsonConverter 사용법, JsonDerivedTypeAttribute 활용, 실무 예제 코드, 주의점과 체크리스트까지 제공합니다. 목표는 바로 적용 가능한 코드와 운영·보안 관점의 점검 항목을 얻는 것입니다.
c# system.text.json polymorphic serialization 핵심 개념
- 폴리모픽 직렬화란 상위 타입 변수에 실제 하위 타입 인스턴스가 들어있을 때, 하위 타입 정보를 유지해 직렬화/역직렬화하는 것을 말합니다.
- System.Text.Json은 기본적으로 정적 타입(컴파일 시 타입) 기준으로 직렬화합니다. 따라서 상위 타입으로 선언된 컬렉션을 역직렬화하면 하위 타입 정보가 손실됩니다.
- 해결책: 타입 식별자(type discriminator)를 JSON에 포함하거나, 커스텀 JsonConverter를 등록하거나, .NET 7+의 JsonDerivedTypeAttribute를 사용합니다.
c# system.text.json polymorphic serialization 실무 예제
다음 예제는 추상 클래스와 두 개의 하위 클래스를 폴리모픽하게 직렬화/역직렬화하는 방법입니다.
using System; using System.Text.Json; using System.Text.Json.Serialization; using System.Collections.Generic; public abstract class Animal { public string Name { get; set; } } public class Dog : Animal { public bool Barks { get; set; } } public class Cat : Animal { public int Lives { get; set; } } // .NET 7+: JsonDerivedTypeAttribute를 사용하는 방법 [JsonDerivedType(typeof(Dog), "dog")] [JsonDerivedType(typeof(Cat), "cat")] public abstract partial class AnimalWithAttributes { } // 커스텀 컨버터 예시 (간단한 디스크리미네이터 방식) public class AnimalConverter : JsonConverter<Animal> { public override Animal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using var doc = JsonDocument.ParseValue(ref reader); if (!doc.RootElement.TryGetProperty("_type", out var t)) throw new JsonException("Missing _type discriminator"); var type = t.GetString(); var json = doc.RootElement.GetRawText(); return type switch { "dog" => JsonSerializer.Deserialize<Dog>(json, options), "cat" => JsonSerializer.Deserialize<Cat>(json, options), _ => throw new JsonException("Unknown type") }; } public override void Write(Utf8JsonWriter writer, Animal value, JsonSerializerOptions options) { writer.WriteStartObject(); if (value is Dog d) { writer.WriteString("_type", "dog"); writer.WriteString("Name", d.Name); writer.WriteBoolean("Barks", d.Barks); } else if (value is Cat c) { writer.WriteString("_type", "cat"); writer.WriteString("Name", c.Name); writer.WriteNumber("Lives", c.Lives); } writer.WriteEndObject(); } } // 사용 예 var options = new JsonSerializerOptions { WriteIndented = true }; options.Converters.Add(new AnimalConverter()); var animals = new List<Animal> { new Dog { Name = "Rex", Barks = true }, new Cat { Name = "Neko", Lives = 9 } }; var json = JsonSerializer.Serialize(animals, options); Console.WriteLine(json); var deserialized = JsonSerializer.Deserialize<List<Animal>>(json, options);
이 코드는 단순한 디스크리미네이터("_type") 필드를 JSON에 포함시키는 방식입니다. 실무에서는 더 견고한 타입 매핑, 네임스페이스 포함 여부, 버전 관리 등을 고려하세요.
JsonDerivedTypeAttribute와 JsonConverter 비교
| 구분 | 항목1 | 항목2 | 항목3 |
|---|---|---|---|
| 내용 | JsonDerivedTypeAttribute | JsonConverter | 범용성 |
| 내용 | 선언 기반(컴파일 시 안전) | 코드 기반(세밀한 제어 가능) | JsonConverter가 더 유연하지만 구현 비용이 큼 |
언제 쓰면 좋고 언제 피해야 할까
-
언제 쓰면 좋은가
- 서버/클라이언트 간에 상속 구조 객체를 주고받아야 할 때
- 이벤트 페이로드나 메시지에서 다양한 하위 타입을 담아야 할 때
- 명확한 타입 디스크리미네이터를 프로토콜에 추가할 수 있을 때
-
언제 피해야 하는가
- 단일 DTO만 사용하는 단순 API (불필요한 복잡성 추가 금지)
- 외부에서 임의 타입을 주입할 수 있는 API (보안 리스크 존재)
- 대규모 동적 타입 계층이 자주 변하는 경우(버전 관리 부담)
실무 체크리스트
-
코드 단계
- JsonDerivedTypeAttribute 또는 JsonConverter 중 팀 표준을 정했는가?
- 역직렬화할 때 디스크리미네이터가 항상 포함되는가?
- 디스크리미네이터 값에 대한 허용 목록(화이트리스트)이 있는가?
-
테스트 단계
- 하위 타입별 직렬화/역직렬화 단위 테스트가 있는가?
- 잘못된/알 수 없는 _type 값에 대한 처리 테스트가 있는가?
-
배포·운영 단계
- 로그에 민감 데이터가 포함되지 않는가?
- 마이그레이션 시 타입 이름 변경 정책을 마련했는가?
성능·보안·운영 주의점
-
성능
- 커스텀 컨버터는 유연하지만 매번 JsonDocument 파싱 등으로 비용이 발생할 수 있습니다. 대량 데이터 처리 시 벤치마크를 권장합니다.
- System.Text.Json은 Utf8 기반으로 GC 압박이 적지만, 중간 문자열 생성(예: GetRawText) 사용 시 메모리 할당이 늘어납니다.
-
보안
- 역직렬화 시 임의 타입 로딩을 허용하면 원격 코드 실행 같은 위험이 있습니다. 반드시 허용된 타입 목록(화이트리스트)을 사용하세요.
- 공개 API에 타입 식별자를 노출할 때 내부 구현을 드러내지 않도록 주의하세요. 필요하면 외부용 별도의 매핑을 사용하세요.
-
운영
- 타입 이름 변경/네임스페이스 변경 시 호환성 문제 발생. 버전 표기나 별도의 타입 매핑 레이어를 설계하세요.
- 로그·모니터링: 실패 역직렬화 이벤트를 수집해 어떤 디스크리미네이터 값이 들어오는지 분석하세요.
구현 판단 기준표
| 구분 | 항목1 | 항목2 | 항목3 |
|---|---|---|---|
| 내용 | 단순 DTO | 소수의 하위 타입 | 다수/빈번 변경되는 타입 |
| 내용 | 기본 직렬화 사용 | JsonDerivedTypeAttribute 권장 | JsonConverter/커스텀 매핑 권장 |
자주 묻는 질문
Q1: System.Text.Json만으로 폴리모픽 직렬화를 완전히 자동화할 수 있나요?
A1: 완전히 자동화하려면 .NET 7 이상의 JsonDerivedTypeAttribute를 사용하면 선언 기반으로 간단해집니다. 하지만 역직렬화 시 허용 목록 제어나 복잡한 매핑이 필요하면 JsonConverter가 필요합니다.
Q2: Newtonsoft.Json의 TypeNameHandling과 같은 기능을 System.Text.Json에서 쓸 수 있나요?
A2: System.Text.Json은 기본적으로 TypeNameHandling 같은 완전 자동 타입 주입 기능을 제공하지 않습니다. 보안상 위험이 있어 의도적으로 제한되어 있습니다. 안전한 폴리모픽 지원은 JsonDerivedTypeAttribute 또는 커스텀 컨버터로 구현합니다.
Q3: 디스크리미네이터 필드를 숨기고 싶습니다. 가능한가요?
A3: 가능하지만 권장되지 않습니다. 내부적으로 매핑 테이블을 두어 외부에 공개되는 값과 내부 타입을 분리하면 보안성과 유연성을 높일 수 있습니다. 또한 API 버전 관리 시 변경 영향을 줄이려면 별도 매핑이 유리합니다.
Q4: 성능 우려가 있는데, 대량 메시지 처리에 추천하는 방식은?
A4: 대량 처리 시에는 JsonConverter 구현을 최적화해 불필요한 JsonDocument 생성이나 문자열 복사를 줄이세요. Utf8JsonReader/Utf8JsonWriter 직접 사용으로 비용을 낮출 수 있습니다.
정리
- c# system.text.json polymorphic serialization은 상속 구조 직렬화 문제를 해결하는 기술이다.
- JsonDerivedTypeAttribute는 선언 기반으로 간단히 적용 가능하고 .NET 버전 의존성이 있다.
- 복잡한 매핑이나 보안 제어는 JsonConverter로 구현하되 성능 비용을 고려해야 한다.
- 실무 체크리스트(테스트, 화이트리스트, 로그)를 반드시 적용하라.
- 타입 변경 시 호환성 정책을 사전에 설계하라.