자바 런타임 시에 인스턴스에 대한 조회와 수정이 가능하도록 한 기술로써 특정 인스턴스의 타입 type 이 무엇이고 그 내용이 무엇인지를 알 수 있게 하는 기술이다.

프로그램 코드에서

        Objec obj = new ActionListenerImpl();

과 같이 참조가 될때 obj 로 참조되는 인스턴스의 타입(여기서는 물론 ActionListenerImpl이다) 을 알아내야할 상황이 필요한 경우가 존재한다. 이를 위해서 Reflection 이라는 기술이 제공되고있다.

런타임시 특정 인스턴스의 타입을 조회하는 것 뿐만 아니라 컴파일시 생성된 클래스 파일에 대한 정보(생성자, 필드, 메쏘드, 부모 클래스 정보 등)를 제공하는데에도 이 기술이 사용된다.

"인스턴스" 와 그 인스턴스의 "클래스정보"를 연결해주고 특정 클래스에 대한 정보를 제공해주는 기술이라고 이해하면 좋을 듯. 중요한것은 인스턴스로부터 특정 타입정보를 알아내는 것, 그리고 특정 타입을 통해서 인스턴스를 생성하는 것이 가능하다는 점.

보통은 위처럼 new ... 를 통해서 프로그램 코드를 통해서 ActionListenerImpl 타입의 인스턴스를 생성하는 것이 일반적이다.(이 방법이 가장 보편적이고 효율적인것도 사실이고) 하지만 때때로 다음과 같이 생성하고자 하는 인스턴스의 타입명(full qualified name)만 가지고 인스턴스를 생성해야할 상황이 존재한다.

      Class cls = Class.forName("com.ynseo.listener.ActionListener");

위와 같이 생성하고자 하는 인스턴스의 타입명을 명시함으로써 타입 정보를 담고 있는 java.lang.Class 를 얻어오고 이를 통해서 인스턴스를 생성하는 것이 가능하다.

    Obj obj = cls.newInstance();

위에서 보인 두 줄의 코드는 기본생성자 new XXX() 가 제공된 경우의 예이다. 생성자에 파라미터가 전달되는 경우도 역시 위와같이 인스턴스를 생성할 수 있다.(방식은 아주 약간, 정말로 약간 복잡하다.)

< 타입 이름을 가지고 인스턴스 생성하기>

위에서 "com.ynseo.listener.ActionListenerImpl" 와 같은 이름을 Full Qualified Name 이라고 한다. 클래스의 패키지 이름까지 완전하게 적어주는 것인데, 이 이름이 클래스로더에게 전달되면 클래스로더는 패키지 이름을 경로명으로 해석해서 주어진 경로에 ActionListenerImpl 의 클래스 파일을 찾아서 로드한다.

로드된 클래스를 통해서 붕어빵 찍듯이 인스턴스를 찍어내데 이것이 위에서 newInstance() 라는 메소드가 하는 일이다.

사용자는 이렇게 생성된 인스턴스의 적절한 타입을 캐스팅해서 사용하면 된다.

        String fullQualifiedName = "com.ynseo.reflection.ReflectionClass";
    // constructor with no params
    Class <?> clazz = Class.forName( fullQualifiedName );
    Object obj = clazz.newInstance();
    assertEquals(obj.getClass(), clazz);
    assertEquals(com.ynseo.reflection.ReflectionClass.class, clazz);
    assertEquals(new ReflectionClass().getClass(), clazz);


위 코드는 기본 생성자 가 정의된 클래스에서 reflection 기술을 이용해서 인스턴스를 생성하고 그 인스턴스를 통해서 다시 인스턴스의 타입정보를 읽어들여서 비교하는 테스트 코드이다.

Class 인스턴스를 얻는 방법으로 .class와 .getClass() 의 두가지 방식이 있는데 이 둘의 차이는 언제 타입 정보를 얻는가에 따라서 나뉜다. (자세한 내용은 [여기]에서 확인 가능함) 물론 위의 코드는 클래스로더의 구현에 따라서 실패할 수도 있지만 자바에서 기본적으로 제공되는 클래스로더를 통해서 위 코드를 테스트하면 모두 통과한다.(별도로 클래스 로더를 구현해야하는 경우가 아니라면 위 테스트코드가 유효하다는 뜻)

아래의 코드는 파라미터를 전달받는 생성자에 대해서 인스턴스를 생성하는 과정을 보여준다.

    // constructor with params
    Class clazz = Class.forName(fullQualifiedName);
   
// constructor 얻어오기
    Constructor <?> constructor = clazz.getConstructor(
        new
Class []{int.class, int.class, double.class, String[].class}
        );
   
// 파라미터 값은 constructor 에서 정의한 타입에 부합하는 값이어야한다.
    Object [] params = new Object []{
            100,
            90,
            34.9,
            new String []{"jane",  "jack"}
    };
    assertEquals( obj, constructor.newInstance( params ) );


생성자가 new ReflectionClass(int, int, double, String[]) 로 주어져 있는 경우 위처럼 생성자에 전달되는 파라미터의 타입정보를 전달해서 java.lang.reflectin.Constructor 인스턴스를 얻어낸 다음, constrocutor에 실제 값 params 을 전달해서 인스턴스를 생성한다.

<Primitive data type>

boolean, char, byte, shrot, int, long, float, double 의 기본 데이터형에 대해서 다음과 같이 타입 정보를 얻는다.

public void test_class_from_primtive_datatype() throws Exception
{
    assertEquals( byte.class, Byte.TYPE );
    assertEquals( int.class, Integer.TYPE );
    assertEquals( double.class, Double.TYPE );
    // primitive data type has no constructor.
    // So it is not possible to create instance from Class.
     // Instead use a wrapper class for the primitive data type.
    assertEquals( 0, byte.class.getConstructors().length );
    assertEquals( 0, int.class.getConstructors().length );
    assertTrue( Byte.class.getConstructors().length > 0 );
}

primitive type인 byte와 byte의 wrapper class인 java.lang.Byte는 서로 다른 타입임을 알아야한다. 예를 들어서

   
assertEquals( byte.class, Byte.class );

는 실패한다. 이를 위해서 각 wrapper class에 TYPE 이라는 필드를 두고 있는데 위의 테스트 코드에서 확인할 수 있듯이 primitive data type과 일치한다.(
primitive data type 의 경우 생성자에 전달되는 파라미터의 타입을 전달할 때 조심해야한다.)

<primitive_type>.class 에서는 아무런 constructor도 정의되어 있지 않다. 즉 new Instance()와 같이 데이터값을 얻어낼 수 없다. 대신에 Wrapper class들(Byte, Integer, Double 등)을 이용해서 동적으로 인스턴스의 생성이 가능하다.


< Class Modifier 얻어오기>

modifier는 다음과 같이 몇가지로 나뉜다.

* public, protected, defualt, private
* static
* final
* abstract
* strictft
* annotation ( 1.5 에서 도입됨)

아래와 같은 샘플 클래스가 주어졌을때

public class
ReflectionClass extends SuperClass
implements Runnable, ChangeListener{
    ...
   
public void stateChanged( ChangeEvent e ){...}
    public static class PublicStaticClass{}
}

modifier 정보를 얻는 테스트코드는 아래와 같다.

    public void test_class_modifier() throws Exception
    {
        assertEquals( "public", Modifier.toString(
            ReflectionClass.class.getModifiers()));
        assertEquals( "public static", Modifier.toString(
             ReflectionClass.PublicStaticClass.class.getModifiers() ) );
    }

이해를 쉽게 하기위해서 modifier 정보를 String 값으로 보여주었지만 Modifier.toString(int) 가 반환하는 String 은 jvm 구현에 의존적이므로 실제로는 각 modifier의 int 값을 확인하는 것이 더 정확하다.
 
java.lang.reflect.Modifier
public static final int ABSTRACT 1024
public static final int FINAL 16
public static final int INTERFACE 512
public static final int NATIVE 256
public static final int PRIVATE 2
public static final int PROTECTED 4
public static final int PUBLIC 1
public static final int STATIC 8
public static final int STRICT 2048
public static final int SYNCHRONIZED 32
public static final int TRANSIENT 128
public static final int VOLATILE 64

Class.getModifier() 는 정수값을 반환하는데 테이블에 정의된 값들의 | 연산에 의해서 표현된다. pubic static 이라면 0x09 (= 0x01 | 0x08 ) 의 값이 반환된다.

<구현된 interface의 추출>

ReflectionClass는 두개의 인터페이스를 구현하고 있다(java.lang.Runnable, javax.swing.event.ChangeListener). 구현된 인터페이스들에 대한 정보는 다음과 같이 얻어올 수 있다.

    public void test_retrieving_interface() throws Exception
    {
        Class <?>[] interfaces = ReflectionClass.class.getInterfaces();
        assertEquals(2, interfaces.length);
        assertEquals(Runnable.class, interfaces[0]);
        assertEquals(ChangeListener.class, interfaces[1]);
    }


인터페이스 역시 그 구현체는 클래스이므로 반환값도 당연히 java.lang.Class이다. 이를 통해서 인터페이스의 상수 필드와 메소드를 알아낼 수가 있다. (동적으로 코드를 생성하는 경우 이런 인터페이스 정보를 이용하는 경우가 많다. )

< superclass 추출 >
Class.getSuperClass()로 상속하고 있는 부모 클래스의 타입 정보를 얻는다.

< getName(), getCanonicalName() >

두 메소드 모두 클래스의 이름을 반환하는데 반환값이 다르다. Class.getName() 은 자바의 스펙에 규정된 이름을 반환하는데 이는 사용자에게 그리 칙숙하지 않은 이름이다.(스펙을 잘 알고 있는 사람이라면 예외이지만 깔려있는 jvm에 신경쓰지 않고 단순히 프로그램만 작성하는 사람에게는 익숙치 않다.)

getCanonicalName() 은 사용자에게 잘 알려져있는 표준적인 이름을 반환한다. 간단한 사용법을 알아보기 위해서 아래의 코드를 참조하면 좋을 듯.

public void test_canonical_name() throws Exception
{
    // normal class has the same canonical name equals to the full qualified name.
    assertEquals(fullQualifiedName, ReflectionClass.class.getCanonicalName());
    // anonymous class has no canonical class name.
    assertEquals( null,
        new Runnable(){
            public void run(){}
        }.getClass().getCanonicalName() );
    assertEquals("byte", Byte.TYPE.getCanonicalName() );
     // comparison between the canonical name and the FQ name of array type.
    assertEquals("[D", new double []{}.getClass().getName());
    assertEquals("double[]",
            new double []{}.getClass().getCanonicalName() );
    assertEquals( "[L"+ fullQualifiedName + ";",
            new ReflectionClass []{}.getClass().getName() );
    assertEquals( fullQualifiedName + "[]",
            new ReflectionClass[]{}.getClass().getCanonicalName() );
}

각 데이터 타입이 클래스파일 내에서 어떻게 정의되는지에 대한 좀 더 자세한 내용은 lava.lang.Class#getName()에서 확인할 수 있다.
<>
ㅇㅇㅇㅇㅇㅇㅇㅇㅇ


Posted by yeori
,