이벤트를 실무에서 잘 쓰려면 EventArgs를 적절히 상속해 필요한 데이터만 안전하게 전달하는 것이 핵심입니다. .NET 권장 패턴(EventHandler<TEventArgs>, OnXxx 메서드)을 따르면 유지보수성과 테스트가 크게 좋아집니다.
1. 왜 EventArgs를 상속하나요?
- 타입 안전하게 이벤트 데이터를 묶어 전달합니다.
- 이벤트 시그니처를 표준화(EventHandler, EventHandler<T>)합니다.
- 불변 설계로 예측 가능성을 높입니다.
- 확장이 쉽고 도메인 의도가 분명해집니다.
2. EventHandler<T>와 On 패턴 기본형
가장 일반적인 패턴은 EventHandler<TEventArgs>와 보호된 가상 OnXxx 메서드입니다. 구독/해제, 스레드 안전 호출을 쉽게 보장합니다.
using System;
public sealed class OrderProcessedEventArgs : EventArgs
{
public int OrderId { get; }
public decimal Amount { get; }
public DateTime ProcessedAt { get; }
public OrderProcessedEventArgs(int orderId, decimal amount, DateTime processedAt)
{
OrderId = orderId;
Amount = amount;
ProcessedAt = processedAt;
}
}
public class OrderService
{
public event EventHandler<OrderProcessedEventArgs>? Processed;
public void Process(int orderId, decimal amount)
{
// ... 처리 로직
OnProcessed(new OrderProcessedEventArgs(orderId, amount, DateTime.UtcNow));
}
protected virtual void OnProcessed(OrderProcessedEventArgs e)
{
// 스레드 안전한 호출 관용구
var handler = Processed;
handler?.Invoke(this, e);
}
}
public class Logger
{
public void Subscribe(OrderService service)
{
service.Processed += OnProcessed;
}
private void OnProcessed(object? sender, OrderProcessedEventArgs e)
{
Console.WriteLine($"Order {e.OrderId} processed: {e.Amount}, at {e.ProcessedAt:o}");
}
}
포인트: EventArgs 파생형은 보통 불변 속성(get 전용)으로 설계합니다. 이벤트 발행 측은 OnXxx에서만 호출하며, 외부에서 임의로 발생시키지 못하도록 보호합니다.
3. 취소 가능한 이벤트: CancelEventArgs 확장
작업 전(BeforeXxx) 훅에서 유효성 검사 등으로 작업을 중단해야 할 때 CancelEventArgs를 상속해 양방향 신호(발행자 <-> 구독자)를 구현합니다.
using System;
using System.ComponentModel;
public sealed class BeforeSaveEventArgs : CancelEventArgs
{
public string? Reason { get; set; }
public string Payload { get; }
public BeforeSaveEventArgs(string payload) => Payload = payload;
}
public class Repository
{
public event EventHandler<BeforeSaveEventArgs>? BeforeSave;
public event EventHandler<EventArgs>? Saved;
public void Save(string payload)
{
var before = new BeforeSaveEventArgs(payload);
OnBeforeSave(before);
if (before.Cancel)
{
Console.WriteLine($"Save canceled: {before.Reason}");
return;
}
// ... 실제 저장
Saved?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnBeforeSave(BeforeSaveEventArgs e)
{
var handler = BeforeSave;
handler?.Invoke(this, e);
}
}
public class Validator
{
public void Attach(Repository repo)
{
repo.BeforeSave += (s, e) =>
{
if (string.IsNullOrWhiteSpace(e.Payload))
{
e.Cancel = true;
e.Reason = "Payload가 비어 있습니다.";
}
};
}
}
포인트: "Before" 이벤트에서만 취소를 허용하고, "Completed/Processed" 이벤트는 불변 데이터만 알립니다.
4. 설계 팁(체크리스트)
- 시그니처: 가능하면 EventHandler 또는 EventHandler<TEventArgs>를 사용합니다(표준화, 도구 지원).
- 이름: Xxx 발생 시 과거 시제(Processed, Completed), 사전 훅은 BeforeXxx로 명확히 합니다.
- 불변성: EventArgs 파생형은 get 전용 속성으로 설계합니다. 단, 취소 등 의도된 양방향 필드만 set 허용합니다.
- 호출: handler?.Invoke(this, e) 또는 로컬 복사 후 호출로 스레드 안전성을 확보합니다.
- 빈 데이터: 데이터가 없으면 EventArgs.Empty를 사용해 할당을 줄입니다.
- 예외: 발행자는 일반적으로 구독자 예외를 삼키지 않습니다. 필요 시 try/catch로 로깅 후 재던지기를 고려합니다.
- 수명: 구독 해제( -= )를 확실히 하여 메모리 누수를 방지합니다(특히 장수(longevity) 오브젝트 구독 시).
'C#' 카테고리의 다른 글
| C# System.Buffers.ArrayPool<T>로 메모리 재활용하기 (0) | 2026.05.26 |
|---|---|
| C# 비동기 스트림(IAsyncEnumerable) 처리와 응용 (0) | 2026.05.26 |
| C# ThreadLocal<T>로 스레드별 데이터 관리 (0) | 2026.05.26 |
| C# 암시적/명시적 변환 연산자(implicit/explicit) 구현하기 (0) | 2026.05.25 |
| C# 구조적 비교와 EqualityComparer<T> 커스터마이징 (0) | 2026.05.25 |