Reflection이란?

Reflection은 자바 언어의 기능중 하나로 프로그램 내부 속성을 조작 할 수 있게 합니다. 예를 들어 Java클래스가 가지고 있는 모든 멤버의 이름을 가져와서 표시 할 수 있습니다.

다른 언어에서는 이러한 기능이 보통 존재하지 않습니다. 예를 들어 파스칼, C,C++ 등의 언어에서 정의 된 함수에 대한 정보를 얻는 방법은 없습니다.

간단한 리플렉션 예제

String 클래스의 풀패스를 통해 String 이 가지고 있는 모든 메소드를 출력하는 간단한 예제입니다.

try {
    Class c = Class.forName("java.lang.String");
    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);
}

리플렉션을 사용하기 위한 3가지 스텝

첫째. 클래스 Class객체를 얻는다. 예를들면 다음과 같은 방법으로 클래스 객체를 얻을 수 있습니다.

Class c1 = Class.forName("java.lang.String"); 
Class c2 = int.class; 
Class c3 = Integer.TYPE; 

둘째. getDeclaredMethods() 와 같은 메소드를 호출 한다. 클래스내에 정의된 메소드들을 모두 가져올 수 있다. 

셋째. 리플렉션 API를 사용하여 정보를 조작하자. 

Class c = Class.forName("java.lang.String");
Method m[] = c.getDeclaredMethods();
System.out.println(m[0].toString());

위의 예제는 첫 번째 메소드의 이름을 출력하게 됩니다.

instanceof 연산자 모의실험 해보기

클래스정보를 손에 쥐고 나면 클래스 객체에 대해 몇가지 궁금한 것을 물어볼 수 있습니다. 예를들면 Class.isInstance 메소드는 instanceof 연산자를 시뮬레이팅 할 수 있습니다.

Class cls = Class.forName("java.lang.String");
boolean b1 = cls.isInstance(3);
Log.e(TAG, "b1="+b1);//b1=false
boolean b2 = cls.isInstance("Test");
Log.e(TAG, "b2="+b2);//b2=true

클래스의 메소드 찾기

리플렉션의 가장 기본적이고 가장 중요한 사용법중 하나가 바로 클래스에 정의된 메소드 찾기 입니다. 메소드를 찾는것 뿐만 아니라 메소드가 가지고 있는 파라미터 타입, Exception타입 , 반환 타입 등을 알아 낼 수 있습니다.

try {
    Class cls = Class.forName("java.lang.String");
    Method methods[] = cls.getDeclaredMethods();
    for (int i = 0; i < methods.length; i++) {
        Method m = methods[i];
        Log.e(TAG, "메소드 이름 = " + m.getName());
        Log.e(TAG, "정의된 클래스이름 = " + m.getDeclaringClass());

        Class pvec[] = m.getParameterTypes();
        for (int j = 0; j < pvec.length; j++) {
            Log.e(TAG, "인자 #" + j + " " + pvec[j]);
        }

        Class evec[] = m.getExceptionTypes();
        for (int j = 0; j < evec.length; j++) {
            Log.e(TAG, "익셉션 #" + j + " " + evec[j]);
        }

        Log.e(TAG,"return type = " + m.getReturnType());
        Log.e(TAG,"-----");
    }
}
catch (Throwable e) {
    Log.e(TAG,e.toString());
}

생성자에 대한 정보 얻기

메소드 찾는 방법과 비슷합니다. 예제를 확인해보겠습니다.

try {
    Class cls = Class.forName("java.lang.String");
    Constructor ctorlist[] = cls.getDeclaredConstructors();
    for (int i = 0; i < ctorlist.length; i++) {
        Constructor ct = ctorlist[i];
        Log.e(TAG,"생성자 이름 = " + ct.getName());
        Log.e(TAG,"정의된 클래스이름 = " + ct.getDeclaringClass());
        Class pvec[] = ct.getParameterTypes();
        for (int j = 0; j < pvec.length; j++){
            Log.e(TAG,"param #" + j + " " + pvec[j]);//생성자 파라미터
        }
        Class evec[] = ct.getExceptionTypes();
        for (int j = 0; j < evec.length; j++){
            Log.e(TAG,"exc #" + j + " " + evec[j]);//익셉션 타입
        }
        Log.e(TAG,"-----");
    }
}
catch (Throwable e) {
    Log.e(TAG,e.toString());
}

생성자는 반환 타입이 없기 때문에 반환타입은 뺐습니다.

클래스 필드 찾기

클래스에 정의된 데이터 필드 또한 찾는게 가능합니다. 예제를 확인 하겠습니다.

try {
    Class cls = Class.forName("java.lang.String");
    Field fieldlist[] = cls.getDeclaredFields();
    for (int i = 0; i < fieldlist.length; i++) {
        Field fld = fieldlist[i];
        Log.e(TAG,"필드명 = " + fld.getName());
        Log.e(TAG,"정의된클래스 = " + fld.getDeclaringClass());
        Log.e(TAG,"필드타입 = " + fld.getType());
        int mod = fld.getModifiers();
        Log.e(TAG,"접근제어자 = " + Modifier.toString(mod));
        Log.e(TAG,"-----");
    }
}
catch (Throwable e) {
    Log.e(TAG,e.toString());
}

이전 예제와 비슷하지만 한가지 새로운 점은 접근제어자(modifier)를 알수 있다는 점입니다.  한가지 재밌는 점은 private 필드도 찾을 수 있습니다.

메소드 이름으로 실행하기

지금까지의 예제는 클래스 정보와 관련된것을 찾거나 표현했다면, 이번에는 조금 다른 방법으로 리플렉션을 사용해보겠습니다. 예를 들면 메소드이름으로 특정 메소드를 실행하는 것입니다.

try {
    Class cls = Class.forName("java.lang.String");
    String data = "Hello World";//테스트용 데이터
    Method lengthMethod = cls.getMethod("length");//length()메소드를 찾는다.
    int length = (int) lengthMethod.invoke(data);//data.length() 수행
    Log.e(TAG, "length=" + length); //length=11 출력

    Method substringMethod = cls.getMethod("substring", int.class, int.class);
    //두개의 int타입이 있는 substring메소드를 가져옵니다.
    String subStr = (String) substringMethod.invoke(data,0, 5);
    //data.substring(0,5)와 같은 효과
    Log.e(TAG,"subStr="+subStr);//Hello 출력
} catch (Throwable e) {
    Log.e(TAG, e.toString());
}

그럼 한가지 의문점이 생길수 있습니다.

private한 메소드도 찾는데 invoke는 못하나?

가능합니다.

public class A {
    public static final String TAG = A.class.getSimpleName();

    private void show() {
        Log.e(TAG, "Hello I am private method");
    }
}
try {
    A a = new A();
    //메소드가 private하여 a.show() 찾을 수가 없음
    Method showMethod = a.getClass().getDeclaredMethod("show");
    showMethod.setAccessible(true); //접근 가능!
    showMethod.invoke(a);
}catch (Exception e){
    Log.e(TAG,e.toString());
}

A라는 클래스에서 private한 메소드 show()를 선언하면 다른 클래스에서는 이 클래스로 만든 인스턴스로는 메소드에 접근이 불가능합니다. 하지만 리플렉션은 이를 가능하게 해줍니다.

Note:getMethod()는 public한 메소드를 가지고 오며, getDeclaredMethod()는 private한 메소드를 포함한 클래스에 선언된 모든 메소드를 가지고 온다.

리플렉션으로 오브젝트 생성하기

리플렉션으로 오브젝트를 생성할 수 있습니다. 예제를 보겠습니다.

public class Person {

    public static final String TAG = Person.class.getSimpleName();

    String name;
    int age;

    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }

    public void sayMyName(){
        Log.e(TAG,String.format("Hello! My name is %s and I'm %d years old",name, age) );
    }
}
try {
    Class personClass = Class.forName("com.charlezz.reflection.Person");
    Constructor personConstructor = personClass.getConstructor(String.class, int.class);
    Person person = (Person) personConstructor.newInstance("Charles",20);
    person.sayMyName();

    //Hello! My name is Charles and I'm 20 years old 출력
}
catch (Throwable e) {
    Log.e(TAG,e.toString());
}

Constructor.newInstance를 이용하여 new 생성자()와 같이 객체를 생성 할 수 있습니다.

필드의 값 변경하기

필드의 값 또한 리플렉션을 이용한다면 변경시킬 수 있습니다.
위의 작성한 Person 클래스를 가지고 다시 한 번 예제를 확인해보도록 하겠습니다.

try {
    Class cls = Class.forName("com.charlezz.reflection.Person");
    Field ageField = cls.getField("age");
    Person person = new Person("Charles", 20);
    Log.e(TAG,"person.age = " + person.age);//person.age = 20
    ageField.setInt(person, 10);
    Log.e(TAG,"person.age = " + person.age);//person.age = 10
}
catch (Throwable e) {
    Log.e(TAG,e.toString());
}

setXXX()메소드를 이용하면 필드의 값을 변경 시킬 수 있다는 점을 확인하였습니다.
실제 제 나이도 10살 어려졌으면 좋겠네요.

Conclusion

오랜만에 자바코드로 예제를 작성해보았습니다. 실제로 리플렉션은 비용이 큰 작업이므로 신중하게 사용하여야 합니다. 대부분의 안드로이드 애플리케이션의 개발에 있어서 리플렉션을 직접적으로 쓸일은 많지 않지만 안드로이드(자바) 개발자라면 반드시 알고 있어야 하는 개념이므로 이번 포스팅을 작성하였습니다. 

Ps: 제 경우에는 안드로이드 BluetoothAdapter의 숨겨진 메소드인 setScanMode(int mode)를 사용하여 discoverable 상태를 지속시킨다거나, 커스텀 롬이 포팅된 기기의 API사용을 위해서 리플렉션을 사용한 경험 등이 있습니다.

 

본문의 예제는 github에서 확인 가능합니다. 감사합니다.

카테고리: Java

2개의 댓글

Jeremih · 2021년 8월 24일 9:19 오전

Parcelable찾아보다가 자바에 리플렉션이란 기능이 있는걸 첨 알았습니다. 자세한 설명 감사합니다. Serializable보다 Pacelable이 퍼포먼스가 좋은 이유를 이해했습니다.

성빈 · 2022년 5월 30일 9:52 오전

좋은 글 감사합니다.

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.