---
title: "대리자는 메서드를 담는 변수다"
description: "값이 아니라 무엇을 할지를 담습니다. 부르는 쪽과 실제 동작을 떼어 놓을 수 있습니다."
author: "유니얼"
pubDate: 2026-09-09
category: "programming/csharp/basics"
tags: ["C#", "기초"]
canonical: https://blog.unialgames.com/posts/csharp-delegate/
---
목록에서 조건에 맞는 것만 세는 메서드를 만든다고 해 봅시다.

```csharp
int CountEven(List<int> nums)
{
    int count = 0;
    foreach (var n in nums) if (n % 2 == 0) count++;
    return count;
}
```

양수만 세려면 `CountPositive` 를 만들고, 100 이상만 세려면 또 만듭니다.
**반복하는 부분은 완전히 같고 `if` 안의 조건만 다릅니다.**

제네릭은 여기서 도움이 안 됩니다. 제네릭은 **타입**을 나중에 정하는 것인데,
여기서 나중에 정하고 싶은 것은 **동작**입니다.

> **무엇을 할지를 변수에 담아 넘길 수 있으면 됩니다.**

## 대리자는 이렇게 생겼다

```csharp
delegate bool Check(int n);
```

**메서드의 생김새**를 적은 것입니다. `int` 하나를 받아 `bool` 을 돌려주는 메서드,
그런 것을 담을 수 있는 타입이 `Check` 입니다.

```csharp
bool IsEven(int n) { return n % 2 == 0; }
bool IsPositive(int n) { return n > 0; }

Check c = IsEven;      // 괄호 없이 이름만
Console.WriteLine(c(4));   // True

c = IsPositive;
Console.WriteLine(c(4));   // True
```

`IsEven` 뒤에 괄호가 없습니다. **부르는 것이 아니라 담는 것**이라 그렇습니다.
괄호를 붙이면 실행되고 그 결과가 담깁니다. 완전히 다른 일입니다.

담아 두고 나중에 `c(4)` 로 부릅니다. 그때 실제로 실행됩니다.

맞아야 하는 것은 **받는 것과 돌려주는 것**뿐입니다. 이름은 상관없습니다.

```csharp
Check c = IsPositive;              // bool(int) 라 됩니다
Check c = Console.WriteLine;       // 생김새가 안 맞아서 컴파일 에러
```

## 이제 조건을 넘길 수 있다

```csharp
int Count(List<int> nums, Check test)
{
    int count = 0;
    foreach (var n in nums) if (test(n)) count++;
    return count;
}

Count(nums, IsEven);
Count(nums, IsPositive);
```

반복하는 코드는 한 벌이고, 조건만 갈아 끼웁니다.
**어떻게 반복할지는 `Count` 가 알고, 무엇을 고를지는 부르는 쪽이 정합니다.**

앞에서 다형성을 볼 때 비슷한 이야기를 했습니다.
거기서는 클래스를 만들어 갈아 끼웠고, 여기서는 메서드 하나를 갈아 끼웁니다.
동작 하나만 다르면 클래스를 만들 것 없이 이쪽이 가볍습니다.

## Action 과 Func 는 미리 만들어 둔 것이다

`delegate` 를 매번 선언하는 것이 번거롭습니다.
생김새가 같으면 이름만 다른 타입을 계속 만들게 됩니다.

그래서 미리 만들어 둔 것이 있습니다.

```csharp
Func<int, bool> test = IsEven;      // int 를 받아 bool 을 돌려준다
Action<string> print = Console.WriteLine;   // string 을 받고 안 돌려준다
```

| | 돌려주는 것 | 꺾쇠 안 |
| --- | --- | --- |
| `Action` | 없음 (`void`) | 받는 타입들 |
| `Func` | 있음 | 받는 타입들 + **맨 뒤가 돌려줄 타입** |

```csharp
Action                      // 받지도 돌려주지도 않음
Action<int>                 // int 를 받고 안 돌려줌
Action<int, string>         // int 와 string 을 받음

Func<int>                   // 안 받고 int 를 돌려줌
Func<int, bool>             // int 를 받고 bool 을 돌려줌
Func<int, int, int>         // int 둘을 받고 int 를 돌려줌
```

`Func` 의 **맨 마지막 칸이 돌려줄 타입**입니다. 이것만 기억하면 됩니다.

앞의 코드를 다시 쓰면 이렇게 됩니다.

```csharp
int Count(List<int> nums, Func<int, bool> test)
```

`delegate` 선언이 사라졌습니다. 실무에서 `delegate` 를 직접 선언할 일은 드뭅니다.

## 여러 개를 담을 수 있다

대리자에는 메서드를 **여러 개** 담을 수 있습니다.

```csharp
Action<string> log = Console.WriteLine;
log += SaveToFile;
log += SendToServer;

log("서버 시작");      // 셋이 순서대로 실행됩니다
```

`+=` 로 더하고 `-=` 로 뺍니다. 부르면 담긴 것이 **등록된 순서대로** 전부 실행됩니다.

여기서 주의할 것이 있습니다. `Func` 처럼 값을 돌려주는 대리자에 여러 개를 담으면
**마지막 것의 결과만** 나옵니다. 나머지는 버려집니다.
그래서 여러 개를 담는 것은 보통 `Action` 입니다.


## null 이면 터진다

대리자는 참조 타입입니다. 아무것도 안 담겨 있으면 `null` 입니다.

```csharp
Action<string> log = null;
log("안녕");      // NullReferenceException
```

담긴 것을 전부 빼도 `null` 이 됩니다. 0개짜리 대리자는 없습니다.

```csharp
if (log != null) log("안녕");
log?.Invoke("안녕");           // 짧게 쓰는 방법
```

`?.` 는 앞이 `null` 이면 그냥 넘어가라는 뜻입니다. `null` 관련 문법은 뒤에서 다룹니다.
`Invoke` 는 대리자를 부르는 메서드입니다. `log("안녕")` 과 같은 일인데,
`?.` 를 붙이려면 이 형태로 써야 합니다.

## 어디에 쓰나

**언제 실행될지는 모르지만 무엇을 할지는 지금 정해 두고 싶을 때** 씁니다.

- 버튼을 눌렀을 때 할 일
- 정렬할 때 무엇을 기준으로 비교할지
- 목록에서 무엇을 고를지
- 작업이 끝나면 알려 줄 곳

전부 "부르는 쪽은 언제인지 알고, 넘기는 쪽은 무엇인지 안다" 는 구조입니다.
둘을 떼어 놓는 것이 대리자가 하는 일입니다.

## 정리

- 대리자는 메서드를 담는 변수입니다. 값이 아니라 무엇을 할지를 담습니다.
- 담을 때는 괄호 없이 이름만 적습니다. 괄호를 붙이면 실행됩니다.
- 받는 것과 돌려주는 것이 맞으면 담을 수 있습니다. 이름은 상관없습니다.
- `Action` 은 안 돌려주고 `Func` 는 돌려줍니다. `Func` 의 꺾쇠 맨 뒤가 돌려줄 타입입니다.
- `+=` 로 여러 개를 담을 수 있고 순서대로 실행됩니다. 값은 마지막 것만 남습니다.
- 아무것도 안 담겨 있으면 `null` 입니다. 부르기 전에 확인합니다.
- 부르는 쪽과 실제 동작을 떼어 놓을 때 씁니다.