리플렉션
자바 리플렉션은 우리가 컴파일 타임에 알 수 없는 정보들( 클래스, 인터페이스, 메서드, 필드 등)에 대해 런타임에 동적으로 접근하고 수정할 수 있게 해준다.
예제
- 리플렉션이 어떤 상황에서 필요한지를 살펴보자.
- 아래와 같이 Hello 라는 클래스가 있다.
@Slf4j
static class Hello{
public String callA(){
log.info("callA");
return "A";
}
public String callB(){
log.info("callB");
return "B";
}
}
- Hello 클래스의 메서드를 호출하기 전과 후에 log를 출력하고자 한다.
- 가장 쉽게 구현하는 방법은 호출하기 전과 후에 아래와 같이 로그를 남기는 것이다.
@Test
void Reflection0() {
Hello target = new Hello();
log.info("start");
String result1 = target.callA();
log.info("result={}", result1);
log.info("start");
String result2 = target.callB();
log.info("result={}", result2);
}
- 이렇게 메서드 이렇게 모든 메서드에 로그와 관련된 코드를 추가하는데 중복이 발생한다. 이 중복을 리플렉션을 이용해 제거해보자.
@Test
void Reflection1() throws Exception {
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
log.info("start");
Method methodCallA = classHello.getMethod("callA");
Object result1 = methodCallA.invoke(target);
log.info("result1={}", result1);
log.info("start");
Method methodCallB = classHello.getMethod("callB");
Object result2 = methodCallB.invoke(target);
log.info("result1={}", result2);
}
- 먼저 java.lang.reflect 패키지에 있는 Method를 사용해서 클래스의 메서드 정보를 가져올 수 있다. 다음과 같이 메서드를 invoke() 함으로써 실행할 수 있게 되므로 이를 공통 함수로 빼낼 수 있게된다.
@Test
void reflection2() throws Exception {
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
Method methodCallA = classHello.getMethod("callA");
dynamicCall(methodCallA, target);
Method methodCallB = classHello.getMethod("callB");
dynamicCall(methodCallB, target);
}
private void dynamicCall(Method method, Object target) throws InvocationTargetException, IllegalAccessException {
log.info("start");
Object result = method.invoke(target);
log.info("result={}", result);
}
- 단점
- 런타임에 동적으로 실행하기 때문에 성능 오버헤드가 발생한다. (컴파일 타임 최적화 불가능)
- 또한 위에서 getMethod 함수를 호출 시 methodName을 파라미터로 넘길 때 값을 실수로 잘못 넘겨도 컴파일 타임에 오류가 발생하지않고 런타임에 발생하게 된다.
JDK 동적 프록시
우리가 앞서 살펴봤던 프록시(https://harrislee.tistory.com/94)를 구현할 때 직접 프록시 객체를 생성하고 이를 Bean으로 등록해줘야 했다. 이렇게 프록시가 필요한 서버 객체의 인터페이스나 구체클래스를 상속하여 매번 프록시 객체를 직접 생성하는 것은 비용이 발생하게 된다.
- 매번 프록시 객체를 개발자가 직접 생성하는 대신 동적으로 프록시를 만들어 주는 기술 중 하나인 JDK 동적 프록시를 살펴보자.
- JDK 동적 프록시는 말 그대로 런타임에 동적으로 프록시를 생성해준다.
- 특징으로는 인터페이스를 기준으로 만들기 때문에 사용하기 위해서는 인터페이스가 필수이다.
예제
- 다음과 같이 간단한 A, B interface와 그 구현체들이 있다.
public interface AInterface {
String call();
}
public class AImpl implements AInterface{
@Override
public String call() {
log.info("A 호출");
return "a";
}
}
public interface BInterface {
String call();
}
public class BImpl implements BInterface{
@Override
public String call() {
log.info("B 호출");
return "b";
}
}
- JDK 동적 프록시 기술을 사용하기 위해서는 InvocationHandler를 구현한 객체가 필요하다.
- InvocationHandler 에는 invoke라는 메서드를 재정의 해줘야 하는데 이 invoke 메서드에서 우리가 필요로 하는 공통 관심사를 구현해주면 된다. 아래의 코드에서는 method.invoke(target, args) 로 실제 메서드를 호출하고 있고 그 전과 후에 시스템 시간을 측정하여 총 소요된 시간을 출력하고 있다.
- method.invoke(target,args) 에서 target은 생성자로 의존성 주입을 받고있는데 이 객체가 실제로 호출하게 되는 객체가 된다. Object 타입으로 받아도 Reflection 기술로 인해 해당 메서드를 실행할 수 있다.
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
- 다음은 JDK 동적 프록시 기술로 실제 프록시를 생성하여 호출하는 테스트를 작성한 코드이다.
@Test
void dynamicA() {
AImpl target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
AInterface proxy = (AInterface)Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
@Test
void dynamicB() {
BImpl target = new BImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
BInterface proxy = (BInterface)Proxy.newProxyInstance(BInterface.class.getClassLoader(), new Class[]{BInterface.class}, handler);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
- Proxy.newProxyInstance(ClassLoader loader, Class\<?>[] interfaces, InvocationHandler h)
- 정적 생성자 메서드로 프록시를 생성하는 코드이다.
- 첫 번째 parameter에는 proxy 클래스를 정의하는 클래스 로더가 필요하다.
- 두 번째 parameter에는 proxy 클래스를 구현하기 위한 interface의 리스트가 들어간다.
- 세 번째는 메서드의 호출을 해주는 InvocationHandler가 들어가게 된다.
- 아래의 그림과 같이 프록시를 직접 만들었을 때는 모든 객체들에 대해 프록시를 구현해줘야 했었는데 JDK 동적 프록시 기술을 사용하면 프록시를 동적으로 생성해주고 우리는 메서드를 실행해주고 부가 기능을 추가해주는 InvocationHandler만 만들어 주면된다.
- 한계
- JDK 동적 프록시는 Interface가 필수적으로 필요하다.
- 그렇다면 인터페이스가 없는 구체 클래스들에 대해서는 어떻게 동적프록시를 적용할 수 있을까?
CGLIB (Code Generator Library)
CGLIB은 바이트 코드를 조작하여 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다. CGLIB를 사용하면 인터페이스가 없는 구체 클래스들에 대해서도 동적프록시를 적용할 수 있다.
package org.springframework.cglib.proxy;
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy
proxy) throws Throwable;
}
- CGLIB에서 동적 프록시를 생성하기 위해서는 앞서 살펴본 JDK 동적 프록시의 InvocationHandler와 비슷한 기능을 하는 MethodInterceptor를 구현해주어야 한다.
- 아래의 구체클래스에 대해 CGLIB을 사용하여 호출 시간을 측정하는 Proxy를 구현해보자
public class ConcreteService {
public void call() {
log.info("ConcreteService 호출");
}
}
public class TimeMethodInterceptor implements MethodInterceptor {
private final Object target;
public TimeMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = methodProxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
- Enhancer라는 객체를 사용해서 아래와 같은 방법으로 proxy를 만들어 준다.
- enhancer.setSuperclass 메서드를 통해 프록시 객체가 상속할 클래스를 지정한다.
- enhancer.setCallback 메서드를 통해 직접 구현한 MethodInterceptor를 콜백으로 넣어준다.
- create 함수를 호출하여 프록시를 생성한다.
@Test
void cglib() {
ConcreteService target = new ConcreteService();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class);
enhancer.setCallback(new TimeMethodInterceptor(target));
ConcreteService proxy = (ConcreteService)enhancer.create();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
}
- CGLIB에서는 클래스 기반으로 상속을 사용하기 때문에 몇 가지 제약이 있다.
- 부모 클래스의 기본 생성자가 필요하다. (자식 클래스를 동적으로 생성하기 위해)
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- 메서드에 final 키워드가 붙으면 해당 메서드를 재정의할 수 없다.
Reference
- https://docs.oracle.com/javase/tutorial/reflect/
- https://www.baeldung.com/java-reflection
- 스프링 핵심 원리 - 고급편, 김영한
'Java' 카테고리의 다른 글
자바 예외 이해 (Checked, Unchekced) (0) | 2022.06.18 |
---|---|
Garbage Collector (정의와 종류) (0) | 2022.06.06 |
Java String이란? (StringBuilder, StringBuffer, String Iterning, String pool) (0) | 2022.06.04 |
JVM (Java Virtual Machine)과 메모리 영역 (0) | 2022.06.02 |
Effective Java - Item 42. 익명 클래스보다는 람다를 사용하라 (0) | 2021.06.19 |