posted by 동건이 2008.08.11 16:27

자바 리플렉션


실행 중인 프로그램 내부에서 자바 타입들에 대해 알아내기 위해 자바 reflection 패키지를 사용할 수 있다. 예를 들면, 특정 클래스의 모든 메소드 명의 목록을 구할 수도 있고, 그것을 보여줄 수도 있다. 혹은 특정 메소드를 나타내는 java.lang.reflect.Method 객체를 가질 수도 있고, 그 객체를 메소드에 대한 일종의 포인터처럼 사용할 수도 있다.

리플렉션에 대해 배우고 있다고 가정해보자. 어떤 프로그램에서 한 객체의 특정 메소드를 호출해야 된다. 그래서 코드는 아래처럼 보일 것이다.

 import java.lang.reflect.*; 
 class A { 
   public void f(int i) { 
      System.out.println("A.f called i = " + i); 
   }
 } 
   public class RefDemo1 { 
      public static void main(String args[]) throws Exception { 
         A aref = new A();
         // "A"에서 "int" 파라미터 한 개를 받는 "f"란 이름의 메소드를 찾아라. 
         Method meth = A.class.getMethod("f", new Class[]{int.class}); 
         // "aref" 객체 상의 메소드를 호출해라. 
         meth.invoke(aref, new Object[]{new Integer(37)}); 
      }
   }

RefDemo1 프로그램은 A 객체를 생성한다. 그리고 그 객체의 f를 리플렉션을 사용하여 호출한다.

이 접근방법은 확실히 작동한다. 그러나 간단한 메소드 호출을 위해 둘러가는 방법을 사용한다. 이 예제에서 리플렉션을 사용하는 대신에 그 메소드를 호출하기 위해 아래처럼 하면 훨씬 더 간단할 것이다.

 aref.f(37); 

만약 이 예제가 리플렉션을 사용하는 좋은 상황이 아니라면 그러면 언제 사용할까? 이 팁은 "인터프리터" 혹은 "객체 실행기"라 불릴 프로그램 예제를 보여주면서 이 질문에 답하려 한다. 그 프로그램은 파일이나 키보드로부터 입력된 한 줄을 읽는다. 그런 다음 이 줄의 데이터에 기반하여 객체를 생성하고 그 객체 상의 메소드를 호출한다.

인터프리터 입력의 예제가 있다.

 > new java.lang.String ABC > call toLowerCase abc 

첫 줄은 ABC 문자열로 새 String 객체를 만들어라고 인터프리터에게 지시하고, 두번째 줄은 인터프리터에게 toLowerCase 메소드를 호출할 것을 지시한다. 또한 메소드의 반환값은 abc가 되거나 아니면 오류 메시지를 생성할 것을 지시한다.

각 메소드는 전달인자와 반환값이 명시되어 있다. 메소드가 호출될 후 입력 파일에 명시된 반환값은 실제 반환값과 비교된다. 만약 그 값들이 다르다면, 오류 플레그가 설정된다.

이러한 접근방법을 사용할 때, 메소드 이름과 전달인자 기대 반환값을 나열한 스크립트 간단히 작성함으로써, 메소드를 테스트 할 수 있다.

그런 프로그램을 어떻게 구현할 수 있을까? 프로그램내에서 이름이 문자열 형태로 존재하는 임의의 클래스와 메소드를 지정할 수 있어야 한다는 것은 명확하다. 이 이름들이 실제 클래스와 메소드로 매핑될 필요가 있다. 그러니까, 만약 프로그램이 클래스 이름의 문자열을 가지고 있다면 이 클래스의 인스턴스를 실제로 만들어야 된다. 프로그램은 그런 다음 주어진 메소드의 이름으로 이 클래스의 메소드를 찾아 호출해야 된다.

리플렉션은 특별히 이런 종류의 프로그래밍 영역을 위해 설계되었다. 리플랙션을 사용하면 문자열 이름으로부터 클래스 인스턴스를 생성하고, 이름으로 메소드를 찾아 실행하는 것이 가능하다. 이런 프로그래밍 언어의 동적 특성은 가끔 "늦은 바인딩(late binding)" 이란 용어로 불리기도 한다. 이와는 반대로 C나 C++ 같은 언어는 "이른 바인딩(early binding)"을 사용한다. 그러니까 실행 시간 동안에는 클래스/함수의 이름을 가질 수 없다.

인터프리터 프로그램이 여기 있다.

 import java.io.*; 
 import java.util.*; 
 import java.lang.reflect.*; 
 class Interpreter { 
    // 입력된 라인과 그 라인의 토큰 목록 
   private String input; 
   private List tokenlist; 
   // 현재 클래스와 그 객체 
   private Class currcls; 
   private Object currobj; 
   // 입력 라인을 실행한다 
   public void execLine(String line) {
      // 입력된 한 라인을 토큰으로 나누고 라인이 비면 리턴한다. 
      input = line; 
      getTokens(); 
      if (tokenlist.size() == 0) { 
         return; 
      }
      System.out.println("Executing line: " + input); 
      // 라인의 종류(생성 혹은 호출)를 얻어 보낸다. 
      String type = (String)tokenlist.get(0); 
      if (type.equals("new")) { 
         execNew(); 
      }
      else if (type.equals("call")) { 
         execCall(); 
      }
      else { 
         msg("Invalid operator on line"); 
         return; 
      }
   } 
   // 클래스의 새 객체를 생성하고 그것을 현재 객체로 설정한다 
   private void execNew() { 
      if (tokenlist.size() < 2) { 
         msg("Missing class name"); 
         return; 
      }
      // 클래스가 로드되지 않았으면, 로드한다. 
      try { 
         currcls = Class.forName((String)tokenlist.get(1)); 
      }
      catch (ClassNotFoundException e) { 
         msg("ClassNotFoundException in forName"); 
         return; 
      }
      // 생성자에 전달할 전달인자를 얻는다. 
      currobj = null; 
      Class args[] = getArgTypes(2); 
      Object vals[] = getArgValues(2); 
      Constructor ctor; 
      // 생성자를 찾는다. 
      try { 
         ctor = currcls.getConstructor(args); 
      } catch (NoSuchMethodException e) { 
         msg("NoSuchMethodException in getConstructor"); 
         return; 
      }
      // 그 객체의 새 인스턴스를 만든다. 
      try { 
         currobj = ctor.newInstance(vals); 
      } catch (InstantiationException e) { 
         msg("InstantiationException in newInstance"); 
         return; 
      } catch (IllegalAccessException e) { 
         msg("IllegalAccessException in newInstance"); 
         return; 
      } catch (InvocationTargetException e) { 
         msg("InvocationTargetException in newInstance"); 
         return; 
      } 
   } 
   // 현재 객체의 메소드를 호출한다. 
   private void execCall() { 
      if (tokenlist.size() < 3) { 
         msg("Missing method or return value"); 
         return; 
      } 
      if (currobj == null) { 
         msg("No current class object"); 
         return; 
      }
      // 메소드 이름과 전달인자를 가져온다. 
      String methname = (String)tokenlist.get(1); 
      Object ret = getRet(); 
      Class args[] = getArgTypes(3); 
      Object vals[] = getArgValues(3); 
      Method meth; Object retobj; 
      // 클래스안에서 메소드를 찾는다. 
      try { 
         meth = currcls.getMethod(methname, args); 
      } catch (NoSuchMethodException e) { 
         msg("Method not found"); 
         return; 
      }
      // 메소드를 호출한다. 
      try { 
         retobj = meth.invoke(currobj, vals); 
      } catch (IllegalAccessException e) { 
         msg("IllegalAccessException in invoke"); 
         return; 
      } catch (InvocationTargetException e) {
        msg("InvocationTargetException in invoke"); 
        return; 
      }
      // 반환값을 체크한다.
      if (ret != null && !ret.equals(retobj)) {
         msg("Invalid return value from method");
         return; 
      }
   } 
   // 입력 라인을 토큰화한다.
   private void getTokens() { 
      tokenlist = new ArrayList(); 
      int strlen = input.length();
      int i = 0; 
      for (;;) { 
         while (i < strlen && Character.isWhitespace( input.charAt(i))) {
            i++; 
         } 
         if (i == strlen) {
            break;
         } 
         StringBuffer sb = new StringBuffer(); 
         while (i < strlen && !Character.isWhitespace( input.charAt(i))) { 
            sb.append(input.charAt(i));
            i++; 
         }
         tokenlist.add(sb.toString());
      }
   } 
   // 메소드 호출에 대한 (예상)반환값을 구한다.
   private Object getRet() { 
      String s = (String)tokenlist.get(2); 
      if (s.equals("void")) { 
         return null; 
      } else if (isNum(s)) {
         return new Integer(s); 
      } else { 
         return s; 
      } 
   } 
   // 생성자나 메소드에 대한 전달인자의 타입을 구한다. 
   private Class[] getArgTypes(int start) { 
      int numargs = tokenlist.size() - start; 
      Class args[] = new Class[numargs]; 
      int j = 0; 
      for (int i = start; i < tokenlist.size(); i++) { 
         String s = (String)tokenlist.get(i); 
         args[j++] = isNum(s) ? int.class : String.class; 
      } 
      return args; 
   } 
   // 생성자나 메소드 호출에 대한 전달인자 값을 구한다. 
   private Object[] getArgValues(int start) { 
      int numargs = tokenlist.size() - start; 
      Object args[] = new Object[numargs]; 
      int j = 0; 
      for (int i = start; i < tokenlist.size(); i++) { 
         String s = (String)tokenlist.get(i); 
         args[j++] = isNum(s) ? (Object)new Integer(s) : (Object)s; 
      }
      return args; 
   } 
   // 오류메시지를 보여준다. 
   private static void msg(String txt) { 
      System.out.println("*** " + txt + " ***"); 
   } 
   // 문자열이 숫자(NNN 혹은 -NNN)인지 아닌지를 결정한다. 
   private static boolean isNum(String s) { 
      int slen = s.length(); 
      int i = slen >= 2 && s.charAt(0) == '-' ? 1 : 0; 
      for (; i < slen; i++) { 
         if (!Character.isDigit(s.charAt(i))) { 
            return false; 
         } 
      } 
      return true; 
   } 
} 
 
public class RefDemo2 { 
   public static void main(String args[]) throws IOException { 
      Reader r; 
      boolean isterm = false; 
      // 명령어라인 파일이 있으면 이용하고 그렇지 않으면 표준 입력을 사용한다. 
      if (args.length == 1) { 
         r = new FileReader(args[0]); 
      } else { 
         r = new InputStreamReader(System.in); 
         isterm = true; 
      } 
      BufferedReader br = new BufferedReader(r); 
      Interpreter in = new Interpreter(); 
      // 입력 라인으로부터 읽고 보낸다. 
      for (;;) { 
         if (isterm) { 
            System.out.print("> "); 
         } 
         String inputline = br.readLine(); 
         if (inputline == null) { 
            break; 
         } 
         in.execLine(inputline); 
      } 
      br.close(); 
   } 
} 

이 프로그램은 만약 명령어 라인상에서 파일이름이 명시되면 파일로부터 명령어를 읽어들인다. 그렇지 않으면 프로그램은 프롬프트(">")를 띄운다. 프로그램은 각각의 입력 라인을 토큰으로 쪼갠다. 토큰은 라인상에서 공백문자로 구분되는 각각이다. 토큰은 문자열 형태로 (토큰)목록에 보관된다. 토큰이 없는 라인들은 무시된다.

이런 형태의 입력 라인은:

 new classname arg1 arg2 ... 

"현재 객체" 되는 클래스 인스턴스를 생성한다. 생성자에 전달할 전달인자를 명시할 수도 있다. 이 경우 리플렉션 메커니즘은 적당한 생성자를 찾아 호출하는데 사용된다. NNN이나 -NNN 같은 형태의 전달인자는 정수형으로 간주 된다. 다른 전달인자는 문자열로 취급된다. 실수형 같은 다른 타입도 다루는 것이 바람직하겠지만, 이러한 접근법은 개념을 보여주기엔 충분하다.

이런 형태의 입력 라인은:

 call methodname returnvalue arg1 arg2 ... 

현재 객체에 대해 메소드 이름과 전달인자의 형(int또는 String)을 사용하여 적절한 메소들 찾아 호출한다.

여기 java.lang.String을 테스트 하는데 사용될 입력 스크립트의 예제가 있다.

> new java.lang.String ABC 
> call toLowerCase abc 
> new java.lang.String abc 
> call concat abcdef def 
> new java.lang.String abc 
> call indexOf 2 c 

script라는 이름의 파일에 이 스크립트를 저장해라. 그런 다음 아래와 같이 RefDemo2 인터프리터 프로그램을 실행하라.

 java RefDemo2 script 

다음과 같은 화면을 보게 될 것이다.

Executing line: new java.lang.String ABC 
Executing line: call toLowerCase abc 
Executing line: new java.lang.String abc 
Executing line: call concat abcdef def 
Executing line: new java.lang.String abc 
Executing line: call indexOf 2 c 

만약 마지막 라인의 "2"를 "3"으로 바꾸면 인터프리터 프로그램은 메소드(indexOf) 구현이나 테스트하는 경우에서 버그가 있음을 나타내는 아래와 같은 오류를 보일 것이다.

 Executing line: call indexOf 3 c *** Invalid return value from method *** 

인터프리터를 사용하는 다른 예제가 있다. 다음과 같은 클래스가 있다고 가정해보자.

public class RefTest { 
   public int sum(int a, int b) { 
      return a + b; 
   } 
   public static int stsum(int a, int b) { 
      return a + b; 
   }
} 

아래 스크립트를 사용해서 테스트를 해볼 수 있다.

new RefTest call sum 10 4 6 
call sum 0 -5 5 
call sum 0 0 0 
call stsum 10 4 6 
call stsum 0 -5 5 
call stsum 0 0 0 

그리고 아래처럼해서 실행해보자.

 javac RefTest.java javac RefDemo2.java java RefDemo2 script 

결과는 다음과 같을 것이다.

Executing line: new RefTest 
Executing line: call sum 10 4 6 
Executing line: call sum 0 -5 5 
Executing line: call sum 0 0 0 
Executing line: call stsum 10 4 6 
Executing line: call stsum 0 -5 5 
Executing line: call stsum 0 0 0 

인터프리터 프로그램은 리플렉션이 얼마나 유용한가를 보여준다. 이런한 종류의 어플리케이션은 임의의 사용자 입력을 받아들이고 그것을 프로그램 내의 클래스와 메소드 이름으로 객체들을 다루는 데 사용한다. 보여주는데 주안점을 두어, 이 팁은 자세한 부분은 생략하고 어떤 주제에 대해서는 얼버무렸다. 그러나 그 프로그램들은 특정한 형태의 문제를 해결하는데 리플렉션의 위력을 잘 보여주었다.

리플렉션에 관한 좀 더 자세한 정보는 아놀드(Arnold), 고슬링(Gosling), 홀름스(Holmes)가 쓴 "자바 프로그래밍 언어(Java Programming Language) 3판"에서 11.2 절 리플렉션을 참조하세요.

 

'개발 > 외부 참조 글' 카테고리의 다른 글

OpenSSL 설치  (0) 2008.08.11
JNI  (0) 2008.08.11
자바 리플렉션  (1) 2008.08.11
OpenSSL  (0) 2008.08.11
GC 튜닝에 대한 의견들  (0) 2008.02.20
JVM 메모리 관리  (0) 2008.02.20

댓글을 달아 주세요

  1. Favicon of http://greenfrog7.egloos.com BlogIcon greenfrog 2008.12.17 11:22  Addr  Edit/Del  Reply

    좋은 자료 잘 봤습니다. 설명이 자세해서 이해하기 쉬웠습니다. 고맙습니다 ^^