Spring Boot 앱을 빌드할 때, 어떤 조건이 충족될 때만 애플리케이션 컨텍스트에 Bean이나 모듈을 로드하고 싶을 때가 있습니다. 테스트 중에 일부 Bean을 비활성화하거나 런타임 환경에서 특정 속성에 반응하는 경우입니다.
Spring은 애플리케이션 컨텍스트의 일부에 적용할 사용자 정의 조건을 정의할 수 있는
@Conditional
이라는 annotation을 도입했습니다. Spring Boot는 이를 기반으로
구축되어 일부 사전 정의된 조건을 제공하므로 직접 구현할 필요가 없습니다.
이 튜토리얼에서는 Conditional로 로드된 Bean이 왜 필요한지 설명하는 몇 가지 사용 사례를 살펴보겠습니다. 그런 다음 조건을 적용하는 방법과 Spring Boot가 제공하는 조건을 살펴보겠습니다. 마무리로 사용자 지정 조건도 구현하겠습니다.
참고자료가 되는 원본 출처로 가 보면, 샘플 코드도 제공하고 있으니 참고하세요.
Conditional Bean 이 필요한 이유는 무엇인가요?
Spring 애플리케이션 컨텍스트에는 런타임에 애플리케이션에 필요한 모든 Bean을 구성하는
객체 그래프가 들어 있습니다. Spring의 @Conditional
annotation을 사용하면 특정 Bean이
해당 객체 그래프에 포함되는 조건을 정의할 수 있습니다.
특정 조건 하에서 콩을 포함하거나 제외해야 하는 이유는 무엇입니까?
제 경험상 가장 흔한 사용 사례는 특정 Bean이 테스트 환경에서 작동하지 않는다는 것입니다. 테스트 중에 사용할 수 없는 원격 시스템이나 애플리케이션 서버에 연결해야 할 수도 있습니다. 따라서 테스트 중에 이러한 Bean을 제외하거나 대체하기 위해 테스트를 모듈화 하고자 합니다.
또 다른 사용 사례는 특정 횡단적 관심사를 활성화하거나 비활성화하려는 경우입니다. 보안을 구성하는 모듈을 빌드했다고 가정해 보겠습니다. 개발자 테스트 중에 사용자 이름과 비밀번호를 매번 입력하고 싶지 않으므로 스위치를 뒤집어 로컬 테스트를 위해 전체 보안 모듈을 비활성화합니다.
또한, 우리는 특정 Bean을 작동할 수 없는 외부 리소스가 있는 경우 에만 로드하고 싶을 수 있습니다.
예를 들어, logback.xml
클래스 경로에서 파일이 발견된 경우에만 Logback 로거를 구성하고
싶습니다.
다음 토론에서 몇 가지 더 많은 사용 사례를 살펴보겠습니다.
조건에 따른 Bean 선언
Spring Bean을 정의하는 모든 곳에서 선택적으로 조건을 추가할 수 있습니다. 이 조건이
충족되는 경우에만 Bean이 애플리케이션 컨텍스트에 추가됩니다. 조건을 선언하려면
아래에 @Conditional...
설명된 annotation 중 하나를 사용할 수 있습니다.
하지만 먼저, 특정 Spring Bean에 조건을 적용하는 방법부터 살펴보겠습니다.
Conditional @Bean
단일 @Bean정의에 조건을 추가하는 경우 조건이 충족되는 경우에만 이 Bean이 로드됩니다.
@Configuration
class ConditionalBeanConfiguration {
@Bean
@Conditional... // <--
ConditionalBean conditionalBean() {
return new ConditionalBean();
}
;
}
Conditional @Configuration
Spring에 조건을 추가하면 @Configuration이 구성에 포함된 모든 Bean은 조건이 충족되는 경우에만 로드됩니다.
@Configuration
@Conditional... // <--
class ConditionalConfiguration {
@Bean
Bean bean() {
...
}
;
}
Conditional @Component
마지막으로 @component, @Service, @Repository, 또는 @Controller 등과 같은 stereotype annotation 중 하나로 선언된 모든 Bean에 조건을 추가할 수 있습니다.
@Component
@Conditional... // <--
class ConditionalComponent {
}
Pre-defined Conditions
Spring Boot는 바로 사용할 수 있는 미리 정의된 @ConditionalOn...
annotation을
제공합니다. 각각을 차례로 살펴보겠습니다.
@ConditionalOnProperty
이 @ConditionalOnProperty
annotation은 제 경험상 Spring Boot 프로젝트에서
가장 일반적으로 사용되는 Conditional annotation입니다. 특정 환경 속성에 따라 Bean을 Conditional로
로드할 수 있습니다.
@Configuration
@ConditionalOnProperty(
value = "module.enabled",
havingValue = "true",
matchIfMissing = true)
class CrossCuttingConcernModule {
...
}
CrossCuttingConcernModule은 module.enabled
값이 있고,true
인 경우에만
로드됩니다. 속성이 전혀 설정되지 않은 경우에도 matchIfMissing
를 true
로 정의했기
때문에 로드됩니다. 이런 식으로, 우리는 달리 결정할 때까지 기본적으로 로드되는 모듈을
만들었습니다.
같은 방식으로 보안이나 스케줄링과 같은 교차적 문제에 대해 다른 모듈을 만들어 특정 (테스트) 환경에서 비활성화할 수도 있습니다.
@ConditionalOnExpression
여러 속성에 기반한 더 복잡한 조건이 있는 경우 @ConditionalOnExpression
을
사용할 수 있습니다.
@Configuration
@ConditionalOnExpression(
"${module.enabled:true} and ${module.submodule.enabled:true}"
)
class SubModule {
...
}
SubModule은 module.enable
속성과 module.submodule.enabled
두 속성 모두에
값이 있는 경우에만 로드됩니다. 속성에 추가하여 속성이 설정되지 않은 경우 기본값으로
사용하라고 Spring에 알립니다. Spring Expression Language 의 전체 확장을 사용할
수 있습니다.
이 방법을 사용하면 부모 모듈이 비활성화되면 비활성화되어야 하는 하위 모듈을 만들 수 있지만, 부모 모듈이 활성화되면 비활성화될 수도 있습니다.
@ConditionalOnBean
때로는 애플리케이션 컨텍스트에서 특정 다른 Bean을 사용할 수 있는 경우에만 Bean을 로드하고 싶을 수 있습니다.
@Configuration
@ConditionalOnBean(OtherModule.class)
class DependantModule {
...
}
DependantModule은 애플리케이션 컨텍스트에 OtherModule 클래스의 Bean이 있는 경우에만 로드됩니다. Bean 클래스 대신 Bean 이름을 정의할 수도 있습니다.
이런 식으로, 예를 들어 특정 모듈 간의 종속성을 정의할 수 있습니다. 한 모듈은 다른 모듈의 특정 Bean이 사용 가능한 경우에만 로드됩니다.
@ConditionalOnMissingBean
마찬가지로, 특정 다른 Bean이 애플리케이션 컨텍스트에 없는 경우에만 Bean을 로드하려는
경우 @ConditionalOnMissingBean
을 사용할 수 있습니다 .
@Configuration
class OnMissingBeanModule {
@Bean
@ConditionalOnMissingBean
DataSource dataSource() {
return new InMemoryDataSource();
}
}
이 예에서 우리는 이미 사용 가능한 데이터 소스가 없는 경우에만 애플리케이션 컨텍스트에 메모리 내 데이터 소스를 주입합니다. 이는 Spring Boot가 테스트 컨텍스트에서 메모리 내 데이터베이스를 제공하기 위해 내부적으로 수행하는 작업과 매우 유사합니다.
@ConditionalOnResource
클래스 경로에서 특정 리소스를 사용할 수 있다는 사실에 따라 Bean을 로드하려는 경우
@ConditionalOnResource
을 사용할 수 있습니다.
@Configuration
@ConditionalOnResource(resources = "/logback.xml")
class LogbackModule {
...
}
LogbackModule클래스 경로에서 logback configuration 파일이 발견된 경우에만 로드됩니다. 이런 식으로, 해당 구성 파일이 발견된 경우에만 로드되는 유사한 모듈을 만들 수 있습니다.
Other Conditions
위에서 설명한 Conditional annotation은 Spring Boot 애플리케이션에서 사용할 수 있는 일반적인 annotation입니다. Spring Boot는 더 많은 Conditional annotation 을 제공합니다. 그러나 이러한 annotation은 그렇게 일반적이지 않으며 일부는 애플리케이션 개발보다는 프레임워크 개발에 더 적합합니다(Spring Boot는 이러한 annotation 중 일부를 비밀리에 많이 사용합니다). 따라서 여기서는 간략하게 살펴보겠습니다.
@ConditionalOnClass
특정 클래스가 클래스 경로에 있는 경우에만 Bean을 로드합니다.
@Configuration
@ConditionalOnClass(name = "this.clazz.does.not.Exist")
class OnClassModule {
...
}
@ConditionalOnMissingClass
특정 클래스가 클래스 경로에 없는 경우에만 Bean을 로드합니다.
@Configuration
@ConditionalOnMissingClass(value = "this.clazz.does.not.Exist")
class OnMissingClassModule {
...
}
@ConditionalOnJndi
JNDI를 통해 특정 리소스를 사용할 수 있는 경우에만 Bean을 로드합니다.
@Configuration
@ConditionalOnJndi("java:comp/env/foo")
class OnJndiModule {
...
}
@ConditionalOnJava
특정 버전의 Java를 실행하는 경우에만 Bean을 로드합니다.
@Configuration
@ConditionalOnJava(JavaVersion.EIGHT)
class OnJavaModule {
...
}
@ConditionalOnSingleCandidate
@ConditionalOnBean
과 유사하지만 주어진 Bean 클래스에 대한 단일 후보가 결정된
경우에만 Bean을 로드합니다. auto-congifurations 외부에 사용 사례가 없을
가능성이 큽니다.
@Configuration
@ConditionalOnSingleCandidate(DataSource.class)
class OnSingleCandidateModule {
...
}
@ConditionalOnWebApplication
웹 애플리케이션 내부에서 실행하는 경우에만 Bean을 로드합니다.
@Configuration
@ConditionalOnWebApplication
class OnWebApplicationModule {
...
}
@ConditionalOnNotWebApplication
웹 애플리케이션 내부에서 실행 하지 않는 경우에만 Bean을 로드합니다 .
@Configuration
@ConditionalOnNotWebApplication
class OnNotWebApplicationModule {
...
}
@ConditionalOnCloudPlatform
특정 클라우드 플랫폼에서 실행하는 경우에만 Bean을 로드합니다.
@Configuration
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
class OnCloudPlatformModule {
...
}
Custom Conditions
Conditional annotation 외에도 우리는 우리만의 조건 annotation을 만들고 논리 연산자를 사용해 여러 조건을 결합할 수 있습니다.
Custom Condition의 정의
운영 체제와 네이티브하게 통신하는 Spring Bean이 있다고 가정해 보겠습니다. 이러한 Bean은 해당 운영 체제에서 애플리케이션을 실행하는 경우에만 로드해야 합니다.
코드를 유닉스 머신에서 실행하는 경우에만 Bean을 로드하는 조건을 구현해 보겠습니다. 이를 위해 Spring의 Condition 인터페이스를 구현합니다.
class OnUnixCondition implements Condition {
@Override
public boolean matches(
ConditionContext context,
AnnotatedTypeMetadata metadata) {
return SystemUtils.IS_OS_LINUX;
}
}
우리는 단순히 Apache Commons의 SystemUtils
클래스를 사용하여 유닉스 계열
시스템에서 실행 중인지 확인합니다. 필요하다면 현재 애플리케이션 컨텍스트
(ConditionContext) 또는 annotation이 달린 클래스(AnnotatedTypeMetadata)에
대한 정보를 사용하는 보다 정교한 로직을 포함할 수 있습니다.
이제 이 조건을 Spring의 @Conditional
annotation과 함께 사용할 준비가 되었습니다.
@Bean
@Conditional(OnUnixCondition.class)
UnixBean unixBean() {
return new UnixBean();
}
OR로 조건 결합
논리적 “OR” 연산자를 사용하여 여러 조건을 단일 조건으로 결합하려면
AnyNestedCondition
을 확장할 수 있습니다.
class OnWindowsOrUnixCondition extends AnyNestedCondition {
OnWindowsOrUnixCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@Conditional(OnWindowsCondition.class)
static class OnWindows {}
@Conditional(OnUnixCondition.class)
static class OnUnix {}
}
여기서는 애플리케이션이 Windows나 Unix에서 실행되는지 여부에 관계없이 충족되는 조건을 생성했습니다.
AnyNestedCondition
부모 클래스는 메서드에 대한 @Conditional
annotation을
평가하고 OR
연산자를 사용하여 이를 결합합니다.
이 조건은 다른 조건과 마찬가지로 사용할 수 있습니다.
@Bean
@Conditional(OnWindowsOrUnixCondition.class)
WindowsOrUnixBean windowsOrUnixBean() {
return new WindowsOrUnixBean();
}
AnyNestedCondition
이나AllNestedConditions
가 동작하지 않나요?
super()
에 전달된ConfigurationPhase
매개변수를 확인합니다. 결합된 조건을@Configuration
Bean에 적용하려면PARSE_CONFIGURATION
값을 사용합니다. 조건을 간단한 Bean에 적용하려면 위의 예시처럼REGISTER_BEAN
을 사용합니다. Spring Boot는 애플리케이션 컨텍스트 시작 시 적절한 시기에 조건을 적용할 수 있도록 이러한 구분을 해야 합니다.
AND를 사용한 조건 결합
조건을 AND
논리와 결합하려면 단일 Bean에 여러 개의 @Conditional...
annotation
을 사용하면 됩니다. 이러한 annotation은 논리적 “AND” 연산자와 자동으로 결합되므로
적어도 하나의 조건이 실패하면 Bean이 로드되지 않습니다.
@Bean
@ConditionalOnUnix
@Conditional(OnWindowsCondition.class)
WindowsAndUnixBean windowsAndUnixBean() {
return new WindowsAndUnixBean();
}
누군가가 내가 모르는 Windows/Unix 하이브리드를 만들지 않는 한, 이 Bean은 반드시 로드되어서는 안 됩니다.
@Conditional
annotation은 단일 메서드나 클래스에서 두 번 이상 사용할 수 없습니다.
따라서 이런 방식으로 여러 annotation을 결합하려면 이러한 제한이 없는 사용자 지정
@ConditionalOn...
annotation을 사용해야 합니다. 아래에서 @ConditionalOnUnix
annotation을 만드는 방법을 살펴보겠습니다.
또는 AND를 사용하여 조건을 단일 @Conditional
annotation으로 결합하려면
Spring Boot의 AllNestedConditions
클래스를 확장할 수 있으며, 이는 위에서 설명한
AnyNestedConditions
과 정확히 동일하게 작동합니다.
NOT을 사용한 조건 결합
AnyNestedCondition
및 AllNestedConditions
과 유사하게, 결합된 조건이
하나도 일치하지 않는 경우에만 Bean을 로드하도록 NoneNestedCondition
을 확장할
수 있습니다.
사용자 정의 @ConditionalOn… annotation 정의
우리는 어떤 조건에 대해서도 custom annotation을 만들 수 있습니다. 우리는 단순히
이 annotation에 다음과 같이 메타 @Conditional
annotation을 달면 됩니다:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnLinuxCondition.class)
public @interface ConditionalOnUnix {}
우리가 새로운 annotation으로 Bean에 annotation을 달 때 Spring은 이 메타 annotation을 평가할 것입니다:
@Bean
@ConditionalOnUnix
LinuxBean linuxBean(){
return new LinuxBean();
}
결론
@Conditional annotation과 Custom @Conditional...
annotation을
생성할 수 있는 기능을 통해, Spring은 이미 우리에게 애플리케이션 컨텍스트의 내용을
제어할 수 있는 많은 기능을 제공합니다.
Spring Boot는 편리한 @ConditionalOn...
annotation을 최상단에 추가하여
사용할 수 있게 해 주고, AllNestedConditions
, AnyNestedCondition
또는
NoneNestedCondition
을 사용하여 조건을 결합할 수 있도록 하여 이를
기반 으로 구축됩니다. 이러한 도구를 사용하면 프로덕션 코드와 테스트를 모듈화할 수 있습니다.
하지만 권한에는 책임이 따르므로 조건문으로 애플리케이션 컨텍스트를 가득 채우지 않도록 주의해야 합니다. 그렇지 않으면 언제 무엇이 로드되는지 추적할 수 없게 됩니다.
Leave a comment