토비의 스프링 부트 - 이해와 원리 강의를 공부하며 정리한 자료입니다.
컨테이너리스 웹애플리케이션 아키텍처는 웹클라이언트와 통신을 처리하는 웹컴포넌트와 웹컴포넌트를 관리하는 웹컨테이너, 요청을 처리하는 비즈니스 로직을 담당하는 스프링 빈과 스프링빈을 관리하는 스프링 컨테이너로 분리되어 있는 구조를 띄고 있다. 스프링 부트는 분리된 아키텍처를 통해 웹컴포넌트의 관리를 위한 서블릿 컨테이너의 설정을 제외한 애플리케이션 개발의 핵심인 비즈니스 로직 개발에만 집중할 수 있는 독립 실행형 자바 애플리케이션 개발을 지원한다.
웹컴포넌트인 서블릿을 관리하는 서블릿 컨테이너에는 웹클라이언트로 들어오는 각 요청들을 처리하는 서블릿들을 등록할 수 있지만 스프링의 경우 디스패처 서블릿이라고 하는 하나의 서블릿을 등록해 웹클라이언트로 들어오는 모든 요청을 일괄적으로 처리하는 프론트 컨트롤러 패턴을 띄고 있다. 디스패처 서블릿의 경우 애플리케이션 컨텍스트에 등록된 스프링 빈 정보를 바탕으로 요청에 대한 핸들러 맵핑을 지원한다.
spring initializer를 통해 생성한 스프링 부트 애플리케이션의 코드는 다음과 같다.
@SpringBootApplication
public class Project01Application {
public static void main(String[] args) {
SpringApplication.run(Project01Application.class, args);
}
}
스프링 웹애플리케이션 실행 시 동작해야 하는 핵심 기능은 다음과 같다.
1. 서블릿 컨테이너 등록
2. 서블릿 컨테이너에 프론트 컨트롤러 역할을 하는 서블릿 등록
3. 스프링 컨테이너 관리를 위한 애플리케이션 컨텍스트 등록
// 설정 빈
@Configuration
// 애노테이션 기반 애플리케이션 컨텍스트 등록
@ComponentScan
public class HellobootApplication {
@Bean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
public static void main(String[] args) {
run(HellobootApplication.class, args);
}
public static void run(Class<?> applicationClass, String... args) {
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext() {
@Override
protected void onRefresh() {
super.onRefresh();
ServletWebServerFactory serverFactory = this.getBean(ServletWebServerFactory.class);
DispatcherServlet dispatcherServlet = this.getBean(DispatcherServlet.class);
// 1. 톰캣 웹서버 생성
// 2. 디스패처 서블릿 등록
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("dispatcherServlet", dispatcherServlet)
.addMapping("/*");
});
// 3. 웹서버 시작
webServer.start();
}
};
//3. 애노테이션 기반 애플리케이션 컨텍스트 등록
applicationContext.register(applicationClass);
// 4. 컨테이너 구동
applicationContext.refresh();
}
}
@Component가 붙은 빈을 애플리케이션 컨텍스트로 등록한다. @Service, @RestController도 @Component를 메타 애노테이션으로 가지는 애노테이션이기 때문에 컴포넌트 스캔의 대상이 된다.
컴포넌트 스캔의 경우 사용된 애노테이션이 사용된 클래스 패키지 하위 영역이 스캔 대상이기 때문에 분리된 패키지의 빈을 등록하기 위해서는 @Import를 사용한다. 동적인 자동 구성 정보 등록할 때는 selectImport 메소드를 오버라이딩한 클래스를 @Import에 등록한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyAutoConfigImportSelector.class)
public @interface EnableMyAutoConfiguration {
}
public class MyAutoConfigImportSelector implements DeferredImportSelector {
private final ClassLoader classLoader;
public MyAutoConfigImportSelector(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> autoConfigs = new ArrayList<>();
//MyAutoConfiguration 애노테이션이 붙은 클래스를 등록한다.
ImportCandidates.load(MyAutoConfiguration.class, classLoader).forEach(autoConfigs::add);
return autoConfigs.toArray(new String[0]);
}
}
조건을 확인해 만족하는 경우에만 컨테이너의 빈으로 등록할 수 있도록 지원하는 애노테이션으로 속성 값으로 matches 메소드를 구현한 클래스를 등록한다.
@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. */
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
스프링은 Environment 추상화는 프로파일과 프로퍼티를 제공하는데 이 정보를 활용한 커스텀 빈 등록을 통해 간단하게 자동 구성의 디폴트 설정을 변경하는 것을 지원한다.
@Value는 프로퍼티에 등록된 치환자를 지정하고 컨테이너 초기화 작업에서 프로퍼티 속성값으로 교체하는 기능을 담당하는데 이때 빈 팩토리의 후처리기로 해당 교체 작업을 수행하는 PropertySourcesPlaceholderConfigurer를 등록해야 한다.
BeanPostProcessor의 postProcessAfterInitialization 메소드를 오버라이딩해 커스텀 애노테이션을 등록한 프로퍼티 빈을 찾아서 바인딩하는 작업을 수행하게 할 수 있다.
@MyAutoConfiguration
public class PropertyPostProcessorConfig {
@Bean
BeanPostProcessor propertyPostProcessor(Environment env) { return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
MyConfigurationProperties annotation = findAnnotation(bean.getClass(), MyConfigurationProperties.class);
if (annotation == null) return bean;
Map<String, Object> attrs = getAnnotationAttributes(annotation); String prefix = (String) attrs.get("prefix");
return Binder.get(env).bindOrCreate(prefix, bean.getClass());
}
};
}
}