핵심 정리
// delegate — 메서드를 값처럼 전달
Action<string> log = msg => Console.WriteLine(msg);
Func<int, bool> isEven = n => n % 2 == 0;
log("hello");
bool result = isEven(4); // true
// event — publisher/subscriber 패턴
public class Button
{
public event Action? Clicked;
public void RaiseClick() => Clicked?.Invoke();
}
var btn = new Button();
btn.Clicked += () => Console.WriteLine("clicked");
btn.RaiseClick();기본 구조
어떤 형태를 먼저 구분하면 되나
delegate와 event는 아래 네 형태를 먼저 구분하면 됩니다.
Action log = () => Console.WriteLine("ok"); // 반환값 없는 콜백
Func<int, bool> isEven = n => n % 2 == 0; // 반환값 있는 콜백
delegate int Operation(int a, int b); // 이름 있는 delegate
public event EventHandler? Changed; // 구독용 eventAction: 반환값 없는 콜백Func: 반환값 있는 콜백- 사용자 정의 delegate: 이름이 중요한 계약
event: 외부에서+=,-=만 허용하는 알림 경계
delegate — 메서드를 타입으로 표현
delegate 는 특정 시그니처를 가진 메서드를 참조하는 타입입니다. 메서드를 변수, 매개변수, 반환값으로 다룰 수 있게 해줍니다.
// 사용자 정의 delegate 타입
delegate int Operation(int a, int b);
Operation add = (a, b) => a + b;
Operation mul = (a, b) => a * b;
Console.WriteLine(add(3, 4)); // 7
Console.WriteLine(mul(3, 4)); // 12Action(반환값 없음)과 Func(반환값 있음)는 자주 쓰는 delegate를 미리 정의해둔 제네릭 타입입니다. 별도 이름이 필요한 계약이 아니라면 이 두 가지로 충분합니다.
Action // () → void
Action<T> // (T) → void
Func<TResult> // () → TResult
Func<T,TResult> // (T) → TResult (마지막 타입이 반환 타입)내부 동작 — 멀티캐스트 델리게이트
C#의 모든 delegate는 내부적으로 멀티캐스트입니다. +=로 여러 메서드를 연결하면 호출 목록(invocation list) 에 추가되고, Invoke() 시 등록 순서대로 모두 호출됩니다.
Action greet = () => Console.WriteLine("Hello");
greet += () => Console.WriteLine("World");
greet += () => Console.WriteLine("!");
greet(); // Hello, World, ! 순서대로 출력
// 반환값이 있는 멀티캐스트는 마지막 반환값만 캡처됨
Func<int> counter = () => 1;
counter += () => 2;
int r = counter(); // r == 2 (마지막 메서드의 반환값)event — 소유권 제한
event 키워드는 delegate를 감싸는 접근 제어 래퍼입니다. 외부 코드는 +=와 -=로만 접근할 수 있고, Invoke()는 선언한 클래스 내부에서만 호출할 수 있습니다.
public class Sensor
{
// 필드형 event — 컴파일러가 add/remove accessor 자동 생성
public event EventHandler<float>? ValueChanged;
private float _value;
public float Value
{
get => _value;
set
{
_value = value;
ValueChanged?.Invoke(this, value); // 내부에서만 발생 가능
}
}
}
var sensor = new Sensor();
sensor.ValueChanged += (s, v) => Console.WriteLine($"값 변경: {v}");
// sensor.ValueChanged?.Invoke(...) ← ❌ 외부에서 호출 불가
sensor.Value = 36.5f;// ❌ delegate 필드를 public으로 열면 외부에서 직접 Invoke 가능
public Action? Changed;
// ✅ 외부 구독만 허용하려면 event
public event Action? SafeChanged;체크포인트
| 요소 | 의미 | 선택 기준 |
|---|---|---|
Action<T> | 반환값 없는 콜백 | 간단한 콜백 전달 |
Func<T,R> | 반환값 있는 콜백 | 값 변환, 조건 판단 |
| 사용자 정의 delegate | 이름 있는 호출 계약 | 명확한 계약 이름이 필요할 때 |
event | 구독/알림용 래퍼 | 상태 변화 알림 |
EventHandler<T> | 표준 이벤트 시그니처 | .NET 이벤트 패턴 |
주의할 점
이벤트를 구독(+=)하고 해제(-=)하지 않으면 메모리 누수가 생깁니다. publisher의 수명이 subscriber보다 길 때, 해제되지 않은 subscriber는 GC 대상이 되지 않습니다.
멀티캐스트 delegate에서 예외가 발생하면 이후 핸들러는 호출되지 않습니다. 여러 핸들러 중 하나가 실패해도 나머지를 실행하려면 invocation list를 수동으로 순회하는 방식을 씁니다.
이벤트 흐름의 시각적 구조는 event 흐름 시각화 카드를 참고하세요.
참고 링크
2 sources