CS

[CS] 동적 프록시

j9972 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

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