ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [CS] 동적 프록시
    CS 2023. 1. 7. 23:44
    728x90

    프록시

    타겟 코드의 수정 없이 접근제어 혹은 부가 기능을 추가하기 위해 주로 사용한다.

    하지만, 프록시 사용을 위해서는 대상 클래스 수만큼의 프록시 클래스를 만들어줘야 한고 그 안의 코드들이 중복된다는 문제점이 있다. ( 클래스 만큼 프록시 클래스를 만드는 것은 너무나 힘들다 )

     

    이러한 문제점을 해결하기 위해서 동적 프록시가 사용되는데, 이는 컴파일 시점이아닌, 런타임 시점에 프록시 클래스를 만들어주는 방식이다.

     

    동적 프록시

    • JDK Dynamic Proxy - JAVA 에서 제공
    • CGLIB - 오픈소스 기술(Spring에서 사용하므로, Spring 의존관계가 있다면 사용할 수 있다.
    •  

    JDK 동적 프록시

    JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스 가 필수다.

    Example )

    JDK 동적 프록시 예제를 표로 나타냄

     

     

    Animal.java

    public interface Animal {
        String bark();
    }
    

    Cat.java

    public class Cat implements Animal{
        @Override
        public String bark() {
            return "냐옹~냐옹~";
        }
    }
    

    Jdk Dynamic Proxy에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성한다.

    TimeInvocationHandler.java

    @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 {
    // proxy - 프록시 자신// method - 호출한 메서드// args - 메소드를 호출할 때 전달한 인수
            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;
        }
    }
    
    @Test
    void dynamicProxyTest() {
        Animal target = new Cat();
    // 동적 프록시에 적용할 핸들러 로직
        TimeInvocationHandler handler = new TimeInvocationHandler(target);
    
    // proxy 동적 생성 (java.lang.reflect.Proxy)// 인터페이스를 기반(new Class[])으로 동작 프록시를 생성하고 그 결과를 반환한다.
        Animal proxy = (Animal)Proxy.newProxyInstance(Animal.class.getClassLoader(), new Class[]{Animal.class}, handler);
        proxy.bark();
        log.info("targetClass={}, proxyClass={}", target.getClass(), proxy.getClass());
    }
    
    targetClass=class hello.proxy.jdkdynamic.code.Cat, proxyClass=class com.sun.proxy.$Proxy12
    
    • Proxy 클래스는 class com.sun.proxy.$Proxy12 동적으로 생성된 클래스이다. 이 프록시는 TimeInvocationHandler 로직을 실행한다.
    • 적용 대상 만큼 프록시 객체를 만들지 않아도 되며, 부가 기능 로직(InvocationHandler)를 한번만 개발해서 공통으로 적용할 수 있다.

     

    CGLIB

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

    예시

    CGLIB 예제를 표로 나타냄

     

    Cat.java

    public class Cat {
        public String bark() {
            return "냐옹~냐옹";
        }
    }
    

    Jdk Dynamic Proxy 에서 InvocationHandler를 제공했듯이, CGLIB는 MethodInterceptor를 제공한다.

    TimeMethodInterceptor.java

    @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 {
    // obj - CGLIB가 적용된 객체// method - 호출된 메서드// args - 메서드를 호출하면서 전달된 인수// methodProxy - 메서드 호출에 사용
        	log.info("TimeProxy 실행");
            long startTime = System.currentTimeMillis();
    
            Object result = methodProxy.invoke(target, args);
    
            long endTime = System.currentTimeMillis();
            long resultTime = endTime - startTime;
            log.info("TimeProxy 종료 resultTime={}", resultTime);
            return result;
        }
    }
    
    @Test
    void cglibProxyTest() {
    // 인터페이스가 없는 구체 클래스
        Cat target = new Cat();
    
    // Enhancer 를 사용하여 프록시를 생성한다.
        Enhancer enhancer = new Enhancer();
    // 구체 클래스를 상속받아 프록시를 생성할 수 있다.
        enhancer.setSuperclass(Cat.class);
    // 프록시에 적용할 실행 로직을 할당한다.
        enhancer.setCallback(new TimeMethodInterceptor(target));
        Cat proxy = (Cat)enhancer.create();
        proxy.bark();
        log.info("targetClass={}, proxyClass={}", target.getClass(), proxy.getClass());
    }
    
    targetClass=class hello.proxy.cglib.code.Cat, proxyClass=class hello.proxy.cglib.code.Cat$$EnhancerByCGLIB$$3ad8d5cd
    
    • 대상클래스$$EnhancerByCGLIB$$임의코드
    • 제약 조건
      • 생성자 체크 - CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
      • 클래스에 final 키워드가 붙으면 상속이 불가하다. - CGLIB 예외 발생
      • 메서드 final 키워드가 붙으면 메서드 오버라이딩이 불가하다. - CGLIB 프록시 로직이 동작하지 않음

     

    결론**, 동적 프록시를 사용하면 프록시를 사용할때처럼 클래스마다 프록시 클래스를 만들지 않고, 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용하여 프록시 객체를 런타임 시점에만 생성해주면 됩니다.**

     

    참조

    https://gong-story.tistory.com/22

    https://cornswrold.tistory.com/576

    스프링 핵심 원리 - 고급편 (김영한님)

    프록시

    타겟 코드의 수정 없이 접근제어 혹은 부가 기능을 추가하기 위해 주로 사용한다.

    하지만, 프록시 사용을 위해서는 대상 클래스 수만큼의 프록시 클래스를 만들어줘야 한고 그 안의 코드들이 중복된다는 문제점이 있다. ( 클래스 만큼 프록시 클래스를 만드는 것은 너무나 힘들다 )

    이러한 문제점을 해결하기 위해서 동적 프록시가 사용되는데, 이는 컴파일 시점이아닌, 런타임 시점에 프록시 클래스를 만들어주는 방식이다.

    동적 프록시

    • JDK Dynamic Proxy - JAVA 에서 제공
    • CGLIB - 오픈소스 기술(Spring에서 사용하므로, Spring 의존관계가 있다면 사용할 수 있다.

    JDK 동적 프록시

    JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스 가 필수다.

    Example )

    Animal.java

    public interface Animal {
        String bark();
    }
    

    Cat.java

    public class Cat implements Animal{
        @Override
        public String bark() {
            return "냐옹~냐옹~";
        }
    }
    

    Jdk Dynamic Proxy에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성한다.

    TimeInvocationHandler.java

    @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 {
    // proxy - 프록시 자신// method - 호출한 메서드// args - 메소드를 호출할 때 전달한 인수
            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;
        }
    }
    
    @Test
    void dynamicProxyTest() {
        Animal target = new Cat();
    // 동적 프록시에 적용할 핸들러 로직
        TimeInvocationHandler handler = new TimeInvocationHandler(target);
    
    // proxy 동적 생성 (java.lang.reflect.Proxy)// 인터페이스를 기반(new Class[])으로 동작 프록시를 생성하고 그 결과를 반환한다.
        Animal proxy = (Animal)Proxy.newProxyInstance(Animal.class.getClassLoader(), new Class[]{Animal.class}, handler);
        proxy.bark();
        log.info("targetClass={}, proxyClass={}", target.getClass(), proxy.getClass());
    }
    
    targetClass=class hello.proxy.jdkdynamic.code.Cat, proxyClass=class com.sun.proxy.$Proxy12
    
    • Proxy 클래스는 class com.sun.proxy.$Proxy12 동적으로 생성된 클래스이다. 이 프록시는 TimeInvocationHandler 로직을 실행한다.
    • 적용 대상 만큼 프록시 객체를 만들지 않아도 되며, 부가 기능 로직(InvocationHandler)를 한번만 개발해서 공통으로 적용할 수 있다.

    CGLIB

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

    예시

    Cat.java

    public class Cat {
        public String bark() {
            return "냐옹~냐옹";
        }
    }
    

    Jdk Dynamic Proxy 에서 InvocationHandler를 제공했듯이, CGLIB는 MethodInterceptor를 제공한다.

    TimeMethodInterceptor.java

    @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 {
    // obj - CGLIB가 적용된 객체// method - 호출된 메서드// args - 메서드를 호출하면서 전달된 인수// methodProxy - 메서드 호출에 사용
        	log.info("TimeProxy 실행");
            long startTime = System.currentTimeMillis();
    
            Object result = methodProxy.invoke(target, args);
    
            long endTime = System.currentTimeMillis();
            long resultTime = endTime - startTime;
            log.info("TimeProxy 종료 resultTime={}", resultTime);
            return result;
        }
    }
    
    @Test
    void cglibProxyTest() {
    // 인터페이스가 없는 구체 클래스
        Cat target = new Cat();
    
    // Enhancer 를 사용하여 프록시를 생성한다.
        Enhancer enhancer = new Enhancer();
    // 구체 클래스를 상속받아 프록시를 생성할 수 있다.
        enhancer.setSuperclass(Cat.class);
    // 프록시에 적용할 실행 로직을 할당한다.
        enhancer.setCallback(new TimeMethodInterceptor(target));
        Cat proxy = (Cat)enhancer.create();
        proxy.bark();
        log.info("targetClass={}, proxyClass={}", target.getClass(), proxy.getClass());
    }
    
    targetClass=class hello.proxy.cglib.code.Cat, proxyClass=class hello.proxy.cglib.code.Cat$$EnhancerByCGLIB$$3ad8d5cd
    
    • 대상클래스$$EnhancerByCGLIB$$임의코드
    • 제약 조건
      • 생성자 체크 - CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
      • 클래스에 final 키워드가 붙으면 상속이 불가하다. - CGLIB 예외 발생
      • 메서드 final 키워드가 붙으면 메서드 오버라이딩이 불가하다. - CGLIB 프록시 로직이 동작하지 않음

    결론**, 동적 프록시를 사용하면 프록시를 사용할때처럼 클래스마다 프록시 클래스를 만들지 않고, 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용하여 프록시 객체를 런타임 시점에만 생성해주면 됩니다.**

    참조

    https://gong-story.tistory.com/22

    https://cornswrold.tistory.com/576

    스프링 핵심 원리 - 고급편 (김영한님)

    'CS' 카테고리의 다른 글

    [CS] OAuth2.0 간단한 이해  (0) 2023.03.10
    [CS] batch Insert  (0) 2023.01.09
    [CS] 직렬화  (0) 2023.01.07
    [CS] JPA 영속성 컨텍스트  (0) 2023.01.06
    [CS] 스프링부트를 쓰는 이유와 자동설정 원리  (0) 2023.01.05
Designed by Tistory.