[CS] 동적 프록시
프록시
타겟 코드의 수정 없이 접근제어 혹은 부가 기능을 추가하기 위해 주로 사용한다.
하지만, 프록시 사용을 위해서는 대상 클래스 수만큼의 프록시 클래스를 만들어줘야 한고 그 안의 코드들이 중복된다는 문제점이 있다. ( 클래스 만큼 프록시 클래스를 만드는 것은 너무나 힘들다 )
이러한 문제점을 해결하기 위해서 동적 프록시가 사용되는데, 이는 컴파일 시점이아닌, 런타임 시점에 프록시 클래스를 만들어주는 방식이다.
동적 프록시
- 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
스프링 핵심 원리 - 고급편 (김영한님)
프록시
타겟 코드의 수정 없이 접근제어 혹은 부가 기능을 추가하기 위해 주로 사용한다.
하지만, 프록시 사용을 위해서는 대상 클래스 수만큼의 프록시 클래스를 만들어줘야 한고 그 안의 코드들이 중복된다는 문제점이 있다. ( 클래스 만큼 프록시 클래스를 만드는 것은 너무나 힘들다 )
이러한 문제점을 해결하기 위해서 동적 프록시가 사용되는데, 이는 컴파일 시점이아닌, 런타임 시점에 프록시 클래스를 만들어주는 방식이다.
동적 프록시
- 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
스프링 핵심 원리 - 고급편 (김영한님)