이벤트와 콜백은 모두 "무언가가 일어났을 때 실행할 코드"를 연결하는 방법이지만 쓰임새와 결합도가 다릅니다. 이 글은 실무에서 어떤 기준으로 선택하면 되는지 간략히 정리합니다.
1. 개념 요약
이벤트: 발행자(Publisher)가 다수의 구독자(Subscriber)에게 상태 변화를 통지하는 .NET 표준 메커니즘입니다. UI, 도메인 이벤트, Pub/Sub에 적합합니다.
콜백: 호출자가 함수(델리게이트)를 인자로 전달해 실행 흐름의 일부를 주입합니다. 1:1 맞춤 동작, 전략 주입, 훅 포인트에 적합합니다.
2. 언제 이벤트를 쓰나요?
여러 구독자가 필요한 방송형 통지, .NET 관례(EventHandler, EventArgs)에 맞춘 공개 API, 수명 주기 전반에 걸친 상태 변경 알림, UI/컴포넌트 간 느슨한 결합이 필요할 때 적합합니다.
3. 언제 콜백을 쓰나요?
특정 호출에 한정된 1회성 훅, 전략/정책을 주입해야 할 때, 테스트에서 동작을 쉽게 대체하고 싶을 때, 동기/비동기 파이프라인에서 세밀한 흐름 제어가 필요할 때 적합합니다.
4. 이벤트 예시: 발행-구독
public sealed class OrderCreatedEventArgs : EventArgs
{
public OrderCreatedEventArgs(Guid orderId) => OrderId = orderId;
public Guid OrderId { get; }
}
public sealed class OrderService
{
public event EventHandler<OrderCreatedEventArgs>? OrderCreated;
public void Create(Order order)
{
// 비즈니스 로직...
OnOrderCreated(new OrderCreatedEventArgs(order.Id));
}
private void OnOrderCreated(OrderCreatedEventArgs e)
=> OrderCreated?.Invoke(this, e);
}
// 사용 측
var svc = new OrderService();
EventHandler<OrderCreatedEventArgs> handler = (s, e) =>
{
Console.WriteLine($"Order created: {e.OrderId}");
};
svc.OrderCreated += handler;
svc.Create(new Order(Guid.NewGuid()));
// 더 이상 필요 없으면 구독 해제(메모리 누수 방지)
svc.OrderCreated -= handler;
포인트: 이벤트는 발행자가 호출 시점을 완전히 제어하며, 구독자는 여러 명일 수 있습니다. 장수 객체에 구독할 때는 반드시 해제하거나 약한 이벤트(예: WPF WeakEventManager)를 고려합니다.
5. 콜백 예시: 호출 단위로 전략 주입
public sealed class OrderResult
{
public OrderResult(Guid id) => Id = id;
public Guid Id { get; }
}
public sealed class OrderService
{
public void Create(
Order order,
Action<OrderResult>? onCompleted = null,
Action<Exception>? onError = null)
{
try
{
// 비즈니스 로직...
var result = new OrderResult(order.Id);
onCompleted?.Invoke(result);
}
catch (Exception ex)
{
onError?.Invoke(ex);
}
}
}
// 사용 측: 호출마다 다른 동작을 주입
var svc = new OrderService();
svc.Create(
new Order(Guid.NewGuid()),
onCompleted: r => Console.WriteLine($"Done: {r.Id}"),
onError: ex => Console.Error.WriteLine(ex.Message));
포인트: 콜백은 호출자가 실행 시점과 내용을 더 세밀하게 주도합니다. 기본적으로 1:1이며 동작을 쉽게 테스트 대체(Mock)할 수 있습니다.
6. 비동기에서의 선택
값을 반환하거나 실패/취소를 전파해야 하는 비동기 작업은 Task/async 사용을 우선합니다. 이벤트는 "상태가 바뀌었음을 알림"에 쓰고, 결과/에러는 Task로 처리하는 구성이 명확합니다.
public sealed class OrderService
{
public event EventHandler<OrderCreatedEventArgs>? OrderCreated;
public async Task<OrderResult> CreateAsync(Order order, CancellationToken ct = default)
{
// await I/O...
await Task.Delay(10, ct);
var result = new OrderResult(order.Id);
OrderCreated?.Invoke(this, new OrderCreatedEventArgs(order.Id));
return result;
}
}
// 사용 측: await로 결과/에러/취소를 자연스럽게 처리
var result = await svc.CreateAsync(order, ct);
Console.WriteLine(result.Id);
팁: 진행률은 IProgress<T> 또는 Progress<T>로, 완료 통지는 Task 반환으로, 상태 브로드캐스트는 이벤트로 분리하면 깔끔합니다.
7. 설계 관점 비교
결합도: 이벤트는 발행자와 구독자가 느슨하게 결합됩니다. 콜백은 호출 지점과 전달된 함수가 강하게 연결됩니다.
수신자 수: 이벤트는 다수, 콜백은 보통 단일입니다.
수명 관리: 이벤트는 구독 해제가 중요합니다. 콜백은 호출이 끝나면 관계가 종료됩니다.
테스트: 콜백은 테스트 더블 주입이 쉽습니다. 이벤트는 테스트에서 구독 후 신호 대기/해제가 필요합니다.
스레드: 이벤트 발생 스레드는 호출 스레드와 동일합니다. UI 스레드 마샬링이 필요하면 SynchronizationContext를 고려합니다. 콜백도 동일 원칙이 적용됩니다.
8. 선택 기준 체크리스트
여러 곳에 동일 통지가 필요합니까? 이벤트가 적합합니다.
호출마다 다른 전략/후처리가 필요합니까? 콜백이 적합합니다.
공개 API에서 .NET 관례를 따를까요? 이벤트를 선호합니다.
비동기 결과/에러/취소 전파가 핵심인가요? Task/async를 우선하고, 필요 시 이벤트로 추가 통지합니다.
장수 발행자-단명 구독자 조합인가요? 이벤트 사용 시 구독 해제 또는 약한 이벤트를 고려합니다.
9. 마이그레이션 팁
콜백 기반 API를 이벤트로 노출하려면 내부에서 콜백을 이벤트 발생으로 연결하는 어댑터를 둡니다. 이벤트 기반을 콜백 스타일로 감싸려면 메서드 내에서 일시 구독하고 즉시 해제하는 패턴을 사용합니다.
10. 결론
브로드캐스트 통지에는 이벤트, 호출 단위 전략 주입에는 콜백, 비동기 결과는 Task/async가 기본 선택입니다. 이 세 가지를 역할별로 분리하면 코드가 단순하고 테스트 가능하며 누수 없이 유지됩니다.
'C#' 카테고리의 다른 글
| C# 인터페이스 다중 상속 시 충돌 해결 전략 (0) | 2026.06.30 |
|---|---|
| C# Reflection으로 Private 멤버 접근 및 테스트 활용 (0) | 2026.06.30 |
| C# sealed 키워드로 상속 제한하기 (0) | 2026.06.26 |
| C# 인스턴스 멤버와 정적 멤버의 메모리 차이 (0) | 2026.06.25 |
| C# 객체 초기화(Object Initializer)와 컬렉션 초기화 비교 (0) | 2026.06.25 |