Spring/[인프런 김영한 스프링 핵심 원리 - 고급편]
[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링이 지원하는 프록시
h2boom
2024. 12. 10. 20:19
프록시 팩토리
- 이전에는 상황에 따라서 인터페이스가 있으면 JDK 동적 프록시를 사용하고 구체 클래스의 경우 CGLIB를 사용했다면 프록시 팩토리 하나로 동적 프록시를 생성할 수 있다.
- 프록시 팩토리 : 스프링에서 제공하는 동적 프록시를 통합해서 편리하게 만들어주는 기능이다.
- 동작 방식 - 인터페이스가 있으면 JDK 동적 프록시를 사용, 구체 클래스만 있으면 CGLIB를 사용한다.
- 부가 기능 적용 시 Advice를 만들면 InvocationHandler나 MethodInterceptor를 신경쓰지않아도 된다.
- 프록시 팩토리를 사용하면 Advice를 호출하는 전용 (스프링에서 제공) InvocationHandler, MethodInterceptor를 내부에서 사용
- Advice 동작 흐름
- 클라이언트가 프록시 팩토리에게 프록시 요청
- 프록시 팩토리는 프록시 기술을 선택 (JDK 동적 프록시 / CGLIB)
- 인터페이스의 경우 JDK 동적 프록시
- 구체 클래스의 경우 CGLIB
- 선택한 기술을 기반으로 동적 프록시를 생성
- JDK 동적 프록시의 경우 스프링에서 제공하는 adviceInvocationHandler를 호출
- CGLIB의 경우 스프링에서 제공하는 adviceMethodInterceptor를 호출
- handler / interceptor는 Advice를 호출
- Advice는 프록시 로직 수행 후 target(실제 객체) 호출
- target(실제 객체) 로직 수행
- 특정 조건에 맞을 때만 프록시 로직을 적용하려면 스프링이 제공하는 Pointcut을 도입해서 일관성 있게 해결가능
프록시 팩토리 - 예제
- Advice는 프록시에 적용하는 부가 기능 로직이다.
- InvocationHandler와 MethodInterceptor 개념과 유사하며 개념적으로 둘을 추상화한 것
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
- Advice를 만드는 가장 기본적인 방법
- MethodInterceptor 인터페이스를 구현해서 만든다.
- org.aopalliance.intercept 패키지의 MethodInterceptor로 CGLIB의 MethodInterceptor와 다르다.
- Interceptor 인터페이스가 Advice를 구현하고 있기 때문
- MethodInvocation invocation : 다음 메소드를 호출하는 방법, 현재 프록시 객체 인스턴스, args, 메소드 정보 등을 가지고 있는 변수이다.
- 기존 파라미터로 제공되는 부분들을 모두 합쳐놓았다.
- MethodInterceptor 인터페이스를 구현해서 만든다.
@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={}", resultTime);
return result;
}
}
- Advice 구현 예제
- TimeAdvice는 MethodInterceptor 인터페이스를 구현
- invocation.proceed() : target(실제 객체) 클래스를 호출하고 그 결과를 반환받는다.
- target 정보도 MethodInvaction invocation 안에 모두 포함되어 있다.
- 프록시 팩토리로 프록시를 생성하는 단계에서 target 정보를 파라미터로 받는다.
@Slf4j
public class ProxyFactoryTest {
@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
public 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 사용")
public 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();
}
}
- Advice 테스트 코드 예제
- interfaceProxy() / concreteProxy()
- new ProxyFactory(target) : 프록시 팩토리를 생성할 때 생성자로 프록시 호출 대상(실제 객체)을 같이 넘겨준다.
- 인스턴스 정보를 기반으로 프록시를 만들어내는데 인터페이스가 있으면 JDK 동적 프록시로 생성, 구체 클래스만 있다면 CGLIB로 동적 프록시를 생성한다.
- proxyFactory.addAdvice(new TimeAdvice()) : 프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직을 설정한다.
- Advice : 프록시가 제공하는 부가 기능 로직
- proxyFactory.getProxy() : 프록시 객체를 생성하고 그 결과를 받는다.
- new ProxyFactory(target) : 프록시 팩토리를 생성할 때 생성자로 프록시 호출 대상(실제 객체)을 같이 넘겨준다.
- interfaceProxy() / concreteProxy()
@Test
@DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용")
public void proxyTargetClass() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true);
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)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
- Advice 테스트 코드 예제
- proxyTargetClass()
- proxyFactory.setProxyTargetClass(true) : 인터페이스가 있더라도 CGLIB를 사용해서 인터페이스가 아닌 클래스 기반으로 동적 프록시를 만든다.
- proxyTargetClass()
- 프록시 팩토리 기술 선택 방법
- 인터페이스가 있는 경우 : JDK 동적 프록시, 인터페이스 기반 프록시
- 인터페이스가 없는 경우 : CGLIB, 구체 클래스 기반 프록시
- proxyTargetClass(true) : CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관이 없다.
- 스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass(true)로 설정해서 사용한다.
- 인터페이스가 있더라도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 동적 프록시 생성
- 프록시 팩토리 서비스 추상화로 CGLIB, JDK 동적 프록시 기술에 의존하지 않고 편리하게 동적 프록시를 사용할 수 있다.
포인트컷, 어드바이스, 어드바이저 - 소개
- 포인트컷(Pointcut) : 어디에 부가 기능을 적용할지, 적용하지 않을지 판단하는 필터 로직이다.
- 주로 메소드 이름으로 필터링한다.
- 어드바이스(Advice) : 프록시가 호출하는 부가 기능(=프록시 로직)이다.
- 어드바이저(Advisor) : 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다.
- 어드바이저 = 포인트컷1 + 어드바이스1
- 부가 기능을 적용할 때 포인트컷으로 어디에 적용할지 정하고 어드바이스로 어떤 로직을 적용할지 정하며 어디에? 어떤 로직을? 정할지 모두 알고 있는 것이 어드바이저이다.
- 역할과 책임을 명확하게 하기위해 분리한 것이다.
- 포인트컷은 대상 여부를 확인하는 필터 역할만 담당
- 어드바이스는 부가 기능 로직만 담당
- 프록시 전체 흐름
- 클라이언트 -> 프록시 호출
- 프록시는 Advisor를 알고 있다.
- 프록시가 Advisor 호출
- Pointcut으로 Advice 적용 여부 확인
- 적용 대상이라면 Advice 부가 기능을 적용 후 target 호출
- 적용 대상이 아닌 경우 부가 기능을 적용하지 않고 target 바로 호출
- 클라이언트 -> 프록시 호출
어드바이저 - 예제
- 어드바이저 Advisor : 하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
- 프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 적용할 지 알 수 있다.
public class AdvisorTest {
@Test
public void advisorTest1() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
}
- 어드바이저 테스트 예제 코드
- new DefaultPointcutAdvisor : Advisor 인터페이스의 가장 일반적인 구현체로 생성자를 통해 포인트컷 하나, 어드바이스 하나를 넣어주면 된다.
- Pointcut.TRUE : 항상 true를 반환하는 포인트컷이다.
- proxyFactory.addAdvisor(advisor) : 프록시 팩토리에 적용할 어드바이저를 지정한다.
- 어드바이저를 지정함으로 어떤 부가 기능을 어디에 적용할지 어드바이저 하나로 알 수 있다.
- 프록시 팩토리를 사용할 때 어드바이저는 필수이다.
- proxyFactory.addAdvice(advice) : 편의 메소드로 호출하면 내부에서는 DefaultPointcutAdvisor(Pointcut.TRUE, advice) 어드바이저가 생성된다.
- 프록시 팩토리는 어드바이저를 알고 있고 어드바이저는 포인트컷과 어드바이스를 알고 있는 관계이다.
직접 만든 포인트컷 예제
- 요구사항 - save() 메소드에는 어드바이스 로직을 적용하지만 find()에는 어드바이스 로직을 적용하지 않도록 하자
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
public interface ClassFilter {
boolean matches(Class<?> clazz);
}
public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
//..
}
- 스프링에서 제공하는 Pointcut 인터페이스
- Pointcut은 ClassFilter와 MethodMatcher로 이뤄져있다.
- ClassFilter는 클래스가 필터와 맞는지, MethodMatcher는 메소드가 맞는지 확인할 때 사용하며 둘 다 true를 반환할 때 어드바이스를 적용할 수 있다.
@Slf4j
public class AdvisorTest {
@Test
@DisplayName("직접 만든 포인트컷")
public void advisorTest2() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
static class MyPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE; //항상 TRUE를 반환
}
@Override
public MethodMatcher getMethodMatcher() {
return new MyMethodMatcher();
}
}
static class MyMethodMatcher implements MethodMatcher {
private String matchName = "save";
@Override
public boolean matches(Method method, Class<?> targetClass) {
boolean result = method.getName().equals(matchName);
log.info("포인트컷 호출 method={} targetClass={}", method.getName(), targetClass);
log.info("포인트컷 결과 result={}", result);
return result;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return false;
}
}
}
- 직접 구현한 포인트컷을 사용하는 예제
- MyPointcut 클래스
- 직접 구현한 포인트컷으로 Pointcut 인터페이스를 구현
- 요구 사항을 만족하기 위해서 메소드 기준으로 로직을 적용
- 클래스 필터는 ClassFilter.true로 지정해서 항상 true를 반환
- 메소드 필터로는 MyMethodMatch를 사용
- MyMethodMatcher
- 직접 구현한 MethodMatcher로 MethodMatcher 인터페이스를 구현
- matches() : method, targetClass 정보가 넘어오며 이 정보로 어드바이스를 적용할지 여부를 판단할 수 있다.
- 예제에서는 메소드 이름이 save인 경우에만 true를 반환하도록 판단 로직 적용
- isRuntime(), matches(... args) : isRuntime()이 참이면 matches(... args)가 대신 호출되며 동적으로 넘어오는 파라미터를 판단 로직으로 사용할 수 있다.
- isRutime()이 false인 경우 클래스 정적 정보만 사용하기에 스프링 내부에서 캐싱을 통한 성능 향상이 가능
- isRutime()이 true인 경우 파라미터가 동적으로 변경된다고 가정하기 때문에 캐싱을 하지 않는다.
- MyPointcut 클래스
스프링이 제공하는 포인트컷 - 예제
- 스프링이 제공하는 대표적인 포인트컷
- NameMatchMethodPointcut : 메소드 이름 기반으로 매칭하며 내부에서는 PatternMatchUtils 사용
- ex) *xxx* 허용
- JdkRegexpMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
- TruePointcut : 항상 참을 반환한다.
- AnnotationMatchingPointcut : 어노테이션으로 매칭한다.
- AspectJExpressionPointcut : aspectJ 표현식으로 매칭한다.
- NameMatchMethodPointcut : 메소드 이름 기반으로 매칭하며 내부에서는 PatternMatchUtils 사용
- 이외에도 스프링에서 제공하는 많은 포인트컷이 존재한다.
@Test
@DisplayName("스프링이 제공하는 포인트컷")
public void advisorTest3() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("save");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
- 스프링이 제공하는 NameMatchPointcut 사용 예제
- pointcut.setMappedName() : 인수로 전달한 메소드 이름과 일치하는 경우 Advice의 부가 기능을 적용한다.
- 실무에서는 사용하기도 편리하고 가장 많은 기능을 제공하는 aspectJ 표현식을 기반으로 하는 AspectJExpressionPointcut을 사용한다.
여러 어드바이저 함께 적용
- 하나의 target에 여러 어드바이저를 적용하려면?
- 여러 프록시를 만드는 방법
- 스프링에서 제공하는 기능을 사용하는 방법
public class MultiAdvisorTest {
@Test
@DisplayName("여러 프록시")
public void multiAdvisorTest1() {
//프록시1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();
//프록시2 생성, target -> proxy1 입력
ProxyFactory proxyFactory2 = new ProxyFactory(proxy1);
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
proxyFactory2.addAdvisor(advisor2);
ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();
//실행
proxy2.save();
proxy2.find();
}
@Slf4j
static class Advice1 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("advice1 호출");
return invocation.proceed();
}
}
@Slf4j
static class Advice2 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("advice2 호출");
return invocation.proceed();
}
}
}
- 여러 어드바이스를 하나의 타겟에 적용하기 (여러 프록시 사용) 예제
- 예제 흐름 : client -> proxy2(advisor2) -> proxy1(advisor1) -> target
- 여러 프록시의 문제는 잘못된 것은 아니지만 적용해야하는 어드바이저 수만큼 프록시를 생성해야 한다.
- 스프링에서 하나의 프록시에 여러 어드바이저를 적용할 수 있도록 기능을 제공한다.
@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
public void multiAdvisorTest2() {
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
//프록시 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
proxyFactory1.addAdvisor(advisor2);
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
//실행
proxy.save();
}
- 하나의 프록시에 여러 어드바이저 적용 예제
- 예제 흐름 : client -> proxy -> advisor2 -> advisor1 -> target
- proxyFactory.addAdvisor()를 통해 하나의 프록시에 원하는 수만큼 어드바이저를 등록할 수 있다.
- 등록하는 순서대로 어드바이저가 호출된다.
- 하나의 프록시에 여러 어드바이저를 등록하면 여러 프록시를 사용하는 것보다 성능은 좋고 결과는 같다.
- 스프링은 AOP를 적용할 때 최적화를 위해 프록시는 하나만 생성하고 하나의 프록시에 여러 어드바이저를 적용한다.
- 하나의 target에 여러 AOP가 동시에 적용되어도 스프링 AOP는 target마다 하나의 프록시만 생성한다.
- 현재 방식의 문제점
- 설정 파일이 지나치게 많다.
- 애플리케이션에 스프링 빈이 100개 있고 프록시로 부가 기능을 적용하려면 100개의 동적 프록시 생성 코드를 만들어야 한다.
- 컴포넌트 스캔
- 컴포넌트 스캔을 사용하는 경우 실제 객체를 빈으로 등록하기 때문에 프록시 적용이 불가능하다.
- 설정 파일이 지나치게 많다.
- 이런 문제점들을 모두 해결하는 방법이 빈 후처리기이다.
정리
- 프록시 팩토리 : 스프링에서 제공하는 동적 프록시를 통합해서 편리하게 만들어주는 기능이다.
- 동작 방식 - 인터페이스가 있으면 JDK 동적 프록시를 사용, 구체 클래스만 있으면 CGLIB를 사용한다.
- 부가 기능 적용 시 Advice를 만들면 InvocationHandler나 MethodInterceptor를 신경쓰지않아도 된다.
- Advice는 프록시에 적용하는 부가 기능 로직으로 InvocationHandler와 MethodInterceptor 개념과 유사하며 개념적으로 둘을 추상화한 것이다.
- Advice는 MethodInterceptor를 구현해서 만든다.
- 스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass(true)로 설정해서 사용한다.
- 인터페이스가 있더라도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 동적 프록시 생성한다.
- 포인트컷(Pointcut) : 어디에 부가 기능을 적용할지, 적용하지 않을지 판단하는 필터 로직이다.
- 어드바이스(Advice) : 프록시가 호출하는 부가 기능(=프록시 로직)이다.
- 어드바이저(Advisor) : 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다.
- 어드바이저 = 포인트컷1 + 어드바이스1
- 부가 기능을 적용할 때 포인트컷으로 어디에 적용할지 정하고 어드바이스로 어떤 로직을 적용할지 정하며 어디에, 어떤 로직을 정할지 모두 알고 있는 것이 어드바이저이다.
- 스프링이 제공하는 ProxyFactory를 사용하면 하나의 프록시에 여러 어드바이저를 등록해서 사용할 수 있다.
- 스프링은 AOP를 적용할 때 최적화를 위해 프록시는 하나만 생성하고 하나의 프록시에 여러 어드바이저를 적용한다.
- 하나의 target에 여러 AOP가 동시에 적용되어도 스프링 AOP는 target마다 하나의 프록시만 생성한다.
출처 : [인프런 김영한 스프링 핵심 원리 - 고급편]
스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런
김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기
www.inflearn.com