👻 @Conditional은 무엇일까?

Spring을 사용하는 환경에서 @Configuration을 붙인 설정 클래스에서 Bean들을 등록하도록 클래스를 작성하면 항상 @Bean을 붙여서 명시한 Bean들은 Spring에 의해서 등록되게 됩니다.

매번 사용하는 Bean들이라면 이런 작동방식은 전혀 불편함을 주지 않을 것입니다.
이런 경우에는 그러면 어떻게 할 수 있을까요? 어떤 경우에는 Tomcat 서버를 이용하고 어떤 경우에는 Jetty 서버를 사용해야 할때는 어떻해야 할까요?

Tomcat과 Jetty 두 서버를 모두 Bean으로 등록하면 서버가 두개여서 어떤 것을 사용할지 모르겠다는 에러를 만나게 될테니까요.

이럴때 사용해야 하는 것이 @Conditional 입니다. Conditon 말 그대로 직역하면 "조건" 입니다. Bean을 생성할때 어떤 조건에 맞을때만 Bean으로 등록하고 그렇지 않으면 등록하지 않는 역할을 해줍니다.

지금부터 @Conditional에 대해서 하나하나 뜯어보겠습니다.

🙂 @Conditional 이해하기


@Conditional 애너테이션은 위와 같이 정의되어 있습니다. 하나하나 살펴보겠습니다.

  • @Target({ElementType.TYPE, ElementType.METHOD}) : 이 애너테이션은 클래스와, 메서드에 붙일 수 있다는 의미 입니다.
  • @Retention(RetentionPolicy.RUNTIME) : 이 애너테이션의 라이프 사이클은 소스코드 컴파일 후 클래스가 메모리에 올라가는 실행시까지 존재해야 한다는 의미입니다.
  • @Documented : 이 애너테이션의 정보가 javadoc으로 작성된 문서에 포함되게 한다는 의미입니다.

이제 아래를 살펴보면 @Conditional은 Class 타입의 value를 가질 수 있습니다.

Class<? extends Condition>의 의미는 무엇일까요? Condition은 Spring이 제공하는 interface 입니다.
Condition 인터페이스는 boolean 타입의 matches 메서드를 가지고 있습니다.

Class<? extends Condition>의 의미는 Condition 인터페이스를 구현한 클래스가 들어올 수 있다는 의미가 됩니다.

그렇다면 테스트 코드 작성을 통해, @Conditonal이 어떻게 동작하는지 확인해보겠습니다.

🦄 @Conditional Test 해보기

먼저 테스트를 진행할 클래스 안에서 Bean으로 사용될 클래스 두개를 작성하겠습니다.


public class TestConditional {


    static class Bean1 {}

    static class Bean2 {}

변수나 메서드를 가지고 있지 않은 단순한 형태의 클래스인 Bean1 & Bean2 클래스를 작성했습니다.

이제 @Conditional 대신 사용할 커스텀 애너테이션을 작성하겠습니다.

	@Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Conditional(MakeConditional.class)
    @interface CustomCondition {
        boolean value();
    }

클래스에만 붙일 수 있도록 @Target을 설정하고, 나머지는 @Conditional과 일치하지만 value 값으로 boolean 타입의 값을 가지도록 했습니다.

메타 애너테이션으로 @Conditional을 가지고, MakeConditional 이라는 Conditon 인터페이스를 구현한 클래스를 넣어 주었습니다.

MakeConditional 클래스를 작성해보겠습니다.

		static class MakeConditional implements Condition {

        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {

           	    Map<String, Object> attrMap =  metadata.getAnnotationAttributes(CustomCondition.class.getName());

            	boolean value = (boolean) attrMap.get("value");

            	return value;
        	}
    	}

MakeConditonal 클래스는 Condition 인터페이스를 구현하고, mathces 메서드를 오버라이딩한 클래스입니다.
@CustomCondition 애너테이션의 메타 데이터를 이용하여, CustomConditon이 가지고 있는 value 값이 true일 경우, Bean으로 등록이 되고, false일 경우 Bean으로 등록되지 않도록 구현하였습니다.

@Conditional 애너테이션 안에, Condition 인터페이스를 구현한 클래스를 넣어주게 되면, 구현 클래스의 matches 메서드의 반환 값에 따라서 Bean 등록 여부를 결정하게 됩니다.

이제 본격적인 테스트를 위해서 Confituration 클래스를 두개 작성하도록 하겠습니다.

	@Configuration
    @CustomCondition(true)
    static class Configuration1 {


        @Bean
        Bean1 bean1() {
            return new Bean1();
        }

        @Bean
        Bean2 bean2() {
            return new Bean2();
        }

    }

    @Configuration
    @CustomCondition(false)
    static class Configuration2 {


        @Bean
        Bean1 bean1() {
            return new Bean1();
        }

        @Bean
        Bean2 bean2() {
            return new Bean2();
        }

    }

Bean1과 Bean2를 Bean으로 등록하는 간단한 설정 클래스입니다. 두 클래스가 다른 점이 있다면 @CustomCondition 애너테이션의 value가 다릅니다. 따라서, Configuration1을 사용하면 Bean1과 Bean2가 Bean으로 정상 등록 될 것이고, Configuration2를 사용하면 Bean으로 등록되지 않아야 테스트가 성공했다고 볼 수 있습니다.

🐅 테스트 케이스 1 - Bean으로 등록되는 경우

	@Test
    void MakeBeanTrueTest() {

        ApplicationContextRunner ac = new ApplicationContextRunner();
        ac.withUserConfiguration(Configuration1.class)
                .run((context -> {

                    Assertions.assertThat(context).hasSingleBean(Bean1.class);
                    Assertions.assertThat(context).hasSingleBean(Bean2.class);

                }));

    }

AnnotationConfigApplicationContext대신 ApplicationContextRunner를 테스트에서 사용하는 이유는, @Conditional 테스트는 Bean이 등록되지 않아, Bean이 없을 경우에는 getBean 메서드를 사용하면 BeanException 예외가 발생하여 테스트가 복잡해질 수 있습니다.

따라서 테스트에 유용한 ApplicationContextRunner 클래스를 사용하겠습니다.

ApplicationContextRunner의 객체를 생성하고, 설정 정보를 가진 클래스로 Configuration1을 사용하도록 하였습니다.
run 메서드안에서, Bean1과 Bean2 타입의 Bean이 존재하는지 hasSingleBean 메서드를 이용해 테스트하는 코드를 작성했습니다.

테스트가 정상적으로 실행되는지 확인해보겠습니다.

테스트가 정상적으로 통과하는 것을 확인할 수 있습니다.

🐨 테스트 케이스 2 - Bean으로 등록되지 않는 경우

	@Test
    void MakeBeanFalseTest() {

        ApplicationContextRunner ac = new ApplicationContextRunner();

        ac.withUserConfiguration(Configuration2.class)
                .run((context) -> {

                    Assertions.assertThat(context).doesNotHaveBean(Bean1.class);
                    Assertions.assertThat(context).doesNotHaveBean(Bean2.class);

                });
    }

테스트 케이스 1과 위의 코드는 거의 유사하지만 테스트를 검증하는 부분에서 hasSingleBean이 아닌 doesNotHaveBean을 사용하였습니다. Configuration2 클래스는 @CustomConditon(false)로 설정하였기 때문에 Bean1과 Bean2가 Bean으로 등록되지 않습니다.

따라서, Bean1과 Bean2 타입의 Bean이 존재하지 않아야 테스트에 성공했다고 볼 수 있습니다.

테스트를 수행해보겠습니다.

테스트가 정상적으로 통과하는 것을 확인할 수 있습니다.

🐸 @Conditional을 파헤치고 남은 것들..!

그렇다면 어떤 WAS를 사용할지 조건에 따라 결정되야 할때 그럴 경우에만 @Conditional을 사용할까요?
또 그것은 아닙니다. Spring에서 제공하는 유용한 기능중에 profile 기능이 있습니다.

즉, Spring profile을 이용해서 개발환경, 테스트환경, 운영환경에 따라서 각각 다른 DB를 사용한다거나, 다른 데이터 접근 기술(Mybatis, JPA)를 사용하도록 @Conditional을 이용해 조건을 줄 수 있습니다.

이 부분에 대해서는 충분한 실습후에 또 다시 소개하도록 하겠습니다.

출처 : 토비의 스프링 부트 이해와 원리 - 인프런

0개의 댓글