포스트

[delegate] 02. event와 EventArgs (EventHandler)

eventdelegate의 사용 패턴을 좀 더 일반화 해서 제공하는 예약어입니다.
예를 들어, 콜백 패턴 혹은 이벤트 패턴이나 옵저버 패턴 등을 구현할 때, 구현하기 쉽도록 다양한 기능을 제공합니다.

제약 조건과 제공 기능

event는 다음과 같은 제약 조건과 기능을 제공합니다.

  1. 캡슐화
    • 이벤트를 통해 구독자와 발생자 간의 상호작용이 캡슐화되어, 객체 간의 느슨한 결합을 유지할 수 있습니다. 이벤트를 발생시키는 객체는 구독자가 누구인지 알 필요가 없고, 구독자는 이벤트를 발생시키는 방법을 알 필요가 없습니다.
    • 이벤트는 클래스 내부에서 선언됨으로써 응집력을 높이고, 클래스 외부에서는 이벤트를 발생시킬 수 없도록 캡슐화합니다.
  2. 외부 호출 불가
    • 이벤트는 이벤트가 정의된 클래스 외부에서 직접 호출할 수 없습니다. 이는 이벤트를 발생시킬 수 있는 권한이 클래스 내부에만 있음을 보장합니다.
  3. 델리게이트 직접 할당 불가
    • 이벤트는 할당 연산자(=)를 통해 델리게이트를 직접 할당하거나 대입할 수 없습니다. 이는 기존에 등록된 메서드 리스트를 안전하게 관리할 수 있도록 보장합니다.
  4. 구독자 관리
    • 이벤트는 +=-= 연산자를 통해서만 구독자를 추가하거나 제거할 수 있습니다.
    • 이벤트가 발생하면 모든 구독자에게 알림을 보냅니다.

즉, event는 다음과 같은 특징을 가진 패턴을 구현할 때, event 예약어를 사용하면 빠르고 쉽게 패턴을 구현하며, 코드를 줄일 수 있습니다.

  1. 클래스에서 이벤트(콜백)을 제공한다.
  2. 클래스 외부에서 이벤트(콜백)을 구독하거나 해지하게 하고 싶다.
  3. 이벤트 발생은 오직 클래스 내부에서만 가능하게 하고 싶다.
  4. 이벤트가 발생하면 구독자에게 메시지를 보내고 싶다.

event 정의

이벤트는 다음과 같은 형태로 정의할 수 있습니다.

1
2
3
4
class 클래스_내부에서
{
	접근제한자 event 델리게이트 이벤트명;
}

만약, 특별한 델리게이트 없이 사용하고 싶다면 다음과 같이 EventHandler를 사용할 수 있지만, 기본적으로 이 핸들러에는 매개변수가 존재합니다.

  • object sender: sender 매개변수는 이벤트를 발생시킨 객체에 대한 참조를 전달합니다. 이를 통해 구독자들은 어떤 객체에서 발생한 이벤트인지를 구분할 수 있습니다.
  • EventArgs e: e 매개변수는 이벤트와 관련된 추가 정보를 전달합니다. 기본적으로 EventArgs 타입을 사용하며, 추가 데이터가 필요할 때는 이를 상속하여 커스텀 이벤트 아규먼트를 만들 수 있습니다.
1
2
3
4
class 클래스명
{
	public event EventHandler 이벤트명;
}

이벤트 구독 및 구독 취소

이벤트를 구독(subscribe)하거나 구독을 취소(unsubscribe)하는 방법은 +=-= 연산자를 사용하면 됩니다.

1
2
3
4
5
6
7
8
9
public void Subscribe(MyClass obj)
{
	obj.MyEvent += HandleEvent;
}

public void Unsubscribe(MyClass obj)
{
	obj.MyEvent -= HandleEvent;
}

델리게이트 대신 이벤트를 사용하는 경우

event를 사용하면 delegate를 사용하는 다양한 디자인 패턴을 더 안전하고 쉽게 구현할 수 있습니다.
예를 들어, 이전에 사용했던 PlayerAction 버튼시 행동에 대한 코드를 작성해 봅니다.

1
2
3
4
5
6
public delegate bool ActionBtnDelegate();

internal class Player
{
    public ActionBtnDelegate actionBtnDelegates;
}

delegate를 선언하고, 이를 이용해 Player 내에서 액션 버튼이 눌렸을 때, 동작을 저장할 actionBtnDelegates를 만듭니다.
하지만, 외부에서도 델리게이트를 등록하고 제거 할 수 있어야 하니 접근 제한자를 public으로 만들게 되었습니다.

예상하실 수 있겠지만, 이렇게 코드를 작성하면 나중에 예기치 못한 버그나 문제가 발생할 수 있습니다.

예를 들어, 어떤 행동을 할 때마다 스테미나가 달고, 문을 여는 행동을 저장하고자 합니다.

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
internal class Stamina
{
    public bool UseStamina()
    {
        Console.WriteLine("스태미너를 사용합니다.");
        return true;
    }
}
internal class Door
{
    public bool Open()
    {
        Console.WriteLine("문을 엽니다.");
        return true;
    }
}
internal class Program
{
	static void Main(string[] args)
	{
		Player player = new Player();
		Stamina stamina = new Stamina();
		player.actionBtnDelegates = stamina.UseStamina;
		
		Door door = new Door();
		player.actionBtnDelegates = door.Open;
		
		player.actionBtnDelegates.Invoke(); // 문을 엽니다.
	}
}

그럼, 어딘가에서 플레이어의 스테미나를 관리하는 델리게이트를 설정했을테고, 또 다른 어딘가에서 문을 여는 동작을 델리게이트에 할당합니다.
이때, 추가하는 += 연산자가 아니라 대입 연산자(=)를 사용해버리면, 기존에 등록되어 있던 동작들이 전부 초기화 됩니다.
따라서, 스테미너 소모 없이 동작을 행할 수 있는 버그가 발생했습니다.
또한, public으로 공개되어 있기 때문에 아무데서나 해당 델리게이트를 호출할 수 있기 때문에, 의도치 않은 동작이 발생할 수도 있습니다.

이를 해결하기 위해, 캡슐화를 하고 은닉성을 부여해 봅시다.

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
33
34
35
public delegate bool ActionBtnDelegate();

internal class Player
{
	private ActionBtnDelegate actionBtnDelegates;

	public void AddAction(ActionBtnDelegate actionBtnDelegate)
	{
		actionBtnDelegates += actionBtnDelegate;
	}
	public void RemoveAction(ActionBtnDelegate actionBtnDelegate)
	{
		actionBtnDelegates -= actionBtnDelegate;
	}
	public void OnAction()
	{
		actionBtnDelegates?.Invoke();
	}
}
// ...
{
	Player player = new Player();
	Stamina stamina = new Stamina();
	player.AddAction(stamina.UseStamina);
	
	Door door = new Door();
	player.AddAction(door.Open);
	player.OnAction();
	// 스태미너를 사용합니다.
	// 문을 엽니다.
	
	player.RemoveAction(door.Open);
	player.OnAction();
	// 스태미너를 사용합니다.
}

Player의 델리게이트를 private로 보호하고, 델리게이트를 추가하거나 제거하는 메서드를 제공했습니다.
OnAction() 메서드의 경우에는 예제로 사용하기 위해 public으로 공개해뒀지만, 필요에 따라 private를 할 수도 있습니다.
이렇게, 캡슐화와 은닉성 제공을 통해 기존 코드보다는 안정적인 코드를 만들었습니다.

이때, 이 코드에 event를 사용한다면 이 코드를 줄일 수 있게 됩니다.

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
public delegate bool ActionBtnDelegate();

internal class PlayerWEvent
{
    public event ActionBtnDelegate actionBtnDelegates;

    public void OnAction()
    {
        actionBtnDelegates?.Invoke();
    }
}
// ...
{
	PlayerWEvent playerWEvent = new PlayerWEvent();
	Stamina stamina = new Stamina();
	playerWEvent.actionBtnDelegates += stamina.UseStamina;
	
	Door door = new Door();
	playerWEvent.actionBtnDelegates += door.Open;
	playerWEvent.Action();
	// 스태미너를 사용합니다.
	// 문을 엽니다.
	
	playerWEvent.actionBtnDelegates -= door.Open;
	playerWEvent.Action();
	// 스태미너를 사용합니다.
}

eventpublic으로 공개했음에도 제약사항으로 인해 외부에서 이벤트를 호출 할 수 없고, 대입 연산자를 사용할 수 없기 때문에, 제공해야 할 메서드의 양이 줄었고, 결과적으로 코드가 줄어든 것을 볼 수 있습니다.

즉, 특정 패턴을 구현하는데 있어, 제약사항과 제공 기능을 통해 좀 더 일반화 된 패턴을 구현할 수 있습니다.


다만, 가끔 event가 좀 더 안전하고 편한 것 같은데 delegate를 쓸 이유가 있을까? 하는 의문이 들수도 있지만, delegate는 좀 더 자유로운 코딩을 가능하게 합니다.

delegate는 특정 메서드 시그니처를 참조할 수 있는 형식을 제공함으로써, 매개변수로 전달하거나 리턴 타입으로 반환 할 수도 있습니다.

이때, EventHandlerevent만으로도 코드를 구성할 수 있다고 하지만, 가끔은 폭 넓은 타입을 제공하는 형식 보다, 제한된 형태의 형식을 제공함으로써 타입 안전한 방법으로 사용하는게 좋은 경우도 있습니다. (우리가 클래스를 캡슐화하고 은닉성을 부여하는 것처럼…)

EventArgs

위에서 잠깐 언급하고 넘어갔던 EventArgs는 C#에서 이벤트 핸들러에 전달되는 데이터의 기본 형식을 정의하는 클래스입니다.

기본적으로 EventArgs는 데이터 없이 이벤트를 전달할 때 사용되는 빈 클래스지만, 이벤트 핸들러가 추가적인 정보를 필요로 할 때는 이 클래스를 상속하여 커스텀 이벤트 아규먼트 클래스를 만들 수 있습니다.

이런 커스텀 EventArgs를 사용하여 이벤트가 발생할 때 추가적인 데이터를 전달할 수 있습니다.

이때, 특정 커스텀 EventArgs로 이벤트 핸들러를 지정할 수 있는 제네릭 델리게이트를 제공합니다.

1
public event EventHandler<MyEventArgs> MyEvent;

EventHandler<TEventArgs>은 제네릭 델리게이트로, TEventArgs 타입의 추가 데이터를 전달할 수 있습니다. 이때, TEventArgsEventArgs를 상속해야 합니다.

1
2
3
4
internal class MyEventArgs : EventArgs
{
	//...
}
예제

비록 좋은 예제를 만들지는 못했지만, 대략 이런식으로 EventArgs를 이용할 수 있다는 것만 봐주시면 좋을 것 같습니다.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 커스텀 EventArgs
internal class MyEventArgs : EventArgs
{
    public MyEventArgs(int n)
    {
        RemainStamina = n;
    }
    public int RemainStamina { get; set; }
}

internal class PlayerWEvent
{
    public event EventHandler<MyEventArgs> actionBtnDelegates;
	// 스테미나를 Player가 가지고 있도록 바꿈
    private StaminaWArgs stamina;

    public PlayerWEvent()
    {
        stamina = new StaminaWArgs();
        actionBtnDelegates += stamina.UseStamina;
    }

    public void Action()
    {
	    // 구독자에게 이벤트를 전달할 때, MyEventArgs를 넘겨줌
        actionBtnDelegates?.Invoke(this, new MyEventArgs(stamina.NowStamina));
    }
}
internal class StaminaWArgs
{
    public int NowStamina { get; set; } = 10;
    // 구독하기 위해서 EventHandler의 형태에 맞춰 (sender, e)를 매개변수로 받아 줌
    public void UseStamina(object? sender, MyEventArgs e)
    {
        NowStamina -= 10;
        Console.WriteLine("스태미너를 사용합니다.");
    }
}
internal class DoorWArgs
{
	// 구독하기 위해서 EventHandler의 형태에 맞춰 (sender, e)를 매개변수로 받아 줌
    public void Open(object? sender, MyEventArgs e)
    {
        if (e.RemainStamina < 10)
        {
            Console.WriteLine("스태미너가 부족합니다.");
            return;
        }
        Console.WriteLine("문을 엽니다.");
        return;
    }
}
// ...
static void Main(string[] args)
{
	PlayerWEvent playerWEvent = new PlayerWEvent();

	DoorWArgs door = new DoorWArgs();
	playerWEvent.actionBtnDelegates += door.Open;
	
	playerWEvent.Action();
	// 스태미너를 사용합니다.
	// 문을 엽니다.
	    
	playerWEvent.Action();
	// 스태미너를 사용합니다.
	// 스태미너가 부족합니다.
}

위 예제는 기존 예제에서 SteminaPlayer가 가지고 있도록 하고, 어떤 행동을 할 때마다 남은 스테미너를 커스텀 EventArgs에 담아서 보낼 수 있도록 구조를 변경한 것입니다.
이벤트를 호출할 때, 인자로 이벤트를 발생한 객체와 남은 스테미너를 담고있는 EventArgs를 함께 전달했습니다.
따라서, playerWEvent.Action();을 호출할 때, MyEventArgs에 담긴 스테미너 정보를 가지고 행동 여부를 판단할 수 있습니다.

정리

event는 델리게이트로 구현되는 다양한 패턴을 더 안전하고 쉽게 사용할 수 있도록 돕습니다.
이를 통해 느슨한 결합을 유지하면서 객체 간의 상호작용을 쉽게 구현할 수 있습니다.
이벤트는 외부에서 직접 호출될 수 없으며, 구독자는 이벤트를 통해 알림을 받을 수 있습니다.

델리게이트 대신 EventHandler를 통해 일반화된 호출을 사용할 수 있는데, EventHandler는 시그니처를 일관되게 유지하고 있으며, 매개변수로는 (object sender, EventArgs e)를 가지고 있습니다.
이는 이벤트 핸들러의 호출을 일관되게 할 수 있으며, 필요시 EventArgs를 상속하고 확장하여 다양한 이벤트에서 필요한 데이터를 쉽게 전달할 수 있습니다.


참고

[delegate] 01.델리게이트의 정의와 정체, 체이닝, 사용 목적

[delegate] 03. 익명 델리게이트와 Func, Action

이 기사는 저작권자의 CC BY-NC-ND 4.0 라이센스를 따릅니다.