HOME > java > basic

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

By JS | 14 Oct 2019

자바의 리플렉션(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());

// Class name: test.Child

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

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

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

// Class name: test.Child

Constructor 찾기

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

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

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

// Constructor: test.Child

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

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

// 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);
}

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

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

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

// 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);

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

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

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);
}

// 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);
}

// 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);

// 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);
}

// 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);
}

// 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()로 호출할 수 있습니다. 첫번째 인자는 호출하려는 객체이고, 두번째 인자는 전달할 파라미터 값입니다. 만약 메소드가 어떤 값을 리턴하면 그 값을 받을 수 있습니다.

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);

// method4: 10
// return value: 10

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

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 메소드에 접근가능하도록 변경됩니다. 실행해보면 호출됩니다.

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

// method1

Field 변경

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

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

Child child = new Child();
Class clazz = Class.forName("test.Parent");
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));

// child.cstr1: 1
// child.cstr1: cstr1

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

Field fld2 = clazz.getDeclaredField("cstr2");
fld.setAccessible(true);
fld.set(child, "cstr2");
System.out.println("child.cstr2: " + fld.get(child));

// 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);

// 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));

// StaticExample.EXAMPLE: Hello, World

정리

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

참고