서블릿과 서블릿 컨테이너 이해하기(Servlet, ServletContainer)
서블릿과 서블릿 컨테이너 이해하기(Servlet, ServletContainer)
서블릿(Servlet)과 서블릿 컨테이너(ServletContainer)는 자바 기반 웹 애플리케이션을 개발하는데 기본이 되고 또 자주 만나게 되는 개념입니다.
하지만 정확하게 어떤 역할을 하고 어떤 방식으로 동작하는지 스스로 명확하게 설명하지 못하는 것 같아 정리해 본 내용입니다.
서블릿(Servlet)
// Servlet 3.0 이상을 지원하는 경우 web.xml 파일을 사용하지 않고 @WebServlet 과 같은 어노테이션 기반의 설정을 사용할 수 있습니다.
@WebServlet(name = "exampleServlet", urlPatterns = "/example")
public class ExampleServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//애플리케이션 로직
}
}
Servlet은 자바 기반의 웹 애플리케이션을 개발하기 위한 서버 측 프로그램 또는 기술입니다.
서블릿은 HTTP 요청(Request)을 받아 이를 처리한 뒤 HTTP 응답(Response)을 생성하여 클라이언트에게 전달하는, 다시 말하면 클라이언트의 요청을 처리하여 동적인 웹 페이지를 생성하는 역할을 하는데요.
서블릿은 아래와 같은 특징을 가집니다.
1. 클라이언트의 요청(Request)에 대해 동적으로 작동하는 웹 컴포넌트
서블릿은 정적인 웹 페이지와는 달리 사용자의 입력이나 기타 동적인 조건에 따른 비즈니스 로직을 수행할 수 있습니다.
2. HTML을 사용하여 응답(Response)
서블릿은 주로 HTML을 사용하여 클라이언트에게 응답을 보내지만, 필요시 'HttpServletResponse' 객체를 통해 'HTML', 'JSON', 'XML' 등 다양한 형식의 데이터를 클라이언트에게 전달할 수 있습니다.
3. Java의 스레드(Thread)를 이용하여 동작
서블릿은 Java의 멀티쓰레딩 기능을 이용하여 동작합니다.
아래 내용을 통해 살펴보겠지만 서블릿 컨테이너는 클라이언트의 각 요청에 대해 새로운 스레드를 생성하거나, 스레드 풀에서 할당하여 요청을 처리하는데요.
서블릿은 이러한 방식을 통해 다수의 클라이언트로부터 들어오는 요청을 동시에 처리할 수 있습니다.
4. MVC 패턴에서의 컨트롤러로 이용
서블릿 컨테이너는 클라이언트로부터 HTTP 요청을 수신하고 요청 URL에 따른 서블릿을 호출합니다.
서블릿은 요청 파라미터를 분석하고 요청에 따른 비즈니스 로직을 수행하여 그 결과를 다시 뷰에 전달하는 컨트롤러의 역할을 하게 됩니다.
5. HTTP 프로토콜을 지원하는 javax.servlet.http.HttpServlet 클래스를 상속
서블릿은 'javax.servlet.http.HttpServlet' 클래스를 상속받아 작성되며, 해당 클래스는 HTTP 프로토콜을 기반으로 동작하는 doGet(), doPost(), doPut(), doDelete() 등의 메서드를 제공합니다.
6. UDP 보다 느린 속도
서블릿은 HTTP 프로토콜을 사용하여 클라이언트와 통신합니다.
HTTP는 TCP 기반 프로토콜이기 때문에 UDP 보다 속도가 느릴 수 있지만 신뢰성과 데이터의 순서를 보장하기 때문에 웹 애플리케이션에 더 적합합니다.
7. HTML 변경 시 Servlet을 재 컴파일해야 한다는 단점
서블릿 코드는 Java로 작성되기 때문에 서블릿 내에 HTML 코드를 포함하고 있다면, 해당 코드가 변경될 때마다 서블릿을 재 컴파일하고 재 배포해야 하는 번거로움이 있으며 이는 개발과 유지보수를 어렵게 하는데요.
이러한 문제를 해결하기 위해 JSP(Java Server Pages) 또는 Thymeleaf와 같은 템플릿 엔진을 사용하여 뷰와 로직을 분리하는 방식을 사용합니다.
서블릿을 쓰는 이유
HTTP 요청과 응답은 위 예시와 같은 형식으로 이루어지는데요.
서블릿은 위와 같은 문자열을 파싱 하여 HttpServletRequest, HttpServletResponse 객체를 통해 사용할 수 있도록 만들어줍니다.
(응답의 경우 HttpServletResponse에서 문자열로 파싱 됩니다.)
만약 서블릿을 사용하지 않는다면 개발자는 위와 같은 HTTP 요청의 문자열을 직접 파싱 하여 처리한 뒤 비즈니스 로직을 수행, 비즈니스 로직의 결과를 다시 HTTP 응답 문자열로 만들어서 반환하는 작업들을 직접 구현해야 합니다.
이처럼 HTTP 요청과 응답을 위해 이미 잘 구현된 'Servlet'을 사용함으로써 개발자는 비즈니스 로직을 구현하는 데 집중할 수 있게 됩니다.
서블릿의 생명주기(Servlet Lifecycle)
서블릿은 위 메서드를 통해 생명주기(Lifecycle)가 관리되며, 각 메서드의 내용은 아래와 같습니다.
init() method
서블릿이 처음 생성될 때 한 번 호출되며, 초기화 작업을 수행합니다.
service() method
클라이언트의 각 요청마다 호출되는 메서드입니다. Http Method Type에 따라 내부적으로 'doGet()'이나 'doPost()' 메서드를 호출합니다.
destroy() method
서블릿이 제거될 때 한 번 호출되며, 자원을 해제하거나 종료 작업을 수행합니다.
서블릿은 어디서 만들어지는가
위 내용까지 서블릿의 개념은 어느 정도 이해가 가지만, 실제로 프로젝트를 개발하는 과정에서 서블릿을 직접 만들어서 사용하는 경우가 거의 없기 때문에 서블릿은 대체 어디서 만들어지고 동작하는지 의문점이 생길 수 있는데요.
Java 기반 웹 애플리케이션을 개발한다면 Spring 또는 Spring Boot 프레임워크를 사용하는데, 해당 프레임워크에서는 'DispatcherServlet'과 'Controller'가 서블릿의 역할을 대신하게 됩니다.
DispatcherServlet은 해당 애플리케이션으로 들어오는 모든 요청을 가장 먼저 받아서 요청에 적합한 Controller에 위임해 주는 프론트 컨트롤러(Front Controller) 역할을 하며, Front Controller란 MVC 구조에서 사용되는 디자인 패턴으로 서블릿 컨테이너의 맨 앞에서 서버로 들어오는 클라이언트의 모든 요청을 받아서 처리해 주는 컨트롤러를 의미합니다.
DispatcherServlet 이전에는 각각의 서블릿을 직접 만들고 해당 서블릿의 URL 매핑을 위해 web.xml에 등록해 주는 과정이 필요했지만 잘 구현된 DispatcherServlet이 애플리케이션으로 들어오는 모든 요청을 핸들링해 주고 공통 작업을 처리해 주면서 개발자가 각각의 서블릿을 만들고 등록하는 번거로움이 줄어들게 되었습니다.
하지만 DispatcherServlet도 결국 서블릿의 한 종류인데요. 때문에 java 코드 또는 web.xml 파일을 통해 등록 및 초기화가 되어야 사용할 수 있다는 특징이 있습니다.
(DispatcherServlet의 상속 관계를 살펴보면 DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet 단계를 통해 결국 HttpServlet을 상속하고 있다는 것을 알 수 있습니다.)
서블릿 컨테이너(Servlet Container)
서블릿은 혼자서 동작할 수 없기 때문에 서블릿을 실행하고 관리해 주는 서블릿 컨테이너 내에서 동작하게 됩니다.
서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기를 관리할 뿐만 아니라, 클라이언트의 요청을 받고 응답할 수 있도록 웹 서버와의 소켓 통신을 지원합니다.
대부분 알고 있는 가장 대표적인 서블릿 컨테이너가 바로 'Apache Tomcat'이며, 그 외에 'Jetty' 등의 서블릿 컨테이너가 있습니다.
서블릿 컨테이너는 아래와 같은 역할을 수행합니다.
1. 웹 서버와의 통신 지원
웹 서버는 클라이언트로부터 HTTP 요청을 수신한 뒤 이를 서블릿 컨테이너로 전달합니다.
서블릿 컨테이너에서는 해당 HTTP 요청을 적절한 서블릿으로 보내고, 서블릿은 비즈니스 로직을 수행한 뒤 응답을 생성하여 다시 서블릿 컨테이너로 반환합니다.
서블릿 컨테이너로 반환된 응답은 웹 서버를 거쳐 클라이언트에게 전달됩니다.
2. 서블릿 생명주기(Lifecycle) 관리
서블릿 컨테이너는 서블릿 컨테이너가 시작되거나 서블릿이 호출되는 시점에 서블릿 클래스를 로딩하여 인스턴스화하고 init() 메서드를 통한 초기화를 실행합니다.
이후 클라이언트의 각 요청에 대해 servlet() 메서드를 호출하여 요청을 처리하며, 컨테이너가 종료되는 시점 또는 서블릿이 불필요한 시점에 destroy() 메서드를 호출합니다.
***
요청이 올 때마다 계속 서블릿 인스턴스를 생성하는 것은 비효율적이기 때문에 서블릿 컨테이너에서 서블릿 객체는 싱글톤으로 관리된다는 특징이 있습니다.
이는 단일 인스턴스를 통해 모든 요청을 처리하기 때문에 메모리 사용에 효율적이며, 인스턴스를 한 번만 생성하고 초기화하기 때문에 각 요청마다 인스턴스를 생성하고 초기화하는 비용이 줄어든다는 장점이 있습니다.
3. 멀티스레드 지원 및 관리
서블릿 컨테이너는 클라이언트로부터 다수의 요청을 처리하기 위해 멀티쓰레딩(multi-threading) 환경을 지원하고 관리합니다.
서블릿 컨테이너는 클라이언트로부터 받는 각 요청에 대해 각각의 스레드를 사용하여 처리하는데요.
컨테이너 내부적으로 스레드 풀(thread pool)을 유지하여 요청을 처리할 스레드를 관리하며, 새로운 요청이 들어오면 스레드 풀에서 사용 가능한 스레드를 할당하여 요청을 처리합니다.
만일 모든 스레드가 사용중일 경우 요청은 큐에 대기하게 되며, 요청에 대한 처리가 끝난 스레드가 반환되면 그때 대기 중인 요청이 다시 처리됩니다.
이러한 멀티스레드 환경으로 인해 다수의 클라이언트 요청을 동시에 처리할 수 있고, 스레드를 계속 생성하고 소멸하는 것이 아니라 스레드 풀을 통해 스레드를 재사용하기 때문에 시스템 자원을 효율적으로 관리할 수 있습니다.
***
서블릿이 싱글톤으로 관리되기 때문에 서블릿 내에서 인스턴스 변수나 공유 자원을 사용하게 되는 경우 멀티스레드 환경으로 인한 문제가 발생할 수 있는데요.
때문에 필요한 경우 동기화(synchronization)를 통한 스레드 안전성에 대한 보장이 필요하다는 특징이 있습니다.
4. 선언적 보안 관리
서블릿 컨테이너에서는 web.xml 설정 파일 또는 어노테이션을 통해 웹 애플리케이션의 보안을 관리할 수 있습니다.
web.xml 파일을 통해 보안 설정을 하는 경우 개발자는 소스코드를 변경하지 않고 보안에 대한 제약, 인증, 권한 등을 수정할 수 있다는 장점이 있습니다.
서블릿 및 서블릿 컨테이너의 동작 과정
1. 웹 서버는 클라이언트로부터 요청을 받아 서블릿 컨테이너에게 해당 요청을 위임합니다.
2. 서블릿 컨테이너는 HttpServletRequest, HttpServletResponse 객체를 생성합니다. 이때 HttpServletRequest는 받은 요청을 기반으로 만들어지게 됩니다.
3. 설정 파일(web.xml) 또는 @WebServlet 어노테이션을 통해 해당 요청을 처리할 서블릿을 매핑하고, 매핑된 서블릿에 HttpServletRequest, HttpServletResponse를 전달합니다.
(요청 URL을 기반으로 적절한 서블릿을 매핑하는 과정에서 스레드 풀에 있는 스레드를 할당합니다.)
4. 매핑된 서블릿은 service() 메서드를 호출하고 GET, POST 여부에 따라 내부적으로 doGet(), doPost() 등의 메서드를 호출하여 처리된 결과를 HttpServletResponse에 담습니다.
5. HttlServletResponse를 웹 서버를 통해 클라이언트에게 반환하고 응답을 종료합니다.
(응답을 클라이언트에게 반환하고 사용이 끝난 스레드를 스레드 풀로 반환합니다.)
6. 응답이 종료된 후 HttpServletRequest, HttpServletResponse를 소멸시킵니다.
< 참고 자료 >