Servlet과 Servlet Container

uncle.ra·2023년 9월 26일
post-thumbnail

📗 Client로 부터 HTTP 요청이 들어오면?

잠시 웹 서버를 직접 다 구현해야 한다고 생각해보자.
Client로 부터 HTTP 요청이 들어왔다. 서버에서는 어떤 동작이 일어날까?🤔

1. 서버 TCP/IP 연결 대기, 소켓 연결(3-way-handshake)

HTTP는 기본적으로 TCP기반으로 통신이 이루어진다.(HTTP/3은 UDP기반으로 동작하지만 여기선 제외하자)
그렇기 때문에 Client로 부터 요청이 오기까지 미리 소켓을 열고 대기를 해야한다.

2. HTTP 요청 메세지 파싱

Socket을 열고 대기하고 있다가 Client로 부터 HTTP 요청이 들어왔다.
InputStream으로 데이터를 읽어들이고 문자열로 변환하면 아래와 같은 HTTP 요청 메세지로 나올 것이다.

GET /request-param HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: jenkins-timestamper-offset=-32400000

그러면 이 메세지를 어떤 HTTP Method를 사용하고 있는지 Host는 누구인지, Content-Type은 어떤지 등 정보를 split() method등을 사용해서 하나하나 잘라서 우리가 데이터를 사용할 수 있도록 파싱해야한다.

3. HTTP 응답 메시지 생성

요청을 처리하고 난 이후에는?
당연히 response를 HTTP 규약에 맞게 header와 body를 작성한다.

4. TCP/IP로 응답 전달 및 소켓 종료(4-way-handshake)

HTTP 응답 메시지 생성이 끝났다면 전달하고 소켓을 종료한다.

📗 Servlet

처음 부터 다 구현해야 한다면, 위에서 언급한 사항들을 전부 구현해줘야 한다.🥹
하지만 우리는 직접 구현하지 않아도 된다. Servlet이 있기 때문에!

Servlet은 "Client로 부터 HTTP 요청이 들어오면?"에서 언급한 내용을 전부 처리해준다.
즉, 우리는 처리해야할 비즈니스 로직에만 신경쓰면 된다.

그렇다면 Servlet이 뭘까?

A servlet is a small Java program that runs within a Web server. Servlets receive and respond to requests from Web clients, usually across HTTP, the HyperText Transfer Protocol. Oracle

Oracle에서는 Servlet에 대해서 아래와 같이 정의한다.

  • 웹서버 내부에서 동작하는 작은 자바 프로그램이다.
  • Servlet은 일반적으로 HTTP를 통해 웹 클라이언트로 부터 요청을 수신하고 응답한다.

위 두 가지 내용을 보더라도 이해가 확실하게 가지 않을 것이다. 잠시 아래의 코드를 살펴보자.

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

	// ...
    
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        System.out.println("HelloServlet.service");
        System.out.println("request = " + request);
        System.out.println("response = " + response);

        String username = request.getParameter("username");
        System.out.println("username = " + username);

        response.getWriter().write("hello " + username);
    }
    // ...
}

위의 코드를 보면 HttpServlet을 상속받아서 HelloServlet을 구현했다.
코드를 local에서 실행한 후에 http://localhost:8080/hello?username=ra 로 요청을 보내면 service() method가 호출된다. 이때 service() method는 HttpServletRequest, HttpServletResponse를 파라미터로 전달 받는다.

여기서 HttpServletRequest, HttpServletResponse가 중요하다.
위에서 언급한 HTTP 요청 메시지 파싱, HTTP 응답 메시지 생성 등에 대한 부분을 Servlet에서 처리해준 객체들이다.

정의를 다시 살펴보자.

Servlet이란, 웹서버 내부에서 동작하는 작은 자바 프로그램이며, 일반적으로 HTTP를 통해 웹 클라이언트로 부터 요청을 수신하고 응답한다.

📗 Servlet 생명 주기

Servlet은 사람(?)과 마찬가지로 생명 주기를 갖고 있다.

잠시 IntelliJ 제공해주는 계층 구조를 보자.

Servlet-Diagram

HttpServlet은 Servlet Interface를 구현했다.
Servlet Interface를 확인해보자.

public interface Servlet {

    public void init(ServletConfig config) throws ServletException;

    public ServletConfig getServletConfig();

    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;

    public String getServletInfo();


    public void destroy();
}

총 5개의 Method가 정의되어 있는데, 이 중 Servlet 생명주기와 관련되어 있는 건 init(), service(), destroy() Method가 존재하고 이 외에 load단계가 포함되어 있다.

servlet-life-cycle

1. load

Servlet Class가 메모리에 적재되는 단계를 의미한다.

2. init() method

Servlet Instance가 생성된 직후에 init() method가 호출된다. Init() method는 생애 주기 중 단 한번만 호출된다.

3. service() method

service() method는 해당 Servlet에 대한 Client의 요청마다 호출된다.

4. destroy() method

destroy() method는 생애 주기 중 단 한만 호출된다.
method 호출 시점은 모든 스레드가 종료(프로그램 종료 시)되거나 시간 초과가 된 후에만 호출된다.

Servlet Instance는 언제 생성될까?🤔

Servlet Instance는 2가지 경우에 생성된다.

  1. Servlet Container가 시작될 때(Servlet 시작 시 로드되도록 구성한 경우에 한함)
  2. Servlet에 대한 첫 번째 요청이 이루어질 때 생성된다.

📗 Servlet Container

그러면 다음으로 알아볼 건 Servlet Container이다.

Servlet Container는 뭘까?

Tomcat과 같이 Servlet을 지원하는 Web Applicaiton Server(WAS)를 Servlet Container라고 하고, Servlet을 관리하는 역할을 수행한다.

Servlet은 사람과 같은 생명주기를 가지고 있지만 주체적으로 행동을 하지 못한다. 그렇기 때문에 Servlet을 관리해주는 Servlet Container가 필요하다.

Servlet Container는 어떤 부분을 관리하는 걸까?🤔

Servlet Container는 무수히 많은 역할을 수행하고 있지만, 그 중에 핵심적인 부분 2가지만 확인해 보자.

1. Servlet의 생명주기 관리

위에서 언급한 Servlet의 생명주기를 관리한다.
Servlet의 init(), service(), destroy() method를 호출해서 Servlet이 적절하게 동작하도록 한다.

2. Client 요청 처리를 위한 Thread Pool 관리

Servlet Container는 Client로 부터 요청을 받을 때마다 Thread Pool에서 대기 중인 Thread를 할당하고 각각의 요청을 이러한 Thread에 매칭시키는 역할을 수행한다.


@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet  extends HttpServlet {
	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		System.out.println("currentThread Name: " + Thread.currentThread().getName());
		super.service(request, response);
	
	}
}

/*

currentThread Name: http-nio-8080-exec-2
currentThread Name: http-nio-8080-exec-3
currentThread Name: http-nio-8080-exec-4
currentThread Name: http-nio-8080-exec-5
currentThread Name: http-nio-8080-exec-6
currentThread Name: http-nio-8080-exec-7
currentThread Name: http-nio-8080-exec-8
currentThread Name: http-nio-8080-exec-9
currentThread Name: http-nio-8080-exec-10
currentThread Name: http-nio-8080-exec-1
currentThread Name: http-nio-8080-exec-2
currentThread Name: http-nio-8080-exec-3
currentThread Name: http-nio-8080-exec-4
currentThread Name: http-nio-8080-exec-5
currentThread Name: http-nio-8080-exec-6
currentThread Name: http-nio-8080-exec-7
currentThread Name: http-nio-8080-exec-8
...
*/

위에 코드를 실행시킨 후에, http://localhost:8080/hello 로 요청을 반복적으로 보내면 요청 마다 Thread Pool에 대기 중인 Thread가 할당되어 로직을 수행하는 걸 알 수 있다. 이 때, Thread Pool에 존재하는 Thread의 갯수는 한정적이다. 그렇기 때문에 동일한 Thread가 할당될 수 있다.

Servlet Container는 어떻게 동작할까?

servlet-container-entire-flow

1. HttpServletRequest, HttpServletResponse 객체를 생성한다.

"Client로 부터 HTTP 요청이 들어오면?"에서 언급했던 것처럼 요청에 대해 적절한 파싱을 통해서 HttpServletRequest, HttpServletResponse 객체를 생성한다.

2. web.xml or annotation 방식으로 url에 해당하는 servlet을 가져온다.

Servlet Container는 Client로 부터 요청 받은 url에 해당하는 서블릿을 찾는다. 이때 서블릿이 초기화 되지 않은 상태라면, load 및 init() method를 호출하여 서블릿을 초기화하게 된다. 초기화 과정은 애플리케이션 실행 동안 단 한 번만 수행된다.

3. service() method 호출

Servlet을 찾은 이후에 Servlet Container는 service() method를 호출한다. service() method는 HTTP method(GET,POST, PATCH, ...)에 따라 doGet(), doPost() 를 호출한다.

4. 요청에 따른 처리가 완료된 이후

요청 처리 이후 HttpServletResponse 객체를 사용하여 응답을 클라이언트에게 반환한다.
반환 이후에 HttpServletRequest 및 HttpServletResponse 객체는 소멸된다.

📗 마치며

글을 쓰기 전에 생각했던 건 사실 Client로 부터의 HTTP 요청을 받았을 때 Spring MVC에서의 동작 과정이였다.
그런데 Spring MVC가 Servlet을 기반으로 동작하기 때문에 Servlet과 Servlet Container에 대해서 먼저 짚고 넘어가지 않으면 설명을 제대로 못할 수 있다는 생각에 주제를 변경하게 되었다.😅

다음 주제로는 꼭 Spring MVC에서의 동작 과정을 다뤄보겠다!

📗 참고

2개의 댓글

comment-user-thumbnail
2023년 10월 3일

알기 쉽게 정리 잘 하셨네요! 잘 보고갑니다~

1개의 답글