2018년 5월 30일 수요일

C# - 이름으로 변수/메서드 접근

public class Exam
{
    public int korean;
    public int english;
    public int math;
}

public class ExamWrite
{
    public void Write(Exam exm, string subject)
    {
        if(subject == "korean")
            Console.WriteLine("korean : {0}", exm.korean);
        if(subject == "english")
            Console.WriteLine("english : {0}", exm.english);
        if(subject == "math")
            Console.WriteLine("math : {0}", exm.math);
    }
}

와 같은 프로그램이 있다고 해 봅시다. subject를 검색해서 같은 이름의 멤버변수값을 출력하는 프로그램이죠.
그런데 만약 과목이 수십개라면 어떨까요? 저런 if문이 수십개가 들어가야 합니다. 이럴 경우에는 문자열로 직접 변수이름을 가져올 수 있습니다. 이런 기능을 reflection이라고 합니다. 그러므로 다음과 같은 using문이 필요합니다.

using System.Reflection;

reflection 기능을 사용하기 위해서는 먼저 해당 객체의 타입을 필요로 합니다. typeof함수로서 Exam이라는 클래스의 정보를 얻어옵니다.

            Type tp = typeof(Exam);

다음에는 이 Exam이라는 클래스의 정보가 담긴 tp라는 객체에서 필드의 정보를 얻어와야겠죠. 필드의 정보를 얻기 위해서는 GetField라는 메서드를 사용합니다.
즉 다음 명령어는 subject에 담겨있는 문자열과 같은 이름의 멤버변수를 찾으라는 명령입니다.

            FieldInfo fld = tp.GetField(subject, BindingFlags.Instance |
                                                 BindingFlags.Static |
                                                 BindingFlags.Public |
                                                 BindingFlags.NonPublic);

여기서 BindingFlags는 각각 일반멤버변수, 정적멤버변수, 공용번수, 공용이 아닌 변수들을 모두 찾으라는 명령어죠. 이 외에 많은 플래그가 있지만, 가장 중요한 것은 이것들입니다. 필요한 것만 골라서 조합하면 됩니다.
만약 제대로 찾았다면 fld에는 그 변수에 대한 정보가 들어갑니다. 찾지 못했다면 null이 반환됩니다.
이 변수의 값을 얻기 위해서는

            object point = fld.GetValue(exm);

와 같이 할 수 있습니다. 위에서 찾은 subject라는 변수의 정보를 exm이라는 객체에서 찾아 그 값을 반환하라는 명령이죠. 다만 아직까지 이 필드의 정확한 타입을 알 수 없기에 반환값은 object형입니다. 하지만 이 명령어가 제대로 실행되었다면 (점수는 모두 정수형이므로) point에는 정수형 값이 들어가 있을 것입니다. 그러므로 값을 출력하기 위해서는

            if (point is int)
                Console.WriteLine("{0} : {1}", fld.Name, (int)point);

와 같이 쓸 수 있습니다.(이때 fld.Name에는 위에서 받아온 필드정보의 이름입니다)
즉,

using System.Reflection;

....

public class ExamWrite
{
    public void Write(Exam exm, string subject)
    {
        Type tp = typeof(Exam);
        FieldInfo fld = tp.GetField(subject, BindingFlags.Instance |
                                             BindingFlags.NonPublic |
                                             BindingFlags.Public |
                                             BindingFlags.NonPublic);
        object point = fld.GetValue(exm);
        if (point is int)
            Console.WriteLine("{0} : {1}", fld.Name, (int)point);
    }
}

와 같이 사용할 수 있죠.

그런데 이런 식으로 하면 한번에 하나의 성적만 출력 가능하죠. 모든 성적을 한번에 출력할 방법은 없을까요?
GetField가 필드 하나의 정보를 얻어오는 것이라면 GetFields는 모든 필드의 정보를 가져오는 메서드입니다. 그러므로 이 함수를 사용하면 모든 필드에 대한 조작을 할 수 있습니다.

using System.Reflection;

....

public class ExamWrite
{
    public void WriteAll(Exam exm)
    {
        Type tp = typeof(Exam);
        FieldInfo[] flds = tp.GetFields(BindingFlags.Instance |
                                        BindingFlags.Static |
                                        BindingFlags.Public |
                                        BindingFlags.NonPublic);
        foreach (var f in flds)
        {
            object point = f.GetValue(exm);
            if (point is int)
                Console.WriteLine("{0} : {1}", f.Name, (int)point);
        }
    }
}

마찬가지로 멤버변수에 값을 넣기 위해서는 FieldInfo의 GetValue 대신 SetValue를 사용할 수 있습니다.

using System.Reflection;

....

public class ExamWrite
{
    public void Perfect(Exam exm)
    {
        Type tp = typeof(Exam);
        FieldInfo[] flds = tp.GetFields(BindingFlags.Instance |
                                        BindingFlags.Static |
                                        BindingFlags.Public |
                                        BindingFlags.NonPublic);
        foreach (var f in flds)
        {
            object point = f.GetValue(exm);
            if (point is int)
                f.SetValue(exm, 100);
        }
    }
}

이것은 exm의 모든 변수값을 100점으로 만드는 함수가 되겠습니다.

만약 다음과 같이

public class Exam
{
    public const int korean;
    public const int english;
    public const int math;
}

와 같이 모두 const로 설정되어 있을 경우에도 이상없이 실행됩니다(다만 Perfect메서드에서는 const변수의 값을 바꾸려 시도하고 있으므로 TargetInvocationException이 뜹니다).

---------------------------

마찬가지로 메서드 역시 이름으로 호출이 가능합니다. 이 경우에는 GetField 대신 GetMethod를 사용할 수 있습니다.

        Type tp = typeof(ExamWrite);
        MethodInfo method = tp.GetMethod("WriteAll");

그리고 이 메서드를 호출하기 위해서는 Invoke 메서드를 사용할 수 있죠.

        method.Invoke(ew, new object[] { exm });

이것 역시 ew라는 객체를 통해 위에서 얻어온 "WriteAll"이라는 메서드를 호출하라는 뜻이죠. 이 메서드의 인수는 object의 배열을 통해 전달됩니다.

using System.Reflection;

....

        static void Main(string[] args)
        {
            Exam exm = new Exam();
            ExamWrite ew = new ExamWrite();

            Type tp = typeof(ExamWrite);
            MethodInfo method = tp.GetMethod("WriteAll");
            method.Invoke(ew, new object[] { exm });
        }

---------------------------

멤버변수나 메서드들은 모두 접근한정자(private, public 등)를 가지고 있습니다. 이 접근한정자는 컴파일시 확인을 하게 됩니다.
그러나 이 방식으로 멤버에 접근하는 것은 컴파일시가 아니라 실행시이기 때문에 접근한정자가 의미가 없습니다. 즉 private로 꼭꼭 숨겨놓은 멤버도 이 방식으로 접근하면 값을 읽어오거나 바꿀 수 있습니다. 이런 것을 주의해야 합니다.