C# 확장 메서드·partial·중첩 클래스: C# 14 extension 블록까지

C# 확장 메서드의 선택 규칙(CS0121·CS1061), C# 14 extension 블록과 확장 프로퍼티를 .NET 10에서 컴파일한 결과, partial 메서드·프로퍼티·생성자, 중첩 클래스 쓰임새.

2026년 11월 보강: 2021년 글의 예제 세 개(확장 메서드, 분할 클래스, 중첩 클래스)를 .NET 10.0.14(C# 14)에서 다시 실행했고, 세 개 모두 수정 없이 같은 결과를 냈습니다. 다만 "확장 메서드는 기존 클래스의 기능을 확장한다"는 설명은 원본 형식을 바꾸지 않고 private 멤버에도 접근할 수 없다는 점을 빠뜨렸고, partial이 붙을 수 있는 대상은 2021년 이후 메서드(접근 한정자 포함, C# 9)·프로퍼티(C# 13)·생성자와 이벤트(C# 14)로 늘었습니다. C# 14의 extension 블록(확장 프로퍼티·정적 확장 멤버)은 직접 컴파일해 결과를 실었습니다.

이 글은 이미 있는 형식에 메서드를 "붙이고" 싶거나, 자동 생성 코드와 손으로 쓴 코드를 나누고 싶거나, 클래스 안에 클래스를 두는 게 맞는지 고민하는 분을 위한 글입니다. 세 기능 모두 형식을 어떻게 나누고 어디에 둘지에 관한 문법입니다. 결론부터 말하면, 확장 메서드는 인스턴스 메서드가 없을 때만 선택되고 using 범위에 좌우됩니다. partial은 주로 소스 생성기와 함께 쓰며, 중첩 클래스는 바깥 클래스의 private에 접근해야 하는 보조 형식에만 씁니다. 프로퍼티 자체는 C# 프로퍼티 기초와 심화에서 다룹니다.

확장 메서드: 2021년 예제 다시 실행

확장 메서드는 static 클래스 안의 static 메서드인데, 첫 번째 매개 변수 앞에 this가 붙어 있어 인스턴스 메서드처럼 값.메서드()로 부를 수 있습니다. 2021년 예제를 그대로 실행했습니다(불필요한 using 생략).

using Extension;

namespace Extension
{
    public static class ExtensionMethod
    {
        public static int Multiplication(this int var, int a, int b)
        {
            int result = var;
            for (int i = 0; i < b; i++)
                result *= a;
            return result;
        }
    }
}
namespace ConsoleApplication21
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("{0}", 5.Multiplication(2, 3));
        }
    }
}
40

결과는 2021년과 같습니다. 이름이 Multiplication이지만 실제 계산은 5 × 2³ = 40, 즉 "var에 a를 b번 곱하기"입니다. 실무라면 MultiplyByPower처럼 동작이 드러나는 이름이 낫습니다. 2021년 출력 끝의 "계속하려면 아무 키나 누르십시오"는 Visual Studio 콘솔이 붙이던 줄로, dotnet run에서는 나오지 않습니다.

중요한 사실 두 가지를 덧붙입니다. 첫째, 확장 메서드는 원래 형식을 바꾸지 않습니다. 컴파일러가 5.Multiplication(2, 3)을 ExtensionMethod.Multiplication(5, 2, 3)이라는 평범한 정적 호출로 바꿀 뿐이므로, int의 private 멤버에는 접근할 수 없습니다. 둘째, 그래서 null에도 호출됩니다.

public static string Truncate(this string? s, int max) =>
    s is null ? "(null)" : s.Length <= max ? s : s[..max] + "…";

Console.WriteLine("Hello, extension".Truncate(5));
string? missing = null;
Console.WriteLine(missing.Truncate(5));                              // NullReferenceException 없음
Console.WriteLine(StringExtensions.Truncate("static call", 6));      // 정적 메서드로 직접 호출
Hello…
(null)
static…

확장 메서드는 어떻게 선택되나

확장 메서드에서 사고가 나는 곳은 대부분 "어떤 메서드가 불리는가"입니다. 컴파일러의 규칙을 그림으로 정리했습니다.

순서도. x.M(args) 호출은 먼저 쓸 수 있는 인스턴스 메서드 M 이 있는지 확인한다. 있으면 그것을 호출하고 같은 이름의 확장 메서드는 모두 무시된다. 없으면 가장 안쪽 네임스페이스 선언부터 바깥쪽으로(각 단계의 using 지시문 포함) 확장 메서드를 찾는다. 가장 적합한 후보가 하나만 발견된 첫 단계에서 그 확장 메서드를 호출한다. 같은 단계에 똑같이 적합한 후보가 둘이면 error CS0121(모호한 호출), 어느 단계에도 후보가 없으면 error CS1061 이며 using 누락이 흔한 원인이다.
x.M() 에 대해 컴파일러가 메서드를 고르는 순서. 인스턴스 멤버가 항상 먼저이고, 맞는 인스턴스 메서드가 없을 때만 안쪽 네임스페이스부터 바깥쪽으로 확장 메서드를 찾는다.

1. 인스턴스 메서드가 항상 먼저입니다. string에 ToUpper라는 확장 메서드를 만들어도 절대 불리지 않으며, 컴파일러는 경고조차 내지 않습니다.

public static string ToUpper(this string s) => "extension ToUpper";
Console.WriteLine("abc".ToUpper());
ABC

라이브러리를 업데이트했더니 그 형식에 같은 이름의 인스턴스 메서드가 새로 생겨, 내 확장 메서드가 조용히 무시되는 경우도 같은 원리입니다.

2. 안쪽 범위가 먼저입니다. 같은 이름의 확장 메서드가 using Shop.Text;로 가져온 네임스페이스와, 호출하는 코드가 속한 Shop.App 네임스페이스에 모두 있으면, 가까운 쪽(Shop.App)이 선택됐습니다.

Shop.App (enclosing namespace)

3. 같은 단계에 둘이면 모호성 에러, 하나도 없으면 using 누락을 의심합니다. using A;와 using B;가 같은 시그니처의 Shout를 가져오고, C 네임스페이스는 using하지 않은 상태로 빌드했습니다.

Program.cs(4,23): error CS0121: 'A.AExt.Shout(string)' 및 'B.BExt.Shout(string)'의 메서드 또는 속성 간 호출이 모호합니다.
Program.cs(5,21): error CS1061: 'int'에는 'Twice'에 대한 정의가 포함되어 있지 않고, 'int' 형식의 첫 번째 인수를 허용하는 액세스 가능한 확장 메서드 'Twice'이(가) 없습니다. using 지시문 또는 어셈블리 참조가 있는지 확인하세요.

CS1061 메시지에 "using 지시문"이 직접 언급되는 것처럼, IDE의 자동 완성에 확장 메서드가 안 보이면 대부분 using이 빠진 경우입니다. CS0121은 AExt.Shout(x)처럼 정적 호출로 바꾸면 피할 수 있습니다.

C# 14 extension 블록: 확장 프로퍼티와 정적 확장 멤버

C# 14에는 extension 블록이 들어왔습니다. 받는 쪽(receiver)을 블록 머리에 한 번만 쓰고, 그 안에 메서드뿐 아니라 프로퍼티, 정적 멤버, 연산자까지 정의합니다. .NET 10 SDK에서 실제로 컴파일·실행한 코드입니다.

int[] scores = [72, 95, 88];
Console.WriteLine(scores.IsEmptyOrNull);          // 확장 프로퍼티
Console.WriteLine(scores.Average2());             // 블록 안의 확장 메서드
List<string> names = ["a", "b"];
Console.WriteLine(names.SecondOrDefault);         // 제네릭 확장 프로퍼티
Console.WriteLine(TimeSpan.FromMinutesSafe(90));  // 형식에 붙는 정적 확장 메서드
Console.WriteLine(string.Banner);                 // 정적 확장 프로퍼티
Console.WriteLine(new Point(5, 5) - new Point(1, 2));   // 확장 연산자

public record Point(int X, int Y);

public static class SequenceExtensions
{
    extension<T>(IReadOnlyList<T>? source)
    {
        public bool IsEmptyOrNull => source is null || source.Count == 0;
        public T? SecondOrDefault => source is { Count: > 1 } ? source[1] : default;
    }

    extension(int[] values)
    {
        public double Average2() => values.Length == 0 ? 0 : values.Sum() / (double)values.Length;
    }

    extension(TimeSpan)          // 이름 없는 receiver = 정적 멤버만
    {
        public static TimeSpan FromMinutesSafe(int m) => TimeSpan.FromMinutes(Math.Max(0, m));
    }

    extension(string)
    {
        public static string Banner => "== t.laboratory ==";
    }

    extension(Point)
    {
        public static Point operator -(Point a, Point b) => new(a.X - b.X, a.Y - b.Y);
    }
}
False
85
b
01:30:00
== t.laboratory ==
Point { X = 4, Y = 3 }

확인한 사실을 정리합니다.

  • 별도 설정 없이 컴파일됩니다. net10.0 기본(C# 14)에서 바로 됩니다. -p:LangVersion=13으로 낮추면 "C# 14가 필요하다"는 안내가 아니라 error CS1513: }가 필요합니다.라는 구문 에러가 나므로, 오래된 SDK에서 이 코드를 열면 원인을 찾기 어렵습니다.
  • 기존 방식과 호환됩니다. static class Ext { extension(int[] values) { … } }로 따로 만들어 보니, 블록 안의 메서드는 Ext.Average2(a)처럼 정적 메서드로 부를 수 있었고(결과 2), 확장 프로퍼티 Total의 게터도 Ext.get_Total(a)라는 정적 메서드로 호출됐습니다(결과 6). 컴파일 결과가 예전 확장 메서드와 같은 모양이라는 뜻입니다.
  • 상태는 둘 수 없습니다. 블록 안에 자동 구현 프로퍼티나 필드를 쓰면 에러입니다. 확장은 여전히 원래 객체에 데이터를 "추가"하지 못합니다.
Program.cs(5,20): error CS9282: 이 멤버는 확장 블록에서 사용할 수 없습니다.
Program.cs(6,21): error CS9282: 이 멤버는 확장 블록에서 사용할 수 없습니다.

partial: 클래스, 메서드, 프로퍼티, 생성자

2021년의 분할 클래스 예제도 그대로 실행됐습니다(Test(), Test2(), Test3() 출력). 다만 클래스 이름이 Nested라서 다음 절의 중첩 클래스와 헷갈리기 쉬웠습니다. partial로 나눈 조각들은 컴파일할 때 하나의 형식으로 합쳐지며, 같은 어셈블리와 같은 네임스페이스에 있어야 합니다.

2026년에 partial을 쓰는 가장 큰 이유는 소스 생성기입니다. System.Text.Json, 정규식 [GeneratedRegex], MVVM Toolkit 등이 "선언은 내가, 구현은 생성기가" 쓰는 구조를 씁니다. 손으로 쓴 쪽과 생성된 쪽을 흉내 낸 예제입니다.

// Order.cs (손으로 쓴 쪽)
public partial class Order
{
    public partial string Code { get; set; }        // C# 13: 선언 부분
    partial void OnShipped();                       // 선택적 훅: 구현이 없음
    public partial Order(int id);                   // C# 14: partial 생성자

    public void Ship()
    {
        OnShipped();                                // 구현이 없으면 호출 자체가 사라짐
        OnValidated(Code);
        Console.WriteLine($"Shipped #{Id} {Code}");
    }
}

// Order.Generated.cs (생성기가 쓴다고 가정)
public partial class Order
{
    public int Id { get; }
    public partial Order(int id) { Id = id; }
    public partial string Code
    {
        get => field;
        set => field = value.Trim().ToUpperInvariant();
    }
    private partial void OnValidated(string code);
}
public partial class Order
{
    private partial void OnValidated(string code) => Console.WriteLine($"validated {code}");
}
validated AB-12
Shipped #7 AB-12
OnShipped: not in the assembly

마지막 줄은 리플렉션으로 OnShipped를 찾은 결과입니다. 접근 한정자 없는 void partial 메서드는 구현이 없으면 선언과 호출이 모두 제거됩니다. 반대로 접근 한정자를 붙인 partial 메서드(C# 9)와 partial 프로퍼티(C# 13)는 구현이 반드시 있어야 합니다.

error CS8795: 부분 메서드 'P.M()'에는 접근성 한정자가 있으므로 구현 파트가 있어야 합니다.
error CS9248: 부분 속성 'Settings.Volume'에는 구현 부분이 있어야 합니다.
partial 대상 버전 한 단계 낮춘 컴파일 결과
클래스·구조체·인터페이스 C# 2 CS8022 (C# 1)
void 메서드(한정자 없음, 구현 선택) C# 3 CS8023 (C# 2)
한정자·반환값 있는 메서드(구현 필수) C# 9 CS8400 (C# 8)
프로퍼티·인덱서 C# 13 CS8703 (C# 12)
생성자·이벤트 C# 14 CS9260 / CS8703 (C# 13)

중첩 클래스: 언제 쓰나

2021년의 중첩 클래스 예제도 그대로 a : 70, a : 60을 출력했습니다. 설명도 정확합니다. 중첩 형식의 기본 접근 수준은 private이고, 중첩 클래스는 바깥 클래스의 private 멤버에 접근할 수 있습니다. 컴파일러로 확인한 두 가지를 덧붙입니다.

var x = new Outer.Helper();          // 한정자 없는 중첩 클래스

public class Outer
{
    class Helper { }
    public int Value = 1;
    public class Inner
    {
        public int Read() => Value;   // 바깥 인스턴스가 자동으로 연결되지 않음
    }
}
Program.cs(1,19): error CS0122: '보호 수준 때문에 'Outer.Helper'에 액세스할 수 없습니다.
Program.cs(10,30): error CS0120: static이 아닌 필드, 메서드 또는 속성 'Outer.Value'에 개체 참조가 필요합니다.

CS0120이 Java나 Kotlin에서 온 개발자가 자주 걸리는 지점입니다. C#의 중첩 클래스는 Java의 static 중첩 클래스와 같아서, 바깥 객체가 자동으로 따라오지 않습니다. 2021년 예제처럼 바깥 객체를 생성자로 받아야 합니다.

실무에서 중첩 클래스가 어울리는 경우는 두 가지 정도입니다.

public sealed class HttpRequestSpec
{
    private HttpRequestSpec(string url, Dictionary<string, string> h) { … }
    public static Builder CreateBuilder() => new();

    public sealed class Builder          // 바깥의 private 생성자를 부를 수 있음
    {
        …
        public HttpRequestSpec Build() => new(url, headers);
    }
}

public sealed class TtlCache<T>(TimeSpan ttl)
{
    private sealed record Entry(T Value, DateTime ExpiresAt);   // 바깥에서는 보이지 않음
    private readonly Dictionary<string, Entry> map = [];
    …
}
https://example.com/api (1 header)
v

빌더는 바깥 클래스의 private 생성자를 부르기 위해, Entry는 구현 세부를 숨기기 위해 중첩했습니다. 반대로 다른 코드에서 Outer.Inner처럼 이름을 자주 써야 하는 public 형식이라면 최상위로 빼는 편이 읽기 쉽습니다(코드 분석 규칙 CA1034가 public 중첩 형식을 경고하는 이유이기도 합니다).

어떤 걸 언제 쓰나

하고 싶은 일 선택
내가 고칠 수 없는 형식(string, IEnumerable<T>)에 도우미 추가 확장 메서드, C# 14부터는 extension 블록
계산된 값을 프로퍼티 문법으로 붙이기 C# 14 확장 프로퍼티 (상태는 불가)
형식 이름에 정적 도우미 붙이기 (TimeSpan.FromMinutesSafe) C# 14 정적 확장 멤버
생성된 코드와 손으로 쓴 코드 분리 partial 클래스 + partial 메서드·프로퍼티
바깥의 private에 접근하는 보조 형식 (빌더, 열거자, 내부 레코드) 중첩 클래스 (private 또는 public)
여러 곳에서 이름으로 쓰는 형식 최상위 형식

LINQ의 Where, Select가 바로 IEnumerable<T>에 대한 확장 메서드입니다. 실제 사용은 LINQ 기초에, 제네릭 확장 메서드의 형식 제약은 C# 제네릭에 정리했습니다.

확인 환경: .NET SDK 10.0.403 / 런타임 10.0.14, C# 14(net10.0 기본값), macOS(Apple Silicon Mac).

자주 묻는 질문

인스턴스 메서드와 같은 이름의 확장 메서드를 만들면 어떻게 되나요?

인스턴스 메서드가 항상 이기고, 확장 메서드는 호출되지 않습니다. 컴파일러는 경고도 내지 않습니다. string에 ToUpper라는 확장 메서드를 만들어 "abc".ToUpper()를 호출하면 확장 메서드가 아니라 string.ToUpper()가 실행되어 ABC가 출력됐습니다.

C# 14에서 확장 프로퍼티를 만들 수 있나요?

네. static 클래스 안에 extension(int[] values) { public int Total => values.Sum(); } 같은 extension 블록을 쓰면 확장 프로퍼티, 정적 확장 멤버, 연산자까지 정의할 수 있고 .NET 10(C# 14)에서 컴파일·실행됐습니다. 다만 자동 구현 프로퍼티와 필드는 블록 안에 둘 수 없습니다(error CS9282).

null 변수에 확장 메서드를 호출하면 NullReferenceException이 나나요?

호출 자체에서는 나지 않습니다. 확장 메서드는 정적 메서드라서 null이 첫 번째 인수로 그대로 전달됩니다. 메서드 안에서 null을 처리하지 않고 멤버에 접근할 때 예외가 납니다. 예제에서는 null 문자열에 Truncate를 호출해 (null)이 출력됐습니다.

  • #C#
  • #확장 메서드
  • #extension
  • #partial
  • #중첩 클래스

t.laboratory

직접 만들고 운영하는 앱·서버·서비스에서 겪은 문제와, 그 해결에 쓴 코드를 기록하는 기술 블로그입니다.

운영자 소개 · 앱

↑