Spring 핵심원리 고급편 - MethodInterceptor

목차

참고

본 포스트는 김영한의 스프링 핵심 원리 - 고급편 내용을 참고해 만들었습니다.

MethodInterceptor - 부가기능 구현

Spring 에서는 프록시 객체에 부가기능을 적용하기 위해 MethodInterceptor 인터페이스를 제공합니다. MethodInterceptor 인터페이스 구현을 통해 프록시 객체에 적용될 부가기능을 생성할 수 있습니다.

MethodInterceptor 인터페이스 내 invoke 메소드 인자 MethodInvocation 객체에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args, 메서드 정보등이 표함돼 있다.

CGLIB 에서 제공하는 MethodInterceptor 와 이름이 비슷하므로 구현시 주의할 필요가 있습니다.

package org.aopalliance.intercept;

public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}

MethodInterceptor 구현

MethodInterceptor 를 이용해 수행 시간 로그를 찍어주는 부가기능 로직 을 수행하는 Advice 를 만들어준다.

@Slf4j
public class TimeAdvice implements MethodInterceptor {

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();

// Object result = method.invoke(target, args);

// Spring 에서 제공하는 프록시 팩토리 기능 적용 start
Object result = invocation.proceed();
// Spring 에서 제공하는 프록시 팩토리 기능 적용 end

long endTime = System.currentTimeMillis();

long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime = {}", resultTime);

return result;
}
}

프록시 객체에 Advice(부가 기능) 적용

ProxyFactory 를 이용해 프록시 객체를 생성합니다. ProxyFactory 는 전달받은 인스턴스 정보를 기반으로 프록시 객체를 생성합니다. 인터페이스가 있는 객체인 경우 JDK 동적 프록시 를 사용해 프록시 객체를 생성하고 인터페이스가 없을 경우에는 CGLIB 를 사용해 프록시 객체를 생성합니다.

생성된 프록시 객체에 addAdvice 메소드를 이용해 부가 기능을 위한 Advice 를 추가한다.

// 인터페이스가 있으면 동적 프록시 사용

ServiceInterface target = new ServiceImpl();

// ProxyFactory 를 생성할 때 프록시 호출 대상을 인자로 넘겨준다.
ProxyFactory proxyFactory = new ProxyFactory(target);

// ProxyFactory 객체에 부가 기능을 위한 Advice 정보를 추가한다.
proxyFactory.addAdvice(new TimeAdvice());

ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());

proxy.save();

ProxyFactory 객체는 JDK Dynamic Proxy 를 기반으로 만들어진 것을 확인할 수 있다.

21:33:13.233 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest - targetClass = class hello.proxy.common.service.ServiceImpl
21:33:13.237 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest - proxyClass = class com.sun.proxy.$Proxy13
21:33:13.242 [Test worker] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 실행
21:33:13.243 [Test worker] INFO hello.proxy.common.service.ServiceImpl - save 호출
21:33:13.243 [Test worker] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 종료 resultTime = 1

프록시 적용 확인

테스트에서 ProxyFactory 를 통해 Proxy 객체가 생성됐는지 확인하고 Advice (부가기능) 로직을 수행하는지 확인합니다.

프록시 객체 생성시 인터페이스가 있는 객체일 경우 JDK Dynamic Proxy 를 이용해 프록시 객체를 생성됐는지 확인하고, 인터페이스가 없는 객체일 경우 CGLIB 를 이용해 프록시 객체를 생성했는지 확인합니다.

Jdk Dynamic Proxy 를 이용한 Proxy 생성 확인

ProxyFactory 를 이용해 프록시를 생성할 때 인터페이스가 있으면 JDK Dynamic Proxy 를 이용해 동적 프록시를 생성한다.

// 인터페이스가 있으면 동적 프록시 사용

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();

// Proxy Factory 를 이용해 만들었을 때 사용가능
assertThat(AopUtils.isAopProxy(proxy)).isTrue(); // AopProxy 를 이용해 동적 프록시를 생성했는지 확인
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue(); // JdkDynamicProxy 를 이용해 동적 프록시를 생성했는지 확인
assertThat(AopUtils.isCglibProxy(proxy)).isFalse(); // CglibProxy 를 이용해 동적 프록시를 생성했는지 확인

com.sun.proxy.Proxy13 를 통해 JdkDynamicProxy 를 이용해 동적 프록시가 생성 된 것을 확인할 수 있다.

23:00:05.128 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest - targetClass = class hello.proxy.common.service.ServiceImpl
23:00:05.132 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest - proxyClass = class com.sun.proxy.$Proxy13
23:00:05.136 [Test worker] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 실행
23:00:05.136 [Test worker] INFO hello.proxy.common.service.ServiceImpl - save 호출
23:00:05.136 [Test worker] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 종료 resultTime = 0

CGLIB 를 이용한 Proxy 생성 확인

// 구체 클래스만 있으면 CGLIB 적용

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();

// Proxy Factory 를 이용해 만들었을 때 사용가능
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();

EnhancerBySpringCGLIB 를 통해 CGLIB 를 이용해 동적 프록시가 생성됐는지 확인할 수 있다.

21:50:00.561 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest - targetClass = class hello.proxy.common.service.ConcreteService
21:50:00.563 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest - proxyClass = class hello.proxy.common.service.ConcreteService$$EnhancerBySpringCGLIB$$8328efc5
21:50:00.565 [Test worker] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 실행
21:50:00.576 [Test worker] INFO hello.proxy.common.service.ConcreteService - ConcreteService 호출
21:50:00.576 [Test worker] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 종료 resultTime = 10

CGLIB 를 이용한 강제 Proxy 생성 확인

ProxyFactory 가 프록시 객체 생성시 인터페이스가 있는 객체더라도 CGLIB 를 이용해 프록시 객체를 생성할 수 있습니다.

ProxyFactory 클래스에서 제공하는 setProxyTargetClass 메소드에 true 값을 넣으면 프록시 객체 생성시 CGLIB 를 이용해 프록시 객체를 생성합니다.

// ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용

ConcreteService target = new ConcreteService();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true); // 이 옵션이 있으면 항상 CGLIB 를 사용해 프록시를 생성한다.
proxyFactory.addAdvice(new TimeAdvice());
ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();

log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());

proxy.call();

// Proxy Factory 를 이용해 만들었을 때 사용가능
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();

EnhancerBySpringCGLIB 를 통해 setProxyTargetClass 메소드를 이용해 CGLIB 를 사용한 프록시 생성을 강제할 수 있음을 확인할 수 있다.

22:14:06.412 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest - targetClass = class hello.proxy.common.service.ConcreteService
22:14:06.414 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest - proxyClass = class hello.proxy.common.service.ConcreteService$$EnhancerBySpringCGLIB$$116d9156
22:14:06.416 [Test worker] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 실행
22:14:06.427 [Test worker] INFO hello.proxy.common.service.ConcreteService - ConcreteService 호출
22:14:06.427 [Test worker] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 종료 resultTime = 10
Share