1. 이벤트 구독 문법
작업 관리 앱을 생각해봐: 작업이 완료되면 이벤트가 발생하고, 핸들러는 예를 들어 알림을 보내거나 UI를 업데이트하거나 로그를 남길 수 있어. C#은 이벤트 구독을 명시적이고 안전하게 해줘: 어떤 핸들러가 특정 이벤트에 반응하는지 분명히 표시할 수 있어.
C#에서 이벤트 구독은 파티에 손님으로 이름 올리는 것과 거의 같아:
publisher.MyEvent += HandlerMethod;
여기서 publisher는 이벤트 MyEvent를 선언한 객체고, HandlerMethod는 이벤트가 발생했을 때 호출될 메서드야.
간단한 예제로 살펴보자. 클릭 수를 세는 애플리케이션이 있다고 가정해보자:
public class Clicker
{
public event Action Clicked;
public void Click()
{
// 무언가 클릭됨!
Clicked?.Invoke();
}
}
이건 Clicked라는 "발행자"야. 이제 핸들러를 구독해보자:
Clicker clicker = new Clicker();
void OnClicked()
{
Console.WriteLine("버튼이 눌렸어!");
}
clicker.Clicked += OnClicked;
// 코드 어딘가에서
clicker.Click();
// → "버튼이 눌렸어!"
어떻게 동작하냐면? "클릭" 이벤트에 우리 메서드 OnClicked를 추가했고, 클릭이 발생할 때마다 이 메서드가 호출돼.
2. 이벤트 핸들러: 종류와 선언 방법
이벤트 핸들러는 이벤트가 발생했을 때 호출되는 메서드야. 시그니처는 이벤트의 델리게이트 타입과 일치해야 해. 예를 들어 이벤트가 public event Action Clicked; 로 선언되어 있다면, 핸들러는 매개변수와 반환값이 없는 메서드여야 해.
Action용 핸들러
void OnClicked()
{
Console.WriteLine("이벤트 발생 (Action)!");
}
표준 EventHandler용 핸들러
고전적인 EventHandler를 사용하면 핸들러는 두 개의 매개변수를 받지: 발신자(object sender)와 이벤트 데이터(EventArgs e):
public class Alarm
{
public event EventHandler AlarmRaised;
public void RaiseAlarm()
{
AlarmRaised?.Invoke(this, EventArgs.Empty);
}
}
Alarm alarm = new Alarm();
void AlarmHandler(object sender, EventArgs e)
{
Console.WriteLine("알람이 울렸어!");
}
alarm.AlarmRaised += AlarmHandler;
alarm.RaiseAlarm();
익명 메서드와 람다식
C#은 구독 시점에 익명 함수나 람다식을 직접 사용할 수 있어:
clicker.Clicked += () => Console.WriteLine("또 다른 클릭!");
이벤트에 인자가 있으면 조금 더 복잡해질 수 있어:
alarm.AlarmRaised += (sender, e) =>
{
Console.WriteLine($"Alarm raised by: {sender}");
};
4. 유용한 뉘앙스
구독과 구독 해제: 중요한 점들
현실에서는 파티에 손님이 너무 많아지거나 누군가 집에 가고 싶어할 수 있어. 델리게이트도 마찬가지: 핸들러는 추가(구독)할 수 있고 제거(구독 해제)할 수 있어:
// 구독
publisher.MyEvent += MyHandler;
// 구독 해제 (더 이상 필요 없을 때)
publisher.MyEvent -= MyHandler;
왜 중요하냐면? 특히 큰 애플리케이션에서 구독 해제를 하지 않으면 핸들러가 "떠 있는" 상태가 되어 가비지 컬렉터가 객체를 정리하지 못해서 메모리 누수가 생길 수 있어.
여러 핸들러
하나의 이벤트에 원하는 만큼 핸들러를 구독할 수 있어. 추가된 순서대로 호출돼.
clicker.Clicked += () => Console.WriteLine("첫 번째 핸들러!");
clicker.Clicked += () => Console.WriteLine("두 번째 핸들러!");
clicker.Click();
// → 첫 번째 핸들러!
// → 두 번째 핸들러!
런타임에 핸들러를 구독하거나 해제하는 것도 가능해서 유연하게 설계할 수 있어.
실제 시나리오에서 왜 중요한가
- UI (Windows Forms, WPF, WinUI, MAUI): 버튼 클릭, 텍스트 변경 같은 것들이 이벤트야.
- FileSystemWatcher: 폴더에 새 파일이 생겼을 때 알림을 받음.
- 비동기 작업: 파일 다운로드 완료, 작업 진행률 업데이트 등.
- 플러그인 시스템: 별도 모듈들이 메인 앱의 이벤트를 구독함.
이벤트 모델은 확장 가능한 아키텍처를 만들게 해줘: 기본 코드를 바꾸지 않고도 새 모듈을 추가해서 기존 이벤트에 구독할 수 있어.
구독 예시
| 이벤트 시그니처 | 구독 예시 | 핸들러 예시 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
5. 흔한 실수와 함정
실수 #1: 핸들러 시그니처 불일치.
이벤트가 event Action<int>로 선언되어 있는데 매개변수 없는 메서드를 구독하려 하면 컴파일 오류가 날 거야. 항상 메서드가 이벤트의 시그니처와 일치하는지 확인해.
실수 #2: 람다에서 변수 캡처.
람다로 구독할 때 주변 스코프의 변수를 캡처할 수 있어(클로저). 구독 후에 그 변수가 바뀌면 핸들러는 변경된 값을 보게 돼서 예상치 못한 동작이 발생할 수 있어.
실수 #3: 초기화되지 않은 객체의 이벤트에 구독.
객체가 생성되기 전에 그 객체의 이벤트에 구독하면 NullReferenceException이 발생할 위험이 있어. 구독하기 전에 객체가 사용 가능한 상태인지 확인해.
실수 #4: 같은 핸들러를 여러 번 구독.
같은 핸들러를 여러 번 구독하면 그만큼 여러 번 호출돼. 버그는 아니지만 의도치 않은 중복 호출을 만들어서 놀랄 수 있어.
GO TO FULL VERSION