스프링 고급편 - 동적 프록시 기술
핵심 내용만 정리하고 자세한 구현 코드는 올리지 않는다.
스프링 부트 3.3.3 버전 기준
✔️ 리플렉션
이전까지 프록시를 사용해 로그 추적기 부가 기능을 적용했지만 대상 클래스만큼 프록시 클래스를 만들어야 한다는 단점이 있다.
따라서 자바의 기본 제공 기술인 JDK 동적 프록시, CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어 낼 수 있다.
하지만 JDK 동적 프록시 생성을 이해하려면 리플렉션을 알아야한다.
리플렉션은 한다미로 메타 데이터 이다.
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("result1={}", result1);
//callB 메서드 정보
Method methodCallB = classHello.getMethod("callB");
Object result2 = methodCallB.invoke(target);
log.info("result2={}", result2);
}
callA, callB 메서드를 직접 호출하는 부분이 Method로 대체되었다.
주의
리플렉션 기술은 런타임에 동작하기에 컴파일 시점에 오류를 잡을 수 없다.
✔️ JDK 동적 프록시
동적 프록시 기술은 개발자가 직접 프록시 클래스를 만들지 않고 프록시 객체를 동적으로 런타임에 개발자 대신 만들어준다.
그리고 동적 프록시에 원하는 실행 로직 지정이 가능하다.
InvocationHandler
JDK 동적 프록시에 적용할 로직(부가 기능)은 InvocationHandler 인터페이스를 구현해서 작성하면 된다.
package java.lang.reflect;
public interface InvocationHandler {
// 부가기능을 구현
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
proxy : 프록시 자신
method : 호출할 메서드
args : 메서드를 호출할 때 전달한 인수
사용
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
public TimeInvocationHandler(Object object) {
this.target = object;
}
@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); // 실제 인스턴스 메서드 호출
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
@Slf4j
public class JdkDynamicProxyTest {
@Test
void dynamicA() {
AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
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());
}
}
프록시 생성 메서드
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h)
loader : 프록시 클래스 인터페이스를 정의하는 클래스 로더
interfaces : 프록시 클래스가 구현할 인터페이스 목록
h : 메서드 호출을 처리하기 위해 호출을 전달하는 호출 핸들러
참고
클래스 로더 란 ?
자바 클래스로더는 자바 클래스를 자바 가상 머신으로 동적 로드하는 자바 런타임 환경의 일부
JVM에서 클래스를 메모리에 로드하는 역할을 담당하는 컴포넌트
동적 프록시로 생성되면 프록시 클래스 이름이 $Proxy1 이렇게 생성된다
.
실행 순서
- 클라이언트가 jdk 동적 프록시의 call()을 실행
- 프록시는 부가 기능을 실행하고 InvocationHandler.invoke()를 호출 (여기서는 TimeInvocationHandler가 구현체)
- handler는 실제 객체를 호출하고 call() 메서드를 실행
- 결과를 반환하고handler는 부가기능을 마저 실행하고 결과를 반환
정리
프록시를 클래스 수만큼 만들 필요 없고, 부가 기능 로직도하나으 ㅣ클래스에 모아서 단일 책임 원칙도 지킬 수 있다.
실제 사용시
Config에서 프록시를 생성하여 프록시를 빈으로 등록한다.
프록시 타입은 타겟의 타입과 같기 때문에 프록시를 빈으로 등록해도 문제 없다.
✔️ CGLIB
Code Generator Library
바이트 코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리
구체 클래스만 가지고 동적 프록시를 만들 수 있다. (상속)
CGLIB는 JDK 동적 프록시의 InvocationHandler와 같은 MethodInterceptor 를 제공한다.
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}
obj : CGLIB가 적용된 객체
method : 호출된 메서드
args : 메서드를 호출하면서 전달된 인수
proxy : 메서드 호출에 사용
CGLIB로 생성된 클래스 이름은 아래와 같은 규칙으로 생성된다.
대상클래스$$EnhancerByCGLIB$$임의코드
@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 proxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = proxy.invoke(target, args); // 실제 대상을 동적으로 호출
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
@Slf4j
public class CglibTest {
@Test
void cglib() {
ConcreteService target = new ConcreteService(); // call()라는 단순 로그 출력 메서드를 가지는 임시 클래스
Enhancer enhancer = new Enhancer(); // cglib 를 만드는 객체
enhancer.setSuperclass(ConcreteService.class); // ConcreteService를 상속받은 cglib를 만들어야하기 때문
enhancer.setCallback(new TimeMethodInterceptor(target));
ConcreteService proxy = (ConcreteService) enhancer.create(); // 이때문에 캐스팅 가능
proxy.call();
}
}
Enhancer : 프록시를 생성 객체
setSuperclass() : 구체 클래스를 상속 받아서 프록시를 생성하기에 상속 받을 구체 클래스를 지정
setCallback() : 프록시에 적용할 실행 로직을 할당
create : 프록시 생성
CGLIB 제약
- 부모 클래스의 생성자 체크
- CGLIB는 자식클래스를 동적으로 생성하기 때문에 기본 생성자 필요
- 클래스에 final 키워드 붙으면 상속 불가
- 메서드에 final 키워드가 붙으면 해당 메서드 오버라이딩 불가
정리
동적 기술을 쓰면 좋지만 인터페이스 사용시 JDK 동적 프록시, 구체 클래스인 경우 CGLIB를 적용해야한다.
두 기술의 부가기능은 같다. 하지만 두가지로 나눠서 관리해야할까?
다음은 target 의 클래스, 인터페이스에 따라 동적으로 프록시를 알아서 만들어 주는 프록시 팩토리에 대해서 공부한다.