2025. 2. 20. 00:16ㆍ개발/C#
이 글은 박상현님의 '이것이 C#이다 개정판'을 참고하여 공부한 내용입니다.
C#을 공부하며 처음 이벤트를 공부했는데 잘 이해가 되지 않았습니다. 대리자와 비슷하지만 더 제한적이어서 사용하기에 더 불편하다는 느낌이 강했습니다. 그런데 C#을 공부하면서 다양한 안정성 기능들을 알게되니, 제가 안정성의 중요함을 잘 모르고 있었다는 것을 깨달았습니다.
이벤트
event 키워드를 선언할 수 있으며, 이벤트는 대리자를 캡슐화하여 더 높은 안정성과 객체 간의 결화도를 낮추는 기능을 제공합니다. 이벤트 시스템의 구성 요소는 이벤트를 소유하고 있는 발행자와 이벤트를 처리할 가입자로 나누어 집니다. 사용방법은 아래와 같습니다.
public delegate void EventHandler();
우선 이벤트가 기반으로 사용할 대리자를 선언합니다. 이때 이벤트 핸들러 즉 가입자 클래스의 발생시킬 메서드의 형식을 정의한 대리자를 선언해야 합니다. 위 예시에서는 인자와 반환값이 없는 Action 형식의 대리자를 선언하였습니다.
public class Publisher
{
public event EventHandler OnEventRaised;
public void RaiseEvent()
{
if (OnEventRaised != null)
OnEventRaised();
}
}
그리고 이벤트를 소유할 발행자 클래스를 정의 합니다. 이벤트의 호출은 직접적으로는 불가능하며 RaiseEvent 메서드를 통해서만 호출이 가능합니다. 이벤트는 public으로 설정했다 하여도 외부에서 직접 호출은 불가는 하며 증감 복합 대입 연사자를 통해 구독과 해제가 가능합니다.
public class Subscriber
{
public void HandleEvent()
{
Console.WriteLine("Event Raise!");
}
}
마지막으로 이벤트가 발생하면 실행될 핸들러를 가진 가입자 클래스를 정의합니다. 이제 이벤트를 발생시킬 준비가 끝났습니다. 이제 이벤트를 구독하고 발생시켜 보겠습니다.
class Program
{
static void Main()
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
publisher.OnEventRaised += subscriber.HandleEvent;
publisher.RaiseEvent(); // print: Event Raise!
}
}
위와 같이 객체를 생성 후 복합 대입 연산자를 통해 이벤트에 발생시킬 핸들러를 등록합니다. 그 후 특정 조건이 만족하면 발행자 클래스의 이벤트 호출 메서드를 사용하여 이벤트를 발생 시킵니다. 이렇게만 보면 대리자와 다른점이 없어 보입니다. 하지만 직접호출할 경우 차이점을 확인할 수 있습니다.
publisher.OnEventRaised();
CS0070 The event 'publisher.OnEventRaised' can only appear on the left hand side of += or -= (except when used from within the type 'Publisher')
위 예시를 실행하면 직접 호출을 방지하기 위한 컴파일러 오류가 발생합니다. 즉 이벤트를 외부에서 강제로 발생시키는 것이 불가능합니다, 그래서 이벤트는 이런 장점을 사용하여 유연한 이벤트 시스템을 설계할 수 있게 합니다.
EventHandler<T>, EventArgs
이벤트를 실제로 사용할 때에는 이벤트를 더욱 간편하게 사용할 수 있는 일반화된 대리자인 EventHandler<T>를 사용합니다. EventHandler의 기본 구조는 아래와 같습니다.
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
위 예시의 TEventArgs는 이벤트 핸들러에 데이터를 전달하는 이벤트 인자 클래스 입니다. EventArgs 클래스이거나 파생 클래스가 사용될 수 있습니다. sender는 이벤트를 발생시킨 객체 즉 publisher를 뜻합니다. EventArgs의 기본 구조는 아래와 같습니다.
public class EventArgs
{
public static readonly EventArgs Empty;
}
기본적으로 비어있는 필드를 하나 가지고 있습니다. 이 클래스를 상속받아 멤버를 정의하면 이벤트에 전달될 데이터를 관리할 수 있습니다. 이 Empty 필드는 데이터가 필요없는 이벤트 핸들러에 사용됩니다.
이벤트 핸들러를 사용하는 이유는 여러가지가 있습니다. 표준화된 이벤트 패턴을 제공하며, EventArgs 클래스를 사용하여 쉽게 인자를 확장 가능합니다. 그리고 대리자를 직접 정의하지 않기에 코드를 더욱 단순화하여 작성할 수 있습니다.
사용방법은 아래와 같습니다.
public class MyEventArgs : EventArgs
{
public string Message { get; }
public MyEventArgs(string message) => Message = message;
}
우선 EventHandler에 들어갈 데이터 형식 클래스를 정의합니다. EventArgs를 상속받은 클래스를 선언하여 문자열 프로퍼티 한개를 정의합니다. 그리고 생성자를 추가하여 데이터 전달과 이벤트 발생을 한번에 할 수 있게 하려고 합니다.
public class Publisher
{
public event EventHandler<MyEventArgs> OnEventRaised;
public void RaiseEvent(string message)
{
OnEventRaised?.Invoke(this, new MyEventArgs(message));
}
}
미리 정의해 놓은 EventArgs 파생 클래스 형식을 사용하여 이벤트 핸들러를 정의합니다. 이벤트 발생 메서드에는 이벤트 핸들러를 통해 이벤트를 발생시키는데, 여기서 ?. Null 조건부 연산자는 앞 객체가 Null인지 확인하여 Null이 아닐경우 뒤에 있는 메서드를 실행하는 연산자 입니다. 그리고 Invoke 메서드를 통해 이벤트를 호출합니다. 앞서 위에서 본 이벤트 핸들러의 기본 구조를 참조해서 사용합니다.
public class Subscriber
{
public void HandleEvent(object sender, MyEventArgs e)
{
Console.WriteLine($"Event Raise: {e.Message}");
}
}
이제 이벤트가 발생하면 처리될 클래스를 정의합니다. 특이한 점은 메서드의 구조입니다. 눈치채셨겠지만 앞서 정의한 이벤트 핸들러와 동일한 구조를 가지고 있습니다. 그래서 전달된 EventArgs 객체를 사용하여 본문에서 데이터를 처리 할 수 있습니다.
class Program
{
static void Main()
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
publisher.OnEventRaised += subscriber.HandleEvent;
publisher.RaiseEvent("Hello, Event!"); // print: "Event Raise: Hello, Event!"
}
}
마지막으로 핸들러를 구독하고 이벤트를 발생시켜 예상대로 동작되는지 확인합니다.
'개발 > C#' 카테고리의 다른 글
C# 리플렉션 (0) | 2025.02.23 |
---|---|
C# LINQ (0) | 2025.02.21 |
C# 대리자, 익명 메서드 그리고 람다식 (0) | 2025.02.18 |
C# 추상 그리고 인터페이스 (2) | 2025.02.14 |
C# 클래스 (0) | 2025.02.13 |