개발 기록

> [Java] 자바 프록시 구현 (JDK Dynamic Proxy, CGLIB) 본문

JAVA

> [Java] 자바 프록시 구현 (JDK Dynamic Proxy, CGLIB)

1z 2024. 1. 21. 10:06

1. 개요

프록시 패턴을 구현하기 위해서는 서비스 interface, 프록시 Class, 프록시 적용 대상 Class 가 필요하다.

프록시가 1개라면 문제없지만, 서비스의 개수만큼 만든다고 하면  n개의 프록시 클래스를 도입해야 하므로 코드의 복잡도가 증가한다.

 

이를 보완해주는 방법으로 Dynamic Proxy와 CGLib가 있다. 

 

2. Dynamic Proxy 

동적 프록시는 개발자가 직접 일일히 프록시 객체를 생성하는 것이 아닌, 런타임에 프록시 객체를 생성하는 기술이다. 
* java.lang.reflect.Proxy 패키지 API 이용 

 

 

(1) Java Proxy 구현 요소 

 

1. Proxy.class

Proxy동적 프록시 클래스 및 인스턴스를 생성하기 위한 정적 메서드를 제공하며 해당 메서드로 생성된 모든 동적 프록시 클래스의 슈퍼클래스이다. 

 

2. Proxy.newProxyInstance

: newProxyInstance() 를 통해 생성 될 Proxy 객체가 구현할 Interface를 정의한다.

: 각 프록시 인스턴스에는 인터페이스를 구현하는 연관된 호출 핸들러 InvocationHandler 객체가 있다.

: 지정된 호출 핸들러에 메서드 호출을 전달하는 지정된 인터페이스에 대한 프록시 인스턴스를 반환한다.

 

☞ loader : 프록시 클래스를 정의하는 클래스 로더

interfaces :  구현할 프록시 클래스의 인터페이스 목록

handler :  메소드 호출을 전달하는 호출 핸들러

Object proxy = Proxy.newProxyInstance(ClassLoader loader          // 클래스로더
                                    , Class<?>[] interface        // 타깃의 인터페이스
                                    , InvocationHandler handler   // 타깃의 정보가 포함된 Handler
public void basicDynamicProxy(){

// 람다
    TestInterface proxy = (TestInterface) Proxy.newProxyInstance(
        TestInterface.class.getClassLoader(),
        new Class[]{TestInterface.class},
        new MyProxyHandler(new TestImpl())
    );
    proxy.call();
}

 

 

3. InvocationHandler

: 프록시 인스턴스의 호출 핸들러 에 의해 구현되는 인터페이스입니다 .

: 각 프록시 인스턴스에는 연관된 호출 핸들러가 있습니다. 프록시 인스턴스에서 메서드가 호출되면 메서드 호출이 인코딩되어 해당 핸들러의 invoke 메서드로 전달된다.

invoke(Object proxy, Method method, Object[] args)
프록시 인스턴스에서 메서드 호출을 처리하고 결과를 반환합니다

 

 

  proxy- 메소드가 호출된 프록시 인스턴스

  method- Method 프록시 인스턴스에서 호출된 인터페이스 메소드에 해당하는 인스턴스

  args- 프록시 인스턴스의 메소드 호출에 전달된 인수 값을 포함하거나 null, 인터페이스 메소드가 인수를 사용하지 않는 경우 객체의 배열

public class MyProxyHandler  implements InvocationHandler {
    private final Object target;
    MyProxyHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 파라미터로 전달받은 메서드를 invoke로 실행
        Object result = method.invoke(target, args);
        return result;
    }
}

 

 

(2)  구현 예제 (LaMDda)

interface Subject {
    void do();
}

// 프록시를 적용할 타겟 객체
class RealSubject implements Subject{
    @Override
    public void do() {
        System.out.println("실제 작업을 처리합니다..");
    }
}
    public void basicDynamicProxy2(){

    // newProxyInstance() 메서드로 동적으로 프록시 객체를 생성.
        // 프록시 핸들러
        Subject proxy = (Subject) Proxy.newProxyInstance(
            Subject.class.getClassLoader(), // 대상 객체의 인터페이스의 클래스로더
            new Class[]{Subject.class}, // 대상 객체의 인터페이스
            (proxy, method, args) -> {
            
                Object target = new RealSubject();
                System.out.println("----realSubject do 메서드 호출 전 전처리----");
                
                // 파라미터로 전달받은 메서드를 invoke로 실행
                Object result = method.invoke(target, args);

                return result;
            }
        );
        proxy.do();

    }
}

 

5. 주의해야 할 점  

1. 메서드가 여러개인 타겟을 프록시화 할경우 : 대상 객체의 모든 메서드를 호출할때마다 프록시 핸들러가 실행된다.
 특정 메서드를 따로 처리하고 싶을 경우, method 명을 검사하여 분기처리 하면된다. 

// SUbject interface 메서드 중 call 메서드만 별도 처리를 하고 싶을 경우
if(method.getName().equals("call")) {
    System.out.println("----call 메서드 호출----");
    Object result = method.invoke(target, args); 
}

 

 

2. 

☞ 동적 프록시에 타켓을 등록할때 타입을 클래스가 아닌 무조건 인터페이스를 파라미터로 넣어야 된다.

  @Autowired 로 주입받을 경우도, 구현체가 아닌 구현 할 인터페이스로 주입 받아야 한다.

  인터페이스를 기반으로 프록시를 동적으로 만들어주기 때문에, 인터페이스가 필수이기 때문이다.

  타켓을 등록할때 타입을 클래스 로 해도 컴파일단에서는 에러를 잡지 못한다. 


 

3. CGLIB 

CGLib는 JDK Dynamic Proxy와는 다르게 인터페이스가 아닌 클래스 기반으로 바이트코드를 조작하여 프록시를 생성하는 방식이다.  인터페이스가 아닌 클래스를 대상으로 동작 가능하고 바이트코드를 조작해 프록시를 만들기 때문에 Dynamic Proxy에 비해 성능이 우수하다.

 

▶ CGLib를 사용하여 프록시를 생성할 때에는 크게 크게 두가지 작업을 필요로 한다.

 - net.sf.cglib.proxy.Enhancer 클래스를 사용하여 원하는 프록시 객체 만들기 

 - methodIntherceptor 인터페이스의 invoke 메서드를 구현.

 - net.sf.cglib.proxy.Callback을 사용하여 프록시 객체 조작하기

 

1. 타겟 클래스생성

다이나믹 프록시를 사용할 땐 인터페이스 구현이 필수였지만 CGLib은 그렇지 않다. 클래스만 작성해준다.

// 프록시를 적용할 대상 타켓
class RealSubject {
    public void do() {
        System.out.println("실제 작업 처리");
    }
}

 

2. MethodInterceptor 인터페이스로 프록시 핸들러를 등록한다.

    public Object intercept(
        Object o,          // CGLIB가 적용된 객체
        Method method,     // 호출된 메서드
        Object[] args,     // 메서드를 호출하면서 전달된 인수
        MethodProxy methodProxy   // 메서드를 호출에 사용
    ) throws Throwable {
// 프록시 핸들러
class MyProxyInterceptor implements MethodInterceptor {

    private final Object target;

    MyProxyInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(
            Object o,
            Method method,
            Object[] args,
            MethodProxy methodProxy
    ) throws Throwable {
        System.out.println("실제 작업 처리전 전 처리");
        
		// 파라미터로 전달받은 메서드를 invoke로 실행
        Object result = method.invoke(target, args); 

        System.out.println("Proxy 종료");

        return result;
    }
}

 

 

3. Enhancer 로 Proxy 객체 생성

- CGLIB는 타겟 객체를 상속하여 프록시화 하기 때문에 상속할 슈퍼 클래스를 등록한다

- setCallback 를 이용해 핸들러를 적용한다.

- create 메소드로 프록시 객체를 생성하고 메소드를 호출한다.

public class Client {
    public static void main(String[] arguments) {

        // 1. 
        Enhancer enhancer = new Enhancer();
        // 2. 구체 클래스를 지정
        enhancer.setSuperclass(RealSubject.class);
        // 3. 프록시 핸들러 할당
        enhancer.setCallback(new MyProxyInterceptor(new MyProxyInterceptor()));
        // 2. 프록시 생성
        // setSuperclass() 에서 지정한 클래스를 상속 받아서 프록시가 만들어진다.
        RealSubject proxy = (RealSubject) enhancer.create();

        // 3. 프록시 호출
        proxy.call();
    }
}

 

4. 주의해야 할 점  

상속을 통해 Proxy를 구현하므로 final 클래스일 경우 Proxy로 생성할 수 없다.

 

 

 

 

 

 

 

 

참고

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

https://inpa.tistory.com/entry/JAVA-☕-누구나-쉽게-배우는-Dynamic-Proxy-다루기