티스토리 뷰

Spring Framework

Spring Bean 과 CGLIB proxy

소농배 2022. 4. 23. 15:03

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 에서 결정되게 된다. 

  1. Target Class 가 interface 인 경우
  2. Target Class 가 Proxy Class 인 경우
  3. 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
링크
«   2024/05   »
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 31
글 보관함