본문 바로가기
Web

[Spring] Servlet과 Spring

by DuncanKim 2022. 7. 27.
728x90

[Spring] Servlet과 Spring

 

 

1. 서블릿이란?

 

서버 어플리케이션 조각 -> Server Application Let -> Servlet

 

처음 웹서버는 클라이언트의 요청에 따라 정적인 페이지로만 응답할 수 있었다. 웹 서버에 동적인 프로그램을 붙여서 페이지를 보여주는 것이 서블릿 방식이다.

 

1) 서블릿이 생겨난 이유

 

http 요청과 응답은 다음과 같다.

 

//request

GET /api/products HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.28.0
Accept: */*
Postman-Token: abfcbcf8-9317-430c-86b9-c00020eb736e
Host: localhost:8080
Accept-Encoding: gzip, deflate, br
Connection: keep-alive


//response

Location: http://localhost:8080/api/products/6
Content-Length: 202
Content-Type: application/json
Data: Sun, 02 May 2022 14:56:41 GMT
Keep-Alive: timeout=60

 

만약 이러한 요청을 직접 해석하고 처리해서 밑의 코드와 같은 텍스트 형식의 응답을 만들어야 하면 굉장히 개발이 어려워질 것이다.

모든 통신규약을 확인하면서 긴 텍스트로 들어온 요청을 분석하고 그에 맞는 처리를 하는 것은 쉽지 않은 일이다.

 

그래서 서블릿으로 요청을 처리하고 응답을 보내주는 방법을 택하는 것이다.

서블릿 메서드를 활용하면 이 긴 텍스트로된 요청을 처리하고, 응답을 내보내 줄 수 있다.

 

 

HTTP 요청을 직접 파싱하지 않고, servlet이라는 도구를 활용해서 개발자가 쓰기 쉬운 형태로 요청을 받아들이고, 다시 그 요청을 HTTP 형식으로 바꾸어주는 것이 servlet인 것이다.

 

이렇게 되면 개발자는 HTTP 요청이 무엇인지 파악하는 노력을 단순히 받아들인 요청을 처리하는 '처리 로직'에만 신경을 쓸 수 있게 된다.

 

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();
        long lastModified;
        if (method.equals("GET")) {
            lastModified = this.getLastModified(req);
            if (lastModified == -1L) {
                this.doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader("If-Modified-Since");
                if (ifModifiedSince < lastModified) {
                    this.maybeSetLastModified(resp, lastModified);
                    this.doGet(req, resp);
                } else {
                    resp.setStatus(304);
                }
            }
        } else if (method.equals("HEAD")) {
            lastModified = this.getLastModified(req);
            this.maybeSetLastModified(resp, lastModified);
            this.doHead(req, resp);
        } else if (method.equals("POST")) {
            this.doPost(req, resp);
        } else if (method.equals("PUT")) {
            this.doPut(req, resp);
        } else if (method.equals("DELETE")) {
            this.doDelete(req, resp);
        } else if (method.equals("OPTIONS")) {
            this.doOptions(req, resp);
        } else if (method.equals("TRACE")) {
            this.doTrace(req, resp);
        } else {
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[]{method};
            errMsg = MessageFormat.format(errMsg, errArgs);
            resp.sendError(501, errMsg);
        }

    }

 

위의 코드는 HttpServlet 추상 클래스의 service 메서드이다.

getMethod()가 어떤 일을 하는지 알아야 좀 더 이해가 쉽겠지만, POST, PUT과 같은 메서드를 파싱 해오는 메서드라고 생각해보자.

파싱 해온 단어를 가지고, 각각 요청에 맞는 일들을 if, else if 문을 통해서 각각 분기해서 doXXX를 불러주고, 처리해주고 있는 것을 알 수 있다.

 

만약 이것이 없었다면, 개발자가 일일이 이것들을 손수 처리해줘야 했다고 상상을 해보자. 아마 납기일을 맞추기 위해서는 아마 밥도 못 먹고 HTTP의 요청을 어떻게 처리할 것인지 가내 수공업으로 일일이 한 땀 한 땀 무엇인가를 이어주고 있었을 것이다. 그렇지만, 간편한 밀 키트와 같이 HTTP 소스를 가공해서 가져다주는 servlet이 생긴 지금은, servlet 안의 메서드에 대해 이해하고, 어떤 때에 그 메서드를 쓸 지만을 생각하면 되는 세상이 되었다. 엄청난 도구를 가지게 된 것이다.

 

 

2) 서블릿 컨테이너와 서블릿이 호출되는 과정

 

서블릿 컨테이너는 서블릿을 담고 관리하는 바구니라고 생각하면 된다. 사용자의 요청이 들어오면 서블릿 컨테이너는 해당 요청과 매핑된 서블릿을 찾게 된다. 서블릿 컨테이너가 서블릿이 어떤 요청과 매핑되어있는지 어떻게 할 수 있을까?

 

설정 파일을 보면 알 수 있다.

 

<servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>servlet.HelloServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
</servlet-mapping>

맨 아래 url 패턴부터 해석을 하자면, /hello라는 요청이 들어오면 HelloServlet이라는 서블릿으로 처리를 하겠다고 읽는다. 그다음 HelloServlet은 서블릿이라는 패키지 아래에 HelloServlet이라는 클래스 파일로 정의되어 있다고 명시해주는 것이다. 

 

이런 설정 파일을 서블릿 컨테이너가 읽어서 이 요청이 어떤 서블릿이 필요한지를 알게 되면 서블릿 인스턴스가 컨테이너에 있는지 확인을 하는 것이다. 만약 인스턴스가 컨테이너에 존재한다면 그 인스턴스를 그대로 사용하게 되는 것이고 없다면 생성한 후 가져가서 사용하게 되는 것이다. 

 

그다음에 Servlet Container에 스레드를 생성하고, res, req(httpresponse, httprequest의 객체 참조 변수)를 인자로 service를 실행한다. 응답을 한 후 res, req 객체는 소멸이 된다.

 

서블릿 객체는 소멸되지 않는다. 소멸하지 않고 있다가 다음에 같은 요청이 들어왔을 때 서블릿 컨테이너에 의해서 호출되어 사용이 된다. 서블릿 컨테이너는 결국 서블릿의 생명주기를 관리하는 객체인 것이다.

 

만약 여러 요청이 동시에 들어온다면 어떻게 될까? 이때는 멀티스레드로 요청을 처리하게 된다. 

 

 

 

2. 프런트 컨트롤러 패턴

 

1) 프론트 컨트롤러 패턴의 필요성

 

멀티스레드로 요청을 처리하게 되면, 다음과 같은 중복 요소들을 만날 수 있게 된다.

ControllerA, B, C 모두 각각 처리하는 것들이 있는데, 공통적으로 처리해야 할 부분들이 생기게 될 수 있다.

공통적으로 리퀘스트를 파싱을 해야 하는 부분이 있다던지 하는 부분이 있을 수 있다.

이런 경우 한꺼번에 공통 로직을 모아서 처리를 해주고, 각각 개별 로직을 적용하는 것이 훨씬 효율적일 수 있다.

그래서 아래와 같이 공통로직을 모아서 처리를 해주는 방식을 취하게 되는데, 이를 프런트 컨트롤러 패턴이라고 한다.

이 부분은 MVC 패턴과도 관련이 있는데, 나중에 자세히 포스팅해보려고 한다.

여기서 모든 요청을 받는 프런트 컨트롤러, servlet을 Dispatcher Servlet이라고 부른다. 서블릿은 하나만 두고 모든 요청을 다 받을 수 있도록 하는 것이다. 위의 각각 분할된 컨트롤러 패턴은 요청을 수행할 때마다 매번 스레드를 생성했지만, 이제는 하나의 서블릿만 정의하고 그 서블릿이 모든 요청을 수행할 수 있도록 하는 전략을 취하는 것이다. 

 

 

2) 디스패처 서블릿의 Web 요청 처리 과정

디스패처 서블릿이 혼자 요청을 처리하는 것은 과부하를 일으킬 수 있다. 그렇기 때문에, 요청 처리 핸들러, 뷰 검색, 핸들러 호출하는 서블릿들을 분할하는 모델을 생각해볼 수 있다. 역할을 분리하고 이를 구현하는 수고로움이 많아진 것 같지만, 실제적으로 개발자가 건드려야 하는 부분들은 핸들러 호출 뒤에 나오는 '처리 핸들러'만 신경 써주면 된다.

 

이는 스프링으로 개발을 진행하기 때문에 가능한 것이다. 1, 2, 3에 해당하는 부분들은 누가 처리를 해주는가? 이 역할을 하는 객체들은 디스패처 서블릿이 스프링 컨테이너로부터 주입을 받아서 사용하고 동작을 하게 되는 것이다.

 

 

3) 스프링 컨테이너

 

서블릿을 주로 알아보는 포스팅이었지만, 스프링까지 이어지는 구조를 설명해보겠다.

 

스프링 컨테이너를 자세히 살펴보면 아래와 같다.

웹 애플리케이션 콘텍스트는 웹 요청 처리 관련 객체들이 담겨있다. 루트 웹 어플리케이션 컨텍스트 안에는 웹 요청 처리 관련된 빈 외의 컴포넌트들 서비스, 레포지토리 관련 객체들이 관리되는 것이다. 

 

서블릿 설정 파일만 잘 작성해주면, 설정대로 생성된 객체가 스프링 컨테이너에서 관리되고 필요한 부분에서 주입받아서 디스패처 서블릿이 알아서 사용할 수 있게 된다. 

 

스프링으로 웹 요청을 처리한다는 것은 스프링 mvc에서 제공하는 디스패처 서블릿과 웹 요청 처리 관련 구현체들을 사용할 수 있다는 이야기이다. 동시에 스프링 컨테이너, 스프링 IoC를 사용하여 개발할 수 있다는 이야기가 된다. 최종적인 스프링 사용 목적은 개발자로 하여금 핸들러 로직(요청 처리 로직)에만 집중할 수 있게 해주는 것이다. 

 

 

 

<참고>

https://www.youtube.com/watch?v=calGCwG_B4Y&list=PLE0hRBClSk5Lcgv3c-I3PH-1LGkv96oYN&index=30

 

728x90

댓글