본문 바로가기

C#

C# 프로퍼티 패턴(Property Patterns) 활용 예제

프로퍼티 패턴은 객체의 속성을 이름으로 매칭해 조건을 간결하게 표현하는 기능입니다. 복잡한 if 중첩 없이 명확하고 읽기 쉬운 검사 로직을 작성할 수 있습니다. 아래 예제로 실제 코드에서 바로 활용하는 방법을 정리합니다.

1. 예제 도메인 모델

모든 예제에서 공통으로 사용하는 간단한 도메인 타입입니다.

using System;

enum OrderStatus
{
    Pending,
    Paid,
    Shipped,
    Cancelled
}

sealed class Order
{
    public Customer? Customer { get; init; }
    public decimal Total { get; init; }
    public OrderStatus Status { get; init; }
    public OrderItem[] Items { get; init; } = Array.Empty<OrderItem>();
}

sealed class Customer
{
    public string Name { get; init; } = "";
    public int Age { get; init; }
    public bool Active { get; init; }
    public string? Role { get; init; }
    public Address Address { get; init; } = new Address();
}

sealed class Address
{
    public string City { get; init; } = "";
    public string Country { get; init; } = "";
}

sealed class OrderItem
{
    public string Sku { get; init; } = "";
    public decimal Price { get; init; }
}

2. 기본 사용: if와 is로 간결한 검사

여러 속성 조건을 한 번에 표현합니다. 관계형 패턴과 함께 쓰면 더욱 직관적입니다.

static bool IsBillable(Order? order) =>
    order is { Status: OrderStatus.Paid, Total: > 0m, Customer: { Active: true } };

order가 null이면 자동으로 매칭 실패하므로 별도 null 체크가 필요 없습니다.

3. switch 표현식으로 비즈니스 규칙 정리

분기 규칙을 위에서 아래로 선언적으로 기술합니다.

static decimal CalcShippingFee(Customer c) =>
    c switch
    {
        { Role: "Admin", Active: true } => 0m,
        { Address: { Country: "KR" }, Age: >= 65 } => 2000m,
        { Address: { Country: "KR" } } => 3000m,
        _ => 5000m
    };

4. 중첩 프로퍼티 패턴과 길이 검사

깊은 속성과 컬렉션 길이 검사도 자연스럽게 표현합니다.

static bool IsSeoulOrderWithItems(Order order) =>
    order is { Items: { Length: > 0 }, Customer: { Address: { City: "Seoul" } } };

5. null 안전 패턴 매칭

프로퍼티 패턴은 null에서 자동으로 실패합니다. 필요시 not null 패턴으로 안전 구문을 만들 수 있습니다.

static bool HasCustomerInKR(Order? order) =>
    order is { Customer: { Address: { Country: "KR" } } };

static void Process(Order? order)
{
    if (order is not null and { Customer: not null })
    {
        Console.WriteLine(order.Customer!.Name); // 안전합니다
    }
}

6. 컬렉션 패턴과 조합(C# 11+)

프로퍼티 패턴과 컬렉션 패턴을 함께 사용하여 요소 조건을 표현합니다.

static bool HasExpensiveItem(Order order) =>
    order is { Items: [ { Price: > 100_000m }, .. ] };

앞쪽 첫 아이템이 100,000원 초과면 매칭됩니다. ..은 나머지 요소를 의미합니다.

7. when 가드와 함께 세밀한 분기

패턴으로 1차 필터링 후, when으로 추가 제약을 적용합니다.

static string RiskLevel(Order order) =>
    order switch
    {
        { Total: > 100_000m } when order.Customer is { Age: < 19 } => "주의",
        { Status: OrderStatus.Cancelled } => "취소",
        _ => "일반"
    };

8. 전체 실행 예제

위 함수를 한 번에 실행해 봅니다.

using System;

// 위 도메인 타입 정의가 있다고 가정

static class Demo
{
    static bool IsBillable(Order? order) =>
        order is { Status: OrderStatus.Paid, Total: > 0m, Customer: { Active: true } };

    static decimal CalcShippingFee(Customer c) =>
        c switch
        {
            { Role: "Admin", Active: true } => 0m,
            { Address: { Country: "KR" }, Age: >= 65 } => 2000m,
            { Address: { Country: "KR" } } => 3000m,
            _ => 5000m
        };

    static bool IsSeoulOrderWithItems(Order order) =>
        order is { Items: { Length: > 0 }, Customer: { Address: { City: "Seoul" } } };

    static bool HasExpensiveItem(Order order) =>
        order is { Items: [ { Price: > 100_000m }, .. ] };

    static string RiskLevel(Order order) =>
        order switch
        {
            { Total: > 100_000m } when order.Customer is { Age: < 19 } => "주의",
            { Status: OrderStatus.Cancelled } => "취소",
            _ => "일반"
        };

    public static void Main()
    {
        var order = new Order
        {
            Status = OrderStatus.Paid,
            Total = 120_000m,
            Customer = new Customer
            {
                Name = "Kim",
                Age = 35,
                Active = true,
                Role = "User",
                Address = new Address { City = "Seoul", Country = "KR" }
            },
            Items = new[] { new OrderItem { Sku = "A-1", Price = 150_000m } }
        };

        Console.WriteLine(IsBillable(order));                 // True
        Console.WriteLine(CalcShippingFee(order.Customer!));   // 3000
        Console.WriteLine(IsSeoulOrderWithItems(order));       // True
        Console.WriteLine(HasExpensiveItem(order));            // True
        Console.WriteLine(RiskLevel(order));                   // 일반
    }
}

9. 팁과 버전 호환

프로퍼티 패턴은 C# 8부터 도입되었습니다. 관계형 및 논리 패턴은 C# 9에서 강화되었고, 컬렉션 패턴은 C# 11에서 추가되었습니다. 속성 이름으로 컴파일 타임 검사되므로 리팩터링에 강합니다. 분기는 위에서 아래로 매칭되니 구체적인 패턴을 먼저 배치하는 것이 좋습니다.

프로퍼티 패턴을 활용하면 DTO 검증, 라우팅 분기, 위험도 분류 같은 로직을 간단하고 읽기 쉽게 유지할 수 있습니다.