SpringBoot : 독립 실행형 Servlet application 만들기 (feat. FrontController 직접 구축)

이가희·2025년 4월 15일

spring + java

목록 보기
10/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 는 수 많은 빈들을 자동으로 구성해주고, 디폴트 값을 유연하게 변경할 수 있도록 합니다. 코드를 통해 이 과정을 이해합니다.

SpringBoot 가 Spring 을 어떻게 활용해서, Servlet Container 를 띄우는지 궁금하지 않은가?
이번에 SpringBoot를 활용해 직접 Servlet Container과 그 안에 Servlet 하나를 띄워보겠다.
그리고 띄워진 하나의 Servlet 을 FrontController 로 만들어 사용해 볼 것이다.

이번 포스팅의 목표는, 기존에 @Controller 등의 어노테이션으로 간단히 처리했던 부분들을
코드로 직접 구현함으로써 HTTP 요청이 어떻게 SpringBoot 에서 처리되는지를 이해하는 것이다.

독립 실행형 (Standalone) servlet application ?

SpringBoot 가 내포하고 있는 주요 개념 중 하나로, 내장 웹서버를 통해 바로 실행가능한 어플리케이션이라는 의미이다.
기존에는 애플리케이션을 띄우기 위해 WAR 파일로 패키징 한 후 WAS 에 올려 실행해야 했다. (불편하고 복잡)
Standalone servlet application 은 내장 웹서버가 있어 서버 설치 없이 간편하게 바로 실행이 가능하다.


1. SpringBoot project 생성

SpringBoot project 를 생성하는 다양한 방법이 있는데,
간단하게 Spring Initializer 를 통해 생성하겠다.

의존성은 Spring Web 만을 추가하였다.

1.1 간단한 api 생성

요청을 보내면 ' hello + (parameter 로 받은 단어) ' 를 출력하는 간단한 api 하나를 생성하겠다.
일단은 익숙한 어노테이션들 (@Controller, @GetMapping 등) 을 통해서 만들 것이다.
그런 다음 어노테이션들에 해당하는 내용을 코드로 구현하고, 어노테이션을 제거한 뒤에도 똑같이 동작되도록 만들 예정이다.

hello api

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(String name) {
        return "Hello " + name;
    }
}

만들어진 api 를 실행시켜보겠다.


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HellobootApplication {

	public static void main(String[] args) {
		SpringApplication.run(HellobootApplication.class, args);
	}

}

@SpringBootApplication 이 붙어져있는 클래스의 main 함수를 실행시키고
우리는 해당 경로 (/hello) 로 요청을 보내면 응답이 올 것이다.

정말 간단하게 api 실행이 완료되었는데 과연 어떻게 Servlet Container가 띄워졌고, 어떻게 내 요청이 HelloController.hello() 에 매핑이 되었을까?

단지 @SpringBootApplication , @RestController , @GetMapping 붙이기만 했을 뿐인데 말이다.

이제부터 이 세 가지 어노테이션을 주석처리하고 직접 코드로 구현해 보아,
우리에게 익숙한 어노테이션들이 어떤 놀라운 일들을 해주고 있었는지 확인 해 보겠다.


2. 독립 실행형 Servlet Application 구축

2.1 어노테이션 없이 코드로 구축

@SpringBootApplication , @RestController , @GetMapping 어노테이션을 주석 처리하겠다.

애플리케이션은 웹서버가 떠야하고 , 웹서버가 뜨면 Servlet Container 가 뜬 거고,
뜬 Servlet Container 안에 Servlet 을 잘 등록해서 요청을 받도록 해야하고...

지금부터 그 코드를 구현해 보겠다.

import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import java.io.IOException;

public class HellobootApplication {

    public static void main(String[] args) {

       1) ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        // 간편하게 TomcatServletWebServer 을 만들 수 있게 도와주는 클래스
        // SpringBoot 의 ServletWebServerFactory 로 받을 수가 있다.
        // tomcat 뿐만 아니라 다른 WebServer 도 동일한 방식으로 동작시킬 수 있도록 SpringBoot 가 추상화 시켰다.


  	   2)  WebServer webServer = serverFactory.getWebServer(servletContext -> servletContext.addServlet("hello", new HttpServlet() {
            @Override
            protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
                String name = req.getParameter("name");

                resp.setStatus(HttpStatus.OK.value());
                resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
                resp.getWriter().println("Hello " + name);
            }
        }).addMapping("/hello"));

        // ServletContextInitializer 을 파라미터로 넘김
        // 이 인터페이스는 onStartup 메소드를 구현해서 서블릿, 필터, 리스너 등을 직접 등록할 수 있게 도와줌 (web.xml 설정을 대체함 )

        //이 인터페이스는 onStartup 이 메소드만이 있고, 파라미터로 ServletContext 가 있음. 따라서 람다식으로 표현함.

        // servlet 을 하나 등록해 보겠음. addServlet 으로 등록하고, 첫 번째 파라미터에는 servlet name 을 넘겨줌
        // 두 번째는 Servlet 인터페이스를 구현한 객체를 넣어줌.
        // HttpServlet 중 service 메소드만 오버라이딩 했음. 응답을 생성해줌 (상태 , 해더, 바디)
        // 이건 /hello 라는 url 에 매핑되어 이곳으로 요청이 들어오면 이 서블릿이 실행될 것임

      3)  webServer.start(); //Tomcat Servlet Container 가 동작하게 됨.
    }
}

이 메인 함수를 실행시키면 이전에 어노테이션으로 만들었던 api 와 똑같이 동작할 것이다.

코드를 천천히 살펴보겠다.
주석만 보아도 이해가 된다면 아래 내용은 읽지 않아도 좋다.

1) TomcatServletWebServerFactory()

이름에서도 알 수 있듯이 톰캣 서버를 간편하게 만들 수 있도록 도와주는 클래스이다.
그런데 생성된 클래스를 SpringBoot가 제공하는 클래스인 ServletWebServerFactory로 받는다.

ServletWebServerFactory는 서버를 띄울 때, 어떤 것을 선택하든 동일한 방식으로 동작시킬 수 있도록 SpringBoot 가 추상화 시켜놓은 클래스이다.
이 클래스의 getWebServer 메소드를 통해 WebServer 객체를 얻고, 이 객체의 start() 메소드를 통해 간편하게 웹서버를 띄울 수가 있다.

참고
아무런 의존성을 추가하지 않아도 바로 TomcatServletWebServerFactory 를 사용할 수가 있는데
이는 SpringBoot Project 를 만들 때 의존성으로 추가한 Spring Web 에 포함되어 있기 때문이다.
시간이 날 때 SpringBoot Project 생성 시 들어가는 의존성들에 대해, 왜 Spring Boot 는 어떠한 것을 동작시킬 때 이러한 라이브러리 집단을 선택했는지 공부해 보아도 도움이 될 것이다.

2) WebServer

  1. ServletWebServerFactorygetWebServer 메소드를 통해서 WebServer 오브젝트를 얻는다. getWebServer 메소드의 파라미터에 ServletContextInitializer 인터페이스를 구현한 클래스를 넘겨주면 된다.
    ServletContextInitializer 는 서블릿 컨텍스트에 서블릿, 필터, 리스너 등을 등록할 수 있도록 도와주는 인터페이스로, 이것을 통해 과거에는 xml 로 작성해야 했던 부분들이 코드로 등록할 수 있게 되었다.

  2. ServletContextInitializer 인터페이스는 onStartup 메소드만이 있어 람다식으로 표현이 가능하다.
    onStartup 메소드는 보이드 메소드이고 파라미터로 ServletContext가 있다.

  3. 위의 코드는 파라미터인 ServletContextaddServlet 메소드를 통해 서블릿을 등록하는 코드이다.
    addServlet 은 첫 번째 파라미터는 서블릿의네임이고, 두 번째 파라미터로 HttpServlet 구현체를 넣으면 된다.

  4. 추상 클래스인 HttpServlet 의 메소드 중에서 service 를 오버라이딩 하였고,
    service 메소드는 파라미터로 HttpServletRequestHttpServletResponse가 있다.
    간단하고 포괄적인 의미에서 파라미터로 요청과 응답이 있다고 보아도 괜찮다.

  5. 우리는 HttpServletRequest 에서 파라미터 name 의 값을 가져왔고,
    이를 응답 (HttpServletResponse) 의 바디 (resp.getWriter().println)에 출력 하였다.
    응답의 상태 코드는 200, 그리고 해더에 컨텐트 타입은 Text_Plain 으로 하였다.

3) webServer.start()

2에서 만든 서블릿이 등록된 웹서버를 실행시키는 메소드이다.
그래서 만든 메인 함수를 실행시키면 톰캣이 뜨고 Servlet Container 안에 등록한 hello Servlet이 존재하게 된다.

이전 api 와 똑같이 요청을 보내면 똑같이 동작을 할 것이다.


2.2 FrontController 구축

2.1 의 구조를 변경해서 FrontController 를 구축해 볼 것이다.

FrontController 란?

기존에는 각기 다른 Servlet 이 다른 url 로 들어오는 요청을 맡아서 처리하는 방식으로 동작한 것과 달리,
제일 앞단에 컨트롤러를 만들고 (=모든 요청을 받는 하나의 서블릿) 여기서 공통적인 작업을 다 처리하고 요청의 종류에 따라 적합한 다른 오브젝트 요청을 위임해서 응답값을 전달하는 방식이다.

앞선 코드에서 Servlet 을 등록하는 부분만을 수정하였다.

   WebServer webServer = serverFactory.getWebServer(servletContext -> servletContext.addServlet("frontcontroller", new HttpServlet() {
                HelloController helloController = new HelloController();
                // 1) 서블릿 초기화하는 앞단에서 만듦. 매번 인스턴스를 만들 필요가 없으니까.

                @Override
                protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
                    //2) 인증, 보안, 다국어, 공통 기능

                    //3) 아래는 매핑 역할
                    if (req.getRequestURI().equals("/hello") && req.getMethod().equals(HttpMethod.GET.name())){
                        String name = req.getParameter("name");

                        String ret = helloController.hello(name);

                        resp.setStatus(HttpStatus.OK.value());
                        resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
                        resp.getWriter().println(ret);
                    }
                    else if (req.getRequestURI().equals("/user")){
                        //
                    }else{
                        //아무 것도 매핑되지 못한다면 404 error 를 return
                        resp.setStatus(HttpStatus.NOT_FOUND.value());
                    }
                }
            }).addMapping("/*")); //모든 요청에 대해 실행될 것임.

            webServer.start(); //Tomcat Servlet Container 가 동작하게 됨.

코드를 한 번 살펴보자.

  1. 서블릿이 초기화 되기 전에 HelloController 인스턴스를 만들었다. 이렇게 되면 매번 HelloController 를 생성하는 것이 아니라 기존의 인스턴스를 사용하게 될 것이다. 그리고 addMapping 을 '/*' 로 설정하였다. 이제 모든 요청들이 이 서블릿을 걸쳐 실행되게 될 것이다.

  2. 요청들에 대해 공통적으로 수행하는 코드를 작성한다. 주로 인증, 보안, 다국어, 공통 기능이 담기게 될 것이다. FrontController를 통해 반복적인 코드 작성을 줄인다는 장점을 이 지점에서 코드로 직접 확인할 수 있다.

  3. FrontController 의 매핑과 바인딩 역할을 보여주는 코드이다. HttpServletRequest 객체에서 요청 url 을 꺼내어 요청에 맞는 컨트롤러의 메소드를 호출 (=매핑) 한다. 만약 해당 컨트롤러의 메소드에 파라미터 값이 필요하다면 HttpServletRequest 객체에서 받은 값을 일반적인 형태 (String, Integer 등) 변환해서 넘겨준다. (=바인딩)
    또한, 아무 것도 매핑되지 못했다면 보통 404 error 를 던지는데 위의 코드에서도 그렇게 구현해 보았다.

참고로 이러한 FrontController 는 SpringBoot 에서는 Dispatcher Servlet 이라는 이름으로 동작되고 있다.

과거와 비교하기 ✨✨
과거에는 개발자가 직접 경로마다 서블릿을 xml 형식으로 등록해서 사용을 하였다.
하지만 현대에는 하나의 servlet (여기서 FrontController 라고 불리는) 을 이용해서 스레드를 통해 병렬 실행하는 방식으로 요청을 처리하는 추세이다.
장점은 xml 형식으로 서블릿을 등록하지 않아도 되어 간편하고, 하나의 진입점으로 모든 요청이 들어오니 관리와 공통 부분 처리가 이전보다 편리해졌다.


이렇게 독립 실행형 Servlet Application 을 만들어 보았다.
기존에 알고 있었던 지식들 (ex: Servlet 은 Servlet Container 가 관리하고, Servlet Container 는 웹 서버에 존재한다는 것 , FrontController 의 개념 등) 을 직접 코드로 확인하면서 개념 이해에 더 도움이 되었으면 좋겠다.

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

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

0개의 댓글