[Java] Reflection
리플렉션이란?
리플렉션은 구체적인 클래스 타입을 알지 못해도, 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API
이렇게 간단한 문장이지만 저 같은 초보 개발자들은 '내가 작성한 코드인데 왜 몰라?'라는 생각이 들며 헷갈릴 수 있습니다. 이 문장을 이해하려면 리플렉션을 언제, 왜 사용해야 하는지 알아야 합니다.
자바는 정적인 언어라 부족한 부분이 많은데 이 동적인 문제를 해결하기 위해서 리플렉션을 사용합니다.
여기서 정적 언어는 컴파일 시점에 타입을 결정하는 언어, 동적 언어는 런타임 시점에 타입을 결정하는 언어를 뜻합니다.
정적 언어에는 대표적으로 Java, C, C++ 등이 있고, 동적 언어에는 Javascript, Python, Ruby 등이 있습니다.
자바에서 리플렉션이 가능한 이유
자바에서 리플렉션이 가능한 이유를 살펴보려면 자바의 컴파일 과정과 JVM에 대해 알아야 합니다.(참고 링크)
자바 소스 코드는 컴파일러에 의해 바이트 코드로 변환이 되고, 클래스 로더에 의해 Runtime Data Area의 클래스 영역이라는 메모리 공간에 저장됩니다.
결국 클래스 영역에는 클래스를 구성하는 각각의 인스턴스 멤버, 정적 멤버, 클래스, 상위 클래스에 대한 정보 등 클래스를 위한 모든 정보를 갖고 있습니다. 그렇게 때문에 런타임 시점에 클래스에 대한 정보를 활용해서 객체를 동적으로 생성할 수 있는 것입니다.
간단한 사용법
리플렉션은 다음과 같은 정보를 가져올 수 있습니다. 이 정보들을 가져와 객체를 생성하거나 메서드를 호출하거나 변수의 값을 변경할 수 있습니다.
- 클래스
- 생성자
- 메서드
- 필드
리플렉션을 적용할 Person 클래스를 만들었습니다.
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public int method(int n) {
return n;
}
}
클래스 찾기
Class 객체는 클래스 또는 인터페이스를 가리킵니다. 다음 코드를 보시면 Person.class처럼 클래스 정보를 할당할 수 있습니다. Class 객체에서 제공하는 getName() 메서드를 사용해 클래스의 이름을 찾을 수 있습니다.
Class clazz = Person.class;
System.out.println("Class name: " + clazz.getName());
//결과
Class name: test.Person
위의 예제는 IDE에서 클래스를 알고 있다는 전제에 사용할 수 있습니다.
만약 클래스를 참조할 수 없고 이름만 알고 있으면 다음과 같이 클래스 이름을 찾을 수 있습니다.
Class clazz2 = Class.forName("test.Person");
System.out.println("Class name: " + clazz2.getName());
//결과
Class name: test.Person
생성자 찾기
다음 코드는 클래스로부터 생성자를 가져오는 코드입니다. getDeclaredConstructor()는 인자 없는 생성자를 가져옵니다. getDeclaredConstructor()에 인자를 넣으면 그 타입과 일치하는 생성자를 찾습니다.
Class clazz = Class.forName("test.Person");
Constructor constructor = clazz.getDeclaredConstructor();
System.out.println("Constructor: " + constructor.getName());
//결과
Constructor: test.Person
getDeclaredConstructors()는 클래스의 모든 생성자를 찾아줍니다.
Constructor constructors[] = clazz.getDeclaredConstructors();
for (Constructor cons : constructors) {
System.out.println("Get constructors in Person: " + cons);
}
//결과
Get constructors in Person: public test.Person()
Get constructors in Person: public test.Person(java.lang.String,int)
메서드 찾기
다음 코드는 이름으로 메서드를 찾는 코드입니다. Class 객체에서 제공하는 getDeclaredMethod()는 인자로 파라미터 정보를 넘겨주면 일치하는 메서드를 찾아줍니다.
Class clazz = Class.forName("test.Person");
Method method = clazz.getDeclaredMethod("method", int.class);
System.out.println("Find out method in Person: " + method);
//결과
Find out method in Person: public int test.Person.method(int)
모든 메서드를 찾으려면, 다음과 같이 getDeclaredMethods를 사용하면 됩니다.
Class clazz = Class.forName("test.Person");
Method methods[] = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("Get methods in Person: " + method);
}
//결과
Get methods in Person: public int test.Person.method(int)
메서드에 Declared가 들어가 있으면 Super 클래스의 정보는 가져오지 않습니다. 만약 상속받은 클래스의 메서드들도 찾고 싶으면 getMethods()를 사용하면 됩니다.
필드 찾기
생성자와, 메서드의 예제와 비슷합니다. getDeclaredField()에 전달된 이름과 일치하는 Field를 찾아줍니다.
Class clazz = Class.forName("test.Person");
Field field = clazz.getDeclaredField("name");
System.out.println("Find out name field in Person: " + field);
//결과
Find out name field in Person: public java.lang.String test.Person.name
객체에 선언된 모든 필드를 찾으려면 getDeclaredFields()를 사용하면 됩니다. 위와 동일하게 상속받은 객체의 정보는 찾아주지 않습니다. 만약, 상속받은 클래스를 포함한 public 필드를 찾으려면 getFields()를 사용하면 됩니다.
Class clazz = Class.forName("test.Person");
Field fields[] = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("Get fields in Person: " + field);
}
자세한 사용 방법은 아래의 레퍼런스를 참고하시면 됩니다.
리플렉션 활용
실제로 개발을 하다 보면 구체적인 클래스를 모를 일이 거의 없기 때문에 리플렉션을 활용할 일은 거의 없습니다.
게다가 Reflection은 마법 같은 힘을 가지고 있는 만큼 치명적인 단점들을 가지고 있기 때문에, 사용하지 않을 수 있다면 사용하지 않는 것이 좋습니다.
치명적인 단점 중 대표적으로 성능 오버헤드가 있습니다. 컴파일 타임이 아닌 런타임에 동적으로 타입을 분석하고 정보를 가져오므로 JVM을 최적화할 수 없기 때문입니다. 뿐만 아니라 직접 접근할 수 없는 private 인스턴스 변수, 메서드에 접근하기 때문에 내부를 노출하면서 추상화가 깨지게 되어 예상할 수 없는 부작용이 발생할 수 있습니다.
결론적으로 리플렉션은 애플리케이션 개발보다는 프레임워크나 라이브러리에서 많이 사용됩니다. 프레임워크나 라이브러리는 사용자가 어떤 클래스를 만들지 예측할 수 없기 때문에 동적으로 해결해주기 위해 리플렉션을 사용합니다.
실제로 intellij의 자동완성, jackson 라이브러리, Hibernate 등등 많은 프레임워크나 라이브러리에서 리플렉션을 사용하고 있습니다.
스프링 프레임워크에서도 리플렉션 API를 사용하는데 대표적으로 스프링 컨테이너의 BeanFactory가 있습니다. 빈은 애플리케이션이 실행한 후 런타임에 객체가 호출될 때 동적으로 객체의 인스턴스를 생성하는데 이때 스프링 컨테이너의 BeanFactory에서 리플렉션을 사용합니다.
Spring Data JPA에서 Entity에 기본 생성자가 필요한 이유도 동적으로 객체 생성 시 리플렉션을 활용하기 때문입니다. 리플렉션 API로 가져올 수 없는 정보 중 하나가 생성자의 인자 정보이기에, 기본 생성자가 반드시 있어야 객체를 생성할 수 있는 것입니다. 기본 생성자로 객체를 생성만 하면 필드 값 등은 리플렉션 API로 넣어줄 수 있습니다.
리플렉션을 활용 예제 - DI 프레임워크
리플렉션을 활용해 간단한 DI 프레임워크를 구현하겠습니다.
먼저 @Autowired 어노테이션을 만들어줍니다.
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoWired {
}
스프링 컨테이너의 BeanFactory와 비슷한 역할을 하는 ContainerService.class를 생성해줍니다.
public class ContainerService {
public static <T> T getObject(Class<T> classType) {
//기본 생성자로 인스턴스 생성
T instance = createInstance(classType);
Stream.of(classType.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(AutoWired.class))
.forEach(field -> {
try {
//기본 생성자로 인스턴스 생성
Object fieldInstance = createInstance(field.getType());
//필드의 접근제어자가 private인 경우 수정 가능하게 설정
field.setAccessible(true);
field.set(instance, fieldInstance);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
return instance;
}
private static <T> T createInstance(final Class<T> classType) {
try {
return classType.getConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
만든 DI 프레임워크가 올바르게 작동하는지 확인하기 위해 MemberController와 MemberService를 생성합니다.
//컨트롤러
public class MemberController {
@AutoWired
private MemberService memberService;
public void foo() {
memberService.foo();
}
}
//서비스
public class MemberService {
public void foo(){
System.out.println("call foo");
}
}
MemberController 인스턴스를 생성한 후 호출하면 정상적으로 작동하는 것을 확인할 수 있습니다.
public static void main(String[] args) {
ContainerService containerService = new ContainerService();
MemberController memberController = containerService.getObject(MemberController.class);
memberController.foo();
}
//출력 결과
call foo
참고
- https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/
- https://dublin-java.tistory.com/53
- https://codechacha.com/ko/reflection/
- https://www.baeldung.com/java-reflection
'프로그래밍 언어 > Java' 카테고리의 다른 글
[Java] Interface (0) | 2021.12.01 |
---|---|
[Java] Generic (0) | 2021.11.24 |
[Java] PermGen과 Metaspace (0) | 2021.11.03 |
[Java] ClassLoader (0) | 2021.11.02 |
[Java] Garbage Collection (0) | 2021.10.31 |
댓글