자바의 리플렉션(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나 다른 예제를 참고해주세요.
참고
Related Posts
- Java - Unsupported class file major version 61 에러
- Java - String.matches()로 문자열 패턴 확인 및 다양한 예제 소개
- Java - 문자열 공백제거 (trim, replace)
- Java - replace()와 replaceAll()의 차이점
- Java - ArrayList 초기화, 4가지 방법
- Java - 배열 정렬(Sorting) (오름차순, 내림차순)
- Java - 문자열(String)을 비교하는 방법 (==, equals, compare)
- Java - StringBuilder 사용 방법, 예제
- Java - 로그 출력, 파일 저장 방법 (Logger 라이브러리)
- Java IllegalArgumentException 의미, 발생 이유
- Java - NullPointerException 원인, 해결 방법
- Seleninum의 ConnectionFailedException: Unable to establish websocket connection 해결
- Java - compareTo(), 객체 크기 비교
- Java - BufferedWriter로 파일 쓰기
- Java - BufferedReader로 파일 읽기
- Java charAt() 함수 알아보기
- Java - BigInteger 범위, 비교, 연산, 형변환
- Java contains()로 문자(대소문자 X) 포함 확인
- Java - Set(HashSet)를 배열로 변환
- Java - 문자열 첫번째 문자, 마지막 문자 확인
- Java - 문자열 한글자씩 자르기
- Java - 문자열 단어 개수 가져오기
- Java - 1초마다 반복 실행
- Java - 배열을 Set(HashSet)로 변환
- Java - 여러 Set(HashSet) 합치기
- Java - 명령행 인자 입력 받기
- Java - 리스트 역순으로 순회, 3가지 방법
- Java - 특정 조건으로 리스트 필터링, 3가지 방법
- Java - HashMap 모든 요소들의 합계, 평균 계산
- Java - 특정 조건으로 HashMap 필터링
- Java - 싱글톤(Singleton) 패턴 구현
- Java - 숫자 왼쪽에 0으로 채우기
- Java - String 배열 초기화 방법
- Java - 정렬된 순서로 Map(HashMap) 순회
- Java - HashMap에서 key, value 가져오기