Spring Framework/토비의 스프링

Chapter 07. 조건부 자동 구성

계란💕 2023. 7. 24. 00:40

@ConditionalOnMissingBean


스타터와 Jetty 서버 구성 추가

  • Jetty를  서블릿 컨테이너로 사용해보자.
  • 조건부 자동 구성: configuration 클래스에  infra structure 역할을 하는 빈의 구성 정보를 담아서 기술 종류별로 잘게 쪼개어 여러 개의 클래스를 만들어 놓고 외부 설정 파일에 이 목록을 나열하고 파일을 읽어서 해당하는 configuration 클래스를 부트가 시작할 때 빈으로 등록한다. 
    • 자동 구성 configuration 클래스들이 스프링 컨테이너에 들어있다. 
    • 스프링 부트에 내장된 @AutoConfiguration 애너테이션은 상위에 @Configuration이 있다. 
    • 다음과 같이 파일을 살펴 보면  144개의 파일 위치 정보가 들어있다. 
    • 부트가 켜지면 144개의 구성 정보 클래스가 모두 로딩된다. 

 

  • spring starter-web
    • 여러 라이브러리가 포함되어 있다. 
    • 터미널 >> ./graglew dependencies --configuration compileClasspath 
    • 을 실행하면  spring starter-web 하위에 여러 개의 라이브러리를 포함하고 있는 것을 알 수 있다. 
    • ex) 스프링 부트 버전을 정하면 그에 맞는 톰캣 버전 호환성까지 맞춰서 정해준다. 

 

 

  • 앞서 본 것과는 다르게  Jetty는 직접 추가해줘야한다. 
    • 다음 디펜던시를 추가해준다. 
implementation 'org.springframework.boot:spring-boot-starter-jetty'

 

  • 관련 라이브러리를 확인할 수 있다. 

 

 

    • JettyWebServerConfig 
      • main() 메서드 안에서
      • ServletWebServerFactory 타입의 빈을 찾아서 이걸로 서블릿 컨테이너를 띄운다. 
<hide/>
@MyAutoConfiguration
public class JettyWebServerConfig {
    @Bean("JettyWebServerFactory")
    public ServletWebServerFactory servletWebServerFactory() {
        return new JettyServletWebServerFactory();
    }
}

 

  • MyAutoConfigImportSelector
<hide/>

@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {

//        List<String> autoConfigs = new ArrayList<>();
//        ImportCandidates.load(MyAutoConfiguration.class, classLoader).forEach(
//            autoConfigs::add);
//        return autoConfigs.toArray(new String[0]);
    return new String[]{
        "tobyspring.config.autoconfig.TomcatWebServerConfig",
        "tobyspring.config.autoconfig.DispatcherServletConfig",
        "tobyspring.config.autoconfig.JettyWebServerConfig"
    };
}

 

  Note) 실행 결과

  • Caused by: org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : TomcatWebServerFactory,JettyWebServerFactory
  • 같은 타입의 중복된 bean이 존재하므로 부트를 시작할 수 없다. (우선 순위를 모르기 때문에 예외발생)
  • 어떻게 우선 순위를 걸 수 있을까?

 


@Conditional 과 Condition

  • @Conditional 
    • matches()를 오버라이드한다. 
      • true: 해당 빈 구성 정보를 사용한다. 
      • false: 해당 빈 구성 정보를 사용하지 않는다. 
    • ConditionContext: 현재 컨테이너와 스프링 컨테이너가 돌아가고 있는 환경에 대한 정보를 얻을 수 있다. 
    • AnnotatedTypeMetadata:  @Conditional을 메타 애너테이션으로 사용 중인 다른 애너테이션들의 정보를 이용할 수 있도록 그 애너테이션들의 메타데이터를 반환한다. 
    • true 반환: JettyConfig 에 붙은 @Conditional 에서는 condition() 이 항상 true를 반환하도록 한다. 
<hide/>
@MyAutoConfiguration
@Conditional(JettyWebServerConfig.JettyCondition.class)
public class JettyWebServerConfig {

    @Bean("jettyWebServerFactory")
    public ServletWebServerFactory servletWebServerFactory() {
        return new JettyServletWebServerFactory();
    }


    static class JettyCondition implements Condition{
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return true;
        }
    }
}

 

  • Tomcat 클래스는 반대로 false를 반환하도록 한다. 
    • 자동 구성 후보들을 불러와서 스프링 컨테이너에 빈으로 등록해달라고 한다. (메서드의 반환값에 따라 스프링 컨테이너가 결정한다.)
    • @Conditional 애너테이션을 참고해서 빈 등록 여부를 결정한다. 
<hide/>
@MyAutoConfiguration
@Conditional(TomcatWebServerConfig.TomcatCondition.class)
public class TomcatWebServerConfig {

    @Bean("TomcatWebServerFactory")
    public ServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }

    static class TomcatCondition implements Condition {

        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return false;
        }
    }
}

 

  Note) 실행 결과 - 정상적으로 부트가 뜬다. 

  • 반대로 true/false를 지정하고나서 부트를 띄우면 jetty가 아닌 tomcat이 뜬다. 
  • 만약 둘다 false이면? 빈을 등록하지 않았을 때와 같은 에러가 난다. 

 


apitest 실패

  • 오류
  • 원인
  • 해결 

  • @Conditional 도 메타 애너테이션으로 사용 가능하다. 
  • @Bean method 에도 동일하게 @Conditional을 붙일 수 있다 .

 


@Conditional 학습 테스트

 

  • 다음과 같은 테스트 클래스를 만들어서
    • getBean()을 통해 빈이 등록됐는지 아닌지 파악 가능하다. 
    • 테스트 목적의 애플리케이션 컨텍스트 구현: ApplicationContextRunner
<hide/>

public class ConditionalTest {


    @Test
    void conditional() {
        // true
        ApplicationContextRunner contextRunner = new ApplicationContextRunner();
        contextRunner.withUserConfiguration(Config1.class)
                .run(context -> {
                    assertThat(context).hasSingleBean(MyBean.class);
                    assertThat(context).hasSingleBean(Config1.class);
                });
        // false
        new ApplicationContextRunner().withUserConfiguration(Config2.class)
                .run(context -> {
                    assertThat(context).doesNotHaveBean(MyBean.class);
                    assertThat(context).doesNotHaveBean(Config2.class);
                });
    }


    @Configuration
    @Conditional(TrueCondition.class)
    static class Config1 {  //
        @Bean
        MyBean myBean() {
            return new MyBean();
        }
    }

    @Configuration
    @Conditional(FalseCondition.class)
    static class Config2 {      // 등록이 안되도록 컨디션 구성
        @Bean
        MyBean myBean() {
            return new MyBean();
        }
    }

    static class MyBean {
    }

    private static class TrueCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return true;
        }
    }

    private static class FalseCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return false;
        }
    }
}

 

 

  • "@BooleanConditional" 을 새로운 애너테이션으로 뺸다. 
<hide/>

public class ConditionalTest {

    @Test
    void conditional() {
        // true
        ApplicationContextRunner contextRunner = new ApplicationContextRunner();
        contextRunner.withUserConfiguration(Config1.class)
                .run(context -> {
                    assertThat(context).hasSingleBean(MyBean.class);
                    assertThat(context).hasSingleBean(Config1.class);
                });
        // false
        new ApplicationContextRunner().withUserConfiguration(Config2.class)
                .run(context -> {
                    assertThat(context).doesNotHaveBean(MyBean.class);
                    assertThat(context).doesNotHaveBean(Config2.class);
                });
    }

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

    @Configuration
    @BooleanConditional(true)
    static class Config1 {  //
        @Bean
        MyBean myBean() {
            return new MyBean();
        }
    }

    @Configuration
    @BooleanConditional(false)
    static class Config2 {      // 등록이 안되도록 컨디션 구성
        @Bean
        MyBean myBean() {
            return new MyBean();
        }
    }

    static class MyBean {}
    private static class BooleanCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            Map<String, Object> annotationAttribute  = metadata.getAnnotationAttributes(BooleanConditional.class.getName());
            Boolean value = (Boolean) annotationAttribute.get("value");
            return value;
        }
    }
}

 


커스텀 @Conditional

  • Tomcat 이라는 클래스의 패키지명: org.apache.catalina.startup; 
  • Server 클래스: org.eclipse.jetty.server;
  • ClassUtils : 클래스와 관련된 유틸 클래스이다. 
  • 기존의 톰캣, 제티 클래스의 오버라이드한 matches()의 반환 값을 다음과 같이 바꿔준다. 

 

  • Tomcat
<hide/>
@MyAutoConfiguration
@Conditional(TomcatWebServerConfig.TomcatCondition.class)
public class TomcatWebServerConfig {

    @Bean("TomcatWebServerFactory")
    public ServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }
    
    static class TomcatCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return ClassUtils.isPresent("org.apache.catalina.startup.Tomcat",
                    context.getClassLoader()
                    );
        }
    }
}

 

  • Jetty
<hide/>
@MyAutoConfiguration
@Conditional(JettyWebServerConfig.JettyCondition.class)
public class JettyWebServerConfig {

    @Bean("jettyWebServerFactory")
    public ServletWebServerFactory servletWebServerFactory() {
        return new JettyServletWebServerFactory();
    }

    static class JettyCondition implements Condition{
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return ClassUtils.isPresent("org.eclipse.jetty.server.Server",
                    context.getClassLoader()
            );
        }
    }
}

 

 Note) 실행 결과 - 에러

  • Caused by: org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans: TomcatWebServerFactory,jettyWebServerFactory
  • 여러 개의 서블릿 웹 서버 팩토리 빈이 존재하므로 부트를 시작할 수 없다.  

 

  • build.gradle
    • 다음과 같이 exclude, module 을 넣어준다. 
    • 아래와 같이 설정한 다음 스프링 부트를 돌리면 Jetty로 실행되어야한다. 
dependencies {
	implementation ('org.springframework.boot:spring-boot-starter-web'){
		exclude  group:  'org.springframework.boot',  module: 'spring-boot-starter-tomcat'
	}
	implementation 'org.springframework.boot:spring-boot-starter-jetty'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

 

 

  • 컨디셔널
    • conditional을 만들 때는 보통 ElementType.Method를 붙여준다. 
    •  
<hide/>
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(MyOnClassConditional.class)
public @interface ConditionalMyOnClass {
    String value();
}

 

  • MyOnClass
<hide/>
public class MyOnClassConditional implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalMyOnClass.class.getName());
        String value = attributes.get("value").toString();
        return ClassUtils.isPresent(value, context.getClassLoader());   // value에 해당하는  클래스가 존재하면 TRUE 없으면 FALSE
    }
}

 

 

  • 기존의 제티 클래스를 다음과 같이 바꾼다. 
    • 처음에 붙였던 @Conditional을 떼고 @ConditionalMyOnClass 를 붙인다. 
<hide/>
@MyAutoConfiguration
@ConditionalMyOnClass( "org.eclipse.jetty.server.Server")
public class JettyWebServerConfig {

    @Bean("jettyWebServerFactory")
    public ServletWebServerFactory servletWebServerFactory() {
        return new JettyServletWebServerFactory();
    }
}

 

  • 톰캣
<hide/>
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {

    @Bean("TomcatWebServerFactory")
    public ServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }
}

 

    • build 파일을 다음과 같이 설정해준 다음 
      • 부트를 띄우면 자동으로 톰캣이 들어간다. 
<hide/>
dependencies {
	implementation ('org.springframework.boot:spring-boot-starter-web')
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

  Note) 

  • @ConditionalMyOnClass
    • attribute에 클래스 이름을 집어넣는다. 
  • OnMyClassCondition
    • Condition 인터페이스를 구현한 클래스
    •  

 


자동 구성 정보 대체하기

  • 스프링 애플리케이션의 구성 정보를 설정하는 방법은 크게 두 가지가 있다. 
  • 1) 사용자 구성 정보(ComponentScan): 
  • 2) 자동 구성 정보(AutoConfiguration):
    • MyOnClassCondition이라는 컨디션 클래스를 통해서 지정한 클래스가 존재하는지 체크해보고  

 

  •  다음과 같이 띄우면?
    • 에러가 난다. 
    • 같은 종류의 빈이 존재하기 때문이다.
    • Caused by: org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : customWebServerFactory,TomcatWebServerFactory
    • 그러면 여러 개의 빈이 있는 경우, 특정 빈을 선택하는 방법은?
<hide/>
@Configuration(proxyBeanMethods = false)
public class WebServerConfiguration {

    @Bean
    ServletWebServerFactory customWebServerFactory(){
        TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        serverFactory.setPort(9090);
        return serverFactory;
    }
}

 

 

  • 기존에 @Conditional을 클래스에 붙여왔지만 메서드에도 붙일 수 있다. 
    • 메서드에 붙이는 경우, 해당 메서드의 return type 형태와 같은 빈이 존재하지 않는 경우에 대해서만 해당 메서드를 빈으로 등록한다. 

 

  • @ConditionalOnMissingBean
    • 다음과 같이 톰캣 설정 클래스에 애너테이션을 적용하고나서야 부트가 돌아간다. 
    • 해당 구성 정보를 만들어두었는지 확인해보고 아닌 경우에만 해당 빈을 등록한다. 
<hide/>
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {

    @Bean("TomcatWebServerFactory")
    @ConditionalOnMissingBean
    public ServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }
}

 

 

 Note)

  • 사용자 구성 정보 쪽에  CustomWebServerFactory  빈을 만들면?
  • 자동 구성 정보 쪽에는 TomcatWebServerConfig
  • 클래스 레벨에서 MyOnClassCondition을 이용해서 톰캣 클래스가 있는지 체크하고 해당 클래스를 빈으로 등록한다. 
    • 그 안에 있는 @Bean 메서드를 하나씩 체크한다. 

 


스프링 부트의 @Conditional

  • @Conditional 스프링 부트 4.XX 버전부터 사용 가능하다. 
  • @Profile도 @Conditional 기능을 한다. 

 

class conditions

  • @ConditionalOnClass
  • @ConditionalOnMissingClass
  • 지정한 클래스의 프로젝트 내 존재를 확인해서 포함 여부를 결정한다. 
  • 주로 @Configuration 클래스 레벨에서 사용하지만 @Bean 메서드에서 적용 가능하다. 
  • 단 클래스 레벨의 검증 없이 @Bean 메서드에만 적용하면 불필요하게 @Configuration 클래스가 빈으로 등록되므로 클래스 레벨 사용을 우선해야한다. 

 

bean conditions

  • @ConditionalOnClass
  • @ConditionalOnMissingClass
  • 빈의 존재 여부를 기준으로 포함 여부를 결정한다. 빈의 타입 또는 이름을 지정할 수 있다. 
  • 지정된 빈 정보가 없으면 메서드의 리턴 타입을 기준으로 빈의 존재 여부를 체크한다. 
  • 컨테이너에 등록된 빈 정보를 기분으로 체크하기 때문에 자동 구성 사이에 적용하려면 @Configuration 클래스의 적용 순서가 중요하다. 개발자가 직접 정의한 커스톰 빈 구성 정보가 자동 구성 정보 처리보다 우선하므로 이 관계에 적용하는 것이 안전하다. 반대로 커스텀 빈 구성 정보에 적용하는 것 피해야한다. 

 

property conditions

  • @ConditionalOnProperty는 스프링의 환경 프로퍼티 정보를 이용한다. 
  • 지정된 프로퍼티가 존재하고 fasle가 아니면 포함 대상이 된다. 특정 값을 가진 경우를 확인하거나 프로퍼티가 존재하지 않을 때 조건을 만족하게 할 수도 있다. 
  • 프로퍼티 존재를 확인해서 빈 오브젝트를 추가하고 해당 빈 오브젝트에서 프로퍼티 값을 이용해서 세밀하게 빈 구성을 할 수도 있다. 

 

resource conditions

  • @ConditionalOnResource는 지정된 리소스(파일)의 존재를 확인하는 조건이다. 

 

web application conditions

  • @ConditionalOnWebApplication
  • @ConditionalOnNotWebApplication
  • 웹 애플리케이션 여부를 확인한다. 모든 스프링 부트 프로젝트가 웹 기술을 사용해야하는 것은 아니다. 

 

SpEL expression conditions

  • @ConditionalOnExpression은 스프링 SpEL(스프링 표현식)의 처리 결과를 기준으로 판단한다. 상세한 조건 설정이 가능하다. 

 


 

Environment 추상화와 프로퍼티

  • content
  • content

 

 

 


title

  • content
  • content

title

  • content
  • content

title

  • content
  • content

 

 

출처 - 인프런 토비의 스프링

https://www.inflearn.com/course/%ED%86%A0%EB%B9%84-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%9D%B4%ED%95%B4%EC%99%80%EC%9B%90%EB%A6%AC/dashboard