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