메서드를 값처럼 다룬다는 뜻
지금까지 메서드는 보통 이름으로 직접 호출했다.
Console.WriteLine("Hello");그런데 프로그램을 만들다 보면 "지금 바로 실행할 메서드"가 아니라 "나중에 실행할 동작"을 다른 코드에 전달해야 할 때가 있다. 예를 들어 버튼을 클릭했을 때 실행할 코드, 데이터가 바뀌었을 때 알림을 받을 코드, 조건에 맞는 값만 골라낼 코드가 그렇다.
C#에서는 이런 상황에 델리게이트(delegate)를 사용한다. 델리게이트는 메서드 참조를 저장하고 호출할 수 있는 타입이다. 쉽게 말하면 "이런 모양의 메서드를 담을 수 있다"라고 정해 두는 그릇이다. 여기서 중요한 모양은 어떤 매개변수를 받는지, 그리고 어떤 값을 반환하는지다. C#에서 엄밀히 말하는 메서드 시그니처는 주로 매개변수 목록을 중심으로 하지만, 델리게이트에 메서드를 연결하려면 반환 타입까지 함께 맞아야 한다.1
다음 예제를 보자.
Operation operation = Add;
Console.WriteLine(operation(3, 4));
operation = Multiply;
Console.WriteLine(operation(3, 4));
int Add(int left, int right)
{
return left + right;
}
int Multiply(int left, int right)
{
return left * right;
}
delegate int Operation(int left, int right);실행 결과는 다음과 같다.
7
12Operation은 int 두 개를 받아 int 하나를 반환하는 메서드를 담을 수 있는 델리게이트다. Add와 Multiply는 둘 다 이 모양에 맞기 때문에 operation 변수에 저장할 수 있다.
중요한 점은 델리게이트 변수가 "계산 결과"를 저장하는 것이 아니라 "호출할 메서드"를 저장한다는 것이다.
Operation operation = Add;이 코드는 Add를 지금 실행하는 코드가 아니다. Add라는 메서드를 나중에 실행할 수 있도록 참조로 저장하는 코드다. 실제 실행은 다음 줄에서 일어난다.
operation(3, 4);
AI 생성 이미지
Action, Func, Predicate 사용하기
매번 직접 delegate 타입을 선언해도 되지만, C#과 .NET에는 자주 쓰는 델리게이트 타입이 이미 준비되어 있다.2
| 타입 | 의미 | 예 |
|---|---|---|
Action | 값을 반환하지 않는 동작 | 출력, 저장, 알림 |
Func | 값을 반환하는 동작 | 계산, 변환 |
Predicate | 참/거짓을 판단하는 동작 | 조건 검사 |
간단한 예제를 보자.
Action<string> print = message => Console.WriteLine(message);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = number => number % 2 == 0;
print("계산을 시작합니다.");
Console.WriteLine(add(10, 20));
Console.WriteLine(isEven(7));
Console.WriteLine(isEven(8));실행 결과는 다음과 같다.
계산을 시작합니다.
30
False
TrueAction<string>은 string을 하나 받고 반환값은 없는 동작이다. 그래서 화면 출력처럼 "작업만 하고 끝나는" 코드에 어울린다.
Func<int, int, int>는 앞의 두 int가 매개변수 타입이고, 마지막 int가 반환 타입이다. 즉 int 두 개를 받아 int 하나를 반환한다는 뜻이다.
Predicate<int>는 int 하나를 받아 bool을 반환하는 조건 검사다. Func<int, bool>로도 표현할 수 있지만, Predicate<int>라고 쓰면 "이 코드는 조건을 판단한다"는 의도가 더 잘 드러난다.
람다식으로 짧은 동작 전달하기
앞 예제에서 사용한 message => Console.WriteLine(message) 같은 문법을 람다식(lambda expression)이라고 한다. 람다식은 이름 없는 작은 메서드를 그 자리에서 작성하는 문법이다. => 기호는 왼쪽의 입력을 오른쪽의 동작이나 결과로 연결한다고 읽으면 된다.3
람다식에는 크게 두 가지 형태가 있다.
Func<int, int> square = x => x * x;
Action<string> greet = name =>
{
string message = $"안녕하세요, {name}님";
Console.WriteLine(message);
};
Console.WriteLine(square(5));
greet("지수");실행 결과는 다음과 같다.
25
안녕하세요, 지수님첫 번째는 식 람다다.
x => x * x오른쪽이 하나의 식이면 그 결과가 자동으로 반환된다.
두 번째는 문장 람다다.
name =>
{
string message = $"안녕하세요, {name}님";
Console.WriteLine(message);
}여러 문장을 실행해야 할 때는 중괄호를 사용한다. 반환값이 필요한 Func에서 문장 람다를 쓴다면 return을 직접 작성해야 한다.
Func<int, int> square = x =>
{
return x * x;
};람다식이 유용한 이유는 짧은 동작을 메서드에 바로 전달할 수 있기 때문이다.
List<int> scores = new List<int> { 55, 70, 83, 40, 100 };
PrintFiltered(scores, score => score >= 60);
void PrintFiltered(List<int> values, Predicate<int> condition)
{
foreach (int value in values)
{
if (condition(value))
{
Console.WriteLine(value);
}
}
}실행 결과는 다음과 같다.
70
83
100PrintFiltered 메서드는 어떤 조건으로 걸러야 하는지 직접 알지 못한다. 대신 Predicate<int>를 매개변수로 받아서 조건 판단을 바깥에서 전달받는다. 호출하는 쪽은 score => score >= 60처럼 필요한 조건만 짧게 넘긴다.
이 방식은 다음 장에서 배울 LINQ의 핵심 흐름과도 연결된다. 컬렉션을 처리할 때 "어떻게 반복할지"보다 "어떤 값을 고를지, 어떻게 바꿀지"를 람다식으로 표현하는 일이 많다.
이벤트로 특정 상황 알리기
이벤트(event)는 어떤 일이 발생했을 때 등록된 코드들을 실행하는 구조다. 이벤트는 델리게이트를 바탕으로 만들어졌지만, 외부 코드가 마음대로 호출하지 못하도록 더 안전한 형태를 제공한다. 이벤트를 선언한 클래스 안에서는 이벤트를 발생시킬 수 있고, 외부에서는 보통 +=로 구독하거나 -=로 구독 해제만 할 수 있다.1
다음은 숫자가 증가할 때마다 알림을 보내는 예제다.
Counter counter = new Counter();
counter.Changed += message => Console.WriteLine(message);
counter.Increase();
counter.Increase();
class Counter
{
private int _count;
public event Action<string>? Changed;
public void Increase()
{
_count++;
Changed?.Invoke($"현재 값: {_count}");
}
}실행 결과는 다음과 같다.
현재 값: 1
현재 값: 2여기서 Counter는 이벤트를 발생시키는 쪽이다. 이런 쪽을 게시자(publisher)라고 부른다. counter.Changed += ...로 이벤트를 등록한 코드는 알림을 받는 쪽이다. 이런 쪽을 구독자(subscriber)라고 부른다.
이벤트를 발생시키는 코드는 다음 줄이다.
Changed?.Invoke($"현재 값: {_count}");?.Invoke는 구독자가 있을 때만 호출하라는 뜻이다. 구독자가 하나도 없으면 Changed는 null일 수 있다. 그래서 Changed.Invoke(...)처럼 바로 호출하면 오류가 날 수 있다.
이벤트는 버튼 클릭, 파일 다운로드 완료, 데이터 변경, 타이머 알림 같은 상황에 자주 사용된다. 사용자는 "그 일이 발생하면 실행할 코드"만 등록하면 되고, 이벤트를 가진 객체는 적절한 시점에 구독자들에게 알려 준다.
이벤트 구독 해제하기
이벤트를 구독했다면 필요 없어졌을 때 해제할 수 있어야 한다.
Counter counter = new Counter();
Action<string> handler = message => Console.WriteLine($"알림: {message}");
counter.Changed += handler;
counter.Increase();
counter.Changed -= handler;
counter.Increase();
class Counter
{
private int _count;
public event Action<string>? Changed;
public void Increase()
{
_count++;
Changed?.Invoke($"현재 값: {_count}");
}
}실행 결과는 다음과 같다.
알림: 현재 값: 1두 번째 Increase에서도 이벤트는 발생한다. 하지만 그 전에 counter.Changed -= handler;로 구독을 해제했기 때문에 더 이상 출력되지 않는다.
주의할 점이 있다. 다음처럼 람다식을 바로 구독하면 나중에 같은 코드를 써도 쉽게 해제할 수 없다.
counter.Changed += message => Console.WriteLine(message);
// 보기에는 같은 코드 같지만, 앞에서 등록한 람다를 정확히 가리키는 것이 아니다.
counter.Changed -= message => Console.WriteLine(message);두 람다식은 겉으로는 같아 보여도 서로 다른 델리게이트 인스턴스로 취급될 수 있다. 그래서 구독 해제가 필요하면 람다식을 변수에 담아 두는 습관이 좋다.
이벤트 구독을 해제하지 않으면 원하지 않는 시점에 코드가 계속 실행될 수 있다. 또한 오래 살아 있는 객체가 이벤트를 가지고 있고, 짧게 사용해야 할 객체가 그 이벤트를 구독한 상태라면 메모리에서 정리되지 못하는 문제가 생길 수 있다. 모든 이벤트에서 반드시 해제해야 하는 것은 아니지만, 수명이 긴 객체의 이벤트를 구독할 때는 해제 시점을 생각해야 한다.
흔한 실수
첫째, 델리게이트의 입력과 반환 타입을 맞추지 않는 실수다.
Func<int, int, int> operation = Add;
string Add(int a, int b)
{
return (a + b).ToString();
}Func<int, int, int>는 int 두 개를 받아 int를 반환해야 한다. 그런데 Add는 string을 반환하므로 컴파일 오류가 난다.
둘째, Func의 마지막 타입을 헷갈리는 실수다.
Func<int, string> convert = number => number.ToString();여기서 int는 입력 타입이고 string은 반환 타입이다. Func에서는 항상 마지막 타입이 반환 타입이다.
셋째, 이벤트를 직접 호출하려는 실수다. 이벤트를 선언한 클래스 밖에서는 이벤트를 직접 실행할 수 없다. 이것이 event 키워드가 주는 안전함이다. 외부 코드는 구독과 구독 해제만 하고, "언제 알릴지"는 이벤트를 가진 객체가 결정한다.
넷째, 짧은 람다식을 너무 많이 겹쳐 읽기 어려운 코드를 만드는 실수다. 람다식은 짧고 분명할 때 좋다. 조건이나 동작이 길어진다면 이름 있는 메서드로 분리하는 편이 더 읽기 쉽다.
작은 연습 문제
Func<int, int, int>를 사용해 두 숫자 중 더 큰 값을 반환하는 람다식을 만들어 보자.max(10, 7)을 호출해 결과를 출력하라.Predicate<string>을 사용해 문자열 길이가 5 이상인지 검사하는 조건을 만들어 보자."apple","hi"를 각각 검사해 보라.TemperatureSensor클래스를 만들고public event Action<int>? Changed;이벤트를 선언해 보자.SetTemperature(int value)메서드에서 온도를 저장하고 이벤트로 새 온도를 알려 보라.3번 문제에서 구독한 핸들러를
-=로 해제한 뒤 다시 온도를 변경해 보자. 해제 후에는 출력이 발생하지 않아야 한다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.