Java - Reflection 쉽고 빠르게 이해하기

자바의 리플렉션(Reflection)은 클래스, 인터페이스, 메소드들을 찾을 수 있고, 객체를 생성하거나 변수를 변경할 수 있고 메소드를 호출할 수도 있습니다. Reflection은 자바에서 기본적으로 제공하는 API입니다. 사용방법만 알면 라이브러리를 추가할 필요 없이 사용할 수 있습니다.

안드로이드에서 Hidden method를 호출할 때 Reflection을 사용할 수 있습니다. SDK에 API가 공개되지 않은 경우 Android Studio에서 참조할 수 없어 호출할 수 없지만, 실제로 hidden API가 존재하기 때문에 리플렉션을 이용해서 호출할 수 있습니다.

또는, 테스트 코드 작성을 위해 private 변수를 변경할 때 리플렉션을 사용할 수 있습니다. 3rd party 라이브러리를 사용하고 이것의 private 변수를 변경하고 싶을 때 리플렉션을 사용하면 라이브러리 코드 변경없이 값을 변경할 수 있습니다.

Reflection은 다음과 같은 정보를 가져올 수 있습니다. 이 정보를 가져와서 객체를 생성하거나 메소드를 호출하거나 변수의 값을 변경할 수 있습니다.

  • Class
  • Constructor
  • Method
  • Field

Reflection은 사용할 때마다 헷갈리는 API인데, 몇번 연습해보면 Reflection에 익숙해질 수 있습니다. 먼저 Reflection으로 클래스 등의 정보를 가져오는 방법을 알아보고, 그 다음에 메소드 호출 및 변수를 변경하는 방법에 대해서 알아보겠습니다.

준비

튜토리얼을 따라하시기 전에 리플렉션을 적용할 클래스를 미리 만들어 두어야 합니다. 자바 라이브러리에 리플렉션을 적용할 수 있지만, 만들어둔 클래스를 보면 이해가 쉽기 때문에 두개의 클래스를 만들었습니다. 다음과 같이 Child와 Parent 클래스를 만들었습니다. Child는 Parent 클래스를 상속합니다.

Parent.java

package test;

public class Parent {
    private String str1 = "1";
    public String str2 = "2";

    public Parent() {
    }

    private void method1() {
        System.out.println("method1");
    }

    public void method2(int n) {
        System.out.println("method2: " + n);
    }

    private void method3() {
        System.out.println("method3");
    }
}

Child.java

package test;

public class Child extends Parent {
    public String cstr1 = "1";
    private String cstr2 = "2";

    public Child() {
    }

    private Child(String str) {
        cstr1 = str;
    }

    public int method4(int n) {
        System.out.println("method4: " + n);
        return n;
    }

    private int method5(int n) {
        System.out.println("method5: " + n);
        return n;
    }
}

Test.java

Test 클래스의 main에서 reflection을 사용할 것입니다. 아래와 같이 다음 3개의 클래스를 import해야 합니다.

  • java.lang.reflect.Method
  • java.lang.reflect.Field
  • java.lang.reflect.Constructor
package test;

import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.Constructor;

class Test {

    public static void main(String args[]) throws Exception {

    }
}

Class 찾기

클래스 Class 객체는 클래스 또는 인터페이스를 가리킵니다. java.lang.Class이며 import하지 않고 사용할 수 있습니다.

다음 코드를 보시면 Child.class처럼 클래스 정보를 할당할 수 있습니다. Class객체는 여러 메소드를 제공하며 getName()은 클래스의 이름을 리턴합니다. 코드 실행 결과는 주석으로 적었습니다.

Class clazz = Child.class;
System.out.println("Class name: " + clazz.getName());

Output:

Class name: test.Child

위의 예제는 IDE에서 클래스를 알고 있다는 전제에 사용할 수 있었습니다. 만약 클래스를 참조할 수 없고 이름만 알고 있다면 어떻게 클래스 정보를 가져와야할까요?

다음 코드는 클래스 이름만으로 클래스 정보를 가져옵니다. Class.forName()에 클래스 이름을 인자로 전달하여 클래스 정보를 가져올 수 있습니다. 패키지 네임이 포함된 클래스 이름으로 써줘야 합니다.

Class clazz2 = Class.forName("test.Child");
System.out.println("Class name: " + clazz2.getName());

Output:

Class name: test.Child

Constructor 찾기

클래스를 찾았으면, 이제 생성자(Constructor)를 찾아보시죠.

다음 코드는 클래스로부터 생성자를 가져오는 코드입니다. getDeclaredConstructor()는 인자 없는 생성자를 가져옵니다.

Class clazz = Class.forName("test.Child");
Constructor constructor = clazz.getDeclaredConstructor();
System.out.println("Constructor: " + constructor.getName());

Output:

Constructor: test.Child

getDeclaredConstructor(Param)에 인자를 넣으면 그 타입과 일치하는 생성자를 찾습니다.

Class clazz = Class.forName("test.Child");
Constructor constructor2 = clazz.getDeclaredConstructor(String.class);
System.out.println("Constructor(String): " + constructor2.getName());

Output:

Constructor(String): test.Child

위 두개의 예제는 어떤 인자를 받는 생성자만 찾는 코드입니다.

다음 코드는 모든 생성자를 가져옵니다. getDeclaredConstructors()는 클래스의 private, public 등의 모든 생성자를 리턴해 줍니다.

Class clazz = Class.forName("test.Child");
Constructor constructors[] = clazz.getDeclaredConstructors();
for (Constructor cons : constructors) {
    System.out.println("Get constructors in Child: " + cons);
}

Output:

Get constructors in Child: private test.Child(java.lang.String)
Get constructors in Child: public test.Child()

다음 코드는 public 생성자만 리턴해줍니다.

Class clazz = Class.forName("test.Child");
Constructor constructors2[] = clazz.getConstructors();
for (Constructor cons : constructors2) {
    System.out.println("Get public constructors in Child: " + cons);
}

Output:

Get public constructors in both Parent and Child: public test.Child()

Method 찾기

다음은 클래스에서 메소드를 찾아보겠습니다.

다음 코드는 이름으로 메소드를 찾는 코드입니다. getDeclaredMethod()의 인자로 메소드의 파라미터 정보를 넘겨주면 일치하는 것을 찾아줍니다.

Class clazz = Class.forName("test.Child");
Method method1 = clazz.getDeclaredMethod("method4", int.class);
System.out.println("Find out method4 method in Child: " + method1);

Output:

Find out method4 method in Child: public int test.Child.method4(int)

인자가 없는 메소드라면 다음과 같이 null을 전달하면 됩니다. getDeclaredMethod()으로 메소드를 찾을 때 존재하지 않는다면 NoSuchMethodException 에러가 발생합니다.

Class clazz = Class.forName("test.Child");
Method method1 = clazz.getDeclaredMethod("method4", null);

만약 인자가 두개 이상이라면 아래처럼 클래스 배열을 만들어서 인자를 넣어주면 됩니다.

Class clazz = Class.forName("test.Child");
Class partypes[] = new Class[1];
partypes[0] = int.class;
Method method = clazz.getDeclaredMethod("method4", partypes);

모든 메소드를 찾으려면, 다음과 같이 getDeclaredMethods를 사용하면 됩니다. 공통적으로 함수 이름에 Declared가 들어가면 Super 클래스의 정보는 가져오지 않습니다.

Class clazz = Class.forName("test.Child");
Method methods[] = clazz.getDeclaredMethods();
for (Method method : methods) {
    System.out.println("Get methods in Child: " + method);
}

Output:

Get methods in Child: public int test.Child.method4(int)
Get methods in Child: private int test.Child.method5(int)

getMethods()는 public 메소드를 리턴해주며, 상속받은 메소드들도 모두 찾아줍니다.

Class clazz = Class.forName("test.Child");
Method methods2[] = clazz.getMethods();
for (Method method : methods2) {
    System.out.println("Get public methods in both Parent and Child: " + method);
}

Output:

Get public methods in both Parent and Child: public int test.Child.method4(int)
Get public methods in both Parent and Child: public void test.Parent.method2(int)
Get public methods in both Parent and Child: public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
Get public methods in both Parent and Child: public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
Get public methods in both Parent and Child: public final void java.lang.Object.wait() throws java.lang.InterruptedException
Get public methods in both Parent and Child: public boolean java.lang.Object.equals(java.lang.Object)
Get public methods in both Parent and Child: public java.lang.String java.lang.Object.toString()
Get public methods in both Parent and Child: public native int java.lang.Object.hashCode()
Get public methods in both Parent and Child: public final native java.lang.Class java.lang.Object.getClass()
Get public methods in both Parent and Child: public final native void java.lang.Object.notify()
Get public methods in both Parent and Child: public final native void java.lang.Object.notifyAll()

Field(변수) 변경

다음은 클래스에서 Field(변수) 정보를 찾아보겠습니다.

생성자와, 메소드의 예제와 비슷합니다. getDeclaredField()에 전달된 이름과 일치하는 Field를 찾아줍니다.

Class clazz = Class.forName("test.Child");
Field field = clazz.getDeclaredField("cstr1");
System.out.println("Find out cstr1 field in Child: " + field);

Output:

Find out cstr1 field in Child: public java.lang.String test.Child.cstr1

객체에 선언된 모든 Field를 찾으려면 getDeclaredFields()를 사용하면 됩니다. 위에서 말한 것처럼 상속받은 객체의 정보는 찾아주지 않습니다.

Class clazz = Class.forName("test.Child");
Field fields[] = clazz.getDeclaredFields();
for (Field field : fields) {
    System.out.println("Get fields in Child: " + field);
}

Output:

Get fields in Child: public java.lang.String test.Child.cstr1
Get fields in Child: private java.lang.String test.Child.cstr2

상속받은 클래스를 포함한 public Field를 찾으려면 getFields()를 사용하면 됩니다.

Class clazz = Class.forName("test.Child");
Field fields2[] = clazz.getFields();
for (Field field : fields2) {
    System.out.println("Get public fields in both Parent and Child: " + field);
}

Output:

Get public fields in both Parent and Child: public java.lang.String test.Child.cstr1
Get public fields in both Parent and Child: public java.lang.String test.Parent.str2

Method 호출

클래스로부터 메소드 정보를 가져와, 객체의 메소드를 호출할 수 있습니다.

메소드 객체를 생성했으면, Method.invoke()로 호출할 수 있습니다. 첫번째 인자는 호출하려는 객체이고, 두번째 인자는 전달할 파라미터 값입니다. 만약 메소드가 어떤 값을 리턴하면 그 값을 받을 수 있습니다.

Child child = new Child();
Class clazz = Class.forName("test.Child");
Method method = clazz.getDeclaredMethod("method4", int.class);
int returnValue = (int) method.invoke(child, 10);
System.out.println("return value: " + returnValue);

Output:

method4: 10
return value: 10

다음 코드는 Parent의 method1()을 호출하는 예제입니다. 이 메소드는 인자가 없기 때문에, getDeclaredMethod()에 인자를 입력하지 않아도 됩니다. 그리고 getDeclaredMethod는 상속받은 클래스의 정보를 가져오지 않기 때문에 Parent에 대한 클래스 정보를 가져와야 합니다.

Child child = new Child();
Class clazz = Class.forName("test.Parent");
Method method = clazz.getDeclaredMethod("method1");
method.invoke(child);

위의 코드를 실행하면 다음과 같은 에러가 발생합니다. 이유는 method1()이 private이기 때문입니다.

Exception in thread "main" java.lang.IllegalAccessException: Class test.Test can not access a member of class test.Parent with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
	at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
	at java.lang.reflect.Method.invoke(Method.java:491)
	at test.Test.main(Test.java:93)

다음처럼 setAccessible(true)로 설정해주면 private 메소드에 접근가능하도록 변경됩니다. 실행해보면 호출됩니다.

Child child = new Child();
Class clazz = Class.forName("test.Parent");
Method method = clazz.getDeclaredMethod("method1");
method.setAccessible(true);
method.invoke(child);

Output:

method1

Field 변경

클래스로부터 변수 정보를 가져와 객체의 변수를 변경할 수 있습니다.

다음 코드는 cstr1 변수를 가져와서 값을 출력하고 변경한 뒤 다시 출력하는 예제입니다.

Child child = new Child();
Class clazz = Class.forName("test.Child");
Field fld = clazz.getField("cstr1");
System.out.println("child.cstr1: " + fld.get(child));

fld.set(child, "cstr1");
System.out.println("child.cstr1: " + fld.get(child));

Output:

child.cstr1: 1
child.cstr1: cstr1

private 변수를 수정하려면 위와 같이 setAccessible(true)로 접근 상태를 변경하면 됩니다.

Child child = new Child();
Class clazz = Class.forName("test.Child");
Field fld2 = clazz.getDeclaredField("cstr2");
fld2.setAccessible(true);
fld2.set(child, "cstr2");
System.out.println("child.cstr2: " + fld2.get(child));

Output:

child.cstr2: cstr2

Static 메소드 호출 또는 필드 변경

지금까지 일반적인 클래스와 객체에 대해서 리플렉션을 사용하는 방법을 알아보았습니다. static 메소드, 필드를 자주 접할 수 있는데요. 이런 static 메소드, 필드들은 어떻게 리플렉션으로 접근할 수 있는지 알아보겠습니다.

StaticExample.java

튜토리얼을 따라하기 위해, static 클래스를 추가해야 합니다.

package test;

public class StaticExample {
    public static String EXAMPLE = "Example";

    public static int getSquare(int num) {
        System.out.println("Get square: " + num * num);
        return num * num;
    }
}

static 메소드의 정보를 가져와서 호출해보겠습니다. 메소드 정보를 가져오는 방법은 위와 동일합니다. 다만 호출할 때 invoke()로 객체를 전달하는 인자에 null을 넣어주시면 됩니다. 그럼 static 메소드가 호출됩니다.

Class clazz = Class.forName("test.StaticExample");
Method method = clazz.getDeclaredMethod("getSquare", int.class);
method.invoke(null, 10);

Output:

Get square: 100

static 필드 정보를 가져오는 방법도 위와 동일합니다. 대신 set() 또는 get()함수를 사용할 때 객체로 전달되는 인자에 null을 넣어야 합니다.

Class clazz = Class.forName("test.StaticExample");
Field fld = clazz.getDeclaredField("EXAMPLE");
fld.set(null, "Hello, World");
System.out.println("StaticExample.EXAMPLE: " + fld.get(null));

Output:

StaticExample.EXAMPLE: Hello, World

정리

저는 메소드 호출이나 변수 변경하려는 목적으로 리플렉션을 사용하는데요. 만약 리플렉션으로 인자의 타입 등의 다른 정보들을 얻고 싶으시다면, oracle 등의 Document나 다른 예제를 참고해주세요.

참고

Loading script...

Related Posts

codechachaCopyright ©2019 codechacha