Reflection
Reflect:
1. (거울·유리·물 위에 상을) 비추다
2. (빛·열·음을) 반사하다, 반향을 일으키다
반사체라는 뜻을 지니는 Reflection은 무엇을 반사한다는 것일까?
JVM에서 자바 애플리케이션을 실행하기 위해서는 소스 코드가 컴파일러를 통해 바이트 코드로 변환되고, 변환된 바이트 코드는 클래스 로더를 통해 JVM 내부의 메모리 영역에 저장되는 과정을 거친다.
과정을 거치며 Method Area에 클래스의 필드, 메서드, 인터페이스 등의 메타 정보가 저장되게 된다. Reflection은 바로 해당 JVM 내의 메서드 영역의 데이터를 참조하여 클래스를 참조하고 검사하는 기능이다. 컴파일 하기 전에 소스 코드가 클래스라면 컴파일 후에 Method Area에 저장된 해당 클래스의 정보가 바로 우리가 오늘 다룰 Reflection의 핵심이다.
컴파일 시에 미리 생성이 되는 클래스라면 유저의 입장에서 클래스가 미리 생성이 됐음을 인지할 수 있어 클래스 조작 및 검사가 쉽다. 하지만 프로그램 동작 시점인 런타임에 클래스가 생성이 된다면? 유저는 클래스의 타입, 메서드, 필드 등 클래스의 정보를 미리 알 수 없다는 단점이 있다. 클래스 로더를 통해 런타임 데이터 영역(Runtime Data Area)의 Method Area에 저장된 데이터를 활용하여 클래스의 정보를 알아내고 조작이 가능하도록 해주는 API가 바로 Reflection인 것이다.
컴파일 타임이 아닌 프로그램이 동작하는 도중인 런타임 시에 사용자와 운영체제와 상호작용을 하며 컴파일 된 클래스, 메서드, 인터페이스 등을 조작하고 검사하는 기능을 제공하는 것이 Reflection이며, 프로그램 동작 중인 런타임 시에 클래스를 검사하고 조작한다는 것이 Reflection이 지니는 가장 큰 장점이라고 할 수 있다.
Reflection을 통해 클래스, 생성자, 메서드에 대한 정보를 가져와보자.
클래스 접근 및 조작
forName()에 인자로 클래스의 이름을 직접 입력하여 해당하는 클래스를 불러오거나,
getClass()로 인스턴스의 클래스를 불러오거나,
.class을 이용하여 클래스 리터럴을 참조할 수 있다.
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
// 1. forName(): 클래스 이름을 직접 입력하여 Class 객체를 얻는 방법
Class test = Class.forName("Unknown");
System.out.println(test); // class Unknown
// 2. getClass(): 객체의 getClass() 메서드를 이용하여 Class 객체를 얻는 방법
Unknown unknown = new Unknown();
Class test2 = unknown.getClass();
System.out.println(test2); // class Unknown
// 3. .class: 클래스 리터럴을 이용하여 Class 객체를 얻는 방법
Class test3 = Unknown.class;
System.out.println(test3); // class Unknown
// 4. getSuperclass(): 상위 클래스의 Class 객체를 얻는 방법
Class test4 = Unknown.class.getSuperclass();
System.out.println(test4); // class UpperClass
}
}
class Unknown extends UpperClass {
}
class UpperClass {
}
생성자, 메서드, 필드 접근 및 조작
getXXX()의 경우 해당 클래스와 상위 클래스의 public 요소들만 가져온다.
getDelcaredXXX()의 경우 해당 클래스와 상위 클래스의 모든 요소들을 가져온다.
invoke()를 통해 클래스의 특정 메서드를 호출 가능하다.
getField()를 통해 클래스의 필드를 불러올 수 있다.
public class Main {
public static void main(String[] args) throws Exception {
Class<?> internalService = Class.forName("InternalService");
Class<?> externalService = Class.forName("ExternalService");
// getField, getDeclaredField
System.out.println(internalService.getField("publicNumber")); // InternalService.publicNumber
System.out.println(internalService.getDeclaredField("privateNumber")); // InternalService.privateNumber
System.out.println(externalService.getField("privateNumber")); // NoSuchFieldException
}
}
interface service {
void work();
void stop();
}
class InternalService implements service {
private final int privateNumber = 0;
public final int publicNumber = 1;
@Override
public void work() {
System.out.println("internalService is working");
}
@Override
public void stop() {
System.out.println("internalService is stopped");
}
}
class ExternalService implements service {
private final int privateNumber = 0;
public final int publicNumber = 1;
@Override
public void work() {
System.out.println("externalService is working");
}
@Override
public void stop() {
System.out.println("externalService is stopped");
}
}
getField 메서드로 private 필드를 가져오려고 할 경우 NoSuchFieldException이 발생하는 것을 확인할 수 있다. private 접근 제한자의 필드를 가져오기 위해서는 getDeclaredField() 메서드를 사용해야 한다.
getConstructor() 를 통해 클래스의 생성자를 불러오고 newInstance() 를 통해 새로운 인스턴스를 생성할 수 있다. 클래스의 생성자를 불러온 후 생성자를 호출하기 위해서는 setAccessible(true)를 통해 접근을 허용해줘야 한다.
invoke(Object) 를 통해 인스턴스의 메서드를 호출하는 것도 가능하다.
가장 놀라웠던 기능인데 setAccessible을 통해 final 키워드의 필드 값도 변경이 가능하다! setAccessible(true)는 대상 객체, 필드의 접근 제한자, final 키워드와 무관하게 자바가 해당하는 대상을 제어할 수 있게 해주는 메서드이기 때문이다. public 객체, 메서드, 필드의 경우에는 setAccessible 처리를 해 줄 필요가 없지만 private일 경우 setAccessible을 통해 접근 허용을 해 줘야 한다.
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// 클래스 객체를 가져온다.
Class<?> clazz = Service.class;
// 생성자를 가져온 후 호출할 수 있도록 설정한다.
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
// 생성자를 호출하여 인스턴스를 생성한다.
Object service = constructor.newInstance("test", 100);
// final 필드 모두 setAccessible(true)로 설정하여 값을 변경할 수 있다.
Field name = clazz.getDeclaredField("name");
name.setAccessible(true);
Field number = clazz.getDeclaredField("number");
number.setAccessible(true);
// 값을 변경한다.
name.set(service, "not test");
number.set(service, 200);
// 메서드 호출할 수 있도록 설정하고 호출한다.
Method print = clazz.getDeclaredMethod("print");
print.setAccessible(true);
print.invoke(service);
}
}
class Service {
private final String name;
private final int number;
private Service(String name, int number) {
this.name = name;
this.number = number;
}
private void print() {
System.out.println("name is : " + name + "\nnumber is : " + number);
}
}
setAccessible
앞서 setAccessible을 통해 데이터의 접근을 허용하면 접근 제어자, final 여부와 상관없이 값을 변경할 수 있는 것을 확인하였다. 이런 데이터에 대한 접근 허용 상태는 setAccessible(false)처리를 할 때까지 유지된다. 그렇다면 해당 접근 허용 상태를 저장하는 공간이 필요하다는 것인데 이 접근 허용 상태는 어디에 저장되는 것일까?
정답은 기존의 접근 제어자 정보를 포함하는 metaspace에 저장된다. 기본적으로 Reflection은 metaspace의 메타 데이터를 활용하여 클래스, 인터페이스, 필드, 메서드에 접근한다. 여기에는 해당 데이터들의 접근 제어자 정보도 포함되기 때문에 접근 허용 상태도 metaspace에 저장된다.
Reflection 적용 및 활용
Reflection을 활용할 수 있는 방법이 없을까?
Reflection의 가장 큰 특징이자 장점인 런타임 시점에 클래스, 메서드, 필드를 검사 및 조작에 집중하자. 컴파일 타임이 아닌 런타임에 들어오는 인자 값을 미리 검사/확인하여 코드가 동적으로 동작하도록 작성할 수 없을까?
인터페이스를 구현하는 두 클래스 InternalService, ExternalService가 있을 때, 일반적으로 코드 작성 시 if문 분기를 통해 값이 들어오는 경우의 수를 모두 처리해 주어야 한다. 코드가 길어지고 지저분하다.
public class Main {
public static void main(String[] args) throws Exception {
final String serviceName = args[0];
Service service = null;
if("class InternalService".equals(serviceName)) {
service = new InternalService("InternalService");
}
if("class ExternalService".equals(serviceName)) {
service = new ExternalService("ExternalService");
}
service.service();
}
}
interface Service {
void service();
}
class InternalService implements Service {
private String name;
InternalService(String name) {
this.name = name;
System.out.println("new InternalService");
}
@Override
public void service() {
System.out.println("InternalService working");
}
}
class ExternalService implements Service {
private String name;
ExternalService(String name) {
this.name = name;
System.out.println("new ExternalService");
}
@Override
public void service() {
System.out.println("ExternalService working");
}
}
앞서 배운 Reflection을 적용할 경우 들어오는 인자 값에 따른 경우를 모두 분기처리하지 않아도 될 뿐만 아니라 기존의 코드처럼 구현체에 의존하는 코드가 아닌 인터페이스만 의존하는 코드를 작성할 수 있다.
import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
String serviceName = args[0];
// serviceName으로 클래스를 불러온다.
Class<?> clazz = Class.forName(serviceName);
// 불러온 클래스의 기본 생성자를 가져온다.
Constructor<?> constructor = clazz.getDeclaredConstructor();
// 기본 생성자로 객체를 생성한다.
Service service = (Service) constructor.newInstance();
service.service();
}
}
References
https://www.geeksforgeeks.org/reflection-in-java/
Reflection in Java - GeeksforGeeks
Reflection in Java
www.geeksforgeeks.org
https://www.youtube.com/watch?v=RZB7_6sAtC4&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC
https://www.youtube.com/watch?v=Q-8FC09OSYg&ab_channel=%EB%B0%B1%EA%B8%B0%EC%84%A0
https://medium.com/msolo021015/%EC%9E%90%EB%B0%94-reflection%EC%9D%B4%EB%9E%80-ee71caf7eec5
자바 Reflection이란?
많은 입문용 자바 서적에서 잘 다루지 않는 Reflection이라는 개념에 대해서 알아보려고 합니다.
medium.com
'Java' 카테고리의 다른 글
[Java] Composition(컴포지션) (0) | 2024.04.09 |
---|---|
[Java] 고유 락(Intrinsic Lock) (0) | 2024.04.08 |
[Java] Object 클래스 (0) | 2024.04.07 |
[Java] Thread(스레드) (1) | 2024.04.07 |
[Java] 가비지 컬렉션(Garbage Collection) (0) | 2024.04.06 |