Spring

[스프링부트] 조건부 자동 구성

hongyb 2024. 4. 19. 01:13

조건부 자동 구성

스프링 부트에서 조건에 따라 클래스를 빈 컨테이너에 등록할지 말지 선택할 수 있다. Jetty 서버를 구성에 추가한 후 조건에 따라 Jetty 서버가 실행될지, Tomcat 서버가 실행될지 스프링 부트가 선택하는 예제를 구현해 보며 조건부 자동 구성을 알아보겠다.

 

Jetty 서버와 Tomcat 서버 설정

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

처음 Spring initializer에서 web 모듈 선택 후 build.gradle을 살펴보면 위와 같이 의존 관계가 설정되어 있는 것을 확인할 수 있다. 'spring-boot-starter'에서 SpringWeb, SpringMVC, Json, Tomcat 라이브러리가 자동으로 추가된다. 'spring-boot-starter'에서 Tomcat만 예외로 설정하고 Jetty 서버를 추가하고 싶으면 아래와 같이 build.gradle을 수정하면 된다.

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'
}

exclude group에서 tomcat module을 제외한 후 새로 jetty 서버의 의존 관계를 설정했다.

 

@Conditional과 Condition

@Conditional 어노테이션은 빈이나 구성 클래스가 조건에 따라 활성화되거나 비활성화되도록 지정할 때 사용된다.

@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();

}

 

Class<? extends Condition>[] value(); 에서 Condition에 주목해야 한다. Condition 인터페이스는 구체적인 조건을 정의하기 위해 사용된다. 이 인터페이스를 구현하여 조건을 만족하는지 여부를 결정하는 로직을 작성할 수 있다. Condition 인터페이스의 matches() 메서드를 구현하여 조건을 검사하고, 조건을 충족하면 true를 반환하고 그렇지 않으면 false를 반환한다.

@FunctionalInterface
public interface Condition {

	/**
	 * Determine if the condition matches.
	 * @param context the condition context
	 * @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
	 * or {@link org.springframework.core.type.MethodMetadata method} being checked
	 * @return {@code true} if the condition matches and the component can be registered,
	 * or {@code false} to veto the annotated component's registration
	 */
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

다음 예제를 보며 어떻게 사용되는지 알아보자.

 

클래스의 존재에 따른 조건부 구성

클래스의 존재에 따라 빈에 등록할지 말지 설정하는 예제를 진행하겠다. 다시 말해 어떤 기술의 클래스가 애플리케이션에 존재한다면, 이 클래스를 자동 구성 클래스에 등록할 것이다. 반대로 클래스에 존재하지 않으면 자동 구성 클래스에 등록되지 않도록 설정할 것이다.

 

Tomcat과 Jetty 중 어떤 서블릿 컨테이너를 사용할지를 해당 서버 라이브러리 클래스가 프로젝트에 포함되어 있는지를 기준으로 조건을 구성할 것이다.

 

우선 Condition 인터페이스를 구현한 MyOnClassCondtion이라는 객체와 @ConditionalMyOnClass를 만들자.

public class MyOnClassCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalMyOnClass.class.getName());
        String value = (String)attributes.get("value");
        return ClassUtils.isPresent(value, context.getClassLoader());
    }
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(MyOnClassCondition.class)
public @interface ConditionalMyOnClass {
    String value();
}

 

위에 코드를 살펴보면 metadata.getAnnotationAttributes(ConditionalMyOnClass.class.getName()); 를 통해 @ConditionalMyOnClass 애노테이션을 달은 클래스의 정보를 모두 가져올 수 있다. 그 후 @ConditionalMyOnClass의 value 값을 가져온다. 이때 value 값은 class의 이름을 String으로 나타낸 것이다. ClassUtils.isPresent() 메서드는 특정 클래스가 현재 프로젝트에 포함되어 있는지 알려주고 있다. value 값이 프로젝트에 있으면 true, 없으면 false를 return 한다.

 

따라서 조건에 따라 구성 정보를 설정하고 싶은 클래스인 Jetty와 Tomcat 설정 클래스는 다음과 같이 작성하면 된다.

@MyAutoConfiguration
@ConditionalMyOnClass("org.eclipse.jetty.server.Server")
public class JettyWebServerConfig {
    @Bean(name = "jettyWebserverFactory")
    @ConditionalOnMissingBean
    public ServletWebServerFactory serverFactory() {
        return new JettyServletWebServerFactory();
    }
}
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {
    @Bean
    @ConditionalOnMissingBean
    public ServletWebServerFactory serverFactory() {
        return new TomcatServletWebServerFactory();
    }
}

위 코드에서 주목할 부분은 @ConditionalMyOnClass("org.apache.catalina.startup.Tomcat") 애노테이션이다. @ConditionalMyOnClass() 애노테이션에 value 값으로 들어간 클래스가 현재 프로젝트에 있으면 해당 클래스를 빈 컨테이너에 등록하고 프로젝트에 없으면 등록하지 않는다.

 

전체 프로젝트 구조는 다음과 같다.

 

스프링 부트의 @Conditional

스프링 부트는 다양한 종류의 @Conditional 애노테이션을 제공한다. 다음 대표적으로 사용되는 애노테이션이다.

 

@ConditionalOnClass

지정된 클래스나 클래스들이 클래스패스에 존재할 때만 빈을 조건부로 생성한다. 지정된 클래스나 클래스들이 클래스패스에 없는 경우, 해당 애노테이션이 붙은 빈은 스프링 애플리케이션 컨텍스트에 등록되지 않는다.

    // 'org.springframework.web.client.RestTemplate' 클래스가 존재할 때만 빈을 생성.
    @Bean
    @ConditionalOnClass(RestTemplate.class)
    public MyService myService() {
        return new MyService();
    }

 

 

@ConditionalOnMissingClass

지정된 클래스나 클래스들이 클래스패스에 없을 때만 빈을 생성합니다. 지정된 클래스나 클래스들이 클래스패스에 존재하는 경우, 해당 애노테이션이 붙은 빈은 등록되지 않는다.

    // 'com.example.MyCustomClass' 클래스가 존재하지 않을 때만 빈을 생성.
    @Bean
    @ConditionalOnMissingClass("com.example.MyCustomClass")
    public AnotherService anotherService() {
        return new AnotherService();
    }

 

@ConditionalOnBean

지정된 빈이 이미 스프링 애플리케이션 컨텍스트에 존재할 때만 빈을 조건부로 생성한다. 지정된 빈이 없는 경우에는 해당 애노테이션이 붙은 빈은 등록되지 않는다.

    // 'DataSource' 타입의 빈이 존재할 때만 빈을 생성.
    @Bean
    @ConditionalOnBean(DataSource.class)
    public DataService dataService(DataSource dataSource) {
        return new DataService(dataSource);
    }

 

 

@ConditionalOnMissingBean

지정된 빈이 이미 스프링 애플리케이션 컨텍스트에 존재하지 않을 때만 빈을 생성한다. 지정된 빈이 이미 존재하는 경우에는 해당 애노테이션이 붙은 빈은 등록되지 않는다.

    // 'MyRepository' 타입의 빈이 존재하지 않을 때만 빈을 생성.
    @Bean
    @ConditionalOnMissingBean(MyRepository.class)
    public DefaultRepository defaultRepository() {
        return new DefaultRepository();
    }

 

결론

 

위 그림은 구성 정보를 설정하는 과정을 보여주고 있다. 스프링 부트는 사용자를 대신해 모든 인프라 스트럭처 빈의 configuration class를 미리 생성해 놓았다. 스프링 부트가 만든 configuration class만 약 150개 정도가 되고 그 목록들이 .imports로 끝나는 파일에 저장되어 있다. 모든 configuration을 등록하는 것이 아닌 Conditional에서 조건을 체크 후 최종 구성 정보를 적용하는 과정을 거친다.

 

구성 정보가 사용자가 등록한 것이 있다면 AutoConfiguration보다 우선 순위를 갖게 된다. 사용자가 따로 설정한 구성 정보가 없다면 AutoConfiguration 정보가 등록되게 된다.