SpringBoot : Containerless Web Application (feat. Spring Container , Dispatcher Servlet)

이가희·2025년 4월 17일

spring + java

목록 보기
11/14
post-thumbnail

SpringBoot 같이 알아가기 [목차]

chapter 의 제목을 누르시면 각 페이지로 이동합니다.
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 없이 구현했던 이전의 코드를 리팩토링하는 것이니 이전 포스팅을 보고 이것을 보는 것을 추천드린다.


1. Spring Container 와 Dispatcher Servlet

앞선 시간에는 frontController 를 간단하게 구현해서 직접 매핑과 바인딩을 하였다.

하지만 api 가 계속 추가될텐데 매번 코드로 직접 매핑해주는 것은 시간 소요도 많이 되고 관리도 힘들어진다.

SpringBoot 는 이 문제를 Spring Container 과 Dispatcher Servlet 으로 해결한다.

해결하는 방식을 간단히 설명하자면 다음과 같다.

Spring Container 에 필요한 빈들을 넣고 이 정보를 Dispatcher Servlet 에게 전달한다.

Dispatcher Servlet 은 SpringBoot 의 frontController 로써 동작을 하는데, 넘겨 받은 정보들을 토대로 요청에 오면 적합한 빈을 실행시키고 응답을 반환한다.

따라서 Spring Container 와 Dispatcher Servlet을 사용한다면
개발자는 완성된 api에 관한 매핑, 바인딩을 하지 않고 그저 빈으로 등록하기만 하면 된다.

이 작업들은 친숙한 어노테이테이션인 @SpringBootApplicationSpringBootApplication.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 가 동작하게 됨.
    }
}

*코드설명

  1. 스프링 컨테이너를 생성하는 코드이다.
    생성된 스프링 컨테이너에 registerBean 메소드를 이용해 필요한 클래스들을 빈으로 등록할 수 있다. 이때, 인터페이스가 아닌 정확한 클래스를 빈으로 등록해야 한다. 이 등록된 빈들은 refresh() 메소드를 통해 초기화가 된다.

더 자세히 알아보기
스프링 컨테이너는 단순히 빈들의 정보를 등록하는 것이 아닌, 어플리케이션 시작 시점에 등록된 빈들을 모두 초기화 해준다. 따라서 만약 어떠한 빈이 초기화 될 때 다른 오브젝트 필요로 하면 알아서 자신의 빈들을 모두 탐색하여 주입시켜준다.
위의 코드를 예시로 들어보자면, HelloController 는 생성될 때 HelloService 인터페이스를 필요로 한다. 스프링 컨테이너는 등록된 빈들에서 HelloService 구현체를 찾아 HelloController 를 초기화한다. 만약 빈들에서 HelloService 가 없다면 NoSuchBeanDefinitionException 을 발생키기고, HelloService 구현체가 여러 개라면 NoUniqueBeanDefinitionException 을 발생시킨다. 이 긴 설명들은, 스프링 컨테이너는 DI(Dependency Injection) 역할도 수행한다고 축약될 수도 있다.

  1. 웹서버에 디스패처 서블릿을 등록한다. 디스패처 서블릿을 생성할 때 파라미터로 스프링 컨테이너를 전달하여 set 을 해준다. 이렇게 하면 요청이 올 때 디스패처 서블릿이 요청 url 을 확인하여 적합한 빈을 실행시킨다.

이대로 실행시키면 예상대로 되지 않을 것이다.
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 연산자를 통해 클래스를 생성하는 것과 어떤 차이가 있을까 ?
-> 스프링 컨테이너는 기본적으로 오브젝트를 딱 한 번만 만들고, 이를 재사용 하는 방식이다. 이 특성 때문에 스프링 컨테이너를 싱글톤 레지스트리라고도 한다.

2. 자바코드 구성 정보

위의 코드에서는 스프링 컨테이너에게 구성 정보를 직접 registerBean() 메소드를 이용해 전달했다.

예전에는 외부 설정 파일등을 이용해 스프링 컨테이너에게로 전달했었는데
2가지 다른 방식으로 전달을 해 볼 것이다.

2.1 @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();
    }
}

이를 실행하면 HelloControllerHelloService 가 빈으로 등록이 될 것이다.

그런데 위의 코드를 보면 이전과 차이점이 있는데, 이전과 달리 AnnotationConfigWebAapplicationContext 를 사용하였고, onRefresh() 메소드를 오버라이딩 하여, 이 시점에 DispatcherServlet 을 등록하고, webServer 를 실행시켰다.

실제로 스프링부트에서도 이 시점에 웹서버를 초기화하고 디스패처 서블릿을 서블릿 컨텍스트에 등록하기 때문에 코드를 변경하였다.

이 시점에 이 작업이 일어나는 이유는 다음과 같다.

  1. onRefrshApplicationContext 가 초기화되고 모든 빈들이 생성된 이후에 호출되는 메소드이다. 만약 이보다 이른 시점에 서버를 시작하면 빈 등록이 덜 끝났을 수도 있어 정상 동작하지 않거나 오류가 발생할 수도 있기 때문이다.

  2. DispatcherServlet 은 Spring 빈을 참조하기에 스프링이 준비된 이후이 등록해야 한다.

2.2 @ComponentScan + @Component

2.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 만 붙여도 빈으로 등록이 되는 것이다.


3. SpringApplication.run()

이제 마무리로 우리에게 익숙한 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 등에 대해서도 이해하는 것에 도움이 되었던 시간이었기를 바란다.

읽어주셔서 감사하다. 😆

참조 : 토비의 스프링 부트 - 이해와 원리

profile
안녕하세요 개발하는 사람입니다.

0개의 댓글