Java

[Java] Reflection

림 림 2021. 9. 5. 20:19
반응형

자바빈에 대해 공부하다가 자바빈은 디폴트 생성자를 가지고있어야 한다는 것을 알게 되었습니다. 그 이유는 툴이나 프레임워크에서 리플렉션을 이용하여 객체를 생성하기 때문이라고 했습니다.

저는 리플렉션이 대략적으로 클래스에 대한 정보를 알아내는 기술이라고 알고 있었기 때문에, 리플렉션을 통해 어떻게 객체를 생성하는 것인지 궁금했습니다. 그래서 리플렉션에 대해 더 자세히 공부해보기로 했습니다.

아래의 오라클 문서를 참고하여 공부하였습니다.

https://www.oracle.com/technical-resources/articles/java/javareflection.html

 

 

Index

1. Reflection이란?

2. 예제 코드

3. Reflection을 위한 설정

4. instanceof 연산자 기능 실행하기: isInstance

5. 클래스의 멤버 메소드 알아내기: getDeclaredMethods

6. 클래스의 생성자 알아내기: getDeclaredConstructors

7. 클래스의 멤버 필드 알아내기: getDeclaredFields

8. 메소드 실행하기: invoke

9. 새로운 객체 생성하기: newInstance

 

 

 

1. Reflection이란?

리플렉션은 자바에서 제공하는 기능 중에 하나입니다. 리플렉션은 실행 중인 자바 프로그램이 프로그램 자신의 정보를 알아낼 수 있고, 프로그램 자신의 속성(properties)을 조작할 수 있게 합니다.

예를 들어, 자바의 클래스는 리플렉션을 통해 자신의 모든 멤버의 이름을 알아낼 수 있고, 필드를 수정하거나 메소드를 실행할 수 있습니다. 리플렉션은 다른 언어에서는 찾아보기 힘든 자바만의 기술입니다. Pascal, C, C++에서는 이렇게 프로그램에 정의된 메소드의 정보를 알아내는 기능이 존재하지 않습니다.

리플렉션의 대표적인 사용 예는 JavaBeans입니다. JavaBeans이란, 빌더 툴을 통해 시각적으로 조작 가능한 소프트웨어 컴포넌트를 말합니다. 빌더 툴은 자바 프로그램을 실행하고 클래스들이 동적으로 로딩될 때, 리플렉션을 통해 그 클래스들에 대한 정보를 얻어냅니다.

 

 

2. 예제 코드

먼저 리플렉션이 어떻게 동작하는지 알아보겠습니다.

아래의 코드는 클래스의 이름으로 클래스에 정의된 멤버 메소드의 이름을 출력하는 코드입니다.

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.util.Stack"이라는 클래스의 이름을 입력해주고 있습니다.

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)
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() 메소드를 호출해서 그 클래스에 정의된 메소드를 받아올 수 있습니다.

이렇게 실행 중인 자바 프로그램에서 클래스의 정보를 알아내는 기술을 Reflection이라고 합니다. 그러면 본격적으로 Reflection 기술을 사용하는 방법에 대해 알아보겠습니다.

 

 

 

3. Reflection을 위한 설정

리플렉션 관련 클래스들은 java.lang.reflection 패키지에 있습니다. 이 클래스들을 활용하기 위해서는 제일 먼저 클래스에 대한 java.lang.Class 타입의 객체를 얻어야 합니다. java.lang.Class는 실행 중인 자바 프로그램에서 클래스와 인터페이스를 표현하는 클래스입니다.

Class 객체를 얻는 방법에는 세 가지가 있습니다.

1. 클래스의 이름

Class.forName 메소드를 통해 클래스의 이름으로 Class 객체를 얻을 수 있습니다.

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

forName 메소드는 다음과 같이 정의되어 있습니다. (Java 11)

@CallerSensitive
public static Class<?> forName(String className) throws ClassNotFoundException {
  Class<?> caller = Reflection.getCallerClass();
  return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

Reflection.getCallerClass 메소드를 통해 이 메소드를 호출하고 있는 클래스의 정보를 받아옵니다. 그리고 forName0 네이티브 메소드를 호출한 결과를 반환합니다.

클래스를 찾을 수 없으면 ClassNotFoundExcepion 예외를 발생시킵니다.

 

2. 기본 자료형

다음과 같은 방법으로 기본 자료형의 Class 객체를 얻을 수 있습니다.

Class<?> c = int.class;

 

3. 기본 자료형의 Wrapper 클래스

다음과 같은 방법으로 기본 자료형의 Wrapper 클래스의 Class 객체를 얻을 수 있습니다.

Class<?> c = Integer.TYPE;

TYPE 은 기본 자료형을 나타내는 Class 객체입니다. 내부적으로는 Class.getPrimitiveClass("int")를 호출한 것과 동일합니다. 불변객체이기 때문에 static final 키워드로 선언되어있는 모습을 볼 수 있습니다.

@SuppressWarnings("unchecked")
public static final Class<Integer>  TYPE = (Class<Integer>) Class.getPrimitiveClass("int");

 

 

4. instanceof 연산자 기능 실행하기: isInstance

isInstance 메소드로 Class 객체의 타입을 확인할 수 있습니다. instanceof 연산자와 같은 기능을 합니다.

다음의 코드는 true를 반환합니다.

try {
	Class<?> c = Class.forName("A");
	boolean b = c.isInstance(new A());
catch (ClassNotFoundException e) {
  // ...
}

 

 

5. 클래스의 멤버 메소드 알아내기: getDeclaredMethods

리플렉션의 가장 중요하고 기본적인 사용 중에 하나는 클래스에 정의된 메소드를 알아내는 것입니다. getDeclaredMethods 메소드는 클래스에 정의된 메소드에 대한 Method 배열을 반환합니다.

다음과 같이 사용할 수 있습니다.

Method[] methods = c.getDeclaredMethods();

 

getDeclaredMethods 메소드의 내부 코드는 다음과 같습니다.

@CallerSensitive
public Method[] getDeclaredMethods() throws SecurityException {
	SecurityManager sm = System.getSecurityManager();
	if (sm != null) {
	    checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true);
	}
	return copyMethods(privateGetDeclaredMethods(false));
}

 

 

6. 클래스의 생성자 알아내기: getDeclaredConstructors

getDeclaredConstructors 메소드로 클래스에 정의된 생성자들을 알아낼 수 있습니다.

Constructor<?>[] constructors = c.getDeclaredConstructors();

 

getDeclaredConstructors 메소드의 내부는 다음과 같습니다.

@CallerSensitive
public Constructor<?>[] getDeclaredConstructors() throws SecurityException {
  SecurityManager sm = System.getSecurityManager();
  if (sm != null) {
      checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true);
  }
  return copyConstructors(privateGetDeclaredConstructors(false));
}

 

 

7. 클래스의 멤버 필드 알아내기: getDeclaredFields

getDeclaredFields 메소드로 클래스에 정의된 필드들을 알아낼 수 있습니다.

Field fieldlist[] = c.getDeclaredFields();

 

getDeclaredFields 메소드의 내부는 다음과 같습니다.

@CallerSensitive
public Field[] getDeclaredFields() throws SecurityException {
  SecurityManager sm = System.getSecurityManager();
  if (sm != null) {
      checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true);
  }
  return copyFields(privateGetDeclaredFields(false));
}

 

 

8. 메소드 실행하기: invoke

Reflection을 통해 알아낸 Method 객체를 통해 메소드를 실행할 수 있습니다. 아래와 같이 invoke 메소드를 실행하면  됩니다.

try {
	Object[] arglist = new Object[2];
	arglist[0] = 0;
	arglist[1] = 1;

	Object retobj = methods[0].invoke("the object the underlying method is invoked from", arglist);
} catch (IllegalAccessException | InvocationTargetException e) {
	e.printStackTrace();
}

 

invoke 메소드의 내부는 다음과 같이 정의되어 있습니다. 파라미터로 메소드를 호출하는 객체와 그 메소드의 파라미터를 받고 있습니다. 

@CallerSensitive
@ForceInline // to ensure Reflection.getCallerClass optimization
@HotSpotIntrinsicCandidate
public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, IllegalArgumentException,
       InvocationTargetException
{
    if (!override) {
        Class<?> caller = Reflection.getCallerClass();
        checkAccess(caller, clazz,
                    Modifier.isStatic(modifiers) ? null : obj.getClass(),
                    modifiers);
    }
    MethodAccessor ma = methodAccessor;             // read volatile
    if (ma == null) {
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
}

 

 

9. 새로운 객체 생성하기: newInstance

getDeclaredConstructors를 통해 Constructor 객체를 얻어낼 수 있었습니다. Constructor 객체의 newInstance 메소드를 통해 새로운 객체를 생성할 수 있습니다. 파라미터로는 생성자의 파라미터를 넘겨줄 수 있습니다.

Constructor<?>[] constructors = c.getDeclaredConstructors();

try {
    Objects obj = (Objects) constructors[0].newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
    e.printStackTrace();
}

 

newInstance 메소드의 내부는 다음과 같습니다.

@CallerSensitive
@ForceInline // to ensure Reflection.getCallerClass optimization
public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
           IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        Class<?> caller = Reflection.getCallerClass();
        checkAccess(caller, clazz, clazz, modifiers);
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

 

 

 

느낀 점

Class 객체를 얻어내어 멤버 필드, 메소드, 생성자에 대한 정보를 알아내고 조작하는 방법에 대해 알아보았습니다. 코드를 직접 보고 나니 Java에서 Reflection 기능을 어떻게 제공하는지 알 수 있었습니다. 또, 이를 통해 빌더 툴이나 프레임워크에서 클래스의 정보를 알아내어 클래스의 생명주기를 관리하는 데에 활용될 수 있다는 것을 알게 되었습니다.

 

반응형