1. 소개
다시 떠올려봅시다: 이벤트는 공개 계약(public contract)으로, 중요한 일이 일어났을 때 구독자들을 "호출"하겠다는 약속입니다. 이전 강의의 예제에서 다음과 같은 선언을 봤을 수 있습니다:
public event Action<string> MessageSent;
또는 심지어:
public delegate void MyHandler(int value);
public event MyHandler SomethingHappened;
이건 동작하지만, 이런 접근은 여러 문제를 낳습니다 — 메서드 시그니처가 제각각인 문제부터 이벤트가 누구로부터 왔는지, 무슨 일이 일어났는지 알 수 없는 문제까지. 만약 키보드의 스페이스바를 눌렀을 때 매번 다른 동작이 실행된다면 어떨까요? 방금 전엔 점프였는데 이번엔 갑자기 인벤토리가 열린다든가. 이게 바로 표준이 없을 때의 악몽입니다.
.NET에는 이벤트를 위한 일종의 헌법 같은 게 있습니다 — 바로 핸들러 시그니처와 이벤트와 함께 전달되는 정보 구조에 대한 규약입니다. 전형적인 스타일은 다음과 같습니다:
void Handler(object sender, EventArgs args);
익숙한 형태죠? WinForms의 버튼 클릭에서부터 ASP.NET의 시스템 이벤트, 서드파티 라이브러리까지 널리 사용됩니다.
2. EventHandler와 EventArgs란?
핵심 아이디어: 각 이벤트는 두 가지를 알려줍니다:
- 누가 이벤트를 발생시켰는가? sender
- 무슨 일이 일어났는가? EventArgs — 추가 데이터
이걸 C#에서는 이렇게 표현합니다:
public delegate void EventHandler(object sender, EventArgs e);
- object sender — 이벤트의 발신자에 대한 참조. 보통은 this를 넘깁니다.
- EventArgs e — 이벤트에 대한 추가 정보가 담긴 객체. 단순한 경우에는 EventArgs.Empty를 쓰고, 복잡한 경우엔 EventArgs를 상속한 커스텀 클래스를 만듭니다.
재미있는 사실
공식 .NET 권장사항에서는 공개 API의 이벤트가 (object sender, EventArgs e) 시그니처를 갖는 것이 권장됩니다. 만약 sender와 EventArgs가 없는 이벤트를 본다면, 그건 보통 작성자가 단순화해서 만든 형태일 가능성이 큽니다.
단일 표준의 큰 이점
표준을 정해두면 이벤트를 로깅하고, 테스트하고, 범용적으로 구독하고, 시스템을 확장하기 쉬워집니다. WinForms, WPF, ASP.NET 소스 코드를 보면 같은 패턴이 반복되는 걸 확인할 수 있습니다.
3. 표준 이벤트 템플릿 사용법
1. 내장 델리게이트 EventHandler 사용
직접 델리게이트를 선언하는 대신 이미 있는 것을 쓰면 됩니다:
public event EventHandler SmthHappened;
이제 핸들러는 항상 같은 형식을 가집니다:
private void OnSmthHappened(object sender, EventArgs e)
{
// 이벤트에 대한 반응 로직
}
구독은 여전히 간단합니다:
myObj.SmthHappened += OnSmthHappened;
중요: 이벤트가 추가 데이터를 전달하지 않는다면 EventArgs.Empty를 사용하세요.
2. 사용자 정의 이벤트 인자 만들기
계산 결과, 파일명, 에러 등 정보를 전달해야 하면 EventArgs를 상속한 클래스를 만듭니다:
public class CalculationEventArgs : EventArgs
{
public double Result { get; }
public CalculationEventArgs(double result) => Result = result;
}
그 다음 제네릭 델리게이트인 EventHandler<TEventArgs>를 사용합니다:
public event EventHandler<CalculationEventArgs> CalculationFinished;
이제 핸들러는 당신의 타입을 받습니다:
private void OnCalculationFinished(object sender, CalculationEventArgs e)
{
Console.WriteLine($"계산 완료. 결과: {e.Result}");
}
4. 애플리케이션 예제
학습용 프로젝트를 확장해봅시다 — 간단한 계산기 예제로, 덧셈/뺄셈을 하고 작업 완료를 이벤트로 알립니다.
미니멀 예제
public class Calculator
{
public event EventHandler<CalculationEventArgs> CalculationPerformed;
public void Add(int a, int b)
{
int result = a + b;
// 이벤트 "발사"
CalculationPerformed?.Invoke(this, new CalculationEventArgs(result));
}
}
public class CalculationEventArgs : EventArgs
{
public int Result { get; }
public CalculationEventArgs(int result) => Result = result;
}
구독하고 사용하기:
var calc = new Calculator();
calc.CalculationPerformed += (sender, e) =>
{
Console.WriteLine($"연산 결과: {e.Result}");
};
calc.Add(10, 20);
// 출력: 연산 결과: 30
이게 전부입니다: 핸들러는 항상 발신자 객체와 인자 객체를 받습니다 — 범용적이고 직관적입니다.
5. 유용한 팁
이벤트 호출 캡슐화: 좋은 스타일
.NET에서는 이벤트 호출 로직을 On 접두사의 보호된 메서드로 분리하는 게 관례입니다:
protected virtual void OnCalculationPerformed(CalculationEventArgs e)
{
CalculationPerformed?.Invoke(this, e);
}
그리고 내부(예: Add, Subtract 등)에서는 이 메서드를 호출합니다:
public void Add(int a, int b) => OnCalculationPerformed(new CalculationEventArgs(a + b));
이 스타일은 파생 클래스가 이벤트 동작을 오버라이드할 수 있게 해주고, 이벤트 호출을 잊어버릴 가능성을 줄여줍니다.
도표: .NET 표준 이벤트 구조
graph LR
A[발행자 객체] -- "event EventHandler/ EventHandler<TEventArgs>" --> B[구독자 목록]
B -- "핸들러 메서드 (object sender, EventArgs e)" --> C[이벤트에 대한 반응]
A -- "this (발신자)" --> C
A -- "EventArgs (데이터)" --> C
이벤트 선언 방식 비교
| 방식 | 발신자 전달 (sender) | 인자 전달 | 범용성 | .NET에서의 사용 |
|---|---|---|---|---|
|
아니오 | 예 | 낮음 | 아님 |
|
예 | 예 | 중간 | 아님(희박) |
|
예 | 아니오 (EventArgs) | 높음 | 예, 표준 |
|
예 | 예 (MyArgs) | 매우 높음 | 예, 표준 |
면접과 실무에서의 실용성
표준 이벤트 템플릿 사용은 .NET 개발자에게 필수적입니다. 면접에서 EventHandler와 sender/EventArgs를 물어볼 가능성이 큽니다. Action<T> 기반 이벤트는 종종 '단순화된 버전'으로 간주됩니다.
실무에서는 이 접근법이 협업, 테스트, 확장성, 유지보수를 더 쉽게 만듭니다. 로깅이나 프로파일러 같은 서드파티 툴도 표준 형태를 쓸 때 통합이 수월합니다.
이벤트 호출 흐름도
flowchart TD
subgraph A[발행자 클래스]
C1((객체))
C2[이벤트를 호출하는 메서드]
C3["event EventHandler<MyArgs>"]
end
subgraph B[구독자 클래스]
D1((구독))
D2["이벤트 핸들러 (object sender, MyArgs args)"]
end
C1 -- 호출 --> C2
C2 -- "Invoke(this, args)" --> C3
C3 -- "알림" --> D1
D1 -- "실행" --> D2
6. 팁, 주의사항과 흔한 실수
1. 핸들러 시그니처. Action<int> 타입 이벤트로 유혹받기 쉽지만, 이렇게 하면 sender와 에코시스템 호환성을 잃습니다.
2. 인자 전달. EventArgs를 일반 파라미터와 혼동하지 마세요. 핸들러에 필요한 모든 데이터를 인자 객체에 담아 전달하세요.
3. 인자에 null 사용 금지. 추가 데이터가 없으면 null 대신 EventArgs.Empty를 사용하세요.
4. 약한 타입화. 모든 것을 한 이벤트에 때려넣는 EventHandler 사용은 지양하세요. 이벤트마다 별도의 파생 클래스(인자 타입)를 만들어 가독성과 안정성을 높이세요.
5. 이벤트 호출 실수. 항상 구독자 존재를 확인하세요: SomeEvent?.Invoke(this, e). 구독자가 없으면 이벤트 참조는 null입니다.
6. 캡슐화 위반. 발행자 클래스 밖에서 이벤트를 직접 호출하지 마세요. 이벤트는 구독/구독해지용이고, 호출은 내부의 On... 메서드를 통해 하세요.
GO TO FULL VERSION