리플렉션
프록시를 사용하면 기존 코드를 수정하지 않고 부가 기능을 추가할 수 있다. 그치만 모든 클래스마다 프록시 클래스를 만드는 것은 비효율적일 것이다. 자바에서는 JDK 동적 프록시 기술이나 CGLIB 같은 오픈소스 기술을 활용하여 동적으로 프록시를 생성할 수 있는데 이때 사용하는 것이 리플렉션이라고 한다.
리플렉션은 클래스나 메서드의 메타정보를 사용해서 동적으로 호출하는 메소드를 변경할 수 있다.
다음 예시를 살펴보자.
다음 메소드의 호출 결과를 출력하고자 할 때 공통 로직을 처리하기 위한 리플렉션 코드이다.
@Slf4j
static class Hello {
public String callA() {
log.info("callA");
return "A";
}
public String callB() {
log.info("callB");
return "B";
}
}
동적으로 클래스를 가져와 클래스가 가지는 메소드를 실행하는 코드이다.
리플렉션을 사용하면 메소드를 가져와 부가 기능을 추가하기에 용이해진다.
@Test
void reflection2() throws Exception {
Class classHello =
Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
Method methodCallA = classHello.getMethod("callA");
dynamicCall(methodCallA, target);
Method methodCallB = classHello.getMethod("callB");
dynamicCall(methodCallB, target);
}
private void dynamicCall(Method method, Object target) throws Exception {
log.info("start method");
Object result = method.invoke(target);
log.info("result={}", result);
}
그렇다면 리플렉션을 사용하는 JDK 동적 프록시와 CGLIB은 어떤 차이가 있을까?
JDK 동적 프록시
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수이다.
그 이유는 해당 인터페이스를 참조하는 프록시 클래스를 만들어 참조 다형성을 활용하기 때문.
JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성하면 된다.
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
- Object proxy : 프록시 자신
- Method method : 호출한 메서드
- Object[] args : 메서드를 호출할 때 전달한 인수
활용 예시 - 메소드 실행 시간 출력
@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 {
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 AImpl implements AInterface {
@Override
public String call() {
log.info("A 호출");
return "a";
}
}
@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());
}
동적 프록시를 도입했을 때 의존관계는 다음과 같아진다.
CGLIB - Code Generator Library
바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다. CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
CGLIB에 적용할 로직은 MethodInterceptor 구현해서 작성하면 된다.
package org.springframework.cglib.proxy;
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}
- obj : CGLIB가 적용된 객체
- method : 호출된 메서드
- args : 메서드를 호출하면서 전달된 인수 proxy : 메서드 호출에 사용
활용 예시 - 메소드 실행 시간 출력
@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;
}
}
jdk 동적 프록시 기술과 거의 동일하다.
@Test
void cglib() {
ConcreteService target = new ConcreteService();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class);
enhancer.setCallback(new TimeMethodInterceptor(target));
ConcreteService proxy = (ConcreteService)enhancer.create();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
}
- Enhancer : CGLIB는 Enhancer 를 사용해서 프록시를 생성
- enhancer.setSuperclass(xxx.class): 어떤 구체 클래스를 상속 받을지 설정
-
enhancer.setCallback(new TimeMethodInterceptor(target))
- enhancer.create(): 프록시 생성

jdk 동적 프록시에서 생성된 프록시 정보가 약간 다름을 확인할 수 있다
제약
상속을 사용하기 때문에 상속에서 오는 몇 가지 제약이 있다.
- 자식 클래스를 동적으로 생성하기 때문에 부모 클래스의 생성자를 체크해야 한다
- final 클래스 키워드가 붙으면 상속 불가능 -> CGLIB에서 예외 발생
- final 메소드는 오버라이딩 불가능 -> CGLIB에서 프록시 로직 동작 X
그럼 인터페이스와 구체 클래스를 구분해서 각각 개발해야 하나 막막할 것이다. 그래서 스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리( ProxyFactory )라는 기능을 제공한다.
프록시 팩토리
프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용한다
부가 기능을 적용할 때 Advice 라는 새로운 개념을 도입했다. 개발자는 InvocationHandler 나 MethodInterceptor를 신경쓰지 않고, Advice 만 만들면 된다. 결과적으로 InvocationHandler 나 MethodInterceptor 는 Advice 를 호출하게 된다.
또한, 특정 조건에만 적용시키기 위해 Pointcut이라는 개념을 도입해서 이 문제를 일관성 있게 해결한다.
Advice 를 만드는 방법은 여러가지가 있지만, 기본적인 방법은 다음 인터페이스를 구현하면 된다.
MethodInterceptor 구현 - 스프링이 제공하는 코드
package org.aopalliance.intercept;
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
CGLIB의 MethodInterceptor 와 이름이 같은 같지만 다른 클래스이고 다른 패키지에 존재한다.
MethodInterceptor 는 Interceptor 를 상속하고 Interceptor 는 Advice 인터페이스를 상속하기 때문에 advice라고 부른다.
@Slf4j
public class TimeAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}ms", resultTime);
return result;
}
}
invocation.proceed() 를 호출하면 target 클래스를 호출하고 그 결과를 받는다.
jdk 동적 프록시, CGLIB과 다르게 target 클래스 정보는 프록시 팩토리 생성 단계에서 전달해준다.
@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
void interfaceProxy() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.save();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
}
@Test
@DisplayName("구체 클래스만 있으면 CGLIB 사용")
void concreteProxy() {
ConcreteService target = new ConcreteService();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
앞서 확인한 jdk 동적 프록시의 정보와 CGLIB 프록시의 정보와 같음을 확인할 수 있다.
이렇게 직접 작정한 advice를 스프링에서 더욱 편하게 제공해준다. 물론 pointcut도 마찬가지다.
다음은 스프링에서 제공하는 advice와 pointcut을 알아보자.
'Study > Spring' 카테고리의 다른 글
AOP (Aspect - Oriented Programming)와 @Aspect (0) | 2023.10.12 |
---|---|
빈 후처리기(Bean PostProcessor)와 어드바이저 (1) | 2023.10.11 |
PRG Post/Redirect/Get (0) | 2023.08.22 |
HTTP 헤더, 파라미터, 바디 조회하는 방법 (0) | 2023.08.21 |
slf4j 로깅 알아보기 (0) | 2023.08.20 |