리플렉션이란 구체적인 클래스 타입을 알지 못하더라도 해당 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API를 의미합니다. 리플렉션은 '반사'라는 의미를 가지는데, 클래스 정보를 마치 거울에 반사된 것처럼 확인할 수 있다는 점에서 리플렉션이라는 이름을 가지게 되었다고 합니다.
자바 코드는 컴파일되면 바이트코드로 변환됩니다. 그리고 이런 바이트코드들은 클래스로더를 통해 JVM 내 메모리 영역에 저장되게 됩니다. 리플렉션은 이런 JVM 메모리 영역으로부터 클래스 정보를 읽어 다양한 작업을 가능하게 해 줍니다. Spring을 사용하면 흔히 접하는 @RequestMapping, DI(Dependency Injection) 컨테이너, JPA의 @Entity까지 모두 리플렉션을 통해 만들어진 기술입니다.
리플렉션을 활용하면 주석을 제외하고 거의 대부분의 정보에 접근할 수 있는데, 심지어는 private 필드나 private 메소드에도 접근이 가능합니다. 이번 아티클에서는 Spring의 GetMapping이 어떤 원리로 작동하는지 알아보면서 리플렉션을 이해해 보겠습니다. 다만 메소드 종류, 사용법과 같은 기본적인 내용은 다루지 않을 예정이니 참고해 주시면 감사하겠습니다.
GetMapping 만들어보기
스프링의 GetMapping은 다음과 같은 구조로 이루어져 있습니다. GetMapping 어노테이션 안에 내부적으로 @RequestMapping을 가지는 형태입니다. 저희는 GetMapping의 value값과 내부에 존재하는 RequestMapping의 method 정보를 이용해 핸들러 메소드가 어떤 method, URI와 매칭이 되어야 하는지 알아내보겠습니다.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
String value() default "";
}
편의를 위해 속성은 value만 지정할 수 있다고 가정합니다.
@Controller
public class MyController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
// hello 메소드가 어떤 HTTP method, Request URI와 매칭되는지 알아내보자!
1. 클래스 정보 가져오기
우선은 어노테이션이 붙은 클래스 정보를 가져와보도록 하겠습니다. 클래스 정보를 가져오는 방법은 여러 가지가 있는데, 이번에는 '클래스명.class'로 접근하는 방식을 사용할 것입니다.
Class<MyController> myControllerClass = MyController.class;
2. GetMapping이 붙은 메소드 정보 가져오기
GetMapping이 붙은 메소드 정보를 가져오도록 하겠습니다. 다음과 같이 컨트롤러 클래스로부터 getDeclaredMethods()를 호출하면 해당 클래스에 정의된 모든 메소드를 가져올 수 있습니다. 모든 메소드를 가져온 뒤, GetMapping 어노테이션이 붙은 메소드만 추출합니다.
Method[] methods = myControllerClass.getDeclaredMethods();
Method targetMethod = null;
for (Method method : methods) {
if (method.isAnnotationPresent(GetMapping.class)) {
targetMethod = method;
break;
}
}
3. GetMapping의 value 값, RequestMapping의 method 값 읽기
어노테이션이 붙은 메소드 위치를 찾아냈으니 request URI와 http method를 알아내야 합니다. http method를 알아내기 위해서는 GetMapping 어노테이션 내에 존재하는 RequestMapping 어노테이션까지 읽어야 한다는 것에 유의해주시면 좋을 것 같습니다.
GetMapping getMapping = targetMethod.getDeclaredAnnotation(GetMapping.class);
RequestMapping requestMapping = getMapping.getClass()
.getDeclaredAnnotation(RequestMapping.class);
String path = getMapping.value();
RequestMethod[] requestMethods = requestMapping.method();
간단한 예시를 통해 GetMapping이 어떻게 리플렉션을 통해 구현될 수 있는지를 알아보았습니다. 위 예제에서는 path와 requestMethod 정보만 추출했는데, 여기서 코드를 조금만 더 추가해 패키지 내의 모든 Controller를 스캔하는 것도 가능합니다. 관련해서는 제가 구현했던 코드가 있는 링크를 참고하시면 좋을 것 같습니다! (코드)
Declared
리플렉션을 사용하다보면 'Declared' 라는 키워드를 자주 마주하게 됩니다.
Class<MyController> myControllerClass = MyController.class;
myControllerClass.getDeclaredMethods();
myControllerClass.getMethods();
// 둘은 무슨 차이일까요?
DeclaredMethods의 경우에는 접근 제어자와 상관없이 상속한 메소드를 제외하고 클래스에서 직접 선언된 메소드들만 가져옵니다. 반면 Declared가 붙지 않은 'getMethods'와 같은 경우에는 상위 클래스와 상위 인터페이스에서 상속한 메소드를 모두 포함해서 public인 메소드들만 가져오게 됩니다.
다만 getDeclaredMethods라고 해서 항상 해당 클래스에 직접 정의한 메소드만 나오는 것은 아닙니다. 예를 들어 Jacoco를 사용하는 경우, 컴파일 타임에 JacocoInit이라는 static 메소드가 삽입되고 이 역시도 getDeclaredMethods의 결과로 나오게 됩니다.
장단점
리플렉션은 굉장히 유연한 기술입니다. DI 프레임워크나 어노테이션 기능을 쉽게 구현할 수 있습니다. 하지만 다음과 같은 문제점들이 존재하기도 하므로 유의해서 사용하면 좋을 것 같습니다.
- 런타임에 호출되므로 JVM 레벨에서 성능 최적화 불가능
- 컴파일 시점에 제공되는 타입 체크 기능을 사용할 수 없음
- 추상화 파괴
참고 자료
'Java' 카테고리의 다른 글
I/O Stream이란 무엇일까? (+ Stream이란?) (2) | 2023.09.04 |
---|---|
Java: 커스텀 예외 사용에 대한 생각 (0) | 2023.03.16 |
Java: Throwable 소개 & API (0) | 2023.03.07 |
Java: enum의 구현방식 알아보기 (8) | 2023.03.06 |
Java: enum 소개 및 API 파헤쳐 보기 (3) | 2023.03.05 |