Community코딩 & 개발github.com

직렬화

.NET 애플리케이션에 적합한 직렬화 형식을 선택합니다. 리플렉션 기반(Newtonsoft.Json)보다는 스키마 기반 형식(Protobuf, MessagePack)을 선호합니다. JSON 시나리오의 경우 System.Text.Json과 AOT 소스 생성기를 사용합니다.

직렬화란 무엇인가요?

이 스킬은 .NET 개발자가 직렬화 형식을 선택하고 구현할 때 모범 사례를 따르도록 돕습니다. API, 메시징, 영속성 등 다양한 사용 사례에 대해 Protobuf, MessagePack, System.Text.Json(소스 생성기 포함)과 같은 스키마 기반 형식을 권장하며, 성능, AOT 호환성, 유지보수성에서 우수합니다. Newtonsoft.Json에서 System.Text.Json으로의 마이그레이션 패턴, 다형성 처리, 와이어 호환성 패턴(관용적 리더, 쓰기 전 읽기 도입)도 상세히 다룹니다. Akka.NET 액터 시스템에서의 직렬화 설정과 함께, BinaryFormatter 사용 금지, 형식 이름 포함 금지 등 반드시 피해야 할 패턴도 명시하여 실무에서 바로 적용 가능한 가이드를 제공합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/serialization

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

.NET에서의 직렬화

이 스킬을 사용해야 하는 경우

다음과 같은 경우 이 스킬을 사용하세요:

  • API, 메시징, 영속성을 위한 직렬화 형식 선택
  • Newtonsoft.Json에서 System.Text.Json으로 마이그레이션
  • AOT 호환 직렬화 구현
  • 분산 시스템을 위한 와이어 형식 설계
  • 직렬화 성능 최적화

스키마 기반 vs 리플렉션 기반

항목스키마 기반리플렉션 기반
예시Protobuf, MessagePack, System.Text.Json (소스 생성)Newtonsoft.Json, BinaryFormatter
페이로드 내 타입 정보없음 (외부 스키마)있음 (타입 이름 포함)
버전 관리명시적 필드 번호/이름암시적 (타입 구조)
성능빠름 (리플렉션 없음)느림 (런타임 리플렉션)
AOT 호환예아니요
와이어 호환성우수낮음

권장: 프로세스 경계를 넘는 모든 작업에는 스키마 기반 직렬화를 사용하세요.


형식 권장 사항

사용 사례권장 형식이유
REST APISystem.Text.Json (소스 생성)표준, AOT 호환
gRPCProtocol Buffers네이티브 형식, 우수한 버전 관리
액터 메시징MessagePack 또는 Protobuf간결, 빠름, 버전 안전
이벤트 소싱Protobuf 또는 MessagePack오래된 이벤트를 영원히 처리해야 함
캐싱MessagePack간결, 빠름
구성JSON (System.Text.Json)사람이 읽기 쉬움
로깅JSON (System.Text.Json)구조화, 파싱 가능

피해야 할 형식

형식문제점
BinaryFormatter보안 취약점, 사용 중단됨, 절대 사용 금지
Newtonsoft.Json 기본값페이로드에 타입 이름 포함되어 이름 변경 시 문제 발생
DataContractSerializer복잡, 버전 관리 불량
XML장황, 느림, 복잡

소스 생성기를 사용한 System.Text.Json

JSON 직렬화의 경우 AOT 호환성과 성능을 위해 소스 생성기와 함께 System.Text.Json을 사용하세요.

설정

// 모든 타입으로 JsonSerializerContext 정의
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(OrderItem))]
[JsonSerializable(typeof(Customer))]
[JsonSerializable(typeof(List<Order>))]
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class AppJsonContext : JsonSerializerContext { }

사용법

// 컨텍스트로 직렬화
var json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);

// 컨텍스트로 역직렬화
var order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);

// ASP.NET Core에서 구성
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});

이점

  • 런타임에 리플렉션 없음 - 모든 타입 정보가 컴파일 타임에 생성됨
  • AOT 호환 - Native AOT 게시와 함께 작동
  • 더 빠름 - 런타임 타입 분석 없음
  • 트림 안전 - 링커가 정확히 필요한 것을 알고 있음

Protocol Buffers (Protobuf)

적합한 대상: 액터 시스템, gRPC, 이벤트 소싱, 장기 유지되는 와이어 형식.

설정

dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

스키마 정의

// orders.proto
syntax = "proto3";

message Order {
    string id = 1;
    string customer_id = 2;
    repeated OrderItem items = 3;
    int64 created_at_ticks = 4;

    // 새 필드 추가는 항상 안전합니다
    string notes = 5;  // v2에서 추가됨 - 이전 리더는 무시함
}

message OrderItem {
    string product_id = 1;
    int32 quantity = 2;
    int64 price_cents = 3;
}

버전 관리 규칙

// 안전: 새 번호로 새 필드 추가
message Order {
    string id = 1;
    string customer_id = 2;
    string shipping_address = 5;  // 새 항목 - 안전
}

// 안전: 필드 제거 (이전 리더는 알 수 없는 필드 무시, 새 리더는 기본값 사용)
// 필드 사용을 중지하고 번호를 예약하세요
message Order {
    string id = 1;
    // customer_id가 제거되었지만 필드 2는 예약됨
    reserved 2;
}

// 안전하지 않음: 필드 타입 변경
message Order {
    int32 id = 1;  // 이전: string - 손상됨!
}

// 안전하지 않음: 필드 번호 재사용
message Order {
    reserved 2;
    string new_field = 2;  // 2번 재사용 - 손상됨!
}

MessagePack

적합한 대상: 고성능 시나리오, 간결한 페이로드, 액터 메시징.

설정

dotnet add package MessagePack
dotnet add package MessagePack.Annotations

계약과 함께 사용

[MessagePackObject]
public sealed class Order
{
    [Key(0)]
    public required string Id { get; init; }

    [Key(1)]
    public required string CustomerId { get; init; }

    [Key(2)]
    public required IReadOnlyList<OrderItem> Items { get; init; }

    [Key(3)]
    public required DateTimeOffset CreatedAt { get; init; }

    // 새 필드 - 이전 리더는 알 수 없는 키를 건너뜀
    [Key(4)]
    public string? Notes { get; init; }
}

// 직렬화
var bytes = MessagePackSerializer.Serialize(order);

// 역직렬화
var order = MessagePackSerializer.Deserialize<Order>(bytes);

AOT 호환 설정

// AOT를 위해 소스 생성기 사용
[MessagePackObject]
public partial class Order { }  // partial로 소스 생성 활성화

// 리졸버 구성
var options = MessagePackSerializerOptions.Standard
    .WithResolver(CompositeResolver.Create(
        GeneratedResolver.Instance,  // 생성된 리졸버
        StandardResolver.Instance));

Newtonsoft.Json에서 마이그레이션

일반적인 문제

NewtonsoftSystem.Text.Json해결 방법
JSON의 $type기본적으로 지원되지 않음판별자 또는 사용자 지정 변환기 사용
JsonPropertyJsonPropertyName다른 특성
DefaultValueHandlingDefaultIgnoreCondition다른 API
NullValueHandlingDefaultIgnoreCondition다른 API
전용 setter[JsonInclude] 필요명시적 옵트인
다형성[JsonDerivedType] (.NET 7+)명시적 판별자

마이그레이션 패턴

// Newtonsoft (리플렉션 기반)
public class Order
{
    [JsonProperty("order_id")]
    public string Id { get; set; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string? Notes { get; set; }
}

// System.Text.Json (소스 생성 호환)
public sealed record Order(
    [property: JsonPropertyName("order_id")]
    string Id,

    string? Notes  // JsonSerializerOptions를 통한 null 처리
);

[JsonSerializable(typeof(Order))]
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class OrderJsonContext : JsonSerializerContext { }

판별자를 사용한 다형성

// .NET 7+ 다형성
[JsonDerivedType(typeof(CreditCardPayment), "credit_card")]
[JsonDerivedType(typeof(BankTransferPayment), "bank_transfer")]
public abstract record Payment(decimal Amount);

public sealed record CreditCardPayment(decimal Amount, string Last4) : Payment(Amount);
public sealed record BankTransferPayment(decimal Amount, string AccountNumber) : Payment(Amount);

// 다음과 같이 직렬화됨:
// { "$type": "credit_card", "amount": 100, "last4": "1234" }

와이어 호환성 패턴

관용적 리더

이전 코드는 알 수 없는 필드를 안전하게 무시해야 합니다:

// Protobuf/MessagePack: 자동 - 알 수 없는 필드는 건너뜀
// System.Text.Json: 허용하도록 구성
var options = new JsonSerializerOptions
{
    UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip
};

쓰기 전 읽기 도입

새 형식에 대해 직렬화 도구보다 역직렬화 도구를 먼저 배포하세요:

// 1단계: 역직렬화 추가 (모든 곳에 배포)
public Order Deserialize(byte[] data, string manifest) => manifest switch
{
    "Order.V1" => DeserializeV1(data),
    "Order.V2" => DeserializeV2(data),  // 새 항목 - V2 읽기 가능
    _ => throw new NotSupportedException()
};

// 2단계: 직렬화 활성화 (다음 릴리스, V1이 모든 곳에 배포된 후)
public (byte[] data, string manifest) Serialize(Order order) =>
    _useV2Format
        ? (SerializeV2(order), "Order.V2")
        : (SerializeV1(order), "Order.V1");

타입 이름을 포함하지 마세요

// 나쁨: 페이로드에 타입 이름 포함 - 클래스 이름 변경 시 와이어 형식이 깨짐
{
    "$type": "MyApp.Order, MyApp.Core",
    "id": "123"
}

// 좋음: 명시적 판별자 - 리팩토링에 안전
{
    "type": "order",
    "id": "123"
}

성능 비교

대략적인 처리량 (높을수록 좋음):

형식직렬화역직렬화크기
MessagePack★★★★★★★★★★★★★★★
Protobuf★★★★★★★★★★★★★★★
System.Text.Json (소스 생성)★★★★☆★★★★☆★★★☆☆
System.Text.Json (리플렉션)★★★☆☆★★★☆☆★★★☆☆
Newtonsoft.Json★★☆☆☆★★☆☆☆★★★☆☆

핫 경로의 경우 MessagePack 또는 Protobuf를 선호하세요.


Akka.NET 직렬화

Akka.NET 액터 시스템의 경우 스키마 기반 직렬화를 사용하세요:

akka {
  actor {
    serializers {
      messagepack = "Akka.Serialization.MessagePackSerializer, Akka.Serialization.MessagePack"
    }
    serialization-bindings {
      "MyApp.Messages.IMessage, MyApp" = messagepack
    }
  }
}

Akka.NET 직렬화 문서를 참조하세요.


모범 사례

해야 할 것

// System.Text.Json에 소스 생성기 사용
[JsonSerializable(typeof(Order))]
public partial class AppJsonContext : JsonSerializerContext { }

// 명시적 필드 번호/키 사용
[MessagePackObject]
public class Order
{
    [Key(0)] public string Id { get; init; }
}

// 불변 메시지 타입에 record 사용
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);

하지 말아야 할 것

// BinaryFormatter 사용 금지 (절대)
var formatter = new BinaryFormatter();  // 보안 위험!

// 와이어 형식에 타입 이름 포함 금지
settings.TypeNameHandling = TypeNameHandling.All;  // 이름 변경 시 손상!

// 핫 경로에 리플렉션 직렬화 사용 금지
JsonConvert.SerializeObject(order);  // 느림, AOT 호환 안 됨

리소스

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

efcore-patterns

Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

관련 스킬