---
title: "확장 메서드는 붙는 것이 아니라 붙어 보이는 것이다"
description: "string 을 고칠 수 없는데 string 에 메서드를 하나 늘리는 법. 그리고 그게 진짜 늘어난 게 아닌 이유."
author: "유니얼"
pubDate: 2026-09-09
category: "programming/csharp/advanced"
tags: ["C#"]
canonical: https://blog.unialgames.com/posts/csharp-extension-method/
---
문자열이 이메일처럼 생겼는지 확인하는 코드를 자주 쓴다고 해 봅시다.

```csharp
if (검사(입력)) { ... }
```

이렇게 쓰는 것보다 이렇게 쓰고 싶습니다.

```csharp
if (입력.이메일인가()) { ... }
```

그런데 `string` 은 우리가 만든 클래스가 아닙니다. 열어서 메서드를 하나 더 넣을 수가 없습니다.
상속을 하려 해도 `string` 은 `sealed` 라 막혀 있습니다.

> **고칠 수 없는 클래스에도 메서드를 붙여 쓸 수 있습니다. 진짜로 붙는 것은 아니고, 붙은 것처럼 부를 수 있게 해 줍니다.**

## 확장 메서드는 이렇게 생겼다

조건이 세 개입니다. 하나라도 빠지면 안 됩니다.

<figure class="diagram"><svg viewBox="0 0 640 190" role="img" aria-label="확장 메서드의 세 가지 조건"><text x="20" y="34" class="mono" font-size="14">static class 문자열도구</text><text x="20" y="62" class="mono" font-size="14">{</text><text x="20" y="90" class="mono" font-size="14">    public static bool 이메일인가(this string s)</text><text x="20" y="118" class="mono" font-size="14">    { ... }</text><text x="20" y="146" class="mono" font-size="14">}</text><line x1="200" y1="28" x2="400" y2="28" stroke="currentColor" stroke-opacity="0.25" /><text x="410" y="33" font-size="12.5">1. 담는 클래스가 static</text><line x1="200" y1="76" x2="400" y2="76" stroke="currentColor" stroke-opacity="0.25" /><text x="410" y="81" font-size="12.5">2. 메서드도 static</text><line x1="330" y1="96" x2="400" y2="96" stroke="currentColor" stroke-opacity="0.25" /><text x="410" y="101" font-size="12.5">3. 첫 매개변수 앞에 this</text><text x="20" y="180" font-size="12.5" opacity="0.7">this 가 붙은 그 타입에 메서드가 생긴 것처럼 보입니다.</text></svg><figcaption>세 조건 중 하나라도 빠지면 그냥 평범한 static 메서드입니다.</figcaption></figure>

```csharp
static class 문자열도구
{
    public static bool 이메일인가(this string s)
    {
        return s.Contains("@") && s.Contains(".");
    }
}
```

이제 이렇게 부를 수 있습니다.

```csharp
string 입력 = "a@b.com";
Console.WriteLine(입력.이메일인가());   // True
```

`string` 은 한 글자도 안 고쳤는데 `.이메일인가()` 가 생겼습니다.

## 진짜로 붙은 것이 아니다

여기가 이 글의 전부입니다. 컴파일러는 `입력.이메일인가()` 를 보면 **그냥 static 메서드 호출로 바꿔 씁니다.**

<x-compare>
<script type="application/json">
{
  "tabs": [
    {
      "label": "내가 쓴 코드",
      "code": "string 입력 = \"a@b.com\";\n\nif (입력.이메일인가())\n{\n    ...\n}"
    },
    {
      "label": "컴파일된 결과",
      "code": "string 입력 = \"a@b.com\";\n\nif (문자열도구.이메일인가(입력))\n{\n    ...\n}",
      "stats": [["string 에 실제로 늘어난 것", "없음"]],
      "tone": "good"
    }
  ]
}
</script>
</x-compare>

`string` 안에는 아무것도 안 들어갔습니다. 부르는 모양만 바뀐 것입니다.
그래서 확장 메서드는 문법 설탕이라고 부릅니다.

이걸 알면 나머지 규칙이 전부 저절로 나옵니다. 외울 것이 없습니다.

## 여기서 나오는 규칙들

**private 은 못 봅니다.** 밖에서 부르는 static 메서드니까요.

```csharp
public static void 출력(this 캐릭터 c)
{
    Console.WriteLine(c.체력);        // public 이면 됩니다
    Console.WriteLine(c._내부값);     // private 이면 에러
}
```

진짜 멤버였다면 `private` 도 봤을 겁니다. 확장 메서드는 남입니다.

**이름이 겹치면 원래 것이 이깁니다.**

```csharp
static class 도구
{
    public static string ToString(this int n) => "내 것";
}

Console.WriteLine(5.ToString());   // "5". 내 것이 아닙니다
```

컴파일러는 진짜 멤버를 먼저 찾고, 없을 때만 확장 메서드를 찾습니다.
그래서 확장 메서드로 기존 동작을 덮어쓸 수 없습니다. 이건 다행입니다.
남의 코드가 갑자기 다르게 동작하면 아무도 못 찾습니다.

**`using` 이 필요합니다.** 확장 메서드는 그 클래스가 있는 네임스페이스를 `using` 해야 보입니다.

```csharp
using 내도구;    // 이게 없으면 .이메일인가() 가 안 보입니다
```

메서드가 분명히 있는데 자동완성에 안 나오면 대개 이것입니다.
`LINQ` 를 쓰려고 `using System.Linq;` 를 적는 이유도 같습니다.

**null 이어도 안 터집니다.** 이건 좀 신기한 자리입니다.

```csharp
string s = null;
Console.WriteLine(s.이메일인가());
```

진짜 메서드였다면 이 순간 `NullReferenceException` 이 납니다. 객체가 없는데 그 객체의 메서드를 부를 수 없으니까요.
그런데 확장 메서드는 `문자열도구.이메일인가(null)` 로 바뀝니다. 매개변수로 `null` 이 들어가는 것뿐입니다.
호출은 멀쩡히 되고, 안에서 `s.Contains` 를 하는 순간 터집니다.

그래서 확장 메서드는 안에서 `null` 검사를 직접 해야 합니다.

```csharp
public static bool 이메일인가(this string s)
{
    if (s == null) return false;
    return s.Contains("@") && s.Contains(".");
}
```

## 우리가 이미 쓰고 있었다

`List` 나 배열에 이런 것을 써 본 적이 있다면 그게 전부 확장 메서드입니다.

```csharp
using System.Linq;

int[] 점수 = { 30, 90, 70 };
int 합 = 점수.Sum();
int 최고 = 점수.Max();
```

`int[]` 에는 `Sum` 이라는 메서드가 없습니다. 배열은 우리가 만든 것도 아니고 고칠 수도 없습니다.
`System.Linq.Enumerable` 이라는 static 클래스가 확장 메서드로 붙여 준 것입니다.

메서드 하나를 붙이는 이 작은 장치 위에 라이브러리 하나가 통째로 올라가 있습니다.
그 이야기는 뒤에서 따로 합니다.

## 언제 쓰고 언제 참나

**쓸 만한 자리.** 고칠 수 없는 타입(`string`, `int`, 남의 라이브러리, Unity 의 `Transform`)에
그 타입만 있으면 답이 나오는 기능을 붙일 때입니다.

```csharp
public static Vector3 XZ만(this Vector3 v)
{
    return new Vector3(v.x, 0f, v.z);
}
```

**참아야 할 자리.** 내가 만든 클래스입니다.
내 클래스면 그냥 안에 메서드를 넣으면 됩니다. 굳이 밖으로 뺄 이유가 없고,
밖으로 빼면 `private` 을 못 보니 오히려 불편해집니다.

그리고 아무 타입에나 붙이지 않는 편이 좋습니다.

```csharp
public static void 출력(this object o) { ... }   // 모든 것에 붙습니다
```

이러면 세상의 모든 변수 뒤에 `.출력()` 이 뜹니다. 자동완성이 쓰레기가 됩니다.

## 정리

- 담는 클래스가 `static`, 메서드도 `static`, 첫 매개변수 앞에 `this`. 세 개가 다 있어야 합니다
- 진짜로 붙는 것이 아니라 **컴파일러가 static 호출로 바꿔 쓰는 것**입니다
- 그래서 `private` 을 못 보고, 이름이 겹치면 원래 멤버가 이기고, `using` 이 필요합니다
- `null` 로 불러도 호출 자체는 됩니다. 안에서 검사해야 합니다
- `Sum`, `Max`, `Where` 같은 것이 전부 이 장치 위에 있습니다