해당 글은 tomcat 10.0.23에서 사용하고 있는 servlet 5.0을 기준으로 작성되었습니다.
서블릿과 CGI
WS를 사용하면 정적인 파일을 응답할 수 있지만 동적인 콘텐츠 생성하거나 사용자가 입력한 데이터를 저장할 수 없다. 이 문제를 해결하기 위해 CGI가 등장했고 자바 진영에서는 Servlet이 등장했다.
서블릿은 CGI에 비해 다음과 같은 이점을 가진다.
1. CGI는 프로세스 단위로 실행된다. 이 때문에 요청당 하나의 스레드를 사용하는 서블릿에 비해 서버에 더 많은 부하를 준다.
2. 서블릿은 자바로 작성되었고 JVM상에서 동작하기 때문에 플랫폼에 의존적이지 않다.
3. 자바에서 구현된 표준 라이브러리들을 그대로 사용해 DB에 접근하거나 소켓을 통해 다른 소프트웨어와 통신할 수 있다.
서블릿의 개략적인 동작과정
서블릿, 클라이언트, 웹 서버 간의 관계를 간단히 나타내면 다음과 같다.
1. 클라이언트의 요청을 받아 내부에 존재하는 데이터(폼 데이터, 쿠키, 미디어 타입 등)를 읽는다.
2. 데이터를 연산해 반환할 데이터를 생성한다. 결과를 생성하기 위해 DB에 접근하기도 한다.
3. 데이터를 클라이언트로 응답한다. 이 데이터는 HTML, XML, GIF 등 다양한 포맷일 수 있다.ㄴ
Servlet Container
서블릿은 컨테이너에 의해 관리된다. 이 컨테이너는 서블릿 엔진으로도 불리며 서블릿 기능을 제공할 수 있게 확장한 웹 서버다. 서블릿은 서블릿 컨테이너에 의해 구현된 요청-응답 패러다임을 통해 웹 클라이언트와 상호작용한다. 서블릿 컨테이너는 MIME 기반 요청을 디코드 해서 MIME 기반 응답을 생성한다. 또 한, 생명주기를 통해 서블릿을 관리한다.
서블릿 컨테이너는 호스트 웹 서버에 내장하거나 웹 서버가 제공하는 확장 API를 통해 추가할 수 있다.
서블릿 컨테이너는 반드시 요청-응답 패러다임을 위해 HTTP/1.1, HTTP/2, HTTPS를 지원해야 한다. 이 중 HTTPS는 선택 사항이다. HTTP 버전 중 1.1과 2를 지원해야 하는 이유는 서블릿 컨테이너가 RFC7234(HTTP/1.1 Caching)에 서술돼 있는 캐싱 메커니즘을 따르기 때문이다. 이 메커니즘으로 인해 요청이 서블릿에 도착하기 전에, 응답이 클라이언트로 전송되기 전에 요청과 응답이 수정될 수 있다. 또는 요청 자체가 서블릿에 도달하지 않을 수 있다.
Servlet Interface
Servlet 인터페이스는 자바 서블릿 API에거 가장 핵심적인 인터페이스다. 모든 서블릿 구현체들이 이 인터페이스를 직간접적으로 implements 하고 있다. GenericServlet과 HttpServelt은 직접적으로 Servlet을 implements 하고 있으며 대부분의 경우 HttpServlet을 extends 하여 서블릿을 구현한다.
Servlet 인터페이스는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 |
package jarkarta.servlet;
public interface Servlet { public void init(ServletConfig config) throws ServletException;
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public void destroy();
public ServletConfig getServletConfig();
public String getServletInfo();
}
|
cs |
이 메서드 중 service() 메서드를 사용해 클라이언트 요청을 처리한다. 이 메서드는 서블릿 컨테이너가 서블릿 인스턴스에게 라우트 한 각 요청마다 한 번씩 호출된다.
웹 컨테이너는 동시에 여러 요청들을 하나의 서블릿에서 service() 메서드를 동시 호출해 처리한다. 이때, 하나의 요청을 하나의 스레드가 담당한다. 따라서 웹 개발자는 서블릿을 thread-safe 하게 디자인해야 한다.
HttpServlet
HttpServlet은 Servlet을 impelments 하고 있는 추상 클래스다. HttpServlet 내에는 HTTP 기반 요청을 처리할 수 있는 메서드들이 존재한다. 이 메서드들의 이름은 do로 시작하며 요청 메서드 별로 존재한다. 예컨대 GET 요청을 처리하는 메서드는 doGet()이다. HttpServlet의 service() 메서드는 요청에 따라 알맞은 메서드를 자도으로 호출하게 구현돼 있다.
HttpServlet이 구현하고 있는 service() 메서드는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
public abstract class HttpServlet extends GenericServlet {
...
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;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// 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);
}
}
private void maybeSetLastModified(HttpServletResponse resp,
long lastModified) {
if (resp.containsHeader(HEADER_LASTMOD)) {
return;
}
if (lastModified >= 0) {
resp.setDateHeader(HEADER_LASTMOD, lastModified);
}
}
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException(lStrings.getString("http.non_http"));
}
service(request, response);
}
...
}
|
cs |
서블릿 인스턴스 개수
통상적인 환경에서 서블릿 컨테이너는 서블릿 선언 하나 당 하나의 서블릿 인스턴스만 생성한다. 만약 여러 개의 인스턴스를 생성해야 한다면 SingleThreadModel 인터페이스를 impelments 하면 된다.
서블릿 생명주기
서블릿은 생명주기에 의해 관리되며 이 생명주기에는 서블릿이 초기화되고, 요청을 처리하고, 사라지는 로직이 정의돼있다. Servlet 인터페이스에 존재하는 init(), service(), destroy() 메서드들이 서블릿 생명주기를 나타내기 위해 존재한다. 따라서 모든 서블릿 구현체들은 GenericServlet, HttpServlet 추상 클래스를 통해 직간접적으로 이 메서드들을 구현해야 한다. init()과 destroy()는 서블릿 생명주기 동안 한 번씩만 호출된다.
서블릿 생명주기는 다음과 같다.
Loading
서블릿 로딩과 초기화는 서블릿 컨테이너가 시작된 후 발생하거나 컨테이너가 서블릿이 요청을 서비스해야 한다고 판단될 때까지 지연시킨다. 서블릿 엔진이 시작되면 서블릿 클래스들은 만드시 서블릿 컨테이너에 위치해야 한다. 서브릿 컨테이너는 일반으로 자바가 클래스를 로드하는 방식으로 서블릿 클래스를 로드한다.
Initialization
서블릿 인스턴스가 생성되면 클라이언트로부터 요청을 받아 처리하기 전에 초기화 작업을 거친다. 이 단계에서는 디비 연결 등의 작업이 수행된다. 서블릿 컨테이너는 서블릿 인스턴스의 inti() 메서드를 호출해 초기화를 진행한다. init() 메서드 호출로 초기화가 진행될 때 ServletConfig 인터페이스의 구현체를 함께 넘긴다. ServletConfig 구현체는 초기 파라미터들과 서블릿 컨테이너에 의해 생성되는 설정 정보들이 포함돼 있다.
GenericServlet에 구현된 init() 메서드를 보면 ServletConfig를 인자로 받는 것을 알 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public abstract class GenericServlet implements Servlet, ServletConfig,
java.io.Serializable {
...
private transient ServletConfig config;
@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
// NOOP by default
}
...
}
|
cs |
Request Handling
서블릿이 정상적으로 초기화되면 서블릿 컨테이너는 이 서블릿을 활용해 요청들을 핸들링한다. ServletRequest는 응답을 받는 객체이며 ServletResponse는 응답에 대한 객체이다. ServletRequest와 ServletResponse는 service() 메서드에 파라미터로 존재한다.
1
2
3
4
5
6
7
8
9
|
public interface Servlet {
...
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
...
}
|
cs |
HTTP 요청의 경우 HTTP에 특화된 HttpServletRequest와 HttpServletResponse를 사용한다. 따라서 HttpServlet에는 HttpServletRequest와 HttpServletResponse를 인자로 받는 service() 메서드가 존재한다.
1
2
3
4
5
6
7
8
9
10
11
12
|
public abstract class HttpServlet extends GenericServlet {
...
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
...
}
...
}
|
cs |
HttpServletRequest와 HttpServletResponse는 각각 ServletRequest, ServletResponse를 implements 하고 있다.
1
2
3
4
5
6
7
8
9
10
|
package jakarta.servlet.http;
public interface HttpServletRequest extends ServletRequest {
...
}
package jakarta.servlet.http;
public interface HttpServletResponse extends ServletResponse {
...
}
|
cs |
End of Service
서블릿 컨테이너가 서블릿이 더 이상 필요 없다고 판단할 경우 destroy 메서드를 호출해 서블릿 인스턴스가 차지하고 있던 리소스를 free 시키고 영속 상태가 존재하면 저장한다.
서블릿 컨테이너가 destroy() 메서드를 호출하기 전에 service() 메서드를 실행하고 있는 스레드들이 실행을 완료해야 함을 보장해야 한다. destroy() 메서드가 호출되면 해당 서블릿 인스턴스에는 더 이상 요청이 라우트 되지 않는다. 만약 컨테이너가 destroy 한 서브릿을 다시 필요로 할 경우 새로운 인스턴스를 생성해야 한다.