리플렉션은 스프링 내부에서 많이 사용되고 있다. 특히 NEXTSTEP 에서 수강한 '만들면서 배우는 Spring 3기' 수업에서 DI를 구현하면서 내부에서 리플렉션이 어떻게 사용되고 있는지 알아봤었다.
그리고 실무에서도 종종 사용했었다. 예를 들어 JdbcTemplate 에 사용될 쿼리문을 하드코딩 하지 않고, 런타임 중에 클래스 정보에 접근해서 동적으로 만드는 것이 필요할때도 사용 했었고, 엑셀 추출시 각 클래스들의 필드 명이 필요할 때 이를 어떤 클래스가 들어오든 동적으로 다 수용가능하도록 내부적으로 클래스를 분석할 때에도 사용했었다.
하지만 리플렉션에 대해서 정확히 무엇이다 라고 정의를 내리고 사용해오진 않았던 것 같다. 이번 강의로 정의를 정확히 찾아봤다.
여러 레퍼런스에서 다양한 정의를 내리고 있는데 나에게 가장 직관적이었던 단어는 '기법' 이었다. 어떤 곳에서는 API 라고 하기도 하고 벨덩에서는 아예 특정 단어로 개념을 구분짓지도 않고 있다.
In this tutorial, we will explore Java reflection, which allows us to inspect and/or modify runtime attributes of classes, interfaces, fields and methods.
Reflection is a feature in the Java programming language. It allows an executing Java program to examine or "introspect" upon itself, and manipulate internal properties of the program. For example, it's possible for a Java class to obtain the names of all its members and display them.
Reflection is an API that is used to examine or modify the behavior of methods, classes, and interfaces at runtime.
정리하자면 리플렉션은 런타임에 클래스 자체에 대한 정보들을 가져오고, 해당 클래스가 가진 자원들을 활용하고 해당 클래스를 조작할 수 있게 해주는 자바의 기법이라고 할 수 있다.
리플렉션 원리
따로 리플렉션 원리가 어떻다 라고 강의에서 구분짓고 있지는 않은데, 내가 필요하다고 판단하여 따로 정리를 한다. 리플렉션을 사용할때 일차적으로 클래스 타입으로 접근을 한다. 아래와 같이 클래스 타입을 할당 할 수 있다.
내가 잘못 알고 있었던 것은 '저게 어떻게 가능한 것인가' 였다. 나는 바이트 코드에 접근하는 줄 알았는데 완전히 잘못알고 있었다.
강의에서 설명하기를 클래스 로딩이 완료된 시점에 힙에 클래스 타입이 적재가 되고, 위와 같은 방식을 사용해서 힙에 할당된 그 클래스 타입에 접근을 할 수 있다는 것이었다.
어노테이션과 리플렉션
리플렉션을 이용해서 어노테이션에 접근을 할 때 영향을 주는 중요한 어노테이션들이 있는데 아래와 같다.
@Retention: 해당 애노테이션을 언제까지 유지할 것인가? 소스, 클래스, 런타임
@Inherit: 해당 애노테이션을 하위 클래스까지 전달할 것인가?
@Target: 어디에 사용할 수 있는가?
'만들면서 배우는 Spring 3기' 정리 노트에도 계속 다룰 예정이고 위에 표시한 의미 그대로라서 특별히 더 풀어서 정리할 부분은 없다.
리플렉션 실습(간단한 예제)
'만들면서 배우는 Spring 3기' 정리 노트를 정리하면서 리플렉션이 아주 빈번하게 많이 사용된 코드들을 정리할 예정이라서 지금 정리하는 것이 큰 의미는 없는데, 과거에 이 강의를 들으면서 사용했던 코드들이 있어서 아래에 남긴다.
public class Book {
private String author;
private String title;
public Book() {
}
public Book(String author, String title) {
this.author = author;
this.title = title;
}
public String getAuthor() {
return this.author;
}
public String getTitle() {
return this.title;
}
public void setAuthor(String author) {
this.author = author;
}
public void setTitle(String title) {
this.title = title;
}
public String getBookInformation() {
return "author : " + this.author + " // " + "title : " + this.title;
}
}
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class App {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<?> book = Class.forName("Book");
Constructor<?> bookConstructor = book.getConstructor();
Book myBook = (Book) bookConstructor.newInstance();
myBook.setAuthor("J. K. Rowling");
myBook.setTitle("Harry Potter");
System.out.println(myBook.getBookInformation());
}
}
8:35:57 오후: Executing task 'App.main()'...
> Task :compileJava UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :App.main()
author : J. K. Rowling // title : Harry Potter
Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/6.3/userguide/command_line_interface.html#sec:command_line_warnings
BUILD SUCCESSFUL in 299ms
2 actionable tasks: 1 executed, 1 up-to-date
8:35:57 오후: Task execution finished 'App.main()'.
여기서 사용된 getConstructor() 에 대해서 잠깐 살펴보면 아래와 같다. Reflection 이 사용되고 있음을 확인할 수 있다.
@CallerSensitive
public Constructor<T> getConstructor(Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException
{
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkMemberAccess(sm, Member.PUBLIC, Reflection.getCallerClass(), true);
}
return getReflectionFactory().copyConstructor(
getConstructor0(parameterTypes, Member.PUBLIC));
}
리플렉션 실습(커스텀 어노테이션 구현)
@Autowired 를 약식으로 구현한 샘플 코드이다. 2년 전쯤에 이 강의를 들을때 실습한 코드를 그대로 남긴다.
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
public class ContainerService {
public static <T> T getObject(Class<T> classType) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
T createdObject = createObject(classType);
Arrays.stream(classType.getDeclaredFields())
.forEach(field -> {
if (field.getAnnotation(Inject.class) != null) {
try {
Object fieldInstance = createObject(field.getType());
field.setAccessible(true); // public, private 무관하게 set 이 가능하도록 처리
field.set(createdObject, fieldInstance);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) {
throw new RuntimeException();
}
}
});
return createdObject;
}
private static <T> T createObject(Class<T> classType) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
return classType.getConstructor().newInstance();
}
}
import org.junit.Assert;
import org.junit.Test;
public class ContainerServiceTest {
@Inject
MyService myService;
@Test
public void diTest() {
MyService myService = ContainerService.getObject(MyService.class);
Assert.assertNotNull(myService);
Assert.assertNotNull(myService.getMyRepository());
}
}
리얼 이런 식으로 사용되진 않고 실습해보는 것에 의미가 있는 코드이다. 리플렉션을 본격적으로 활용하는 부분은 '만들면서 배우는 Spring 3기' 정리 노트에 남길 예정이다.