스프링 부트 5 - 자동 구성 기반 애플리케이션
빈 오브젝트의 역할과 구분
- 애플리케이션 빈: 개발자가 어떤 빈을 사용하겠다고 명시적으로 구성 정보를 제공한 것을 말한다.
- 컨테이너 인프라스트럭처 빈: 스프링 컨테이너 자신이거나, 스프링 컨테이너가 계속 기능을 확장하면서 추가해온 것들을 빈으로 등록시켜서 사용하는 빈들. 개발자가 이런 빈들을 등록 해달라고 요청하지 않지만, 컨테이너가 스스로 빈으로 등록해서 동작시키는 방식으로 이용한다.
- ApplicationContext: 컨테이너는 자기 자신도 빈으로 접근할 수 있도록 등록한다.
- Environment: 외부 설정과 같은 환경 정보에 접근하기 위한 빈
- BeanPostProcessor, BeanFactoryPostProcessor: 빈 오브젝트를 생성, 초기화, 관계 맺기, 라이프 사이클을 관리하는 과정에서 기능을 추가할 수 있는 빈
- DefaultAdvisorAutoProxyCreator: AOP 등록 등을 위한 빈
컨테이너 인프라스트럭처 빈은 우리의 관심사가 아니다. 일반적으로 애플리케이션에서 해당 빈들을 조회하거나 참조하지 않는다. 물론 필요한 경우 DI로 받아서 활용할 수 있다.
개발자가 신경써야할 빈은 애플리케이션 빈이다. 애플리케이션 빈은 두 가지 종류로 나눌 수 있다.
- 애플리케이션 로직 빈
- 애플리케이션의 기능(비즈니스 로직, 도메인 로직)을 담고 있는 클래스로 만들어지는 빈들이다.
- 컴포넌트 스캐너에 의해서 빈 생성 및 등록된다.
- 예시) 컨트롤러 빈, 서비스 빈
- 애플리케이션 인프라스트럭처 빈(이하 인프라 빈)
- 대부분 기술과 관련된 로직을 담고 있다.
- 주로 개발자가 직접 작성 및 등록하지는 않고, 이미 만들어져 있는 것(라이브러리)을 사용한다고 명시하는 일만 한다.
- 예시) DataSource, JpaEntityManagerFactory, JdbcTransactionManager.
스프링 부트의 빈 등록 방법
- 컴포넌트 스캔(ComponentScan) - 주로 애플리케이션 로직 빈은 사용자가 직접 구성 정보를 등록한다.
- 자동 구성(AutoConfiguration) - 주로 애플리케이션 인프라 빈은 자동 구성에 의해서 등록된다.
스프링 부트는 애플리케이션의 환경 또는 build.gradle
의 dependencies
를 이용해 인프라 빈을 자동으로 구성해준다. 스프링 부트를 이용한 web server을 만들 때 디펜전시로 spring-boot-starter-web
을 추가하는 이유는, 이 패키지에 web 서버를 구성하기 위한 여러가지 구성 정보 및 라이브러리가 포함되기 때문이다. 즉, 스프링 부트는 web 서버를 구동하기 위한 여러 인프라 빈을 자동 구성해준다.
지금부터 스프링 부트의 작동 원리를 이해하기 위해 직접 인프리 반의 자동 구성을 코드로 구성해보자.
인프라 빈 구성 정보의 분리(@Import)
먼저 컴포넌트 스캔의 범위에 대해 알 필요가 있다. 컴포넌트 스캔의 범위는 @ComponentScan
이 붙은 클래스의 패키지를 포함한 모든 하위 패키지가 된다.
우리는 인프라스트럭처 빈을 애플리케이션 로직이 있는 패키지와 분리하고 싶다. 이때 @Import
를 사용하면 @Component
를 가지고 있는 클래스를 스캔 대상이 아니어도 빈으로 추가할 수 있다. 주로 설정 클래스인 @Configuration
이 붙은 클래스를 빈으로 등록하기 위해 사용한다.
사용 예시(using 메타 에노테이션)
1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
@ComponentScan
@Import({DispatcherServletConfig.class, TomcatWebServerConfig.class})
public @interface MySpringBootApplication {...}
1
2
3
4
5
6
7
8
// 상위 다른 패키지
@Configuration
public class TomcatWebServerConfig {
@Bean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
1
2
3
4
5
6
7
8
// 상위 다른 패키지
@Configuration
public class DispatcherServletConfig {
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
}
동적으로 자동 구성 등록
인프라 빈을 동적으로 자동 구성 등록을 할 수 없을까? 예를 들면, Tomcat과 Jetty를 동적으로 선택하고 싶은 경우이다. 매번 @Import
의 class 정보들을 직접 바꾸지 않고, 외부 파일을 이용해 구성 정보를 동적으로 바꿔보자.
이를 구현하기 위해 스프링 3.1부터 추가된 ImportSelector
을 사용해야 한다. 이 인터페이스의 selectImports
메소드를 구현하면, 빈으로 등록할 클래스들을 ~.class
대신 문자열(String) 배열을 사용할 수 있다.
- 인터페이스 및 메소드 정의
1
2
3
4
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
...
}
우리는 ImportSelector
을 직접 사용하기보다는 이를 조금 확장한 DeferredImportSelector
을 사용해보자. DeferredImportSelector
은 다른 설정 클래스(@Configuration
)의 구성 정보 생성 작업이 모두 끝난 다음에, ImportSelector
가 실행되도록 순서를 뒤로 지연하게 만든 것이다.
1
2
3
4
5
6
7
8
9
public class MyAutoConfigImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] {
"tobyspring.config.autoconfig.DispatcherServletConfig",
"tobyspring.config.autoconfig.TomcatWebServerConfig"
};
}
}
1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
@ComponentScan
@Import(MyAutoConfigImportSelector.class) // ImportSelector 사용
public @interface MySpringBootApplication {...}
순서를 뒤로 지연시킨 이유는, DB 또는 외부 설정파일에서 정보를 읽거나 현재 환경 정보를 참고해서 등록할 빈들을 결정할 수도 있기 때문이다.
자동 구성 정보를 외부 파일에서 읽기
위 코드 예시에서는 등록할 인프라 빈을 String타입의 배열로 직접 하드코딩했다. 이를 외부 파일에서 읽어오도록 바꿔보자.
단순 String이기 때문에 외부 텍스트 파일을 읽어서(자바의 기본적인 파일 읽기) 배열로 만드는 것은 간단하다. 하지만 규격화된 방식으로 외부 파일을 작성하는 방법이 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
}
1
2
3
4
@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으로 적어준다.
1
2
3
4
// resources.META-INF.spring 폴더에
// 'tobyspring.config.MyAutoConfiguration.imports' 라는 이름으로 파일 만들기
tobyspring.config.autoconfig.TomcatWebServerConfig
tobyspring.config.autoconfig.DispatcherServletConfig
Reference
해당 게시물은 인프런 - 토비의 스프링 부트 이해와 원리을 기반으로 작성되었습니다.