Community코딩 & 개발github.com

타입 디자인 성능

성능을 고려한 .NET 타입 디자인 방법을 안내합니다. 클래스 봉인, readonly 구조체 사용, 정적 순수 함수 선호, 조기 열거 방지, 적절한 컬렉션 타입 선택 등을 다룹니다.

타입 디자인 성능란 무엇인가요?

이 스킬은 .NET 개발자가 성능에 최적화된 타입을 설계할 수 있도록 돕는 종합 가이드입니다. 클래스를 봉인(sealed)하여 JIT 가상화 해제(devirtualization)를 유도하고, 불변 값 타입에는 `readonly` 구조체를 사용하여 방어적 복사를 방지합니다. 또한, 부작용이 없는 정적 순수 함수를 선호하여 성능과 테스트 용이성을 높이고, 불필요한 조기 열거를 피하기 위해 `IEnumerable` 반환과 지연된 구체화를 권장합니다. `ValueTask`와 `Task`의 적절한 사용법, `Span<T>`와 `Memory<T>`를 활용한 저수준 메모리 최적화, 그리고 API 경계에서 불변 컬렉션(`IReadOnlyList<T>`, `FrozenDictionary`)을 반환하는 방법을 제시합니다. 다양한 C# 코드 예제와 함께, 잘못된 패턴(안티 패턴)과 올바른 패턴을 대조하여 설명하므로, 초보자부터 고급 개발자까지 모두 유용하게 활용할 수 있습니다.

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

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

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

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

문서

성능을 위한 타입 디자인

이 스킬을 사용해야 할 때

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

  • 새로운 타입과 API를 설계할 때
  • 성능 문제를 찾기 위해 코드를 검토할 때
  • 클래스, 구조체, 레코드 중에서 선택해야 할 때
  • 컬렉션과 열거 가능(Enumerable) 항목을 다룰 때

핵심 원칙

  1. 타입을 봉인(seal)하세요 - 명시적으로 상속을 위해 설계된 경우가 아니라면
  2. readonly 구조체를 선호하세요 - 작고 불변인 값 타입에 사용
  3. 정적 순수 함수를 선호하세요 - 더 나은 성능과 테스트 용이성
  4. 열거를 지연시키세요 - 필요할 때까지 구체화(materialize)하지 마세요
  5. 불변 컬렉션을 반환하세요 - API 경계에서

클래스 기본 봉인

클래스를 봉인하면 JIT 가상화 해제가 가능해지고 API 의도가 명확해집니다.

// 권장: 상속용이 아닌 클래스는 봉인
public sealed class OrderProcessor
{
    public void Process(Order order) { }
}

// 권장: 레코드도 봉인 (레코드는 클래스입니다)
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);

// 비권장: 이유 없이 봉인 해제
public class OrderProcessor  // 서브클래싱 가능? 의도적인가?
{
    public virtual void Process(Order order) { }  // 가상 = 더 느림
}

장점:

  • JIT가 메서드 호출을 가상화 해제할 수 있음
  • "이것은 확장 지점이 아님"을 명확히 전달
  • 우발적인 호환성 깨짐 방지

값 타입을 위한 Readonly 구조체

구조체는 불변일 때 readonly여야 합니다. 이는 방어적 복사를 방지합니다.

// 권장: 불변 값 타입을 위한 Readonly 구조체
public readonly record struct OrderId(Guid Value)
{
    public static OrderId New() => new(Guid.NewGuid());
    public override string ToString() => Value.ToString();
}

// 권장: 작고 수명이 짧은 데이터를 위한 Readonly 구조체
public readonly struct Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }
}

// 비권장: 가변 구조체 (방어적 복사 유발)
public struct Point  // Readonly 아님!
{
    public int X { get; set; }  // 가변!
    public int Y { get; set; }
}

구조체 사용 시점

구조체 사용 조건클래스 사용 조건
작음 (일반적으로 16바이트 이하)더 큰 객체
수명이 짧음수명이 긺
자주 할당됨공유 참조 필요
값 의미 체계 필요ID 의미 체계 필요
불변가변 상태

정적 순수 함수 선호

부작용이 없는 정적 메서드는 더 빠르고 테스트하기 쉽습니다.

// 권장: 정적 순수 함수
public static class OrderCalculator
{
    public static Money CalculateTotal(IReadOnlyList<OrderItem> items)
    {
        var total = items.Sum(i => i.Price * i.Quantity);
        return new Money(total, "USD");
    }
}

// 사용 - 예측 가능하고 테스트 가능
var total = OrderCalculator.CalculateTotal(items);

장점:

  • Vtable 조회 없음 (더 빠름)
  • 숨겨진 상태 없음
  • 테스트 쉬움 (순수 입력 → 출력)
  • 설계상 스레드 안전
  • 명시적 종속성 강제
// 비권장: 종속성을 숨기는 인스턴스 메서드
public class OrderCalculator
{
    private readonly ITaxService _taxService;  // 숨겨진 종속성
    private readonly IDiscountService _discountService;  // 숨겨진 종속성

    public Money CalculateTotal(IReadOnlyList<OrderItem> items)
    {
        // 이것이 실제로 무엇에 의존할까요?
    }
}

// 더 나은 방법: 매개변수를 통한 명시적 종속성
public static class OrderCalculator
{
    public static Money CalculateTotal(
        IReadOnlyList<OrderItem> items,
        decimal taxRate,
        decimal discountPercent)
    {
        // 모든 입력이 보입니다
    }
}

과용하지 마세요 - 상태나 다형성이 진짜 필요한 경우에는 인스턴스 메서드를 사용하세요.


열거 지연

필요할 때까지 열거 가능 항목을 구체화하지 마세요. 과도한 LINQ 체인을 피하세요.

// 나쁨: 조기 구체화
public IReadOnlyList<Order> GetActiveOrders()
{
    return _orders
        .Where(o => o.IsActive)
        .ToList()  // 구체화됨!
        .OrderBy(o => o.CreatedAt)  // 또 다른 반복
        .ToList();  // 다시 구체화됨!
}

// 좋음: 마지막까지 지연
public IReadOnlyList<Order> GetActiveOrders()
{
    return _orders
        .Where(o => o.IsActive)
        .OrderBy(o => o.CreatedAt)
        .ToList();  // 단일 구체화
}

// 좋음: 호출자가 모든 항목을 필요로 하지 않을 수 있으면 IEnumerable 반환
public IEnumerable<Order> GetActiveOrders()
{
    return _orders
        .Where(o => o.IsActive)
        .OrderBy(o => o.CreatedAt);
    // 호출자가 구체화 시점 결정
}

비동기 열거

async와 IEnumerable을 사용할 때 주의하세요:

// 나쁨: LINQ에서의 비동기 - 숨겨진 할당
var results = orders
    .Select(async o => await ProcessOrderAsync(o))  // 항목당 Task!
    .ToList();
await Task.WhenAll(results);

// 좋음: 스트리밍을 위해 IAsyncEnumerable 사용
public async IAsyncEnumerable<OrderResult> ProcessOrdersAsync(
    IEnumerable<Order> orders,
    [EnumeratorCancellation] CancellationToken ct = default)
{
    foreach (var order in orders)
    {
        ct.ThrowIfCancellationRequested();
        yield return await ProcessOrderAsync(order, ct);
    }
}

// 좋음: 병렬 처리를 위한 배치 처리
var results = await Task.WhenAll(
    orders.Select(o => ProcessOrderAsync(o)));

ValueTask vs Task

자주 동기적으로 완료되는 핫 경로에는 ValueTask를 사용하세요. 실제 I/O의 경우 그냥 Task를 사용하세요.

// 권장: 캐시되거나 동기 경로에 ValueTask 사용
public ValueTask<User?> GetUserAsync(UserId id)
{
    if (_cache.TryGetValue(id, out var user))
    {
        return ValueTask.FromResult<User?>(user);  // 할당 없음
    }

    return new ValueTask<User?>(FetchUserAsync(id));
}

// 권장: 실제 I/O에는 Task 사용 (더 간단, 함정 없음)
public Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
    // 항상 데이터베이스에 접근
    return _repository.CreateAsync(cmd);
}

ValueTask 규칙:

  • ValueTask를 두 번 이상 await하지 마세요
  • 완료 전에 .Result 또는 .GetAwaiter().GetResult()를 사용하지 마세요
  • 확실하지 않으면 Task를 사용하세요

바이트 처리를 위한 Span과 Memory

저수준 연산에는 byte[] 대신 Span<T>와 Memory<T>를 사용하세요.

// 권장: 동기 연산에는 Span 사용
public static int ParseInt(ReadOnlySpan<char> text)
{
    return int.Parse(text);
}

// 권장: 비동기 연산에는 Memory 사용
public async Task WriteAsync(ReadOnlyMemory<byte> data)
{
    await _stream.WriteAsync(data);
}

// 비권장: 배열 할당 강제
public static int ParseInt(string text)  // 문자열 할당됨
{
    return int.Parse(text);
}

일반적인 Span 패턴

// 할당 없이 슬라이싱
ReadOnlySpan<char> span = "Hello, World!".AsSpan();
var hello = span[..5];  // 할당 없음

// 작은 버퍼를 위한 스택 할당
Span<byte> buffer = stackalloc byte[256];

// 더 큰 버퍼에는 ArrayPool 사용
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
    // 버퍼 사용...
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);
}

컬렉션 반환 타입

API에서는 불변 컬렉션 반환

// 권장: 불변 컬렉션 반환
public IReadOnlyList<Order> GetOrders()
{
    return _orders.ToList();  // 호출자가 내부 상태를 변경할 수 없음
}

// 권장: 정적 데이터에는 고정(frozen) 컬렉션 사용 (.NET 8+)
private static readonly FrozenDictionary<string, Handler> _handlers =
    new Dictionary<string, Handler>
    {
        ["create"] = new CreateHandler(),
        ["update"] = new UpdateHandler(),
    }.ToFrozenDictionary();

// 비권장: 가변 컬렉션 반환
public List<Order> GetOrders()
{
    return _orders;  // 호출자가 변경 가능!
}

내부 변경은 괜찮음

public IReadOnlyList<OrderItem> BuildOrderItems(Cart cart)
{
    var items = new List<OrderItem>();  // 내부적으로는 가변

    foreach (var cartItem in cart.Items)
    {
        items.Add(CreateOrderItem(cartItem));
    }

    return items;  // IReadOnlyList로 반환
}

컬렉션 가이드라인

시나리오반환 타입
API 경계IReadOnlyList<T>, IReadOnlyCollection<T>
정적 조회 데이터FrozenDictionary<K,V>, FrozenSet<T>
내부 구축List<T>, 그 후 읽기 전용으로 반환
단일 항목 또는 없음T? (nullable)
0개 이상, 지연IEnumerable<T>

빠른 참조

패턴이점
sealed class가상화 해제, 명확한 API
readonly record struct방어적 복사 없음, 값 의미 체계
정적 순수 함수Vtable 없음, 테스트 가능, 스레드 안전
.ToList() 지연단일 구체화
핫 경로에 ValueTaskTask 할당 회피
바이트에 Span<T>스택 할당, 복사 없음
IReadOnlyList<T> 반환불변 API 계약
FrozenDictionary정적 데이터에 가장 빠른 조회

안티 패턴

// 비권장: 이유 없는 봉인 해제 클래스
public class OrderService { }  // 봉인하세요!

// 비권장: 가변 구조체
public struct Point { public int X; public int Y; }  // Readonly로 만드세요

// 비권장: 정적이 될 수 있는 인스턴스 메서드
public int Add(int a, int b) => a + b;  // Static으로 만드세요

// 비권장: 여러 ToList() 호출
items.Where(...).ToList().OrderBy(...).ToList();  // 끝에 하나의 ToList

// 비권장: 공개 API에서 List<T> 반환
public List<Order> GetOrders();  // IReadOnlyList<T> 반환

// 비권장: 항상 비동기인 연산에 ValueTask 사용
public ValueTask<Order> CreateOrderAsync();  // 그냥 Task 사용

리소스

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.

관련 스킬