C#의 타입 안전 컬렉션: NonEmptyList가 런타임 예외를 없애는 방법

발행: (2026년 1월 2일 오전 09:16 GMT+9)
8 min read
원문: Dev.to

I’m ready to translate the article for you, but I’ll need the full text you’d like translated. Could you please provide the content (excluding the source line you’ve already included)? Once I have the article text, I’ll translate it into Korean while preserving the formatting, markdown, and code blocks as requested.

전통적인 방어적 프로그래밍

public async Task CalculateAverageOrderValueAsync(int customerId)
{
    var orders = await _repository.GetOrdersAsync(customerId);
    return orders.Average(o => o.Total); // Throws if orders is empty
}
if (orders.Any())
{
    return orders.Average(o => o.Total);
}
return 0m; // What does zero mean here? No orders? An error?

단점

IssueWhy it’s a problem
Silent failuresReturning a default masks the underlying issue
Code duplicationEmpty checks are scattered throughout the codebase
Semantic ambiguityIs 0 a valid average or an error state?

타입‑안전 솔루션: NonEmptyList

컬렉션이 반드시 하나 이상의 요소를 가져야 함을 타입 시그니처에 직접 표현할 수 있다면 어떨까요? 바로 **NonEmptyList**가 제공하는 기능입니다.

public async Task CalculateAverageOrderValueAsync(
    NonEmptyList<Order> orders)   // 컴파일 타임에 비어 있지 않음이 보장됨
{
    return orders.Average(o => o.Total); // 언제나 안전
}

컴파일러가 이제 비즈니스 규칙을 강제합니다. 호출자는 비어 있지 않은 컬렉션을 제공해야 하므로, 검증을 런타임에서 컴파일 타임으로 옮길 수 있습니다.

dotnet add package Masterly.NonEmptyList

.NET 6.0.NET 8.0과 호환됩니다.

NonEmptyList는 함수형 언어(예: Scala의 List)에서 영감을 받았습니다. 세 가지 기본 순회 특성을 제공합니다:

속성Type설명
HeadT첫 번째 요소(존재가 보장됨)
TailNonEmptyList?나머지 요소들, 단일 요소인 경우 null
InitNonEmptyList?마지막을 제외한 모든 요소, 단일 요소인 경우 null
var numbers = new NonEmptyList(1, 2, 3, 4, 5);

var head = numbers.Head;   // 1
var tail = numbers.Tail;   // NonEmptyList { 2, 3, 4, 5 }
var init = numbers.Init;   // NonEmptyList { 1, 2, 3, 4 }
var last = numbers.Last;   // 5

패턴 매칭

var (first, rest) = numbers;
// first = 1, rest = NonEmptyList { 2, 3, 4, 5 }

비어 있지 않음 보장을 유지한 변환

var orders = new NonEmptyList(order1, order2, order3);

// DTO로 매핑
NonEmptyList<OrderDto> dtos = orders.Map(o => new OrderDto(o));

// 중첩 컬렉션 평탄화
NonEmptyList<Item> allItems = orders.FlatMap(o => o.Items);

집계

전통적인 LINQ는 컬렉션이 비어 있을 수 있기 때문에 시드를 요구합니다:

// 빈 컬렉션이면 예외 발생
decimal total = orders.Aggregate(0m, (sum, o) => sum + o.Total);

NonEmptyList를 사용하면 시드 없이 Reduce를 사용할 수 있습니다:

// 항상 안전 – 컬렉션이 비어 있지 않음이 보장됨
decimal total = orders.Reduce((sum, o) => sum + o.Total);

요소 타입과 결과 타입이 다를 때:

var summary = orders.Fold(
    new OrderSummary(),
    (summary, order) => summary.AddOrder(order)
);

분할 및 그룹화

var (highValue, standard) = orders.Partition(o => o.Total > 1000m);

var byStatus = orders.GroupByNonEmpty(o => o.Status);
// 결과: Dictionary<Status, NonEmptyList<Order>>

단일‑요소와 다중‑요소 경우를 명시적으로 처리

string ProcessOrders(NonEmptyList<Order> orders) =>
    orders.Match(
        single: order => $"Processing single order: {order.Id}",
        multiple: (first, remaining) =>
            $"Processing batch: {first.Id} and {remaining.Count} more"
    );

이 패턴은 재귀 알고리즘과 배치 처리 로직에서 특히 빛을 발합니다.

비동기 지원

NonEmptyList는 순차 및 병렬 비동기 작업을 모두 제공합니다:

var enrichedOrders = await orders.MapAsync(
    async order => await _enrichmentService.EnrichAsync(order)
);

var results = await orders.MapParallelAsync(
    async order => await _processor.ProcessAsync(order),
    maxDegreeOfParallelism: 4
);

var totalRevenue = await orders.FoldAsync(
    0m,
    async (sum, order) => sum + await _calculator.CalculateRevenueAsync(order)
);

불변성

var immutable = new ImmutableNonEmptyList(1, 2, 3);

// 모든 연산은 새로운 인스턴스를 반환합니다
var appended   = immutable.Append(4);   // [1, 2, 3, 4]
var prepended  = immutable.Prepend(0); // [0, 1, 2, 3]

// 원본은 변경되지 않음
Console.WriteLine(immutable.Count);   // 3

가변 및 불변 변형 간 변환:

var immutable = mutableList.ToImmutable();
var mutable   = immutableList.ToMutable();

직렬화

내장 System.Text.Json 컨버터는 (역)직렬화를 자동으로 처리합니다:

var orders = new NonEmptyList(order1, order2);

string json = JsonSerializer.Serialize(orders);
var restored = JsonSerializer.Deserialize<NonEmptyList<Order>>(json);

역직렬화는 비어 있지 않은 제약을 강제합니다:

// 설명이 포함된 메시지와 함께 JsonException을 발생시킵니다
JsonSerializer.Deserialize<NonEmptyList<Order>>("[]");

Entity Framework Core 통합

전용 패키지를 추가하면 데이터베이스 영속성 지원이 포함됩니다:

dotnet add package Masterly.NonEmptyList.EntityFrameworkCore
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // Store as JSON column
    modelBuilder.Entity<Order>()
        .Property(p => p.Tags)
        .HasNonEmptyListConversion();

    // Configure relationships with non‑empty constraint
    modelBuilder.Entity<Order>()
        .HasNonEmptyManyWithOne(
            o => o.LineItems,
            li => li.Order,
            li => li.OrderId
        );
}

타입을 사용하는 예제 서비스

public class OrderProcessingService
{
    public async Task<ProcessingResult> ProcessBatchAsync(
        NonEmptyList<Order> orders,
        CancellationToken cancellationToken = default)
    {
        // Validate all orders in parallel
        var validationResults = await orders.MapParallelAsync(
            async order => await _validator.ValidateAsync(order, cancellationToken),
            maxDegreeOfParallelism: 8,
            cancellationToken);

        // Continue only if every order is valid
        if (validationResults.Any(r => !r.IsSuccess))
            return ProcessingResult.Failure("One or more orders are invalid.");

        // Process the batch
        var processingResults = await orders.MapParallelAsync(
            async order => await _processor.ProcessAsync(order, cancellationToken),
            maxDegreeOfParallelism: 8,
            cancellationToken);

        return ProcessingResult.Success(processingResults);
    }
}

Summary

  • Compile‑time guarantee that a collection is never empty.
  • Eliminates a whole class of runtime exceptions (InvalidOperationException, NullReferenceException).
  • Rich functional API (Map, FlatMap, Reduce, Fold, Partition, async variants, etc.).
  • Immutable & mutable variants, seamless conversion.
  • First‑class support for JSON serialization and EF Core persistence.

Add the package, replace ordinary IEnumerable/List where a non‑empty guarantee is required, and let the compiler enforce your business rules. 🚀

코드 예제

elAsync(
    order => _validator.ValidateAsync(order, cancellationToken),
    maxDegreeOfParallelism: 8
);

// Partition by validation result
var (valid, invalid) = orders
    .Zip(validationResults)
    .Partition(pair => pair.Item2.IsValid);

if (invalid.Any())
{
    _logger.LogWarning("Found {Count} invalid orders", invalid.Count);
}

// Process valid orders
var processed = valid switch
{
    null => new ProcessingResult(0, 0m),
    var validOrders => await ProcessValidOrdersAsync(
        validOrders.Map(pair => pair.Item1),
        cancellationToken
    )
};

return processed;
}

private async Task ProcessValidOrdersAsync(
    NonEmptyList<Order> orders,
    CancellationToken cancellationToken)
{
    // Calculate totals – Reduce is safe, no empty check needed
    var totalRevenue = orders.Reduce((sum, o) => sum + o.Total);

    // Process with pattern matching
    await orders.Match(
        single: async order =>
            await _processor.ProcessSingleAsync(order, cancellationToken),
        multiple: async (priority, remaining) =>
        {
            await _processor.ProcessPriorityAsync(priority, cancellationToken);
            await remaining.ForEachAsync(
                order => _processor.ProcessAsync(order, cancellationToken)
            );
        }
    );

    return new ProcessingResult(orders.Count, totalRevenue);
}

이상적인 사용 사례

  • 빈 컬렉션이 허용되지 않는 도메인 모델 (예: 주문은 반드시 라인 아이템을 가져야 함)
  • 최소 하나의 요소가 필요하도록 설계된 API 계약
  • 빈 입력이 오류가 되는 집계 연산
  • 함수형 프로그래밍 패턴 (head/tail 재귀)

다음과 같은 경우 대안을 고려하세요

  • 빈 컬렉션이 도메인에서 유효한 경우.
  • 빈 상태에서 컬렉션을 점진적으로 구축해야 할 때.
  • 성능이 중요하고 할당 오버헤드가 문제인 경우.

리소스

  • GitHub 저장소
  • 문서 위키
  • NuGet 패키지
  • EF Core 패키지

요약

NonEmptyList는 잘 확립된 함수형 프로그래밍 패턴을 C#에 도입하여, 개발자가 도메인 제약을 타입 시스템에 직접 표현할 수 있게 합니다. 검증을 런타임에서 컴파일 타임으로 옮김으로써 전체 버그 카테고리를 제거하고, 코드를 보다 표현력 있게 자체 문서화된 형태로 만들 수 있습니다.

이 라이브러리가 프로젝트에 도움이 된다면, GitHub에서 ⭐️ 를 눌러 주세요.

Back to Blog

관련 글

더 보기 »

2026년 MiniScript 로드맵

2026 Outlook: 2025가 마무리됨에 따라 2026을 내다볼 시간입니다! MiniScript는 이제 8년이 되었습니다. 많은 프로그래밍 언어는 실제로 …