Spring Boot 앱을 빌드할 때, 어떤 조건이 충족될 때만 애플리케이션 컨텍스트에 Bean이나 모듈을 로드하고 싶을 때가 있습니다. 테스트 중에 일부 Bean을 비활성화하거나 런타임 환경에서 특정 속성에 반응하는 경우입니다.

Spring은 애플리케이션 컨텍스트의 일부에 적용할 사용자 정의 조건을 정의할 수 있는 @Conditional 이라는 annotation을 도입했습니다. Spring Boot는 이를 기반으로 구축되어 일부 사전 정의된 조건을 제공하므로 직접 구현할 필요가 없습니다.

이 튜토리얼에서는 Conditional로 로드된 Bean이 왜 필요한지 설명하는 몇 가지 사용 사례를 살펴보겠습니다. 그런 다음 조건을 적용하는 방법과 Spring Boot가 제공하는 조건을 살펴보겠습니다. 마무리로 사용자 지정 조건도 구현하겠습니다.

참고자료가 되는 원본 출처로 가 보면, 샘플 코드도 제공하고 있으니 참고하세요.

Conditional Bean 이 필요한 이유는 무엇인가요?

Spring 애플리케이션 컨텍스트에는 런타임에 애플리케이션에 필요한 모든 Bean을 구성하는 객체 그래프가 들어 있습니다. Spring의 @Conditionalannotation을 사용하면 특정 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인 경우에만 로드됩니다. 속성이 전혀 설정되지 않은 경우에도 matchIfMissingtrue로 정의했기 때문에 로드됩니다. 이런 식으로, 우리는 달리 결정할 때까지 기본적으로 로드되는 모듈을 만들었습니다.

같은 방식으로 보안이나 스케줄링과 같은 교차적 문제에 대해 다른 모듈을 만들어 특정 (테스트) 환경에서 비활성화할 수도 있습니다.

@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부모 클래스는 메서드에 대한 @Conditionalannotation을 평가하고 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은 반드시 로드되어서는 안 됩니다.

@Conditionalannotation은 단일 메서드나 클래스에서 두 번 이상 사용할 수 없습니다. 따라서 이런 방식으로 여러 annotation을 결합하려면 이러한 제한이 없는 사용자 지정 @ConditionalOn...annotation을 사용해야 합니다. 아래에서 @ConditionalOnUnix annotation을 만드는 방법을 살펴보겠습니다.

또는 AND를 사용하여 조건을 단일 @Conditionalannotation으로 결합하려면 Spring Boot의 AllNestedConditions클래스를 확장할 수 있으며, 이는 위에서 설명한 AnyNestedConditions과 정확히 동일하게 작동합니다.

NOT을 사용한 조건 결합

AnyNestedConditionAllNestedConditions과 유사하게, 결합된 조건이 하나도 일치하지 않는 경우에만 Bean을 로드하도록 NoneNestedCondition을 확장할 수 있습니다.

사용자 정의 @ConditionalOn… annotation 정의

우리는 어떤 조건에 대해서도 custom annotation을 만들 수 있습니다. 우리는 단순히 이 annotation에 다음과 같이 메타 @Conditionalannotation을 달면 됩니다:

@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