본문 바로가기

Java

Reflection, JDK 동적 프록시, CGLIB

리플렉션

자바 리플렉션은 우리가 컴파일 타임에 알 수 없는 정보들( 클래스, 인터페이스, 메서드, 필드 등)에 대해 런타임에 동적으로 접근하고 수정할 수 있게 해준다.

예제

  • 리플렉션이 어떤 상황에서 필요한지를 살펴보자.
  • 아래와 같이 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에서는 클래스 기반으로 상속을 사용하기 때문에 몇 가지 제약이 있다.
    1. 부모 클래스의 기본 생성자가 필요하다. (자식 클래스를 동적으로 생성하기 위해)
    2. 클래스에 final 키워드가 붙으면 상속이 불가능하다.
    3. 메서드에 final 키워드가 붙으면 해당 메서드를 재정의할 수 없다.

Reference