0. 리플렉션

  • 자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다.
  • JDK 동적 프록시를 이해하기 위해서는 먼저 자바의 리플렉션 기술을 이해해야 한다. 리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.

ReflectionTest

  • package hello.proxy.jdkdynamic;
      
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
      
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
      
    @Slf4j
    public class ReflectionTest {
      
        @Test
        void reflection0() {
            Hello target = new Hello();
      
            //공통 로직 1 시작
            log.info("start");
            String result1 = target.callA();
            log.info("result1={}", result1);
            //공통 로직 1 종료
            //
            // 공통 로직 2 시작
            log.info("start");
            String result2 = target.callB();
            log.info("result2={}", result2);
            //공통 로직 2 종료
        }
          
        @Slf4j
        static class Hello{
            public String callA(){
                log.info("callA");
                return "A";
            }
      
            public String callB(){
                log.info("callB");
                return "B";
            }
        }
    }
      
    
  • reflection0() : 공통 로직1과 공통 로직2는 호출하는 메서드만 다르고 전체 코드 흐름이 완전히 같다.
  • 여기서 공통 로직1과 공통 로직 2를 하나의 메서드로 뽑아서 합치는 건 쉬워 보이지만 메서드로 뽑아서 공통화하는 것이 생각보다 어렵다. 왜냐하면 중간에 호출하는 메서드가 다르기 때문이다.
  • 이럴 때 사용하는 기술이 바로 리플렉션이다.

ReflectionTest - reflection1 추가

  • @Test
    void reflection1() throws Exception {
        //클래스 정보
        Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
      
        Hello target = new Hello();
      
        //callA 메서드 정보
        Method methodCallA = classHello.getMethod("callA");
        Object result1 = methodCallA.invoke(target);
        log.info("result={}", result1);
      
        //callB 메서드 정보
        Method methodCallB = classHello.getMethod("callB");
        Object result2 = methodCallB.invoke(target);
        log.info("result={}", result1);
    }
      
    @Slf4j
    static class Hello{
        public String callA(){
            log.info("callA");
            return "A";
        }
      
        public String callB(){
            log.info("callB");
            return "B";
        }
    }
    
    • Class.forName(“hello.proxy.jdkdynamic.ReflectionTest$Hello”) : 클래스 메타정보를 획득한다. 참고로 내부 클래스는 구분을 위해 $ 를 사용한다.
    • classHello.getMethod(“call”) : 해당 클래스의 call 메서드 메타정보를 획득한다.
    • methodCallA.invoke(target) : 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다. 여기서 methodCallA 는 Hello 클래스의 callA() 이라는 메서드 메타정보이다.
      • methodCallA.invoke(인스턴스) 를 호출하면서 인스턴스를 넘겨주면 해당 인스턴스의 callA() 메서드를 찾아서 실행한다. 여기서는 target 의 callA() 메서드를 호출한다

여기서 중요한 핵심은 클래스나 메서드 정보를 동적으로 변경할 수 있다는 점이다

ReflectionTest - reflection2 추가

  •  @Test
        void reflection2() throws Exception {
        //클래스 정보
        Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
        
        Hello target = new Hello();
        
        //callA 메서드 정보
        Method methodCallA = classHello.getMethod("callA");
        dynamicCall(methodCallA, target);
        
        //callB 메서드 정보
        Method methodCallB = classHello.getMethod("callB");
        dynamicCall(methodCallB, target);
    }
      
    private void dynamicCall(Method method, Object target) throws Exception {
        log.info("start");
        Object result = method.invoke(target);
        log.info("result1={}", result);
    }
      
    @Slf4j
    static class Hello{
        public String callA(){
            log.info("callA");
            return "A";
        }
      
        public String callB(){
            log.info("callB");
            return "B";
        }
    }
    
  • dynamicCall(Method method, Object target)

    • 공통 로직1, 공통 로직2를 한번에 처리할 수 있는 통합된 공통 처리 로직이다.
    • Method method : 클래스의 메서드 메타 정보
    • Object target : 실제 실행할 인스턴스 정보가 넘어온다. 타입이 Object 라는 것은 어떠한 인스턴스도 받을 수 있다는 뜻이다.

주의

  • 리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다. 하지만 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
  • 따라서 리플렉션은 일반적으로 사용하면 안된다. 리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.

1. JDK 동적 프록시 - 소개

기본 예제 코드 작성 (src/test)

AInterface / AImpl

  • package hello.proxy.jdkdynamic.code;
    a
    public interface AInterface {
        String call();
    }
    
  • package hello.proxy.jdkdynamic.code;
      
    import lombok.extern.slf4j.Slf4j;
      
    @Slf4j
    public class AImpl implements AInterface{
        @Override
        public String call() {
            log.info("A 호출");
            return "a";
        }
    }
    

BInterface / BImpl

  • package hello.proxy.jdkdynamic.code;
      
    public interface BInterface {
        String call();
    }
    
  • package hello.proxy.jdkdynamic.code;
      
    import lombok.extern.slf4j.Slf4j;
      
    @Slf4j
    public class BImpl implements BInterface{
        @Override
        public String call() {
            log.info("B 호출");
            return "b";
        }
    }
    

2. JDK 동적 프록시 - 예제 코드 (src/test)

  • JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성하면 된다.

JDK 동적 프록시가 제공하는 InvocationHandler

  • package java.lang.reflect;
      
    public interface InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
    }
    

    제공되는 파라미터는 다음과 같다.

    • Object proxy : 프록시 자신
    • Method method : 호출한 메서드
    • Object[] args : 메서드를 호출할 때 전달한 인수

TimeInvocationHandler

  • package hello.proxy.jdkdynamic.code;
      
    import lombok.extern.slf4j.Slf4j;
      
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
      
    @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); //call()
      
            long endTime = System.currentTimeMillis();
            long resultTime = endTime - startTime;
            log.info("TimeProxy 종료 resultTime={}", resultTime);
            return result;
        }
    }
    
  • TimeInvocationHandler 은 InvocationHandler 인터페이스를 구현한다. 이렇게해서 JDK 동적 프록시에 적용할 공통 로직을 개발할 수 있다.

  • Object target : 동적 프록시가 호출할 대상

  • method.invoke(target, args) : 리플렉션을 사용해서 target 인스턴스의 메서드를 실행한다. args 는 메서드 호출시 넘겨줄 인수이다.

JdkDynamicProxyTest

  • package hello.proxy.jdkdynamic;
      
    import hello.proxy.jdkdynamic.code.*;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
      
    import java.lang.reflect.Proxy;
      
    @Slf4j
    public class JdkDynamicProxyTest {
      
        @Test
        void dynamicA(){
            AInterface target = new AImpl();
            //동적 프록시에 적용할 핸들러 로직이다.
            TimeInvocationHandler handler = new TimeInvocationHandler(target);
      
            //동적 프록시는 java.lang.reflect.Proxy 를 통해서 생성할 수 있다.
            //클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다. 그러면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.
            AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);
      
            proxy.call();
            log.info("targetClass={}", target.getClass());
            log.info("proxyClass={}", proxy.getClass());
        }
      
        @Test
        void dynamicB(){
            BInterface target = new BImpl();
            TimeInvocationHandler handler = new TimeInvocationHandler(target);
      
            BInterface proxy = (BInterface) Proxy.newProxyInstance(BInterface.class.getClassLoader(), new Class[]{BInterface.class}, handler);
      
            proxy.call();
            log.info("targetClass={}", target.getClass());
            log.info("proxyClass={}", proxy.getClass());
        }
    }
      
    

dynamicA() 출력 결과

  • TimeInvocationHandler - TimeProxy 실행
    AImpl - A 호출
    TimeInvocationHandler - TimeProxy 종료 resultTime=0
    JdkDynamicProxyTest - targetClass=class hello.proxy.jdkdynamic.code.AImpl
    JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy1
    

생성된 JDK 동적 프록시

  • proxyClass=class com.sun.proxy.$Proxy1 이 부분이 동적으로 생성된 프록시 클래스 정보이다. 이것은 우리가 만든 클래스가 아니라 JDK 동적 프록시가 이름 그대로 동적으로 만들어준 프록시이다. 이 프록시는 TimeInvocationHandler 로직을 실행한다.

실행 순서

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

image-20230323150343288

정리

  • 예제를 보면 AImpl , BImpl 각각 프록시를 만들지 않았다. 프록시는 JDK 동적 프록시를 사용해서 동적으로 만들고 TimeInvocationHandler 는 공통으로 사용했다.
  • JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 된다.

JDK 동적 프록시 도입 후

  • image-20230323150427316
  • image-20230323150434583

3. JDK 동적 프록시 - 적용1

  • JDK 동적 프록시는 인터페이스가 필수이기 때문에 V1 애플리케이션에만 적용할 수 있다.

LogTraceBasicHandler

  • 먼저 LogTrace 를 적용할 수 있는 InvocationHandler 를 만들자.

  • package hello.proxy.config.v2_dynamicproxy;
      
    import hello.proxy.trace.TraceStatus;
    import hello.proxy.trace.logtrace.LogTrace;
      
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
      
    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{
                //LogTrace 에 사용할 메시지이다.
                //Method 를 통해서 호출되는 메서드 정보와 클래스 정보를 동적으로 확인할 수 있기 때문에 이 정보를 사용하면 된다.
                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;
            }
        }
    }
    

DynamicProxyBasicConfig

  • 동적 프록시를 사용하도록 수동 빈 등록을 설정하자.

  • package hello.proxy.config.v2_dynamicproxy;
      
    import hello.proxy.app.v1.*;
    import hello.proxy.trace.logtrace.LogTrace;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
      
    import java.lang.reflect.Proxy;
      
    @Configuration
    public class DynamicProxyBasicConfig {
      
        @Bean
        public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
            OrderControllerV1 orderControllerV1 = new OrderControllerV1Impl(orderServiceV1(logTrace));
            OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
                    new Class[]{OrderControllerV1.class},
                    new LogTraceBasicHandler(orderControllerV1, logTrace));
            return proxy;
        }
      
        @Bean
        public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
            OrderServiceV1 orderServiceV1 = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
            OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader(),
                    new Class[]{OrderServiceV1.class},
                    new LogTraceBasicHandler(orderServiceV1, logTrace));
            return proxy;
        }
      
      
        @Bean
        public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace){
            OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
      
            OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(OrderRepositoryV1.class.getClassLoader(),
                    new Class[]{OrderRepositoryV1.class},
                    new LogTraceBasicHandler(orderRepository, logTrace));
            return proxy;
        }
    }
    

newProxyInstance 매개변수 (from gpt)

  • ClassLoader loader: 다이나믹 프록시 클래스를 로딩하는 데 사용되는 클래스 로더입니다. 다이나믹 프록시 클래스는 런타임 시간에 생성되므로, 기존에 컴파일된 클래스 파일에서는 찾을 수 없습니다. 따라서, 프록시 클래스를 로딩하기 위해서는 새로운 클래스 로더가 필요합니다.
  • Class<?>[] interfaces: 프록시 객체가 구현해야 하는 인터페이스들의 목록입니다. 다이나믹 프록시 객체는 특정 인터페이스를 구현하는 객체이므로, 구현해야 하는 인터페이스를 명시해야 합니다. 이 인터페이스 목록은 배열 형태로 전달됩니다.
  • InvocationHandler h: 다이나믹 프록시 객체에서 메서드 호출 시 실행되는 로직을 정의하는 InvocationHandler 객체입니다. InvocationHandler 인터페이스는 invoke() 메서드를 정의하고 있으며, 다이나믹 프록시 객체에서 메서드가 호출될 때마다 이 메서드가 실행됩니다. invoke() 메서드는 실제 메서드가 호출되기 전에 호출되며, 매개변수로는 프록시 객체, 호출된 메서드, 호출된 메서드의 매개변수 등이 전달됩니다.

오류났던 것

  • newProxyInstance 2번째 매개변수로 orderControllerV1.getClass() 이라고 넣었다.
  • orderControllerV1 = new orderControllerV1Impl(orderServiceV1(logTrace)); 로 구현되어 있으므로 값은 orderControllerV1Impl 이다.
  • 인터페이스 목록이 들어가야 하므로 OrderControllerV1.class 가 들어가야 한다.

.getClass() vs .class (from gpt)

  • .getClass()와 .class 모두 클래스 정보를 가져오는 용도로 사용되는 메서드이지만, 사용 방법과 동작 방식이 다릅니다.
  • .getClass()는 객체의 클래스 정보를 가져오는 메서드입니다. 즉, 해당 객체가 어떤 클래스의 인스턴스인지를 나타내는 클래스 정보를 가져옵니다. 이 메서드는 Object 클래스에 정의되어 있으므로 모든 객체에서 사용할 수 있습니다.
  • 예를 들어, 다음과 같은 코드를 실행하면, 문자열 “Hello”가 String 클래스의 인스턴스인지 확인할 수 있습니다.
javaCopy codeString str = "Hello";
Class clazz = str.getClass();
System.out.println(clazz.getName());  // java.lang.String
  • .class는 클래스의 정보를 나타내는 리터럴(literal)입니다. 이 리터럴을 사용하면, 해당 클래스의 클래스 정보를 가져올 수 있습니다.
  • 예를 들어, 다음과 같은 코드를 실행하면, String 클래스의 클래스 정보를 가져올 수 있습니다.
javaCopy codeClass clazz = String.class;
System.out.println(clazz.getName());  // java.lang.String

즉, .getClass()는 객체를 생성한 클래스의 정보를 가져오고, .class는 클래스 자체의 정보를 가져옵니다.

실행

  • ProxyApplication -> @Import(DynamicProxyBasicConfig.class)
  • 실행 : http://localhost:8080/v1/request?itemId=hello
  • 특 x

그림으로 정리

  • 직접 프록시 사용

image-20230323154847782

image-20230323154931843

  • JDK 동적 프록시 사용

image-20230323154917129

image-20230323154940852

4. JDK 동적 프록시 - 적용2

  • 메서드 이름 필터 기능 추가
  • http://localhost:8080/v1/no-log 요구사항에 의해 이것을 호출 했을 때는 로그가 남으면 안된다.
  • 메서드 이름을 기준으로 특정 조건을 만족할 때만 로그를 남기는 기능을 개발해보자.

LogTraceFilterHandler

  • package hello.proxy.config.v2_dynamicproxy;
      
    import hello.proxy.trace.TraceStatus;
    import hello.proxy.trace.logtrace.LogTrace;
    import org.springframework.util.PatternMatchUtils;
      
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
      
    public class LogTraceFilterHandler implements InvocationHandler {
      
        private final Object target;
        private final LogTrace logTrace;
        private final String[] patterns;
      
        public LogTraceFilterHandler(Object target, LogTrace logTrace, String[] patterns) {
            this.target = target;
            this.logTrace = logTrace;
            this.patterns = patterns;
        }
      
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            TraceStatus status = null;
      
            //메서드 이름 필터
            String methodName = method.getName();
            //save, request, reque*, *est
            if (!PatternMatchUtils.simpleMatch(patterns, methodName)) {
                return method.invoke(target, args);
            }
      
            try{
                String message = method.getDeclaringClass().getSimpleName()
                        + "." + method + "()";
                status = logTrace.begin(message);
      
                //로직 호출
                Object result = method.invoke(target, args);
                logTrace.end(status);
                return result;
            }catch (Exception e){
                logTrace.exception(status, e);
                throw e;
            }
        }
    }
    

다음 기능 추가(PatternMatchUtils.simpleMatch(..) )

  • private final String[] patterns;
      
    //메서드 이름 필터
    String methodName = method.getName();
    //save, request, reque*, *est
    if (!PatternMatchUtils.simpleMatch(patterns, methodName)) {
        return method.invoke(target, args);
    }
    
    • xxx : xxx가 정확히 매칭되면 참
    • xxx* : xxx로 시작하면 참
    • *xxx : xxx로 끝나면 참
    • xxx : xxx가 있으면 참
  • String[] patterns : 적용할 패턴은 생성자를 통해서 외부에서 받는다.

DynamicProxyFilterConfig

  • package hello.proxy.config.v2_dynamicproxy;
      
    import hello.proxy.trace.TraceStatus;
    import hello.proxy.trace.logtrace.LogTrace;
      
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
      
    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;
            }
        }
    }
    
  • public static final String[] PATTERNS = {“request”, “order”, “save*”};

    • 적용할 패턴이다. request , order , save 로 시작하는 메서드에 로그가 남는다.
  • LogTraceFilterHandler : 앞서 만든 필터 기능이 있는 핸들러를 사용한다. 그리고 핸들러에 적용 패턴도 넣어준다.

실행

  • @Import(DynamicProxyFilterConfig.class) 으로 방금 만든 설정을 추가한다.
  • 실행
    • http://localhost:8080/v1/request?itemId=hello
    • http://localhost:8080/v1/no-log
  • 실행해보면 no-log 가 사용하는 noLog() 메서드에는 로그가 남지 않는 것을 확인할 수 있다.

JDK 동적 프록시 - 한계

JDK 동적 프록시는 인터페이스가 필수이다. 그렇다면 V2 애플리케이션 처럼 인터페이스 없이 클래스만 있는 경우에는 어떻게 동적 프록시를 적용할 수 있을까? 이것은 일반적인 방법으로는 어렵고 CGLIB 라는 바이트코드를 조작하는 특별한 라이브러리를 사용해야 한다.

5. CGLIB - 소개

CGLIB: Code Generator Library

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

참고로 우리가 CGLIB를 직접 사용하는 경우는 거의 없다. 이후에 설명할 스프링의 ProxyFactory 라는 것이 이 기술을 편리하게 사용하게 도와주기 때문에, 너무 깊이있게 파기 보다는 CGLIB가 무엇인지 대략 개념만 잡으면 된다.

공통 예제 코드 (src/test)

  • 인터페이스와 구현이 있는 서비스 클래스 - ServiceInterface , ServiceImpl
  • 구체 클래스만 있는 서비스 클래스 - ConcreteService

ServiceInterface

  • package hello.proxy.common.service;
      
    public interface ServiceInterface {
      
        void save();
      
        void find();
    }
    

ServiceImpl

  • package hello.proxy.common.service;
      
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
      
    @Slf4j
    public class ServiceImpl implements ServiceInterface {
      
        @Override
        public void save() {
            log.info("save 호출");
        }
      
        @Override
        public void find() {
            log.info("find 호출");
        }
    }
    

ConcreteService

  • package hello.proxy.common.service;
      
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
      
    @Slf4j
    public class ConcreteService {
        public void call(){
            log.info("ConcreteService 호출");
        }
    }
    

6. CGLIB - 예제 코드 (src/test)

  • JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler 를 제공했듯이, CGLIB는 MethodInterceptor 를 제공한다.

MethodInterceptor -CGLIB 제공

  • package org.springframework.cglib.proxy;
    public interface MethodInterceptor extends Callback {
    	Object intercept(Object obj, Method method, Object[] args, MethodProxy
    proxy) throws Throwable;
    }
    
    • obj : CGLIB가 적용된 객체
    • method : 호출된 메서드
    • args : 메서드를 호출하면서 전달된 인수
    • proxy : 메서드 호출에 사용

TimeMethodInterceptor

  • package hello.proxy.cglib.code;
      
      
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.cglib.proxy.MethodInterceptor;
    import org.springframework.cglib.proxy.MethodProxy;
      
    import java.lang.reflect.Method;
      
    @Slf4j
    public class TimeMethodInterceptor implements MethodInterceptor {
      
        private final Object target;
      
        public TimeMethodInterceptor(Object target) {
            this.target = target;
        }
      
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            log.info("TimeProxy 실행");
            long startTime = System.currentTimeMillis();
      
            //methodProxy 로 실행
            Object result = methodProxy.invoke(target, args); //call()
      
            long endTime = System.currentTimeMillis();
            long resultTime = endTime - startTime;
            log.info("TimeProxy 종료 resultTime={}", resultTime);
            return result;
        }
    }
    
  • TimeMethodInterceptor 는 MethodInterceptor 인터페이스를 구현해서 CGLIB 프록시의 실행 로직을 정의한다.
  • Object target : 프록시가 호출할 실제 대상
  • proxy.invoke(target, args) : 실제 대상을 동적으로 호출한다.
    • 참고로 method 를 사용해도 되지만, CGLIB는 성능상 MethodProxy proxy 를 사용하는 것을 권장한다.

CglibTest

  • package hello.proxy.cglib;
      
    import hello.proxy.cglib.code.TimeMethodInterceptor;
    import hello.proxy.common.service.ConcreteService;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    import org.springframework.cglib.proxy.Enhancer;
      
    @Slf4j
    public class CglibTest {
      
        @Test
        void cglib(){
            ConcreteService target = new ConcreteService();
      
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(ConcreteService.class);
            enhancer.setCallback(new TimeMethodInterceptor(target));
            ConcreteService proxy = (ConcreteService) enhancer.create();
            log.info("targetClass={}", target.getClass());
            log.info("proxyClass={}", proxy.getClass());
      
            proxy.call();
        }
    }
    
  • Enhancer : CGLIB는 Enhancer 를 사용해서 프록시를 생성한다.
  • enhancer.setSuperclass(ConcreteService.class) : CGLIB는 구체 클래스를 상속 받아서 프록시를 생성할 수 있다. 어떤 구체 클래스를 상속 받을지 지정한다.
  • enhancer.setCallback(new TimeMethodInterceptor(target)) 프록시에 적용할 실행 로직을 할당한다. enhancer.create() : 프록시를 생성한다. 앞서 설정한 enhancer.setSuperclass(ConcreteService.class) 에서 지정한 클래스를 상속 받아서 프록시가 만들어진다.

JDK 동적 프록시는 인터페이스를 구현(implement)해서 프록시를 만든다. CGLIB는 구체 클래스를 상속 (extends)해서 프록시를 만든다.

실행 결과

CglibTest - targetClass=class hello.proxy.common.service.ConcreteService
CglibTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$25d6b0e3
TimeMethodInterceptor - TimeProxy 실행
ConcreteService - ConcreteService 호출
TimeMethodInterceptor - TimeProxy 종료 resultTime=9

CGLIB가 생성한 프록시 클래스 이름

  • CGLIB를 통해서 생성된 클래스의 이름을 확인해보면,
    • ConcreteService\(EnhancerByCGLIB\)25d6b0e3
  • CGLIB가 동적으로 생성하는 클래스 이름은 다음과 같은 규칙으로 생성된다.
    • 대상클래스\(EnhancerByCGLIB\)임의코드

그림으로 정리

  • image-20230323161759462

CGLIB 제약

  • 클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
    • (JDK proxy 제약 -> CGLIB proxy 제)
    • 부모 클래스의 생성자를 체크해야 한다. -> CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
    • 클래스에 final 키워드가 붙으면 상속이 불가능하다. -> CGLIB에서는 예외가 발생한다.
    • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. -> CGLIB에서는 프록시 로직이 동작하지 않는다.

댓글남기기