C#에서 이벤트 구조의 심층 분석

이벤트는 C# 언어에서 대리자(Delegate) 기반의 강력한 메커니즘으로, 객체 간 느슨한 결합(loose coupling)을 가능하게 합니다. 이 문서에서는 이벤트의 완전한 선언 방식부터 그 본질, 필요성, 네이밍 규칙까지 심도 있게 다룹니다.

이벤트의 구성 요소

이벤트 기반 프로그래밍 모델은 다섯 가지 핵심 요소로 구성됩니다:

  1. 이벤트 소유자: 이벤트를 발생시키는 주체 (예: 고객)
  2. 이벤트 자체: 발생하는 동작 (예: 주문하기)
  3. 이벤트 응답자: 이벤트에 반응하는 객체 (예: 웨이터)
  4. 이벤트 핸들러: 이벤트 발생 시 실행되는 로직
  5. 이벤트 구독: 응답자가 이벤트에 등록하는 과정

이벤트의 완전한 선언

간단한 문법 축약형(event 키워드 사용)보다 먼저, 이벤트의 내부 작동 원리를 이해하기 위해 수동 방식의 완전한 선언을 살펴보겠습니다.

1. 이벤트 소유자 클래스 정의

public class Guest
{
    public double TotalBill { get; set; }

    public void Settle()
    {
        Console.WriteLine($"맛있었어요! ${TotalBill} 지불합니다.");
    }
}

2. 이벤트 데이터 전달용 클래스 작성

.NET 설계 가이드라인에 따라 이벤트 인수 클래스는 EventArgs를 상속하며 이름 끝에 EventArgs 접미사를 붙입니다.

public class MealOrderedEventArgs : EventArgs
{
    public string MenuItem { get; set; }
    public string Portion { get; set; }
}

3. 대리자 타입 선언

이벤트 처리기를 위한 대리자를 정의합니다. 관례상 이름 끝에 EventHandler를 사용합니다.

public delegate void OrderProcessingHandler(Guest customer, MealOrderedEventArgs args);

4. 대리자 필드 및 이벤트 선언

대리자 인스턴스를 저장할 비공개 필드와 이벤트 멤버를 정의합니다.

private OrderProcessingHandler _orderProcessor;

public event OrderProcessingHandler PlacedOrder
{
    add
    {
        _orderProcessor += value;
    }
    remove
    {
        _orderProcessor -= value;
    }
}

5. 이벤트 트리거 로직 구현

이벤트는 자동으로 발생하지 않으며, 특정 조건에서 명시적으로 호출되어야 합니다.

public void DecideOrder()
{
    for (int i = 0; i < 3; i++)
    {
        Console.WriteLine("주문 고민 중...");
        Thread.Sleep(800);
    }

    var e = new MealOrderedEventArgs
    {
        MenuItem = "초콜릿 케이크",
        Portion = "large"
    };

    // null 체크 후 대리자 호출
    _orderProcessor?.Invoke(this, e);
}

6. 이벤트 응답자 구현

public class Server
{
    public void HandleOrder(Guest client, MealOrderedEventArgs data)
    {
        Console.WriteLine($"{data.MenuItem} 주문받았습니다. 바로 준비하겠습니다.");

        decimal basePrice = 12m;
        switch (data.Portion)
        {
            case "large":
                basePrice *= 1.5m;
                break;
            case "small":
                basePrice *= 0.7m;
                break;
        }

        client.TotalBill = (double)basePrice;
        Console.WriteLine($"이 주문은 ${basePrice}입니다.");
    }
}

이벤트의 간소화된 선언

C#은 아래와 같은 간결한 문법을 제공합니다.

public event OrderProcessingHandler PlacedOrder;

컴파일러는 이를 자동으로 앞서 설명한 완전한 구조로 변환합니다. 숨겨진 대리자 필드는 `<>b__` 형식의 이름으로 생성되며, 리플렉션 도구로 확인할 수 있습니다.

이벤트가 필요한 이유

대리자 필드를 public으로 공개하면 다음과 같은 문제가 발생합니다.

// 위험한 예: 직접 대리자 호출 허용
public OrderProcessingHandler PlacedOrder; // event 키워드 없음

// 외부 코드에서 악의적 호출 가능
customer.PlacedOrder(customer, maliciousArgs); // 보안 취약점!

반면 event 키워드를 사용하면 외부에서 +=, -= 외의 접근이 차단되어 안정성이 보장됩니다.

이벤트의 본질

이벤트는 사실상 대리자 필드를 감싸는 래퍼(wrapper)입니다. 이 래퍼는 다음을 제외한 모든 접근을 차단합니다:

  • 이벤트 핸들러 추가 (+=)
  • 이벤트 핸들러 제거 (-=)

즉, 이벤트는 캡슐화 원칙을 강화하여 의도치 않은 호출로부터 객체를 보호합니다.

명명 규칙(Naming Conventions)

  • 대리자 이름: [이벤트이름]EventHandler (예: OrderEventHandler)
  • 이벤트 이름: 현재 또는 과거 형태의 동사 (예: PlacedOrder, OrderCompleted)
  • 트리거 메서드: On[이벤트이름] 패턴 사용 (예: OnPlacedOrder())

표준 EventHandler 활용

.NET은 일반적인 시나리오를 위해 EventHandler<TEventArgs> 제네릭 대리자를 제공합니다.

public event EventHandler<MealOrderedEventArgs> PlacedOrder;

이 방식은 커스텀 대리자 정의 없이도 유연한 이벤트 처리가 가능하게 합니다.

이벤트와 대리자의 관계

이벤트는 대리자 위에 구축된 추상화 계층입니다. 모든 이벤트는 내부적으로 하나 이상의 대리자 인스턴스를 사용하여 핸들러 목록을 관리합니다. 즉, 대리자는 기반 기술(infrastructure), 이벤트는 설계 패턴(design pattern)이라 할 수 있습니다.

태그: C# event handling Delegates .NET Events EventHandler

5월 22일 03:39에 게시됨