리플렉션이란, 간단히 말해 컴파일 시점에 타입을 결정하는 정적 언어인 Java에서, 런타임시에 구체적인 Class의 Type을 알지 못하더라도 동적으로 클래스의 메서드, 타입, 변수들에 접근하여 정보를 추출할 수 있도록 해주는 api이다. 예를들어 다음의 상황들에서 리플렉션을 사용하고 있다
Reflection의 사용
- Dynamic Binding: 코드에서 동적으로 Class를 사용할 때 사용
- Spring Framework: DI, Annotation, Test Code 작성 등에서 사용
- MVC: View에서 넘어오는 데이터를 객체에 바인딩할 때 사용
- Hibernate: @Entity 클래스에 setter가 없으면, 해당 필드에 값을 바로 주입
- IDE: 자동 완성 기능
Example
- 당신은 블로그를 개발하고 있고, 그 블로그는 테마를 정해서 글을 작성해야 한다
- 현재는 두 가지 테마 'Light Mode'와 'Dark Mode'가 존재해서, 글 작성시 switch문을 사용하여 테마를 정하게 하려고 한다
public static void main(String[] args) {
// 블로그 생성
Blog.Owner owner = Blog.Owner.blogOwner("KDH", 28);
Blog myBlog = Blog.createNewBlog(owner);
// 테마 선택 후 글 작성
Blog.Writing writing = Blog.Writing.write(selectTheme("DarkMode"), "Hello, World!");
myBlog.addWriting(writing);
}
// 테마 선택하여 글 작성
public static BlogTheme selectTheme(String themeName) {
return switch (themeName) {
case "LightMode" -> LightMode.writingWithLightMode();
case "DarkMode" -> DarkMode.writingWithDarkMode();
default -> throw new IllegalArgumentException("No such theme");
};
}
- 그런데 테마 선택의 자유도를 높이기 위해 유저들이 요청한 커스텀 테마를 추가해주려고 한다면 어떻게 해야할까?
- 'writingWithTheme' 메서드를 보면, 처음 몇 개의 테마만 존재할때는 switch문으로 가볍게 대응할 수 있을 것이다. 하지만 테마의 수가 10개, 50개, 100개가 된다면?
- if/else나 switch문이 수십, 수백개가 필요 할 것이고, 이는 객체지향의 핵심 원칙인 재사용성과 유지보수성을 저해하는 결과를 가져오게 된다
// Reflection을 사용하여 동적 바인딩
public static BlogTheme selectTheme(String themeName) {
try {
Class<?> blogThemeClass = Class.forName("reflection." + themeName);
Constructor<?> constructor = blogThemeClass.getConstructor();
return (BlogTheme) constructor.newInstance();
} catch (Exception e) {
throw new RuntimeException("No such theme: " + themeName);
}
}
- 하지만 이처럼 Reflection을 사용하여 Dynamic Binding을 한다면, 새로운 테마가 생성되더라도 'selectTheme' 메서드의 수정 없이 테마의 이름만으로 코드를 재사용할 수 있으므로 유연하게 확장 가능한 코드를 작성할 수 있다
Reflection
그렇다면 리플렉션이 어떻게 동적으로 클래스의 정보를 가져오는 것인지, 또한 어떤 메서드가 존재하는지 알아보자
Reflection 동작 과정
- JVM이 처음 실행되는 과정에서, ClassLoader가 정의된 Class의 정보(필드, 메서드, 타입 등)의 바이트 코드를 읽은 후 JVM의 Metaspace에 로드한다
- Reflection API를 사용하기 위해, 대상이 되는 Target 클래스의 'Class' 객체를 가져온다. 'Class' 클래스에는 Target 클래스의 정보(method, field, constructor 등)가 담겨져 있다
- 'Class' 객체에서 다음의 메서드들을 사용하여서 정보를 가져올 수 있다
- class, forName, newInstance, getInterfaces : Target의 Class 객체나 interface를 가져온다
- getMethod : Target의 메서드를 가져온다
- getField : Target의 필드를 가져온다
- getConstructor : Target의 생성자를 가져온다
Reflection Method
- 'Class' 객체 가져오기
public static void main(String[] args) throws Exception{
// 1. Target을 직접 사용하는 경우
DarkMode darkMode = new DarkMode();
Class<?> theme = DarkMode.class;
Class<?> theme = darkMode.getClass();
// 2. Target path만 알고 있는 경우
Class<?> theme = Class.forName("reflection.DarkMode");
}
- 'Target'의 Constructor 가져오기
public static void main(String[] args) throws Exception{
// 1. Default Constructor를 사용하는 경우
Constructor<?> constructor = theme.getDeclaredConstructor();
// 2. String 파라미터를 가진 Constructor를 사용하는 경우
Constructor<?> constructor = theme.getDeclaredConstructor(String.class);
// 3. 모든 생성자 가져오기
Constructor<?>[] constructors = theme.getDeclaredConstructors();
// 4. public 생성자만 가져오기
Constructor<?>[] constructors = theme.getConstructors();
// 생성자로 객체 생성
BlogTheme blogTheme = (BlogTheme) constructor.newInstance("KDH");
}
- 'Target'의 Method 가져오기
public static void main(String[] args) throws Exception{
// Class 객체 가져오기
Class<?> theme = DarkMode.class;
// 1. method 이름을 사용해, 파라미터가 없는 method 가져오기
Method method = theme.getDeclaredMethod("checkTheme");
// 2. method 이름을 사용해, 파라미터가 있는 method 가져오기
Method method = theme.getDeclaredMethod("updateCreator", String.class);
// 3. 모든 method 가져오기
Method[] methods = theme.getDeclaredMethods();
// 4. 상속받은 method와 public method 가져오기
Method[] methods = theme.getMethods();
// method 실행: 메서드.invoke(메서드를 호출할 객체, 메서드에 전달할 파라미터)
BlogTheme newTheme = (BlogTheme) theme.getDeclaredConstructor().newInstance();
method.invoke(newTheme, "DH");
// 접근제어자 무시하고 method 실행
method.setAccessible(true);
method.invoke(newTheme, "KDH");
}
- 'Target'의 Field값 가져오기
public static void main(String[] args) throws Exception {
// Class 객체 가져오기
Class<?> theme = DarkMode.class;
// 1. Class 객체에서, 접근제어자 무시하고 이름으로 field 가져오기
Field field = theme.getDeclaredField("themeName");
System.out.println("field = " + field.getName());
// 2. theme, theme super 객체에서, 이름으로 public field 가져오기
Field field = theme.getField("themeName");
// 3. theme 객체에서, 접근제어자 무시하고 모든 field 가져오기
Field[] fields = theme.getDeclaredFields();
// 4. theme, theme super 객체에서, 이름으로 모든 public field 가져오기
Field[] fields = theme.getFields();
}
- 'Target'의 Field값 수정하기
public static void main(String[] args) throws Exception {
// Class 생성
Class<?> theme = DarkMode.class;
Constructor<?> constructor = theme.getDeclaredConstructor();
BlogTheme blogTheme = (BlogTheme) constructor.newInstance();
// Field 가져오기
Field field = theme.getDeclaredField("themeName");
// 1. public field인 경우
field.set(blogTheme, "DarkMode");
// 2. private field인 경우
field.setAccessible(true);
field.set(blogTheme, "DarkMode");
}
단점
- 컴파일 시점에 타입 확인이 불가능하여 컴파일 시에 타입 확인이나 예외 검사를 할 수 없다. 즉 런타임 시에만 확인이 가능하므로 위험하다
- 클래스, 메서드, 필드 등을 접근하여 직접 이용하기 때문에, OOP의 특징인 추상화를 위반한다
- 불변 객체든, private field든 어떤 곳이든 접근하여 필드를 바꿀 수 있으므로 주의해야 한다
- Reflection이 생성하는 객체에 대한 정보는 JVM에 캐시되어 있지 않기 때문에, 초기 생성시 많은 자원이 필요하다(단, 한번 캐싱된 이후로는 일반 객체와 큰 차이가 없다)
- 런타임시 Java 보안 관리자에게 일부 권한을 부여 받는데, 이는 보안 취약점이 될 수 있다
주의해서 사용하도록 하자.. !
Reference
- 스프링의 DI는 어떻게 동작하는걸까?: https://velog.io/@suyeon-jin/%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%98-DI%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B1%B8%EA%B9%8C
- 리플렉션이란 무엇일까?: https://jeongkyun-it.tistory.com/225
- Reflection은 무엇이고 언제 어떻게 사용하는 것이 좋을까: https://velog.io/@alsgus92/Java-Reflection%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%96%B8%EC%A0%9C%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EA%B2%83%EC%9D%B4-%EC%A2%8B%EC%9D%84%EA%B9%8C
- 대혼돈의 질서 파괴범 Reflection API에 대해 알아보자 (Java): https://7357.tistory.com/194