티스토리 뷰

728x90
반응형

서론


Spring을 사용하여 개발을 하다보면 @Transactional이라는 어노테이션을 많이 보고, 많이 사용하곤 합니다. 이때 클래스 레벨이나 메서드 레벨에 @Transactional 어노테이션이 선언되어 있으면 Proxy로 수행이 되는구나 생각만하고 지나치는 경우가 있습니다. 하지만 이러한 Proxy로 인해 AOP Self Invocation과 같은 겪고 싶지 않은 여러 경험들을 하게 될 수 있습니다. 그렇기 때문에 우리는 스프링에서 Proxy가 어떻게 동작하는지 알아야할 필요성이 있습니다.

 

 

JDK Dynamic Proxy


JDK Dynamic Proxy는 java.lang.reflect 패키지에 속한 Proxy 클래스를 사용합니다.  따라서 리플랙션을 사용하여 동적으로 프록시를 생성해주기 때문에 이름부터 Dynamic Proxy입니다. 

 

 

💡 JDK Dynamic Proxy를 사용하여 프록시를 생성하는 방법

 

  • 아래는 JDK Dynamic Proxy를 사용하여 프록시 객체를 생성하는 방법입니다.
Object proxy = Proxy.newProxyInstance(ClassLoader       // 클래스로더
                                    , Class<?>[]        // 타깃의 인터페이스
                                    , InvocationHandler // 타깃의 정보가 포함된 Handler
               );

 

📜 코드로 살펴보자.

 

  • 아래 코드를 보면 하나의 인터페이스를 만들고 두개의 구현체가 있으며, 프록시 핸들러를 만들었습니다.
// 1. 인터페이스를 만듭니다.
public interface PaymentService {

    void pay();
}

// 2. 인터페이스를 구현한 구체 클래스를 만듭니다.
public class KakaoPayService implements PaymentService {

    @Override
    public void pay() {
        System.out.println("카카오페이를 사용하여 결제...");
    }
}

public class NaverPayService implements PaymentService {

    @Override
    public void pay() {
        System.out.println("네이버페이를 사용하여 결제...");
    }
}

// 3. 프록시 핸들러를 구현합니다.
public class PaymentInvocationHandler implements InvocationHandler {

    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }
}

 

▶︎ 실행

 

  • 실행을 시켜보면 의도한대로 결과가 도출되는 것을 알 수 있습니다. 또한 클래스의 이름을 출력해보면 com.sun.proxy.&Proxy0 이라는 것을 알 수 있으며 이를 보고 인터페이스를 사용하여 프록시를 생성했구나 알 수 있습니다.
public class Main {

    public static void main(String[] args) {
        PaymentService kakaoPayService = (PaymentService) Proxy.newProxyInstance(PaymentService.class.getClassLoader(),
                new Class[]{PaymentService.class},
                new PaymentInvocationHandler(new KakaoPayService())
        );

        PaymentService naverPayService = (PaymentService) Proxy.newProxyInstance(PaymentService.class.getClassLoader(),
                new Class[]{PaymentService.class},
                new PaymentInvocationHandler(new NaverPayService())
        );

        System.out.println("naverPayService class : " + naverPayService.getClass().getName()); // naverPayService class : com.sun.proxy.$Proxy0
        System.out.println("kakaoPayService class : " + kakaoPayService.getClass().getName()); // kakaoPayService class : com.sun.proxy.$Proxy0
        naverPayService.pay(); // 네이버페이를 사용하여 결제...
        kakaoPayService.pay(); // 카카오페이를 사용하여 결제...
    }
}

 

동작 원리 

 

  • 클라이언트가 메서드를 수행하면 JDK Dynamic Proxy가 메서드 처리를 Invocateion Handler에게 위임한 후 Invocation Handler는 부가 기능을 수행한 뒤에 Target에게 기능을 위임합니다.

 

💡 결론

 

  • JDK Dynamic Proxy는 인터페이스 기반이다 보니 인터페이스가 필수 있습니다.
  • 리플랙션을 사용하다 보니 느립니다.
  • InvocationHandler의 invoke 메서드를 구현해야 합니다.

 

CGLIB


CGLIB는 Code Generator Library의 약자로 클래스의 바이트코드를 조작하여 Proxy 객체를 생성해주는 라이브러리입니다.

 

 

💡 CGLIB를 사용하여 프록시를 생성하는 방법

 

  • 아래는 CGLIB를 사용하여 프록시 객체를 생성하는 방법입니다.
Enhancer enhancer = new Enhancer();
         enhancer.setSuperclass(NaverPayService.class); // 타깃 클래스
         enhancer.setCallback(PaymentInterceptor);      // Handler
Object proxy = enhancer.create(); // Proxy 생성

 

📜 코드로 살펴보자.

 

  • JDK Dynamic Proxy를 사용하면 인터페이스를 만든 후 해당 인터페이스를 구현한 구체 클래스를 만들어야하지만 CGLIB를 사용한 경우는 구체 클래스를 상속받아 만들어지므로 인터페이스를 만들 필요가 없습니다.
  • MethodInterceptor 인터페이스를 구현한 구체 클래스에서는 Method 클래스의 invoke 메서드를 사용할 수도 있지만 MethodProxy 클래스의 invoke 메서드를 사용하는게 조금 더 빠르다고 합니다.
// 1. 클래스를 만듭니다.
public class KakaoPayService {

    public void pay() {
        System.out.println("카카오페이를 사용하여 결제...");
    }
}

public class NaverPayService {

    public void pay() {
        System.out.println("네이버페이를 사용하여 결제...");
    }
}

// 2. MethodInterceptor 인터페이스를 구현한 구체 클래스를 만듭니다.
public class PaymentInterceptor implements MethodInterceptor {

    private final Object target;

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

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        return methodProxy.invoke(target, args);
    }
}

 

▶︎ 실행

 

  • 클래스의 이름을 출력해보면 CGLIB를 사용한 것을 알 수 있습니다.
public class Main {

    public static void main(String[] args) {
        NaverPayService target = new NaverPayService();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(NaverPayService.class);
        enhancer.setCallback(new PaymentInterceptor(target));

        NaverPayService naverPayService = (NaverPayService) enhancer.create();
        System.out.println("naverPayService class : " + naverPayService.getClass().getName()); // naverPayService class : com.gift.kakao.domain.payment.NaverPayService$$EnhancerByCGLIB$$55c553e8
        naverPayService.pay(); //네이버페이를 사용하여 결제...
    }
}

 

동작 원리 

 

  • 클라이언트가 메서드를 수행하면 CGLIB가 메서드 처리를 MethodInterceptor에게 위임한 후 MethodInterceptor는 부가 기능 수행한 뒤 Target에게 기능을 위임합니다.

 

💡 결론

 

  • 리플랙션이 아닌 바이트 코드를 조작하므로 빠릅니다.
  • 인터페이스를 만들지 않아도 됩니다.
  • 상속을 이용하여 프록시 객체를 생성합니다.
  • 상속을 이용하기 때문에 final이 선언되어 있는 클래스는 프록시 객체를 생성할 수 없습니다.

 

🤔 왜 처음부터 CGLIB을 사용하지 않았을까?


JDK Dynamic Proxy는 리플랙션을 사용하고, 인터페이스가 있어야 합니다. 반면 CGLIB는 리플랙션을 사용하지도 않고, 인터페이스가 없어도 될 뿐만 아니라 바이트 코드를 조작하므로 속도도 빠릅니다. 하지만 왜 처음부터 CGLIB를 사용하지 않았을까요?

 

1. Enhancer 라이브러리

 

  • CGLIB를 사용하기 위해서는 Enhancer 라이브러리가 필수입니다. Spring에서 이를 쓰기 위해서는 의존성을 추가했어야 했습니다.
  • 하지만 Spring 3.2 버전부터 spring-core로 리패키징이 된 상태입니다.

 

2. default 생성자

 

  • CGLIB를 구현하기 위해서는 반드시 default 생성자를 필요로 했습니다.
  • 하지만 Objenesis 라는 라이브러리를 사용하여 기본 생성자 없이도 객체 생성이 가능하게 되었습니다.

 

3. 생성자 중복 호출

 

  • CGLIB Proxy의 메서드를 호출하면 타겟의 생성자가 2번 호출된다는 단점이 있었습니다.
  • 하지만 Objenesis 라이브러리를 사용하여 개선이 되었다고 합니다.

 

현 상황에서 CGLIB는?


Spring 4.3과 Spring Boot 1.4 버전부터는 기본적으로 CGLIB를 사용하고 있고 또한 JPA Hibernate에서도 기본적으로 CGLIB를 사용하고 있다고 합니다.

 

 

 

 

참고

 

 

 

 

 

 

 

 

 

728x90
반응형