포스트

C# 이벤트 트리거와 핸들링

코드 전체로 이벤트를 브로드캐스팅하는 자바스크립트와 달리, C#은 이벤트 트리거와 핸들링에 관찰자 모델을 채택했습니다. 이벤트를 브로드캐스팅하는 방법은 꽤 직관적이고 편리하지만, 전역 변수가 그러하듯 몇몇 사이드 이펙트가 발생하기도 합니다.

C#, 정확히는 닷넷의 이벤트 처리는 이와 비교해 조금 복잡하고 어려울 수 있지만, 오히려 엄격한 규칙과 조건에 따라 동작하므로 코드 동작 시나리오가 더욱 정제되고 관리가 편해질 수 있으리라 생각합니다.

이벤트 발신 측

C#에서 이벤트는 EventHandler 를 사용합니다. 자바스크립트에서 이벤트 처리를 해보았다면 혼동이 올 수 있는데, 웹에서 “이벤트 핸들러”라는 표현은 일반적으로 이벤트를 수신하는 측에 사용하기 때문입니다.

1
2
public event EventHandler CustomEvent;
CustomEvent?.Invoke(null, EventArgs.Empty);

실제로 위의 예문과 같이 사용하게 되는데, EventHandlerInvoke 를 사용하여 이벤트를 발생시킵니다.

이벤트 수신 측

하지만 그렇다고 하더라도 이벤트 수신 측을 이벤트 핸들러라고 표현하지 않는 것은 아닙니다.

1
2
3
4
5
6
7
8
9
10
public static void Main()
{
    CustomEvent += _EventHandler;
    CustomEvent?.Invoke(null, EventArgs.Empty);
}

static void _EventHandler(object sender, EventArgs e)
{
    Console.WriteLine($"CustomEvent Invoked: by {sender}");
}

이벤트 핸들러는 여전히 이벤트 핸들러입니다. 이러한 혼동은 EventHandler 가 대리자로 정의되기 때문에 발생합니다.

EventHandlerpublic delegate void EventHandler(object? sender, EventArgs e) 로 선언된 대리자이기 때문에 이벤트 핸들러의 매개 변수는 (object?, EventArgs) 로 강제됩니다.

정리하자면 EventHandler 대리자에 1. (object?, EventArgs) 를 매개 변수로 받는 이벤트 핸들러 함수를 구독시켜 2. EventHandler에서 이벤트를 발생시키면 3. 이벤트 핸들러 함수가 이 이벤트를 수신하는 형식으로 동작한다고 정리할 수 있겠습니다.

이벤트 수신과 발신

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using System;

public class Program
{
    public static void Main()
    {
        SimpleEventClass simpleEventObject = new SimpleEventClass();
        // 이벤트 구독
        simpleEventObject.CustomEvent += onCustomEvent;
        // 이벤트 발생 Wrapper 함수 호출
        simpleEventObject.InvokeEvent();
        // 이벤트 구독 해제
        simpleEventObject.CustomEvent -= onCustomEvent;
    }

    static void onCustomEvent(object sender, EventArgs e)
    {
        Console.WriteLine($"CustomEvent Invoked: by {sender}");
    }
}

public class SimpleEventClass
{
    public event EventHandler CustomEvent;
    
    // 이벤트 발생 Wrapper
    public void InvokeEvent()
    {
        CustomEvent?.Invoke(this, EventArgs.Empty);
    }
}

출력

1
CustomEvent Invoked: by SimpleEventClass

이벤트에 매개 변수 전달하기

이벤트를 발생시키는 Invoke()EventArgs.Empty를 넣어서 보내고 있는데, EventArgs 는 이벤트 수신자에게 매개 변수를 전달하는 역할을 합니다. EventArgs.Empty는 빈 매개변수 구현체이므로 매개 변수를 전달하고자 한다면 매개 변수의 포맷부터 정의해야 합니다.

1
2
3
4
5
6
7
8
class CustomEventArgs : EventArgs
{
  public int x;
  public CustomEventArgs(int x = -1)
  {
    this.x = x;
  }
}

이벤트 핸들러는 EventArgs 객체를 매개 변수로 받으므로, 이벤트 핸들러가 매개 변수를 제대로 수신하려면 정의할 매개 변수 포맷도 EventArgs 를 상속해야 합니다.

이벤트 발신 측

1
2
3
4
5
6
7
8
9
10
public class SimpleEventClass
{
    public event EventHandler CustomEvent;
    
    // 이벤트 발생 Wrapper
    public void InvokeEvent()
    {
        CustomEvent?.Invoke(this, new CustomEventArgs());
    }
}

이벤트 수신 측

1
2
3
4
5
6
7
8
9
public class Program
{
    public static void Main() { ... }

    static void onCustomEvent(object sender, EventArgs e)
    {
        Console.WriteLine($"CustomEvent Invoked: by {sender}, value: {((CustomEventArgs)e).x}");
    }
}

이벤트 발신 측에서 생성하고 넘겨준 매개 변수인 CustomEventArgs는 수신 측 이벤트 핸들러에 EventArgs 로 박싱되어 전달됩니다. 따라서 형변환 연산자를 사용하여 ((CustomEventArgs)e).x, 언박싱 후 사용합니다.

add 접근자와 remove 접근자

EventHandler는 마치 클래스의 생성자, 소멸자와 같이 구독, 구독 해제 시의 동작을 정의할 수 있습니다. 주로 EventHandler를 숨기고, 외부에 구독, 구독 해제의 “통로”를 만들 때 사용한다는 점에서 마치 EventHandler의 getter, setter 같습니다.

이벤트 발신자 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ComplexAddRemoveEventClass
{
    // 이벤트 핸들러 은닉
    private EventHandler handler;
    
    // 외부에 노출
    public event EventHandler CustomEvent
    {
        add
        {
            Console.WriteLine("Called add accessor in ComplexAddRemoveEventClass.");
            this.handler += value;
        }
        remove
        {
            Console.WriteLine("Called remove accessor in ComplexAddRemoveEventClass.");
            this.handler -= value;
        }
    }

    public void InvokeEvent() { ... }
}

이벤트 수신/발신

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Program
{
    public static void Main()
    {
        ComplexAddRemoveEventClass complexObject = new ComplexAddRemoveEventClass();
        // 이벤트 핸들러 구독
        complexObject.CustomEvent += onCustomEvent;
        // 이벤트 발생
        complexObject.InvokeEvent();
        complexObject.CustomEvent -= onCustomEvent;
    }

    // 이벤트 수신
    static void onCustomEvent(object sender, EventArgs e)
    {
        Console.WriteLine($"CustomEvent Invoked: by {sender}, value: {((CustomEventArgs)e).x}");
    }
}

출력

1
2
3
Called add accessor in ComplexAddRemoveEventClass.
CustomEvent Invoked: by ComplexAddRemoveEventClass, value: -1
Called remove accessor in ComplexAddRemoveEventClass.