[Back-end]/[Spring]

[Spring] 다이내믹 프록시(DynamicProxy)

DevLoki 2021. 11. 30. 23:54

(프록시에 대한 이해가 부족하신 분들은 이전 포스팅을 참고하세요!!)

 

[Spring] 프록시와 디자인패턴

프록시와 디자인 패턴 스프링의 3대 기반기술 중 AOP를 공부하던 중 관심사 분리를 위한 다이내믹 프록시와 팩토리 빈이라는 개념의 등장에 당황했습니다. 평소 객체지향과 디자인 패턴을 공부

yejun-the-developer.tistory.com

 

프록시를 사용하여 기존코드를 수정하지 않고, 타깃의 기능을 추가하거나 접근을 제한할 수 있었습니다. 그러나 이런 프록시에도 아직 여러 단점이 존재합니다.

  1. 프록시가 멤버변수로 타깃 오브젝트를 가지고 있기 때문에 타깃오브젝트에 종속적입니다. 똑같은 기능을 수행하는 프록시라 하더라도, 여러 타깃에 적용하려면 타깃의 갯수만큼 프록시를 생성해야합니다. 이때 프록시는 똑같은 기능을 수행하기 때문에 많은 코드 중복이 일어납니다.
  2. 프록시를 사용하지 않는 메서드에도 타깃으로 위임하는 메서드를 작성해야합니다. 또한 타깃인터페이스 메서드의 변경에 영향을 받아 함께 수정해야합니다.

위와 같은 문제를 해결하는 방법이 바로 다이내믹(동적)프록시 입니다. 이름 그대로 프록시를 개발자가 직접 생성하는 것이 아닌 런타임에 동적으로 생성해줍니다.


다이내믹 프록시

다이내믹 프록시는 리플렉션 기능을 이용하여 프록시를 동적으로 생성해줍니다.

리플렉션(Reflection)

리플렉션은 '구체적인 클래스 타입을 알지 못해도 그 클래스의 변수, 메서드, 타입에 접근할 수 있도록 해주는 자바 API'입니다. 예를 들어, class A 를 생성하고 자동타입 변환을 이용해 Object a = new A(); 객체를 생성했을 때, a만을 가지고 클래스 타입을 알수 없습니다. 하지만 리플렉션을 이용하면 a.getClass()를 사용해서 클래스의 메타 정보를 가져오거나, Method method = a.class.getMethod("메서드명")을 사용하여 메서드 타입 오브젝트를 생성하고 정보를 조회할 수 있습니다.

Method 인터페이스에 정의된 invoke() 메서드를 사용하여 특정 오브젝트의 메서드를 추상화하여 실행시킬 수 있습니다.

Method 인터페이스 invoke() 메서드의 구조는 다음과 같습니다.

public Object invoke(Object obj, Object... args){}

파라미터로 타깃오브젝트(obj)와 메서드의 파라미터 목록(args)을 받아 실행시킨 후 결과를 Object 타입으로 반환합니다.

분류

다이내믹 프록시는 크게 인터페이스 유무에 따라 크게 두가지로 분류할 수 있습니다.

  • 인터페이스가 존재하는 경우 → JDK동적 프록시
  • 인터페이스가 없는 경우 → CGLIB

JDK동적 프록시

JDK 동적 프록시는 구현클래스가 아닌 인터페이스를 기반으로 프록시를 생성하기 때문에 인터페이스가 반드시 필요합니다.

JDK 동적 프록시를 만드는 방법은 간단합니다.

java.lang.reflect의 InvocationHandler를 구현하면 됩니다.

InvocationHandler는 invoke()메서드 한개만 가진 인터페이스 입니다.

public Object invoke(Object proxy, Method method, Object[] args)

리플렉트의 invoke()와 구조가 비슷합니다.

  • proxy : 자기 자신을 나타냅니다.
  • method : 호출된 메서드입니다.
  • args : 호출된 메서드의 매개변수입니다.

InvocationHandler 요청처리 구조

예시

public class TestInvocationHandler implements InvocationHandler {

    private TargetInterface target; //타깃 오브젝트를 멤버 변수로 유지해야 합니다.

    public TestInvocationHandler(TargetInterface target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        log.info("프록시 부가기능 시작");  //여기에 부가기능 로직을 추가
        Object result = method.invoke(target, args);  //실제 타깃 오브젝트로 요청을 위임합니다.
        log.info("프록시 부가기능 종료");  //여기에 부가기능 로직을 추가

        return result;
    }
}
  • class TargetInterface : run()단일 메서드를 가지고 있는 인터페이스입니다. (이전글 참조)
  • private TargetInterface target; : 다이내믹 프록시로부터 전달받은 요청을 실제 타깃에게 위임해야 하기 때문에 멤버변수로 주입을 받습니다.
  • public Object invoke() : 다이내믹 프록시로부터 요청을 받으면 자동으로 실행되는 메서드로 매게변수에 method 나 args 등이 자동으로 주입이 되기 때문에, 부가 기능과 실제타깃으로 요청을 위임하는 코드만 작성할 수 있습니다.

테스트 코드

@Test
void testInvocationHandler(){
    TargetInterface target = new TargetImpl();
    TestInvocationHandler invocationHandler = new TestInvocationHandler(target);

    //프록시 생성
    TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(TargetInterface.class.getClassLoader(), new Class[]{TargetInterface.class}, invocationHandler);

    //프록시 실행
    proxy.run();
    log.info("proxy = {}", proxy.getClass());

}

테스트 코드 결과화면

결과분석

proxy.run() 메서드가 실행되면 그림과 같은 순서로 호출됩니다.

JDK동적 프록시 요청 흐름

다른 타깃에 같은 부가기능을 수행하는 프록시를 적용하려면 기존에는 적용하려는 타깃의 인터페이스와 일치하는 프록시 인터페이스를 만들어야 했습니다. 그러나 JDK 동적 프록시, 즉 InvocationHandler를 사용하여 타깃에 종속적인 프록시 인터페이스 생성없이 Proxy.newProxyInstance(...)를 사용하여 동적으로 생성할수 있게 되었습니다.


CGLIB

평소에 스프링을 사용하던 중 빈을 출력했을 때 다음과 같이 EnhancerByCGLIB 를 보신 경험이 있을 겁니다.

이는 바로 CGLIB를 사용하여 프록시를 생성했기 때문에 생기는 마크였습니다.

CGLIB는 JDK동적 프록시와 달리 바이트 코드를 조작해서 동적으로 클래스를 생성해주는 라이브러리 입니다. 또한 인터페이스 없이 구체 클래스만 존재하더라도 프록시를 생성할 수 있습니다.

CGLIB는 MethodInterceptor 를 통해 구현합니다.

public interface MethodInterceptor extends Callback{
    Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}

MethodInterceptor는 단일 메서드 인터페이스로 JDK 동적 프록시의 InvocationHandler와 굉장히 유사한 구조와 동작 방식을 가지고 있습니다.

  • obj : CGLIB가 적용된 객체입니다.
  • method : 호출된 메서드입니다.
  • args : 호출된 메서드의 매개변수입니다.
  • proxy : 실제 타깃의 메서드를 invoke 할 때 사용됩니다. method를 이용하여 실행이 가능하지만 성능상 proxy.invoke()를 권장합니다.

예시

public class TestMethodInterceptor implements MethodInterceptor {

    private TargetImpl target; //타깃 오브젝트를 멤버 변수로 유지해야 합니다.

    public TestMethodInterceptor(TargetImpl target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

        log.info("프록시 부가기능 시작");  //여기에 부가기능 로직을 추가
        Object result = proxy.invoke(target, args);  //실제 타깃 오브젝트로 요청을 위임합니다.
        log.info("프록시 부가기능 종료");  //여기에 부가기능 로직을 추가

        return result;

    }
}

테스트 코드

@Test
public void testMethodInterceptor(){

    TargetImpl target = new TargetImpl();

    //프록시 생성
    Enhancer enhancer = new Enhancer();  //Enhancer 오브젝트를 생성
    enhancer.setSuperclass(TargetImpl.class);  //생성할 프록시의 타입을 지정합니다(TargetImpl을 상속합니다.)
    enhancer.setCallback(new TestMethodInterceptor(target));  //MethodInterceptor를 설정합니다.
    TargetImpl proxy = (TargetImpl) enhancer.create();  //프록시를 생성한 후 타입 캐스팅

    proxy.run();
    log.info("proxy = {}", proxy.getClass());
}

결과화면


정리

  • 다이내믹 프록시는 런타임시 동적으로 프록시를 생성해주는 기술입니다.
  • 다이내믹 프록시는 리플렉션이라는 기술로 구현합니다.
  • 인터페이스가 있으면 JDK 동적 프록시, 없으면 CGLIB를 사용합니다.
  • JDK 동적 프록시 -> InvocationHandler
  • CGLIB -> MethodInterceptor

다이내믹 프록시를 적용함으로써 많은 코드를 개선하며 클래스 수와 중복을 줄일 수 있었지만, 인터페이스 유무에 따라 JDK 동적 프록시와 CGLIB를 만들어야 하는 불편함이 존재합니다. 이런 문제를 해결하기 위해서 프록시 팩토리를 사용합니다.

 

읽어주셔서 감사합니다.