본문 바로가기
Java & Kotlin/Spring

@Profile 대신 @Conditional로 유연하게 Bean 등록하기

by devson 2022. 1. 1.

@Profile을 사용하면 spring.profiles.active에 따라 Bean을 등록하거나 하지않거나 할 수 있다.

하지만 @Profile은 String 값을 입력받기 때문에 하드 코딩으로 인해 일괄 변경이 힘들 때가 있고,

여러 profile 조건이 붙는 경우 스프링에서 처리할 수 있는 문법을 알아야하는 등 불편한 점이 있다.

 

그래서 개인적으로 @Profile을 사용하기 보다는 @Conditional을 사용해서 profile 별 Bean 등록 처리하는 것을 선호한다.

이번 포스팅에서는 @Conditional과 이를 사용하여 @Profile을 대체하는 방법에 대해 알아보자.

 

예제 코드는 여기에서 확인할 수 있다.

 


@Conditional

먼저 @Conditional 에 대해 알아보자

@Conditional은 Bean 등록 조건 정보를 담은 Condition을 입력받아 Bean을 등록할지 말지를 결정한다.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

    /**
     * All {@link Condition} classes that must {@linkplain Condition#matches match}
     * in order for the component to be registered.
     */
    Class<? extends Condition>[] value();

}

 

사실 @Profile도 내부적으로 @Conditional을 사용한다.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class) // <=== @Conditional 사용
public @interface Profile {

    /**
     * The set of profiles for which the annotated component should be registered.
     */
    String[] value();

}

 

그러면 Conditional을 사용하여 profile 별 Bean 등록 처리를 하려면

profile을 확인하는 custom Condition을 만들고 @Conditional에서 이 custom Condition을 사용하면 될 것 같다.

이러한 과정을 코드로 알아보자.

 

Profile

Profile은 우리가 운용할 시스템의 profile 환경을 나타낸다.

(그렇기 때문에 각자 상황에 다를 수 있다)

enum class Profile(
    val value: String
) {
    LOCAL("local"),
    DEVELOP("develop"),
    QA("qa"),
    STAGE("stage"),
    PROD("prod");

    fun isEqualTo(profile: String): Boolean = (this.value == profile)
}

 

Condition

앞서 설명했듯이 custom Condition은 profile을 확인하고 Bean을 등록할 것 인지를 결정한다.

 

여러 Condition으로 확장할 수 있도록 BaseProfileCondition을 만들도록 하자.

import org.springframework.context.annotation.Condition
import org.springframework.context.annotation.ConditionContext
import org.springframework.core.type.AnnotatedTypeMetadata

abstract class BaseProfileCondition : Condition {
    override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
        val activeProfiles = context.environment.activeProfiles
        val targetProfiles = this.getTargetProfiles()

        return activeProfiles.any {
            this.isActiveProfileMatchedWithTargetProfiles(it, targetProfiles)
        }
    }

    abstract fun getTargetProfiles(): List<Profile>

    private fun isActiveProfileMatchedWithTargetProfiles(activeProfile: String, targetProfile: List<Profile>): Boolean =
        targetProfile.any { it.isEqualTo(activeProfile) }
}

Bean을 등록할지를 정하는 것은 Condition#matches 메서드이다.

 

BaseProfileCondition은 이 matches를 구현하게 되는데 아래와 같은 과정을 거치게 된다.

  1. 현재 active profile을 가져온다.
  2. Bean을 등록할 조건인 profile을 가져온다.
  3. 1, 2의 profile을 비교하여 Bean을 등록할지를 결정한다.

여기서 과정 2를 추상 메서드로 만들었기 여러 profile을 지원하도록 확장할 수 있다. 

 

 

LocalProfileCondition

LocalProfileCondition은 local 환경에서만 Bean을 적용하도록 아래와 같이 Profile.LOCAL만을 리턴하도록 한다.

 

 

import com.tistory.devs0n.profile.profile.Profile

class LocalProfileCondition : BaseProfileCondition() {
    override fun getTargetProfiles(): List<Profile> = listOf(Profile.LOCAL)
}

 

LocalDevelopProfileCondition

여러 Profile에 거쳐 Bean을 등록하고자 한다면 여러 Profile을 리턴하면된다.

LocalDevelopProfileConditionlocal, dev 환경에서 사용하도록 아래와 같이 구현하였다.

import com.tistory.devs0n.profile.profile.Profile

class LocalDevelopProfileCondition : BaseProfileCondition() {
    override fun getTargetProfiles(): List<Profile> = listOf(Profile.LOCAL, Profile.DEVELOP)
}

 

NotLocalProfileCondition

이미 만들어놓은 BaseProfileCondition의 반대로 적용하려고 하면 아래와 같이 구현하면된다.

NotLocalProfileConditionlocal이 아닌 환경에서 사용하도록 하였다.

import org.springframework.context.annotation.Condition
import org.springframework.context.annotation.ConditionContext
import org.springframework.core.type.AnnotatedTypeMetadata

class NotLocalProfileCondition : Condition {
    private val localProfileCondition = LocalProfileCondition()
    
    override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean =
        !this.localProfileCondition.matches(context, metadata)
}

 

Test

이제 위에서 작업한 코드를 테스트를 통해 확인하자.

사용 방법은 아래 ConditionalTest.ConditionalBean 클래스에서 확인할 수 있듯이

자바: @Conditional(Condition.class) / 코틀린: @Conditional(Condition::class)로 사용하면된다.

import com.tistory.devs0n.profile.condition.LocalDevelopProfileCondition
import com.tistory.devs0n.profile.condition.LocalProfileCondition
import com.tistory.devs0n.profile.condition.NotLocalProfileCondition
import com.tistory.devs0n.profile.profile.Profile
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Conditional
import org.springframework.context.annotation.Configuration


class ConditionalTest {
    @Test
    fun `local 환경`() {
        val ac = AnnotationConfigApplicationContext()
        ac.environment.setActiveProfiles(Profile.LOCAL.value)
        ac.register(ConditionalBean::class.java)
        ac.refresh()

        assertThat(ac.beanDefinitionNames.contains("localBean")).isTrue
        assertThat(ac.beanDefinitionNames.contains("localDevelopBean")).isTrue
        assertThat(ac.beanDefinitionNames.contains("notLocalDevelopBean")).isFalse
    }

    @Test
    fun `develop 환경`() {
        val ac = AnnotationConfigApplicationContext()
        ac.environment.setActiveProfiles(Profile.DEVELOP.value)
        ac.register(ConditionalBean::class.java)
        ac.refresh()

        assertThat(ac.beanDefinitionNames.contains("localBean")).isFalse
        assertThat(ac.beanDefinitionNames.contains("localDevelopBean")).isTrue
        assertThat(ac.beanDefinitionNames.contains("notLocalDevelopBean")).isTrue
    }

    @Configuration
    class ConditionalBean {
        @Bean("localBean")
        @Conditional(LocalProfileCondition::class)
        fun localBean(): ConditionalBean = ConditionalBean()

        @Bean("localDevelopBean")
        @Conditional(LocalDevelopProfileCondition::class)
        fun localDevelopBean(): ConditionalBean = ConditionalBean()

        @Bean("notLocalDevelopBean")
        @Conditional(NotLocalProfileCondition::class)
        fun notLocalBean(): ConditionalBean = ConditionalBean()
    }
}
@Bean으로 Bean을 등록했기 때문에 위와 같이 메서드 위에 @Conditional을 적용하였지만,
@Component로 Bean을 등록한다면 @Component 주변에 @Conditional을 적용해도 된다.

 

테스트를 실행시키면 다음과 같이 원하는 대로 잘 동작함을 확인할 수 있다.

 


 

@Conditional을 사용하여 @Profile을 대체하는 방식에 대해 알아보았다.

꼭 위와 같이 할 필요는 없지만 Bean 등록 조건이 복잡한 케이스가 있다면

앞서 살펴본 것과 같이 따로 Condition 객체를 만들어서 적용할 수 있으니 상황에 맞게 적용하면 좋을 듯 하다.

댓글