스프링 부트의 자동 환경설정(Auto Configuration)

스프링 부트의 자동 환경설정

기존에 많이 사용하는 스프링의 경우 MVC, AOP, JPA, JDBC 등 웹 어플리케이션 동작에 필요한 설정을 수동으로 진행해야했다. 새로운 프로젝트를 진행하면 이와 같은 설정을 다시 작성해야하는 불필요한 시간을 소비했다. 스프링 부트는 WEB, JPA, AOP등 다양한 설정을 자동으로 적용하도록 한다.

스프링과 스프링부트의 설정은 어떻게 다를까
먼저 과거 스프링의 설정 방식을 살펴볼 필요가 있다.

1
2
3
4
...
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSurffix(".jsp");
...

기존 스프링의 ViewResolver 설정은 이처럼 작성을 해줘야했다.

1
2
3
4
5
spring:
mvc:
view:
prefix: /WEB-INF/views/
surffix: .jsp

스프링 부트는 YAML이나 Properties 파일을 통해 간단히 설정이 가능하다.(물론 Web Starter가 의존성 추가되어야 한다) 이로 인해 가질 수 있는 장점은 가독성과 반복된 설정이 줄어들 수 있다.

1
2
3
4
5
6
7
8
9
@RestController
@SpringBootApplication
public class DemoWebApplication {

public static void main(String[] args) {
SpringApplication.run(DemoWebApplication.class, args);
}
...
}

스프링 부트의 자동 환경설정 동작 방식을 알아보고자 한다.
위 클래스는 스프링 부트를 기동하는 클래스이다. @SpringBootApplication은 스프링 부트를 실행하기 위한 필수 어노테이션이다.
단순히 실행을 위한 어노테이션이라 생각할 수 있지만 그 내부에는 자동설정을 위한 어노테이션을 포함하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM,
classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}

스프링 부트 Reference에는 자동설정을 위해서 @Configuration 클래스 내 @EnableAutoConfiguration 애노테이션을 추가하라고 설명하고 있다.(SpringBootConfiguration 어노테이션이 Configuration 어노테이션을 포함하고 있다.) 결론적으로 SpringBootApplication 어노테이션은 자동설정 사용을 위한 어노테이션까지 함께 담고 있다.

1
2
3
4
5
6
7
8
9
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}

EnableAutoConfiguration은 AutoConfigurationImportSelector를 Import하고 있다. AutoConfigurationImportSelector는 자동 설정 기능을 담당한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class AutoConfigurationImportSelector
implements DeferredImportSelector, ... {
...

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

...

protected AutoConfigurationEntry getAutoConfigurationEntry(
AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
...

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
}

AutoConfigurationImportSelector는 selectImports 메소드를 통해 @Configuration 어노테이션이 설정된 클래스명을 로드한다. getAutoConfigurationEntry 메소드는 내부적으로 getCandidateConfigurations 메소드를 통해 spring.factories를 로드하여 설정파일에서 사용할 목록을 생성한다.(SpringFactoriesLoader class 참고)

removeDuplicates 메소드를 통해 중복된 설정과 getExclusions 메소드를 통해 제외할 설정을 모아 삭제처리 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

spring.factorie는 이와 같이 key&value 형태로 클래스 목록을 담고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"name": "spring.redis.pool.max-wait",
"type": "java.lang.Integer",
"description": "Maximum amount of time (in milliseconds) a connection allocation should block\n before throwing an exception when the pool is exhausted. Use a negative value\n to block indefinitely.",
"defaultValue": -1,
"deprecation": {
"replacement": "spring.redis.jedis.pool.max-wait",
"level": "error"
}
},
{
"name": "spring.redis.pool.min-idle",
"type": "java.lang.Integer",
"description": "Target for the minimum number of idle connections to maintain in the pool. This\n setting only has an effect if it is positive.",
"defaultValue": 0,
"deprecation": {
"replacement": "spring.redis.jedis.pool.min-idle",
"level": "error"
}
},

해당 파일 경로 내 함께 위치한 spring-configuration-metadata.json은 자동 설정 클래스의 프로퍼티에 대한 정보를 제공하는 정의 파일이다.

그렇다면 이 모든 클래스를 자동으로 설정에 추가하는 것일까?
그건 너무 시스템에 있어 비효율적인 방법이다.
스프링 부트는 이러한 경우를 @Conditional 어노테이션을 이용하여 설정한다.

1
2
3
4
5
6
7
8
9
@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
...
}

해당 클래스는 레디스 설정을 위한 설정 클래스이다. @ConditionalOnClass가 선언 되어있다.
이는 해당 클래스가 프로젝트의 클래스 경로 상에 존재하는 경우를 의미한다.
해당 조건을 만족한다면 해당 설정이 Bean으로 등록 되는 것이다.

스프링 부트는 이와 같은 어노테이션을 이용해 자동 설정의 적용 조건, 시점 등을 관리한다.

  • 순서 조건
    @AutoConfigureAfter, @AutoConfigureBefore는 특정 자동설정 클래스가 적용된 전 후에 대한 적용 조건이다.

  • 클래스 조건
    @ConditionalOnClass과 ConditionalOnMissingClass은 특정 클래스가 클래스 경로에 존재하거나 존재하지 않는 경우에 대한 적용조건이다.

  • Bean 조건
    @ConditionalOnBean 과 @ConditionalOnMissingBean는 특정 빈이 존재하거나 존재하지 않는 경우에 대한 적용조건이다.

  • 리소스 조건
    @ConditionalOnResource은 특정 리소스가 존재하는 경우에만 적용되는 조건이다.

  • 웹 어플리케이션 조건
    @ConditionalOnWebApplication 과 @ConditionalOnNotWebApplication은 어플리케이션이 웹 어플리케이션인지 아닌지에 대한 적용조건이다. 스프링 WebApplicationContext를 사용하고, session 스코프를 정의하거나 StandardServletEnvironment를 가지고 있는 어플리케이션은 웹 어플리케이션으로 본다.

  • 프로퍼티 조건
    @ConditionalOnProperty는 환경의 특성에 따라 설정할 수 있도록 도와준다. prefix와 name 속성을 이용하여 프로퍼티명을 명시한다. havingValue 속성을 통해 값에 대한 조건, matchIfMissing을 통해 존재하지 않는 경우 기본 값을 지정한다.

  • JAVA 조건
    @ConditionalOnJava는 Java 버전에 따라 적용되는 조건이다.

  • SpEL 조건
    @ConditionalOnExpression은 SpEL을 이용한 true/false에 따라 적용되는 조건이다.

  • JNDI 조건
    @ConditionalOnJndi는 JNDI lookup 가능 여부에 따른 적용 조건이다.

스프링 환경 특성(|프로퍼티)에 따라 설정을 할 수 있도록 해준다. prefix와name속성으로 체크해야할 특성을 명시한다. 존재하고false가 아닌 특성은 기본적으로(|설정 없이도|자동으로) 매치가 될 것이다. 상세하게 체크하고 싶다면 havingValue와matchIfMssing` 속성을 사용하면 된다.

즉 위에서 첨부된 레디스 캐시의 적용 조건을 해석해보자면 이와 같다.

  • @ConditionalOnClass(RedisConnectionFactory.class) : 해당 클래스가 존재하는 경우 적용
  • @AutoConfigureAfter(RedisAutoConfiguration.class) : 해당 자동적용 조건 클래스가 적용된 이후에 적용
  • @ConditionalOnBean(RedisConnectionFactory.class) : 해당 빈이 등록 된 경우에 적용
  • @ConditionalOnMissingBean(CacheManager.class) : 해당 빈이 적용되지 않은 경우에 적용

자동 설정된 값을 변경해야하는 경우는 Properties나 YAML 파일 내에 아래 링크를 참고하여 수정한다.

https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html

Share