이번 글에서는 서블릿 컨테이너가 자동으로 생성하고 관리해주는 서블릿에 대해 자세하게 알아보겠습니다.
01. 서블릿 클래스의 계층 구조
서블릿은 웹 서버에서 실행되는 JAVA EE 기반의 클래스입니다. Servlet 클래스를 작성할 때는 정해진 규칙을 따라야 하는데 그중 하나는 HttpServlet 클래스를 상속받아야 한다는 것입니다. 만약 사용자의 로그인 요청을 처리하는 LoginServlet이라는 서블릿 클래스를 작성한다고 한다면 LoginServlet은 HttpServlet의 상속을 받아야 합니다.
서블릿 객체는 개발자가 생성 및 관리하지 않습니다. 서블릿 객체는 서블릿 컨테이너가 자동으로 생성하고 관리합니다. 서블릿 컨테이너는 서블릿 클래스로부터 단 하나의 객체만 생성합니다.
서블릿 컨테이너는 Web Server로부터 서블릿 처리를 요청받으면, 요청받은 서블릿 객체가 메모리에 존재하는지 확인합니다. 만약 요청받은 서블릿 객체가 메모리에 없으면 서블릿 클래스(.class)를 로딩합니다. 그리고 기본 생성자를 호출하여 객체를 생성합니다.
만약 요청받은 서블릿 객체가 메모리에 있다면 스레드 풀에서 스레드 객체를 얻은 후 이 스레드 객체가 서블릿의 콜백 메서드 중 하나인 service() 메서드를 호출합니다.
* 서블릿 컨테이너는 스레드 풀(thread pool)이라는 컬렉션을 생성하고 운영합니다. 이 스레드 풀에는 서블릿 컨테이너가 생성한 다수의 스레드 객체가 등록되어 있습니다.
서블릿 객체는 기본 생성자에 의해 생성됩니다. 만약 서블릿 클래스의 생성자에 매개변수가 있다면 서블릿 컨테이너는 서블릿 객체를 생성하지 못하고 에러를 출력합니다. 서블릿 컨테이너는 자신이 어떤 서블릿 객체를 생성하고 관리해야 하는지에 대한 정보를 WEB-INF 폴더 안에 있는 web.xml 파일을 통해 획득합니다. web.xml 파일에는 URL과 서블릿 클래스가 매핑되어 있습니다. 특정 서블릿 클래스와 매핑된 URL로 서버에 최초 요청이 들어오면 서블릿 컨테이너는 그 서블릿 클래스의 객체를 생성합니다.
web.xml의 서블릿 매핑 예시
레이지 로딩과 프리 로딩 서블릿 클래스의 객체는 기본적으로 레이지 로딩(lazy-loading)되는 객체입니다. 레이지 로딩이란 사용자(브라우저)가 서버에 요청을 전달할 때 객체가 생성되는 것을 의미합니다. 레이지 로딩은 브라우저의 요청에 대해서 반응 속도는 느리지만 서버 메모리를 효율적으로 사용할 수 있는 장점이 있습니다. 이와 반대되는 개념은 프리 로딩(pre-loading)입니다. 프리 로딩이란 사용자의 요청과 무관하게 서블릿 컨테이너가 구동되는 시점에 XML 설정 파일에 등록된 객체가 생성되는 것을 의미합니다. 프리 로딩은 브라우저의 요청에 대해서 반응 속도는 빠르지만 사용하지 않는 객체를 미리 생성하기 때문에 메모리를 낭비한다는 단점이 있습니다. 만약 특정 서블릿 클래스를 프리 로딩하고 싶다면 다음과 같이 <servlet> 요소 안에 <load-on-startup> 요소를 추가해주면 됩니다.
03. 서블릿의 라이프 사이클 (Life Cycle)
서블릿 객체가 생성된 후, 서블릿 컨테이너는 서블릿을 운용하는 과정에서 서블릿이 가진 콜백 메서드 (1) init(), (2) service()와 doGet() / doPost(), (3) destroy()를 호출합니다.
* HttpServlet의 콜백 메서드인 init(), service(), destroy()는 최상위 인터페이스인 Servlet에서 정의된 메서드입니다.
* HTTP 요청 처리를 위한 doGet(), doPost() 메서드는 HttpServlet에서 추가된 메서드입니다.
* 서블릿 컨테이너는 서블릿 객체를 생성하고 관리할 때 최상위 인터페이스인 Servlet으로 묵시적 타입 변환을 합니다. 따라서 서블릿 클래스를 작성할 때 Servlet 인터페이스를 구현하거나 GenericServlet을 상속해도 됩니다. 하지만 HTTP 프로토콜상에서 사용자의 요청을 처리하는 서블릿 클래스를 작성하기 위해서는 HttpServlet 클래스를 상속받은 클래스를 사용해야 합니다.
(1) 서블릿 객체의 초기화: init() 메서드
init() 메서드는 서블릿 객체가 생성된 직후 호출됩니다. init() 메서드는 서블릿 객체의 멤버 변수를 초기화합니다. 만약 요청된 서블릿 객체가 이미 메모리에 로드되어 있으면 서블릿 객체를 생성하고 초기화 과정은 생략됩니다.
* init() 메서드 대신 init(ServletConfig config) 메서드를 사용하면, ServletConfig 객체를 통해 web.xml이라는 외부 파일에서 설정한 변수 값을 불러올 수 있습니다.
service() 메서드는 브라우저의 요청이 있을 때마다 호출됩니다. 서블릿 컨테이너에 서블릿 처리 요청이 들어왔을 때, 메모리에 서블릿 객체가 로드되어 있든 아니든 서블릿 객체는 스레드 풀에서 하나의 스레드 객체를 할당받아 service() 메서드를 호출합니다. service() 메서드는 매개변수로 HttpServletRequest와 HttpServletResponse 객체를 전달받아 브라우저의 요청을 처리합니다.
HttpServlet 클래스가 상속받은 service() 메서드는 브라우저의 요청이 GET 방식인 경우에는 doGet() 메소드를, 브라우저의 요청이 POST 방식인 경우에는 doPost() 메소드를 자동으로 호출하도록 프로그래밍되어 있습니다.
* service() 메서드를 오버라이딩할 경우, doGet(), doPost() 메서드가 정상적으로 호출되지 않습니다. service() 메서드에 내용을 작성하고 싶다면 super() 메서드를 이용하면 됩니다.
(3) 서블릿 객체의 소멸: destroy() 메서드
destroy() 메소드는 서블릿 객체가 리로딩되거나 삭제되기 직전에 호출됩니다. 서블릿 객체를 삭제하는 방법에는 두 가지가 있습니다.
첫 번째는 서버를 중지하여 서블릿 컨테이너를 종료하는 것입니다. 서블릿 컨테이너는 종료되기 직전에 자신이 생성하여 관리하던 모든 서블릿 객체를 삭제합니다. 두 번째는 서블릿의 소스를 수정하는 것입니다. 서블릿 컨테이너는 서블릿 객체에 대한 소스 수정이 발생하면 기존에 생성된 서블릿을 수정된 서블릿으로 변경합니다. 이를 리로딩(reloading)이라고 합니다.
서블릿 객체의 라이프 사이클을 직접 확인해보겠습니다.
web.xml 파일에서 아래의 서블릿 LifeCycleCheckServlet은 URL /lifecyclecheck.do와 매핑되어 있습니다. 톰캣 서버를 가동한 후, /lifecyclecheck.do로 요청을 보내면 다음과 같이 콘솔 결과가 출력됩니다.
서블릿 컨테이너가 LifeCycleCheckServlet의 처리를 요청받았을 때, 메모리에 LifeCycleCheckServlet 클래스(.class)가 로드되어 있지 않으므로 서블릿 컨테이너는 기본 생성자를 이용하여 LifeCycleCheckServlet 객체를 생성합니다. 객체가 생성된 직후에는 init() 메서드가 수행됩니다. 이제 스레드 풀에서 스레드 객체가 service() 메서드를 호출합니다. 브라우저가 서버로 GET 요청을 보냈으므로 service() 메서드는 doGet() 메서드를 호출합니다.
이후에 브라우저에서 같은 URL, 즉 같은 서블릿으로 반복해서 요청을 보내는 경우, 메모리에 서블릿 객체가 이미 생성되어 있으므로 기본 생성자와 init() 메서드는 더 이상 호출되지 않고, service() 메서드 및 doPost() 메서드만 수행됩니다.
서블릿 클래스의 코드를 수정하면, 기존의 서블릿 객체가 삭제되기 직전에 콜백 메서드인 destroy()가 호출됩니다.