Java 리플렉션에 대한 재고(reflection)

|

http://www.hanb.co.kr/network/view.html?bi_id=1369

 

일상에서의 리플렉션(reflection)이란 거울 속에 비친 자신의 모습입니다. 프로그래밍 세상에서의 리플렉션은 프로그램이 자신의 모습을 보고 심지어 수정하기 까지 하는 것을 말합니다. Java reflection API는 바로 그런 기능을 언어의 기본 요소인 클래스, 필드, 메소드를 들여다 볼 수 있는 평범한 Java API를 통해 제공합니다. 리플렉션을 이해하는 것은 여러분이 자주 사용하는 툴을 이해하는데 도움이 됩니다. Eclipse가 어떻게 자동완성으로 메소드 이름을 만들어 줄까? Tomcat은 web.xml파일에 있는 클래스 이름을 가지고 웹의 요청을 처리할 서블릿을 실행하는 걸까? Spring은 어떻게 마술 같은 dependency injection을 하는 것일까? 여러분의 프로그램에서도 리플렉션을 사용하여 보다 동적이고 유연한 코드를 작성하실 수 있습니다. 리플렉션을 사용하면 이전에 본적 없는 클래스들을 매우 멋지게 처리할 수 있습니다.

클래스 만들기

이미 말했듯이 리플렉션의 기본 아이디어는 프로그램이 동작하는 내부에 집어 넣을 API를 제공하는 것입니다. Java에서 가장 기본이 되는 사상이 바로 클래스기 때문에(클래스 없이 자바 프로그램을 만들어 보세요) Class 클래스부터 살펴보는 것이 좋겠습니다. 이것의 객체는 Class 타입일 것입니다. 일단 Class객체를 가지게 되면 그것으로부터 클래스에 관련된 모든 정보를 뽑아낼 수 있습니다. 클래스의 이름, 그것이 public 인지 abstract 인지 final 인지 그리고 심지어 상위 클래스까지 말이죠.

이 정도면 이론은 충분합니다. 자, 이제 리플렉션 현미경을 가지고 아래에 있는 매우 간단한 Employee 클래스를 살펴봅시다.

package com.russolsen.reflect;

public class Employee
{
   public String _firstName;
   public String _lastName;
   private int _salary;

      public Employee()
   {
      this( "John", "Smith", 50000);
   }
 
   public Employee(String fn, String ln, int salary)
   {
      _firstName = fn;
      _lastName = ln;
      _salary = salary;
   }
   
      public int getSalary()
   {
      return _salary;
   }
   
   public void setSalary(int salary)
   {
      _salary = salary;
   }
   
   public String toString() 
   {
      return "Employee: " + _firstName +  " "
             + _lastName + " " + _salary;
   }

}

Class 객체를 만드는 가장 쉬운 방법은 해당 클래스 객체의 getClass 메소드를 호출하는 것입니다. 아래에 있는 코드는 Employee 객체를 만들고 그것의 Class 객체를 만들어서 클래스에 대한 다양한 정보들을 출력합니다.

package com.russolsen.reflect;

import java.lang.reflect.Modifier;

public class GetClassExample
{
   public static void main(String[] args)
   {
 
      Employee employee = new Employee();
      
      Class klass = employee.getClass();
      
      System.out.println( "Class name: " + klass.getName());
      System.out.println( 
            "Class super class: " + klass.getSuperclass());
      
      int mods = klass.getModifiers();
      System.out.println( 
            "Class is public: " + Modifier.isPublic(mods));
      System.out.println( 
            "Class is final: " +  Modifier.isFinal(mods));
      System.out.println( 
            "Class is abstract: " + Modifier.isAbstract(mods)); 

   }

}

코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

Class name: com.russolsen.reflect.Employee
Class super class: class java.lang.Object
Class is public: true
Class is final: false
Class is abstract: false

예제에서 보이듯이 클래스의 이름과 상위 클래스를 알아내는 것은 다른 접근 메소드들(Getters or Accessors)을 호출하는 것처럼 쉬운 일입니다. 만약 해당 클래스가 public 인지 abstract 인지 final 인지 알고 싶다면 약간 복잡해 집니다. 이 모든 정보가 getModifires에 의해 하나의 int 값으로 패키징되어 넘어오기 옵니다. 다행히 Java는 Modifier 클래스를 통해 getModifiers에서 넘어온 숫자를 가지고 여러 일을 할 수 있는 static 메소드들을 제공해 줍니다.

객체에 getClass를 호출하는 것 만이 Class 객체를 얻을 수 있는 유일한 방법은 아닙니다. 클래스 이름을 사용하여 직접 얻을 수도 있습니다.

Class klass = Employee.class;

세 번째 방법은 좀 더 흥미로운 방법입니다. 문자열을 통해서 Class 객체를 생성할 수 있습니다. 물론 그 문자열이 클래스 이름을 포함하고 있을 때 말이죠. 다음과 같이 Class 클래스에 있는 forName 을 호출하여 얻을 수 있습니다.

      Class klass = Class.forName("com.russolsen.reflect.Employee");
      
      System.out.println( "Class name: " + klass.getName());
      System.out.println( 
            "Class super class: " + klass.getSuperclass());
      
      // Print out the rest...

forName을 사용할 때 한 가지 주의할 것은 클래스 이름 앞에 완전한 패키지 경로를 붙여줘야 한다는 것입니다. 평범하고 늙은 “Employee” 말고 “com.russolsen.reflect.Employee" 여야만 합니다. forName을 통해 리플렉션 API의 기본적인 강력함(그리고 멋진 것)을 살펴봤습니다. 클래스 이름을 포함한 문자열로 시작할 수도 있고 class로 끝낼 수도 있습니다.

바로 인스턴스 만들기

class 객체를 가져오고 그것에 관한 정보를 찾는 것은 그것 자체로도 흥미롭고 유용합니다. 하지만 진짜 재미있는 것은 리플렉션을 사용하여 무언가를 실제 해보는 것입니다. Class 객체를 가지고 할 수 있는 가장 눈에 띄는 작업은 그 클래스의 새로운 객체를 만드는 것입니다. newInstance 메소드를 사용하여 간단하게 만들 수 있습니다. 실제 사용하는 것을 보여 주기 위하여 아래에 있는 간단한 프로그램은 커맨드 라인 인자(command line argument)로 Class 객체를 만들고 그 클래스 타입의 객체를 만드는 프로그램을 보여줍니다.

package com.russolsen.reflect;

public class NewInstanceExample
{
   public static void main(String[] args)
      throws ClassNotFoundException,
      InstantiationException, IllegalAccessException
   {

      Class klass = Class.forName(args[0]);
      Object theNewObject = klass.newInstance();
      System.out.println("Just made: " + theNewObject);
   }
}

위 코드를 "com.russolsen.reflect.Employee" 인자와 함께 실행하면 새로운 Employee 객체를 만들게 됩니다.

Just made: Employee: John Smith 50000

Run it again, but this time feed it "java.util.Date" and you will get:

Just made: Tue Feb 27 20:25:20 EST 2007

간단한 코드 몇 줄로 얼마나 많은 유연성을 얻게 되었는지 생각해보세요. 위에 있는 프로그램은 실제 Employee 나 Date 클래스에 관해 아는 것이 하나도 없지만 각각의 새로운 객체들을 만들 수 있습니다. 이것이야 말로 Java 프로그래밍을 하는 또 다른 방법입니다.

인자 없는 생성자 너머에

Class.newInstance 메소드를 호출하는 것은 인자 없이 new를 사용하는 것과 동일합니다. 그러나 만약 인자가 없는 생성자 즉 default 생성자가 없는 클래스에 newInstance를 호출하면 어떻게 될까요? 좋을 일 없습니다. 별로 맘에 안 드는 InstantiationException을 받게 됩니다.

좋은 소식이 있습니다. 생성자 인자를 필요로 하는 클래스의 객체를 동적으로 만들 수 있습니다. 하지만 약간 더 힘든 작업을 해야 합니다. 그건 바로 클래스에서 해당 생성자를 찾고 적당한 인자를 사용하여 그것을 호출하는 일입니다. 적당한 생성자를 찾는 일은 여러분이 찾고자 하는 생성자에 대한 정보를 가지고 getConstrucor 메소드를 사용하면 됩니다. 그럼 Constuctor 객체를 얻게 되고 그것을 사용하여 새로운 객체를 만들 수 있습니다.

Let's see how this all works in code:
      Class klass = Class.forName("com.russolsen.reflect.Employee");

      Class[] paramTypes = {
            String.class, 
            String.class, 
            Integer.TYPE };
      
      Constructor cons = klass.getConstructor(paramTypes);
      
      System.out.println( "Found the constructor: " + cons);

      
      Object[] args = { 
            "Fred", 
            "Fintstone", 
            new Integer(90000) };
      
      Object theObject = cons.newInstance(args);
      System.out.println( "New object: " + theObject);

생성자들 사이의 차이점은 오직 그것들이 가지고 있는 매개 변수들입니다. 따라서 getConstructor 메소드에 찾고자 하는 생성자에 들어갈 매개변수 각각의 Class들 객체의 배열을 넘겨줍니다. 위에 있는 예제에서는 두 개의 String 그리고 하나의 int를 받는 생성자를 찾게 됩니다. Constructor 객체를 얻은 뒤 새로운 객체를 생성하는 일은 간단합니다. 실제 인자로 들어갈 객체의 배열을 newInstance 메소드를 호출하면서 넘겨주면 됩니다.

geConstructor에는 조그만 지뢰가 하나 있습니다. 파라미터의 타입으로 생성자를 식별하여 원하는 것을 찾을 때 primitive 인자와 그것의 wrapper 클래스를 잘 구별해야 합니다. 생성자가 인자로 primitive 타입인 int를 받는 것인가 아니면 그것의 삼촌 격인 java.lang.Integer 클래스의 객체를 받는 건가요? 만약 java.lang.integer 같은 wrapper 타입의 객체를 받는 생성자라면 Integer.class처럼 wrapper 클래스를 사용하면 됩니다. primitive 타입인 int를 사용하는 생성자라면 Integer.TYPE을 사용합니다. TYPE은 primitive을 위해 마련한 것입니다. 모든 wrapper 클래들은 static 타입인 TYPE 필드를 가지고 있고 이것을 사용하여 primitive 타입이라는 것을 알려줄 수 있습니다

 

 

클래스를 좀 더 자세히 살펴보기

첫 번째 예제에서 봤듯이 Class 객체는 그것의 이름이나 상위 클래스 같은 정보를 제공합니다. 이 이름을 사용하여 좀 더 자세히 rank 와 시리얼 넘버(serial number) 차원의 정보까지 알 수 있습니다. 예를 들어 getMethods 메소드를 사용하여 클래스가 가진 모든 public 메소드를 찾을 수 있습니다.

      Class klass = Class.forName("com.russolsen.reflect.Employee");
           
      Method[] methods = klass.getMethods();
      
      for(Method m : methods )
      {
         System.out.println( "Found a method: " + m );
      }

getMethods 는 클래스가 가지고 있는 public 메소드 각각에 해당하는 Method 객체의 배열을 반환합니다.

Found a method: public java.lang.String com.russolsen.reflect.Employee.toString()
Found a method: public int com.russolsen.reflect.Employee.getSalary()
Found a method: public void com.russolsen.reflect.Employee.setSalary(int)
Found a method: public native int java.lang.Object.hashCode()
Found a method: public final native java.lang.Class java.lang.Object.getClass()
Found a method: public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
Found a method: public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
Found a method: public final void java.lang.Object.wait() throws java.lang.InterruptedException
Found a method: public boolean java.lang.Object.equals(java.lang.Object)
Found a method: public java.lang.String java.lang.Object.toString()
Found a method: public final native void java.lang.Object.notify()
Found a method: public final native void java.lang.Object.notifyAll()

getMethods는 클래스의 사용자(client) 입장이기 때문에, 배열에는 자신이 가지고 있는 모든 public 메소드 (Object 까지 달하는 모든 상속 계층의 상위 클래스들에 있는 public 메소드까지 포함하여)를 배열에 담아 줍니다.

만약 하나의 메소드에만 관심이 있다면, getMethod(단수 형태 입니다.)를 사용할 수 있습니다. getConstructor와 비슷하게 동작하지만 파라미터 타입들 뿐만 아니라 메소드의 이름도 넘겨 줘야 합니다. 아래에 있는 코드는 하나의 int 파라미터를 가지는 setSalary 라는 메소드를 찾습니다.

      Class klass = Class.forName("com.russolsen.reflect.Employee");
      Class[] paramTypes = {Integer.TYPE };
      Method setSalaryMethod = 
         klass.getMethod("setSalary", paramTypes);
           
      System.out.println( "Found method: " + setSalaryMethod);

리플렉션을 사용하여 메소드를 호출하는 것은 constuctor를 호출하는 것과 매우 유사합니다. 앞서 살펴봤던 Method 객체만 있으면 되고 메소드에 인자로 넘겨 줄 배열과 메소드를 호출할 객체가 필요합니다. 아래에 있는 코드는 우리 직원에게 월급을 올려주기 위해서 Employee 객체에 있는 setSalary 메소드를 호출합니다.

      Class klass = Class.forName("com.russolsen.reflect.Employee");

      Class[] paramTypes = {Integer.TYPE };
      Method setSalaryMethod = 
         klass.getMethod("setSalary", paramTypes);
      
      Object theObject = klass.newInstance();
      Object[] parameters = { new Integer(90000) };
      
      setSalaryMethod.invoke(theObject, parameters);

그냥 theObject.setSalary(9000)를 호출하지 않고 왜 그렇게 귀찮게 했을까요? 위에 있는 코드를 자세히 살펴보시길 바랍니다. 코드의 첫 번째 줄을 빼면 위에 있는 프로그램은 완전히 일반적입니다. 어떤 클래스의 어떤 객체든 상관없이 setSalary 메소드를 호출할 것입니다. 약간만 수정하면 어떤 클래스의 어떤 객체든 거기에 있는 모든 메소드를 호출하는 코드로 사용할 수 있습니다.

필드 가지고 놀기

리플렉션을 사용하여 메소드를 호출하는 데에서 그치지 않습니다. 필드에 대한 모든 권한 역시 가지고 있습니다. getMethods 처럼 getFields는 해당 클래스 또는 그것의 상위 클래스에 있는 모든 public 필드 각각에 대한 Fileld 객체의 배열을 반환합니다.

      Class klass = Class.forName("com.russolsen.reflect.Employee");
      
      System.out.println( "Class name: " + klass.getName());
      
      Field[] fields = klass.getFields();
      
      for(Field f : fields )
      {
         System.out.println( "Found field: " + f);
      }

Employee 가 두 개의 public 필드를 가지고 있기 때문에 두 개의 멤버를 가진 배열을 얻게 됩니다.

Class name: com.russolsen.reflect.Employee
Found field: public java.lang.String com.russolsen.reflect.Employee._firstName
Found field: public java.lang.String com.russolsen.reflect.Employee._lastName

getField메소드를 사용하여 특정 필드 하나만 가져올 수도 있습니다.

      Field field = klass.getField("_firstName");
      System.out.println("Found field: " + field);

Field 객체를 가지고 get 메소드를 호출하면 필드가 가진 값을 가져올 수 있고 set을 사용하여 값을 설정할 수 있습니다.

      Object theObject = new Employee("Tom", "Smith", 25);
      
      Class klass = Class.forName("com.russolsen.reflect.Employee");
      
      Field field = klass.getField("_firstName");
      
      Object oldValue = field.get(theObject);
      System.out.println( "Old first name is: " + oldValue);
      
      field.set( theObject, "Harry");
      Object newValue = field.get(theObject);
      System.out.println( "New first name is: " + newValue);

위에 있는 코드를 실행하고 _firstName 필드가 변하는 것을 주의 깊게 살펴보시기 바랍니다.

Old first name is: Tom
New first name is: Harry

규칙 깨기

Java의 가장 신성한 규칙 중 하나를 깨는 방법을 얘기하지 않고서는 리플렉션을 마무리할 수 없습니다. 여러분 모두 잘 알다시피 해당 클래스 밖에서는 private 메소드를 호출 할 수 없습니다. 그렇죠? 아마 평범한 기술을 사용하는데 그친다면 하지 못할 것입니다. 하지만 리플렉션을 사용하면 거의 모든 걸 할 수 있습니다.

private 메소드를 호출하기 위해서 가장 먼저 해야 할 일은 호출하고 싶어 하는 메소드를 나타내는Method객체를 얻는 것입니다. getMethod로는 얻을 수 없습니다. 그건 오직 public 메소드만 반환합니다. private(또는 protected)메소드를 가져오는 방법은 getDeclaredMethod를 사용하는 것입니다. getMethod는 해당 클래스의 사용자 관점(client’s view)에서 public 메소드만 가져오지만, getDeclaredMethod는 클래스에 선언한 모든 메소드를 반환합니다. 아래에 있는 예제에서 java.util.ArrayList 클래스에 있는 private 메소드인 removeRange 를 가져옵니다.

      ArrayList list = new ArrayList();
      list.add("Larry");
      list.add("Moe");
      list.add("Curley");

      System.out.println("The list is: " + list);

      Class klass = list.getClass();

      Class[] paramTypes = { Integer.TYPE, Integer.TYPE };
      Method m = klass.getDeclaredMethod("removeRange", paramTypes);

      Object[] arguments = { new Integer(0), new Integer(2) };
      m.setAccessible(true);
      m.invoke(list, arguments);
      System.out.println("The new list is: " + list);

private 메소드를 받은 뒤에 간단히 setAccessable 안전장치를 제거하고 호출하면 됩니다.

The list is: [Larry, Moe, Curley]
The new list is: [Curley]

removeRange 메소드는 리스트에서 주어진 범위의 아이템을 제거하는 것처럼 보입니다. 이것은 매우 강력한 마술입니다. java.util 에 있는 클래스에 접근하여 그 안에 있는 private 메소드를 호출할 수 있습니다. 이것을 사용하여 코드의 의도를 우회하여 private 메소드를 호출하는 취미를 즐기실 건가요? 아니죠! 그러나 저런 것들이 약간은 유용할 때가 있습니다.

결론

리플렉션을 사용하여 Java의 규칙을 무시하는 것처럼 보이는 것들을 하는 프로그램을 작성할 수 있습니다. 일반적으로는 알 수 없는 클래스에 관한 정보를 모두 알아내는 코드를 작성할 수 있습니다. 그리고 동적으로 알아낸 정보에 어떤 행위를 할 수도 있습니다. 즉 새로운 객체를 만들고 메소드를 호출하고 필드에 값을 설정하거나 가져올 수 있습니다. 극단적인 경우에서는 클래스의 private 멤버들에 접근할 수 있습니다. 점점 복잡해져가는 Java 개발 툴의 동작을 이해하기 위해서는 리플렉션에 대한 이해가 필요합니다. 또한 “평범한” Java 프로그램이 할 수 있는 것 이상의 프로그램을 작성해야 할 때도 필요합니다.

Resources

And