스프링 컨테이너에 빈(Bean)을 등록하면 기본적으로 싱글톤 패턴으로 빈을 관리하게 된다. 따라서 해당 빈을 요청하게 되면 항상 동일한 빈을 반환해주는 것을 확인할 수 있다.
물론 기본 빈 등록 방식이(IoC / DI) 싱글톤 방식이라는 의미이며, 요청할 때마다 새로운 객체를 생성하여 반환해주는 프로토타입 방식 등 빈 스코프에 따라 다른 방식도 제공한다.
싱글톤 패턴이란 클래스의 인스턴스가 1개만 생성됨을 보장해주는 디자인 패턴으로 해당 인스턴스를 공유하여 사용함으로써 메모리를 절약할 수 있는 장점이 있다.
싱글톤 패턴에 대해서 공부하고 싶다면 여기 를 참고하면 된다.
MyBean이라는 클래스를 하나 선언해주고 이를 AppConfig 클래스에서 빈으로 등록해주도록 설정해주자.
public class MyBean {
...
}
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
테스트 결과는 아래와 같다.
@SpringBootTest
class SingletonContainerTest {
@Test
@DisplayName("싱글톤 컨테이너 : 동일한 빈 반환")
void test() {
AppConfig appConfig = new AppConfig();
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
assertThat(appConfig.myBean()).isNotSameAs(appConfig.myBean());
System.out.println("MyBean = " + appConfig.myBean());
System.out.println("MyBean = " + appConfig.myBean());
assertThat(ac.getBean(MyBean.class)).isSameAs(ac.getBean(MyBean.class));
System.out.println("MyBean = " + ac.getBean(MyBean.class));
System.out.println("MyBean = " + ac.getBean(MyBean.class));
}
}
첫번째 assertThat의 경우 생성자 호출 메서드가 두번 반환되기 때문에 서로 다른 주소 값이 출력된다.
두번째 assertThat의 경우 Application Context로 부터 빈을 호출받고 두 빈의 주소는 서로 같게 출력된다. 이게 스프링의 포인트이다.
이처럼 스프링 컨테이너는 싱글톤을 보장해주기 때문에 싱글톤 컨테이너라고도 불린다.
또한, 우리는 싱글톤 패턴을 위한 코드를 작성하지 않아도 스프링 프레임워크가 알아서 싱글톤 패턴을 적용해주기 때문에 굉장히 편리한 장점이 있다.
그렇다면 Java 코드에는 싱글톤 패턴을 위한 아무런 코드가 존재하지 않음에도 불구하고 스프링 프레임워크는 어떻게 빈들을 싱글톤 패턴으로 관리해줄 수 있는 것일까?
이는 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 가능하게 해준다.
스프링 프레임워크는 @Configuration 어노테이션이 달린 클래스에 속한 @Bean들을 스프링 컨테이너에 등록할 때 해당 클래스의 빈들에게 싱글톤 패턴을 적용하기 위해 임의의 다른 클래스를 만들어서 스프링 빈으로 등록한다.
CGLIB의 내부 기술은 굉장히 복잡하지만 대충 다음과 같은 로직으로 싱글톤 패턴을 적용해준다고 예상할 수 있다.
// CGLIB 내부..
@Bean
public MyBean myBean() {
if (myBean이 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MyBean를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
실제로 스프링 컨테이너에 등록한 AppConfig 클래스를 조회해보면 순수한 클래스가 아닌 CGLIB가 붙은 클래스가 조회되는 것을 확인할 수 있다.
@SpringBootTest
class SingletonContainerTest {
@Test
@DisplayName("싱글톤 컨테이너 : 동일한 빈 반환")
void test() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println("AppConfig = " + ac.getBean(AppConfig.class));
}
}
주의해야 할 점은 반드시 @Configuration 어노테이션이 존재해야 한다는 것이다. 이를 통해 빈 팩토리를 상속한 Application Context로써의 역할을 할 수 있다.(정확히는 Root Application Context)
스프링 프레임워크는 스프링 빈을 등록할 때 @Configuration 어노테이션이 붙은 빈들만 CGLIB를 적용하여 싱글톤을 보장해준다.
@Bean만 사용해도 스프링 빈으로 등록되지만, 의존관계 주입이 필요해서 메서드를 직접 호출하는 경우에는 싱글톤을 보장해주지 않는다.