@ConditionalOnMissingBean
스타터와 Jetty 서버 구성 추가
- Jetty를 서블릿 컨테이너로 사용해보자.
- 조건부 자동 구성: configuration 클래스에 infra structure 역할을 하는 빈의 구성 정보를 담아서 기술 종류별로 잘게 쪼개어 여러 개의 클래스를 만들어 놓고 외부 설정 파일에 이 목록을 나열하고 파일을 읽어서 해당하는 configuration 클래스를 부트가 시작할 때 빈으로 등록한다.
- 자동 구성 configuration 클래스들이 스프링 컨테이너에 들어있다.
- 스프링 부트에 내장된 @AutoConfiguration 애너테이션은 상위에 @Configuration이 있다.
- 다음과 같이 파일을 살펴 보면 144개의 파일 위치 정보가 들어있다.
- 부트가 켜지면 144개의 구성 정보 클래스가 모두 로딩된다.
- spring starter-web
- 여러 라이브러리가 포함되어 있다.
- 터미널 >> ./graglew dependencies --configuration compileClasspath
- 을 실행하면 spring starter-web 하위에 여러 개의 라이브러리를 포함하고 있는 것을 알 수 있다.
- ex) 스프링 부트 버전을 정하면 그에 맞는 톰캣 버전 호환성까지 맞춰서 정해준다.
- 앞서 본 것과는 다르게 Jetty는 직접 추가해줘야한다.
- 다음 디펜던시를 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-jetty'
- 관련 라이브러리를 확인할 수 있다.
- JettyWebServerConfig
- main() 메서드 안에서
- ServletWebServerFactory 타입의 빈을 찾아서 이걸로 서블릿 컨테이너를 띄운다.
<hide/>
@MyAutoConfiguration
public class JettyWebServerConfig {
@Bean("JettyWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new JettyServletWebServerFactory();
}
}
- MyAutoConfigImportSelector
<hide/>
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// List<String> autoConfigs = new ArrayList<>();
// ImportCandidates.load(MyAutoConfiguration.class, classLoader).forEach(
// autoConfigs::add);
// return autoConfigs.toArray(new String[0]);
return new String[]{
"tobyspring.config.autoconfig.TomcatWebServerConfig",
"tobyspring.config.autoconfig.DispatcherServletConfig",
"tobyspring.config.autoconfig.JettyWebServerConfig"
};
}
Note) 실행 결과
- Caused by: org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : TomcatWebServerFactory,JettyWebServerFactory
- 같은 타입의 중복된 bean이 존재하므로 부트를 시작할 수 없다. (우선 순위를 모르기 때문에 예외발생)
- 어떻게 우선 순위를 걸 수 있을까?
@Conditional 과 Condition
- @Conditional
- matches()를 오버라이드한다.
- true: 해당 빈 구성 정보를 사용한다.
- false: 해당 빈 구성 정보를 사용하지 않는다.
- ConditionContext: 현재 컨테이너와 스프링 컨테이너가 돌아가고 있는 환경에 대한 정보를 얻을 수 있다.
- AnnotatedTypeMetadata: @Conditional을 메타 애너테이션으로 사용 중인 다른 애너테이션들의 정보를 이용할 수 있도록 그 애너테이션들의 메타데이터를 반환한다.
- true 반환: JettyConfig 에 붙은 @Conditional 에서는 condition() 이 항상 true를 반환하도록 한다.
- matches()를 오버라이드한다.
<hide/>
@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;
}
}
}
- Tomcat 클래스는 반대로 false를 반환하도록 한다.
- 자동 구성 후보들을 불러와서 스프링 컨테이너에 빈으로 등록해달라고 한다. (메서드의 반환값에 따라 스프링 컨테이너가 결정한다.)
- @Conditional 애너테이션을 참고해서 빈 등록 여부를 결정한다.
<hide/>
@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;
}
}
}
Note) 실행 결과 - 정상적으로 부트가 뜬다.
- 반대로 true/false를 지정하고나서 부트를 띄우면 jetty가 아닌 tomcat이 뜬다.
- 만약 둘다 false이면? 빈을 등록하지 않았을 때와 같은 에러가 난다.
apitest 실패
- 오류
- 원인
- 해결
- @Conditional 도 메타 애너테이션으로 사용 가능하다.
- @Bean method 에도 동일하게 @Conditional을 붙일 수 있다 .
@Conditional 학습 테스트
- 다음과 같은 테스트 클래스를 만들어서
- getBean()을 통해 빈이 등록됐는지 아닌지 파악 가능하다.
- 테스트 목적의 애플리케이션 컨텍스트 구현: ApplicationContextRunner
<hide/>
public class ConditionalTest {
@Test
void conditional() {
// true
ApplicationContextRunner contextRunner = new ApplicationContextRunner();
contextRunner.withUserConfiguration(Config1.class)
.run(context -> {
assertThat(context).hasSingleBean(MyBean.class);
assertThat(context).hasSingleBean(Config1.class);
});
// false
new ApplicationContextRunner().withUserConfiguration(Config2.class)
.run(context -> {
assertThat(context).doesNotHaveBean(MyBean.class);
assertThat(context).doesNotHaveBean(Config2.class);
});
}
@Configuration
@Conditional(TrueCondition.class)
static class Config1 { //
@Bean
MyBean myBean() {
return new MyBean();
}
}
@Configuration
@Conditional(FalseCondition.class)
static class Config2 { // 등록이 안되도록 컨디션 구성
@Bean
MyBean myBean() {
return new MyBean();
}
}
static class MyBean {
}
private static class TrueCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return true;
}
}
private static class FalseCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
}
}
- "@BooleanConditional" 을 새로운 애너테이션으로 뺸다.
<hide/>
public class ConditionalTest {
@Test
void conditional() {
// true
ApplicationContextRunner contextRunner = new ApplicationContextRunner();
contextRunner.withUserConfiguration(Config1.class)
.run(context -> {
assertThat(context).hasSingleBean(MyBean.class);
assertThat(context).hasSingleBean(Config1.class);
});
// false
new ApplicationContextRunner().withUserConfiguration(Config2.class)
.run(context -> {
assertThat(context).doesNotHaveBean(MyBean.class);
assertThat(context).doesNotHaveBean(Config2.class);
});
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Conditional(BooleanCondition.class)
@interface BooleanConditional {
boolean value();
}
@Configuration
@BooleanConditional(true)
static class Config1 { //
@Bean
MyBean myBean() {
return new MyBean();
}
}
@Configuration
@BooleanConditional(false)
static class Config2 { // 등록이 안되도록 컨디션 구성
@Bean
MyBean myBean() {
return new MyBean();
}
}
static class MyBean {}
private static class BooleanCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> annotationAttribute = metadata.getAnnotationAttributes(BooleanConditional.class.getName());
Boolean value = (Boolean) annotationAttribute.get("value");
return value;
}
}
}
커스텀 @Conditional
- Tomcat 이라는 클래스의 패키지명: org.apache.catalina.startup;
- Server 클래스: org.eclipse.jetty.server;
- ClassUtils : 클래스와 관련된 유틸 클래스이다.
- 기존의 톰캣, 제티 클래스의 오버라이드한 matches()의 반환 값을 다음과 같이 바꿔준다.
- Tomcat
<hide/>
@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
<hide/>
@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 ClassUtils.isPresent("org.eclipse.jetty.server.Server",
context.getClassLoader()
);
}
}
}
Note) 실행 결과 - 에러
- Caused by: org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans: TomcatWebServerFactory,jettyWebServerFactory
- 여러 개의 서블릿 웹 서버 팩토리 빈이 존재하므로 부트를 시작할 수 없다.
- build.gradle
- 다음과 같이 exclude, module 을 넣어준다.
- 아래와 같이 설정한 다음 스프링 부트를 돌리면 Jetty로 실행되어야한다.
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'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
- 컨디셔널
- conditional을 만들 때는 보통 ElementType.Method를 붙여준다.
<hide/>
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(MyOnClassConditional.class)
public @interface ConditionalMyOnClass {
String value();
}
- MyOnClass
<hide/>
public class MyOnClassConditional implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalMyOnClass.class.getName());
String value = attributes.get("value").toString();
return ClassUtils.isPresent(value, context.getClassLoader()); // value에 해당하는 클래스가 존재하면 TRUE 없으면 FALSE
}
}
- 기존의 제티 클래스를 다음과 같이 바꾼다.
- 처음에 붙였던 @Conditional을 떼고 @ConditionalMyOnClass 를 붙인다.
<hide/>
@MyAutoConfiguration
@ConditionalMyOnClass( "org.eclipse.jetty.server.Server")
public class JettyWebServerConfig {
@Bean("jettyWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new JettyServletWebServerFactory();
}
}
- 톰캣
<hide/>
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {
@Bean("TomcatWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
- build 파일을 다음과 같이 설정해준 다음
- 부트를 띄우면 자동으로 톰캣이 들어간다.
<hide/>
dependencies {
implementation ('org.springframework.boot:spring-boot-starter-web')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Note)
- @ConditionalMyOnClass
- attribute에 클래스 이름을 집어넣는다.
- OnMyClassCondition
- Condition 인터페이스를 구현한 클래스
자동 구성 정보 대체하기
- 스프링 애플리케이션의 구성 정보를 설정하는 방법은 크게 두 가지가 있다.
- 1) 사용자 구성 정보(ComponentScan):
- 2) 자동 구성 정보(AutoConfiguration):
- MyOnClassCondition이라는 컨디션 클래스를 통해서 지정한 클래스가 존재하는지 체크해보고
- 다음과 같이 띄우면?
- 에러가 난다.
- 같은 종류의 빈이 존재하기 때문이다.
- Caused by: org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : customWebServerFactory,TomcatWebServerFactory
- 그러면 여러 개의 빈이 있는 경우, 특정 빈을 선택하는 방법은?
<hide/>
@Configuration(proxyBeanMethods = false)
public class WebServerConfiguration {
@Bean
ServletWebServerFactory customWebServerFactory(){
TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
serverFactory.setPort(9090);
return serverFactory;
}
}
- 기존에 @Conditional을 클래스에 붙여왔지만 메서드에도 붙일 수 있다.
- 메서드에 붙이는 경우, 해당 메서드의 return type 형태와 같은 빈이 존재하지 않는 경우에 대해서만 해당 메서드를 빈으로 등록한다.
- @ConditionalOnMissingBean
- 다음과 같이 톰캣 설정 클래스에 애너테이션을 적용하고나서야 부트가 돌아간다.
- 해당 구성 정보를 만들어두었는지 확인해보고 아닌 경우에만 해당 빈을 등록한다.
<hide/>
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {
@Bean("TomcatWebServerFactory")
@ConditionalOnMissingBean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
Note)
- 사용자 구성 정보 쪽에 CustomWebServerFactory 빈을 만들면?
- 자동 구성 정보 쪽에는 TomcatWebServerConfig
- 클래스 레벨에서 MyOnClassCondition을 이용해서 톰캣 클래스가 있는지 체크하고 해당 클래스를 빈으로 등록한다.
- 그 안에 있는 @Bean 메서드를 하나씩 체크한다.
스프링 부트의 @Conditional
- @Conditional 스프링 부트 4.XX 버전부터 사용 가능하다.
- @Profile도 @Conditional 기능을 한다.
class conditions
- @ConditionalOnClass
- @ConditionalOnMissingClass
- 지정한 클래스의 프로젝트 내 존재를 확인해서 포함 여부를 결정한다.
- 주로 @Configuration 클래스 레벨에서 사용하지만 @Bean 메서드에서 적용 가능하다.
- 단 클래스 레벨의 검증 없이 @Bean 메서드에만 적용하면 불필요하게 @Configuration 클래스가 빈으로 등록되므로 클래스 레벨 사용을 우선해야한다.
bean conditions
- @ConditionalOnClass
- @ConditionalOnMissingClass
- 빈의 존재 여부를 기준으로 포함 여부를 결정한다. 빈의 타입 또는 이름을 지정할 수 있다.
- 지정된 빈 정보가 없으면 메서드의 리턴 타입을 기준으로 빈의 존재 여부를 체크한다.
- 컨테이너에 등록된 빈 정보를 기분으로 체크하기 때문에 자동 구성 사이에 적용하려면 @Configuration 클래스의 적용 순서가 중요하다. 개발자가 직접 정의한 커스톰 빈 구성 정보가 자동 구성 정보 처리보다 우선하므로 이 관계에 적용하는 것이 안전하다. 반대로 커스텀 빈 구성 정보에 적용하는 것 피해야한다.
property conditions
- @ConditionalOnProperty는 스프링의 환경 프로퍼티 정보를 이용한다.
- 지정된 프로퍼티가 존재하고 fasle가 아니면 포함 대상이 된다. 특정 값을 가진 경우를 확인하거나 프로퍼티가 존재하지 않을 때 조건을 만족하게 할 수도 있다.
- 프로퍼티 존재를 확인해서 빈 오브젝트를 추가하고 해당 빈 오브젝트에서 프로퍼티 값을 이용해서 세밀하게 빈 구성을 할 수도 있다.
resource conditions
- @ConditionalOnResource는 지정된 리소스(파일)의 존재를 확인하는 조건이다.
web application conditions
- @ConditionalOnWebApplication
- @ConditionalOnNotWebApplication
- 웹 애플리케이션 여부를 확인한다. 모든 스프링 부트 프로젝트가 웹 기술을 사용해야하는 것은 아니다.
SpEL expression conditions
- @ConditionalOnExpression은 스프링 SpEL(스프링 표현식)의 처리 결과를 기준으로 판단한다. 상세한 조건 설정이 가능하다.
Environment 추상화와 프로퍼티
- content
- content
title
- content
- content
title
- content
- content
title
- content
- content
출처 - 인프런 토비의 스프링
'Spring Framework > 토비의 스프링' 카테고리의 다른 글
Chapter 08. 외부 설정을 이용한 자동 구성 (0) | 2023.08.02 |
---|---|
Chapter 06. 메타 애너테이션과 합성 애너테이션 (2) | 2023.07.14 |
Chapter 05. DI와 테스트, 디자인 패턴 (0) | 2023.07.04 |
Chapter 04. 독립 실행형 스프링 애플리케이션 (0) | 2023.06.24 |
Chapter 03. 독립 실행형 서블릿 애플리케이션 (0) | 2023.06.20 |