이전에 작성했던 Spring AOP 블로그 글 옮긴 내용
1. AOP ??
Spring AOP는 스프링에서 지원하는 AOP(Aspect Oriented Programming)을 말한다. Aspect는 부가 기능을 의미한다. 부가 기능은 비즈니스 로직를 담당하지는 않지만 애플리케이션의 유지보수와 안정성을 위해서는 필요한 기능이다. 대표적인 예시로 로깅과 트랜잭션이다. 만약 AOP를 사용하지 않으면 비즈니스 로직 사이에 부가 기능에 관련된 코드를 추가해야 한다. 비즈니스 로직 사이에 추가된 부가 기능 코드들을 비즈니스 로직의 가독성을 떨어뜨리고 같은 부가 기능에 대한 코드 중복이 발생해 유지보수가 어렵다. AOP을 통해 비즈니스 로직과 부가 기능을 분리하고 코드를 한 곳에서 관리해 로직의 응집도와 유지보수성을 높힐 수 있다.
2. Spring AOP 설정 방법
스프링 부트에서 스프링 AOP를 사용하기 위해서는 의존성 하나만 추가하면 Spring AOP를 사용할 수 있다. 아래 의존성을 추가하면 aspectj.weaver와 aspectJ 관련 라이브러리를 등록해 asspectj에서 제공하는 어노테이션을 사용할 수 있다.(e.g. @Pointcut, @AfterThrowing, @Around...)
implementation 'org.springframework.boot:spring-boot-starter-aop'
스프링 부트에서 AOP 관련 클래스를 자동으로 빈으로 등록하고 실행할 설정 클래스들의 이름을 담고 있어 관련 설정 클래스를 실행시킨다.(at org.springframework.boot.autoconfigure.AutoConfiguration.imports) 이 중 스프링 AOP 설정을 담당하는AopAutoConfiguration 클래스를 호출 및 실행시켜 AOP 관련 설정을 자동으로 지원한다.
//META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
@AutoConfiguration
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Advice.class)
static class AspectJAutoProxyingConfiguration {
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
static class JdkDynamicAutoProxyConfiguration {
}
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class CglibAutoProxyConfiguration {
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class ClassProxyingConfiguration {
@Bean
static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
return (beanFactory) -> {
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
};
}
}
}
3. Spring AOP 가 GCLib proxy 를 선택한 이유
스프링 AOP는 부가 기능을 제공하는 방법으로 프록시를 사용한다. 스프링에서 지원하는 프록시를 생성하는 방식 아래와 같다.
- JDK Dynamic Proxy : Java Reflection 에서 제공하는 Proxy 를 생성하는 방식 (인터페이스 기반으로 프록시 생성)
- CGLlib(Code Generation Library) : 클래스의 바이트코드를 조작하여 Proxy 를 생성하는 라이브러리 (구체 클래스 기반으로 프록시 생성)
[1] CGLib proxy 을 사용하기 위한 개선 과정
Spring 4.3(SpringBoot 1.4) 버전 이전에는 JDK Dynamic Proxy 를 기본 프록시로 사용했지만 이후에는 CGLib을 사용하고 있다. 이전 버전까지는 CGLib 를 사용하는데 복잡함이 존재했지만 다음과 같이 해결되었다.
- 외부 의존성(net.sf.cglib.proxy.Enhancer.proxy)를 추가해야 했다. 하지만 spring version 3.2 부터 CGLib을 Spring Core 패키지에 포함시켜 더이상 의존성을 추가하지 않아도 된다.
- default constructor 를 선언하고 생성자를 두번 호출해야 하는 문제가 있었지만, spring version 4 부터 Objensis 라이브러리의 도움을 받아 이 문제 또한 개선되었다.
[2] CGLib proxy 을 사용한 이유 : 성능
CGLib 을 사용하게 된 가장 주된 이유는 성능이다. CGLib은 타깃 클래스에 대한 바이트 코드를 조작하고 이후에 호출시에 조작된 바이트 코드를 재사용하기 때문에 relection을 사용하는 JDK Dynamic Proxy에 비해 성능이 빠르다. (AOP Benchmark) AopAutoConfiguration을 확인해보면 Default로 Cglib 프록시 생성 방식을 사용하고 있는 것을 확인할 수 있다.
@AutoConfiguration
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Advice.class)
static class AspectJAutoProxyingConfiguration {
...
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class CglibAutoProxyConfiguration {
}
}
...
}
4. ProxyFactory
ProxyFactory는 동적 프록시를 통합하여 편리하게 생성하는 역할을 한다. ProxyFactory 를 사용하면 동적 프록시 기술을 신경쓰지 않고 Proxy 를 사용할 수 있는 장점이 있다. 각각의 동적 프록시 기술은 해당 Advisor 를 통해 프록시 생성을 위임하는데 각 동적 프록시는 해당 핸들러에게 프록시 생성 역할을 위임하여 동적 프록시 기술을 신경쓰지 않고 Proxy를 생성할 수 있다
- JDK Dynamic Proxy : (JDKDynamicProxy) adviceInvocationHandler
- CGLIB : (CglibAopProxy) adviceMethodInterceptor
package org.springframework.aop.framework;
public class ProxyFactory extends ProxyCreatorSupport {
...
public ProxyFactory(Class<?> proxyInterface, Interceptor interceptor) {
addInterface(proxyInterface);
addAdvice(interceptor);
}
}
@Configuration
public class ProxyFactoryConfig {
@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;
}
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);
}
}
5. BeanPostProcessor
BeanPostProcessor는 빈을 생성하는 과정에서 ProxyFactory에서 생성한 프록시로 등록하는 작업을 진행한다. 자동으로 빈을 프록시 객체로 대체해주기 때문에 프록시를 빈으로 등록해야하는 번거로움을 줄일 수 있다.
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
6. Weaving
위빙(weaving)은 비즈니스 로직에 부가 기능을 추가하는 것을 의미한다. 위빙 방법에는 컴파일 시점, 클래스 로딩 시점, 런타임 시점(프록시)이 있다. 현재 스프링 AOP는 런타임 위빙을 선택하고 있다. 런타임 위빙을 선택한 이유는 설정이 편리하기 때문이다. 컴파일 시점과 클래스 로딩 시점 위빙을 사용하기 위해서는 AspectJ 컴파일러, AspectJ 클래스 로더 조작기(바이트 코드 조작)이 필요하다.
'spring > summary' 카테고리의 다른 글
[Spring Batch] Job 실행 프로퍼티, JobParameter, Scope (0) | 2024.08.26 |
---|---|
Spring Batch Architecture (0) | 2024.05.10 |
hibernate.query.in_clause_parameter_padding = true ? (1) | 2024.03.06 |
JPA 에 UUID version 7 적용하기 (0) | 2024.03.04 |
[JPA] 코드로 보는 OSIV 와 고찰 (2) | 2024.01.30 |