안드로이드 개념을 공부하던 중 직렬화라는 개념에 대해서 공부를 했다. 직렬화란 메모리에 올라가 있는 정보를 byte 단위의 코드로 나열하는 것이다. 이를 통해서 객체와 같은 정보를 전달할 수 있게 하는 것이다.

직렬화를 가능하게 하는 방법 중에는 Serializable과 Parcelable을 구현하는 2가지 방법이 존재한다. 그 중 Serializable은 구현은 상당히 쉬우나 속도가 느리다는 단점이 있다.

속도가 느린 이유는 내부적으로 Reflection을 사용하기 때문에 필요없는 쓰레기 객체들을 만들어내고 이를 제거하기 위해 GC가 동작해서 비용이 발생하게 된다.

그렇다면 여기서 말하는 Reflection은 무엇일까??

Reflection

객체를 통해 클래스의 정보를 분석해 내는 프로그래밍 기법을 말한다.
리플렉션은 구체적인 클래스 타입을 알지 못해도 컴파일된 바이트 코드를 통해 그 클래스의 메소드, 타입, 변수들을 접근할 수 있도록 해주는 자바 API를 말한다. 이게 무슨 의미일까?

구체적인 클래스 타입을 알지 못하면 메소드를 실행할 수 없나?

아래 코드를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
public class Car{
public void drive(){
// Do anything.
}
}

public class Main{
public static void main(String[] args){
Object car = new Car();
car.drive(); // 컴파일 에러
}
}

위에 코드 블록에서 컴파일 에러가 나는 이유는 모든 클래스의 조상 클래스인 Object라는 타입으로 Car 클래스의 인스턴스를 담을 수는 있지만 사용 가능한 메소드는 Object의 메소드와 변수들 뿐이기 때문이다. 그러니까 car 인스턴스의 메소드는 사용하지 못하는 것이다.

이런 식으로 구체적인 타입의 클래스를 모를 때 사용하는게 리플렉션이다. 그렇다면 또 의문이 생긴다. 역시 의문을 가지는 것은 아주 좋다.

내가 만드는 프로그램의 코드 흐름인데, 내가 사용할 클래스의 타입과 이름을 모르는 경우가 있을까?

그렇다. 일반적으로는 만나기 힘든 경우이다. 코드를 작성할 시점에는 어떤 타입의 클래스를 사용할지 모르는 경우가 있다. 내가 지금 작성하는 코드가 나중에 어떤 기능이 추가되어서 어떻게 필요에 따라 사용될지 모르는 경우 같은 것이다. 이럴 때는 실행할 시점, 그러니까 런타임에 지금 실행되고 있는 클래스를 가져와서 실행을 해야 되는 것이다.

즉, 설계할 때는 사용될 클래스가 어떤 타입인지 모르지만 리플렉션을 이용해서 코드를 일단 작성하고 실행 시점에 확인해서 활용할 수 있도록 하는 메커니즘이다.

그렇다면 어떻게 이게 가능한 것일까??

자바 클래스 파일은 바이트 코드로 컴파일 되어 static한 영역에 위치하게 된다. 때문에 클래스 이름만 알고 있다면 언제든, 이 영역을 뒤져서 클래스에 대한 정보를 가져올 수 있는 것이다. 아래는 가져올 수 있는 정보들이다.

  • ClassName
  • Clas Modifiers(public, private, synchronized 등)
  • Package Info
  • Superclass
  • Implemented Interfaces
  • Constructors
  • MethodsFields
  • Annotations

간단한 예제

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

1
2
3
4
5
6
7
8
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가지 스텝

  1. 클래스 Class 객체를 얻는다.
1
2
3
Class c1 = Class.forName("java.lang.String");
Class c2 = int.class;
Class c3 = Integer.Type;
  1. getDeclaredMethods()와 같은 메소드를 호출하여 클래스 내에 정의된 메소드를 모두 가져올 수 있다.
1
Method[] m = c1.getDeclaredMethods();
  1. 리플렉션 API를 사용하여 정보를 조작 및 얻기.
1
2
3
Class c = Class.forName("java.lang.String");
Method m[] = c.getDeclaredMethods();
System.out.println(m[0].toString());

첫번째 메소드의 이름을 출력하게 된다.

사용해보기

instanceof 연산자 모의실험 해보기

클래스 정보를 얻고 나면 클래스 객체에 대한 정보도 얻을 수 있다. Class.isInstance 메소드는 instanceof 연산자를 시뮬레이팅 할 수 있다.

1
2
3
4
5
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

b1에는 String 클래스인 cls가 3과 같은 타입이 아님을 알려주고 b2에는 String 클래스인 cls가 Test와 같은 타입임을 알려준다.

클래스의 메소드 찾기

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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());
}

생성자 정보 얻기

메소드를 찾는 방법과 비슷하다. 참고로 생성자는 반환 타입이 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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());

클래스 필드 찾기

클래스에 정의된 데이터 필드 또한 찾는게 가능하다. 접근 제어자(modifier)를 알 수 있다. 그리고 private 필드도 찾을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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());
}

메소드 이름으로 실행하기

메소드 이름으로 특정 메소드를 실행하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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한 메소드도 찾았는데 실행은 못시킬까??

결론부터 말하자면 가능하다.

1
2
3
4
5
6
7
public class A{
public static final String TAG = A.class.getSimpleName();

public void show(){
Log.e(TAG,"Hello I am Private method");
}
}
1
2
3
4
5
6
7
8
9
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()를 선언하면 다른 클래스에서는 이 클래스로 만든 인스턴스로는 show() 메소드에 접근이 불가능하다. 하지만 리플렉션은 이를 가능하게 해준다.

getMethod() 메소드는 public한 메소드를 가지고 오며, getDeclaredMethod()는 private한 메소드를 포함한 클래스에 선언된 모든 메소드를 가지고 온다. setAccessible을 true로 설정하여 private한 메소드에 접근할 수 있다.

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

리플렉션으로 오브젝트를 생성하는 예는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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) );
}
}
1
2
3
4
5
6
7
8
9
10
11
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 생성자와 같이 객체를 생성할 수 있다.

필드의 값 변경하기

필드의 값 또한 리플렉션을 이용한다면 변경시킬 수 있다.

1
2
3
4
5
6
7
8
9
10
11
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());
}

set~~() 메소드를 이용하여 필드의 값을 변경시킬 수 있다.

결론

실제로 리플렉션은 비용이 큰 작업이므로 신중하게 사용해야 한다. 나는 안드로이드 애플리케이션 개발을 하면서 리플렉션을 사용해 본 적은 없지만 이렇게 개념적인 부분을 알게 되어서 너무 좋았다. 참고할 자료들이 많아서 개념을 잘 잡을 수 있었다.

참고