프록시 팩토리(스프링이 지원하는 프록시)
동적 프록시의 문제점
- 인터페이스가 있는경우에는 JDK동적 프록시를 적용하고 그렇지 않은 경우에 CGLIB를 적용하려면 어떻게 해야할까?
- 동시에 사용하는 경우에는 중복으로 만들어서 관리해야하나?
- 특정 조건에 맞을때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면
프록시 팩토리
스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리 라는 기능을 제공한다.
프록시 팩토리는 인터페이스가 있으면 JDK동적프록시를 사용하고 구체 클래스만 있으면 CGLIB를 지원한다.
클라이언트가 프록시팩토리로 요청을 하면 프록시 팩토리가 상황에 따라서 jdk동적프록시나 CGLIB구체클래스 프록시로 반환해준다.
- 부가기능을 적용하기위해 각각 중복으로 따로 만들어야 할까?
스프링은 이문제를 해결하기 위해 부가 기능을 적용할때 'Advice'라는 새로운 개념을 도입했다.
개발자는 'Advice'만 만들면 InvocationHandler나 MethodInterceptor를 신경쓰지않아도 된다. - 특정 조건에 맞을때 프록시 로직을 적용하는 기능도 공통으로 제공된다.이때 스프링에선 'Pointcut'을 사용하면된다.
- MethodInterceptor와 어드바이스가 이름이 같기때문에 패키지를 잘 봐야한다.
- org.aopalliance.intercept에 있다.
ServiceInterface target = new ServiceImpl();
//여기서 프록시를 만들때 타캣을 넣어준다.
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
//프록시팩토리를 사용할 때만 사용가능
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
proxyFactory.getProxy(): 프록시 객체를 생성하고 그 결과를 받는다.
위와같이 해도되고 인스턴스에 직접 클래스정보를 출력해서 확인할 수 있다 .getClass();
프록시팩토리의 기술 선택방법
- 대상에 인터페이스가 있으면: JDK 동적프록시, 인터페이스 기반프록시
- 대상에 인터페이스가 없으면: CGLIB, 구체클래스 기반 프록시
- 'proxyTargetClass=ture': CGLIB구체 클래스 기반 프록시, 인터페이스 여부와 상관없음
정리
프록시 팩토리의 서비스 추상화 덕분에 구체적인 프록시기술에 의존하지 않고 매우 편리하게 동적프록시를 생성할 수 있다.
내부적으로 핸들링이 돼 있기 때문이다.
참고
스프링 부트는 AOP를 적용할때 기본적으로 'proxyTargetClass=ture'로 설정해서 사용한다.
따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체클래스를 기반으로 프록시를 생성한다.
포인트컷, 어드바이스,어드바이저
- 포인트컷: 어디에 부가기능을 적용할지, 어디에 부가기능을 적용하지 않을지 판단하는 필터링 로직이다. 주로 클래스와 메서드 이름으로 필터링한다.
이름 그대로 어떤 포인트에 기능을 적용할지 하지않으리 잘라서 구분하는것이다. - 어드바이스: 이전에 본 것 처럼 프록시가 호출하는 부가 기능이다. 단순하게 프록시 로직같은 것
- 어드바이저: 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다.
예를 들어 부가 기능 로직을 적용해야하는데 포인트컷으로 어디에 적용할지 선택하고,
어드바이스로 어떤 로직을 적용할지 선택하는 것이다.
어디와 어떤 로직을 모두 알고 있는것이 어드바이저 이다.
역할과 책임
이렇게 구분한 이유 역할과 책임을 명확하게 분리한 것이다.
- 포인트 컷은 대상여부를 확인하는 필터 역할만 담당한다.
- 어드바이스는 깔끔하게 부가기능 로직만 담당한다.
- 어드바이저는 위 두가지 기능을 모두 담당한다.
사용예시
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();
- DefaultPointcutAdvisor : Advisor 인터페이스의 가장 일반적인 구현체 이다. 생성자를 통해 하나의 포인트컷과 하나의 어드바이스를 넣으면된다.
- Pointcut.TRUE: 항상 true를 반환하는 포인트 컷이다.
- proxyFactory.addAdvisor(advisor): 프록시 팩토리에 적용할 어드바이저를 지정한다.
어드바이저는 내부에 포인트컷과 어드바이스를 모두 가지고 있다. 따라서 어디에 어떤 부가기능을 적용해야할지 어드바이스 하나로 알 수 있다.
프록시 팩토리를 사용할때 어드바이저는 필수이다.
스프링이 제공하는 포인트컷
스프링은 무수히 많은 포인트 컷을 제공한다.
대표적인 몇가지
- NameMatchMethodPointcut: 메서드 이름을 기반으로 매칭한다.
- JdkRegexpMethodPointcut: JDK정규 표현식을 기반으로 포인트컷을 매칭한다.
- TruePointcut: 항상 참을 반환한다.
- AnnotationMatchingPointcut: 애노테이션으로 매칭한다.
- AspectJexpressionPointcut: aspectJ 표현식으로 매칭한다
가장 중요한것은 aspectJ표현식
여기에서 실무에서 사용하기도 편리하고 기능도 가장 많은 aspectJ표현식을 기반으로 사용하는
AspectJExpressionPointcut을 사용하게 된다.
여러 어드바이저 함께 적용
어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가지고 있다. 여러 어드바이저를 하나의 target에 적용하려면?
프록시를 여러개를 만드는 방법이 있지만 좋지않다.
@Test
@DisplayName("하나의 프록시 여러 어드바이저")
void multiAdvisorTest2(){
//client -> proxy->advisor1 -> advisor2 -> target
//여러 어드바이저 적용
//이때 각 어드바이저의 포인트컷이 적용된상태로 실행된다.
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
//프록시 1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
//순서대로 동작
proxyFactory1.addAdvisor(advisor2);
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();
//실행
proxy1.save();
중요
스프링 AOP를 처음 공부하거나 적용하면 AOP적용 수 만큼 프록시가 생성된다고 착각하게 된다.
스프링은 AOP를 적용할때 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.
정리하면 하나의 target에 여러 AOP가 동시에 적용되어도 스프링은 AOP는 target마다 하나의 프록시만 생성하고
그 안에 여러 어드바이저를 넣는다는것이다.
문제
- 설정이 너무 많다
예를 들어 애플리케이션의 스프링빈이 100개가 있다면 프록시를 통해 100개다 부가기능을 제공하려면 100개의 동적프록시를 생성해야한다. - 컴포넌트 스캔
컴포넌트 스캔을 하게 되면 위방법으로 불가능 하다. 왜냐하면 프록시를 스프링빈에 등록한 방식이기때문에 이미 컴포넌트스캔된 실제객체를 등록해버리는 방식에선 사용할 수 없다.
이를 해결하려면 빈 후처리기를 알아야한다.
'Springboot' 카테고리의 다른 글
@Aspect Springboot AOP 시작 (0) | 2023.08.14 |
---|---|
빈 후처리기 (0) | 2023.08.14 |
JDK동적프록시/CGLIB (0) | 2023.08.14 |
java리플렉션 (0) | 2023.08.14 |
탬플릿 메서드 패턴 (0) | 2023.08.14 |