[C# 고급] 이벤트 처리기 작성하기

대입을 확정한 이후로 한동안 다른 프로젝트를 진행하느라 카페에 자주 못들렀습니다. 조만간 오픈 소스 프로젝트 몇 종류를 공개적으로 런칭하게 될 것 같습니다. 많은 관심 부탁드리면서 간단한 기술을 또 하나 알려드리고자 합니다. 이 강좌를 읽으시기 전에 이벤트를 선언하는 방법과 대리자의 특성에 대해서 숙지하시면 유용하실 것입니다.

이벤트는 흔히 구독 (Subscription)과 해지 (Cancellation)의 개념을 사용합니다. 이는 C#의 문법을 통해서 관찰하면 훨씬 쉽게 이해할 수 있습니다. 다음의 예를 살펴보도록 하지요.


public delegate void SampleCallback(object arg1, object arg2, object arg3);

public class Ex1
{
  public event SampleCallback Sample;

  public Ex1()
  {
      this.Sample += new SampleCallback(SampleTest);
      this.FireSampleEvent();

      this.Sample -= new SampleCallback(SampleTest);
  }

  protected void FireSampleEvent()
  {
      if(this.Sample != null)
          this.Sample(123, 456f, “7890”);
  }

  private void SampleTest(object arg1, object arg2, object arg3)
  {
      Console.Out.WriteLine(arg1);
      Console.Out.WriteLine(arg2);
      Console.Out.WriteLine(arg3);
  }

  [MTAThread()]
  public static void Main(string[] arguments)
  {
      Ex1 tester = new Ex1();
      Console.WriteLine(“Tester launched.”);
  }
}

위의 코드에서는 Sample이라는 이벤트에 SampleCallback 형식의 대리자를 구독하고 해지하는 예를 보여주었습니다. 아주 기본적인 이벤트 처리 방법을 보여주는 예제입니다. 하지만 지금 보여드릴 예제는 C# 문법을 깊숙히 살펴보지 않으신 분들께는 상당히 생소한 예제가 될 것입니다.


public delegate void SampleCallback(object arg1, object arg2, object arg3);

public class Ex1
{
  public event SampleCallback ExactSample;

  public event SampleCallback Sample
  {
      add
      {
          Console.Out.WriteLine(“Event Added.”);
          this.ExactSample += value;
      }
      remove
      {
          Console.Out.WriteLine(“Event Removed.”);
          this.ExactSample -= value;
      }
  }

  public Ex1()
  {
      this.Sample += new SampleCallback(SampleTest);
      this.FireSampleEvent();

      this.Sample -= new SampleCallback(SampleTest);
  }

  protected void FireSampleEvent()
  {
      if(this.Sample != null)
          this.Sample(123, 456f, “7890”);
  }

  private void SampleTest(object arg1, object arg2, object arg3)
  {
      Console.Out.WriteLine(arg1);
      Console.Out.WriteLine(arg2);
      Console.Out.WriteLine(arg3);
  }

  [MTAThread()]
  public static void Main(string[] arguments)
  {
      Ex1 tester = new Ex1();
      Console.WriteLine(“Tester launched.”);
  }
}

결과적으로는 위의 예제와 동일하지만 콘솔 윈도우에는 메시지가 몇 줄 더 기록될 것입니다. 기록되는 부분의 코드는 이벤트 선언을 확장한 곳에서 쓰여지는 코드인것을 알 수 있습니다. add 섹션에서는 이벤트를 등록하는 과정에 일어나는 일들이 기록되어있으며 remove 섹션에서는 반대로 이벤트를 해지하는 과정에서 일어나는 일들일 기록되어있음을 알 수 있습니다. 그리고 재미있는 점은, value라는 키워드가 프로퍼티에서처럼 여전히 유효하다는 점입니다. 그렇다면 value는 무슨 형식일까요?


public delegate void SampleCallback(object arg1, object arg2, object arg3);

public class Ex1
{
  public event SampleCallback ExactSample;

  public event SampleCallback Sample
  {
      add
      {
          Console.Out.WriteLine(“Event Added. Type: ” + value.ToString());
          this.ExactSample += value;
      }
      remove
      {
          Console.Out.WriteLine(“Event Removed. Type: ” + value.ToString());
          this.ExactSample -= value;
      }
  }

  public Ex1()
  {
      this.Sample += new SampleCallback(SampleTest);
      this.FireSampleEvent();

      this.Sample -= new SampleCallback(SampleTest);
  }

  protected void FireSampleEvent()
  {
      if(this.Sample != null)
          this.Sample(123, 456f, “7890”);
  }

  private void SampleTest(object arg1, object arg2, object arg3)
  {
      Console.Out.WriteLine(arg1);
      Console.Out.WriteLine(arg2);
      Console.Out.WriteLine(arg3);
  }

  [MTAThread()]
  public static void Main(string[] arguments)
  {
      Ex1 tester = new Ex1();
      Console.WriteLine(“Tester launched.”);
  }
}

눈치가 빠르신 분들은 value 키워드가 어떤 형식의 변수인지 금새 알아채셨을 것입니다. 컴파일해보면 나타나겠지만 SampleCallback 대리자임을 나타낼 것입니다. 그리고 value 변수가 대리자이기 때문에 대리자를 가지고 처리할 수 있는 일은 모두 가능합니다. 그래서 위의 예제에서는 전달된 대리자를 다른 이벤트에 += 연산자와 -= 연산자를 사용하여 이벤트 구독과 해지를 대신 처리하도록 코드를 변형하였습니다. 그리고 += 연산자와 -= 연산자는 위에서 나타난 그대로 반드시 new 키워드와 함께 쓰여야만 하는 것이 아닌 것도 알 수 있습니다.

위의 예제에서는 이벤트 구독과 해지를 단순히 다른 이벤트에게도 위임하는 방법을 보여주었습니다만, 이 정도 단계에까지 왔다면 이벤트 자체를 직접 관리할 수도 있지 않겠는가라는 생각을 하시는 분들도 계실 것입니다. 이러한 호기심을 해결해 드리기 위하여 또 하나의 예제 코드를 보여드립니다.


using System;
using System.Collections;

public delegate void MyDelegate1(int i);
public delegate void MyDelegate2(string s);
public delegate void MyDelegate3(int i, object o);
public delegate void MyDelegate4();

public class PropertyEventsSample
{
  private Hashtable eventTable = new Hashtable();

  public event MyDelegate1 Event1
  {
     add
     {
        eventTable[“Event1”] = (MyDelegate1)eventTable[“Event1”] + value;
     }
     remove
     {
        eventTable[“Event1”] = (MyDelegate1)eventTable[“Event1”] – value;
     }
  }

  public event MyDelegate1 Event2
  {
     add
     {
        eventTable[“Event2”] = (MyDelegate1)eventTable[“Event2”] + value;
     }
     remove
     {
        eventTable[“Event2”] = (MyDelegate1)eventTable[“Event2”] – value;
     }
  }

 public event MyDelegate2 Event3
  {
     add
     {
        eventTable[“Event3”] = (MyDelegate2)eventTable[“Event3”] + value;
     }
     remove
     {
        eventTable[“Event3”] = (MyDelegate2)eventTable[“Event3”] – value;
     }
  }

  public event MyDelegate3 Event4
  {
     add
     {
        eventTable[“Event4”] = (MyDelegate3)eventTable[“Event4”] + value;
     }
     remove
     {
        eventTable[“Event4”] = (MyDelegate3)eventTable[“Event4”] – value;
     }
  }

  public event MyDelegate3 Event5
  {
     add
     {
        eventTable[“Event5”] = (MyDelegate3)eventTable[“Event5”] + value;
     }
     remove
     {
        eventTable[“Event5”] = (MyDelegate3)eventTable[“Event5”] – value;
     }
  }

  public event MyDelegate4 Event6
  {
     add
     {
        eventTable[“Event6”] = (MyDelegate4)eventTable[“Event6”] + value;
     }
     remove
     {
        eventTable[“Event6”] = (MyDelegate4)eventTable[“Event6”] – value;
     }
  }
}

public class MyClass
{
  public static void Main()
  {
  }
}

위의 예제에서는 이벤트를 해시 테이블을 통하여 관리할 수 있도록 통제합니다. 또한, += 연산자와 -= 연산자의 의미를 정확하게 보여주고 있습니다. 사실, 막연하게 생각하고 있던 += 연산자와 -= 연산자의 정확한 의미는 위에 아주 잘 표현되어있습니다.


eventTable[“Event6”] = (MyDelegate4)eventTable[“Event6”] + value;
eventTable[“Event6”] = (MyDelegate4)eventTable[“Event6”] – value;

이 한 줄 안에는 상당히 의미있는 내용들이 농축되어있습니다.

대리자는 보통 하나의 메서드만을 사용하여 초기화하는 것이 일반적입니다. 그러나, 여기에 나타난 것처럼 대리자는 한 메서드에 대해서만 컨테이너를 생성하는 것이 아니라, 메서드 컬렉션을 관리하고 있습니다. 이것을 정확한 표현으로는 호출 목록이라고 하며, 호출 목록에 또 다른 메서드들을 더하고 빼는 것을 컴바인과 제거라고 표현합니다. 그리고 이렇게 형성된 호출 목록을 감싸는 대리자를 멀티캐스트 대리자라고 부릅니다.

위의 구문은 다시 정리하면 아래와 같습니다.


eventTable[“Event6”] += value;
eventTable[“Event6”] -= value;

그렇다면 여기서 이벤트의 역할이 궁금해집니다. 이벤트는 멀티캐스트 대리자를 하나의 속성으로 관리하고 구체화할 수 있는 또 하나의 규격으로 이해하시면 됩니다. 사실 이제껏 보아왔던 이벤트에 사용하는 += 연산자와 -= 연산자는 멀티캐스트 대리자로 처리하더라도 얼마든지 가능했던 일입니다. 하지만 멀티캐스트 대리자는 호출 목록을 관리하기 위한 하나의 수단일 뿐 본 강좌에서 살펴본것과 같이 하나의 프로퍼티로서 확장하는 것은 어렵습니다.

이벤트와 대리자 사이의 차이점은 기존에 널리 사용되던 개념만을 가지고는 이와 같은 상황에 직면했을 때 사라지게 됩니다. 하지만 본 강좌에서는 이와 같이 모호한 상황에 대해서 구체적인 결론을 내리고자 합니다.

단순히 변수에 값을 집어넣고 빼는 것만으로 보았을 때에는 변수나 프로퍼티가 유사한 것처럼 보이지만 본질적으로 변수는 변수일 뿐, 프로퍼티와 같이 커스터마이징을 할수는 없는것을 여러분들께서는 알고 계십니다.

대리자와 이벤트 사이의 관계도 이와 같이 정의할 수 있습니다. 대리자나 이벤트 모두 호출 목록을 만들어서 관리할 수 있는 것은 동일합니다. 그러나, 대리자는 호출 목록을 관리하는 것 까지이지만 이벤트는 어떻게 호출 목록을 추가하고 제거할 것인지에 관해서 다룰 수 있는 프로퍼티의 역할을 이행할 수 있습니다.

긴 강좌를 읽어주셔서 감사합니다. 많은 도움이 되셨기를 바랍니다.

댓글 남기기