티스토리 뷰
AbstractAutoProxyCreatorSpring 에서는 AOP 기능을 수행하기위해 객체를 빈으로 등록하기 전에 Proxy 로 감싸진 클래스를 생성하여 빈으로 관리한다.
이때 사용되는 Proxy Library 가 CGLIB 와 Dynamic Proxy 이다. 이 중 CGLIB 가 만들어지는 과정을 코드 레벨로 확인한다.
테스트 코드 작성에 사용된 spring, aspectj 버전은 아래와 같다.
implementation group: 'org.springframework', name: 'spring-context', version: '5.3.19'
implementation group: 'org.aspectj', name: 'aspectjweaver', version: '1.9.9.1'
Spring Context 로 관리되는 모든 빈은 Proxy 클래스로 만들어질까?
CGLIB 의 생성 과정을 알아보기 전에 Spring Context 로 관리되는 Bean 은 모두 Proxy 객체로 만들어지는지가 궁금해졌다.
결론부터 말하자면 Spring Bean 은 필요할 경우에만 Proxy 객체로 만들어진다.
정말 그런지 눈으로 확인해보기 위한 아주 간단한 테스트 코드를 작성해보았다
일반 클래스가 Spring Context 의 Bean 으로 관리 되는 경우
public class Main {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
MemberService memberService = (MemberService) applicationContext.getBean("memberService");
memberService.increment();
}
}
@Component
public class MemberService {
private int count = 0;
public void increment() {
count++;
}
}
@Configuration
public class MyConfig {
@Bean
public MemberService memberService() {
return new MemberService();
}
}
Spring Context 를 생성하고 MemberService 를 빈으로 등록한 후에 가져오는 아주 간단한 테스트 코드이다.
이 테스트 코드를 실행하여 Spring Context 에서 가져온 Bean 의 주소를 확인하면 아래와 같다
CGLIB 나 Dynamic Proxy 로 감싸진 객체가 아닌 일반 객체를 받아온 것을 확인할 수 있다.
Proxy 클래스가 Spring Context 의 Bean 으로 등록되는 경우
이제 MemberService 의 함수에 AOP 를 작성하여 Proxy 클래스가 필요한 환경을 만들어본다.
@Aspect
@Component
public class AopService {
@Around("execution(* member.MemberService.increment(..))")
public void log() {
System.out.println("CALLED");
}
}
@Configuration
@EnableAspectJAutoProxy
public class MyConfig {
@Bean
public MemberService memberService() {
return new MemberService();
}
@Bean
public AopService aopService() {
return new AopService();
}
}
MemberService.increment() 함수가 호출될 경우에 AopService.log() 함수가 호출되도록 AOP 를 작성해주었다.
동일하게 테스트 코드를 실행한 결과는 아래와 같이 CGLIB 프록시 클래스가 Bean 으로 등록된 것을 확인할 수 있다.
Proxy 클래스로 관리되는 경우와 아닌 경우의 차이는 Spring 의 어떤 코드에 의해서 구별되는 것일까
Proxy 클래스가 Bean 으로 관리되는 경우와 아닌 경우의 차이
이 차이는 Spring Context 가 Bean 후보들을 스캔하여 Bean 으로 생성하려고 할때 구분이 된다. 빈이 생성되는 과정에서 AbstractAutoProxyCreator.wrapIfNecessary() 함수가 호출되는데 이름에서 알 수 있듯이 필요한 경우에 Bean 으로 만들고자 하는 클래스를 Proxy 로 Wrap 하는 역할을 수행하는 함수이다.
여기서 Proxy 필요 여부를 확인하는 조건문은 아래와 같다.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
Bean 으로 등록하고자 하는 Class 에 필요한 Interceptor 가 존재하는지 확인하고 Interceptor 가 없다면 객체를 그대로 리턴하고 Interceptor 가 필요하다면 Proxy 클래스를 만들어 리턴한다.
위에 테스트했던 각각의 경우를 Break Point 를 잡고 디버그 해본 결과
AOP 를 걸지 않았던 경우
AOP 를 걸었던 경우
이러한 차이로 SpringContext 가 필요 여부에 따라서 Bean 을 Proxy 클래스로 만들지 아닐지를 결정하여 Spring Context 에 등록하게 된다. 한가지 흥미로운 부분은 AOP 와 같이 실제 빈으로 등록되는 Class 에 가해진 변경뿐만 아니라 AOP 로 다른 클래스에서 해당 클래스를 Proxy 로 만들어야 하는 경우에도 Proxy 로 만들도록 동작된다는 것이다. 즉 Spring 은 모든 Bean 의 관계를 파악하고 Proxy 로 등록해야할지를 결정하는 것으로 보인다.
Proxy 클래스가 Proxied 클래스의 변수로 주입될 수 있는 이유
Proxy 클래스와 Proxied 클래스는 엄연히 다른 클래스이다. 하지만 Spring 이 Proxy 로 Wrapping 한 클래스를 주입해주어 사용할 수 있다. 어떻게 서로 다른 클래스를 하나의 멤버 변수로 주입 받아 사용할 수 있는지 확인해본다.
이것 또한 결론부터 말하면 CGLIB 의 경우에는 Proxy 클래스가 Proxied 클래스를 상속받게 된다. 서로 부모-자식의 관계를 갖게 되니 자식 Class 가 부모 Class 로 주입되어 함수 호출의 문제 없이 사용이 가능한 것이다.
이로 인해서 한가지 더 확인할 수 있는 사실은 final 로 선언된 클래스는 부모-자식 관계를 가질 수 없으므로 CGLIB 가 Proxy 클래스를 만들 수 없다. 따라서 Spring Context 가 빈을 생성하는 과정에서 아래와 같은 예외가 발생하게 되는 것이다.
Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class member.MemberService: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Cannot subclass final class member.MemberService
CGLIB vs Dynamic Proxy 의 선택
Proxy 클래스로 타겟 클래스를 감싸기로 결정되었다면 CGLIB 또는 Dynamic Proxy 둘 중 어떤 라이브러리를 사용하여 Proxy 클래스를 생성할지 결정하게 된다.
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (!NativeDetector.inNativeImage() &&
(config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
Proxy 라이브러리는 런타임에 DefaultAopProxyFactory 에서 결정되게 된다.
- Target Class 가 interface 인 경우
- Target Class 가 Proxy Class 인 경우
- Target Class 가 Lambda Class 인 경우
위 세가지 중 하나라도 만족한다면 Dynamic Proxy 라이브러리를 사용하여 Proxy class 가 생성된다. Spring 의 Repository 가 Dynamic Proxy 로 프록시 클래스가 생성되는것은 1번 조건에 충족한 이유이다.
위에 작성한 예제 코드는 위 조건들을 하나도 만족하지 않아 CGLIB 프록시가 생성되게 된다.
CGLIB Proxy 클래스의 생성
CGLIB 의 Proxy Class 는 런타임에 생성된다. 즉, 새로운 Java Class 가 런타임에 생성되는 것이다. 이 작업은 CglibAopProxy.getProxy(ClassLoader classLoader); 함수에서 실행된다.
getProxy() 내부에서 Enhancer.java 라는 클래스를 이용하여 Proxy Class 를 생성하게 된다. Enhancer.java 의 Java doc 을 확인하면 해당 클래스가 어떤 역할을 하고 있는지 알 수 있다.
Generates dynamic subclasses to enable method interception.
함수 Interception 을 활성화 하기 위하여 동적으로 Subclass 를 생성한다.
The dynamically generated subclasses override the non-final methods of the superclass and have hooks which callback to user-defined interceptor implementations.
동적으로 생성된 Subclass 는 부모 클래스의 non-final 함수를 override 한다. 그리고 유저가 정의한 interceptor 구현을 콜백한다.
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);
Enhancer 가 만들어지는 부분을 보면 Target Class 를 SuperClass 로 지정해주고 있다. 또한, 필요한 interface, Class 이름, CallbackFilter 들을 설정해주고 있다.
이렇게 만들어진 Enhancer.java 를 이용하여 ReflectUtils.java 의 defineClass() 함수에서 실제로 Proxy Class 를 만들게 된다.
defineClass() 의 리턴값인 Class 를 Break Point 로 잡아서 확인해보면 CGLIB 에 의해서 새로운 클래스가 생성된 것을 확인할 수 있다. 더하여 Super Class 는 TargetClass 로 지정되어있다. 결국 Proxy Class 가 Target Class 의 Subclass 이므로 DI 에 의해서 Target Class 타입으로 주입될 수 있는 것이다.
더하여 Target Class 인 MemberService.java 는 increment() 라는 함수 단 하나만 가지고 있지만 Proxy Class 에는 40개 이상의 함수들이 정의되어있다. 이는 Proxy Class 생성 과정에서 implements 한 interface 들의 영향으로 보인다.
함수 리스트에 보면 MemberService.java 의 increment() 가 있는 것을 확인할 수 있다 (9번). 따라서 Proxy Class 가 Subclass 로 주입되었을때 increment() 메서드를 정상적으로 호출할 수 있는 것이다.
결론
Spring 은 CGLIB 와 Dynamic Proxy 모두 사용하고있으며 어떤걸 사용할지는 조건에 따라 다르다. CGLIB 는 Proxy Class 를 만들때 TargetClass 를 부모로 가지는 SubClass 를 만들기 때문에 DI 가 가능하다.
'Spring Framework' 카테고리의 다른 글
Spring JPA 와 Dynamic Proxy (0) | 2022.04.24 |
---|---|
Spring Framework - RedirectAttributes 설명 (0) | 2021.12.17 |
Spring RequestMapping 우선순위 (0) | 2021.02.04 |
- Total
- Today
- Yesterday
- notifyAll()
- ConcurrentHashMap
- wait()
- DyanomoDB
- Flux
- RouteDefinition
- Seperate Chaining
- RoutePredication
- MariaDB
- mariadb-connector-j
- ResultSet
- circurit breaker
- notify()
- getBoolean
- rate limit
- Lazy
- reative
- N+1
- spring cloud gateway
- referencedColumnName
- aurora
- router
- GlobalFilter
- mariada-connector
- HashMap
- reactor
- AbstractMethodError
- dynamodb
- msyql-connector-java
- custom config data convertion
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |