이전 글에서는 스프링에서 빈을 만드는 방법과 그 원리에 대해서 알아보았다. 이번 글에서는 언제 스프링이 일반 빈을 스프링으로 바꾸어 주는지 알아보자.
스프링에서 빈이 프록시로 변환되는 과정
도대체 언제 스프링은 순수하게 만들어진 Bean을 프록시로 바꿔쳐서 부가기능을 추가해주고 Advisor들을 넣어주는 걸까?
참고로 스프링에서는 프록시를 적용할 필요가 없다면 프록시가 아닌 일반 자바 객체를 빈으로 등록한다.
@Service
class ExampleService(
) {
fun saveInternal() {
}
}
@SpringBootTest
class ProxyTest {
@Autowired
lateinit var exampleService: ExampleService
@Test
fun test() {
println(exampleService)
}
}
// 결과
// jungwoo.experiment.aspect.example.ExampleService@678b3746
우선 스프링 공식 문서에서는 스프링 aop를 쓰고자 한다면 EnableAspectJAutoProxy를 켜라고 한다.
스프링 부트에서는 spring-configuration-metadata.json 설정으로 인해 자동으로 @EnableAspectJAutoProxy가 켜진다. 즉, 따로 설정해줄 필요가 없다.
설정 클래스를 Import해주고 있는데 들어가보자.
해당 클래스에서 AOP 기능을 위해 AspectJAnnotationAutoProxyCreator가 빈으로 등록된다.
다이어그램에서 보면 이 클래스는 BeanPostProcessor 를 구현하고 있는 것을 볼 수 있다.
BeanPostProcessor 란?
GPT에 의하면 BeanPostProcessor는 다음과 같다고 한다.
스프링 빈 후처리기(Spring Bean Post Processor)는 스프링 프레임워크에서 빈(Bean)의 초기화 단계 전후에 특정 작업을 수행할 수 있도록 해주는 인터페이스입니다. 스프링 컨테이너가 빈을 생성하고 초기화한 후, 추가적인 처리를 위해 빈 후처리기를 사용할 수 있습니다. 이 인터페이스를 통해 빈 생성 과정에 개입하여 빈의 속성을 변경하거나 추가적인 로직을 적용할 수 있습니다.
이 빈 후처리기는 Autowired Annotation을 보고 빈 주입을 해준다던지 스프링 빈 기능을 확장하기 위해 여러 곳에 쓰인다. 다 설명하기에는 너무 많을 것 같아 다른 글로 한번 자세히 정리해보겠다. 지금은 우선 그저 빈의 초기화 단계 전후에 특정 작업을 수행할 수 있도록 해준다고만 이해해두자.
BeanPostProcessor 인터페이스는 다음과 같다.
사용예시는 다음과 같다.
class BeanPostProcessorTest {
@Test
fun basic() {
// Config 클래스를 빈으로 등록
val context = AnnotationConfigApplicationContext(Config::class.java)
// 빈을 가져와서 메소드 호출
// beaA는 A 타입이지만 MyBeanPostProcessor에서 B로 변환되어서 B의 메소드가 호출됨
val a = context.getBean("beanA", B::class.java)
a.helloB()
}
@Configuration
class Config {
@Bean(name = ["beanA"])
fun a(): A {
return A()
}
@Bean
fun myBeanPostProcessor(): MyBeanPostProcessor {
return MyBeanPostProcessor()
}
}
class A {
fun helloA() {
println("helloA")
}
}
class B {
fun helloB() {
println("helloB")
}
}
class MyBeanPostProcessor : BeanPostProcessor {
override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? {
println("beanName: $beanName, bean: $bean")
if (bean is A) {
return B()
}
return bean
}
}
}
위 예시에서 볼 수 있듯이 빈후처리기는 A라는 객체를 B 객체로 바꿀 수 있을만큼 강력하다.
아마 이제 AspectJAnnotationAutoProxyCreator가 빈 후처리 과정을 통해 일반 빈 객체를 프록시로 대체하는 것이라고 감이 올 것 같다.
Bean이 초기화 되는 과정과 빈 후처리기 더 자세히 >>
Bean이 초기화 되는 과정을 순서대로 보자.
- 생성자를 불러 객체를 만든다.
- Setter를 부른다.
- 위 두 과정에서 BeanDefinition을 이용하여 빈 주입이 일어난다.
- BeanPostProcessor의 postProcessBeforeInitialization 메서드가 불려 빈에 특정 작업을 수행한다.
- @PostConstruct 메서드가 불린다.
- InitilizingBean 인터페이스의 afterPropertiesSet 메서드가 불린다.
- xml 의 init 메서드가 불린다.
- BeanPostProcessor의 postProcessAfterInitialization 메서드가 불려 빈에 특정 작업을 수행한다.
- 빈이 스프링 컨텍스트에 저장된다.
AspectJAnnotationAutoProxyCreator
AspectJAnnotationAutoProxyCreator의 역할
- 스프링 AOP가 적용되어야할 빈들을 프록시로 변환되게 만들어준다.
- 즉, Advisor의 PointCut들을 보면서 빈으로 빈을 객체로 만든 후 PointCut 조건에 걸리면 aop가 적용될 확률이 있는 빈이므로 프록시로 바꿔쳐버린다.
- 안걸리면 프록시로 변환할 이유가 없으므로 바꾸지 않는다.
- @Aspect를 어드바이저로 변환해서 저장한다.
즉, 위에서도 이야기 했지만 모든 빈이 프록시가 되는 것은 아니다.
프록시 변환은 어디서 해주고 있을까??
결국 빈 후처리 메서드가 처리해주고 있을 거니까 상속 구조를 따라가보자.
AspectJAnnotationAutoProxyCreator가 상속하고 있는 AbstractAutoProxyCreator에서 빈 후처리기 인터페이스의 메서드인 postProcessAfterInitialization가 보인다.
내부로 들어가면 그냥 자기 자체를 다시 return하거나 advice를 가지고 있으면 자기 자신 대신 proxy를 return하는 코드가 보인다.
getAdvicesAndAdvisorsForBean 메서드를 잘 봐보자.
위 메서드를 통해 빈 메서드 실행 시에 실행될 수 있는 Advisor가 있는지 찾고 없다면 자기 자신을 그냥 return하고 있다면 빈을 프록시로 변환하여 반환한다. 프록시를 만들 때는 당연히 실행될 수 있는 Advisor들만 전달하여 프록시를 만들어준다.
createProxy 메서드에서 더 자세히 확인할 수 있다.
코드의 전부는 아니지만 createProxy 메서드를 추상화해서 확인해보면 다음과 같다.
public Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource, boolean classOnly) {
// ProxyFactory를 사용
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
// proxyTargetClass 설정 여부에 따라 인터페이스 기반 jdk 동적 프록시를 사용할지, CGLIB을 사용할지 결정
if (proxyFactory.isProxyTargetClass()) {
// Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios)
if (Proxy.isProxyClass(beanClass) || ClassUtils.isLambdaClass(beanClass)) {
// Must allow for introductions; can't just set interfaces to the proxy's interfaces only.
for (Class<?> ifc : beanClass.getInterfaces()) {
proxyFactory.addInterface(ifc);
}
}
}
else {
// No proxyTargetClass flag enforced, let's apply our default checks...
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
// Bean에 적용될 수 있는 Advice들을 Bean에 등록
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
// 생략
return proxyFactory.getProxy(classLoader));
}
Q. 일반 빈들 보다 어드바이저와 BeanPostProcessor는 먼저 등록되어있어야하지 않을까?
BeanPostProcessor들은 실제로 그냥 먼저 등록된다.
어드바이저는 언제 빈으로 등록되는지 궁금해서 디버깅 해보니까 getAdvicesAndAdvisorsForBean가 불리는 시점에 모두 빈으로 등록하기 때문에 필요한 시점에 모두 빈으로 등록되는 것으로 확인했다. 즉, 일반 빈들에 AbstractAutoProxyCreator 빈후처리기가 동작하는 시점에 빈으로 모두 등록되어 일반 빈들에 해당 어드바이저들이 적용될 수 있다.
@EnableAspectJAutoProxy proxyTargetClass와 CGLIB 과 Jdk 동적 프록시 비교
위에 스프링 aop를 쓰고자 한다면 EnableAspectJAutoProxy를 켜야한다고 했다. 여기 기본 설정에 ProxyTargetClass = false로 되어있는 것을 확인할 수 있는데, 앞에 글에도 언급했지만 ProxyTargetClass에 따라 프록시 구현 방식이 달라진다.
- true이면 무조건 CGLIB 기반
- false면 인터페이스가 있으면 jdk dynamic proxy, 없으면 CGLIB
그렇다면 CGLIB과 Jdk 동적프록시의 각각 장단점은 무엇일까?
Jdk동적 프록시의 단점
결국 인터페이스 기반프록시이므로 프록시를 원래의 Target 클래스로 타입 캐스팅할 수 없는 문제가 있다.
이 때문에 인터페이스 타입이 아니라 Target 클래스의 타입으로 의존성 주입을 하고 싶을 때 프록시 타입으로 의존성 주입이 되지 못하는 이슈가 발생할 수 있다.
당연히 일반적인 상황에선 인터페이스 주입하는 것이 자연스럽겠지만 테스트 등 어쩔 수 없는 상황이 있을 때 예상하지 못한 이슈가 발생할 수도 있다.
CGLIB의 단점
기본 생성자가 필수인 문제
상속을 하기 위해서 자식 클래스는 부모 클래스의 생성자를 무조건 호출해야한다. CGLIB에서는 이때 기본생성자를 호출하도록 한다. 즉, 프록시를 만드는 모든 객체에서는 기본 생성자를 필수로 해야한다는 한계가 있다.
생성자 두번 호출 문제
기본 target클래스를 만들 때 당연하게도 생성자가 호출되고 CGLIB을 통해서 프록시를 만들때 상속하는 과정에서 부모 클래스의 생성자를 호출하여 생성자를 두번 호출하게 된다.
생성자에 로깅을 해놓게 되면 로깅이 두번 남거나 하는 그런 문제들이 생길수도 있다.
final 클래스나 메서드는 쓰지 못한다.
→ 사실 거의 안써서 문제되진 않는다.
objenesis를 통해서 생성자 없이도 객체를 생성할 수 있게 하여 기본 생성자 문제와 생성자 두번 호출 문제를 해결한다.
마무리
여기까지 스프링에서 AOP를 만드는 원리와 그 방법에 대해 보다 심도있게 알아보았다!
이 글들을 통해서 조금이나마 도움이 되었으면 좋겠다!!
'서버 > 스프링' 카테고리의 다른 글
@SpringBootTest의 webEnvironment를 통한 통합테스트 환경 구축 및 비교하기 (0) | 2024.11.03 |
---|---|
ThreadLocal과 MDC를 통해 알아보는 스프링에서의 로깅 원리 (0) | 2024.11.03 |
[스프링 AOP] 2. 스프링 AOP 프록시 생성 원리 (0) | 2024.10.02 |
[스프링 AOP] 1. 스프링 AOP와 Proxy 패턴 (6) | 2024.09.28 |
[JPA] 기본 키 매핑 전략 정리 (0) | 2022.10.30 |