김영한님의 스프링 핵심 원리 기본편 강의 중 싱글톤 컨테이너에 대해 배우던 중에 발생한 문제?이다.
코드를 직접 노출할 수는 없으니 살짝 줄여서 써보자면 아래와 같다.
@Configuration
public class AppConfig {
@Bean
public static ProductRepository productRepository() {
return new ProductRepositoryImpl();
}
@Bean
public ProductService productService() {
return new ProductServiceImpl(new productRepository());
}
@Bean
public StockService stockService() {
return new StockServiceImpl(new productRepository());
}
}
제품에 대한 정보가 저장된 DB와의 연결을 추상화하는 ProductRepository
와, 해당 bean을 주입 받는 ProductService
, StockService
가 각각 있다고 해보자.
이론상 스프링이 별도의 설정이 없는 한 각 bean들이 한 번 만들어지고 재사용되도록 설정되어 있으므로, 아래 서비스가 문제 없이 통과할 거라고 생각했었다.
public class SingletonBeanTest {
@Test
void sameInstanceTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
ProductRepository productRepository = ac.getBean("productRepository",
ProductRepository.class);
ProductServiceImpl productService = ac.getBean("productService",
ProductServiceImpl.class);
StockServiceImpl stockService = ac.getBean("stockService",
StockServiceImpl.class);
// 테스트를 위해 service에 getProductRepository 메서드가 있다고 가정
ProductRepository productRepository1 = productService.getProductRepository();
ProductRepository productRepository2 = stockService.getProductRepository();
assertThat(productRepository1).isSameAs(productRepository2);
assertThat(productRepository1).isSameAs(productRepository);
}
}
공부한 내용대로라면 두 서비스가 bean 선언 내부에서 productRepository
메서드를 각각 호출한다 하더라도, 스프링이 만약 productRepository
bean이 이미 만들어져있으면 다시 인스턴스를 만들지 않고 원래 있던 인스턴스의 주소를 호출할 것이므로 위의 테스트는 통과해야 한다.
근데 실패했다. (...)
너무 간단한 코드라 어디서 잘못됐는지 통 찾을 수가 없어서 해당 강의에 대한 이야기를 나누는 커뮤니티에 들어가봤는데, 나와 같은 문제를 겪고 계신 분이 계셨다!
빠밤,, 위의 configuration 파일에 저번 강의 때 테스트한다고 넣어 놨던 static 키워드가 남아 있어서 문제가 발생한 것이었다. 정정하면 아래와 같다.
@Configuration
public class AppConfig {
// static 키워드 빠짐!
@Bean
public ProductRepository productRepository() {
return new ProductRepositoryImpl();
}
@Bean
public ProductService productService() {
return new ProductServiceImpl(new productRepository());
}
@Bean
public StockService stockService() {
return new StockServiceImpl(new productRepository());
}
}
해결은 했는데, 이유가 뭘까?
우선, @Bean
이 붙은 메서드는 스프링 컨테이너의 프록시에 의해 실행 흐름이 가로채어져 각각의 설정(singleton, prototype, ..)에 맞는 올바른 인스턴스를 반환하게 된다.
반면 static 키워드가 붙는 경우, configuration에 붙은 이 프록시가 만들어지기 전에 static 메서드(위의 경우 productRepository
) 실행을 위한 별도의 configuration 객체가 만들어지기에 의도했던 컨테이너의 관리 범위 바깥에 있는 것과 마찬가지이므로 프록시가 실행 흐름을 가로채지 않는 것이다.
좀 자세히 보려면 BeanFactoryPostProcessor
쪽을 공부해봐야 할 것 같은데, 일단은 스프링 컨테이너가 bean에 대한 lifecycle을 돌릴 준비를 하기 전에 static 메서드가 먼저 메모리에 올라가지 않기 때문에 컨테이너의 관리를 받지 않아서 원래 의도했던 singleton 방식으로 작동하지 않았다- 정도로 이해하고 넘어가려고 한다. 아래 링크 남겨 놓을테니까 나중에 꼭 봐라 박유빈,,,,