korean IT student

동적 프록시 기술 - 1 본문

back-end/SPRING

동적 프록시 기술 - 1

현창이 2021. 12. 15. 16:50

Java 동적 프록시 기술을 알아보자.

  • JDK 동적 프록시
  • CGLIB 

 

먼저 JDK 동적 프록시, CGLIB 앞서 자바 리플렉션에 대해 알아보자. 

 

Reflection 이란?

  • 리플렉션은 클래스나 메서드의 메타정보를 사용해서 동적으로 호출하는 메서드를 변경할 수 있다.
  • 컴파일 시간이 아닌 실행 시간(Run Time)에 동적으로 특정 클래스의 정보를 추출해낼 수 있습니다.

아래의 예제를 보면서 이해를 해보자!!

 

import java.lang.reflect.Method;

public class Reflection {

    public static void main(String[] args) throws Exception {
        World target = new World();


        // 기존 사용 예시
        String city1 = target.city1();
        System.out.println("공통로직 : " + city1);

        String city2 = target.city2();
        System.out.println("공통로직 : " + city2);

        System.out.println(World.class.getName()); // class 경로 확인


        Class classTemp = Class.forName("Reflection$World"); // 클래스정보 추출

        // Reflection 사용예시 1
        Method method1 = classTemp.getMethod("city1"); // 메서드 추출
        method1.invoke(target); // 추출한 메서드 실행
        Object result =  method1.invoke(target);
        System.out.println("공통로직 : " + result);

        Method method2 = classTemp.getMethod("city2"); // 메서드 추출
        method2.invoke(target); // 추출한 메서드 실행
        Object result2=  method2.invoke(target);
        System.out.println("공통로직 : " + result2);

        //  Reflection 사용예시 2 > 동적메서드 사용
        Method method3 = classTemp.getMethod("city1"); // 메서드 추출
        dynamicMethod(method3, target);

        Method method4 = classTemp.getMethod("city2"); // 메서드 추출
        dynamicMethod(method4, target);


    }

    public static class World {
        public String city1() {
            return "1";
        }
        public String city2() {
            return "2";
        }
    }

    public static void dynamicMethod(Method method, Object target) throws Exception{

        Object result =  method.invoke(target);
        System.out.println("공통로직 = " + result);

    }

}
  • Reflection을 사용함으로 써 메서드를 호출할 때마다 필요한 공통 로직을 dynamicMethod를 통해 한번에 처리할 수 있다.
  • 메서드의 앞뒤로 공통 로직을 메서드로 빼는 것은 어려운데, 이를 쉽게 가능하게 한다.
    • 리플렉션은 클래스와 메서드 정보를 동적으로 가져와 메서드를 호출함으로써, 메더스의 공통 로직을 쉽게 추가할 수 있는 장점이 있다.
  •  하지만 실행 시간(Run Time)에 실행이 되므로 컴파일 오류를 잡을 수 없는 단점이 있다.
    •  정말 필요할 때만 사용하자!!

 

JDK 동적 프록시

JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다.

따라서 인터페이스가 필수이다.

즉, 인터페이스 선언에 대한 강제성이 있다는 단점이 있다.

 

아래 예제를 참고하며 JDK 동적 프록시를 알아보자.

 

인터페이스 생성

public interface Target {
    String helloTarget();
}

인터페이스 구현

public class TargetImpl implements Target {
    @Override
    public String helloTarget() {
        System.out.println("TargetImpl : 구현체입니다.");
        return "구현체입니다.";
    }
}

InvocationHandler인터페이스 구현

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class TargetHandler implements InvocationHandler {

    private final  Object target;

    public TargetHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("proxy 시작");
        Object result = method.invoke(target, args);
        System.out.println("proxy 종료");
        return result;
    }
}

Main 호출

import java.lang.reflect.Proxy;

public class DynamicProxy {

    public static void main(String[] args) {

        Target target = new TargetImpl(); // 실제 객체 생성

        TargetHandler handler = new TargetHandler(target); // 동적프록시에 적용할 핸들러에 실제 객체 주입합니다.

        // java.lang.reflect.Proxy -> 동적프록시 생성
        // 인터페이스를 구현한 클래스 정보, 인터페이스 정보, 핸들러 입력
        Target proxy = (Target) Proxy.newProxyInstance(Target.class.getClassLoader(), new Class[]{Target.class},handler);

        // 프록시 객체 메서드 호출
        proxy.helloTarget();

        System.out.println("targetClass : "+ target.getClass());
        System.out.println("proxyClass : "+ proxy.getClass());
    }
}
  • 프록시 객체 메서드를 호출 시 실행 순서 (proxy.helloTarget())
    • 1.  JDK 동적 프록시의 helloTarget() 호출
    • 2.  InvocationHandler의 구현체인 TargetHandler.invoke() 호출
    • 3. method.invoke(target, args)를 호출하여 target인 실제 객체(TargetImpl)를 호출한다.
    • 4. TargetImpl 인스턴스 호출

실행 결과

결론

  • 인터페이스를 구현하는 class들만 target이 될 수 있다. -> 모든 Target class는 구현체가 있어야 한다(강제성)
  • invoke() 메소드를 통해 구현함으로 코드 중복을 줄일 수 있다.
  • 그럼 인터페이스가 없어도 되는 CGLIB를 알아보자

CGLIB

CGLIB는 바이트 코드를 조작해서 프록시 객체를 생성하는 외부 라이브러리이다.

또한 인터페이스가 없어도 proxy를 생성할 수 있다.

스프링 프레임워크를 사용한다면 스프링 내부 소스 코드에 포함되어 별도의 외부 라이브러리를 추가하지 않아도 된다.

 

아래 예제를 참고하며 CGLIB를 알아보자.

 

target 클래스 생성

public class Target  {

    public String helloTarget() {
        System.out.println("Target : 구현체입니다.");
        return "구현체입니다.";
    }
}

 

MethodInterceptor 구현

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class TargetInterceptor implements MethodInterceptor {

    private final  Object target;

    public TargetInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("proxy 시작");
        Object result = methodProxy.invoke(target, args);
        System.out.println("proxy 종료");
        return result;
    }

}
  • proxy로 핸들링할 handler 역할  -> MethodInterceptor 인터페이스로 정의되어있다.

Main 호출

 

import net.sf.cglib.proxy.Enhancer;

public class DynamicProxy {

    public static void main(String[] args) {
        Target target = new Target();


        // 프록시 객체를 생성하는 역할
        Enhancer enhancer = new Enhancer();
        // target 클래스를 상속
        enhancer.setSuperclass(Target.class);
        // 프록시에 적용할 실행 로직 할당
        enhancer.setCallback(new TargetInterceptor(target));
        // 프록시를 생성
        Target proxy = (Target) enhancer.create();

        proxy.helloTarget();

        System.out.println(target.getClass());
        System.out.println(proxy.getClass());
    }
}

실행 결과

 

결론

  • CGLIB는 target 클래스를 상속해서 프록시를 생성
  • 이 과정에서 Final 메소드 또는 클래스를 재정의 할 수 없어서 proxy를 생성할 수 없는 단점
  • 바이트 코드를 조작해서 프록시 객체를 생성 하므로 JDK 동적 프록시 보다 성능적으로 우세

 

 

이 두 개를 바탕으로 다음의 스프링이 지원하는 프록시를 알아보자

 

 

 

[참고] - 스프링 핵심 원리 -고급편(김영환) 

 

Comments