개요
저번 게시글에서 JDK 동적 프록시와 CGLIB(https://jaimemin.tistory.com/2025)에 대해서 정리했는데 요약을 하자면 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB가 적용되는 것을 확인할 수 있었습니다.
보통 프로젝트를 개발할 때 인터페이스와 클래스를 혼용해서 사용하기 때문에 두 기술을 함께 사용하는데, 그렇다면 "부가기능을 제공하기 위해 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 중복으로 생성해서 관리해야 할까?"라는 의구심이 생겼습니다.
다행히도 스프링에서는 위와 같이 복잡한 방식 말고 동적 프록시를 통합해서 편리하게 생성해주는 ProxyFactory 기능을 제공하고 있으며 이번 게시글에서는 ProxyFactory에 대해 간단하게 정리해보겠습니다.
1. ProxyFactory
프록시 팩토리는 인터페이스가 존재할 경우 JDK 동적 프록시를 사용하고, 구체 클래스만 존재할 경우 CGLIB를 사용하도록 해주는 기능입니다. (물론, 설정을 통해 인터페이스에 대해서도 CGLIB를 적용하게 해줄 수 있습니다.)
프록시 팩토리의 사용흐름은 아래와 같습니다.
- client에서 ProxyFactory로 proxy 요청
- ProxyFactory에서 proxy 기술 선택 (JDK 동적 프록시 혹은 CGLIB)
- proxy 생성
- proxy 반환
* SpringBoot에서는 AOP 적용 시 기본적으로 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시 생성
2. Advice
Advice는 ProxyFactory에서 JDK 동적 프록시와 CGLIB를 함께 사용하기 위해 JDK 동적 프록시를 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 생성하는 것을 방지하기 위해 도입된 개념입니다.
즉, 정리하자면 개발자 입장에서는 InvocationHandler와 MethodInterceptor를 생성할 필요 없이 Advice만 만들면 두 기술을 모두 적용시킬 수 있습니다,
ProxyFactory를 사용하면 Advice를 호출하는 전용 InvocationHandler와 MethodInterceptor를 내부에서 사용합니다.
2.1 ProxyFactory 예제
기본적인 예제 코드는 개요에서 언급한 게시글에 있는 에제와 유사한 형태입니다.
다만, 이번에는 InvocationHandler와 MethodIntercetor를 각각 생성하는 대신 둘을 개념적으로 추상화한 Advice를 통해 프록시를 적용해보겠습니다.
Advice는 기본적으로 org.aopalliance.intercept 패키지 내 MethodInterceptor를 구현하여 생성하면 됩니다.
MethodInterceptor.java
* MethodInvocation invocation: 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, 매개변수, 메서드 정보 등이 포함되어 있음 (기존에 파라미터로 제공한 부분들이 모두 하나의 객체 내 포함되어 있음)
ProxyFactory 예제
* 기존처럼 method.invoke(target, args)와 같이 target을 injection 받고 메서드 호출을 위한 매개변수 전달을 할 필요 없이 invocation.proceed() 메서드만 호출하면 되므로 간편해짐
* target 클래스의 정보가 MethodInvocation invocation 내 포함되어 있기 때문 (ProxyFactory로 proxy를 생성하는 단계에서 이미 target 정보를 매개변수로 받음)
* 인터페이스가 있더라도 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성하기 위해서는 proxyTargetClass=true로 설정해줘야 함
3. Pointcut, Advice, Advisor
앞서 2.1에서 살펴본 MethodInterceptor.java 패키지를 보면 알 수 있다시피 ProxyFactory에는 AOP 개념이 적용됩니다.
따라서, AOP 개념에 대해서 간단히라도 알아볼 필요성이 있습니다.
- Pointcut: 부가기능을 적용할지 말지 판단하는 필터 역할
- 주로 클래스명과 메서드명을 통해 필터링 진행
- Advice: 프록시가 호출하는 부가 기능 (프록시 로직)
- Advisor: 하나의 Pointcut과 하나의 Advice를 가진 형태 (Pointcut + Advice)
위의 개념들은 역할과 책임을 명확하게 분리하기 필요하기 위해 존재합니다.
Pointcut은 대상 여부를 확인하는 필터 역할만 담당하고 Advice는 부가 기능 로직만 담당합니다.
이 둘을 합친 것이 Advisor이고 client에서 proxy를 호출할 때 Advisor를 통해 Advice 적용 여부를 확인(Pointcut)하고 적용할 대상이라면 부가기능을 적용(Advice)해줍니다.
3.1 Advisor 예제
* 직접 구현한 포인트 컷에서는 클래스명이 아닌 메서드명으로만 필터링을 진행
* 메서드명이 "function"일 경우에 MyMethodMatcher 클래스 내 matches() 메서드가 true를 반환해 advice 적용
* isRuntime() 값이 참이면 matches(... args) 메서드가 대신 호출
* isRuntime() 값이 거짓일 경우 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능 (하지만, isRuntime()이 참일 경우 매개변수가 다이내믹하게 변경된다고 가정하므로 캐싱 X)
전체적인 흐름
- function() 호출
- client가 proxy의 function() 호출
- pointcut에서 ExampleInterface 클래스의 function() 메서드에 advice 적용할지 판단
- pointcut이 true를 반환하므로 TimeCalculateAdvice 호출해 부가 기능 부여
- 실제 객체의 function() 호출
- noLog() 호출
- client가 proxy의 noLog() 호출
- pointcut에서 ExampleInterface 클래스의 noLog() 메서드에 advice 적용할지 판단
- pointcut이 false를 반환하므로 부가 기능 부여 X
- 실제 객체의 noLog() 호출
3.2 스프링 프레임워크에서 제공하는 Pointcut
스프링에서는 앞선 예제에서 사용한 NameMatchMethodPointcut과 함께 다양한 pointcut을 제공합니다.
- NameMatchMethodPointcut: 메서드명 기반으로 포인트 컷 매칭
- JdkRegexpMethodPointcut: JDK 정규 표현식 기반으로 포인트컷 매칭
- TruePointcut: 항상 true 반환
- AnnotationMatchingPointcut: 어노테이션 기반으로 포인트컷 매칭
- AspectJExpressionPointcut: aspectJ 표현식으로 포인트컷 매칭
* 실무에서는 aspectJ표현식을 기반으로 사용하는 AspectJExpressionPointcut을 주로 사용
3.3 하나의 프록시에서 여러 Advisor 적용하는 방법
앞서 Advisor는 하나의 Pointcut과 하나의 Advisor로 구성되어 있다고 언급했습니다.
따라서 하나의 프록시에 여러 부가 기능을 추가하기 위해서는 Advisor를 여러 개 등록해줘야 합니다.
여러 Advisor를 적용하는 방법은 크게 두 가지가 있습니다.
1. ProxyFactory를 두 개 생성하여 각각 advisor를 적용하고 모두 target에 부여
2. ProxyFactory 하나를 생성하여 addAdvisor 메서드를 통해 멀티 advisor 등록
1번과 2번 모두 가능한 방법이지만 클린 코드 관점에서 보면 2번이 훨씬 좋은 코드입니다.
간단한 예시 코드는 아래와 같으며 편의상 pointcut은 무조건 true를 반환하도록 했습니다.
참고
인프런 스프링 핵심 원리 - 고급편 (김영한 강사님)
'Spring' 카테고리의 다른 글
[SpringBoot] @Aspect 어노테이션 (0) | 2021.12.23 |
---|---|
[SpringBoot] 빈 후처리기 (BeanPostProcessor) 정리 (0) | 2021.12.21 |
[SpringBoot] ThreadLocal 간단 정리 (0) | 2021.11.13 |
[SpringBoot] 파일 업로드 및 다운로드 (2) | 2021.08.14 |
[SpringBoot] 스프링 TypeConverter 정리 (1) | 2021.08.11 |