컨테이너 인프라스트럭처 빈은 우리의 관심사가 아니다. 일반적으로 애플리케이션에서 해당 빈들을 조회하거나 참조하지 않는다. 물론 필요한 경우 DI로 받아서 활용할 수 있다.
개발자가 신경써야할 빈은 애플리케이션 빈이다. 애플리케이션 빈은 두 가지 종류로 나눌 수 있다.
스프링 부트의 빈 등록 방법은 크게 두 가지로 나눌 수 있다.
스프링 부트는 애플리케이션의 환경 또는 build.gradle
의 dependencies
를 이용해 인프라 빈을 자동으로 구성해준다.
스프링 부트를 이용한 web server을 만들 때 디펜전시로 spring-boot-starter-web
을 추가하는 이유는, 이 패키지에 web 서버를 구성하기 위한 여러가지 구성 정보 및 라이브러리가 포함되기 때문이다. 즉, 스프링 부트는 web 서버를 구동하기 위한 여러 인프라 빈을 자동 구성해준다.
지금부터 스프링 부트의 작동 원리를 이해하기 위해 직접 인프리 반의 자동 구성을 코드로 구성해보자.
먼저 컴포넌트 스캔의 범위에 대해 알 필요가 있다. 컴포넌트 스캔의 범위는 @ComponentScan
이 붙은 클래스의 패키지를 포함한 모든 하위 패키지가 된다.
인프라스트럭처 빈을 애플리케이션 로직이 있는 패키지와 분리하고 싶다. 이때 @Import
를 사용하면 @Component
를 가지고 있는 클래스를 스캔 대상이 아니어도 빈으로 추가할 수 있다. 주로 설정 클래스인 @Configuration
이 붙은 클래스를 빈으로 등록하기 위해 사용한다.
사용 예시(using 메타 에노테이션)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
@ComponentScan
@Import({DispatcherServletConfig.class, TomcatWebServerConfig.class})
public @interface MySpringBootApplication {...}
// 다른 상위 패키지
@Configuration
public class TomcatWebServerConfig {
@Bean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
// 다른 상위 패키지
@Configuration
public class DispatcherServletConfig {
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
}
인프라 빈을 동적으로 자동 구성 등록을 할 수 없을까? 예를 들면, Tomcat과 Jetty를 동적으로 선택하고 싶은 경우이다. 매번 @Import
의 class 정보들을 직접 바꾸지 않고, 외부 파일을 이용해 구성 정보를 동적으로 바꿔보자.
이를 구현하기 위해 스프링 3.1부터 추가된 ImportSelector
을 사용해야 한다. 이 인터페이스의 selectImports
메소드를 구현하면, 빈으로 등록할 클래스들을 ~~.class
대신 문자열(String) 배열을 사용할 수 있다.
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
...
}
우리는 ImportSelector
을 직접 사용하기보다는 이를 조금 확장한 DeferredImportSelector
을 사용해보자. DeferredImportSelector
은 다른 설정 클래스(@Configuration
)의 구성 정보 생성 작업이 모두 끝난 다음에, ImportSelector
가 실행되도록 순서를 뒤로 지연하게 만든 것이다.
public class MyAutoConfigImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] {
"tobyspring.config.autoconfig.DispatcherServletConfig",
"tobyspring.config.autoconfig.TomcatWebServerConfig"
};
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
@ComponentScan
@Import(MyAutoConfigImportSelector.class) // ImportSelector 사용
public @interface MySpringBootApplication {...}
순서를 뒤로 지연시킨것이 중요한 이유는, DB 또는 외부 설정파일에서 정보를 읽거나 현재 환경 정보를 참고해서 등록할 빈들을 결정할 수도 있기 때문이다.
위 코드 예시에서는 등록할 인프라 빈을 String타입의 배열로 직접 하드코딩했다. 이를 외부 파일에서 읽어오도록 바꿔보자.
단순 String이기 때문에 외부 텍스트 파일을 읽어서(자바의 기본적인 파일 읽기) 배열로 만드는 것은 간단하다. 하지만 규격화된 방식으로 외부 파일을 작성하는 방법이 있다.
public class MyAutoConfigImportSelector implements DeferredImportSelector {
private final ClassLoader classLoader;
public MyAutoConfigImportSelector(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> autoConfigs = new ArrayList<>();
ImportCandidates.load(MyAutoConfiguration.class, classLoader)
.forEach(autoConfigs::add);
return autoConfigs.toArray(String[]::new);
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration(proxyBeanMethods = false) // 굳이 @Bean으로 주입할 일이 없음. 즉, 굳이 프록시 필요 없음
public @interface MyAutoConfiguration {}
ImportCandidates.load(…)을 통해 외부 파일에서 정보를 읽어올 수 있다. 이때 인자는 2개가 필요한데, 하나는 @Configuration
이 붙은 애노테이션 클래스, 하나는 classLoader 이다.
애플리케이션의 클래스 패스에서 리소스(파일 등)을 읽어올 때는 클래스 로더(classLoader)를 사용한다. 클래스 로더는 생성자 DI를 통해서 주입받을 수 있다.
주입 받을 외부파일 위치와 파일 이름은 정해져있다. ImportCandidates.load(…)의 인자1로 넣어준 애노테이션 이름 뒤에 .imports를 붙인 이름을 resources/META_INF/spring 폴더에 만든다. 그리고 그 파일 내에 자동 등록할 인프라 빈을 패키지 이름부터 full name으로 적어준다.
// resources.META-INF.spring 폴더에
// 'tobyspring.config.MyAutoConfiguration.imports' 라는 이름으로 파일 만들기
tobyspring.config.autoconfig.TomcatWebServerConfig
tobyspring.config.autoconfig.DispatcherServletConfig
reference
해당 게시물은 인프런 - 토비의 스프링 부트 이해와 원리을 기반으로 작성되었습니다.
강의 내용을 축약하고 생략한 부분도 많기 때문에, 게시물만으로 해당 개념을 이해하지 못할 수 있습니다.
스프링 부트를 사용하시는 분들에게 해당 강의를 적극적으로 추천합니다.