@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를 구현하게 되는데 아래와 같은 과정을 거치게 된다.
- 현재 active profile을 가져온다.
- Bean을 등록할 조건인 profile을 가져온다.
- 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을 리턴하면된다.
LocalDevelopProfileCondition은 local, dev 환경에서 사용하도록 아래와 같이 구현하였다.
import com.tistory.devs0n.profile.profile.Profile
class LocalDevelopProfileCondition : BaseProfileCondition() {
override fun getTargetProfiles(): List<Profile> = listOf(Profile.LOCAL, Profile.DEVELOP)
}
NotLocalProfileCondition
이미 만들어놓은 BaseProfileCondition의 반대로 적용하려고 하면 아래와 같이 구현하면된다.
NotLocalProfileCondition은 local이 아닌 환경에서 사용하도록 하였다.
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 객체를 만들어서 적용할 수 있으니 상황에 맞게 적용하면 좋을 듯 하다.
'Java & Kotlin > Spring' 카테고리의 다른 글
Request Rate Limiting with Spring Cloud Gateway - 0. 개요 (0) | 2022.01.30 |
---|---|
[Spring with Kotlin] sealed class로 다양하게 요청 받기 (1) | 2022.01.28 |
Spring Data JPA - Write, Read Only 분리 적용하기 - 2. 무력화 되는 경우 (3) | 2021.12.18 |
Spring Data JPA - Write, Read Only 분리 적용하기 - 1. 설정 및 원리 (3) | 2021.12.17 |
Testcontainers MongoDB init script 실행하기 (0) | 2021.10.29 |
댓글