Spring Framework/토비의 스프링

Chapter 03. 독립 실행형 서블릿 애플리케이션

계란💕 2023. 6. 20. 00:01

Containerless 개발 준비

  • 컨테이너 리스 방식은 서블릿 컨테이너와 관련된 번거롭고 복잡한 작업들에 대해 개발자가 더이상 신경쓰지 않도록 스프링 컨테이너에 올라가는 빈에만 집중할 수 있도록 해준다. 
  • main() 메서드 하나만 실행시켰는데 Tomcat(Java의 서블릿 컨테이너)이 뜨고 스프링 컨테이너도 뜬다. 애플리케이션을 동작시킨다.
  • 그런데 만약, Spring Boot가 없다면?

 


서블릿 컨테이너 띄우기

  • 서블릿 컨테이너(Servlet container)란?
    • 자바의 표준 기술인 servlet에 대해 서블릿을 구현한 컨테이너를 말하며 종류가 다양하다.  ex) Tomcat
  •  springio를 통해 스프링 프로젝트를 셋업하면 자동으로 내장형 톰캣(embedded tomcat)이라는 라이브러리가 제공된다. 
  • 기본으로 세팅된 HellobootApplication의 애너테이션과 run() 부분을 지우고 직접 구현해보자. 
  • Spring boot는 Tomcat 외에도 Jetty(제티)같은 다른 서블릿 컨테이너도 지원할 수 있고 일관된 방식으로 동작하도록 추상화되어 있다. 

 

  Ex)

<hide/>
public class HellobootApplication {
    public static void main(String[] args) {

        TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer();
        webServer.start();
    }
}
  • TomcatServletWebServerFactory: 스프링 부트가 Tomcat servlet 컨테이너를 내장해서 프로그램에서 코드로 쉽게 시작해서 도우미 역할을 하는 클래스이다. 
    • getWebServer(): 서블릿 컨테이너를 생성하다.
    • WebServer: 인터페이스

   

  Note) 실행 결과 - 404에러 (정상으로 부트가 뜸)

 


서블릿 등록

  • Servlet container안에 들어가는 Servlet은  웹 컴포넌트와 같다. 
  • getWebServer() 안에 매개변수로 ServletContainerInitializer를 넣을 수 있다. 
  • ServletContextInitializer: 메서드가 하나인 functional 인터페이스이며 람다식으로 대체 가능하다. 
  • addServlet(): servlet를 생성한다. 
    • 다음, 어느 서블릿이 처리할 건지 매핑해줘야한다. 
    • addServlet() 을 통해 생성된 서블릿에 이어서 addMapping()을 통해서 매핑해준다. 
    • addMapping("/hello"): "/hello"를 통해 들어온 웹 요청이 service 메서드로 들어온다. 
    • 웹 응답의 세 가지 요소 status code/body/content-type를 세팅한다. 
  • HttpServlet를 상속하는 익명클래스를 만든다. HttpServlet 안에 선언된 메서드의 일부를 override
    • service(): 요청을 만드는데 필요한 object, 응답을 만드는데 필요한 object가 매개변수로 전달된다. 

 

  Ex) 

<hide/>

public class HellobootApplication {

    public static void main(String[] args) {

        TomcatServletWebServerFactory 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().print("Hello Servlet ! ");
                }
            }).addMapping("/hello");
        });
        webServer.start();
    }
}

 

  Note) 실행 결과

 


서블릿  요청 처리

  • 앞에서 살펴본 방식을 보면 Header를 String 형태로 하드 코딩해서 넣는 형태인데 이는 오타를 낼 수 있어서 위험하다.
  • 따라서, Enum 형태로 정의된 클래스를 활용한다. HttpHeaders.CONTENT_TYPE

 

  Ex) 

<hide/>
public static void main(String[] args) {
        TomcatServletWebServerFactory 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().print("Hello ! " + name);
                }
            }).addMapping("/hello");
        });
        webServer.start();
    }
}

 

  Note) 실행 결과

 


프론트 컨트롤러

  • 앞서 살펴본 서블릿은 각 요청에 대해 담당한 서블릿을 하나씩 매핑해주는 작업이 필요했다. 
  • 그런데, 대부분의 서블릿에서 필요로 하는 공통적인 작업이 각 서블릿 코드 안에 중복되어 존재한다. 중복을 없애서 효율성을 높이자.
  • 각 URL에 맞게 각기 다른 서블릿이 매핑된다. 
  • 모든 서블릿에 공통적으로 필요한 작업에 대해 컨트롤러라는 오브젝트에서 공통적으로 처리
  • 요청의 종류에 따라서, 로직을 처리하는 다른 오브젝트에게 다시 위임해서 전달하는 방식으로 처리하려한다. 
  •  프론트 컨트롤러를 내장한 프레임워크가 등장하기 시작했다. 
    • ex) 인증, 보안, 다국어 처리, 공통 처리 등

 


프론트 컨트롤러로 전환

  • 기존에 만들었던 "hello"라는 서블릿 이름을 frontcontroller로 변경한다. 
  • 중앙화된 처리를 위해서 모든 요청을 다 받을 수 있도록 "/*" 라고 넣어준다. 
  • 기존에는 서블릿 컨테이너가 url을 통해서 서블릿을 매핑했다면 이제는 프론트 컨트롤러가 그 역할을 하려한다. 
  • 따라서, 다음과 같이 url 값에 따라서 로직을 구분하려한다. 
  • 그럼  컨트롤러 클래스의 @GetMapping 은 어떻게 기능을 구현할 수 있을까?
    • request.getMethod()를 통해서 Http 메서드의 종류를 파악할 수 있다. 

 

 

  Ex)

<hide/>
public static void main(String[] args) {
    TomcatServletWebServerFactory 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().print("Hello ! " + name);
                }
                else if(req.getRequestURI().equals("/user")){

                }else {
                    resp.setStatus(HttpStatus.NOT_FOUND.value());
                }
            }
        }).addMapping("/*");
    });
    webServer.start();
}

 

  Note) 실행 결과

  • 위 로직의 else문으로 들어가서 404 에러가 반환된다. 

 


Hello 컨트롤러 매핑과 바인딩

  • 앞에서 만든 컨트롤러 클래스를 하나 만들어서 컨트롤러 안에 있는 메서드 hello()에 처리를 위임한다. 
  • 웹 요청에 대해서 인자 값을 넘겨주는 걸 "바인딩" 이라고 한다. 
  • 지금까지 스프링을 사용하지 않고 서블릿 기술만 가지고 프론트 컨트롤러를 만들었다.  
<hide/>
public static void main(String[] args) {
    TomcatServletWebServerFactory 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 result = helloController.hello(name);
                    resp.setStatus(HttpStatus.OK.value());
                    resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
                    resp.getWriter().print(result);
                }
                else if(req.getRequestURI().equals("/user")){

                }else {
                    resp.setStatus(HttpStatus.NOT_FOUND.value());
                }
            }
        }).addMapping("/*");
});
    webServer.start();
}

 


title

  • content
  • content

title

  • content
  • content

 

출처 - 인프런 토비의 스프링

https://www.inflearn.com/course/%ED%86%A0%EB%B9%84-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%9D%B4%ED%95%B4%EC%99%80%EC%9B%90%EB%A6%AC/dashboard