C# Delegate & Event 시스템
델리게이트란 무엇인가
섹션 제목: “델리게이트란 무엇인가”델리게이트(Delegate)는 메서드를 가리키는 타입 안전한 함수 포인터입니다. C의 함수 포인터와 달리 타입 검사가 컴파일 타임에 이루어지고, 인스턴스 메서드와 정적 메서드 모두 참조할 수 있습니다.
// 델리게이트 타입 선언delegate int MathOperation(int a, int b);
// 메서드 정의static int Add(int a, int b) => a + b;static int Multiply(int a, int b) => a * b;
// 델리게이트 인스턴스 생성 및 호출MathOperation op = Add;Console.WriteLine(op(3, 4)); // 7
op = Multiply;Console.WriteLine(op(3, 4)); // 12델리게이트 내부 구조
섹션 제목: “델리게이트 내부 구조”컴파일러는 delegate 선언을 MulticastDelegate를 상속하는 클래스로 변환합니다.
// 컴파일러가 생성하는 대략적인 구조sealed class MathOperation : MulticastDelegate{ public MathOperation(object target, IntPtr method) { ... } public virtual int Invoke(int a, int b) { ... } public IAsyncResult BeginInvoke(int a, int b, ...) { ... } public int EndInvoke(IAsyncResult result) { ... }}MulticastDelegate는 내부적으로 _invocationList 배열을 유지하여 여러 메서드를 체인으로 연결합니다.
멀티캐스트 델리게이트
섹션 제목: “멀티캐스트 델리게이트”+= 연산자로 여러 메서드를 하나의 델리게이트에 연결할 수 있습니다.
delegate void Notify(string message);
static void LogToConsole(string msg) => Console.WriteLine($"[Console] {msg}");static void LogToFile(string msg) => Console.WriteLine($"[File] {msg}");static void LogToServer(string msg) => Console.WriteLine($"[Server] {msg}");
Notify notifier = LogToConsole;notifier += LogToFile;notifier += LogToServer;
// 세 메서드가 순서대로 호출됨notifier("시스템 시작");반환값 주의사항
섹션 제목: “반환값 주의사항”멀티캐스트 델리게이트에서 반환값이 있으면 마지막으로 호출된 메서드의 반환값만 얻을 수 있습니다.
delegate int Calculate(int x);
Calculate calc = x => x + 1;calc += x => x * 2;calc += x => x - 3;
int result = calc(5); // 마지막 메서드만: 5 - 3 = 2
// 모든 반환값을 얻으려면 직접 열거foreach (Calculate d in calc.GetInvocationList().Cast<Calculate>()){ Console.WriteLine(d(5));}event 키워드
섹션 제목: “event 키워드”event 키워드는 델리게이트 필드를 이벤트로 노출합니다. 외부에서 =(직접 할당)은 금지되고, +=/-=만 허용됩니다.
class Button{ // event 없이: 외부에서 = 로 모든 구독자를 날릴 수 있음 public Action? Clicked_Unsafe;
// event 있음: 외부에서는 += / -= 만 가능 public event Action? Clicked;
public void SimulateClick() { Clicked?.Invoke(); }}
var btn = new Button();
btn.Clicked += () => Console.WriteLine("구독자 A");btn.Clicked += () => Console.WriteLine("구독자 B");
// 컴파일 오류: 외부에서 = 할당 불가// btn.Clicked = () => Console.WriteLine("덮어씀");
btn.SimulateClick();// 구독자 A// 구독자 BEventHandler 패턴
섹션 제목: “EventHandler 패턴”.NET 표준 이벤트 패턴은 EventHandler<TEventArgs>를 사용합니다.
// 이벤트 데이터 클래스public class OrderEventArgs : EventArgs{ public int OrderId { get; init; } public decimal Amount { get; init; }}
// 발행자 (Publisher)public class OrderService{ public event EventHandler<OrderEventArgs>? OrderPlaced;
public void PlaceOrder(int id, decimal amount) { // 주문 처리 로직 ...
// 이벤트 발생 (null 조건 연산자로 구독자 없을 때 보호) OrderPlaced?.Invoke(this, new OrderEventArgs { OrderId = id, Amount = amount }); }}
// 구독자 (Subscriber)var service = new OrderService();
service.OrderPlaced += (sender, e) =>{ Console.WriteLine($"주문 {e.OrderId}: {e.Amount:C}");};
service.PlaceOrder(1001, 59900);Action, Func, Predicate
섹션 제목: “Action, Func, Predicate”사용자 정의 델리게이트 대신 내장 제네릭 델리게이트를 활용합니다.
// Action: 반환값 없음 (void)Action<string> log = msg => Console.WriteLine(msg);Action<int, int> print = (a, b) => Console.WriteLine(a + b);
// Func: 마지막 타입 매개변수가 반환 타입Func<int, int, int> add = (a, b) => a + b;Func<string, bool> isLong = s => s.Length > 10;
// Predicate: bool 반환 (Where 등에서 사용)Predicate<int> isEven = n => n % 2 == 0;bool result = isEven(4); // true
// List의 FindAll은 Predicate를 사용var evens = new List<int> { 1, 2, 3, 4, 5 }.FindAll(isEven);약한 이벤트 패턴 (Memory Leak 방지)
섹션 제목: “약한 이벤트 패턴 (Memory Leak 방지)”이벤트 구독은 강한 참조를 생성합니다. 구독 해제 없이 구독자가 사라지면 메모리 누수가 발생합니다.
// 문제 상황class EventSource{ public event Action? Updated;}
class Subscriber{ public Subscriber(EventSource source) { // 람다가 source.Updated에 강한 참조를 유지 source.Updated += OnUpdate; }
private void OnUpdate() => Console.WriteLine("업데이트");
// Dispose 없이 Subscriber가 GC 되어도 // source.Updated가 OnUpdate를 참조 → Subscriber 메모리 해제 안 됨}
// 해결책 1: 명시적 구독 해제class SafeSubscriber : IDisposable{ private readonly EventSource _source;
public SafeSubscriber(EventSource source) { _source = source; _source.Updated += OnUpdate; }
private void OnUpdate() => Console.WriteLine("업데이트");
public void Dispose() { _source.Updated -= OnUpdate; }}
// 사용using var sub = new SafeSubscriber(eventSource);델리게이트와 람다의 관계
섹션 제목: “델리게이트와 람다의 관계”람다 표현식은 컴파일러가 익명 메서드 또는 표현식 트리로 변환합니다.
// 클로저: 외부 변수 캡처int multiplier = 3;Func<int, int> triple = x => x * multiplier;
// 컴파일러가 생성하는 대략적인 코드class Closure{ public int multiplier; public int Method(int x) => x * multiplier;}
var closure = new Closure { multiplier = 3 };Func<int, int> triple = closure.Method;클로저는 변수 값이 아닌 변수 자체를 캡처합니다.
var actions = new List<Action>();for (int i = 0; i < 3; i++){ int captured = i; // 루프 변수를 지역 변수에 복사 actions.Add(() => Console.WriteLine(captured));}
foreach (var a in actions) a();// 0, 1, 2 (captured를 복사하지 않으면 모두 3)스레드 안전 이벤트 호출
섹션 제목: “스레드 안전 이벤트 호출”멀티스레드 환경에서 이벤트 호출 시 null 검사와 호출 사이에 구독 해제가 일어날 수 있습니다.
// 위험: null 검사 후 Invoke 사이에 다른 스레드가 -= 할 수 있음if (Updated != null) Updated(); // NullReferenceException 가능
// 안전: 로컬 변수에 복사 (C# 6+ null 조건 연산자 권장)Updated?.Invoke();
// 또는 명시적 복사var handler = Updated;handler?.Invoke();| 개념 | 설명 |
|---|---|
| delegate | 타입 안전한 함수 포인터. MulticastDelegate 기반 |
| event | 외부 = 할당 금지. 캡슐화된 델리게이트 |
| Action/Func | 내장 제네릭 델리게이트. 커스텀 정의 불필요 |
| 멀티캐스트 | += 로 여러 메서드 연결. 반환값은 마지막 것만 |
| 메모리 누수 | 구독 해제(-=) 또는 IDisposable 패턴으로 방지 |
델리게이트와 이벤트는 C# 리액티브 프로그래밍, 옵저버 패턴, UI 프레임워크의 근간을 이룹니다. 내부 구조를 이해하면 메모리 누수나 경쟁 조건 같은 미묘한 버그를 사전에 방지할 수 있습니다.