Quick Reference
delegate는 메서드를 값처럼 전달하는 호출 계약이고, event는 publisher만 발생시키고 외부는 +=/-=로만 구독하게 하는 알림 경계입니다. 일반 .NET event는 동기 void handler이므로, 비동기 완료가 필요하면 Task를 반환하는 별도 API를 설계합니다.
public sealed class Counter
{
private int _value;
public event EventHandler<CountChangedEventArgs>? CountChanged;
public void Increment()
{
_value++;
CountChanged?.Invoke(this, new CountChangedEventArgs(_value));
}
}
public sealed class CountChangedEventArgs(int value) : EventArgs
{
public int Value { get; } = value;
}- 간단한 callback은
Action/Func,ref·out또는 도메인 이름이 필요한 계약은 사용자 정의 delegate를 씁니다. event는 외부의 대입·호출을 막지만, publisher 내부의 예외 정책과 구독 해제 시점은 따로 정해야 합니다.- multicast delegate는 등록 순서로 동기 호출하며 한 handler가 예외를 던지면 기본 호출은 그 지점에서 멈춥니다.
- publisher가 subscriber보다 오래 살면 subscriber가
-=또는Dispose()로 해제해야 합니다.
Delegate 계약
Action<string> log = message => Console.WriteLine(message);
Func<int, bool> isEven = value => value % 2 == 0;
public delegate bool RetryPolicy(Exception error, int attempt);
RetryPolicy retryNetworkErrors = (error, attempt) =>
error is HttpRequestException && attempt < 3;Action은 반환값 없는 callback, Func는 마지막 type argument가 반환값인 callback입니다. 사용자 정의 delegate는 호출 계약을 이름으로 드러내거나 ref/out 같은 시그니처를 표현해야 할 때 적합합니다.
delegate에 여러 handler를 +=하면 하나의 invocation list가 됩니다. 반환값이 있는 multicast delegate는 모든 handler를 호출하지만 호출 결과로는 마지막 handler의 반환값만 받습니다. 각 결과가 필요하면 GetInvocationList()를 순회하는 명시적 정책을 만듭니다.
Event 소유권과 수명
외부 코드는 event에 handler를 추가하거나 제거할 수 있지만, publisher 외부에서 Invoke하거나 delegate를 대입할 수 없습니다. handler를 해제할 수 있도록 같은 delegate 인스턴스를 유지하는 것이 중요합니다.
public sealed class CounterPanel : IDisposable
{
private readonly Counter _counter;
public CounterPanel(Counter counter)
{
_counter = counter;
_counter.CountChanged += OnCountChanged;
}
private void OnCountChanged(object? sender, CountChangedEventArgs args)
{
Console.WriteLine(args.Value);
}
public void Dispose() => _counter.CountChanged -= OnCountChanged;
}publisher가 singleton, UI root, 장수명 서비스라면 구독 중인 subscriber를 참조한 채 유지할 수 있습니다. subscriber가 더 짧게 살면 lifecycle 종료 지점에서 해제합니다. local event delegate를 snapshot으로 가져와 호출하는 관례는 구독 목록을 읽는 순간의 일관성을 주지만, snapshot 뒤의 unsubscribe가 이미 잡힌 handler 실행을 취소하지는 않습니다.
예외와 비동기 정책
일반 EventHandler<T> 호출은 fail-fast입니다. 예외 하나가 이후 handler 호출을 막아도 되는지, handler별로 기록하고 계속할지 publisher가 정책을 정합니다. 무조건 예외를 삼키면 실패가 사라지므로, isolation 정책에는 관찰·보고 경로가 필요합니다.
public static async Task NotifyAllAsync(
IEnumerable<Func<CountChangedEventArgs, CancellationToken, Task>> handlers,
CountChangedEventArgs args,
CancellationToken cancellationToken)
{
Task[] tasks = handlers.Select(handler => handler(args, cancellationToken)).ToArray();
await Task.WhenAll(tasks);
}async void event handler는 publisher가 완료·예외를 await할 수 없습니다. 단순 UI 알림에는 허용될 수 있지만, 저장·네트워크처럼 성공 여부가 필요한 흐름은 위처럼 Func<..., Task>를 받는 별도 async API로 모델링합니다. 동시성 제한과 aggregate failure는 Task.WhenAll에서 다룹니다.
자주 틀리는 부분
inline lambda를 += 한 뒤 같은 모양의 새 lambda를 -= 해도 원래 handler는 제거되지 않습니다. 제거해야 한다면 delegate를 변수에 보관하거나 이름 있는 메서드를 구독하세요.
event는 thread-safe publisher를 자동으로 만들지 않습니다. 상태 변경, 구독 변경, handler 실행이 동시에 일어나는 경우의 잠금·snapshot·종료 정책은 별도로 설계해야 합니다.
참고 링크
2 sources