💡이 포스팅은 토비님의 인프런 강의인 토비의 스프링 부트 - 이해와 원리를 수강하고 학습한 내용을 정리한 포스팅입니다.
토비님의 강의를 수강하며 정리한 GitHub Repository입니다.
이번 챕터에 대해 정리할 내용은 다음과 같습니다.
- @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에 대해 정리한 아래 포스팅도 참고해 보면 좋을 것 같습니다.
IntelliJ에서 Class 계층 구조 확인하기
토비님 강의를 보다 보면 Class의 계층 구조를 IntelliJ에서 쉽게 확인하는 것을 보았는데, 한 번도 사용해보진 않았어서 한번 정리해 보겠습니다.
IntelliJ에는 Class의 계층 구조, Interface의 구현체 목록들을 Icon을 통해 쉽게 제공하고 있습니다.
위와 같이 해당 Class, Interface 왼쪽에 있는 Icon을 클릭하면 쉽게 계층 구조를 확인할 수 있습니다.
보다 더 상세하게 계층 구조를 확인하고 싶은 경우 해당 Class, Interface에서 Ctrl + H을 입력하면 아래와 같이 상세한 계층 구조를 확인할 수 있습니다.
'Lecture > 토비의 스프링 부트 - 이해와 원리' 카테고리의 다른 글
토비의 스프링 부트 - Spring JDBC 자동 구성 개발 (0) | 2023.02.22 |
---|---|
토비의 스프링 부트 - 외부 설정을 이용한 자동 구성 (1) | 2023.02.18 |
토비의 스프링 부트 - 자동 구성 기반 애플리케이션 (0) | 2023.02.12 |
토비의 스프링 부트 - DI와 테스트, 디자인 패턴 (0) | 2023.02.08 |
토비의 스프링 부트 - 독립 실행형 스프링 애플리케이션 (1) | 2023.02.05 |
댓글