[처음 배우는 스프링부트2] 자동 환경 설정 이해하기
[본 내용은 김영제님의 저서 '처음 배우는 스프링 부트 2'를 기반으로 작성되었음을 미리 알립니다.]
스프링의 가장 강력한 기능 중 하나인 '자동 환경 설정'은 우리가 프레임워크를 사용함에 있어 보다 편리한 환경을 제공합니다.
각종 라이브러리들이 스프링의 자동설정 의존성에 따라 설정이 자동으로 적용되며,
@EnableAutoConfiguration + @Component 어노테이션 이나 이를 포함한 @SpringBootApplication 어노테이션 중 하나를 사용하면 아주 간편하게 '자동 환경 설정' 기능을 사용할 수 있습니다.
(참고로 @SpringBootApplication 어노테이션의 경우 스프링 부트를 사용함에 있어서 필수 어노테이션입니다!)
그럼 어노테이션을 본격적으로 알아보도록 할까요?
일단 빈은 스프링의 ApplicationContext(Bean Factory)에 의해 관리되고 스프링의 DI를 통해 제어권이 위임되어 생명주기가 관리됩니다.
기존 스프링의 경우 빈을 생성하고 이를 사용하기 위해선 하나씩 수동으로 빈을 등록하여 사용했었어야 했습니다.
의존성을 추가할 때 일일이 의존관계를 생각하여 수동으로 열심히 등록을 해줬는데,
실제 코드에서 쓰려고 하니 의존성 주입이 제대로 되지 않아 컴파일에러가 발생하거나 예상치 못한 익셉션들이 발생하곤 하죠..
여기에서 발전된 스프링부트는 이런 의존성의 조합을 보다 쉽게 사용하기 위해 고맙게도 미리 의존성 관계에 의해 묶여있는 '스타터'를 제공합니다.
예를 들면 'spring-boot-starter-web' 같은 것들 말이죠.
'스타터'라는 묶음을 제공하기 때문에, 스프링부트에선 수동 설정을 지양합니다.
@SpringBootConfiguration 어노테이션은 스프링부트의 설정을 나타내는 어노테이션으로 @Configuration을 대체하여 스프링 부트에서만! 사용됩니다.
@EnableAutoConfiguration 어노테이션은 자동 설정의 핵심 어노테이션입니다. 경로를 기반으로 설정 자동화를 수행하는 녀석입니다.
@ComponentScan 어노테이션은 익히 아시다싶이, 특정 패키지를 기반으로 설정에 사용할 @Component 설정 클래스들을 찾아냅니다.
basePackages 프로퍼티 값에 별도 경로를 설정하지 않으면 해당 어노테이션이 위치한 패키지의 루트가 경로로 설정되죠.
우리가 스프링부트에서 주로 사용하게 될 @SpringBootApplication 어노테이션은 @SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan을 합쳐놓은 그야말로 궁극의(?!) 어노테이션이라고 할 수 있습니다.
그러니 저희는 부트를 사용할 때 굳이 저 어노테이션들을 하나씩 선언할 필요 없이, @SpringBootApplication 어노테이션 하나만 선언을 하면 되겠죠?
그럼 '자동 환경 설정'의 핵심 어노테이션인 @EnableAutoConfiguration에 대해 조금 더 자세히 알아보도록 하겠습니다.
@EnableAutoConfiguration은 어떻게 생겼을까요??
EnableAutoConfiguration.class를 한번 확인해보도록 하겠습니다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
@interface로 선언되어 어노테이션으로 사용할 수 있게 만들어져 있군요.
이 중 자동 설정이 가능하도록 지원해주는 녀석은 @Import({AutoConfigurationImportSelector.class}) 입니다.
그럼 AutoConfigurationImportSelector는 어떻게 생겼을까요?
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
private static final AutoConfigurationImportSelector.AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationImportSelector.AutoConfigurationEntry();
private static final String[] NO_IMPORTS = new String[0];
private static final Log logger = LogFactory.getLog(AutoConfigurationImportSelector.class);
private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";
private ConfigurableListableBeanFactory beanFactory;
private Environment environment;
private ClassLoader beanClassLoader;
private ResourceLoader resourceLoader;
public AutoConfigurationImportSelector() {
}
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if(!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
if(!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.filter(configurations, autoConfigurationMetadata);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
윗 부분을 약간 긁어왔습니다.
AutoConfigurationImportSelect클래스는 DeferredImportSelector 인터페이스를 구현한 클래스입니다.
오버라이드 받은 selectImports() 메서드가 자동 설정한 빈을 결정하는 주요 메서드입니다.
모든 후보 빈을 getCandidateConfigurations() 메서드를 사용해 불러오게 됩니다.
우선 메서드를 호출하게 되면 /META-INF/spring.factories에 정의된 자동 설정할 클래스들을 먼저 불러오게 됩니다.
여기엔 불필요한 빈과 중복된 빈들도 있을 수 있기 때문에 중복 제거를 위해 getExclusions()를 호출하고, 불필요한 빈을 removeDeplicates()를 호출해 정리하여 필요한 빈들만 남겨놓습니다.
이렇게 자동 설정의 대상이 되는 빈들을 정리하는데, 그럼 후보가 되기 위해 정의된 빈들을 참고하기 위한 파일들이 필요할겁니다.
그런 파일들은 무엇이 있을까요?
우선은 위에서 언급한 /META-INF/spring.factories 파일입니다.
이 파일은 자동 설정 타깃 클래스들의 목록을 나타내는 파일입니다. @EnableAutoConfiguration 어노테이션을 사용하면 이 파일 내의 클래스들이 자동 설정의 타깃이 됩니다.
다음으론 /META-INF/spring-configuration-metadata.json 파일입니다.
여기엔 자동 설정에 사용할 프로퍼티 파일들이 정의되어있어 추후 사용 시 프로퍼티에 값만 오버라이드해 사용할 수 있도록 지원합니다.
마지막으로 org/springframework/boot/autoconfigure 입니다.
의존성을 주입하게되면 사용가능하며 미리 구현해놓은 자동 설정 리스트입니다. '{특정설정의 이름}AutoConfiguratiion'과 같은 형식으로 정의가 되어 있습니다.
위 파일은 모두 spring-boot-autoconfiguration에 미리 정의도어 있고 프로퍼티 값을 사용해 설정 클래스 내부의 값들을 변경할 수 있습니다.
만약 H2를 적용한다면 우선 spring.factories에 자동 설정 대상으로 존재하는지 확인 후 spring-configuration-metadata.json에 주요 프로퍼티들이 어떻게 선언되어 있는지 확인합니다.
만약 변경을 하고자 하면 applicatio.yml 또는 application.properties 파일 내에 프로퍼티를 선언해 오버라이드 할 수 있습니다.
{
"name": "spring.h2.console.path",
"type": "java.lang.String",
"description": "Path at which the console is available.",
"sourceType": "org.springframework.boot.autoconfigure.h2.H2ConsoleProperties",
"defaultValue": "\/h2-console"
},
H2에 대한 설정이 기록된 metadata.json 내부입니다.
spring.factories파일 내부에 h2를 검색해보시면 해당 파일이 설정된 H2ConsoleAutoConfiguration 클래스를 찾아 들어가보면
Prefix가 'spring.h2.console'로 선언된 설정값들을 불러오도록 되어있는 것을 확인할 수 있습니다.
@Configuration
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({WebServlet.class})
@ConditionalOnProperty(
prefix = "spring.h2.console",
name = {"enabled"},
havingValue = "true",
matchIfMissing = false
)
@EnableConfigurationProperties({H2ConsoleProperties.class})
public class H2ConsoleAutoConfiguration {
한마디로 설정값(metadata.json 내부에 기록된 아이들)의 prefix 다음 값을 통해 프로퍼티를 변경할 수 있다는 말이 되겠지요!
그럼 console의 path에 대한 정보를 변경해보도록 하겠습니다. 변경하고자 할 땐 다음과 같이 사용할 수 있습니다.
spring:
h2:
console:
path: /h2-test
이를 나열해서 쓰면 spring.h2.console.path 가 됩니다.
기존에 적용된 /h2-console을 변경된 /h2-test로 console을 접속 가능하도록 변경합니다.
실행을 한번 해보도록 하죠!
URL은 http://localhost:8080/h2-test가 될겁니다.
왜냐하면 프로퍼티를 오버라이드 했으니까!
'자동 환경 설정'을 통해 기존에 사용하기 위해 의존성 추가며 기타 등등의 작업을 해주었던 그때보다 훠어어얼씬 빨리 H2 Console을 띄우고 접속할 수 있는 환경이 되었습니다.
H2 뿐만 아니라 다양한 라이브러리들이 적용 가능하니, 확인해보시면 개발하시는데 굉장한 도움이 될 것 같습니다^^
긴 글 읽어주셔서 감사합니다^^