Post

스프링 부트 2 - 스프링 없이 서블릿으로 애플리케이션 만들기

임베디드 톰켓

서블릿은 자바의 표준 기술이고, 이 표준 기술을 구현한 제품들이 많이 존재한다. 그 중 가장 대표적인 서블릿 컨테이너가 톰켓이다.

톰켓도 자바로 만들어진 프로그램이다. 즉, 클래스에 오브젝트로 만들고 어떤 메소드를 실행하면 동작한다. 톰켓은 ‘임베디드 톰켓’이라는 라이브러리를 제공해준다. 스프링 부트를 처음 프로젝트 생성할 때 이미 임베디드 톰켓이 라이브러리에 들어와있다.

톰켓을 정상적으로 실행하고, 서블릿을 띄우기 위해서는 준비해야 하는 설정들이 꽤 많이 있다. 하지만 우리가 굳이 몰라도 괜찮다. 톰켓 서블릿 웹 서버의 복잡한 생성 과정과 복잡한 설정 등을 미리 결정하고, 우아하게 생성해주는 도우미 클래스가 있다. 바로 ServletWebServerFactory이다.

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

먼저 ServletWebServerFactory을 생성해준다. 이때 구현체로 TomcatServletWebServerFactory(톰켓)를 선택해주자. 팩토리에서 WebServer를 만든 후 start하면, 서블릿 컨테이너(톰켓) 띄우기는 성공이다.


서블릿 추가하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 {
                    String name = req.getParameter("name");
                    resp.setStatus(HttpStatus.OK.value());
                    resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
                    resp.getWriter().println("Hello " + name); // body
                }
            }).addMapping("/hello");
        });
        webServer.start();
    }

위 코드를 한 번에 파악하기는 어려우니, 조금씩 분리해서 알아보자.

1
WebServer webServer = serverFactory.getWebServer(servletContext -> {...});

serverFactory.getWebServer()의 파라미터로 서블릿 컨텍스트를 넣어줄 수 있다. 일단 여기서는 익명 객체로 만들고자 한다. 또한 서블릿 컨텍스트가 구현하는 인터페이스가 @FunctionalInterface 이므로 람다식으로 전환이 가능하다.


1
2
servletContext.addServlet(...) // 서블릿 추가
  .addMapping("/hello"); // 서블릿이 처리해야 하는 urlPattern

servletContextaddServlet이라는 함수의 파라미터를 통해 서블릿을 추가할 수 있다. 또한 그렇게 생성된 객체에 .addMapping()을 통해 이 서블릿이 처리해야 하는 urlPattern을 지정해줄 수 있다.


1
2
3
4
5
6
7
8
9
10
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); // body
        }
    }
).addMapping("/hello");

서블릿 생성 시 설정을 복잡하게 넣어줄 수 있지만, 여기서는 많은 부분이 이미 결정되어 있는 HttpServlet을 사용하자.

HttpServlet에 오버라이딩 가능한 메소드가 여러개 있다. 우리는 service(HttpServletRequest req, HttpServletResponse resp)만 직접 구현하자.

service 함수 바디는 response의 status, header(contentType), body를 지정해주는 코드이다.


이제 애플리케이션을 실행한 후 /localhost:8080/hello에 요청을 보내면, 정상적인 응답이 반환되는 것을 확인할 수 있다.



프론트 컨트롤러

위에서 만든 서블릿에는 문제가 하나 있다. 모든 서블릿이 처리해야할 공통의 로직이 있다고 가정하자. 대표적으로 인증, 보안, 다국어, 공통 기능 등이 있다. 만약 서블릿이 100개 라면, 똑같은 코드들이 100번 등장하게 된다.

그래서 등장한게 프론트 컨트롤러이다. 모든 클라이언트의 요청을 받아서 공통의 로직을 처리해준다. 공통 로직 처리 이후에는 요청의 종류에 따라서 로직을 처리하는 다른 오브젝트한테 요청을 위임한다. 만약 공통의 후처리가 필요하다면, 모든 response가 프론트 컨트롤러를 거치게 만들면 된다.

이 방식을 프론트 컨트롤러 패턴이라고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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); // body
        }
        else if (req.getRequestURI().equals("/user")) {
          // Something..
        }
        else {
          resp.setStatus(HttpStatus.NOT_FOUND.value());
        }
      }
    }).addMapping("/*");
  });
  webServer.start();
}

아까 서블릿을 추가하는 코드와 크게 다르지 않다. 다른 점은

  1. 컨트롤러 오브젝트를 생성한다. (공통 로직을 제외한, 나머지 로직 처리를 위임할 오브젝트)
  2. Request의 Uri에 따라서(if문) 수행할 로직을 작성한다.(매핑)


지금까지는 스프링을 사용하지 않고, 서블릿만을 사용하여 애플리케이션을 만들었다. 다음 게시물에서는 스프링을 사용하여 독립 실행형 애플리케이션을 만들어보자.



Reference

해당 게시물은 인프런 - 토비의 스프링 부트 이해와 원리을 기반으로 작성되었습니다.

This post is licensed under CC BY 4.0 by the author.