Miscellaneous
Spring의 @Configuration이 싱글톤을 보장하는 방법 본문
서론
취준 스터디에서 싱글톤과 프록시에 대해 공부하던 중, @Configuration 어노테이션이 빈을 관리하는 방법에 대해 조사하게 되었습니다.
조사 과정에서 @Configuration의 내부 동작 방식을 깊이 살펴보았고, 빈이 관리되는 과정을 추적하였습니다.
해당 포스트에서는 그 과정을 정리하여, @Configuration이 어떻게 빈을 싱글톤으로 보장하는지 원리를 살펴보겠습니다.
본론
@Configuration
public class AppConfig {
@Bean
public A a() {
return new A(b());
}
@Bean
public B b() {
return new B();
}
}
위 코드는 간단한 Configuration 클래스입니다.
위 코드에서 @Configuration이나 @Bean 어노테이션이 없다면, 총 2개의 B 인스턴스가 생성될 것입니다.
하지만, 어노테이션 덕분에, 우리는 B가 단 한번만 생성된다는 것을 보장할 수 있습니다.
그럼 @Configuration은 어떻게 빈을 싱글톤으로 관리할까요?
스프링은 이를 프록시를 이용하여 구현했습니다.
스프링은 @Configuration이 붙은 클래스에 대해서 내부적으로 새로운 클래스를 생성합니다.
다시 말해, 위의 AppConfig를 바탕으로 아래의 새로운 클래스가 생성되어 AppConfig 대신 빈을 관리합니다.
class AppConfig$$EnhancerBySpringCGLIB extends AppConfig {
private BeanFactory beanFactory;
@Override
public A a() {
if (beanFactory.containsSingleton("a")) { //이미 존재하는 빈인지 확인
return (A) beanFactory.getSingleton("a"); //존재한다면 해당 빈 return
}
A a = super.a(); // 존재하지 않는다면 빈 생성 및 저장
beanFactory.registerSingleton("a", a);
return a;
}
@Override
public B b() {
// A와 동일 ...
}
}
(실제 코드가 이렇다는 것은 아니고, 대략적인 로직이 위와 같습니다!)
@Configuration이 붙은 클래스를 바탕으로 위와 같은 새로운 클래스가 생성되고, @Bean이 붙은 메서드에 대해서 위와 같은 메서드가 구현됩니다.
위 코드에서 주의 깊게 보아야 하는 부분은 총 2가지 입니다.
1. 클래스명 맨 뒤 CGLIB
2. beanFactory.containsSingleton()
1. CGLIB
생성된 클래스명을 보면, CGLIB라는 단어를 확인하실 수 있습니다.
CGLIB는 스프링이 프록시를 생성하는 방법 중 하나입니다.
Spring이 프록시를 생성하는 방법에는 두가지가 존재합니다.
클래스 기반의 프록시는 CGLIB를, 인터페이스 기반 프록시는 JDK Dynamic Proxy를 사용합니다.
예상하셨다시피, @Configuration 클래스는 CGLIB를 이용하여 프록시가 생성됩니다.
@Configuration은 대부분 구체 클래스에 정의되고, @Bean 메서드 또한 클래스 안에서 정의되기 때문입니다.
CGLIB에 의해서 구현체 클래스를 바탕으로 이를 상속받아 프록시가 생성됩니다.
따라서 CGLIB에 의해 생성된 클래스는 AppConfig를 상속받고 있으며, 해당 클래스에서 @Override된 메서드에서는 동일한 시그니쳐의 부모 메서드를 호출합니다. (super.a())
※ CGLIB는 런타임 동안 바이트코드를 조작하여 AppConfig를 상속한 새로운 클래스를 메모리에 생성합니다. 따라서 생성된 클래스는 .java 파일로 생성되는 것은 아닙니다!
2. beanFactory.containsSingleton()
BeanFactory는 빈을 생성 및 관리하는 최상위 인터페이스입니다.
Spring의 빈 컨테이너 계층 구조에서 DefaultSingletonBeanRegistry가 실제 싱글톤 빈 관리를 담당합니다. 그리고 해당 클래스 내부에는 아래와 같은 Map이 존재합니다.
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
해당 Map의 key로는 빈의 이름을, value는 빈 인스턴스를 관리합니다.
위 클래스의 containsSingleton()과 getSinleton() 메서드는 SingletonBeanRegistry 인터페이스에서 정의되며, DefaultSingletonBeanRegistry에서 실제 구현됩니다.
결론
결론적으로,
@Configuration 어노테이션은 CGLIB 프록시를 통해 빈의 싱글톤을 보장합니다.
Spring은 런타임에 원본 Configuration 클래스를 상속한 새로운 프록시 클래스를 생성하고, 각 @Bean 메서드를 Override하여 싱글톤 관리 로직을 삽입합니다.
이를 통해 개발자가 메서드를 여러 번 호출하더라도 동일한 빈 인스턴스가 반환되는 것입니다.
추가로,
@Configuration가 아닌, @Component로 빈을 생성하는 경우에는 CGLIB 프록시를 만들지 않습니다.
따라서 해당 클래스 내부에서 @Bean을 사용해도 싱글톤이 보장되지 않습니다.
또, @Configuration(proxyBeanMethods = false)로 선언하는 경우에도 프록시를 생성하지 않아, 싱글톤이 깨지게 됩니다.
이 글이 @Configuration의 동작 원리를 이해하는 데 도움이 되셨기를 바랍니다.
감사합니다.