면접을 위해 스프링 프레임워크의 정의부터 한땀 한땀 정의하고 있었는데 서블릿 기반이라는데 도저히 이해도 잘 못하겠고 설명도 못하겠다. 근간이 되는 기술을 모르는 것은 창피한 일이다. 정의해보자.
자바 서블릿(Java Servlet)은 자바를 사용하여 웹페이지를 동적으로 생성하는 서버측 프로그램 혹은 그 사양을 말하는데 흔히 서블릿이라 불린다.
쉽게 말하면 웹 서버 프로그래밍을 하기 위해 개발된 자바코드라고 할 수 있다.
자바 서블릿은 웹 서버의 성능을 향상하기 위해 사용되는 자바 클래스의 일종이다.
JSP와 비슷하지만 JSP는 HTML 문서 안에 자바 코드를 포함하는 반면에, 서블릿은 자바코드안에 HTML을 포함하고 있다는 점에서 차이점이 있다.
위키백과
더 자세한 것은 공식 문서에서의 정의를 확인해보자.
위의 설명을 이해하면서 머지? 이거 CGI 랑 똑같은게 아닌가? 이런 생각이 들었는데
CGI는 역시 웹서버로 동적 컨텐츠를 만들기 위한 프로세스를 실행시키는 것이고, 서블릿도 같지만 쓰레드 프로그래밍을 기반으로 하여 서버의 자원을 덜 잡아 먹는 기술이라는 것으로 인식하였다.
서블릿은 JavaEE(Java Platform, Enterprise Edition) 에 포함되어 있는 기술이다.
JavaEE 는 현재 Jakarta EE 가 되었다.
관련해서는 https://www.samsungsds.com/kr/insights/java_jakarta.html
를 참조하자.
자바 서블릿은 서블릿 컨테이너가 생명주기를 관리한다고도 한다. 서블릿 컨테이너는 무엇일까?
자바 서블릿과 상호 작용하는 웹 서버의 일부인 웹 컨테이너로 다음을 담당한다.
우리가 잘 아는 서블릿 컨테이너로는 톰캣이 있는데 이건 다른 페이지에서 자세하게 정리하겠다. 요약하면 다음과 같은 일을 한다.
맨밑에 참조한 사이트의 링크를 적어놓았다. 영어 원문이 궁금하면 그걸 보는게 좋다. 나는 이걸 해석할 뿐이다.
애플리케이션 배포 후에 서블릿 컨테이너 다이어 그램
서버 측
클라이언트 측
서버를 시작할 때 서블릿 컨테이너는 다음과 같은 일을 한다.
모든 웹어플리케이션을 인식한다.
웹어플리케이션을 배포하고 각 어플리케이션의 "서블릿 컨텍스트" 라고 불리는 오브젝트를 준비한다.
웹어플리케이션을 배포 하는 동안에는
컨테이너가 WEB-INF 폴더에 web.xml 을 인식한다.
web.xml 파일을 인식하면, 컨테이너가 해당 파일을 컨텐츠를 로딩, 파싱, 읽는 과정을 수행할 것이다.
web.xml 파일에 표시 이름이나 매개 변수 같은 각 어플리케이션 레벨의 데이터를 "서블릿 컨텍스트" 객체에 저장한다.
클라이언트로부터 서버에 HTML form을 통한 post 요청이 전달 되는 경우
메인 서버로 요청이 전달되고 서버가 요청이 유효한지 체크한다.
유효하다면 서블릿 컨테이너로 전달되고, 컨테이너가 어플리케이션 이름과 어떤 리소스가 실행되어야 할지 인식한다.
서블릿 컨테이너가 요청을 통해 URL 패턴과 매칭되는 HelloServlet 클래스를 인식하는데
인식을 위해 WEB_INF 폴더로 간다.
web.xml 파일에 URL 패턴을 기반으로 한 특별한 서블릿을 찾고 클래스 폴더로 가서 .class 파일을 찾는다.
서블릿 컨테이너가 .class 파일을 인식하면 서블릿 라이프 사이클이 시작된다.
서블릿 라이프 사이클
컨테이너가 서블릿 로딩을 시작한다.
컨테이너가 같은 서블릿을 사용가능한지 확인하고, 아니라면 .class 파일을 로드한다. (바이트코드가 메모리로)
위와 같은 동작을 실행하기 위해 컨테이너가 class c= class.forName("helloServlet")
을 호출한다.
서블릿이 로딩되고, 서블릿 객체를 생성한다.
그리고 나서 "servletConfig" 라는 객체를 생성하고, 서블릿의 모든 데이터를 여기에 저장한다.
이를 수행하기 위해 컨테이너가 object obj = c.newInstance() 를 호출한다.
서블릿을 초기화하기 위해 init() 메소드를 servletConfig를 매개변수로 함께 호출한다.
GenericServlet의 service() 메소드가 request와 response 객체와 함께 호출되고, 사용된 메소드가 POST이므로 doPost()가 호출된다. 한번 깃허브를 뒤져봤다.
/**
* Receives standard HTTP requests from the public <code>service</code> method and dispatches them to the
* <code>do</code><i>XXX</i> methods defined in this class. This method is an HTTP-specific version of the
* {@link jakarta.servlet.Servlet#service} method. There's no need to override this method.
*
* @param req the {@link HttpServletRequest} object that contains the request the client made of the servlet
*
* @param resp the {@link HttpServletResponse} object that contains the response the servlet returns to the client
*
* @throws IOException if an input or output error occurs while the servlet is handling the HTTP request
*
* @throws ServletException if the HTTP request cannot be handled
*
* @see jakarta.servlet.Servlet#service
*/
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 if (method.equals(METHOD_PATCH)) {
doPatch(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);
}
}
컨테이너가 doPost() 메소드를 실행하고, response 객체 안에 응답 관련 내용들이 생성된다.
doPost() 메소드를 실행한 후에 쓰레드가 없어지고, 컨테이너가 응답객체을 메인서버로 부터 클라이언트로 보낸다.
클라이언트 사이드에 응답이 표시되면 프로토콜이 커넥션을 없앤다.
컨테이너는 클라이언트에 응답이 보내졌다고 이해하고 request, response 객체를 없앤다.
이후에 컨테이너는 추가요청이 있을 때까지 기다리다가 컨테이너가 서블릿을 없앤다.
(톰캣의 경우 서버 셧다운까지 유지하나 다른 경우에는 특정시간 까지만 유지한다.)
어플리케이션 서버가 멈추면 컨테이너가 servlet config 및 servlet 객체들을 없애는데 destory() 메소드를 호출하여 회수한다.
컨테이너가 서블릿 바이트 코드를 메모리로부터 언로드 시키고, ServletContext 객체를 없앤다.
자바 서블릿은 main() 메소드가 존재하지 않고, 서블릿 컨테이너가 서블릿을 인스턴스화 시키고 요청과 응답을 처리하기 위해 새로운 쓰레드 객체를 만든다.
단일 서블릿과 컨테이너에서 많은 요청을 처리하기 위해서는 다수의 쓰레드를 만든다.
참조 한 책 및 사이트
https://joonyk.tistory.com/16
https://www.geeksforgeeks.org/servlet-flow-of-execution/
https://ckddn9496.tistory.com/48
https://github.com/jakartaee/servlet/blob/master/spec/src/main/asciidoc/servlet-spec-body.adoc