독립 실행형 서블릿 애플리케이션 살펴보기 (1)

들어가며


  • 해당 포스팅은 토비의 스프링 부트 - 이해와 원리를 학습하며 정리한 글입니다.
  • 토비님의 강의 내용과 제가 이해한 방식대로 작성된 부분이 혼합 되어 작성되어 있습니다.

0. Containerless 개발 준비


package tobyspring.helloboot;

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

@SpringBootApplication
public class TobyspringApplication {

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

}
  • spring boot를 사용하면 간단하게 코드 몇줄로 서버를 실행시킬 수 있습니다

  • 이러한 서버가 어떻게 만들어지는지에 대해 도움을 받지 않고 만들어보며 이해해 해보겠습니다!

  • 컨테이너 설치와 배포 등의 작업을 하지 않고 서블릿 컨테이너를 동작시키는 방법을 코드로 구현합니다.

package tobyspring.helloboot;
public class TobyspringApplication {

    public static void main(String[] args) {
        System.out.println("hello containerless");
    }

}
  • 다음과 같이 스프링 부트가 사용하는 라인들을 모두 제거하고, 빈 main() 메소드만 남깁니다.

1. 서블릿 컨테이너(톰캣 웹 서버) 띄우기


  • 첫번째 스텝으로 다음과 같은 구조를 갖출 수 있게 구성합니다.

  • Servelt Container를 띄웁니다.

  • 스프링 부트 프로젝트를 만들 때 web 모듈을 선택하면 다음과 같이 내장형 톰캣 라이브러리가 추가됩니다.

코드


package tobyspring.helloboot;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;

public class TobyspringApplication {

    public static void main(String[] args) {
        ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer();
        webServer.start();
    }

}
  • 다음과 같이 spring에서 제공하는 TomcatServletWebServerFactory 인스턴스를 생성합니다.

  • ServletWebServerFactory의 경우 추상화 되어 있습니다.

  • 만약 톰캣이 아닌 제티를 사용하고 싶다면, TomcatServletWebServerFactory가 아닌 JettyServletWebServerFactory를 사용해도 됩니다.

실행 및 확인


  • 다음과 같이 톰캣 서버가 구동이 되는 확인 할 수 있습니다.

  • 톰캣 서버(서블렛 컨테이너)가 띄어진 것을 확인해 볼 수 있습니다.

2. 서블렛 추가


  • 두번째 스텝으로 다음과 같은 구조를 갖출 수 있게 구성합니다.

  • 이제 요청을 받아 기능을 수행하는 Servelt 을 등록합니다.

코드


ServletContextInitializer

package tobyspring.helloboot;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;

public class TobyspringApplication {

    public static void main(String[] args) {
        ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer(new ServletContextInitializer() {
            @Override
            public void onStartup(ServletContext servletContext) throws ServletException {

            }
        });
        webServer.start();
    }
}
  • serverFactory.getWebServer() 에 전달하여 Servlet을 등록합니다.

  • 서블릿을 등록하려면 ServletContext가 필요합니다.

  • 이를 전달하고 등록하는 초기화 작업을 할 때는 ServletContextInitializer를 구현한 오브젝트(new ServletContextInitializer())를 ServletWebServerFactoy의 getWebServer 메서드에 전달합니다.

  • parameterServletContextInitializer입니다.

    • 서블릿 컨테이너서블릿을 등록하는데 필요한 작업을 수행하는 Object를 만들때 사용합니다.
  • 추상화된 Interface이며, onStartup 메소드를 Override 합니다.

public static void main(String[] args) {
    ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer(servletContext -> {

        });

        webServer.start();
}
  • 다만 이는 @FunctionalInterface 이므로 이와 같이 간편하게 람다식으로 전환해서 사용합니다.

ServletContext 등록


package tobyspring.helloboot;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class TobyspringApplication {

    public static void main(String[] args) {
        ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer(servletContext -> {
            servletContext.addServlet("hello", new HttpServlet() {
                @Override
                protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                    resp.setStatus(200);
                    resp.setHeader("Content-type","text/plain");
                    resp.getWriter().println("Hello Servlet");
                }
            }).addMapping("/hello");
        });
        webServer.start();
    }

}
  • 서블릿을 등록할 때에는 ServletContext.addServlet() 을 통해 서블릿 이름서블릿 오브젝트를 이용합니다.

    • 여기서는 서블릿 이름을 hello로 하고, 서블릿 오브젝트(new HttpsServlet())를 등록합니다.
  • 서블릿에서는 HttpServletRequest를 이용해서 요청 정보(req)를 가져오고, HttpServletResponse 를 통해 응답을 만드는 작업(resp)을 수행합니다.

응답 생성


 resp.setStatus(200);
 resp.setHeader("Content-type","text/plain");
 resp.getWriter().println("Hello Servlet");
  • 3가지 요소(상태 코드, 헤더, 바디)를 이용해서 response를 생성합니다.

  • 하지만 이렇게 하드코딩하면 실수할 수 있기 때문에 다음과 같이 enum을 활용합니다.

resp.setStatus(HttpStatus.OK.value());
resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().println("Hello Servlet");

매핑


servletContext.addServlet("hello", new HttpServlet() {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setStatus(HttpStatus.OK.value());
        resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
        resp.getWriter().println("Hello Servlet");
    }
}).addMapping("/hello");
  • 이때 서블릿어떤 역할을 해야 할지 서블릿 컨테이너결정해주는 Mapping을 해줘야 합니다.

    • 해당 작업은 .addMapping("/hello"); 를 ServletContext.addServlet()에 체이닝하여 수행할 수 있습니다.

    • /hello 경로로 매핑 작업을 합니다.

서블릿 요청 처리


  • 웹 클라이언트로부터 전달 받은 요청을 서블릿 기능을 작성할 때 활용합니다.
servletContext.addServlet("hello", new HttpServlet() {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, 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");
  • 여기서는 URL로 전달된 파라미터 값을 추출해서 사용합니다.

  • 다음과 같이 서블릿 컨테이너서블릿이 잘 등록되고, 매핑을 통해 알맞는 서블릿을 찾아가서 기능이 잘 수행되는 것을 확인할 수 있습니다.

3. 프론트 컨트롤러


  • 모든 서블릿에 공통적으로 등장하는 코드를 중앙화된 컨트롤러 오브젝트에서 일괄적으로 처리하게 만드는 방식을 프론트 컨트롤러 패턴이라고 합니다.

  • 서블릿은 요청마다 각각 직접 매핑을 해야합니다.

  • 그래서 요청이 많아지면 서블릿이 여러개가 됩니다.

  • 그렇게 되면 서블릿 코드안에 공통적인 작업들이 중복되어서 등장합니다.

  • 그래서 이를 해결하기 위해 프론트 컨트롤 패턴을 사용합니다.

package tobyspring.helloboot;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class TobyspringApplication {

    public static void main(String[] args) {
        ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer(servletContext -> {
            servletContext.addServlet("frontcontroller", new HttpServlet() {
                @Override
                protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                    // 인증, 보안, 다국어, 등 공통 기능 처리

                    if (req.getRequestURI().equals("/hello") && req.getMethod().equals(HttpMethod.GET.name())) {
                        String name = req.getParameter("name");

                        resp.setStatus(HttpStatus.OK.value());
                        resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
                        resp.getWriter().println("Hello" + name);
                    } else if (req.getRequestURI().equals("/user")) {
                        //
                    } else {
                        resp.setStatus(HttpStatus.NOT_FOUND.value());
                    }

                }
            }).addMapping("/*");
        });
        webServer.start();
    }

}
  • .addMapping("/*") 를 ServletContext.addServlet()에 체이닝하여 수행할 수 있습니다.

  • /* 와일드 카드 경로로 모든 매핑 작업을 합니다.

  • 다음과 같이 req.getRequestURI() 를 통해 경로를 파악하고, req.getMethod() 를 통해 메소드를 파악하여 각각의 요청들을 구분지어 로직을 나눕니다.

  • 이렇게 구분짓게 되면 공통적인 작업을 처리할 수 있고, 서블릿 마다 똑같은 코드를 여러번 사용할 필요도 없습니다.

package tobyspring.helloboot;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class TobyspringApplication {

    public static void main(String[] args) {
        ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer(servletContext -> {
            HelloController helloController = new HelloController();
            servletContext.addServlet("frontcontroller", new HttpServlet() {
                @Override
                protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                    // 인증, 보안, 다국어, 등 공통 기능 처리

                    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 {
                        resp.setStatus(HttpStatus.NOT_FOUND.value());
                    }

                }
            }).addMapping("/*");
        });
        webServer.start();
    }

}
  • 다음과 같이 기능을 담당하는(service) 부분을 구분해서 작동시킬 수 있습니다.

  • helloController 객체 인스턴스를 생성해서 hello 메소드를 통해 기능을 구분시킵니다.

4. 마치며


  • 독립 실행형 애플리케이션을 간단하게 구현하였습니다.

  • 물론 스프링 부트를 사용하면 구현이 되어 있기에, 서블릿 컨테이너와 서블릿을 생성하지 않아도 편리하고 간단하게 서버를 구동시킬 수 있습니다.

  • 하지만 이러한 과정을 통해서 스프링 부트가 어떻게 돌아가는지 이해할 수 있고 이를 통해 스프링 부트를 더 잘 사용할 수 있습니다.