전 게시물의 마지막 부분에서는 자동 구성 정보를 외부 파일에서 읽은 후, 동적으로 자동 구성을 등록하도록 했었다. 이 기능을 만들기 위해 MyAutoConfiguration
애노테이션 클래스를 만들고, 이 클래스의 full path+name 으로 만들어진 텍스트 파일(resources/META_INF/spring/{애노테이션 클래스 이름}.imports
)을 생성했었다.
스프링 부트도 비슷한 방식으로 구현이 되어있다. 스프링 부트에 이 기능을 하는 클래스 애노테이션 이름은AutoConfiguration
이다. 또한 자동 구성할 빈들의 목록이 저장되어 있는 텍스트 파일은 org.springframework.boot.autoconfigure.AutoConfiguration.imports
이다.
해당 파일을 열어보면 자동 구성이 되는 144개의 Configuration 클래스들을 확인할 수 있다.(web 라이브러리 사용시). 이처럼 스프링 부트는 자기 주장이 강하다. 사용할 기술이나 설정, 라이브러리 버전 등을 이미 결정해놓았다.
각각의 Configuration 클래스들은 내부에 1개 이상의 빈 애노테이션이 붙은 팩토리 메소드를 가진다. 또한 Configuration 클래스 자체도 빈으로 등록된다. 우리가 애플리케이션을 시작할 때마다 기본적으로 400~500개 정도의 빈들을 다 생성한다고 생각해보자. 불필요한 리소스가 너무 많다.
따라서 실제 스프링 부트는 이런 방식으로 모든 빈들을 다 생성하진 않는다.
예를 들어, Thymeleaf를 사용하지 않는다고 가정하자. 그러면 불필요하게 ThymeleafConfiguration에 있는 빈들을 생성할 필요가 없다.
이렇게 어떤 조건을 걸어서 어떤 빈들의 생성 여부를 결정하는 방법을 살펴보자.
이 게시물의 예시로는 서블릿 컨테이너로 Tomcat과 Jetty를 선택적으로 사용하는 기능을 구현해보자.
Tomcat 라이브러리는 스프링 디펜전시로 web을 선택하면 자동으로 임폴트 된다.
Jetty 라이브러리는 spring-boot-starter
가 지원하므로 아래와 같이 간단하게 임폴드 할 수 있다.
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jetty'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Tomcat과 Jetty를 빈으로 등록하기 위해 설정 클래스를 만들자.
@MyAutoConfiguration
public class TomcatWebServerConfig {
@Bean("tomcatWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
@MyAutoConfiguration
public class JettyWebServerConfig {
@Bean("jettyWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new JettyServletWebServerFactory();
}
}
자동 구성 정보에 추가하기 위해, 전에 만들어둔 ~.imports
파일을 아래와 같이 작성한다.
tobyspring.config.autoconfig.TomcatWebServerConfig
tobyspring.config.autoconfig.JettyWebServerConfig
tobyspring.config.autoconfig.DispatcherServletConfig
이대로 애플리케이션을 실행하면 Tomcat이 사용될까, Jetty가 사용될까?
정답은 애플리케이션이 실행되지 않는다.(에러)
에러 메시지는 대략 아래와 같다.
org.springframework.context.ApplicationContextException:
Unable to start web server;
nested exception is org.springframework.context.ApplicationContextException:
Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans
ServletWebServerFactory 빈이 여러개 존재하기 때문에 애플리케이션을 시작할 수 없다고 뜬다.
Tomcat과 Jetty를 자유롭게 바꿔가며 사용하고 싶다면 어떻게 해야할까?
자동구성할 빈들이 텍스트로 나열되어 있는 ~.imports
을 수정하지 않고, 자동 구성 빈 정보를 변경해보자.
스프링에서는 해당 클래스(또는 빈)를 생성할지, 말지 동적으로 결정할 수 있는 애노테이션(@Conditional
)을 지원한다.
먼저 코드로 확인하자
@MyAutoConfiguration
@Conditional(JettyWebServerConfig.JettyCondition.class)
public class JettyWebServerConfig {
@Bean("jettyWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new JettyServletWebServerFactory();
}
static class JettyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return true; // 이 클래스를 빈으로 사용하겠다.
}
}
}
클래스 레벨에 @Conditional
을 붙이고, 그 내부에 Condition
을 구현한 클래스를 넣어준다. 일단 위 코드에서는 내부 static 클래스로 구현했다.
해당 인터페이스의 구현 메소드는 matches
는 이 클래스의 빈을 생성할지(true 반환), 말지(false 반환) 결정하는 역할을 한다. 그 인자로는 전체적인 어플리케이션 환경을 얻을 수 있는 context를 받아서 이용할 수 있다.
다음과 같이 톰켓 설정 클래스도 작성하면 서블릿 컨테이너로 Jetty를 사용하게 할 수 있다.
@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; // 이 클래스를 빈으로 사용 안하겠다.
}
}
}
ClassUtils
사용그럼 자동 구성 빈들을 변경할 때마다 matches
메소드의 반환 값을 바꿔줘야 하는가?
대표적으로 스프링 부트가 사용하는 방법은 ‘해당 라이브러리가 프로젝트에 존재하는가?’ 이다.
참고로 Tomcat(또는 Jetty)ServletWebServerFactory.class
는 스프링 부트에 들어 있는 클래스이다. 이 클래스가 존재한다고 해서 라이브러리가 존재하는 것은 아니다.
톰켓 라이브러리의 가장 메인이 되는 클래스는 “org.apache.catalina.startup.Tomcat", 제티 라이브러리의 가장 메인이 되는 클래스는 "org.eclipse.jetty.server.Server" 이다. 프로젝트 내에 이 클래스들의 존재 여부를 이용하여 어떤 컨테이너를 사용할지 결정해보자.
어떤 라이브러리(클래스)가 이 프로젝트에 포함되어 있는지 존재 여부는 어떻게 아는가?
스프링이 제공하는 유틸리티 중 ClassUtils
이 존재한다. 이 클래스의 메소드를 이용해 어떤 클래스가 이 프로젝트에 포함되어 있는지 구분이 가능하다.
@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 ClassUtils.isPresent("org.apache.catalina.startup.Tomcat", context.getClassLoader());
// jetty 클래스는 아래와 같이 작성.
// return ClassUtils.isPresent("org.eclipse.jetty.server.Server", context.getClassLoader());
}
}
}
이제 프로젝트에 포함되어 있는 라이브러리에 따라서 서블릿 컨테이너가 자동 구성을 다르게 해줄 것이다.
// Jetty로 실행
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'
}
// Tomcat로 실행
dependencies {
implementation ('org.springframework.boot:spring-boot-starter-web')
}
Condition을 사용하는 톰켓 설정 클래스와 제티 설정 클래스는 아주 유사하게 생겼다. ClassUtils
의 메소드에 들어가는 문자열 값만 다른 것 뿐이다. 따라서 메타 어노테이션을 이용하여 이 부분을 공통 어노테이션으로 리팩토링 하자.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(MyOnClassCondition.class)
public @interface ConditionalMyOnClass {
String value();
}
public class MyOnClassCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> attrs = metadata.getAnnotationAttributes(ConditionalMyOnClass.class.getName());
String value = (String) attrs.get("value");
return ClassUtils.isPresent(value, context.getClassLoader());
}
}
이제 자동 구성 설정 파일에서 다음과 같이 사용할 수 있다.
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {
@Bean("tomcatWebServerFactory")
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
사용자가 인프라 빈을 직접 빈으로 생성하고 싶을 때는 자동 구성이 다음과 같이 진행되면 된다.
먼저 사용자가 설정 클래스에서 빈으로 직접 생성하는 코드를 작성해보자.
@Configuration(proxyBeanMethods = false)
public class WebServerConfiguration {
@Bean
ServletWebServerFactory customerWebServerFactory() {
TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
serverFactory.setPort(9090);
return serverFactory;
}
}
톰켓 서블릿 컨테이너를 직접 생성 후, 포트를 9090으로 설정 후 빈으로 등록하는 코드이다.
이제 자동 구성을 제어해야 한다. 해당 타입이 이미 빈으로 등록 되었는지 여부는 어떻게 알 수 있을까?
이 부분은 꽤나 로직이 복잡하기 때문에, 스프링 부트가 제공하는 애노테이션(@ConditionalOnMissingBean
)을 사용해보자.
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {
@Bean("tomcatWebServerFactory")
@ConditionalOnMissingBean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
이런 패턴은 자주 쓰인다. 클래스 레벨에서는 ‘해당 라이브러리가 프로젝트에 포함되어 있는가?’ 를 체크하고, 빈 팩토리 메서드에서는 ‘해당 타입의 빈을 개발자가 구성 정보로 만들었는가?’ 를 체크하는 방법이다.
@Conditional은 스프링 4.0부터 추가된 애노테이션이다. 이 애노테이션을 이용하여 만들어진 스프링 부트의 애노테이션들을 살펴보자
@Conditional(ProfileCondition.class)
public @interface Profile {...}
클래스 레벨에서 @ConditionalOnClass로 1차 체크하고,
빈 레벨에서 @ConditionalOnMissingBean로 2차 체크하는 조합은 가장 대표적으로 사용되는 방식이다.
reference
해당 게시물은 인프런 - 토비의 스프링 부트 이해와 원리을 기반으로 작성되었습니다.
강의 내용을 축약하고 생략한 부분도 많기 때문에, 게시물만으로 해당 개념을 이해하지 못할 수 있습니다.
스프링 부트를 사용하시는 분들에게 해당 강의를 적극적으로 추천합니다.