공통 컴포넌트를 개발하다 보면, 이벤트 순서를 제어하고 싶을 때가 있습니다. 제가 등록하지 않은(누군가가 먼저 등록한) 이벤트를 말이죠.
저는 어떤 경우냐면, 공통 컴포넌트 개발팀에서 만든 A 컴포넌트가 있고, 이걸 저희 팀에 맞게 제가 A를 이용해서 B라는 일종의 헬퍼 컴포넌트를 만듭니다. 그런데, 기존 팀원들은 A를 이미 다른 곳에 (많이)적용한 후 라는 거죠.
여기서 문제가 발생하게 됩니다. A 컴포넌트의 특정 이벤트는 제가 만든 B 컴포넌트에서만 사용해야 된다거나, A컴포넌트의 특정이벤트를 다른 팀원이 사용하고 있다 하더라도, 꼭 B 컴포넌트에서 먼저 호출해야 되는 경우가 생깁니다.
그렇다고 기존에 A 컴포넌트를 사용했던 클래스를 하나하나 수정하거나, A컴포넌트 자체에 대한 변경을 요청할 수도 없는 상황입니다.
자, 이런 드물지만 절박한 상황에서 우리의 리플렉션님을 사용하게 되는 거죠.
아래에서 Form 클래스의 MouseClick 이벤트를 가로채는 예제를 하나하나 만들어 가면서 설명하겠습니다.
[코드 1]
using System;
using System.Windows.Forms;
namespace EventCollectionSample
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.Text = string.Empty;
this.MouseClick += new MouseEventHandler(Form1_MouseClick);
EventChanger();
}
private void Form1_MouseClick(object sender, MouseEventArgs e)
{
this.Text += "0";
}
}
}
[코드1]은 단순히 폼에 마우스 클릭이벤트가 일어났을 때, 타이틀바에 ‘0’을 계속해서 찍어주는 윈도우폼입니다.
이 코드중 생성자에 등록되는 MouseClick 이벤트를 가로채서 다른 이벤트 핸들러를 먼저 호출하도록 해보겠습니다. EventChanger 메서드를 주목해 주세요. MouseClick 이벤트가 등록된 이후(!)에 호출되는 메서드임을 주의 깊게 보시면서 이제 이 메서드의 내용을 채워보도록 하죠.
프로그램이 CLR에 의해 메모리에 로딩되게 되면, 객체들은 사용되지 않더라도 메모리(그중에서 힙)의 특정 부분에 객체의 정보(접근자, 멤버, 메서드 등등)를 기록해 놓습니다. 이 정보가 일종의 신상명세(?)라고 할 수 있는데, 이게 바로 System.Type 클래스의 인스턴스로서 System.Object 클래스의 GetType 메서드로 알아낼 수 있습니다.
(여기에 대한 자세한 설명은 길어지고, 본문의 내용과 살짝 거리가 있으니, 다음에 한번 다루도록 하겠습니다.)
어쨌든, Type 을 얻어오면, GetMethod 라는 메서드를 통해서 해당 타입에 속한 메서드를 가져올 수 있습니다. 자, 이벤트 목록을 한번 가져와 봅시다. 근데, 이벤트 목록은 어떻게 가져와야 될까요?
일단 우리가 이벤트를 가져오려고 하는 클래스를 봅시다. 이 클래스는 Form 클래스인데, 상속 트리를 거슬러 올라가면,
System.ComponentModel.Component 클래스를 상속받고 있음을 알고 있습니다. 바로 이곳에 우리가 찾을
protected EventHandlerList Events 라는 프러퍼티가 있습니다. 요놈이 자손대대(!)로 사용되어 Form 클래스에서도 사용되는 것이지요.
잠깐 언급을 하자면, EventHandlerList는 다수의 이벤트를 효과적으로 관리하기 위해 사용하는 클래스입니다.
마지막으로 하나더, 프러퍼티는 런타임에서 get_, set_ 라는 접두어를 가지는 인자 없는 메서드로 바뀌는걸 알고 계십니까? 위의 protected EventHandlerList Events 는 실제 런타임시에 메서드로 변환이 됩니다. 아래와 같이 말이죠.
protected EventHandlerList Events
{
get
{
if (events == null)
{
events = new EventHandlerList(this);
}
return events;
}
}
↓ ↓ ↓
protected EventHandlerList get_Events()
{
if (events == null)
{
events = new EventHandlerList(this);
}
return events;
}
자, 이제 어떤 메서드를 찾아야 하는지 알게 되었네요, “
get_Events” 가 우리가 찾을 메서드 이름입니다.
자, 아래와 같이 메서드 정보를 얻어올 수 있습니다.
MethodInfo mi = this.GetType().GetMethod("get_Events", BindingFlags.NonPublic | BindingFlags.Instance);
아까 찾은 메서드명을 “get_Events”로 넘겨주는 것을 볼 수 있구요, 두 번째 인자는 Flag형태의 열거형인데, 꽤 많은 종류가 있으나, 두 종류만 설명 드리자면,
NonPublic, Public 과
Instance, Static 이 크게 두 분류로 나뉠 수 있습니다. 클래스의 멤버는 크게 인스턴스멤버(Instance)인지, 정적멤버(Static)인지로 나뉠 수 있습니다. 그리고, 한정자의 경우 공개인지(Public), 비공개(NonPublic)인지로 나뉠 수 있습니다. 그래서 위의
두 분류 중 하나씩은 꼭 지정해 주셔야지, 멤버 검색에 성공할 수 있습니다. 지금은 Events는 비공개이고, 인스턴스멤버라는 것을 알고 있기 때문에 두 개만 한정적으로 지정해 주었습니다. 그러나, 공개여부나 이런 것을 알 수 없을 때라던지, 범용적인 목적으로 구성하실 때는 웬만한 조건을 다 걸어주시는 것이 좋겠죠.
자, 우리가 왜 메서드 정보를 얻어왔을까요? MethodInfo 클래스에는 Invoke 라는 강력한 메서드가 있기 때문에 위의 예에서처럼 비공개 메서드도 실행시킬 수 있습니다. Events 프러퍼티가 공개였다면, 이 고생(?)을 하지 않아도 되겠죠^^; 자 그럼 현재 폼의 실제 Events 값을 얻어보도록 하겠습니다.
EventHandlerList evtList = mi.Invoke(this, new object[] { }) as EventHandlerList;
첫 번째 인자는 메서드를 실행시킬 기준이 되는 인스턴스를 넘겨줍니다. 우리는 현재 폼의 Events를 얻고 싶은 거죠? 그래서 this를 넘겨줍니다. 두 번째 인자는 해당 메서드가 인자를 가질 때 넘겨줄 인자목록입니다. 아까 얘기했듯이 프러퍼티는 메서드로 변환될 때 인자 없는 메서드로 바뀐다고 했었죠? 그래서 비어있는 배열을 넘겨 줍니다. 마지막으로 Invoke 메서드는 반환 값을 object 로 넘겨주는데, 우리는 Events 프러퍼티가 EventHandlerList 클래스를 넘겨준다는 것을 알고 있습니다. 따라서 적절하게 캐스팅해줍니다.
이제, EventHandlerList에 대해서 잠깐 알아보겠습니다.
EventHandlerList는 키, 값 쌍으로 구성되는 컬렉션인데요, 키로 object를, 값으로 Delegate를 사용합니다. 닷넷컨트롤의 경우 대부분의 이벤트가 여기에 모두 등록되어있습니다. 우리가 찾고자 하는 MouseClick도 마찬가지라는 거죠. 그런데 키값을 알아야 찾을 텐데, 키값은 어디에 있죠? 잠시 닷넷 프레임웍 소스의 Control.cs를 살펴보죠.
Control.cs 일부
private static readonly object EventMouseClick = new object();
private static readonly object EventMouseDoubleClick = new object();
private static readonly object EventMouseCaptureChanged = new object();
private static readonly object EventMove = new object();
private static readonly object EventResize = new object();
일부만 보여드렸는데, 감이 오십니까? 네, 일단은 이놈도 비공개 이고, 정적이며, 값으로 객체 인스턴스를 사용하고 있네요. 그리고 네이밍룰이 Event+’이벤트이름’ 이라는 것도 알 수 있는데요, 나중에 범용클래스로 만들 때 써먹으면 좋겠네요^^;
비공개 이기 때문에 아까처럼 타입에서 찾아야 되는데요. 아까와는 접근 제한자가 틀리죠? 아까는 protected였기 때문에 자손대대로 내려와서 현재 클래스(Form을 상속받은)에도 있었지만, private은 해당 클래스인 Control에만 존재합니다. 즉 현재클래스(this)의 부모에게서 값을 얻어와야 합니다. 이때 써먹으라고 Type 클래스에는
BaseType 이라는 멤버가 있는데, 이 멤버는 현재 타입이 어떠한 타입을 상속받은 경우 해당 타입을 가리키게 되어있습니다. 자 그럼 우리의 클래스(this)는
Form -> ContainerControl -> ScrollableControl -> Control 이와 같이 총 네 번의 상속을 받으므로 아래와 같이 해줍니다.
FieldInfo fi = this.GetType().BaseType.BaseType.BaseType.BaseType.GetField("EventMouseClick", BindingFlags.NonPublic | BindingFlags.Static);
....=ㅂ=;; 완전 무식하게 예를 보여드렸습니다만, 실제 구현에서는 재귀호출로 탐색해나가는 구조면 좀더 깔끔할 듯 합니다. (
예제는 예제일 뿐!!)
아까와 다르게 우리가 찾으려는 EventMouseClick 가 정적멤버이기 때문에 플래그 중에 Instance가 Static으로 바뀌었구요, 또한 우리가 찾을 놈이 변수이기 때문에 GetField 메서드를 사용했습니다. 반환 값은 FieldInfo 클래스이며, 메서드 사용은 GetMethod와 동일합니다.
아까 메서드의 Invoke를 사용해서 해당 메서드를 실행시켰다면, 필드는 실행이 아니라, 값을 얻어오는 것이죠? 따라서 GetValue라는 메서드를 사용합니다. 아까와 같이 해당 필드값을 가져올 인스턴스를 인자로 넘겨줍니다.
object keyMouseClick = fi.GetValue(this);
그러면 해당 인스턴스(this)의 원하는 필드(fi)의 값이 object로 반환됩니다.
자, 다시 조금 전의 상황을 다시 떠올려 봅시다.
특정 필드(그것도 private)의 값이 왜 필요했죠? EventHandlerList 의 키값으로 써먹기 위해서였죠. 그럼 이제 키값을 알았으니, 키값을 통해 MouseClick 이벤트의 Delegate를 얻어와 볼게요.
Delegate del = evtList[keyMouseClick];
아까 얻어온 this의 이벤트 리스트(evtList) 컬렉션에서 MouseClick 키값(fi.GetValue(this))으로 Delegate를 가져왔습니다. 시험 삼아서 한번 호출해 볼까요?
del.DynamicInvoke(this, null);
대리자를 직접 호출해줬더니, 우리가 처음 [코드1]에서 등록했던 이벤트 핸들러인 Form1_MouseClick 가 호출되는 것을 볼 수 있습니다.
이제 우리가 바꿔 치거나, 지우거나, 순서를 바꿀 이벤트를 찾았으니, EventHandlerList 클래스를 통해서 쉽게 요리가 가능해졌습니다.
EventHandlerList에는 AddHandler, RemoveHandler 메서드가 있기 때문에, 특정 이벤트의 추가나 삭제를 쉽게 할 수 있습니다.그럼 일단 기존 이벤트는 지우고,
evtList.RemoveHandler(keyMouseClick, del);
우리가 원하는 새로운 이벤트를 추가해줍니다. 우선 이벤트 핸들러로 사용할 메서를 만들고,
private void Form1_MouseClickV2(object sender, MouseEventArgs e)
{
this.Text += "1";
}
추가해 줍니다.
evtList.AddHandler(keyMouseClick, new MouseEventHandler(Form1_MouseClickV2));
여기까지 하면, 우리가 새롭게 정의한 이벤트 핸들러로 교체되구요, 순서를 바꾸고 싶었다면, 다시 아까 삭제했던 이벤트를 추가해 줍니다. EventHandlerList 는 추가된 순서대로 처리하기 때문에, 새로운 이벤트를 뺐다가, 다른 뭔가를 넣고, 다시 넣게 되면 처리 순서가 바뀌겠죠.
evtList.AddHandler(keyMouseClick, del);
자, 이제 우리가 필요로 하는 단편적(!)인 기능은 완성됐습니다. 실행해서 결과를 보게 되면, Form1_MouseClickV2 가 먼저 실행되고, 나중에 Form1_MouseClick 이 실행되는 것을 볼 수 있습니다.
지금까지 작성한 코드는 아래와 같습니다.
완성된 EventChanger 메서드
private void EventChanger()
{
// 메서드(프러퍼티) 정보를 가져옴
MethodInfo mi = this.GetType().GetMethod("get_Events", BindingFlags.NonPublic | BindingFlags.Instance);
// 메서드정보와 인스턴스를 통해 실제 메서드를 실행시켜 결과 값을 받음
EventHandlerList evtList = mi.Invoke(this, new object[] { }) as EventHandlerList;
// 필드 정보를 가져옴
FieldInfo fi = this.GetType().BaseType.BaseType.BaseType.BaseType.GetField("EventMouseClick", BindingFlags.NonPublic | BindingFlags.Static);
// 필드 정보와 인스턴스를 통해 실제 값을 가져옴
object keyMouseClick = fi.GetValue(this);
// 이벤트 목록에서 등록된 이벤트를 가져옴
Delegate del = evtList[keyMouseClick];
// 테스트를 위해 실행시켜봄
// del.DynamicInvoke(this, null);
// 기존 이벤트 제거
evtList.RemoveHandler(keyMouseClick, del);
// 새로운 이벤트 추가
evtList.AddHandler(keyMouseClick, new MouseEventHandler(Form1_MouseClickV2));
// 기존 이벤트 다시 추가(순서변경)
evtList.AddHandler(keyMouseClick, del);
}
리플렉션을 통한 비공개 메서드, 필드에 대한 접근이 이 메서드의 주요 내용입니다. 이 메서드의 모든 과정은 예외 처리나, 다른 클래스를 넘겼을 때의 범용적인 처리는 전혀 되어있지 않은데요, 이런 부분은 직접 처리하실 수 있을 거라고 생각합니다. 다음 기회에 시간이 되면 범용적인 클래스로 만들어서 올려보겠습니다.
긴 글 읽어 주셔서 감사합니다.
0