Spring으로 Servlet을 다룬다는 것

hyyyynjn·2021년 11월 22일
4
post-thumbnail

Servlet이란

평범한 웹 서버

  • 처음의 웹서버는 클라이언트 요청에 대해 정적인 페이지로만 응답할 수 있었다
    -> 웹서버에 웹서버 프로그램을 붙여 동적인 페이지를 생성하였다

서블릿도 동적인 페이지를 만들기 위해 웹서버에 붙이는 프로그램 중 하나이다

  • HTTP 요청, 응답 정보이다.
    만약 개발자들이 위와 같은 텍스트를 처리, 분석하고 규약에 맞게 응답으로 내보낸다면 개발자들이 해야할 일이 많아진다.

  • 텍스트 형태의 HTTP 요청을 파싱하는 것을 HttpServletRequest.getMethod() 메소드를 호출하여 대신할 수 있다.

서블릿을 이용하여 웹 요청을 다루면, 개발자들은 비지니스 로직에 더 집중할 수 있게 된다.


public class MyServlet extends HttpServlet {

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
    }

    @Override
    public void destroy() {
        super.destroy();
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.service(req, resp);
    }
}
  • 서블릿이 생성되면 init 메소드가 호출되고
    소멸되면 destroy 메소드가 호출된다.
  • service 메소드는 클라이언트의 요청을 처리할 때 호출되는 메소드이다.
public abstract class HttpServlet extends GenericServlet 
{
    private static final String METHOD_DELETE = "DELETE";
    private static final String METHOD_HEAD = "HEAD";
    private static final String METHOD_GET = "GET";
    private static final String METHOD_OPTIONS = "OPTIONS";
    private static final String METHOD_POST = "POST";
    private static final String METHOD_PUT = "PUT";
    private static final String METHOD_TRACE = "TRACE";
    
// 중략

    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // servlet doesn't support if-modified-since, no reason
                // to go through further expensive logic
                doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < lastModified) {
                    // If the servlet mod time is later, call doGet()
                    // Round down to the nearest second for a proper compare
                    // A ifModifiedSince of -1 will always be less
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
            
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
            
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
            
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
            
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
            
        } else {
            //
            // Note that this means NO servlet supports whatever
            // method was requested, anywhere on this server.
            //

            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);
            
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }

}
  • HttpServlet 추상 클래스를 보면, service 메소드의 역할을 자세히 알 수 있다.
    HTTP Method XXX에 대한 요청이 들어오면 -> doXXX() 메소드를 호출한다.

개발자들은 처리하고 싶은 요청 HTTP 메소드에 해당하는 doXXX() 메소드를 재정의해주면 된다.

public class MyServlet extends HttpServlet {

// 중략

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter writer = resp.getWriter();
        writer.println("hello hjoon!");
    }
}
  • MyServlet이 처리할 URL을 매핑해주면,
    해당 URL로 GET 메소드 요청이 들어올 때 재정의된 doGet() 메소드가 실행된다.

서블릿으로 요청을 처리하기 위해서 service의 메소드만 재정의해서 처리 방법을 지정하면 된다.

Servlet Container & Servlet 동작 방식

서블릿 컨테이너는 서블릿을 담아 관리하는 녀석이다.

  • 사용자의 요청이 들어오면 서블릿 컨테이너는 해당 요청과 매핑된 서블릿을 설정 파일 web.xml을 통해 찾는다.

    <servlet>
        <servlet-name>MyServlet</servlet-name>
        <servlet-class>com.jsp.web.MyServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>MyServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
    • 서블릿 컨테이너가 어떤 서블릿이 어떤 요청과 매핑되어있는지 알기 위해 위와 같이 설정파일 web.xml에 미리 정의되어있어야 한다.
  • 서블릿 컨테이너는 설정 파일을 읽고 필요한 서블릿을 확인한 후, 해당 서블릿 인스턴스가 컨테이너에 있는지 확인한다.
    -> 있다면, 그 인스턴스를 그대로 사용하고
    -> 없다면, 생성한다 (이 때, 서블릿의init 메소드가 호출된다)

  • 서블릿 컨테이너의 스레드를 생성하고, 미리만든 HttpServletResponse, HttpServletRequest 객체를 인자로 service 메소드를 호출한다.
    (서비스가 호출될때, 재정의된 doXXX() 메소드가 수행된다)

  • 이후 HttpServletResponse, HttpServletRequest 객체를 소멸시킨다.
    하지만 서블릿은 소멸시키지 않는다.

생성한 서블릿을 소멸시키지 않는 이유는 무엇일까
-> 서블릿은 Singleton 으로 관리되기 때문이다.

  • 서블릿 인스턴스는 소멸되지 않고 그대로 있다가
    다음번에 같은 요청이 들어올 경우
    서블릿 컨테이너에 의해 다시 호출되어 재사용 된다.

서블릿 컨테이너는 결국 서블릿의 생명주기를 관리하는 객체이다.

  • 서블릿을 생성하고, 필요한 시점에 호출하고, 적절한 시점에 소멸시키는 역할을 한다.
    이 과정을 개발자가 아니라 서블릿 컨테이너에게 위임된다.

만약 하나의 요청을 처리하는 도중에 다른 요청이 들어오면 어떻게 될까?

  • 서블릿 컨테이너멀티스레드로 이를 처리한다.

  • 스레드당 다른 서블릿이 처리할 수 있고,
    여러 스레드에서 한 서블릿의 여러 요청을 동시에 처리할 수 있다.

  • 멀티스레드를 사용한다는 것 자체만으로 조심해야 한다.
    -> 멀티스레드 자체로 큰 비용이 발생한다. (Context Switching 시 오버헤드 발생)
    -> 요청에 따라 많은 스레드를 생성하다 서버의 하드웨어적 한계를 넘어버리면 터질 수 있다.

요청마다 서블릿을 정해주는 매커니즘은 비효율적이다.

  1. 멀티스레딩을 다루어야 한다
    -> 비용 측면에서 비효율적

  2. Handler의 공통 로직이 매번 중복된다
    -> 개발 측면에서 비효율적

Front Controller 패턴

클라이언트의 요청을 앞 단에서 처리하는 매니저를 두는 패턴이 프론트 컨트롤러 패턴이다.

Spring MVC는 프론트 컨트롤러 패턴을 따른다.

  • 모든 요청을 받는 프론트 컨트롤러를 Dispatcher Servlet이라고 한다.
    -> 하나의 Dispatcher Servlet만 두고 모든 요청을 다 받을 수 있다.

  • 이전에는 요청에 따라 모든 서블릿을 정의하고
    요청을 수행할 때마다 매번 스레드를 생성했다.

  • 프론트 컨트롤러를 사용하고나서
    하나의 서블릿만 정의하고 모든 요청을 수행할 수 있다.

DispatcherServlet의 요청 처리 과정

클라이언트의 요청이 많이 몰려서 Dispatcher Servlet의 작업이 밀리면 어떡하지..?
-> 역할 분담을 해야 한다.

  • Dispatcher Servlet
    모든 클라이언트의 요청을 받고

  • Handler Mapping
    요청을 처리할 때, Controller를 찾아서 반환해주고

  • Handler Adapter
    해당 Controller의 메소드를 호출하여 처리 로직을 수행하고
    그 결과를 Model And View 객체로 변환해서 Dispatcher Servlet에게 넘기고

  • View Resolver
    Dispatcher ServletView Resolver를 통해 View를 찾거나 생성한다.

View에 Model 데이터를 넣어 JSP, Thymeleaf 처럼 데이터를 담은 출력 파일로 응답한다.

하지만 이와 같이 역할 분담 자체만으로 일이 많아진다.

  • Spring 덕분에 Handler Mapping, Handler Adapter, View Resolver 들을 개발자가 직접 개발할 필요가 없어진다.
    -> 이 3개는 Spring Container로부터 주입받아 Dispatcher Servlet이 이들을 사용하고 동작하게 된다.

  • 결국 개발자들이 신경써야할 부분은 처리 Handler (Controller) 뿐이다.

Spring Container

프로그램이 동작하는 동안 사용되는 Java 객체들을 프레임워크가 대신 관리하게 하고 보관하기 위해서 사용되는 바구니이다.

  • Servlet WebApplicationContext
    웹 요청 처리와 관련된 객체들이 담겨 있다.
    (Controllers, ViewResolver, HandlerMapping)

  • Root WebApplicationContext
    웹 요청 처리와 관련된 Bean 이외의 컴포넌트들이 관리된다.
    (Service, Repositories)

Spring Container는
개발에 필요한 부분이나 Dispatcher Servlet이 요청을 처리할 때 필요한 부분을 알아서 주입해준다.

  • 서블릿 설정파일을 작성해주면
    설정대로 생성된 객체가 Spring Container에 의해 관리되고
    필요한 부분에 주입받아 Dispatcher Servlet이 알아서 사용할 수 있게 된다.

  • Dispatcher Servlet의 요청처리 뿐만아니라
    Controller등을 포함한 어플리케이션 안에서
    필요한 부분에 Root WebApplicationContext 를 활용하여 개발할 수 있다.

결국 Spring으로 웹 요청을 처리한다는 것은
Spring MVC에서 제공하는 Dispatcher Servlet과 웹 요청 처리 관련 구현체들을 사용할 수 있다는 뜻이다.
동시에 Spring Container, 즉 Spring IoC를 활용하여 개발할 수 있다는 의미다.

  • 이로써 개발자는 요청처리 로직들에만 신경 쓸 수 있게 된다.

  • 살펴보면 좋을 키워드들

0개의 댓글