본문 바로가기
Lecture/토비의 스프링 부트 - 이해와 원리

토비의 스프링 부트 - 조건부 자동 구성

by Soono991 2023. 2. 18.

💡이 포스팅은 토비님의 인프런 강의인 토비의 스프링 부트 - 이해와 원리를 수강하고 학습한 내용을 정리한 포스팅입니다.

 

토비님의 강의를 수강하며 정리한 GitHub Repository입니다.

 

GitHub - kiekk/inflearn-toby-spring-boot

Contribute to kiekk/inflearn-toby-spring-boot development by creating an account on GitHub.

github.com

 

이번 챕터에 대해 정리할 내용은 다음과 같습니다.

  • @Conditional, @Profile
  • @ConditionalOn*
  • IntelliJ에서 Class 계층 구조 확인하기

이번 포스팅과 관련해서는 아래 영상도 같이 참고하시면 좋을 것 같습니다.

https://www.youtube.com/watch?v=ssT24xB9UTc&t=0s 

 

@Conditional, @Profile

@Conditional 애노테이션은 Spring 4.0부터 도입된 애노테이션으로 특정 조건에 따라 Bean을 등록할지 말지 결정하는 애노테이션으로 value에 Condition Interface의 구현체를 전달하면 됩니다.

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

}

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

}

Condition Interface는 matches() 메서드를 구현하면 되는데, 이 메서드에 Bean 등록 조건을 작성하면 됩니다.

강의에서는 해당 클래스가 Bean으로 등록되어야 하는 조건을 다른 곳에서 사용할 필요가 없기 때문에 정적 내부 클래스로 만들어 사용했었습니다.

 

@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;
        }
    }
}

 

실제 Tomcat 관련 설정을 보면 @Conditional 대신 뒤에 설명할 @ConditionalOnClass 애노테이션을 사용한 것을 확인할 수 있습니다.

/**
 * Nested configuration if Tomcat is being used.
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
public static class TomcatWebServerFactoryCustomizerConfiguration {

    @Bean
    public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
            ServerProperties serverProperties) {
        return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
    }

}

그리고 지난번 포스팅에 정리했던 @Configuration 애노테이션의 proxyBeanMethods 속성을 false(기본값 true)로 설정하면 @Configuration 애노테이션을 사용한 클래스의 proxy가 생성되지 않습니다.

 

그렇게 되면 @Configuration의 특수한 기능인 Bean을 싱글톤으로 생성하는 기능을 지원받을 수 없기 때문에 매번 새로운 객체가 생성될 것입니다.

 

위의 경우에는 Tomcat 설정을 딱 한 번만 사용하기 때문에 굳이 싱글톤으로 관리할 필요가 없어지게 되므로 proxyBeanMethods 속성을 false로 주어 불필요한 객체 생성 (@Configuration에 의해 생성된 proxy)을 방지할 수 있습니다.

 

 

@Profile 애노테이션도 설정된 profile에 의해 Bean이 등록될지 말지를 결정하는 애노테이션인데, 내부를 살펴보면 @Profile 애노테이션도 @Conditional 애노테이션을 사용하는 것을 확인할 수 있습니다.

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

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

}

/**
 * {@link Condition} that matches based on the value of a {@link Profile @Profile}
 * annotation.
 *
 * @author Chris Beams
 * @author Phillip Webb
 * @author Juergen Hoeller
 * @since 4.0
 */
class ProfileCondition implements Condition {

	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
		if (attrs != null) {
			for (Object value : attrs.get("value")) {
				if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
					return true;
				}
			}
			return false;
		}
		return true;
	}

}

@Profile 내부에 @Conditonal 애노테이션으로 ProfileConditon 클래스에 작성된 조건에 의해 Bean 등록 여부를 결정하는데 ProfileConditon 클래스를 확인해 보면 프로젝트 실행 시 전달된 profile 값들 중 @Profile 애노테이션에 작성된 profile의 여부에 따라 Bean을 등록하는 것을 확인할 수 있습니다.

 

@ConditionalOn*

Spring Boot는 @Conditional 애노테이션을 확장한 여러 애노테이션을 제공하고 있습니다.

 

Class Conditions

  • @ConditionalOnClass
  • @ConditionalOnMissingClass
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {

	/**
	 * The classes that must be present. Since this annotation is parsed by loading class
	 * bytecode, it is safe to specify classes here that may ultimately not be on the
	 * classpath, only if this annotation is directly on the affected component and
	 * <b>not</b> if this annotation is used as a composed, meta-annotation. In order to
	 * use this annotation as a meta-annotation, only use the {@link #name} attribute.
	 * @return the classes that must be present
	 */
	Class<?>[] value() default {};

	/**
	 * The classes names that must be present.
	 * @return the class names that must be present.
	 */
	String[] name() default {};

}

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

	/**
	 * The names of the classes that must not be present.
	 * @return the names of the classes that must not be present
	 */
	String[] value() default {};

}

@ConditionalOnClass, @ConditionalOnMissingClass 애노테이션에 작성한 클래스가 프로젝트 안에 있을 경우 Bean으로 등록되며, Bean 등록 로직을 OnClassCondition 클래스가 담당합니다.

 

Bean Conditions

  • @ConditionalOnBean
  • @ConditionalOnMissingBean
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {

	/**
	 * The class types of beans that should be checked. The condition matches when beans
	 * of all classes specified are contained in the {@link BeanFactory}.
	 * @return the class types of beans to check
	 */
	Class<?>[] value() default {};

	/**
	 * The class type names of beans that should be checked. The condition matches when
	 * beans of all classes specified are contained in the {@link BeanFactory}.
	 * @return the class type names of beans to check
	 */
	String[] type() default {};

	/**
	 * The annotation type decorating a bean that should be checked. The condition matches
	 * when all the annotations specified are defined on beans in the {@link BeanFactory}.
	 * @return the class-level annotation types to check
	 */
	Class<? extends Annotation>[] annotation() default {};

	/**
	 * The names of beans to check. The condition matches when all the bean names
	 * specified are contained in the {@link BeanFactory}.
	 * @return the names of beans to check
	 */
	String[] name() default {};

	/**
	 * Strategy to decide if the application context hierarchy (parent contexts) should be
	 * considered.
	 * @return the search strategy
	 */
	SearchStrategy search() default SearchStrategy.ALL;

	/**
	 * Additional classes that may contain the specified bean types within their generic
	 * parameters. For example, an annotation declaring {@code value=Name.class} and
	 * {@code parameterizedContainer=NameRegistration.class} would detect both
	 * {@code Name} and {@code NameRegistration<Name>}.
	 * @return the container types
	 * @since 2.1.0
	 */
	Class<?>[] parameterizedContainer() default {};

}

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

	/**
	 * The class types of beans that should be checked. The condition matches when no bean
	 * of each class specified is contained in the {@link BeanFactory}.
	 * @return the class types of beans to check
	 */
	Class<?>[] value() default {};

	/**
	 * The class type names of beans that should be checked. The condition matches when no
	 * bean of each class specified is contained in the {@link BeanFactory}.
	 * @return the class type names of beans to check
	 */
	String[] type() default {};

	/**
	 * The class types of beans that should be ignored when identifying matching beans.
	 * @return the class types of beans to ignore
	 * @since 1.2.5
	 */
	Class<?>[] ignored() default {};

	/**
	 * The class type names of beans that should be ignored when identifying matching
	 * beans.
	 * @return the class type names of beans to ignore
	 * @since 1.2.5
	 */
	String[] ignoredType() default {};

	/**
	 * The annotation type decorating a bean that should be checked. The condition matches
	 * when each annotation specified is missing from all beans in the
	 * {@link BeanFactory}.
	 * @return the class-level annotation types to check
	 */
	Class<? extends Annotation>[] annotation() default {};

	/**
	 * The names of beans to check. The condition matches when each bean name specified is
	 * missing in the {@link BeanFactory}.
	 * @return the names of beans to check
	 */
	String[] name() default {};

	/**
	 * Strategy to decide if the application context hierarchy (parent contexts) should be
	 * considered.
	 * @return the search strategy
	 */
	SearchStrategy search() default SearchStrategy.ALL;

	/**
	 * Additional classes that may contain the specified bean types within their generic
	 * parameters. For example, an annotation declaring {@code value=Name.class} and
	 * {@code parameterizedContainer=NameRegistration.class} would detect both
	 * {@code Name} and {@code NameRegistration<Name>}.
	 * @return the container types
	 * @since 2.1.0
	 */
	Class<?>[] parameterizedContainer() default {};

}

@ConditionalOnBean, @ConditionalOnMissingBean 애노테이션에 작성한 Bean의 존재 여부를 통해 Bean 등록 여부를 결정합니다.

이때 Bean 등록 로직을 OnBeanCondition 클래스가 담당합니다.

 

💡위 애노테이션을 사용할 경우 컨테이너에 등록된 빈 정보를 기준으로 체크하기 때문에 @Configuration의 적용 순서가 매우 중요합니다.

💡 만약 커스텀 빈 정보를 등록할 때 @ConditionOnBean, @ConditionOnMissingBean 애노테이션을 사용하면 Spring Boot Auto Configuration 보다 먼저 동작하기 때문에 오류가 발생할 수 있습니다.

 

Property Conditions

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

	/**
	 * Alias for {@link #name()}.
	 * @return the names
	 */
	String[] value() default {};

	/**
	 * A prefix that should be applied to each property. The prefix automatically ends
	 * with a dot if not specified. A valid prefix is defined by one or more words
	 * separated with dots (e.g. {@code "acme.system.feature"}).
	 * @return the prefix
	 */
	String prefix() default "";

	/**
	 * The name of the properties to test. If a prefix has been defined, it is applied to
	 * compute the full key of each property. For instance if the prefix is
	 * {@code app.config} and one value is {@code my-value}, the full key would be
	 * {@code app.config.my-value}
	 * <p>
	 * Use the dashed notation to specify each property, that is all lower case with a "-"
	 * to separate words (e.g. {@code my-long-property}).
	 * @return the names
	 */
	String[] name() default {};

	/**
	 * The string representation of the expected value for the properties. If not
	 * specified, the property must <strong>not</strong> be equal to {@code false}.
	 * @return the expected value
	 */
	String havingValue() default "";

	/**
	 * Specify if the condition should match if the property is not set. Defaults to
	 * {@code false}.
	 * @return if the condition should match if the property is missing
	 */
	boolean matchIfMissing() default false;

}

@ConditionalOnProperty 애노테이션은 작성한 프로퍼티가 Spring의 Environment에 있는지 존재 여부를 통해 Bean 등록 여부를 결정합니다.

이때 Bean 등록 로직을 OnPropertyCondition 클래스가 담당합니다.

 

  • name: property key를 의미합니다.
  • prefix: property key의 접두사를 의미합니다.
  • havingValue: 작성한 key의 value가 맞는지 확인합니다.
  • matchIfMissing: 조건에 맞는 속성이 없을 경우 Bean 생성 여부를 결정합니다. (기본 값: false)

 

 

Resource Conditions

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

	/**
	 * The resources that must be present.
	 * @return the resource paths that must be present.
	 */
	String[] resources() default {};

}

@ConditionalOnResource 애노테이션에 작성한 리소스의 존재 여부를 통해 Bean 등록 여부를 결정합니다.

이때 Bean 등록 로직을 OnResourceConditions 클래스가 담당합니다.

 

Web Application Conditions

  • @ConditionalOnWebApplication
  • @ConditionalOnNotWebApplication
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnWebApplicationCondition.class)
public @interface ConditionalOnWebApplication {

	/**
	 * The required type of the web application.
	 * @return the required web application type
	 */
	Type type() default Type.ANY;

	/**
	 * Available application types.
	 */
	enum Type {

		/**
		 * Any web application will match.
		 */
		ANY,

		/**
		 * Only servlet-based web application will match.
		 */
		SERVLET,

		/**
		 * Only reactive-based web application will match.
		 */
		REACTIVE

	}

}

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

}

@ConditionalOnWebApplication, @ConditionalOnNotWebApplication 애노테이션은 해당 프로젝트가 웹 애플리케이션인지의 여부에 따라 Bean 등록 여부를 결정합니다.

이때 Bean 등록 로직을 OnWebApplicationCondition 클래스가 담당합니다.

 

War Deployment Conditions

  • @ConditionalWarDeployment
/**
 * {@link Conditional @Conditional} that matches when the application is a traditional WAR
 * deployment. For applications with embedded servers, this condition will return false.
 *
 * @author Madhura Bhave
 * @since 2.3.0
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnWarDeploymentCondition.class)
public @interface ConditionalOnWarDeployment {

}

class OnWarDeploymentCondition extends SpringBootCondition {

	@Override
	public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
		ResourceLoader resourceLoader = context.getResourceLoader();
		if (resourceLoader instanceof WebApplicationContext applicationContext) {
			ServletContext servletContext = applicationContext.getServletContext();
			if (servletContext != null) {
				return ConditionOutcome.match("Application is deployed as a WAR file.");
			}
		}
		return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnWarDeployment.class)
				.because("the application is not deployed as a WAR file."));
	}

}

이 애노테이션은 프로젝트를 War로 배포할 경우 Bean으로 등록합니다.

이때 Bean 등록 로직을 OnWarDeploymentCondition 클래스가 담당합니다.

 

SpEL Expression Conditions

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

	/**
	 * The SpEL expression to evaluate. Expression should return {@code true} if the
	 * condition passes or {@code false} if it fails.
	 * @return the SpEL expression
	 */
	String value() default "true";

}

스프링 SpEL의 처리 결과를 기준으로 Bean 등록 여부를 결정합니다.

이때 Bean 등록 로직을 OnExpressionCondition 클래스가 담당합니다.

 

@Conditional Annotation에 대해 정리한 아래 포스팅도 참고해 보면 좋을 것 같습니다.

 

Spring Conditional annotation

스프링의 @Conditional에 대해 알아보자.

circlee7.medium.com

 

IntelliJ에서 Class 계층 구조 확인하기

토비님 강의를 보다 보면 Class의 계층 구조를 IntelliJ에서 쉽게 확인하는 것을 보았는데, 한 번도 사용해보진 않았어서 한번 정리해 보겠습니다.

 

IntelliJ에는 Class의 계층 구조, Interface의 구현체 목록들을 Icon을 통해 쉽게 제공하고 있습니다.

위와 같이 해당 Class, Interface 왼쪽에 있는 Icon을 클릭하면 쉽게 계층 구조를 확인할 수 있습니다.

보다 더 상세하게 계층 구조를 확인하고 싶은 경우 해당 Class, Interface에서 Ctrl + H을 입력하면 아래와 같이 상세한 계층 구조를 확인할 수 있습니다.

댓글