JDK Dinamic Proxy, CGLib Proxy

Terror·2024년 9월 15일
0

개요

  • 앞서 AOP의 개념에 대해 알아보고 자바 AOP를 구현하기 위해 Dynamic Proxy와, CGLib, AspectJ를 쓸 수 있음을 알았다
  • Spring AOP에서는 "인터페이스 유무"에 따라 인터페이스 기반의 프록시 생성시 Dynamic Proxy, CGLib을 사용하는데
    Spring boot AOP에서는 CGLib을 default로 사용한다고 한다

    interface가 있음 -> Dynamic Proxy
    interface가 없음 -> CGLib

  • 오늘은 Spring AOP를 완벽하게 이해하기 위해 먼저 스프링 없이 순수 자바로 Dynamic와 CGLib를 실습해보며 이해해보려 한다

JDK Dynamic Proxy와 CGLib

  • Dynamic Proxy와 CGLib은 모두 런타임 위빙 방식이며 프록시 패턴으로 동작한다
  • 따라서 메서드 실행 시에만 위빙이 가능하다
  • 그래서 Dynamic Proxy와 CGLib을 사용하는 스프링 AOP도 메서드 실행 조인포인트만 지원한다

Proxy Pattern 이란?

프록시 패턴이란 소프트웨어 디자인 패턴 중 하나로 오리지널 객체(Real Object) 대신 프록시 객체 (Proxy Object)를 사용해 로직의 흐름을 제어하는 디자인 패턴이다

  • Dynamic Proxy와 CGLib는 기본적으로 프록시 패턴으로 동작하여 원래 소스코드를 수정하지 않고 프록시 객체를 생성하여 흐름을 제어해 기능을 삽입할 수 있다
  • 오리지널 객체의 메서드 호출 결과를 바꿀 순 없다

JDK Dynamic Proxy

  • JDK에서 제공하는 Dynamic Proxy는 "interface를 기반으로 Proxy를 생성"하는 방식이다
  • interface를 기반으로 Proxy를 생성해주기 때문에 인터페이스의 존재가 필수적이다
  • 자바에서는 Reflection 활용한 Proxy 클래스를 제공해주고 있다
  • Java.lang.relfect.Proxy 클래스의 newProxyInstance() 메서드를 이용하여 프록시 객체를 생성한다

Reflection 이란?

  • reflection 이란 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메서드, 타입, 변수)등등에 접근할 수 있게 해주는 Java API이다
  • 자바에서는 JVM이 실행되면 사용자가 작성한 자바 코드가 컴파일러를 거쳐 바이트 코드로 변환되어 static 영역에 저장된다
  • Reflection API는 이 정보를 활용하여 필요한 정보를 가져온다
  • 자바의 Reflection API는 값비싼 API이기 떄문에, Dynamic Proxy는 리플렉션을 하는 과정에서 성능이 좀 떨어진다는 단점이 있다

reflection 자체가 애초에 객체의 메모리주소값을 통해서 가져오는것이 아닌, static 영역에 저장된 메타데이터, 클래스 들의 정보를 가져올때 직접 가져오기 때문에 오버헤드 가능성이 높다

Dynamic Proxy가 생성되고, 사용될떄 Reflection API를 활용해서 실제 사용되는 메서드등의 정보를 JVM에 로드된 메타데이터 등에서 가져온다

다이나믹 프록시를 실제로 써보자!

인터페이스 선언

  • Dynamic Proxy를 생성하기 위해서는 인터페이스가 필수적이기 때문에 하나 만들어주자
package proxy;

public interface Animal {
    void eat();
    void drink();
}

인터페이스 구현

package proxy;

public class Rabbit implements Animal {
    @Override
    public void eat() {
        System.out.println("토끼가 먹습니다");
    }

    @Override
    public void drink() {
        System.out.println("토끼가 마십니다");
    }
}
package proxy;

public class Tiger implements Animal {
    @Override
    public void eat() {
        System.out.println("호랑이가 먹습니다");
    }

    @Override
    public void drink() {
        System.out.println("호랑이가 마십니다");
    }
}

프록시 핸들러 구현

  • 프록시 객체를 생성할때 필요한 핸들러를 구현해보자
  • InvocationHandler를 상속받아 구현 할 수 있다
package proxy;

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

public class AnimalProxyHandler implements InvocationHandler {
    Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        System.out.println("::: BEFORE :::");

        if (method.getName().equals("eat")) {
            System.out.println("::: EAT 메서드 호출전 :::");
            result = method.invoke(target,args); // 메서드 호출
            System.out.println("::: EAT 메서드 호출후 :::");
        }
        if (method.getName().equals("drink")) {
            System.out.println("::: DRINK 메서드 호출전 :::");
            result = method.invoke(target,args); // 메서드 호출
            System.out.println("::: DRINK 메서드 호출후 :::");
        }        
        System.out.println("::: AFTER :::");
        return result;
    }
}

잘되는지 Test 해보자

package proxy;

import java.lang.reflect.Proxy;

public class DynamicProxyTest {
    public static void main(String[] args) {
        test1();
    }
    static void test1(){
        Animal rabbit = (Animal) Proxy.newProxyInstance(
                Animal.class.getClassLoader(),
                new Class[]{Animal.class},
                new AnimalProxyHandler(new Rabbit())
        );
        rabbit.eat();
    }
}

  • 흠 잘나오고 있는모습이다
    1. 인터페이스를 만들고, 인터페이스를 오버라이딩 하여 구현한 클래스 준비
    2. InvcationHandler를 오버라이딩하여, 자신이 처리하고싶은 내용들을 작성한다
    3. 실제 코드에서 새로운 프록시 객체를 생성할때 인터페이스로 구현한 클래스의 내용들을 잘 넣는다
    4. 해당 프록시 객체를 실제로 사용할시 우리가 invcationHandler에서 오버라이딩하여 처리한 내용들이 표출된다
  • 느낌이 상당히 우리가 AOP를 처리하였을때와 유사한것을 확인 할 수 있다
  • 다이나믹 프록시의 경우 인터페이스가 필수적이다
  • 인터페이스 없을때는 디폴트로 CGLib 프록시 객체가 생성되어 대체된다, 같이한번 확인해보자

CGLib

  • CGLib은 JDK Dynamic Proxy와는 다르게 인터페이스가 아닌 "클래스를 기반으로 바이트 코드를 조작하여 프록시를 생성"하는 방식이다

프록시 핸들러 구현

  • CGLib을 사용하여 프록시를 생성할 때에는 크게 두가지 작업을 필요로 한다
    • net.sf.cglib.proxy.Enahcer 클래스를 사용하여 원하는 프록시 객체 만들기
    • next.sf.cglib.proxy.Callback 을 사용하여 프록시 객체 조작하기
  • 프록시 객체를 조작하기 위해 MethodInterceptor 방식과 Dynamic Proxy에서 사용하였던 InvcationHandler 방식을 사용하여 조작 할 수 있다
  • InvocationHander 방식을 사용할 경우 바이트 코드조작이 아닌, reflector을 활용 할 수도있지만 앞서 설명하였듯이 직접적으로 JVM에 로드되있는 메타데이터를 뽑아와서 사용 하다보니 오버헤드가 발생 할 수있고, 기본적으로는 MethodInterceptor 를 사용한다
  • 일단은 InvcationHnadler를 통한 방법도 가능은 하니 함께 알아보자

클래스 작성

  • 인터페이스는 필요없으니 클래스만 만들어주자
public class Rabbit {
    public void eat(){
        System.out.println("토끼가 먹습니다");
    }
}

reflection을 활용하여 만들어보자

  • import만 바꾸어 주면된다
package org.terror.codeplaygroundspring.proxy;

import net.sf.cglib.proxy.InvocationHandler;

import java.lang.reflect.Method;

public class AnimalProxyHandler implements InvocationHandler {
    Object target;

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

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        Object result = null;
        System.out.println("::: BEFORE :::");
        if (method.getName().equals("eat")) {
            System.out.println("::: EAT 메서드 호출 :::");
            result = method.invoke(target, objects);
            System.out.println("::: EAT 메서드 호출 끝 :::");
        }
        return result;
    }
}

예제 출력결과

:::BEFORE:::
::: EAT 메서드 호출:::
토끼가 먹습니다
::: EAT 메서드 호출 끝 :::

  • 이 나올 것이다
  • 왜 출력을 예제로 보나요?
  • 직접 실행시켜보면 이러한 예외가 발생되는데, 이는 Java 9이상의 환경부터는 CGLib 내부적으로 실행되는 일부 메서드를 비공개로 설정하여서 그렇기 때문에 그렇다
  • 뭔가 찾아보면 방법은 있겠지만 일단 나는 이러려니 하고 넘어갔다 (블로그에서도 그렇게 나와있었으니 ㅎ;)

MethodInterceptor 방식을 활용하여 구현해보자

  • 먼저 MethodInterceptor을 만들어주자
package org.terror.codeplaygroundspring.proxy;

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

import java.lang.reflect.Method;

public class PrintLogInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Object result = null;
        System.out.println("::: BEFORE :::");
        result = methodProxy.invokeSuper(o, objects);
        System.out.println("::: AFTER :::");
        return result;
    }
}
package org.terror.codeplaygroundspring;

import net.sf.cglib.proxy.Enhancer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.terror.codeplaygroundspring.proxy.PrintLogInterceptor;
import org.terror.codeplaygroundspring.proxy.Rabbit;

public class MethodInterceptorTest {
    @Test
    @DisplayName("메서드 인터셉터를 활용한 CGLib Proxy 테스트")
    public void test1(){
        //Enhancer 객체를 생성
        Enhancer rabbitEnhancer = new Enhancer();
        // setSuperclass() 메소드에 프록시할 클래스 지정
        rabbitEnhancer.setSuperclass(Rabbit.class);
        // 로그 출력해주는 인터셉터 지정
        rabbitEnhancer.setCallback(new PrintLogInterceptor());
        Rabbit rabbit = (Rabbit) rabbitEnhancer.create(); // 프록시 생성

        rabbit.eat();
    }
}
  • 이것도 사용해보아도, 똑같이 오류가 나지만 우리가 예상한대로 나올것이다
    **before log**
    토끼가 음식을 먹습니다.
    **after log**

마치며

  • 이 이후에 CallbackFilter를 통해 추가적으로 구현 할 수 있는 부분은 아래 참조 블로그 링크에 남겨놓았으니 확인하면 좋을것 같다

끝!

참조 블로그

https://velog.io/@suhongkim98/JDK-Dynamic-Proxy%EC%99%80-CGLib

profile
테러대응전문가

0개의 댓글