블로그로 돌아가기
C#

c# system.text.json polymorphic serialization 실무 가이드

c# system.text.json polymorphic serialization의 개념과 실무 적용 방법을 코드 예제와 체크리스트로 정리합니다. 상속 구조 직렬화, 커스텀 컨버터, 보안 및 운영 주의점을 다룹니다.

System.Text.Jsonpolymorphic-serializationJSON 직렬화상속-데시리얼라이즈
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
내용JsonDerivedTypeAttributeJsonConverter범용성
내용선언 기반(컴파일 시 안전)코드 기반(세밀한 제어 가능)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로 구현하되 성능 비용을 고려해야 한다.
  • 실무 체크리스트(테스트, 화이트리스트, 로그)를 반드시 적용하라.
  • 타입 변경 시 호환성 정책을 사전에 설계하라.