조건부 자동 구성, @Conditional에 대해 알아보자
들어가며
- 해당 포스팅은 토비의 스프링 부트 - 이해와 원리를 학습하며 정리한 글입니다.
- 토비님의 강의 내용과 제가 이해한 방식대로 작성된 부분이 혼합 되어 작성되어 있습니다.
예시 상황, 스프링 부트 스타터
-
스프링 부트의 스타터는 애플리케이션에 포함시킬 의존 라이브러리 정보를 담고 있습니다. Maven 또는 Gradle의 의존 라이브러리 목록에 추가하면 스프링 부트가 선정한 기술의 종류와 버전에 해당하는 라이브러리 모듈이 프로젝트에 포함됩니다.
-
예를 들어,
spring-boot-starter는 스프링 코어, 스프링 부트 코어를 포함하여 자동 구성, 애노테이션, 로깅 등에 필요한 의존 라이브러리가 포함되어 있습니다.spring-boot-starter-web은 Spring Initializr에서 web 모듈을 선택하면 추가되는 스타터로, Spring Web, Spring MVC, Json, Tomcat 라이브러리가 추가됩니다. -
이러한
spring-boot-starter-web를 사용하게 되면 자동으로 톰캣을 사용하게 되는데, 다른 웹서버인 Jetty를 사용하고 싶으면 어떻게 할까요? 두 서버 모두 Bean으로 등록된다면 웹서버가 여러개로 매핑되어 에러가 발생할 것입니다. 이럴 때 사용하는 것이 특정 조건에 따라 Bean을 등록 여부를 결정할 수 있는 @Conditional 입니다.
@Conditional과 Condition
- 스프링 4.0에 추가된 @Conditional 애노테이션은 특정 조건을 만족하는 경우에만 컨테이너에 빈으로 등록되도록 합니다. 이 애노테이션은 @Configuration 클래스와 @Bean 메소드에 적용할 수 있습니다.
- 즉, @Conditional 어노테이션은 조건부로 빈을 등록하는 데 사용됩니다. 이는 특정 조건이 만족될 때만 빈을 등록함으로써 애플리케이션의 구성과 동작을 동적으로 제어할 수 있게 합니다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
참고
ApplicationContextRunner contextRunner = new ApplicationContextRunner();
contextRunner.withUserConfiguration(Config1.class)
.run(context -> {
assertThat(context).hasSingleBean(MyBean.class);
assertThat(context).hasSingleBean(Config1.class);
});
- 스프링 부트는 matches 메서드에서
ApplicationContextRunner를 사용하여 스프링 컨테이너에 빈이 등록되었는지를 테스트하는데 유용합니다.
클래스 기준 조건부 구성
- 스프링 부트가 사용하는 @Conditional의 대표적인 방법은 클래스의 존재를 확인하는 것입니다. 특정 클래스가 현재 프로젝트에 포함되어 있는지를 확인할 때는 스프링
ClassUtils.isPresent()를 사용합니다.
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());
}
}
자동 구성 정보 대체하기
- 개발자가 자동 구성으로 등록되는 빈과 동일한 타입의 빈을 @Configuration/@Bean을 이용해서 직접 정의하는 경우, 이 빈 구성이 자동 구성을 대체할 수 있습니다. 예를 들어, 아래와 같은 코드가 있으면 자동 구성 빈이 등록되지 않습니다.
@Bean("tomcatWebServerFactory")
@ConditionalOnMissingBean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Configuration(proxyBeanMethods = false)
public class WebServerConfiguration {
@Bean
ServletWebServerFactory customerWebServerFactory() {
TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
serverFactory.setPort(8080);
return serverFactory;
}
}
- @Bean("tomcatWebServerFactory")
- @ConditionalOnMissingBean 애노테이션이 적용된 servletWebServerFactory 메소드는 TomcatServletWebServerFactory 타입의 빈이 컨텍스트에 존재하지 않을 때만 빈을 등록합니다.
- WebServerConfiguration 클래스
- 이 클래스는 개발자가 직접 정의한 customerWebServerFactory 빈을 포함하고 있습니다.
- 이 빈은 TomcatServletWebServerFactory 타입이며, 포트를 8080으로 설정합니다.
- 이 빈이 컨텍스트에 등록되면, @ConditionalOnMissingBean 애노테이션이 적용된 자동 구성 빈(servletWebServerFactory)은 등록되지 않습니다.
- 이로 인해 여러개의 웹 서버가 빈으로 등록 될지에 대한 여부를 결정할 수 있다
스프링 부트의 다양한 @Conditional 애노테이션
스프링 부트는 여러 가지 @Conditional 애노테이션을 제공하여 다양한 조건을 기반으로 빈을 등록할 수 있게 합니다.
@ConditionalOnClass
- 특정 클래스가 클래스패스에 존재하는 경우에만 빈을 등록합니다.
@Configuration
@ConditionalOnClass(name = "com.example.MyClass")
public class MyClassConfiguration {
@Bean
public MyClass myClass() {
return new MyClass();
}
}
@ConditionalOnMissingClass
- 특정 클래스가 클래스패스에 존재하지 않는 경우에만 빈을 등록합니다.
@Configuration
@ConditionalOnMissingClass("com.example.MyMissingClass")
public class MyMissingClassConfiguration {
@Bean
public MyMissingClass myMissingClass() {
return new MyMissingClass();
}
}
@ConditionalOnBean
- 특정 빈이 스프링 컨테이너에 존재하는 경우에만 빈을 등록합니다.
@Configuration
@ConditionalOnBean(name = "myExistingBean")
public class MyExistingBeanConfiguration {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
@ConditionalOnMissingBean
- 특정 빈이 스프링 컨테이너에 존재하지 않는 경우에만 빈을 등록합니다.
@Configuration
@ConditionalOnMissingBean(name = "myMissingBean")
public class MyMissingBeanConfiguration {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
@ConditionalOnProperty
- 특정 프로퍼티가 설정된 경우에만 빈을 등록합니다.
@Configuration
@ConditionalOnProperty(name = "example.property", havingValue = "true")
public class MyPropertyConfiguration {
@Bean
public MyPropertyBean myPropertyBean() {
return new MyPropertyBean();
}
}
@ConditionalOnResource
- 특정 리소스가 클래스패스에 존재하는 경우에만 빈을 등록합니다.
@Configuration
@ConditionalOnResource(resources = "classpath:my-resource.txt")
public class MyResourceConfiguration {
@Bean
public MyResourceBean myResourceBean() {
return new MyResourceBean();
}
}
@ConditionalOnWebApplication
- 애플리케이션이 웹 애플리케이션인 경우에만 빈을 등록합니다.
@Configuration
@ConditionalOnWebApplication
public class MyWebApplicationConfiguration {
@Bean
public MyWebApplicationBean myWebApplicationBean() {
return new MyWebApplicationBean();
}
}
@ConditionalOnNotWebApplication
- 애플리케이션이 웹 애플리케이션이 아닌 경우에만 빈을 등록합니다.
@Configuration
@ConditionalOnNotWebApplication
public class MyNotWebApplicationConfiguration {
@Bean
public MyNotWebApplicationBean myNotWebApplicationBean() {
return new MyNotWebApplicationBean();
}
}
@ConditionalOnExpression
- SpEL(Spring Expression Language) 표현식을 이용해 조건을 판단합니다.
@Configuration
@ConditionalOnExpression("#{T(java.time.LocalDateTime).now().hour < 12}")
public class MyMorningConfiguration {
@Bean
public MyMorningBean myMorningBean() {
return new MyMorningBean();
}
}
@Profile도 @Conditional이다.
- 사실 직접 프로젝트를 경험하면서 가장 많이 @Conditional 어노테이션을 (간접적으로) 사용한 부분은 @Profile을 사용하면서 입니다.
- 개발단과 운영단에서 필요한 빈과 필요없는 빈이 존재하여서, 이를 구분 짓기 위해서 @Profile 애노테이션을 많이 사용하였습니다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
/**
* The set of profiles for which the annotated component should be registered.
*/
String[] value();
}
- 인터페이스를 살펴보면, 특정 프로파일이 활성화된 경우에만 조건부로 빈을 등록할 수 있는
@Profile애노테이션의 경우 @Conditional이 포함되어 있는 것을 알 수 있습니다.
@Bean
@Profile("dev")
public SecurityFilterChain filterChainDev(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
.requestMatchers(new AntPathRequestMatcher("/swagger-ui/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/v3/**")).permitAll()
);
setHttp(http);
return http.build();
}
@Bean
@Profile("prod")
public SecurityFilterChain filterChainProd(HttpSecurity http) throws Exception {
setHttp(http);
return http.build();
}
- 예를들어, 다음과 같이 swagger에 대한 문서화 제공이 개발단에서만 이루어져야 하기 때문에 @Profile을 통해 조건부로 빈이 등록되도록 하였습니다.
- production일 경우에는 스웨거를 사용하지 않는 SecurityFilterChain 빈을 등록하고, development인 경우에는 스웨거를 사용하는 SecurityFilterChain 빈을 등록하였습니다.
마치며
- 간단하게 @Conditional에 대해서 살펴보았습니다.
- @Conditional을 통해 스프링 부트의 조건부 자동 구성을 진행하여 애플리케이션의 다양한 요구사항을 유연하게 처리할 수 있습니다.
- 평소에 많이 사용하고 있었던 @Profile에서도 사용되는 애노테이션인 만큼, 알고 사용하는 것이 중요하기 떄문에 좋은 학습이였습니다.
