S E P H ' S

[Spring] 9. Spring AOP - 동적 프록시 본문

Programing & Coding/Spring

[Spring] 9. Spring AOP - 동적 프록시

yoseph0310 2023. 6. 25. 20:27

AOP 포스팅

4. Spring AOP와 디자인 패턴 (1)

5. Spring AOP - 총정리 (2)

 

 

AOP를 달성하기 위해 사용한 디자인 패턴을 알아보면서 어떤 식으로 개선을 해왔는지 알아봤다. 하지만 5번 포스팅의 마지막 정리 부분에서 말했듯이 프록시 패턴이나 데코레이터 패턴을 사용하더라도 부가 기능을 도입하고자 하는 클래스의 개수만큼 필요한 클래스를 만들어야 한다는 문제점이 있었다. 이를 동적 프록시 적용 (JDK 동적 프록시, CGLIB, ProxyFactory) 으로 해결할 수 있다고 했었다.


동적 프록시(Dynamic Proxy)

앞서 말했듯이 프록시를 적용해야할 클래스가 너무 많다면 프록시 클래스를 그 개수 만큼 일일이 만드는 것은 엄청난 노동이다. 이를 해소하기 위한 기술이 동적 프록시이다. 동적인 시점 즉, 런타임 시점에 프록시를 자동으로 만들어서 적용해주는 기술이다. 자바에서의 대표적인 동적 프록시 기술은 JDK 동적 프록시와 CGLIB(Byte Code Generator LIBrary)가 있다. 두 기술 모두 동적 프록시를 가능케 하지만 차이점은 다음과 같다.

 

JDK 동적 프록시

  • 인터페이스 기반으로 프록시를 생성한다.
  • 자바에서는 리플렉션을 사용하여 프록시를 생성한다. (JDK 동적 프록시 관련 클래스도 reflect 패키지에 존재한다.)

CGLIB

  • 클래스 기반(인터페이스도 가능)으로 프록시를 생성한다.
  • ASM 프레임워크를 활용하여 바이트코드를 조작하여 프록시를 생성한다. (리플렉션도 적절히 활용)
ASM 프레임워크

Java Byte Code 조작 및 분석 프레임워크. 클래스를 동적으로 생성하거나 수정할 수 있다. 같은 기능을 하는 BCEL이라는 것이 있지만 성능과 속도면에서 ASM이 우수하다. ASM 프레임워크의 동작 원리의 핵심은 리플렉션과 다르게 클래스를 클래스로더에 로딩하지 않고도 클래스의 구조를 파악할 수 있다는 점이다.
리플렉션(Reflection)

JVM에서 실행되는 애플리케이션의 런타임 동작을 검사하거나 수정할 수 있는 기능이 필요한 프로그램에서 사용된다.
클래스의 구조를 개발자가 확인할 수 있고, 값을 가져오거나 메소드를 호출하는데 사용된다.

동적 프록시의 필요성

 

@Override
public void save(String itemId) {
    TraceStatus status = null;
    
    try {
    	status = logTrace.begin("orderRepository.save()");
        
        // 로직 호출
        target.save(itemId);
        logTrace.end(status);
        
    } catch (Exception e) {
    	logTrace.exception(status, e);
        throw e;
    }
}

 

@Override
public void orderItem(String itemId) {
    TraceStatus status = null;
    
    try {
    	status = logTrace.begin("orderService.orderItem()");
        
        // 로직 호출
        target.orderItem(itemId);
        logTrace.end(status);
        
    } catch (Exception e) {
    	logTrace.exception(status, e);
        throw e;
    }
}

 

4. Spring AOP와 디자인 패턴 (1) 에서 작성되었던 프록시 객체의 코드이다. 살펴보자면 다음과 같은 사실을 알 수 있다. 

  1. 부가 기능을 적용하는 코드가 거의 동일하다.
  2. 실제 타겟 객체를 불러오는 시점도 거의 동일하다.

코드는 거의 비슷한데 메소드나 다른 방법으로 뽑아내기가 쉽지 않다는 것을 알 수 있다. 왜냐하면 target이 호출하는 함수명이 다르기 때문이다. 1번 코드는 save, 2번 코드는 orderItem을 호출했다. 메소드 이름이 다르기 때문에 뽑아내기가 쉽지 않다.


자바의 리플렉션 활용

실행되는 시점에 동적으로 프록시를 생성하고 동적으로 메소드를 넘겨주면 된다. 동적으로 프록시를 생성하려면 자바의 리플렉션 기술을 활용해야 한다. 자바의 리플렉션 기술을 사용하여 클래스나 메소드의 메타 정보를 동적으로 확보하고 이를 이용하여 동적 프록시를 생성하여 코드도 동적으로 호출할 수 있다.

 

@Test
void reflection1() throws Exception {
    Hello hello = new Hello();
    
    // 클래스 메타 정보를 받아온다.
    Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
    
    // 클래스 메타 정보에서 메소드 명으로 검색하여 메소드를 뽑아온다.
    Method methodCallA = classHello.getMethod("callA");
    
    // 메소드를 실행하며 인스턴스를 넘겨준다.
    Object result1 = methodCallA.invoke(hello);
    
    log.info("result1 = {}", result1);
}

 

위는 자바의 리플렉션을 통해 메소드를 동적으로 불러온 테스트 코드이다. 여기서 알 수 있는 내용은 다음과 같다.

  1. 클래스 메타 정보를 받아오면, 그 메타 정보에서 메소드를 뽑을 수 있다.
  2. 메소드는 문자로 검색하는데 클래스 명은 인자로 넘겨 동적으로 검색할 수 있다.
  3. 반환받은 메소드에 객체를 넘겨서 실행하면 원하는 결과를 얻을 수 있다.

자바 리플렉션 보다는 JDK 동적 프록시, CGLIB를 이용하자

자바 리플렉션은 치명적인 단점이 있다. 문자열로 전달되기 때문에 잘못된 값을 전달하더라도 컴파일 시점에 오류를 검출할 수 없다. 이런 이유 때문에 유용한 기술이나 사용하기 까다롭다. 리플렉션을 사용한 동적 메소드 호출은 되도록 사용하지 않도록 한다.

 

제목에서 언급한 것처럼 JDK 동적 프록시, CGLIB를 사용하자. 위에서 간단히 설명했지만 이것들에 대해서 다시 한번 간단히 짚고 넘어가자. 

 

JDK 동적 프록시

  • InvocationHandler 인터페이스를 구현하여 사용
  • 반드시 인터페이스가 존재해야함

CGLIB

  • MethodInterceptor 인터페이스를 구현하여 사용
  • 인터페이스가 없어도 구현 가능

JDK 동적 프록시 사용 예제 - 반드시 인터페이스가 있어야함

JDK 동적 프록시는 자바의 리플렉션 기술을 활용하여 동적으로 프록시를 생성한다. 동적으로 프록시를 생성하려면 다음과 같은 방법으로 해야한다.

 

  1. InvocationHandler 인터페이스를 구현한 구현체를 만든다.
  2. 프록시가 참조할 실제 객체를 만든다.
  3. Proxy.newProxyInstance 정보를 통해 프록시 객체를 가져온다. 이때, 클래스 로더와 클래스 정보의 메타 정보를 가져온다.

리플렉션이 클래스의 메타정보를 바탕으로 메소드를 동적으로 불러온다는 것을 생각해보면 방식이 크게 다르지는 않다.

 

InvocationHandler를 구현한 클래스 - 동적 플래시의 몸통 역할

 

@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
    
    private final Object target;
    
    public TimeInvocationHandler(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();
        Object result = method.invoke(target, args);
        
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        
        log.info("TimeProxy 종료, resultTime = {}", resultTime);
        return result;
    }
}
  • InvocationHandler를 구현한다.
  • InvocationHandler는 프록시 객체의 뼈대가 된다. 따라서 내부적으로 참조할 타겟을 가져야 한다.
  • InvocationHandler의 invokedpsms Proxy 정보, 동적 메소드 정보, 인수가 함께 넘어온다.
  • method.invoke() 를 통해 실제 객체를 불러올 수 있다. 이때 invoke에 Target과 인수를 넘겨주면 된다.

 

테스트 코드에서 프록시 객체 동적으로 생성하기

 

@Test
void dynamicJDKA() {
    // 타겟 객체 생성
    AImpl a = new AImpl();
    
    // 프록시 객체 몸통 만들기
    TimeInvocationHandler handler = new TimeInvocationHandler(a);
    
    // 메타 정보와 프록시 몸통 정보 넘겨주고 프록시 만들기
    AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), 
    							new Class[]{AInterface.class}, handler);
    
    // 동적 프록시를 통해 메소드 정보가 넘어감.
    proxy.call();
}
  • 넘겨줄 타겟을 만든다.
  • 프록시 몸통을 만드나. 이때 이미 InvocationHandler를 구현해둔 객체를 생성하고 타겟값을 넘겨준다. 이 시점에 프록시 몸통은 실제 타겟을 알게 된다.
  • Proxy.newProxyInstance에 클래스 로더 정보, 클래스 정보, 프록시 몸통 정보를 넘겨주고 프록시를 생성한다.
  • proxy.call()을 하면 프록시를 호출한다.
    • 프록시를 호출하면 자동으로 InvocationHandler의 invoke 메소드가 실행된다.
    • 이때 메소드명 + 클래스 메타 정보를 통해 메소드가 invoke에 넘어가게 된다.
    • 프록시는 이미 실제 타겟을 알고 있다.
    • 따라서 프록시 객체는 동적으로 원하는 타겟, 원하는 메소드를 뽑아와서 실행한다.

 

실행 순서 정리

  1. 클라이언트는 JDK 동적 프록시의 call() 을 실행한다.
  2. JDK 동적 프록시는 InvocationHandler.invoke() 를 호출한다. TimeInvocationHandler가 구현체로 존재하므로 TimeInvocationHandler.invoke()가 호출된다.
  3. TimeInvocationHandler가 내부 로직을 수행하고 method.invoke(target, args)를 호출하여 target인 실제 객체 (AImpl)를 호출한다.
  4. AImpl 인스턴스의 call()이 실행된다.
  5. AImpl 인스턴스의 call()의 실행이 끝나면 TimeInvocationHandler로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.

 

JDK 동적 프록시로 만들어지는 프록시 객체 확인

  • 타겟 클래스와 JDK 동적 프록시로 생성한 프록시를 .getClass()로 확인한것
  • JDK 동적 프록시는 "com.sun.proxy"라는 이름으로 만들어진다.

 

정리

 

JDK 동적 프록시를 이용하면 '클래스 정보, 클래스 로더 정보, 타겟 정보'를 넘겨주어 동적으로 프록시를 생성할 수 있다. 이 때 InvocationHandler를 구현한 구현체를 만들어야 하는데, 이 구현체는 각 프록시 객체가 사용할 몸통이라고 생각하면 된다.

 

JDK 동적 프록시로 만들어진 프록시 객체를 통해 메소드를 실행하면, 메소드는 자동으로 InvocationHandler.invoke() 메소드를 실행한다. 따라서 필요한 프록시 기능은 InvocationHandler.invoke() 안에 구현해두면 된다. 결과적으로 핵심 기능과 부가 기능을 나누게 되며 단일 책임 원칙(SRP)를 지킬 수 있게 되고, 프록시 클래스를 많이 만들어야 하는 문제도 해결할 수 있다.

 

<JDK 동적 프록시 도입 전 - 직접 프록시 생성>

JDK 동적 프록시 도입 전에는 이렇게 각 인터페이스마다 그에 맞는 프록시를 하나씩 생성하여 의존관계를 설정해야 했다.

 

또한 런타임에서 클라이언트는 aProxy에 aProxy는 실제 객체에 의존했다.

 

 

<JDK 동적 프록시 도입 후>

JDK 동적 프록시를 도입한 후에는 개발자는 공통으로 쓰일 프록시 몸통인 InvocationHandler만 구현하고 의존관계를 주입하기만 하면 된다. 즉, 프록시 인터페이스 하나만 만들어 나눠쓰면 되는 것이다.

 

그리고 클라이언트가 동적 프록시를 호출하는 순간 InvocationHandler.invoke()가 실행되는데 이 때 메소드 메타 정보가 동적으로 전달된다. 그리고 이 메타정보는 invoke 메소드 내에서 method.invoke를 통해 동적으로 실행할 수 있게 된다.

 


JDK 동적 프록시, 로그 추적기 도입

JDK 동적 프록시를 구현하는 과정을 정리해보면 다음과 같다.

  1. JDK 동적 프록시를 생성하려면 클래스 메타 정보, 클래스 정보, 프록시 몸통 정보가 주어져야한다.
  2. JDK 동적 프록시로 생성된 프록시를 실행하면, 실행하는 시점에 InvocationHandler.invoke()가 실행되면서 동적으로 메소드명이 넘어간다.
  3. 프록시의 몸통은 InvocationHandler의 구현체이다. 이때 내부적으로 Target을 갖도록 설계하여 어떤 타겟을 실행해야 하는지 알고 있다.
  4. JDK 동적 프록시를 생성하려면 반드시 인터페이스가 존재해야한다.

JDK 동적 프록시를 통해 넘어온 메소드를 동적으로 실행할 수 있기 때문에 반복적으로 프록시 클래스를 만들 필요가 없어졌다.

 

 

로그 추적기 적용을 위한 InvocationHandler 구현

InvocationHandler는 프록시 객체가 사용할 몸통이다. 따라서 기존의 프록시 형태를 가져야한다. 내부적으로 Target 객체를 가지도록 한다. 그리고 메소드를 오버라이딩 해준다. 타겟의 실행은 method.invoke(target, args) 로 실행하도록 한다.

 

public class LogTraceBasicHandler implements InvocationHandler {
    
    private final Object target;
    private final LogTrace logTrace;
    
    public LogTraceBasicHandler(Object target, LogTrace logTrace) {
        this.target = target;
        this.logTrace = logTrace;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        TraceStatus status = null;
        
        try {
            String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
            status = logTrace.begin(message);
            
            // 핵심 기능
            Object result = method.invoke(target, args);
            
            logTrace.end(status);
            return result;
            
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

 

  • 내부적으로 타겟 참조값을 가졌다.
  • 생성자를 통해 의존관계 주입을 해준다.
  • method.invoke(target, args)를 통해 실제 객체를 실행해준다.

 

JDK 동적 프록시의 의존관계 설정

 

@Configuration
public class LogTraceBasicConfig {
    
    @Bean
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
        OrderControllerV1 target = new OrderControllerV1Impl(orderServiceV1(logTrace));
        
        // 프록시 몸통 완성
        LogTraceBasicHandler handler = new LogTraceBasicHandler(target, logTrace);
        
        // 클래스 메타 정보 + 프록시 몸통 정보 넘겨줌. 프록시 생성
        OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader()),
                                                                             new Class[]{OrderControllerV1.class}, handler);
                                                                             
        return proxy;                              
    }
    
    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
        OrderServiceV1 target = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
        
        // 프록시 몸통 완성
        LogTraceBasicHandler handler = new LogTraceBasicHandler(target, logTrace);

        // 클래스 메타 정보 + 프록시 몸통 정보 넘겨줌. 프록시 생성
        OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader()),
                                                                             new Class[]{OrderServiceV1.class}, handler);
                                                                             
        return proxy;                                                                   
    }
    
    @Bean
    public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
        OrderRepositoryV1 target = new OrderRepositoryV1Impl();
        
        // 프록시 몸통 완성
        LogTraceBasicHandler handler = new LogTraceBasicHandler(target, logTrace);

        // 클래스 메타 정보 + 프록시 몸통 정보 넘겨줌. 프록시 생성
        OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(OrderRepositoryV1.class.getClassLoader()),
                                                                             new Class[]{OrderRepositoryV1.class}, handler);
                                                                             
        return proxy;
    }
}

 

  • 실제 객체(Target) 하나 생성.
  • 프록시 몸통(InvocationHandler)에 실제 객체를 넘겨 참조 가능하도록 한다.
  • Proxy.newProxyInstance에 클래스 로더 정보, 클래스 정보, 프록시 몸통 정보를 던져 프록시 생성.
  • 만들어진 프록시 객체를 반환.

 

정리

클래스 의존관계

직접 프록시 사용

직접 클래스 별로 프록시 객체를 만들어 등록하면 위와 같은 의존관계를 가진다. 인터페이스를 프록시 객체가 구현한다. 여기에 JDK 동적 프록시를 사용하면 아래와 같은 의존관계가 된다.

 

JDK 동적 프록시 사용

JDK 동적 프록시를 사용하면 각 클래스에 대한 구현체를 만들 필요가 없다. InvocationHandler라는 프록시 몸통을 구현하고 이것을 공용으로 사용할 수 있기 때문이다.

 

 

런타임 객체 의존 관계

직접 프록시 사용

 

직접 프록시를 사용하면 클라이언트는 프록시 객체를 호출한다. 프록시 객체는 내부적으로 실제 객체를 참조하고 있고 이 실제 객체를 실행한다. 또 이 실제 객체는 다른 프록시 객체를 참조하면서 내부적으로 프록시 체이닝이 발생한다.

 

 

JDK 동적 프록시 사용

JDK 프록시를 사용하면 클래스가 동적 프록시를 호출하는 순간 InvocationHandler.invoke()가 실행된다. 이때, 이미 타겟 정보를 알고 있는 상황이고 메소드가 동적으로 넘어온다. 그리고 동적으로 넘어온 메소드가 method.invoke() 되면서 실제 타겟이 실행되는 방향이 된다.

 

 

로그 추적기 적용을 위한 InvocationHandler 구현 - 유사 포인트 컷

내부적으로 String 문자열을 가져 포인트 컷을 가져갈 수도 있다. 특정 String 문자열이 내가 실행할 메소드의 이름이라고 지정한다. 그래서 이 이름과 패턴이 매칭되지 않으면, 부가 기능을 실행하지 않도록 할 수 있다.

 

public class LogTraceFilterHandler implements InvocationHandler {
    /.../
    private final String[] patterns;
    
    @Override
    public Object invoke(Object target, Method method, Object[] args) throw Throwable {
        
        String name = method.getName();
        // 포인트 컷 구현 지점
        if (!PatternMatchUtils.simpleMatch(patterns, name)) {
            return method.invoke(target);
        }
        
        // 성공 로직
        TraceStatus status = null;
        
        try {
            String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
            status = logTrace.begin(message);
            
            // 핵심 기능
            Object result = method.invoke(target, args);
            
            logTrace.end(status);
            return result;
            
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

위 InvocationHandler가 포인트 컷을 원시적으로 구현한 형태이다. 넘어온 문자열 패턴과 현재 동적으로 넘어온 메소드의 이름이 매치되지 않으면, 실행하지 않는다.

 

그런데 한가지 주의해야할 것이 있는데, 바로 매칭이 되지 않았을 때 전체 프로그램의 흐름은 정상적으로 돌아가야한다는 점이다. 부가기능은 실행하지 않고 타겟은 실행해야 한다. 따라서 매칭이 되지 않았을 때라도 method.invoke()는 반드시 실행해야한다.

 

JDK 동적 프록시의 한계

JDK 동적 프록시의 한계는 반드시 인터페이스가 필요하다는 점이다. 실무에서는 항상 인터페이스만 있지 않다. 구현체만 있어도 동적으로 프록시를 생성해야한다. 인터페이스 없이 구현체만 있는 클래스의 프록시를 동적으로 생성하기 위해 CGLIB라는 라이브러리를 사용한다.

 


CGLIB : Code Generator Library

CGLIB는 바이트 코드를 조작하여 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다. 따라서 인터페이스가 없이 구체클래스만으로 필요한 동적 프록시를 만들어낼 수 있다. 따라서 구체 클래스만 있을 경우 CGLIB 기술을 사용하여 동적 프록시를 생성하도록 한다.

 

스프링은 동적 프록시 코드 추상화를 ProxyFactory로 제공한다. ProxyFactory에 CGLIB 기술을 도입해서 구체 클래스를 동적 프록시로 생성해준다.

 


CGLIB 라이브러리를 이용한 동적 프록시 생성 테스트 코드

CGLIB도 JDK 동적 프록시 생성과 맥락은 크게 다르지 않다. 프록시 몸통을 만들고 그 몸통에 타겟을 넘긴다. 그리고 일련의 과정을 통해 프록시 객체를 만들어낸다. 프록시 몸통은 다음과 같다.

  • JDK 동적 프록시 : InvocationHanlder의 구현
  • CGLIB 동적 프록시 : MethodInterceptor의 구현

따라서 MethodInterceptor를 구현하고 그걸 프록시를 만드는 CGLIB 객체에 전달하면 된다.

 

MethodInterceptor 구현 : CGLIB 동적 프록시 구현을 위한 프록시 몸통

 

@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {
    
    private final Object target;
    
    public TimeMethodInterceptor(Object target) {
        this.target = target;
    }
    
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy 
    methodProxy) throws Throwable {
        log.info("프록시 객체 실행");
        
        long startTime = System.currentTimeMillis();
        
        Object result = methodProxy.invoke(target, args);
        
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        
        log.info("프록시 객체 종료, resultTime = {}", resultTime);
        return result;
    }
}
  • JDK 동적 프록시의 InvocationHandler와 구조가 거의 동일
  • method.invoke()도 가능하나 methodProxy.invoke() 하는 것이 성능이 더 낫다고 함.

 

CGLIB로 프록시 생성하기

 

@Test
void cglibTest() {
    
    // 타겟 생성
    ConcreteService target = new ConcreteService();
    
    // 프록시 몸통 생성
    TimeMethodInterceptor timeMethodInterceptor = new TimeMethodInterceptor(target);
    
    // CGLIB 프록시 생성자 만들기
    Enhancer enhancer = new Enhancer();
    
    // 어떤 구체 클래스를 만들지 명시
    enhancer.setSuperclass(ConcreteService.class);
    
    // 몸통 정보를 CGLIB 생성자에게 알려줌
    enhancer.setCallback(timeMethodInterceptor);
    
    // CGLIB 생성자로부터 프록시 가져오기
    ConcreteService proxy = (ConcreteService) enhancer.create();
    proxy.call();
    
    log.info("target = {}", target.getClass());
    log.info("proxy = {}", proxy.getClass());
}
  1. 타겟을 먼저 생성한다.
  2. MethodInterceptor 구체 클래스를 생성하며 타겟을 넘긴다. 이때 생성되는 것은 프록시 몸통 정보이다.
  3. CGLIB 프록시 생성자 역할을 할 Enhancer()를 만든다.
  4. Enhancer에 setCallback을 통해 프록시 몸통 정보를 넘긴다.
  5. Enhancer에 어떤 클래스를 대상으로 만들지 정보를 알린다.
  6. Enhancer를 통해 프록시를 생성한다.

 

CGLIB로 생성한 클래스 살펴보기

타겟 클래스와 CGLIB로 생성한 프록시의 클래스이다. CGLIB로 생성된 프록시 객체는 EnhancerByCGLIB가 붙는 것을 알 수 있다.

 

CGLIB 정리

클래스 의존관계

CGLIB 클래스 의존관계는 다음과 같다. Enhancer의 SuperClass로 ConcreteService에 의존하는 것을 알려주어 프록시를 만든다. 그리고 이 CGLIB에 프록시 몸통(MethodInterceptor)를 던져준다.

 

런타임 객체 의존관계

 

런타임 객체 의존관계도 JDK와 크게 다르지 않다. 클라이언트가 CGLIB 프록시를 호출하면 CGLIB 호출은 바로 MethodInterceptor.interceptor()를 바로 실행한다. 그리고 interceptor() 안에서 Method.invoke() 혹은 MethodProxy.invoke()를 통해 실제 객체를 실행한다.

 


JDK 동적 프록시, CGLIB 동적 프록시 마지막 정리

  • JDK 동적 프록시
    • 자바의 리플렉션 기술로 동적 프록시를 만든다.
    • 클래스 로더 정보, 클래스 정보, 프록시 몸통 정보, 타겟 정보를 넘겨 동적으로 프록시를 생성해 메소드를 실행한다.
    • 프록시 몸통 정보는 InvocationHandler를 구현한다. 이것은 공통으로 사용될 프록시 몸통이다.
    • 생성은 Proxy.newProxyInstance()에 정보를 넘겨 만든다.
    • 반드시 프록시 객체를 생성할 타겟의 인터페이스가 있어야 한다.
    • JDK 동적 프록시 객체를 호출하면 invoationHandler.invoke()가 바로 실행된다. 이때 동적으로 메소드 정보가 넘어간다.
    • JDK 동적 프록시로 만들어진 프록시는 com.sun.proxy로 표시된다.
    • 프록시 몸통에 어떤 클래스를 기본으로 할지 전달해준다. 따라서 실제 객체와 바꿔치기를 할 수 있다.
  • CGLIB 동적 프록시
    • CGLIB 라이브러리를 이용해 바이트 코드 조작을 통해 동적으로 프록시를 생성한다.
    • CGLIB의 Enhancer를 통해 만든다.
    • 구체 클래스 정보(SuperClass)와 공통 프록시 몸통 정보(MethodInterceptor)를 넘겨 만든다.
    • MethodInterceptor의 구현체가 공통 프록시 몸통 정보가 된다.
    • CGLIB는 바이트 코드를 조작하여 실제 구체 클래스를 상속받은 클래스로 프록시 객체를 만든다.
    • CGLIB 동적 프록시 객체를 호출하면 MethodInterceptor.intercept()가 바로 실행된다. 이때 동적으로 메소드 정보가 넘어간다.
    • CGLIB로 만들어진 프록시는 EnhancerByCGLIB가 붙는다.
    • CGLIB에 SuperClass를 지정하여 어떤 것을 상속받아 구현할지 알려줌. 따라서 실제 객체와 바꿔치기가 가능하다.

정리

굉장히 내용이 많아 복잡하지만 한차례씩 정독하고 나니 이해가 조금씩은 되는 것 같다. 

 

하지만 이들의 한계점 또한 존재한다. 인터페이스가 있는 곳에는 JDK, 없는 곳은 CGLIB로 동적 프록시를 생성해줘야 하는데 그러면 의존관계 주입이 2배가 된다. 또한 MethodInterceptor와 InvocationHandler를 각각 2개를 만들어 등록해야한다. 이런 반복적인 부분을 스프링은 프록시 팩토리라는 것으로 추상화하여 제공한다. 다음 포스팅은 이것에 대해서 다뤄볼 것이다.

 


출처 

 

스프링 AOP : 동적 프록시 적용(JDK 동적 프록시, CGLIB)

이 글은 인프런의 김영한님 강의를 보고 복습하며 정리한 글입니다. 동적 프록시의 필요 앞선 글(https://ojt90902.tistory.com/699)에서 프록시 개념을 도입했다. 다형성을 이용해 프록시 개념을 많이 도

ojt90902.tistory.com