2013. 7. 25. 16:19

리플렉션(reflection)은 자바 프로그래밍 언어의 기능 중 하나다. 리플렉션을 사용하면 자바 프로그램을 실행해서 해당 프로그램을 조사하거나 스스로를 살펴볼(introspect) 수 있다. 또한 프로그램의 내부 프로퍼티를 조작할 수도 있다. 예를 들어 리플렉션을 사용하면 자바 클래스에서 해당 클래스의 멤버의 이름을 모두 획득해서 표시할 수 있다.

자바 클래스에서 스스로를 조사하고 조작하는 이러한 기능이 그리 대단해 보이지 않을 수도 있다. 하지만 이러한 기능을 지원하지 않는 프로그래밍 언어도 있다. 예를 들어 파스칼, C, C++ 프로그램에서는 해당 프로그램 안에 정의된 함수에 대한 정보를 얻을 수 있는 방법이 없다.

실제로 자바 빈(JavaBeans)에서 리플렉션을 적용했다. 자바 빈을 사용하면 소프트웨어 컴포넌트를 빌더 툴을 사용해서 시각적으로 조작할 수 있다. 이러한 빌더 툴은  자바 컴포넌트(클래스)가 동적으로  로드될 때, 리플렉션을 사용해서 해당 컴포넌트의 프로퍼티를 획득한다.


간단한 예제
리플렉션이 어떻게 동작하는지 아래의 간단한 예를 통해 살펴보자:


 import java.lang.reflect.*;
 
   public class DumpMethods {
      public static void main(String args[])
      {
         try {
            Class c = Class.forName(args[0]);
            Method m[] = c.getDeclaredMethods();
            for (int i = 0; i < m.length; i++)
            System.out.println(m[i].toString());
         }
         catch (Throwable e) {
            System.err.println(e);
         }
      }
   }


아래와 같이 실행해 보면

java DumpMethods java.util.Stack

결과는 다음과 같다

public java.lang.Object java.util.Stack.push(java.lang.Object)
public synchronized java.lang.Object java.util.Stack.pop()
public synchronized java.lang.Object java.util.Stack.peek()
public boolean java.util.Stack.empty()
public synchronized int java.util.Stack.search(java.lang.Object)

보다시피 java.util.Stack 클래스의 메서드 명이 출력된다. 메서드의 파라미터와 리턴 타입 또한 패키지 명을 포함해서 출력된다.


이 프로그램은 Class.forName을 사용해서 기술된 클래스를 로드한 후, getDeclaredMethods를 호출해서 해당 클래스에 정의된 메서드 목록을 얻는다. java.lang.reflect.Method는 클래스에서 단일 메서드를 나타내는 클래스다.


리플렉션 사용을 위한 설정
Method와 같이 리플렉션을 위한 클래스는 java.lang.reflect 패키지에서 찾을 수 있다. 이 패키지에 있는 클래스를 사용하려면 세 단계를 거쳐야만 한다. 첫 단계로 조작하려는 클래스에 해당하는 java.lang.Class 객체를 얻어야 한다. java.lang.Class는 실행 중인 자바 프로그램에서 클래스 또는 인터페이스를 나타내기 위해 사용한다.

Class 객체를 얻을 수 있는 한 가지 방법은 다음과 같다


Class c = Class.forName("java.lang.String");

와 같이 사용하면 String에 대한 Class 객체를 얻을 수 있다. 또는 다음과 같이 사용할 수도 있다:

Class c = int.class; // 또는

Class c = Integer.TYPE; 

와 같이 사용하면 기본형에 대한 Claass 정보를 얻을 수 있다. 두 번째 방법의 경우, 기본현에 대한 래퍼 클래스(Integer와 같이)에 미리 정의된 TYPE 필드에 접근한다.


두 번째 단계는 getDeclaredMethods와 같은 메서드를 호출하는 작업으로, getDeclaredMethods를 호출하면 해당 클래스에 선언된 메서드의 목록을 모두 얻을 수 있다.

이러한 정보를 모두 얻고 나면, 세 번째 단계로 리플렉션 API를 사용해서 해당 정보를 조작할 수 있다. 예를 들어 아래와 같은 일련의 문장을 실행하면,

Class c = Class.forName("java.lang.String");
Method m[] = c.getDeclaredMethods();
System.out.println(m[0].toString());

String에 선언된 첫 번째 메서드를 텍스트 형식으로 표시하게 된다.


이후에 다룰 예제에서는 지금 설명한 세 단계를 결합해서 사용한다. 이를 통해 특정한 목적에 맞게 리플렉션을 활용하는 방식을 효과적으로 처리하는 예제를 한꺼번에 보여주고자 한다.

instanceof 연산자 흉내내기
Class 정보를 일단 얻고 나면, 이 다음에는 주로 해당 Class 객체에 대하 기본적인 질문은 하는 작업을 한다. 예를 들어 Class.isInstance 메서드를 사용하면 instanceof 연산자를 흉내낼 수 있다

class A {}

   public class instance1 {
      public static void main(String args[])
      {
         try {
            Class cls = Class.forName("A");
            boolean b1 = cls.isInstance(new Integer(37));
            System.out.println(b1);
            boolean b2 = cls.isInstance(new A());
            System.out.println(b2);
         }
         catch (Throwable e) {
            System.err.println(e);
         }
      }
   }

이 예제에서는 클래스 A에 대한 Class 객체를 생성한 후, 해댕 객체가 클래스 A의 인스턴스인지 검사한다. 해당 객체는 Integer(37)의 인스턴스는 아니지만, new A()의 인스턴스이기는 하다.

클래스의 메서드에 대해 조사하기
리플렉션을 사용할 때 가장 기본적이면서도 유용한 사용처는 특정 클래스에 정의된 메서드를 조사하는 일이다. 아래의 코드를 사용하면 클래스의 메서드를 조사할 수 있다:

import java.lang.reflect.*;

   public class method1 {
      private int f1(Object p, int x) throws NullPointerException
      {
         if (p == null)
            throw new NullPointerException();
         return x;
      }
        
      public static void main(String args[])
      {
         try {
           Class cls = Class.forName("method1");
        
            Method methlist[] = cls.getDeclaredMethods();
            for (int i = 0; i < methlist.length; i++) {  
               Method m = methlist[i];
               System.out.println("name =" + m.getName());
               System.out.println("decl class = " + m.getDeclaringClass());

               Class pvec[] = m.getParameterTypes();
               for (int j = 0; j < pvec.length; j++)
                  System.out.println("param #" + j + " " + pvec[j]);

               Class evec[] = m.getExceptionTypes();
               for (int j = 0; j < evec.length; j++)
                  System.out.println("exc #" + j + " " + evec[j]);

               System.out.println("return type = " + m.getReturnType());
               System.out.println("-----");
            }
         }
         catch (Throwable e) {
            System.err.println(e);
         }
      }
   }

이 프로그램에서는 먼저 method1에 대한 Class 객체를 얻는다. 그런 후 해당 객체에 getDeclaredMethods를 호출해서 Method 객체의 목록을 얻는다. method1 클래스에 정의된 메서드마다 하나의 Method 객체를 얻을 수 있다. 이때 public, protected, package, private 메서드 모두 포함된다. getDeclaredMethods가 아니라 getMethods를 사용하면 상속받은 메서드에 대한 정보까지도 얻을 수 있다.

이렇게 Method 객체의 목록을 얻고 나면, 이후의 작업은 간단한다. 메서드마다 단순히 파라미터 타입, 예외 타입, 리턴 타입을 출력한다. 이때 타입이 기본형이든지 아니면 클래스 형이든지 관계없이, 각 타입은 Class 기술자를 사용해서 차례대로 표시된다. 이 프로그램을 실행하면 출력은 다음과 같다:

name = f1
decl class = class method1
param #0 class java.lang.Object
param #1 int
exc #0 class java.lang.NullPointerException
return type = int
-----
name = main
decl class = class method1
param #0 class [Ljava.lang.String;
return type = void
-----

생성자에 대한 정보 얻기

클래스의 생성자에 대한 정보를 얻는 일도 마찬가지 방식으로 처리할 수 있다. 다음 예를 보자:

import java.lang.reflect.*;

        
   public class constructor1 {
      public constructor1()
      {
      }
        
      protected constructor1(int i, double d)
      {
      }
        
      public static void main(String args[])
      {
         try {
           Class cls = Class.forName("constructor1");
        
           Constructor ctorlist[] = cls.getDeclaredConstructors();
         for (int i = 0; i < ctorlist.length; i++) {
               Constructor ct = ctorlist[i];
               System.out.println("name = " + ct.getName());
               System.out.println("decl class = " + ct.getDeclaringClass());

               Class pvec[] = ct.getParameterTypes();
               for (int j = 0; j < pvec.length; j++)
                  System.out.println("param #" + j + " " + pvec[j]);

               Class evec[] = ct.getExceptionTypes();
               for (int j = 0; j < evec.length; j++)
                  System.out.println("exc #" + j + " " + evec[j]);

               System.out.println("-----");
            }
          }
          catch (Throwable e) {
             System.err.println(e);
          }
      }
   }

생성자에서는 사실 리턴 타입이 없으므로, 이 예제에서는 리턴 타입에 대한 정보는 얻지 않는다.

이 프로그램을 실행하면 출력은 다음과 같다

name = constructor1
decl class = class constructor1
-----
name = constructor1
decl class = class constructor1
param #0 int
param #1 double
-----

클래스 필드에 대한 정보 얻기

특정 클래스에 정의된 데이터 필드에 대한 정보를 얻을 수도 있다. 이를 위해 아래와 같은 코드를 사용할 수 있다:

import java.lang.reflect.*;

       
   public class field1 {
      private double d;
      public static final int i = 37;
      String s = "testing";
       
      public static void main(String args[])
      {
         try {
            Class cls = Class.forName("field1");
       
            Field fieldlist[] = cls.getDeclaredFields();
            for (int i = 0; i < fieldlist.length; i++) {
               Field fld = fieldlist[i];
               System.out.println("name = " + fld.getName());
               System.out.println("decl class = " + fld.getDeclaringClass());
               System.out.println("type = " + fld.getType());
               int mod = fld.getModifiers();
               System.out.println("modifiers = " + Modifier.toString(mod));
               System.out.println("-----");
            }
          }
          catch (Throwable e) {
             System.err.println(e);
          }
       }
   }

이 예제는 앞에서 본 예제와 비슷하다. 차이점은 여기에서 Modifier를 사용했다는 점이다. Modifier는 리플렉션용 클래스로, "private int"에서 보듯이 필드 멤버의 제한자를 나타낸다. 제한자 자체는 정수로 표현되고, Modifier.toString를 사용해서 문자열 형식으로 된 값을 얻을 수 있다. 이때 "공식적인" 선언 순서("static이 "final"보다 먼저 나온다)에 따른다. 이 프로그램의 실행 결과는 다음과 같다:

name = d

decl class = class field1
type = double
modifiers = private
-----
name = i
decl class = class field1
type = int
modifiers = public static final
-----
name = s
decl class = class field1
type = class java.lang.String
modifiers =
-----

메서드와 마찬가지로, 클래스에 선언된 필드에 대한 정보만을 얻을 수 있을 뿐만 아니라(getDeclaredFields), 슈퍼 클래스에 정의된 필드에 대한 정보도 얻을 수 있다(getFields).

이름으로 메서드를 호출하기
지금까지의 예제에서는 클래스의 정보를 얻는 방법과 관련해서 설명했다. 이와는 다른 방법으로도 리플렉션을 사용할 수 있다. 예를 들어 주어진 이름으로 메서드를 호출할 수도 있다.

아래의 예제를 통해, 주어진 이름을 사용해 메서드를 호출하는 방식을 살펴보자:

 import java.lang.reflect.*;

        
   public class method2 {
      public int add(int a, int b)
      {
         return a + b;
      }
        
      public static void main(String args[])
      {
         try {
           Class cls = Class.forName("method2");

           Class partypes[] = new Class[2];
           partypes[0] = Integer.TYPE;
           partypes[1] = Integer.TYPE;

           Method meth = cls.getMethod("add", partypes);

           method2 methobj = new method2();

           Object arglist[] = new Object[2];
           arglist[0] = new Integer(37);
           arglist[1] = new Integer(47);

           Object retobj = meth.invoke(methobj, arglist);

           Integer retval = (Integer)retobj;
           System.out.println(retval.intValue());
         }
         catch (Throwable e) {
            System.err.println(e);
         }
      }
   }

특정 프로그램에서 add 메서드를 호출하기를 원하지만, 실행 시간 이전에는 어느 메서드를 호출할지 모른다고 가정해보자. 즉 메서드의 이름이 실행 시간에 주어진다고 해보자 (예를 들어 자바빈 개발 환경에서는 실제로 이러한 방식으로 처리된다). 위의 프로그램에서는 이러한 방식을 처리하는 한 가지 방법을 보여준다.

getMethod를 사용하면 정수 타입의 파라미터를 두개를 받고 인자로 주어진 이름을 가지는 메서드를 클래스에서 찾을 수 있다. 이 메서들 찾아서 Method 객체에 저장한 후, 적절한 타입의 객체 인스턴스에 대해 해당 Method 객체를 호출한다. 메서드를 호출하려면 파라미터 목록이 반든시 생성되어야 한다. 이 예제의 경우 Integer 객체를 사용해 기본형인 37과 47 정수 값을 래핑한다. 리턴 값(84)도 Integer객체로 래핑해야 한다.

새로운 객체 생성하기

생성자를 호출하는 일은 메서드를 호출하는 일과는 다르다. 사실 생성자를 호출하는 일은 새로운 객체를 생성하는 일이다 (더 정확하게 말하면 새로운 객체를 생성하는 일은 메모리를 할당하고 동시에 객체를 생성하는 일이다). 따라서 앞의 예제와 거의 비슷하게 만들어보자면 다음과 같다:

 import java.lang.reflect.*;

        
   public class constructor2 {
      public constructor2()
      {
      }
        
      public constructor2(int a, int b)
      {
         System.out.println("a = " + a + " b = " + b);
      }
        
      public static void main(String args[])
      {
         try {
           Class cls = Class.forName("constructor2");
           Class partypes[] = new Class[2];
            partypes[0] = Integer.TYPE;
            partypes[1] = Integer.TYPE;

            Constructor ct  = cls.getConstructor(partypes);

            Object arglist[] = new Object[2];
            arglist[0] = new Integer(37);
            arglist[1] = new Integer(47);

            Object retobj = ct.newInstance(arglist);
         }
         catch (Throwable e) {
            System.err.println(e);
         }
      }
   }

이 경우 기술된 파라미터 타입으로 처리할 수 있는 생성자를 찾은 후, 해당 생성자를 호출해서 객체의 새로운 인스턴스를 생성한다. 이 방식은 완전히 동적이라는데 그 가치가 있다. 즉 생성자를 찾고, 해당 생성자를 컴파일 시간이 아니라 실행 시간에 호출한다.


필드의 값 변경하기
리플렉션을 활용할 수 있는 또 다른 사용처는 객체의 데이터 필드를 변경하는 일이다. 이 방식도 마찬가지로 리플렉션은 본질적으로 동적이라는데 그 가치가 있다. 즉 실행 중인 프로그램에서 이름을 기반으로 필드를 찾고, 해당 필드를 변경할 수 있다. 아래 예제에서 이 방식을 어떻게 사용하는지 보여준다:

import java.lang.reflect.*;
        
   public class field2 {
      public double d;
        
      public static void main(String args[])
      {
         try {
            Class cls = Class.forName("field2");
            Field fld = cls.getField("d");
            field2 f2obj = new field2();
            System.out.println("d = " + f2obj.d);

            fld.setDouble(f2obj, 12.34);
            System.out.println("d = " + f2obj.d);
         }
         catch (Throwable e) {
            System.err.println(e);
         }
      }
   }

예제에서 보다시피 필드 d의 값을 12.34로 변경한다.


배열 사용하기

마지막으로 리플렉션을 사용해서  배열을 생성하고 조작해보자. 자바 언어에서 배열이란 클래스의 특별한 타입으로, 배열에 대한 참조는 Object 참조에 할당할 수 있다.

아래 예제를 통해 배열이 어떻게 동작하는지 살펴보자:

 import java.lang.reflect.*;
        
   public class array1 {
      public static void main(String args[])
      {
         try {
            Class cls = Class.forName("java.lang.String");
            Object arr = Array.newInstance(cls, 10);
            Array.set(arr, 5, "this is a test");
            String s = (String)Array.get(arr, 5);
            System.out.println(s);
         }
         catch (Throwable e) {
            System.err.println(e);
         }
      }
   }

이 예제에서는 사이즈가 10인 String 배열을 생성한 후, 배열의 5번째 위치에 문자열 값을 할당한다. 그리고 나서 해당 값을 꺼내 표시한다.

아래 코드와 같이 배열을 좀더 세밀하게 조작할 수도 있다:

  import java.lang.reflect.*;

        
   public class array2 {
      public static void main(String args[])
      {
         int dims[] = new int[]{5, 10, 15};
         Object arr = Array.newInstance(Integer.TYPE, dims);
        
         Object arrobj = Array.get(arr, 3);
         Class cls = arrobj.getClass().getComponentType();
         System.out.println(cls);
         arrobj = Array.get(arrobj, 5);
         Array.setInt(arrobj, 10, 37);
        
         int arrcast[][][] = (int[][][])arr;
         System.out.println(arrcast[3][5][10]);
      }
   }

이 예제에서는 5 x 10 x 15 차원의 정수 배열을 만든 후, 해당 배열에서 [3][5][10] 위치에 있는 값에 37을 할당한다. 이때 다차원 배열은 사실 배열의 배열이다. 따라서 예를 들어 맨 처음 Array.get을 호출하고 나면, 그 결과인 arrobj는 10 x 15 배열에 해당한다. 이 배열에서 다시 배열을 꺼내서 사이즈가 15인 배열을 얻는다. 그리고 나서 이 배열의 10번째 이치에 Array.setInt를 사용해서 값을 할당한다.

생성하는 배열의 타입이 동적이며, 컴파일 시간에 배열의 타입이 정해지지 않아도 됨을 유심히 봐야 한다.


결론

자바 리플렉션을 사용하면 클래스와 데이터 구조에 대한 정보를 이름을 기반으로 동적으로 얻을 수 있다는 점에서 매우 유용하다. 뿐만 아니라 리플렉션을 사용하면 자바 프로그램을 실행하는 동안에 해당 클래스를 조작할 수 있다. 이 기능은 믿을 수 없을 정도로 강력하며, C, C++, 포트란, 파스칼과 같은 자바 이외의 언어에서는 리플렉션에 해당하는 기능이 전혀 없다.


'java' 카테고리의 다른 글

별찍기  (0) 2013.07.25
배열 + 제어문으로 중복값 찾기  (0) 2013.07.25
Enumeration, 열거형  (0) 2013.07.25
자바 디컴파일러 JAD - 다운로드 사용법  (0) 2013.07.25
아스키코드 값  (0) 2013.07.25
Posted by 1+1은?