[스프링]Spring MVC과 Servlet?

JANG SEONG SU·2023년 8월 24일
0

Sping

목록 보기
4/9

1. Servlet 이란?

Servlet 등장 배경

초기 웹서버(Web Server)는 정적 페이지만 요청/응답할 수 있었다. 한마디로 웹 서버(Web Server)는 단순히 정해진 HTML파일을 주고 받는 그냥 무식한 서버라고 생각하면 된다. 현재 우리가 자주 접하는 웹 페이지는 대부분 동적 페이지로, 예를 들어 우리가 네이버에 로그인을 하면 우리 이름이 "***님 안녕하세요" 라고 뜨는 것은 실제로 웹 서버의 뒤에 WAS(Web Application Server)가 동적 페이지를 처리해줬기 때문이다.

심지어 개발자들이 위와 같은 요청 텍스트를 직접 처리하고 파싱하여, 다시 규약에 맞게 응답 텍스트를 만들어 보내야 한다면 매우 번거롭다.
(앞으로 나올 Servlet은 이러한 과정을 대신 처리해주어 개발자는 비즈니스 로직에 더 신경쓸 수 있게 됨)

더 자세한 설명은 생략하고, WAS가 등장하기 이전에 사실은 CGI(Common Gateway Interface)라는 인터페이스가 등장하여 동적 데이터의 처리를 가능하게 해주었다.

CGI 등장 이후 웹 서버인 Apache와 그 뒤에 CGI 사이에 규약을 만들어 동적 웹 페이지를 구현할 수 있었다. CGI에는 문제가 있었다. 바로 요청이 들어올 때마다 Process를 생성하여 처리한다는 것이었다. 심지어 좀 전과 같은 요청이 들어와도 이를 재사용하지 못하고 새로운 Process를 생성하는 등 비효율적인 동작이 이루어졌다.

이를 개선하기 위해, 드디어 Servlet이 등장한다.

이전의 매 요청마다 Process를 생성하는 CGI를 개선하여, 매 요청마다 Process 생성보다 비용이 작은 Thread를 생성하는 방식으로 바뀌었다. 생성된 Thread는 Servlet 구현체와 연결되고, Servlet 인터페이스를 구현받은 구현체의 메소드를 실행한다.

Servlet 생명주기

  • init() : 서블릿 인스턴스 생성
  • service() : 실제 기능이 수행됨
    • Http Method(GET,POST,PUT,DELETE)에 따라 doGet(), doPost(), doPut(), doDelete()를 실행
  • destory() : 보통 container가 종료되는 시점에 호출

👉이때 각 메소드는 Servlet Container에 의해 호출되고, Servlet Container는 서블릿 인스턴스의 생명주기를 관리한다.

Tomcat은 WAS중 하나로, Servlet Container 기능을 제공한다. Tomcat을 서블릿 컨테이너라고 부르긴 하지만, 엄밀히 말하면 내장 웹 서버 등의 부가 기능도 제공하므로 WAS라고 부르는 것이 더 타당하다. Servlet ⊂ Tomcat

Servlet 동작과정

  • 사용자가 URL을 입력하면 요청이 서블릿 컨테이너로 전송된다.
  • 요청을 전송 받은 서블릿 컨테이너는 HttpRequest, HttpResponse 객체를 생성한다.
  • 사용자가 요청한 URL이 어느 서블릿에 대한 요청인지 찾는다. 위 예제에서는 helloServlet을 찾게 된다.
  • 서블릿의 service() 메소드를 호출한 후 클라이언트의 GET, POST 여부에 따라 doGet(), doPost() 메소드를 호출한다.
  • 동적 페이지를 생성한 후 HttpServletResponse 객체에 응답을 보낸다.
  • 클라이언트에 최종 결과를 응답한 후 HttpServletRequest, HttpServletResponse 객체를 소멸한다.

하지만 멀티 스레딩 역시 오버헤드는 존재한다.(스레드 생성 비용/컨텍스트 스위치 등등)

멀티 스레드의 문제는 Thread Pool(스레드 풀)로 개선을 하였고, 이에 대한 포스팅은 추가로 작성할 예정이다.

또한 각 Servlet마다 중복 코드가 존재하게 된다. 이를 해결하기 위해 드디어 마지막 Spring MVC가 등장한다.

Spring MVC 등장 배경

순수 Servlet 시절

Spring MVC가 없던 과거에는, URL마다 서블릿을 생성하고 web.xml로 서블릿을 관리했다. URL마다 서블릿이 필요하다 보니, 매번 서블릿 인스턴스를 만들어야했다. 또한, 각 서블릿마다 공통 기능을 하는 코드들이 중복해서 발생하기도 했다.

web.xml

<web-app>
    <!-- 1. aliases 설정 -->
    <servlet>
        <servlet-name>welcome</servlet-name>
        <servlet-class>servlets.WelcomeServlet</servlet-class>
    </servlet>

    <!-- 2. 매핑 -->
    <servlet-mapping>
        <servlet-name>welcome</servlet-name>
        <url-pattern>/welcome</url-pattern>
    </servlet-mapping>

</web-app>

특정 url 요청이 오면 서블릿 컨테이너가 xml설정 파일을 통해 해당 서블릿이 동작하게 해준다.

WelcomeServlet.class

public class WelcomeServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String username = request.getParameter("username"); // 요청 값 꺼내기
        String authorizationHeader = request.getHeader("authorization"); // 헤더 값 꺼내기
        
        /* do something */
        /*
        * Request Method에 따라
        * doGet(), doPost(), doPut(), doDelete()를 실행한다.
        */

        response.setContentType("text/plain"); // 응답 형식 설정
        response.setCharacterEncoding("utf-8"); // 응답 인코딩 설정
        response.getWriter().write("hello " + username); // HTTP Response 메시지 작성
    }
}
  1. service 메서드에 있는 request, response 매개변수는 서블릿(Tomcat이라고도 할 수 있음)에서 처리를 해준 결과이고, GET과 Form의 POST 요청은 모두 HttpServletRequest.getParameter() 로 간단하게 꺼낼 수 있다.
  2. Request Method에 따라 service 메소드 내에서 doGet(), doPost(), doPut(), doDelete()를 실행한다. Servlet에서 doGet(), doPost()와 같은 메소드를 직접 실행하지 않고, service 내에서 실행하는 이유는 서블릿 컨테이너에서는 서블릿이 어떤 http 메서드인지 알 필요없이 service() 해주면 되어서 코드가 더 간결해지기 때문이라고 생각한다.
    실제 구현할 때는 service메소드 수정 없이 doGet(), doPost()와 같은 메소드만 오버라이드해서 직접 구현한다.
  3. service 메소드에서 Json return 타입을 만들거나, View를 만들어서 클라이언트에게 전송하는 방식이다.
  4. 서블릿 객체는 싱글톤이기 때문에 한 번 생성되면 이후에 재사용하여 호출만 하게 된다.

하지만 여전히 서블릿 간의 코드 중복이 발생하기 때문에, 이를 해결하기 위해 Front Controller 패턴이 등장하게 된다.

Front Controller

Front Controller 패턴은 모든 요청을 프론트 컨트롤러라는 하나의 서블릿에게 보내고, 프론트 컨트롤러는 각 요청에 맞는 Controller를 찾아서 호출하는 역할을 한다. 그래서 공통 기능은 프론트 컨트롤러에서 처리하고, 서로 다른 코드들만 각 컨트롤러에서 처리하도록 할 수 있다.

이것은 Spring MVCDispatcher Servlet과 같다.

2. Dispatcher Servlet

web.xml

<web-app>
        <servlet>
                <servlet-name>example</servlet-name>
                <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
                <load-on-startup>1</load-on-startup>
        </servlet>

        <servlet-mapping>
                <servlet-name>example</servlet-name>
                <url-pattern>/example/*</url-pattern>
        </servlet-mapping>

</web-app>

이전의 순수 Servlet에서는 여러 개의 서블릿을 모두 web.xml에서 관리하였지만, Front Contrlloer 패턴을 적용한 Spring MVC에서는 Dispatcher Servlet 1개만 web.xml에 관리해주면된다.
결국 모든 요청은 이 Dispatcher Servlet 으로 보내지게 된다.

  1. 클라이언트의 웹 요청이 DispatcherServlet 으로 간다.
  2. 웹 요청을 Handler Mapping에 위임하여 해당 요청을 처리할 Handler(Controller)를 탐색한다. (보통은 DefaultAnnotationHandlerMapping 방식으로 어노테이션으로 url과 매핑하는 방식 사용)
  3. 찾은 Handler(Controller)를 실행할 수 있는 HandlerAdapter를 탐색한다.
  4. 찾은 Handler Adapter실제 Handler의 process 메소드를 실행한다.
  5. Handler의 반환 값은 ModelandView이고, DispatcherServlet에게 전달한다.
  6. DispatcherServlet은 View 이름을 ViewResolver에게 전달하고, ViewResolver는 해당하는 실제 View 객체를 전달한다.
  7. DispatcherServletView에게 Model을 전달하고 화면 표시를 요청한다. (Model은 Controller에서 만든 데이터이다.) 이때, Model이 null이면 View를 그대로 사용하고, 그렇지 않으면 View에 Model 데이터를 렌더링한다.
  8. 최종적으로 DispatcherServlet은 View 결과(HttpServletResponse)를 클라이언트에게 반환한다.

Hadnler(=Controller) 매핑에서 찾은 Handler로 바로 Controller를 실행하지 않고, 굳이 Handler Adapter를 통해 Controller를 실행하는 이유는 인터페이스가 다른 Controller 구현체들을 return 값이 ModelandView로 통일된 Adapter로 실행하기 위해서이다.
즉, 인페이스가 다른 어떤 Controller이든, Adapter를 통해서 위 사진의 구조를 유지할 수 있다.

위 흐름은 @Controller 기준이며, @RestController의 경우 6번과 7번 과정이 생략된다. 즉, ViewResolver를 타지 않고 반환 값에 알맞는 MessageConverter를 찾아 응답 본문을 작성한다.


profile
Software Developer Lv.0

0개의 댓글