목차
AOP (Aspect Oriented Programming)
관점 지향 프로그래밍은 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화 하겠다는 의미이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 만한다.
예로들어 핵심적인 관점(core concerns)은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 로깅, 실행시간 측정, 시큐리티, 트랜잭션 등이 될 수 있다.
A Concern is a term that refers to a part of the system divided on the basis of the functionality
관심사는 기능에 따라 구분되는 시스템의 일부를 지칭하는 용어
Crosscutting Concerns (흩어진 관심사)
- 핵심로직과 함께 반복해서 쓰여지며 부가 기능을 수행하는 코드/로직을 뜻함
- 반대로 core concerns 은 비즈니스 로직, 핵심 관심사 뜻함
- 로깅, 실행 시간 측정, 시큐리티, 트랜잭션 등이 예이다
흩어진 관심사를 Aspect 모듈화하여 비즈니스(핵심) 로직에서 분리하고 재사용을 하겠다는게 AOP의 취지이다 (OOP의 보완역할)
AOP 사용할 경우(장점) | AOP 사용하지 않을 경우 (단점) |
- 중복 코드 제거 - 효율적인 유지보수 - 재활용성 극대화 - 높은 생산성 |
- 중복 코드를 만들어서 유지보수가 어려워짐 - 핵심로직과 부가기능이 섞여 있어 가독성이 떨어지게 됨 |
AOP 용어 정리
용어 순서에 조금 의미를 두고 표를 작성해보았다
Adivce 선언 및 어노테이션 실행 순서
- Advice는 @Aspect 모듈 내에서 부가 기능을 정의해둔 메서드를 나타냄
- Advice는 하나 이상의 Pointcut 가짐
- 실행 순서를 나타내는 어노테이션과 함께 쓰임
@Component
@Aspect
public class CustomValidationAdvice {
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postMapping() {}
@Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)")
public void putMapping() {}
// 여기가 advice
@Around("postMapping() || putMapping()")
public Object validationAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 로깅, 시간 측정 등 부가 로직 구현
}
}
- @Around : 해당 메서드 호출 전/후 실행, ProceedingJoinPoint 메서드 파라미터로 받음
- @Before : 해당 메서드 호출 전 실행
- @AfterThrowing : 해당 메서드가 예외를 던질 경우
- @AfterReturning : 해당 메서드가 성공적으로 종료시
- @After : 해당 메서드 호출 후 실행
@Around의 경우 메서드 파라미터로 ProceedingJoinPoint를 사용하고 proceed()를 호출한다.
나머지 어노테이션은 JoinPoint를 파라미터로 사용하고 proceed를 호출하지 않는다.
스프링 AOP 특징
- Spring Bean에만 AOP 적용 가능 (@Bean, @Component 활용)
- Spring AOP 는 런타임 프록시 패턴 사용
- Spring AOP의 JoinPoint는 메서드 지점으로 제한됨 (반면 AspectJ는 더 넓은 지점에 적용 가능하다함)
- Spring boot 2.0 버전 부터 GCLIB이 기본이고, 설정 수정하여 JDK Dynamic Proxy를 사용가능 (참고)
// defualt , CGLIB 동작
spring.aop.proxy-target-class=true
// JDK Dynamic Proxy 동작
spring.aop.proxy-target-class=false
참고. AOP Proxies (공식)
https://docs.spring.io/spring-framework/reference/core/aop/introduction-proxies.html
CGLIB vs JDK Dynamic Proxy
Spring boot 2.0 버전 부터 GCLIB이 기본이고, 선택하여 JDK Dynamic Proxy 사용가능하다.
CGLIB | JDK Dynamic Proxy |
- 클래스 기반 - 클래스의 바이트 코드를 조작하여 프록시 객체 생성 - 리플렉션 API 사용하지 않으므로 상대적으로 성능이 좋다 - MethodInterceptor 구현 |
- 인터페이스 기반 - 구현체는 사용할 수 없다(ClassCastException) - 리플렉션 API 활용하여 성능이 떨어진다 - InvocationHandler 구현 |
동적 프록시 직접 생성 해보기
참고로 해당 내용의 경우 Framework를 만들 사람이 아니면 굳이 해보지 않아도 된다고 적혀 있었다 (:
개인적인 생각이지만 예시들을 직접 타이핑 해봤을 때
부가 기능(advice) 정의가 reflection API 의 InvocationHandler 구현하거나, spring aop 의 MethodInterceptor 구현하는 행위로 보이고, 프록시 대상 지정(point cut, join point)이 프록시 생성자에 인터페이스나 구현체를 인자로 전달하는 행위로 보인다
(1) java.lang.reflection API 사용
- Baeldung의 예제 사용 (CGLIB 예제는 개별 링크 참조)
- 동적 프록시를 사용함으로써 런타임에 대상 클래스에 대한 프록시 객체를 일괄 생성 가능해지고, 부가 기능 로직을 InvocationHandler 또는 advice 한 곳에 구현해 두면 되기 때문에 중복 코드가 줄어 듦
TimingDynamicInvocationHandler 클래스
InvocationHandler 인터페이스 구현 (advice 해당)
public class TimingDynamicInvocationHandler implements InvocationHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(TimingDynamicInvocationHandler.class);
private final Map<String, Method> methods = new HashMap<>();
private Object target;
public TimingDynamicInvocationHandler(Object target) {
this.target = target;
// target 클래스가 가지는 모든 메서드를 Map에 등록
for(Method method : target.getClass().getMethods()) {
this.methods.put(method.getName(), method);
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.nanoTime();
Object result = methods.get(method.getName()).invoke(target, args);
long end = System.nanoTime();
LOGGER.info("Executing {} finished in {} ns", method.getName(), end - start);
return result;
}
}
reflection API를 사용해서 아래와 같이 Proxy 객체를 생성 시 Map인터페이스를 넘기고 구현 클래스는 HashMap 넘김
@Test
void dynamicProxy4() {
// java.lang.reflection
Map mapProxyInstance = (Map) Proxy.newProxyInstance(
ProxyFactoryTest.class.getClassLoader(), // 프록시 클래스를 만들 클래스로더
new Class[]{ Map.class }, // 프록시 클래스가 구현할 인터페이스(배열)
new TimingDynamicInvocationHandler(new HashMap<>()) // 메서드가 호출되었을 때 실행될 핸들러
);
mapProxyInstance.put("hello", "world");
}
19:12:13.824 [main] INFO proxy.handler.TimingDynamicInvocationHandler - Executing put finished in 219784 ns
InvocationHandler 인터페이스는 추상메서드가 하나 뿐이기 때문에 람다 표현식으로 처리가능한 것을 확인가능했다
@Test
void dynamicProxyByLambdaExpression() {
// java.lang.reflection
Map proxyInstance = (Map) Proxy.newProxyInstance(
ProxyFactoryTest.class.getClassLoader(), // 프록시 클래스를 만들 클래스로더
new Class[]{ Map.class }, // 프록시 클래스가 구현할 인터페이스 배열
(proxy, method, methodArgs) -> {
if("get".equals(method.getName())) {
return 42;
} else {
throw new UnsupportedOperationException("Unsupported method : " + method.getName());
}
}
);
assertThat(proxyInstance.get("hello")).isEqualTo(42); // 임의 key 호출시 42 리턴
assertThatThrownBy(() -> proxyInstance.put("hello", "world")) // get method 아닌 경우 예외 출력
.isInstanceOf(UnsupportedOperationException.class);
}
(2) ProxyFactory 사용
- MethodInterceptor 인터페이스를 구현하여 advice를 작성한다
- cglib의 MethodInterceptor와 패키지 다르므로 import 주의 (org.springframework.cglib.proxy.MethodInterceptor)
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TimeAdvice implements MethodInterceptor {
Logger log = LoggerFactory.getLogger(TimeAdvice.class);
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeAdvice 실행");
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
log.info("total : {}", endTime - startTime);
return result;
}
}
@DisplayName("interface 기반으로 둔 JDK Dynamic Proxy 생성하여 확인한다")
@Test
void proxyByInterfaceBased() {
ServiceInterface target = new ServiceImpl();
// 프록시 팩토리 생성시, 프록시의 호출 대상을 인자로 넘긴다.
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice()); // Adivce 추가 (*하나의 프록시에는 여러 advice 추가 가능)
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy(); // advice 적용된 proxy 객체 가져오기
//실행
proxy.save();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue(); // ok
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
System.out.println(proxy.getClass()); // class com.sun.proxy.$Proxy9
}
setProxyTargetClass(true) 설정하게 되면 CGLIB 사용하여 Proxy 생성하는 것을 확인가능했다
@DisplayName("setProxyTargetClass를 true 설정하면 CGLIB으로 proxy를 생성한다")
@Test
void proxyByClassBased() {
//given
ServiceImpl target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true); // 구체 클래스 사용하기 = CGLIB 사용
proxyFactory.addAdvice(new TimeAdvice());
//when
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
//then
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
System.out.println(proxy.getClass());
}
JDK Dynamic Proxy 인데, 구현체를 넣을 경우 프록시 생성시 ClassCastException이 발생한다
@Test
void proxyByInterfaceBased() {
ServiceImpl target = new ServiceImpl();
// 프록시 팩토리 생성시, 프록시의 호출 대상을 인자로 넘긴다.
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());// 정의한 Advice 주입
ServiceImpl proxy = (ServiceImpl) proxyFactory.getProxy(); // class case exception 발생
//실행
proxy.save();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
System.out.println(proxy.getClass());
}
Pointcut 종류 (추후 작성 예정)
https://docs.spring.io/spring-framework/docs/2.0.x/reference/aop.html
참고.
JDK Dynamic Proxy와 CGLIB의 차이점 무엇일까?
https://stackoverflow.com/questions/23700540/cross-cutting-concern-example
느리더라도 꾸준하게 - [Sping] AOP와 JDK Dynamic Proxy, CGLIB
https://engkimbs.tistory.com/746
'공부 > Spring' 카테고리의 다른 글
[Spring Boot] Simple Cache, EhCache(v3.10.8) 간단 테스트 해보기 (0) | 2024.07.22 |
---|---|
Spring boot 3.x + Security 6.x + @WebMvcTest 회고 (0) | 2024.06.26 |
[JPA] Date 타입 포맷 맞춰주는 @Temporal (0) | 2022.06.20 |
[JUnit] org.junit.runners.model.InvalidTestClassError: Invalid test class (0) | 2022.06.19 |
[ERROR] org.springframework.oxm.UncategorizedMappingException: Unknown JAXB exception (0) | 2022.06.12 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!