스프링 부트 6 - 조건부 자동 구성
스프링 부트가 제공하는 자동 구성 정보
전 게시물의 마지막 부분에서는 자동 구성 정보를 외부 파일에서 읽은 후, 동적으로 자동 구성을 등록하도록 했었다. 이 기능을 만들기 위해 MyAutoConfiguration
애노테이션 클래스를 만들고, 이 클래스의 full path+name 으로 만들어진 텍스트 파일(resources/META_INF/spring/{애노테이션 클래스 이름}.imports
)을 생성했었다.
스프링 부트도 비슷한 방식으로 구현이 되어있다. 스프링 부트에 이 기능을 하는 클래스 애노테이션 이름은 AutoConfiguration
이다. 또한 자동 구성할 빈들의 목록이 저장되어 있는 텍스트 파일은 org.springframework.boot.autoconfigure.AutoConfiguration.imports
이다.
org.springframework.boot.autoconfigure.AutoConfiguration.imports
해당 파일을 열어보면 자동 구성이 되는 144개의 Configuration 클래스들을 확인할 수 있다(web 라이브러리 사용시). 이처럼 스프링 부트는 자기 주장이 강하다. 사용할 기술이나 설정, 라이브러리 버전 등을 이미 결정해놓았다.
각각의 Configuration 클래스들은 내부에 1개 이상의 빈 애노테이션이 붙은 팩토리 메소드를 가진다. 또한 Configuration 클래스 자체도 빈으로 등록된다. 우리가 애플리케이션을 시작할 때마다 기본적으로 400~500개 정도의 빈들을 다 생성한다고 생각해보자. 불필요한 리소스가 너무 많다.
따라서 실제 스프링 부트는 이런 방식으로 모든 빈들을 다 생성하진 않는다. 예를 들어, Thymeleaf를 사용하지 않는다고 가정하자. 그러면 불필요하게 ThymeleafConfiguration에 있는 빈들을 생성할 필요가 없다.
본 게시물에서는 조건을 걸어서 어떤 빈들의 생성 여부를 결정하는 방법을 살펴보고자 한다.
이 게시물의 예시로는 서블릿 컨테이너로 Tomcat과 Jetty를 선택적으로 사용하는 기능을 구현해보자.
Jetty 라이브러리 추가
Tomcat 라이브러리는 스프링 디펜전시로 web을 선택하면 자동으로 임폴트 된다.
Jetty 라이브러리는 spring-boot-starter
가 지원하므로 아래와 같이 간단하게 임폴드 할 수 있다.
1
2
3
4
5
6
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jetty'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Tomcat과 Jetty를 빈으로 등록하기 위해 설정 클래스를 만들자.
1
2
3
4
5
6
7
@MyAutoConfiguration
public class TomcatWebServerConfig {
@Bean("tomcatWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
1
2
3
4
5
6
7
@MyAutoConfiguration
public class JettyWebServerConfig {
@Bean("jettyWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new JettyServletWebServerFactory();
}
}
자동 구성 정보에 추가하기 위해, 전 게시물에서 만든 ~.imports
파일에 아래와 같은 내용을 추가한다.
1
2
3
tobyspring.config.autoconfig.TomcatWebServerConfig
tobyspring.config.autoconfig.JettyWebServerConfig
tobyspring.config.autoconfig.DispatcherServletConfig
이대로 애플리케이션을 실행하면 Tomcat이 사용될까, Jetty가 사용될까?
정답은 애플리케이션이 실행되지 않는다.(에러)
에러 메시지는 대략 아래와 같다.
1
2
3
4
org.springframework.context.ApplicationContextException:
Unable to start web server;
nested exception is org.springframework.context.ApplicationContextException:
Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans
ServletWebServerFactory 빈이 여러개 존재하기 때문에 애플리케이션을 시작할 수 없다고 뜬다.
Tomcat과 Jetty를 자유롭게 바꿔가며 사용하고 싶다면 어떻게 해야할까? 자동구성할 빈들이 텍스트로 나열되어 있는 ~.imports
을 수정하지 않고, 자동 구성 빈 정보를 변경해보자.
@Conditional, Condition
스프링에서는 해당 클래스(또는 빈)를 생성할지 말지 동적으로 결정할 수 있는 애노테이션(@Conditional
)을 지원한다.
먼저 코드로 확인하자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@MyAutoConfiguration
@Conditional(JettyWebServerConfig.JettyCondition.class)
public class JettyWebServerConfig {
@Bean("jettyWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new JettyServletWebServerFactory();
}
static class JettyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return true; // 이 클래스를 빈으로 사용하겠다.
}
}
}
클래스 레벨에 @Conditional
을 붙이고, 그 내부에 Condition
을 구현한 클래스를 넣어준다. 일단 위 코드에서는 내부 static 클래스로 구현했다.
해당 인터페이스의 구현 메소드는 matches
는 이 클래스의 빈을 생성할지(true 반환), 말지(false 반환) 결정하는 역할을 한다. 그 인자로는 전체적인 어플리케이션 환경을 얻을 수 있는 context를 받아서 이용할 수 있다.
다음과 같이 톰켓 설정 클래스도 작성하면 서블릿 컨테이너로 Jetty를 사용하게 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@MyAutoConfiguration
@Conditional(TomcatWebServerConfig.TomcatCondition.class)
public class TomcatWebServerConfig {
@Bean("tomcatWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
static class TomcatCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false; // 이 클래스를 빈으로 사용 안하겠다.
}
}
}
ClassUtils 사용
그럼 자동 구성 빈들을 변경할 때마다 matches
메소드의 반환 값을 바꿔줘야 하는가?
이 문제를 해결하기 위해 스프링 부트가 사용하는 방법 중 하나는 ‘해당 라이브러리가 프로젝트에 존재하는가?’ 이다.
참고로 Tomcat(또는 Jetty)ServletWebServerFactory.class
는 스프링 부트에 들어 있는 클래스이다. 이 클래스가 존재한다고 해서 라이브러리가 존재하는 것은 아니다.
톰켓 라이브러리의 가장 메인이 되는 클래스는 “org.apache.catalina.startup.Tomcat”, 제티 라이브러리의 가장 메인이 되는 클래스는 “org.eclipse.jetty.server.Server” 이다. 프로젝트 내에 이 클래스들의 존재 여부를 이용하여 어떤 컨테이너를 사용할지 결정해보자.
어떤 라이브러리(클래스)가 이 프로젝트에 포함되어 있는지 존재 여부는 어떻게 아는가?
스프링이 제공하는 유틸리티 중 ClassUtils
이 존재한다. 이 클래스의 메소드를 이용해 어떤 클래스가 이 프로젝트에 포함되어 있는지 구분이 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@MyAutoConfiguration
@Conditional(TomcatWebServerConfig.TomcatCondition.class)
public class TomcatWebServerConfig {
@Bean("tomcatWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
static class TomcatCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return ClassUtils.isPresent("org.apache.catalina.startup.Tomcat", context.getClassLoader());
// jetty 클래스는 아래와 같이 작성.
// return ClassUtils.isPresent("org.eclipse.jetty.server.Server", context.getClassLoader());
}
}
}
이제 프로젝트에 포함되어 있는 라이브러리에 따라서 서블릿 컨테이너가 자동 구성을 다르게 해줄 것이다.
확인을 위해 build.gradle에서 dependency를 조작하며, 애플리케이션 실행 시 어떤 컨테이너를 사용하는지 확인해보자.
1
2
3
4
5
6
7
// Jetty로 실행 (tomcat을 exclude)
dependencies {
implementation ('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
implementation 'org.springframework.boot:spring-boot-starter-jetty'
}
1
2
3
4
// Tomcat로 실행
dependencies {
implementation ('org.springframework.boot:spring-boot-starter-web')
}
메타 애노테이션으로 리팩토링
Condition을 사용하는 톰켓 설정 클래스와 제티 설정 클래스는 아주 유사하게 생겼다. ClassUtils
의 메소드에 들어가는 문자열 값만 다른 것 뿐이다. 따라서 메타 어노테이션을 이용하여 이 부분을 공통 어노테이션으로 리팩토링 하자.
1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(MyOnClassCondition.class)
public @interface ConditionalMyOnClass {
String value();
}
1
2
3
4
5
6
7
8
public class MyOnClassCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> attrs = metadata.getAnnotationAttributes(ConditionalMyOnClass.class.getName());
String value = (String) attrs.get("value");
return ClassUtils.isPresent(value, context.getClassLoader());
}
}
이제 자동 구성 설정 파일에서 다음과 같이 사용할 수 있다.
1
2
3
4
5
6
7
8
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {
@Bean("tomcatWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
자동 구성 정보를 대체하기
사용자가 인프라 빈을 직접 빈으로 생성하고 싶을 때는 자동 구성이 다음과 같이 진행되면 된다.
- 사용자가 설정 클래스에서 빈으로 직접 생성
- 자동 구성 시에
- 해당 타입이 이미 빈으로 생성되어 있다면 → 무시
- 해당 타입이 빈으로 생성되어 있지 않다면 → 빈으로 생성
먼저 사용자가 설정 클래스에서 빈으로 직접 생성하는 코드를 작성해보자.
1
2
3
4
5
6
7
8
9
@Configuration(proxyBeanMethods = false)
public class WebServerConfiguration {
@Bean
ServletWebServerFactory customerWebServerFactory() {
TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
serverFactory.setPort(9090);
return serverFactory;
}
}
톰켓 서블릿 컨테이너를 직접 생성 후, 포트를 9090으로 설정 후 빈으로 등록하는 코드이다.
이제 자동 구성을 제어해야 한다. 해당 타입이 이미 빈으로 등록 되었는지 여부는 어떻게 알 수 있을까?
이 부분은 꽤나 로직이 복잡하기 때문에, 스프링 부트가 제공하는 애노테이션(@ConditionalOnMissingBean
)을 사용해보자.
1
2
3
4
5
6
7
8
9
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {
@Bean("tomcatWebServerFactory")
@ConditionalOnMissingBean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
이런 패턴은 자주 쓰인다. 클래스 레벨에서는 ‘해당 라이브러리가 프로젝트에 포함되어 있는가?’ 를 체크하고, 빈 팩토리 메서드에서는 ‘해당 타입의 빈을 개발자가 구성 정보로 만들었는가?’ 를 체크하는 방법이다.
@Conditional을 확장한 스프링 부트의 애노테이션
@Conditional은 스프링 4.0부터 추가된 애노테이션이다. 이 애노테이션을 이용하여 만들어진 스프링 부트의 애노테이션들을 살펴보자
- @Profile
- 운영 환경(개발, 배포, 테스트)에 따라서 빈들을 다르게 구성하고 싶을 때 사용하는 애노테이션이다.
- @ConditionalOnClass (주로 클래스 레벨에서 체크)
- 프로젝트 내에 지정한 클래스의 존재 여부를 확인 후, 존재하면 해당 클래스가 유효하게 된다.
- 이를 응용하여 라이브러리의 메인 클래스를 지정하면, 해당 라이브러리의 존재 여부에 따라 설정 클래스의 포함 여부를 결정할 수 있다.
- 주로 @Configuration 클래스 레벨에서 사용한다.
- 반대로 지정한 클래스가 없다면, 애노테이션이 붙은 클래스를 포함시켜주는 @ConditionalOnMissingClass도 존재한다.
- @ConditionalOnMissingBean, @ConditionalOnBean
- 빈의 존재 여부를 기준으로 포함 여부를 결정한다. 이때 빈의 타입 또는 이름으로 지정할 수 있다.(생략 시 리턴 타입을 기준으로)
- 등록 당시에 컨테이너의 등록된 빈 정보를 기준으로 체크하기 때문에, 자동 구성 사이에 적용하려면 설정 클래스의 적용 순서가 중요하다.
- 하지만 개발자가 직접 빈을 등록하는 로직은 자동 구성보다 더 우선적으로 실행되기 때문에, 순서를 크게 고려하지 않아도 된다.
- 클래스 레벨에서 @ConditionalOnClass로 1차 체크하고, 빈 레벨에서 @ConditionalOnMissingBean로 2차 체크하는 조합은 가장 대표적으로 사용되는 방식이다.
- @ConditionalProperty
- 지정된 프로퍼티가 존재하고 값이 false가 아니면 포함 대상이 된다.
- @ConditionalOnResource
- 지정된 리소스의 존재 여부에 따라서 조건을 확인한다.
- @ConditionalOnWebApplication, @ConditionalOnNotWebApplication
- 해당 프로젝트가 웹 기술을 사용하는지의 여부를 확인한다.
- @ConditionalOnExpression
- 스프링 SpEL의 처리 결과를 기준으로 포함 여부를 판단한다. 매우 섬세하게 설정할 수 있다.
Reference
해당 게시물은 인프런 - 토비의 스프링 부트 이해와 원리을 기반으로 작성되었습니다.