일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch4
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch13
- 스프링 mvc1 - 스프링 mvc
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch2
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch9
- 스프링 db2 - 데이터 접근 기술
- 스프링 mvc2 - 로그인 처리
- 2024 정보처리기사 수제비 실기
- 2024 정보처리기사 시나공 필기
- 스프링 mvc1 - 서블릿
- jpa - 객체지향 쿼리 언어
- 스프링 mvc2 - 검증
- @Aspect
- 자바의 정석 기초편 ch1
- jpa 활용2 - api 개발 고급
- 스프링 입문(무료)
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch7
- 타임리프 - 기본기능
- 게시글 목록 api
- Today
- Total
나구리의 개발공부기록
스프링이 지원하는 프록시, 프록시 팩토리(소개/예제), 포인트컷/어드바이스/어드바이저 - 소개, 예제(어드바이저/직접 만든 포인트컷/스프링이 제공하는 포인트컷/여러 어드바이저 함께 제공), 프록시 팩토리 적용 본문
스프링이 지원하는 프록시, 프록시 팩토리(소개/예제), 포인트컷/어드바이스/어드바이저 - 소개, 예제(어드바이저/직접 만든 포인트컷/스프링이 제공하는 포인트컷/여러 어드바이저 함께 제공), 프록시 팩토리 적용
소소한나구리 2024. 11. 10. 19:06출처 : 인프런 - 스프링 핵심 원리 - 고급편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 프록시 팩토리 - 소개
1) 기존 동적 프록시의 문제점을 해결
(1) 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용
- 스프링은 유사한 구체적인 기술들이 있을 때 그것들을 통합해서 일관성 있게 접근할 수 있고 더욱 편리하게 사용할 수 있는 추상화된 기술을 제공하는데, 동적 프록시도 통합해서 편리하게 만들어주는 프록시 팩토리(ProxyFactory)라는 기능을 제공함
- 이전에는 상황에 따라서 개발자가 직접 JDK 동적 프록시를 사용하거나 CGLIB를 사용해야 했다면 프록시 팩토리 하나로 편리하게 동적 프록시를 생성할 수 있음
- 프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용하며 옵션을 통해 설정을 변경할 수도 있음
- 즉, 클라이언트는 프록시 팩토리만 의존하면 프록시 팩토리가 알아서 환경에 따라 JDK 동적 프록시나 CGLIB를 생성해주어 매우 편리하게 동적 프록시를 사용할 수 있음
(2) 두 기술을 함께 사용할 때 부가 기능을 적용하기 위한 방법
- 스프링은 이 문제를 해결하기 위해 부가기능을 적용할 때 Advice라는 새로운 개념을 도입함
- 개발자는 InvocationHandler(JDK 동적 프록시)나 MethodInterceptor(CGLIB)를 신경쓰지 않고 Advice만 만들면 됨
- 프록시 팩토리를 사용하면 Advice를 호출하는 전용 InvocationHandler와 MethodInterceptor를 내부에서 사용하여 결과적으로 개발자가 만든 Advice를 호출하게 됨
(3) 특정 조건에 맞을 때 프록시 로직을 적용하는 기능을 공통으로 제공
- 앞서 특정 메서드 이름의 조건에 맞을 때만 프록시 부가 기능이 적용되는 코드를 필터를 통해 직접 만들었음
- 스프링은 Pointcut이라는 개념을 도입해서 이 문제를 일관성 있게 해결함
2. 프록시 팩토리 - 예제
1) 예제1
(1) Advice 생성
- Advice는 프록시에 적용하는 부가 기능 로직이며 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor의 개념을 추상화 한 것이므로 프록시 팩토리를 사용하면 둘 대신에 Advice를 사용하면 됨
- Advice를 만드는 방법은 여러가지만 있지만 기본적인 방법으로는 아래의 인터페이스를 구현하면 됨
(2) 스프링이 제공하는 MethodInterceptor
- org.aopalliance.intercept 패키지에 있는 MethodInterceptor를 구현하면 되며 CGLIB의 MethodInterceptor와 이름이 같기 때문에 패키지 이름에 주의해야함
- MethodInterceptor -> Interceptor -> Advice 로 상속관계가 되어있기 때문에 해당 인터페이스를 구현하는 것을 Advice를 구현한 것과 같음
- MethodInvocation 내부에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args, 메서드 정보 등이 포함되어있으며 기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 됨
package org.aopalliance.intercept;
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
@Nullable
Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
(3) TimeAdvice - 실제 Advice 생성
- test 하위의 common 패키지에 advice 패키지를 생성 후 작성
- MethodInterceptor 인터페이스를 구현할 때 import 패키지 이름에 주의
- 기존의 프록시코드와 거의 유사하지만 프록시에는 항상 target(실제 클래스의 정보)가 있어야한다고했는데 여기에서는 보이지 않음
- invocation.proceed()를 호출하면 target클래스를 호출하고 그 결과를 받으며, 여기 안에 target 클래스의 정보가 모두 포함되어있음
- 그 이유는 바로 다음에 확인할 수 있는데 프록시 팩토리로 프록시를 생성하는 단계에서 이미 target정보를 파라미터로 전달받기 때문임
package hello.proxy.common.advice;
import org.aopalliance.intercept.MethodInterceptor; // import 패키지에 주의
@Slf4j
public class TimeAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
// target을 직접 입력해주지 않아도 됨, invocation.proceed() 메서드에서 알아서 동작함
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
(4) ProxyFactoryTest 작성 및 실행결과
- proxy하위에 proxyfactory 패키지를 만들어서 테스트 코드를 작성
- new ProxyFactory(target) : 프록시 팩토리를 생성할 때 프록시의 호출 대성을 넘겨주고 이 인스턴스 정보를 기반으로 프록시를 만들어냄,
- 이 인스턴스에 인터페이스가 있다면 JDK 동적 프록시를 생성하고 인터페이스 없이 구체 클래스만 있다면 CGLIB를 통해서 동적 프록시를 생성함
- 해당 예제에서는 target이 new ServiceImpl()의 인스턴스이기 때문에 ServiceInterface라는 인터페이스가 있으므로 JDK 동적 프록시를 생성함
- proxyFactory.addAdvice(new TimeAdvice()) : 프록시 팩토리를 통해서 만든 프록시가 사용할 부가기능 로직을 설정함
- proxyFactory.getProxy() : 프록시 객체를 생성하고 그 결과를 받음
- 프록시 팩토리를 통해 프록시를 생성하면 AopUtils를 사용하여 편리하게 프록시의 여러가지 정보를 확인할 수 있으며 .getClass()처럼 인스턴스의 클래스 정보를 직접 출력해서 확인할 수도 있음
- AopUtils.isAopProxy() : 프록시 팩토리를 통해 생성한 프록시이면 참
- AopUtils.isJdkDynamicProxy() : 프록시 팩토리를 통해 생성한 프록시이며 JDK 동적 프록시인 경우 참
- AopUtils.isCglibProxy() : 프록시 팩토리를 통해서 프록시가 생성되고 CGLIB 동적 프록시인 경우 참
- 실행해보면 테스트로 적용한 Advice가 정상 동작하는 로그와 proxy.getClass에서 jdk.proxy3.$proxy13으로 jdk 동적 프록시가 적용되어 프록시가 생성된 로그를 확인할 수 있음
- 프록시 팩토리로 생성한 프록시이기에 AopUtils를 활용한 테스트코드들도 모두 통과함
package hello.proxy.proxyfactory;
@Slf4j
public class ProxyFactoryTest {
@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
void interfaceProxy() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target); // ProxyFactory에 target을 입력하여 생성
proxyFactory.addAdvice(new TimeAdvice()); // proxyFactory에 advice를 입력
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy(); // proxy꺼내기
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.save();
proxy.find();
// 프록시 팩토리에서 만든 프록시는 AopUtils를 사용하여 프록시의 다양한 정보를 확인할 수 있음
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
}
}
/*
13:56:31.592 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest -- targetClass=class hello.proxy.common.service.ServiceImpl
13:56:31.594 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest -- proxyClass=class jdk.proxy3.$Proxy13
13:56:31.597 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 실행
13:56:31.598 [Test worker] INFO hello.proxy.common.service.ServiceImpl -- save 호출
13:56:31.598 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=1
13:56:31.598 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 실행
13:56:31.598 [Test worker] INFO hello.proxy.common.service.ServiceImpl -- find 호출
13:56:31.598 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
*/
2) 예제2
(1) ProxyFactoryTest - concreteProxy 추가
- 구체 클래스만 있는 ConcreteService에 프록시 팩토리를 적용
- 프록시 팩토리는 인터페이스 없이 구체 클래스만 있으면 CGLIB를 사용해서 프록시를 적용함
- 실행 결과 로그를 보면 생성된 프록시가 SpringCGLIB로 동작하는 것을 확인할 수 있으며 테스트 코드에서도 isCglibProxy()가 true로 통과됨
@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();
// 프록시 팩토리에서 만든 프록시는 AopUtils를 사용하여 프록시의 다양한 정보를 확인할 수 있음
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
/*
14:30:13.842 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest -- targetClass=class hello.proxy.common.service.ConcreteService
14:30:13.844 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest -- proxyClass=class hello.proxy.common.service.ConcreteService$$SpringCGLIB$$0
14:30:13.846 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 실행
14:30:13.846 [Test worker] INFO hello.proxy.common.service.ConcreteService -- ConcreteService 호출
14:30:13.846 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
*/
(2) ProxyFactoryTest - proxyTargetClass 추가
- 인터페이스가 있어도 CGLIB를 사용해서 인터페이스가 아닌 클래스 기반으로 동적 프록시를 생성할 수 있음
- 프록시팩토리가 제공하는 옵션 중 .setProxyTargetClass()메서드에 true를 입력하면 인터페이스가 있어도 강제로 CGLIB를 사용하여 클래스 기반의 프록시를 만들어줌
- 실행 결과를 보면 인터페이스가 있는 인스턴스임에도 SpringCGLIB로 프록시가 생성되고 isCglibProxy()메서드도 true로 동작하여 테스트가 통과되는 것을 확인할 수 있음
- 해당 옵션은 실무에서 종종 나오기때문에 꼭 알고 있어야함
@Test
@DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고 클래스 기반 프록시를 사용")
void proxyTargetClass() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true); // 인터페이스가 아닌 TargetClass를 기반으로 프록시를 만들도록 설정
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();
}
/*
14:32:55.696 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest -- targetClass=class hello.proxy.common.service.ServiceImpl
14:32:55.698 [Test worker] INFO hello.proxy.proxyfactory.ProxyFactoryTest -- proxyClass=class hello.proxy.common.service.ServiceImpl$$SpringCGLIB$$0
14:32:55.699 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 실행
14:32:55.699 [Test worker] INFO hello.proxy.common.service.ServiceImpl -- save 호출
14:32:55.700 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=1
*/
(3) 프록시 팩토리의 기술 선택 방법
- 대상에 인터페이스가 있으면 JDK 동적 프록시 - 인터페이스 기반 프록시로 생성
- 대상에 인터페이스가 없으면 CGLIB - 구체 클래스 기반 프록시로 생성
- proxyTargetClass(true) 옵션을 사용하면 인터페이스 여부와 상관없이 CGLIB - 구체 클래스 기반 프록시로 생성
(4) 정리
- 프록시 팩토리의 서비스 추상화 덕분에 구체적인 CGLIB, JDK 동적 프록시 기술에 의존하지 않고 매우 편리하게 동적 프록시를 생성할 수 있음
- 프록시의 부가 기능 로직도 특정 기술에 종속적이지 않게 Advice 하나로 편리하게 사용할 수 있음
** 참고
- 스프링부트 2.x 부터 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정해서 사용하기에 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성함
- 자세한 이유는 강의 뒷부분에서 설명
3. 포인트컷, 어드바이스, 어드바이저 - 소개
1) 용어 정리
(1) 포인트컷(Pointcut)
- 어디에 부가 기능을 적용할 것인지, 적용하지 않을 것인지 판단하는 필터링 로직
- 주로 클래스와 메서드 이름으로 필터링하며 이름 그대로 어떤 포인트(Point)에 기능을 적용할지, 하지 않을지 잘라서(cut) 구분하는 것이라고 이해하면 됨
(2) 어드바이스(Advice)
- 프록시가 호출하는 부가 기능이며 단순히 프록시 로직이라고 생각하면 됨
(3) 어드바이저(Advisor)
- 하나의 포인트컷과 하나의 어드바이스를 가지고 있음
(4) 역할과 책임
- 이렇게 구분한 것은 역할과 책임을 명확하게 분리한 것임
- 포인트컷은 대상 여부를 확인하는 필터 역할만 담당하고 어드바이스는 깔끔하게 부가 기능 로직만 담당함
- 이 둘을 합치면 어드바이저가되며 스프링의 어드바이저는 하나의 포인트컷과 하나의 어드바이스로 구성됨
** 참고
- 해당 단어들에 대한 정의는 문맥상 이해를 돕기 위해 프록시에 맞추어서 설명되었음
- 이후에 AOP 부분에서 다시한번 AOP에 맞춰서 정리할 예정임
- 아래의 전체 구조는 이해를 돕기위한 것이며 실제 구현은 다를 수 있음
4. 포인트컷, 어드바이스, 어드바이저 - 예제
1) 예제1 - 어드바이저
- 어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가지고 있으며 프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 제공할 지 알 수 있음
(1) AdvisorTest 생성 및 실행
- test하위에 advisor패키지를 생성 후 작성
- new DefaultPointcutAdvisor : Advisor 인터페이스의 가장 일반적인 구현체, 생성자를 통해 하나의 포인트컷과 하나의 어드바이스를 넣어주면되고 어드바이저는 하나의 포인트컷과 하나의 어드바이스로 구성됨
- Pointcut.TRUE: 항상 true를 반환하는 포인트컷
- new TimeAdvice( ) : 앞서 개발한 TimeAdvice 어드바이스를 적용
- proxyFactory.addAdvisor(advisor) : 프록시 팩토리에 적용할 어드바이저를 지정
- 위에서 지정한 어드바이저는 내부에 포인트컷과 어드바이스를 모두 가지고 있어 어디에 어떤 부가기능을 적용해야할 지 어드바이저 하나로 알 수 있으므로 프록시 팩토리를 사용할 때 어드바이저는 필수임
- 이전의 프록시 팩토리를 사용해보는 예제에서는 proxyFactory.addAdvice()로 직접 어드바이스를 바로 적용했는데, 이것은 단순히 편의 메서드이고 addAdvice()의 내부에서 지금코드와 똑같은 어드바이저가 생성되도록 코드가 작성되어있음
- 테스트 코드를 실행해보면 save, find 메서드 모두 TimeAdvice가 적용되어 정상적으로 동작하는 것을 확인할 수 있음
package hello.proxy.advisor;
public class AdvisorTest {
@Test
void advisorTest1() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
// advisor 생성 - 포인트컷은 항상 참이고, 직접 정의한 TimeAdvice()를 어드바이스로 사용
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
}
2) 예제2 - 직접 만든 포인트 컷
- save()메서드에는 어드바이스 로직을 적용하고 find()메서드에는 어드바이스 로직을 적용하지 않도록 포인트 컷을 직접 만들어서 적용
- 물론 어드바이스에 로직을 추가하여 메서드 이름을 보고 코드를 실행할지 말지 if문 등을 활용할 수 있지만 재사용성이 떨어지고 단일책임원칙에 위배가 되기 때문에 이런 기능에 특화된 포인트 컷을 사용하는 것이 좋음
(1) 스프링이 제공하는 Pointcut 관련 인터페이스들
- 포인트 컷은 크게 클래스가 맞는지 확인하는 ClassFilter와 메서드가 맞는지 확인하는 MethodMatcher 둘로 이루어지며 둘 다 true로 반환해야 어드바이스를 적용할 수 있음
- 일반적으로는 스프링이 만들어둔 구현체를 사용하며 개념 학습 차원에서 간단히 직접 구현해보는 실습을 진행
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
public interface ClassFilter {
boolean matches(Class<?> clazz);
}
public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
// ...
}
(2) AdvisorTest에 advisorTest2 추가
- 기존 로직은 똑같으며 new DefaultPointcutAdvisor()를 생성할 때 직접 만든 MyPointcut을 생성하여 사용하도록 입력
(3) MyPointcut
- 직접 구현한 포인트컷으로 Pointcut 인터페이스를 구현
- 실제 객체의 메서드를 기준으로 로직을 적용하면 되며 클래스 필터는 항상 True를 반환하도록하고 메서드 비교 기능은 직접 구현한 MyMethodMatcher를 사용하도록 설정
(4) MyMethodMatcher
- MethodMatcher 인터페이스를 구현하여 메소드 비교기능을 직접 정의
- matches() : 이 메서드에 method, targetClass 정보가 넘어오고 이 정보로 어드바이스를 적용할지 말지 판단할 수 있음
- 해당 로직에서는 이름이 save인 경우에 true를 반환하도록 판단 로직을 적용함
** 중요하지 않은 부분
- isRuntime()과 matches(Method method, Class<?> targetClass, Object... args) : isRuntime값이 true일 때 매개변수가 3개인 matches 메서드가 대신 호출되며 동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있음
- isRuntime()이 false인 경우 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능하지만 true인 경우에는 매개변수가 동적으로 변경된다고 가정하기 때문에 캐싱을 하지 않도록 동작함
- 크게 중요하지 부분은 아니므로 참고만 하고 넘어가면 됨
(5) 실행결과
- 실행해보면 save()를 호출할 때는 어드바이스가 적용되어 TimePorxy가 실행되었지만 find()를 호출할 때는 포인트컷 결과가 result=false로 동작하여 TimeProxy가 동작하지 않음을 로그로 확인할 수 있음
@Test
@DisplayName("직접 만든 포인트 컷")
void advisorTest2() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
// 직접 구현한 MyPointcut()을 사용하여 advisor를 생성
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(); // 직접 구현한 메서드 확인 로직을 생성하여 반환
}
}
// 메서드 이름이 save일때만 true로 반환하도록 구현
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;
}
}
/*
16:31:01.526 [Test worker] INFO hello.proxy.advisor.AdvisorTest -- 포인트컷 호출 method=save targetClass=class hello.proxy.common.service.ServiceImpl
16:31:01.528 [Test worker] INFO hello.proxy.advisor.AdvisorTest -- 포인트컷 결과 result=true
16:31:01.530 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 실행
16:31:01.530 [Test worker] INFO hello.proxy.common.service.ServiceImpl -- save 호출
16:31:01.530 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
16:31:01.530 [Test worker] INFO hello.proxy.advisor.AdvisorTest -- 포인트컷 호출 method=find targetClass=class hello.proxy.common.service.ServiceImpl
16:31:01.530 [Test worker] INFO hello.proxy.advisor.AdvisorTest -- 포인트컷 결과 result=false
16:31:01.530 [Test worker] INFO hello.proxy.common.service.ServiceImpl -- find 호출
*/
(6) 그림으로 확인
- save()메서드는 Pointcut에서 Advice의 적용여부를 확인하고 true가 반환되어 프록시 Advice를 적용하여 부가기능을 적용한 후 실제 인스턴스의 save()를 호출함
- 하지만 find()메서드는 Pointcut에서 Advice의 적용여부를 확인할 때 false가 반환되어 Advice를 적용하지 않고 바로 find()를 호출하여 부가기능이 적용되지 않은 채 실제 인스턴스의 find()를 호출함
3) 예제3 - 스프링이 제공하는 포인트 컷
- 스프링은 우리가 필요한 포인트컷을 이미 대부분 제공함
- 스프링이 제공하는 NameMatchMethodPointcut을 사용해서 구현해보기(가장 쉬운 포인트컷)
(1) advisorTest3 추가
- new NameMatchMethodPointcut를 생성하여 setMappedName() 메서드에 통과할 메서드의 이름을 지정하면 포인트컷이 간단하게 완성됨
- 출력 결과를 보면 save()메서드는 TimeProxy가 적용되었지만 find()에는 적용되지 않은것을 확인할 수 있음
@Test
@DisplayName("스프링이 제공하는 포인트컷")
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();
}
/* 실행 결과
16:53:07.039 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 실행
16:53:07.041 [Test worker] INFO hello.proxy.common.service.ServiceImpl -- save 호출
16:53:07.041 [Test worker] INFO hello.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
16:53:07.042 [Test worker] INFO hello.proxy.common.service.ServiceImpl -- find 호출
*/
(2) 스프링이 제공하는 포인트컷
- 무수히 많은 포인트컷을 제공하는 스프링의 대표적인 포인트컷
- NameMatchMethodPointcut : 메서드 이름을 기반으로 매칭하며 내부에서는 PatternMatchUtils를 사용함(예시 - *xxx* 허용)
- JdkRegexMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매칭
- TruePointcut : 항상 참을 반환
- AnnotationMatchingPointcut : 애노테이션으로 매칭
- AspectJExpressionPointcut : aspectJ 표현식으로 매칭
(3) 가장 중요한 것은 aspectJ 표현식
- 사실 다른것은 중요하지 않고 실무에서는 사용하기도 편리하고 기능도 가장 많은 aspectJ 표현식을 기반으로 사용하는 AspectJExpresstionPointcut을 사용함
- AOP를 설명할 때 aspectJ 표현식과 사용방법을 다시 설명할 예정이므로 지금은 Pointcut의 동작 방식과 전체 구조를 이해하는 것에 초점을 맞춤
4) 예제4 - 여러 어드바이저와 함께 적용
- 여러 어드바이저를 하나의 target에 적용하는 방법을 알아보는 예제
- 즉, 하나의 target에 여러 어드바이스를 적용하는 방법을 예제로 확인해보기
(1-1) 여러 프록시를 만들어서 적용 - MultiAdvisorTest 생성
- target을 입력받아 생성된 프록시와, 프록시를 입력받아 생성된 프록시 2개의 프록시를 생성하여 각각의 어드바이스를 적용시키는 방식으로 여러 프록시를 적용
- 그림으로 보면 프록시를 생성할 때 이전 프록시를 입력하면서 프록시를 생성하여 마지막으로 실제 target의 로직을 호출하는 프록시는 전체 부가기능이 적용된 프록시가 되어 로직을 호출하게 됨
- 포인트컷을 항상 true가 반횐되도록 적용하여 어드바이스가 항상 적용되도록 설정 후 실행해보면 요구사항에 맞게 여러 어드바이저를 함께 사용하여 1개의 target에 여러 어드바이스가 적용되었음
package hello.proxy.advisor;
public class MultiAdvisorTest {
@Test
@DisplayName("여러 프록시")
void multiAdvisorTest1() {
// client -> proxy2(advisor2) -> proxy1(advisor1) -> target 구조
// 프록시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 생성
ProxyFactory proxyFactory2 = new ProxyFactory(proxy1); // target이 아닌 생성한 프록시를 입력하여 프록시 팩토리를 생성
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
proxyFactory2.addAdvisor(advisor2);
ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();
proxy2.save();
}
@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();
}
}
}
/*
17:53:47.793 [Test worker] INFO hello.proxy.advisor.MultiAdvisorTest$Advice2 -- advice2 호출
17:53:47.795 [Test worker] INFO hello.proxy.advisor.MultiAdvisorTest$Advice1 -- advice1 호출
17:53:47.795 [Test worker] INFO hello.proxy.common.service.ServiceImpl -- save 호출
*/
(1-2) 여러 프록시의 문제
- 이 방법이 잘못된 것은 아니지만 프록시를 2번 생성해야 한다는 문제가 있음
- 즉, 적용해야하는 어드바이저가 100개 1000개면 그에 대응하는 프록시를 똑같이 생성해야 한다는 문제가 발생함
(2-1) 하나의 프록시, 여러 어드바이저 적용
- 스프링은 이 문제를 해결하기 위해서 하나의 프록시에 여러 어드바이저를 적용할 수 있게 만들어 두었음
(2-2) MultiAdvisorTest - multiAdvisorTest2() 추가
- 하나의 프록시에 여러 어드바이저를 addAdvisor로 입력할 수 있으며 입력한 순서대로 로직이 동작하므로 원하는 어드바이저의 순서대로 입력해 주면 됨
- 테스트 코드를 작성하면 똑같이 정상적으로 2개의 어드바이저가 동작하며 결과적으로 여러 프록시를 사용할 때와 비교해서 결과는 같고 성능은 더 좋음
@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2() {
// client -> proxy -> advisor2 -> advisor1 -> target
// 어드바이저 생성
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
// 프록시 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvisor(advisor2);
proxyFactory.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
// 실행
proxy.save();
}
** 중요
- 매우 간단하게 설명할 수 있는 부분을 이렇게 풀어서 설명한 이유는 스프링의 AOP를 처음 공부하거나 처음 사용하면 AOP 적용 수 만큼 프록시가 생성된다고 착각하는 사람이 많다고 하며 실무 개발자들도 이렇게 생각하는 경우가 많다고 함(이제는 김영한님 강의 덕분에 안그럴듯)
- 스프링은 AOP를 적용할 때 최적화를 진행해서 지금처럼 프록시는 하나만 만들고 하나의 프록시에 여러 어드바이저를 적용함
- 즉, 하나의 target에 여러 AOP가 동시에 적용되어도 스프링의 AOP는 target마다 하나의 프록시만 생성한다는 것을 꼭 기억할 것
5. 프록시 팩토리 - 적용
1) 인터페이스가 있는 v1에 적용
- 프록시 팩토리를 사용해서 인터페이스가 있는 v1 애플리케이션에 프록시를 생성하여 적용
(1) LogTraceAdvice
- main의 proxy.config 하위 패키지에 v3_proxyfactory.advice 패키지를 만들어서 생성
- 실제 target정보는 invocation에 들어가있으므로 적용할 LogTrace만 생성자로 만들어주면 됨
- method의 정보를 invocation에서 꺼내서 message를 만들 수 있으며 invocation.proceed()로 실제 비즈니스 로직을 동작시키도록 작성
package hello.proxy.config.v3_proxyfactory.advice;
public class LogTraceAdvice implements MethodInterceptor {
private final LogTrace logTrace;
public LogTraceAdvice(LogTrace logTrace) {
this.logTrace = logTrace;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
TraceStatus status = null;
try {
Method method = invocation.getMethod(); // invocation에서 method 정보를 꺼낼 수 있음
String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
status = logTrace.begin(message);
Object result = invocation.proceed(); // 실제 비즈니스 로직 동작
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
(2) ProxyFactoryConfigV1 - 빈 등록 설정
- getAdvisor(): 여러 계층에서 어드바이저를 편하게 입력받을 수 있도록 메서드를 생성
- 포인트 컷은 NameMatchMethodPointcut에는 심플 매칭 기능이 있어서 *을 매칭할 수 있는데, no-log메서드는 어드바이스가 동작하지 않도록 request*, order*, save*로 setMappedNames()를 설정하면 request, order, save로 시작하는 메서드에는 포인트컷이 true를 반환함
- 어드바이저에는 앞서 생성한 포인트컷과 LogTraceAdvice를 가지고 있으며 반환값은 new DefaultPointcutAdvisor()로 어드바이저를 생성하여 반환
- 프록시 팩토리에 각각의 target과 advisor를 등록하여 프록시를 생성하여 스프링빈으로 등록함
package hello.proxy.config.v3_proxyfactory;
@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
ProxyFactory factory = new ProxyFactory(orderController);
factory.addAdvisor(getAdvisor(logTrace));
OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
ProxyFactory factory = new ProxyFactory(orderService);
factory.addAdvisor(getAdvisor(logTrace));
OrderServiceV1 proxy = (OrderServiceV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
ProxyFactory factory = new ProxyFactory(orderRepository);
factory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV1 proxy = (OrderRepositoryV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
return proxy;
}
// 여러곳에서 advisor를 입력해야해서 메서드로 추출
private Advisor getAdvisor(LogTrace logTrace) {
// pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*"); // 가변인자로 적용할 포인트컷 이름을 입력할 수 있음
// advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice); // advisor 반환
}
}
(3) ProxyApplication 수정 및 실행
- @Import로 ProxyFactoryConfigV1을 등록하면 요구사항대로 Logtrace가 정상 동작하며, 로그를 확인해보면 JDK 동적 프록시가 생성되어 동작하는 것을 확인할 수 있음
@Import(ProxyFactoryConfigV1.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
// 기존 코드 생략
}
2) 구체 클래스만 있는 v2에 적용
- 인터페이스가 없고 구체 클래스만 있는 v2 애플리케이션에 LogTrace기능을 프록시 팩토리를 통해서 적용
- 어드바이스는 전에 만든 LogTraceAdvice를 사용
(1) ProxyFactoryConfigV2
- ProxyFactory가 알아서 동적 프록시를 생성해주기 때문에 코드의 로직은 전부 동일함
- 각 계층의 target만 V2의 Controller, Service, Repository로만 변경해주면 됨
package hello.proxy.config.v3_proxyfactory;
@Slf4j
@Configuration
public class ProxyFactoryConfigV2 {
@Bean
public OrderControllerV2 OrderControllerV2(LogTrace logTrace) {
// ProxyFacotry를 생성하는 로직은 동일, OrderConrollerV2로만 변경
}
@Bean
public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
// ProxyFacotry를 생성하는 로직은 동일, OrderServiceV2 변경
}
@Bean
public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
// ProxyFacotry를 생성하는 로직은 동일, OrderRepositoryV2 변경
}
// getAdvisor()메서드 로직 동일
}
(2) ProxyApplication 수정 및 실행
- @Import로 ProxyFactoryConfigV2를 입력 후 애플리케이션을 실행해보면 정상적으로 모두 동작하고 CGLIB로 프록시가 동작하는 로그를 확인할 수 있음
@Import(ProxyFactoryConfigV2.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
// 기존 코드 생략
}
3) 남은 문제
- 프록시 팩토리와 어드바이저 같은 개념 덕분에 지금까지 고민했던 문제들을 해결되었음
- 프록시도 깔끔하게 적용하고 포인트컷으로 부가 기능을 적용할지도 명확하게 정의할 수 있었으며 원본 코드를 전혀 손대지 않고 프록시를 통해 부가 기능도 적용할 수 있었음
(1) 문제1 - 너무 많은 설정
- 그러나 설정 파일이 지나치게 너무 많으며 설정파일의 로직도 보면 중복 코드가 상당히 많음
- 애플리케이션에 스프링 빈이 100개가 있으면 여기에 프록시를 통해 부가 기능을 적용하려고 할 때 100개의 동적 프록시 생성 코드를 만들어야 하기에 설정 지옥을 경험하게 될 수 있음
- 최근에는 스프링 빈을 등록하지 않고 컴포넌트 스캔을 사용하는 경우가 많은데 직접 스프링빈을 등록하는것에 더해 프록시를 적용하는 코드까지 빈 생성 코드에 넣어야하는 번거로움이 있음
(2) 문제2 - 컴포넌트 스캔
- 애플리케이션 V3처럼 컴포넌트 스캔을 사용하는 경우 지금까지 학습한 방법으로는 프록시 적용이 불가능함
- 지금까지 학습한 방법으로 프록시를 적용하려면 실제 객체가 아니라 부가 기능이 있는 프록시를 실제 객체 대신 스프링 컨테이너에 빈으로 등록해야 함
- 그러나 컴포넌트 스캔을 사용하면 실제 객체를 컴포넌트 스캔으로 스프링 컨테이너에 스프링 빈으로 전부 등록해 버린 상태이기 때문에 적용이 불가능함
** 이 두가지 문제를 한번에 해결하는 방법이 바로 다음에 설명할 빈 후처리기임