
| chapter | 설명 |
|---|---|
| 1. Spring Boot 의 개념 | Spring Boot 는 Spring 의 업그레이드 버전 아니야? Spring Boot 가 왜 탄생하게 되었고, 어떤 것인지에 대한 설명이 있습니다. |
| 2. 독립 실행형 Servlet application 구축 | '독립 실행형 ' 의 개념을 알고, 직접 FrontController 까지 구축해보면서 SpringBoot 의 Dispatcher Servlet 동작 방식을 이해하는 기반을 다집니다. |
| 3. Containerless Web Application | 앞선 이해를 기반으로 Spring Boot 의 @SpringBootApplication , SpringBootApplication.run() 을 유사하게 구현해보며 Spring Boot 동작 방식을 이해합니다. (포함되는 개념 : Spring Container, Dispatcher Servlet, Configuration , ComponentScan 등) |
4. @AutoConfiguration 파헤쳐보기 | Spring Boot 의 @AutoConfiguration 을 유사하게 직접 구현하여, Spring Boot가 수 많은 빈들을 어떻게 자동으로 구성해주는지 학습합니다. |
| 5. 자동 구성 빈 오브젝트의 디폴트 값 변경하기 | Spring Boot 는 수 많은 빈들을 자동으로 구성해주고, 디폴트 값을 유연하게 변경할 수 있도록 합니다. 코드를 통해 이 과정을 이해합니다. |
앞선 시간에 이어, 이번에는 독립 실행형 스프링 어플리케이션을 만들어 볼 것이다.
Spring Container 에 직접 빈들을 등록하고, 이 정보들을 Dispatcher Servlet에 넘겨주는 과정등을 통해 스프링부트 프로젝트를 만들면 초기에 나오는 SpringBootApplication.run 가 어떤식으로 동작하는지 직접 코드로 확인하겠다.
Spring Container 와 Dispatcher Servlet 없이 구현했던 이전의 코드를 리팩토링하는 것이니 이전 포스팅을 보고 이것을 보는 것을 추천드린다.
앞선 시간에는 frontController 를 간단하게 구현해서 직접 매핑과 바인딩을 하였다.
하지만 api 가 계속 추가될텐데 매번 코드로 직접 매핑해주는 것은 시간 소요도 많이 되고 관리도 힘들어진다.
SpringBoot 는 이 문제를 Spring Container 과 Dispatcher Servlet 으로 해결한다.
해결하는 방식을 간단히 설명하자면 다음과 같다.
Spring Container 에 필요한 빈들을 넣고 이 정보를 Dispatcher Servlet 에게 전달한다.
Dispatcher Servlet 은 SpringBoot 의 frontController 로써 동작을 하는데, 넘겨 받은 정보들을 토대로 요청에 오면 적합한 빈을 실행시키고 응답을 반환한다.
따라서 Spring Container 와 Dispatcher Servlet을 사용한다면
개발자는 완성된 api에 관한 매핑, 바인딩을 하지 않고 그저 빈으로 등록하기만 하면 된다.
이 작업들은 친숙한 어노테이테이션인 @SpringBootApplication 과 SpringBootApplication.run() 을 통해 이루어지고 있었다.
그럼 이제 직접 코드로 살펴보자.
지난 포스팅에서 만든
요청을 오면 'hello + 파라미터로 받은 값' 을 출력해주는 간단한 api 을 활용할 것인데 구조를 조금 변경하였다.
HelloController 에서 바로 구현 서비스를 참조하는 것이 아니라, HelloService 라는 인터페이스를 참조하도록 하였다.

이를 Spring Container 에 넣고 Dispatcher Servlet 을 사용하는 코드는 아래와 같다.
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import tobyspring.helloboot.containerless.HelloController;
import tobyspring.helloboot.containerless.SimpleHelloService;
public class ContainerlessBootApplication {
public static void main(String[] args) {
//1)Spring Container 생성
GenericWebApplicationContext applicationContext = new GenericWebApplicationContext();
applicationContext.registerBean(HelloController.class); //빈으로 등록
applicationContext.registerBean(SimpleHelloService.class); //직접 빈으로 등록할 때는 인터페이스가 아닌 정확한 클래스를 빈으로 등록해야 함.
//스프링 컨테이너가 알아서 빈들을 자기 순서대로 만들어줌
//컨트롤러를 만들어야 하는데 파라미터로 헬로 서비스가 필요한 경우 -> 자기의 빈들 다 탐색해서 파라미터로 넘겨주어 빈으로 만듦.
//여러개를 빈으로 등록하는경우? -> @Primary 를 통해 기본 빈을 지정하거나 @Qualifier 로 명시적으로 주입할 수도 있음.
// 이것을 해주지 않는다면 NoUniqueBeanDefinitionException 에러가 나게됨.
//기존에는 xml 으로 빈을 등록해주고 생성자에다 어떤 빈을 주입할 지도 기술을 했었음
applicationContext.refresh(); //bean object 를 만들어줌 (모든 초기화가 일어남)
ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
//2) 디스패처 서블릿 등록
WebServer webServer = serverFactory.getWebServer(servletContext ->
servletContext.addServlet("dispatcherServlet", new DispatcherServlet(applicationContext) {
}).addMapping("/*"));
webServer.start(); //Tomcat Servlet Container 가 동작하게 됨.
}
}
*코드설명
registerBean 메소드를 이용해 필요한 클래스들을 빈으로 등록할 수 있다. 이때, 인터페이스가 아닌 정확한 클래스를 빈으로 등록해야 한다. 이 등록된 빈들은 refresh() 메소드를 통해 초기화가 된다. 더 자세히 알아보기
스프링 컨테이너는 단순히 빈들의 정보를 등록하는 것이 아닌, 어플리케이션 시작 시점에 등록된 빈들을 모두 초기화 해준다. 따라서 만약 어떠한 빈이 초기화 될 때 다른 오브젝트 필요로 하면 알아서 자신의 빈들을 모두 탐색하여 주입시켜준다.
위의 코드를 예시로 들어보자면, HelloController 는 생성될 때 HelloService 인터페이스를 필요로 한다. 스프링 컨테이너는 등록된 빈들에서 HelloService 구현체를 찾아 HelloController 를 초기화한다. 만약 빈들에서 HelloService 가 없다면NoSuchBeanDefinitionException을 발생키기고, HelloService 구현체가 여러 개라면NoUniqueBeanDefinitionException을 발생시킨다. 이 긴 설명들은, 스프링 컨테이너는 DI(Dependency Injection) 역할도 수행한다고 축약될 수도 있다.
이대로 실행시키면 예상대로 되지 않을 것이다.
404 error 가 반환될 것인데 HelloController도 수정을 해야한다.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
private final HelloService helloService;
public HelloController(HelloService helloService) {
this.helloService = helloService;
}
@GetMapping("/hello")
public String hello(String name) {
if( name == null || name.trim().length() == 0) throw new IllegalArgumentException();
return helloService.sayHello(name);
}
}
@RestController 혹은 @Controlelr를 붙여야 Dispatcher Servlet 이 url을 매핑해야 할 클래스라고 인식을 한다.
요청이 오면 이 어노테이션들이 붙은 클래스들에서 GetMapping, PostMapping 등을 확인하여 url 에 매핑할 메소드를 탐색한다.
더 알아가기
스프링 컨테이너에 빈을 등록하는 것은, new 연산자를 통해 클래스를 생성하는 것과 어떤 차이가 있을까 ?
-> 스프링 컨테이너는 기본적으로 오브젝트를 딱 한 번만 만들고, 이를 재사용 하는 방식이다. 이 특성 때문에 스프링 컨테이너를 싱글톤 레지스트리라고도 한다.
위의 코드에서는 스프링 컨테이너에게 구성 정보를 직접 registerBean() 메소드를 이용해 전달했다.
예전에는 외부 설정 파일등을 이용해 스프링 컨테이너에게로 전달했었는데
2가지 다른 방식으로 전달을 해 볼 것이다.
@Configuration + @Bean클래스에 @Configuration 어노테이션을 붙인 후, @Bean 어노테이션을 통해 여러 개의 빈을 직접 등록하는 방식이다,
@Bean 은 메소드에 붙여야 하고, 붙이게 되면 메서드의 리턴 객체가 빈으로 등록이 된다.
빈 생성 과정이 복잡하거나 의존성을 조립해야 할 때, 외부 라이브러리 객체를 빈으로 등록해야 할 때 등의 상황에서 주로 사용한다.
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
@Configuration //구성 정보를 가지고 있는 클래스다.
public class ContainerlessBootApplication {
@Bean //helloController method 의 return 값인 HelloController 가 빈으로 등록될 것임.
public HelloController helloController (HelloService helloService) {
return new HelloController(helloService);
}
@Bean
public HelloService helloService () {
return new SimpleHelloService();
}
public static void main(String[] args) {
//GenericWebApplicationContext 에서 AnnotationConfigWebApplicationContext 으로 변경함
//그래야 @Configuration 을 사용할 수 있음.
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext() {
@Override
protected void onRefresh() {
super.onRefresh(); // 생략하면 안 됨
ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
WebServer webServer = serverFactory.getWebServer(servletContext ->
servletContext.addServlet("dispatcherServlet", new DispatcherServlet(this) {
}).addMapping("/*"));
webServer.start();
}
};
applicationContext.register(ContainerlessBootApplication.class); //register() method 에 파라미터로 @Configuration 이 있는 클래스를 전달함.
applicationContext.refresh();
}
}
이를 실행하면 HelloController 와 HelloService 가 빈으로 등록이 될 것이다.
그런데 위의 코드를 보면 이전과 차이점이 있는데, 이전과 달리 AnnotationConfigWebAapplicationContext 를 사용하였고, onRefresh() 메소드를 오버라이딩 하여, 이 시점에 DispatcherServlet 을 등록하고, webServer 를 실행시켰다.
실제로 스프링부트에서도 이 시점에 웹서버를 초기화하고 디스패처 서블릿을 서블릿 컨텍스트에 등록하기 때문에 코드를 변경하였다.
이 시점에 이 작업이 일어나는 이유는 다음과 같다.
onRefrsh 는 ApplicationContext 가 초기화되고 모든 빈들이 생성된 이후에 호출되는 메소드이다. 만약 이보다 이른 시점에 서버를 시작하면 빈 등록이 덜 끝났을 수도 있어 정상 동작하지 않거나 오류가 발생할 수도 있기 때문이다.
DispatcherServlet 은 Spring 빈을 참조하기에 스프링이 준비된 이후이 등록해야 한다.
@ComponentScan + @Component2.1 의 방법보다 훨씬 간편한 방법이다.
빈으로 등록하고 싶은 클래스에 @Component 를 붙이고
스프링 컨테이너, 웹서버를 초기화하고 디스패처 서블릿을 등록하는 클래스에 @ComponentScan 을 붙이면 된다.
예시
@Component
public class SimpleHelloService implements HelloService {
@Override
public String sayHello(String name) {
return "Hello " + name;
}
}
@Configuration
@ComponentScan
public class ContainerlessBootApplication {
public static void main(String[] args) {
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext() {
@Override
protected void onRefresh() {
super.onRefresh();
ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
WebServer webServer = serverFactory.getWebServer(servletContext ->
servletContext.addServlet("dispatcherServlet", new DispatcherServlet(this) {
}).addMapping("/*"));
webServer.start();
}
};
applicationContext.register(ContainerlessBootApplication.class);
applicationContext.refresh();
}
}
단지 어노테이션만 붙이면 되니까 정말 편리하게 빈으로 등록할 수 있지만,
빈으로 등록되는 클래스가 많아지면 정확히 어떤 것들이 등록되었는지 파악하기가 번거로워 질 수도 있다.
한 가지 특징으로는, 메타 어노테이션도 스캔이 된다는 점이다.
메타 어노테이션?
어노테이션 위의 붙은 어노테이션을 말한다. 예를 들어 친숙한 어노테이션인@Controller어노테이션을 들어가보면, @Target, @Retention , @Documented , @Component 가 있다. 이것들은@Controller의 메타 어노테이션이 되고, 메타 어노테이션에 @Component 가 있으니@Controller만 붙여도 빈으로 등록이 되는 것이다.
이제 마무리로 우리에게 익숙한 SpringApplication.run() 메소드와 유사한 메소드를 만들 것이다.
우선 앞선 코드에서 applicationContext 를 만들고, 웹 서버를 초기화하며, DispatcherServlet 을 등록했던 코드들을 하나의 메소드로 묶고, 새 클래스를 만들어 그 안에 넣는다.
나는 MySpringApplication 이라는 클래스를 만들어 그 안에 넣었다.
public class MySpringApplication {
public static void run(Class<?> applicationClass) {
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext() {
@Override
protected void onRefresh() {
super.onRefresh();
ServletWebServerFactory serverFactory = this.getBean(ServletWebServerFactory.class);
DispatcherServlet dispatcherServlet = this.getBean(DispatcherServlet.class);
dispatcherServlet.setApplicationContext(this);
WebServer webServer = serverFactory.getWebServer(servletContext ->
servletContext.addServlet("dispatcherServlet", new DispatcherServlet(this) {
}).addMapping("/*"));
webServer.start();
}
};
applicationContext.register(applicationClass);
applicationContext.refresh();
}
}
메소드로 만들었으니 앞선 코드에서는 이 메소드를 사용하도록 변경한다.
@Configuration
@ComponentScan
public class ContainerlessBootApplication {
public static void main(String[] args) {
MySpringApplication.run(ContainerlessBootApplication.class);
}
}
스프링부트 프로젝트를 처음 만들면 보이는 부트 시작 클래스와 유사하지 않은가?
그런데 이를 실행하면 에러가 날 것이다.
웹서버와 디스패처 서블릿이 빈으로 등록되지 않았기 때문이다.
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
@ComponentScan
public class ContainerlessBootApplication {
@Bean
public ServletWebServerFactory servletWebServerFactory(){
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet(){
return new DispatcherServlet();
}
public static void main(String[] args) {
MySpringApplication.run(ContainerlessBootApplication.class);
}
}
위처럼 빈으로 등록해준 다음 실행한다면 제대로 동작할 것이다.
완전히 똑같이 구현하지는 않았지만 , 간단하게 직접 코드로 구현해보면서
SpringBoot 가 어떻게 프로젝트를 간단한 run 메소드로 실행시키는지 알아보았다.
SpringBoot 뿐만 아니라 WebServer , FrontController 등에 대해서도 이해하는 것에 도움이 되었던 시간이었기를 바란다.
읽어주셔서 감사하다. 😆
참조 : 토비의 스프링 부트 - 이해와 원리