RSS구독하기:SUBSCRIBE TO RSS FEED
즐겨찾기추가:ADD FAVORITE
글쓰기:POST
관리자:ADMINISTRATOR
'리플렉션'에 해당되는 글 2
리플렉션을 통해서 타입을 컨트롤하는 것은 일반 개발자(컴파일러/프레임웍 등의 개발자가 아닌)에게는 거의 필요가 없거나, 사용을 권장하지 않는 기능입니다. 하지만, 실무를 접하다 보면 부득이하게 필요한 경우가 발생하기 마련이고, 제한적으로 사용하면 도움이 되기에 이러한 팁을 작성해 보았습니다.


// 테스트를 위한 클래스
public class IHavePrivateMember
{
    public IHavePrivateMember()
    {
        this.mySecretName = "Lemon";
    }

    private string mySecretName;
    private void PrintSecretName()
    {
        Console.WriteLine("My secret name is {0}.", this.MySecretName);
    }

    private string MySecretName
    {
        get { return this.mySecretName; }
    }
}


위와 같은 클래스의 private 멤버인 mySecretName 변수, MySecretName 프러퍼티, PrintSecretName 메서드에 접근해 보도록 하겠습니다.

IHavePrivateMember 클래스의 타입을 얻어오는 것에서부터 리플렉션이 시작됩니다. CLR에서 관리되는 모든 타입은 System.Type 클래스의 인스턴스인데, 이 클래스는 객체의 타입을 유지, 관리해주는 많은 멤버들을 가지고 있습니다. 그 중 GetMembers 메서드를 통해서 해당 타입의 멤버들을 MemberInfo 클래스로 가져올 수 있습니다.

MemberInfo[] privateMembers = typeof(IHavePrivateMember).GetMembers(
                BindingFlags.Instance | BindingFlags.NonPublic);


MemberInfo 클래스는 다음과 같은 상속 구조를 가지면서 다른 구체화된 멤버정보 클래스들의 베이스가 됩니다.

사용자 삽입 이미지
GetMembers는 위에서 보이는 MemberInfo를 상속받는 클래스들을 MemberInfo클래스의 배열로 리턴해줌으로써 MemberInfo.MemberType을 살펴보고 이를 적절히 캐스팅해서 사용할 수 있습니다.

if (privateMembers[i].MemberType == MemberTypes.Field)
{
}


이와 같이 타입을 확인후에 해당 클래스로 캐스팅하여 적절한 액션을 취하면 되겠지요. 필드를 대상으로 취할 수 있는 액션은 값을 가져오는 것이니깐 GetValue 정도의 메서드가 적절할 듯 싶습니다. 그런데, GetValue 메서드는 파라메터를 하나 요구하는데요, 이 멤버가 Static 멤버가 아닌, 인스턴스멤버이기 때문에 값을 가져오려면 반드시 인스턴스가 필요하기 때문입니다.

IHavePrivateMember secret = new IHavePrivateMember();
Console.WriteLine("멤버변수 {0} / 값 : {1}",
    privateMembers[i].Name,
    ((FieldInfo)privateMembers[i]).GetValue(secret)
);


이렇게 값을 얻어올 실제 인스턴스를 넘겨줍니다.

하나를 알면 열을 안다고 했던가요? 나머지 타입들도 같은 맥락으로 사용하실 수 있습니다. 메서드는 실행이니까 Invoke, 프러퍼티도 값을 가져오니까 GetValue 이런식으로 사용하시면 됩니다.

콘솔응용프로그램으로 실행되는 전체 소스코드는 아래와 같습니다.

using System;
using System.Reflection;

namespace AccessPrivateMemberSample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            IHavePrivateMember secret = new IHavePrivateMember();

            // 멤버로 검색할 조건을 인자로써 지정해줌
            MemberInfo[] privateMembers = typeof(IHavePrivateMember).GetMembers(
                BindingFlags.Instance | BindingFlags.NonPublic);

            for (int i = 0; i < privateMembers.Length; i++)
            {
                if (privateMembers[i].MemberType == MemberTypes.Field)
                {
                    Console.WriteLine("멤버변수 {0} / 값 : {1}",
                        privateMembers[i].Name,
                        ((FieldInfo)privateMembers[i]).GetValue(secret)
                    );
                }
                else if (privateMembers[i].MemberType == MemberTypes.Property)
                {
                    Console.WriteLine("프러퍼티 : {0} / 값 : {1}",
                        privateMembers[i].Name,
                        ((PropertyInfo)privateMembers[i]).GetValue(secret, null)
                    );
                }
                else if (privateMembers[i].MemberType == MemberTypes.Method)
                {
                    MethodInfo mi = privateMembers[i] as MethodInfo;
                    if (mi != null && mi.GetParameters().Length == 0)
                    {
                        Console.WriteLine("메서드 {0} 실행 ----------", mi.Name);
                        mi.Invoke(secret, null);
                        Console.WriteLine("메서드 {0} 실행 끝 --", mi.Name);
                    }
                }
            }
        }
    }

    // 테스트를 위한 클래스
    public class IHavePrivateMember
    {
        public IHavePrivateMember()
        {
            this.mySecretName = "Lemon";
        }

        private string mySecretName;
        private void PrintSecretName()
        {
            Console.WriteLine("My secret name is {0}.", this.MySecretName);
        }

        private string MySecretName
        {
            get { return this.mySecretName; }
        }
    }
}


결과
메서드 PrintSecretName 실행 ----------
My secret name is Lemon.
메서드 PrintSecretName 실행 끝 --
메서드 get_MySecretName 실행 ----------
메서드 get_MySecretName 실행 끝 --
메서드 Finalize 실행 ----------
메서드 Finalize 실행 끝 --
메서드 MemberwiseClone 실행 ----------
메서드 MemberwiseClone 실행 끝 --
프러퍼티 : MySecretName / 값 : Lemon
멤버변수 mySecretName / 값 : Lemon
계속하려면 아무 키나 누르십시오 . . .

2009/03/31 20:27 2009/03/31 20:27
http://lemonwidz.com/tc/trackback/21
공통 컴포넌트를 개발하다 보면, 이벤트 순서를 제어하고 싶을 때가 있습니다. 제가 등록하지 않은(누군가가 먼저 등록한) 이벤트를 말이죠.

저는 어떤 경우냐면, 공통 컴포넌트 개발팀에서 만든 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, PublicInstance, 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);
}


리플렉션을 통한 비공개 메서드, 필드에 대한 접근이 이 메서드의 주요 내용입니다. 이 메서드의 모든 과정은 예외 처리나, 다른 클래스를 넘겼을 때의 범용적인 처리는 전혀 되어있지 않은데요, 이런 부분은 직접 처리하실 수 있을 거라고 생각합니다. 다음 기회에 시간이 되면 범용적인 클래스로 만들어서 올려보겠습니다.

긴 글 읽어 주셔서 감사합니다.

2009/03/19 09:50 2009/03/19 09:50
http://lemonwidz.com/tc/trackback/18
지송  | 2009/04/02 16:19
아하.. 여기가 레몬님 블로그구낭..

이벤트 검색하다 즐겨찾기로 저장해둔 곳이... 여기였네용.

하하.... 좋은 하루 되세요
레몬  | 2009/04/02 20:00
하하^^;;
지송님께서 찾아주셔서 리플까지 남겨주시니 감사합니다^^;
뽀씰  | 2010/05/28 10:28
좋은 정보 얻어갑니다 ^^ 감사해요~
joy  | 2012/05/02 11:19
어려운 내용임에도 불구하고 쉽고 재미있게 이해하게됬군요.
재미있게 읽었습니다. ^^
전체 (23)
사진이야기 (4)
프로그래밍 (18)
  1. Nyaonge's Home  2011
    [C#] ?? 연산자(물음표 두개)
  1. 2012/03 (1)
  2. 2011/12 (2)
  3. 2009/07 (1)
  4. 2009/04 (1)
  5. 2009/03 (9)