본문 바로가기

Java

DynamicProxy & CGLib

 

Spring 과 자바진영의 라이브러리들을 공부하다보면

DynamicPrxoy(이하 동적프록시) 와 CGLib 라는 단어가 자주 등장한다.

동적프록시 그리고 CGLib 를 이해해서 더욱 더 흡수력을 높여보자.

 

동적프록시를 알기 전에 우선 프록시를 알아야한다.

Proxy

프록시는 전반적인 분야에서 다양한 의미로 쓰이지만,

하나의 공통적인 부분은 실제 오브젝트에 직접 접근을 하는 것이 아니라 요청을 한번 받아주는 오브젝트를 통하여  

실제 오브젝트를 숨기고 보호하여 확장성을 얻는 역할을 한다.

 

동적프록시에서 말하는 프록시도 이 이념은 같다.

 

Proxy Pattern

프록시 패턴이란, 위에서 기술한 프록시의 개념을 코드에서 구현하기 위한 디자인패턴이다.

데코레이터 패턴과 거의 유사하지만, GOF 에서는 둘을 목적에 따라 구분한다.

프록시 패턴은 실제 구현체의 보호가 목적이고, 데코레이터 패턴은 새로운 기능을 추가하기 쉽게 함이 목적이다

프록시 패턴 구조

프록시 패턴의 구조는 위와 같다.

실제 구현체와 동일한 인터페이스를 구현하는 Proxy 객체를 생성하고,

클라이언트에게는 Proxy 객체를 인터페이스로서 전달한다.

클라이언트는 실제 구현체에서 필요한 기능을 인터페이스로서 전달받기 때문에, 실제 구현체인지 프록시객체인지는 알 수 없다.

클라이언트가 메시지를 보내면, 프록시 객체가 받아서 실제 구현체에게 메시지를 전달하고,

실제 구현체가 하지 않는 확장된 기능을 수행하여 다시 클라이언트에게 반환한다.

 

즉, 특정 클라이언트가 기존 기능에서 어떠한 추가적인 기능이 필요할때 실제 구현체를 수정할 필요없이, 프록시 객체를 생성하여 추가적인 기능을 수행하면되어 실제 구현체는 보호받을 수 있게된다.

.

상세한 프록시 패턴의 구현은 따로 디자인패턴으로서 포스팅하고, 이제 원래 목적인 동적프록시를 살펴보자.

 

동적 프록시

개념

동적 프록시는, 위 프록시 패턴의 단점을 보완하기위해 나온 개념이다.

프록시 패턴은 추가적인 기능이 필요할 때마다, 해당 인터페이스의 프록시 객체에서 모든 메서드를 구현한 새로운 프록시 객체를 생성해야한다. 특정 메서드에 대해서만 추가적인 기능을 수행하고 싶어도, 모든 인터페이스를 구현해야한다.

 

이 단점을 자바 진영은 reflection 을 이용하여 해결한다.

이를 런타임에서 리플렉션을 이용하여 프록싱한다고 해서 동적 프록시라고 부른다.

 

사용법

java 는 동적프록시 기능을 InvocationHandler 라는 인터페이스를 통하여 지원한다.

 

// Client 에 제공될 인터페이스
public interface Greeting { 
    String greeting();
}

// 실제 구현체
public class WorldCommonGreeting implements Greeting {

    public WorldCommonGreeting() {}

    @Override
    public String greeting() {
        return "Hello?";
    }
}

위와 같은 인터페이스와 구현체가 있다고 하자.

나는 한국인이라서 영어로만 인사하는게 아니꼽다.. 그래서 한국어 인사를 추가하고 싶다.

하지만 한국어 인사는 WorldCommonGreeting 이란 클래스의 책임에 적절하지 않다. 

인터페이스의 메서드가 하나라 그냥 프록시 구현체를 생성하면 되지만,

인터페이스에 메서드가 더 추가될 가능성이 크고, 구현체가 늘 수록 인터페이스에 메서드를 추가하기 어려워진다. 

동적 프록시기능을 적용해보자.

 

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

public class KoreanGreetingProxyHandler implements InvocationHandler {

    private Object proxyTarget;

    public KoreanGreetingProxyHandler(Object proxyTarget) {
        this.proxyTarget = proxyTarget;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object methodResult = method.invoke(proxyTarget, args);
        if (method.getName().startsWith("greeting")) {
            return methodResult + "_안녕하세욥";
        }
        return methodResult;
    }
}

InvocationHandler 를 구현한 클래스이다. 

invoke() 메서드는 proxy: 프록시 대상 객체, method: 호출된 메서드, args: 파라미터를 인자로 받는다.

클라이언트에 전달된 인터페이스로서의 프록시객체 메서드를 호출하면, invoke 메서드가 호출되면서 

실 구현체의 메서드를 invoke 하고, 구현된 추가 기능을 수행하는 로직을 수행한다.

 

동적프록시는 프록시 패턴으로서의 프록시 기능을 온전히 수행해야하기때문에,  실제 객체에 delegate 하는 부분을 꼭 구현해야한다.

 

    @Test
    public void testJdkDynamicProxy() {
        Greeting commonGreeting = new WorldCommonGreeting();
        Greeting proxyGreeting = (Greeting) Proxy.newProxyInstance(null,
                new Class[]{Greeting.class},
                new KoreanGreetingProxyHandler(commonGreeting));

        assertEquals("Hello?", commonGreeting.greeting()); // true
        assertEquals("Hello?_안녕하세욥", proxyGreeting.greeting()); // true
    }

프록시 객체 생성은 위와 같이 Proxy.newProxyInstance() 메서드를 통하여 수행된다.

해당 메서드는 3가지 인자를 받는다.

1. ClassLoader null 일시 기본 클래스로더

2. 프록시가 구현할 인터페이스 배열, 생성된 프록시 객체는 전달된 인터페이스들의 메서드를 가짐

3. 전달된 인터페이스의 메서드를 수행할때 실행될 invoke 가 구현된 InvocationHandler 

 

이렇게 프록시 객체를 생성하여 클라이언트에게 인터페이스로서 전달하면, 

전달된 인터페이스의 어떤 메서드를 수행하든 구현한 InvocationHandler Invoke 메서드가 호출된다.

그렇기 때문에, 반환 타입이 구현 내용의 반환값과 다른 메서드거나 하면 오류가 발생한다.

리플렉션을 사용하여 개발할때는 항상 예외처리를 숙고해서 해야한다.

 

이제 CGLib 를 살펴보고, 둘의 차이점을 비교해보자.

 

CGLib (Code Generate Library)

개념

 

CGLib 는 jdk 에 내장되어 있는 자바의 순수한 기능이 아닌 바이트코드를 조작하여 프록시 객체를 생성해주는 라이브러리다.

인터페이스 기반이 아닌 실제 클래스를 상속하여 모든 메서드를 재정의 하는 방식으로

프록시 객체를 생성하므로 꼭 인터페이스를 구현할 필요가 없으며,

상속을 통하여 타깃 클래스에 대한 정보를 직접 제공받고, 바이트 코드를 조작하여 동작하므로 jdk 동적프록시보다 성능상으로 이점이 있다.

다만, 상속을 제한하는 final 클래스나 메서드는 프록시를 생성할 수 없다.

 

 

Cglib 도식화

사용법

 

    @Test
    public void testCglibDynamicProxyWithMethodInterceptor() {
        MethodInterceptor methodInterceptor = new MethodInterceptor() {
            final WorldCommonGreeting commonGreeting = new WorldCommonGreeting();

            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws InvocationTargetException, IllegalAccessException {
                String invokedResult = (String) method.invoke(commonGreeting, objects);
                return invokedResult + "_안녕하세욥";
            }
        };

        WorldCommonGreeting proxy =
                (WorldCommonGreeting) Enhancer.create(WorldCommonGreeting.class, methodInterceptor);


        assertEquals("Hello?_안녕하세욥", proxy.greeting());
    }

Enhancer.create() 메서드를 통하여 프록시의 대상이 될 콘크리트 클래스와 구현한 methodInterceptor를 전달해주면 끝이다.

상세 사용법은 링크를 참조하자 https://www.baeldung.com/cglib

 

Spring 에서의 CGLib 와 Jdk 동적프록시

 

동적프록시 기능의 대표적인 예시는 Spring AOP 의 런타임 위빙이 있다.

Spring AOP 는 지정한 호출 시점에서 IoC 컨테이너가 프록시 빈을 생성하여 타깃 메소드가 호출되는 시점에 

부가 기능을 수행할 메소드를 판단하여 부가기능을 주입하는데 이를 런타임 위빙이라고 한다.

 

Spring AOP 는 원래 jdk 동적 프록시 기반으로 동작하였으나, CGLib가 버전이 업그레이드 됨에 따라 문제점이 개선되고

스프링 코어 모듈에 포함된 후 부터 CGLib를 통한 AOP 를 지원하게 되었다.

 

Spring 은 위 그림과 같은 구조인 ProxyFactory 가 AopProxy 를 생성할 때, 인터페이스의 유무에 따라 구분하여 생성한다.

설정 파일에서 ProxyFactory 의 proxyTargetClass 를 true 로 설정해주면 항상 CGLib 를 통하여 프록시를 생성하도록 설정할 수 있다.

 

추가적으로 Hibernate 도 프록시 엔티티를 생성할때, CGLib 를 이용한다.

 

 

생각 정리

얼마전에 Marker Annotation 과 리플렉션을 이용하여 NoSQL 에 엔티티에 대한 before, after, auditor 에 대한 로그를 쌓는 기능을 개발했는데 CGLib 를 이용해서 별도의 라이브러리로 만들어서 사용할 수 있을것같다. 시간나면 해봐야겠다.

'Java' 카테고리의 다른 글

부동소수점과 BigDecimal  (0) 2022.03.18
ThreadLocal - 쓰레드 내에서 변수를 공유하고 싶다면  (0) 2022.02.17