[web] 서블릿(servlet)
Web Server 와 WAS(Web Application Server)
초기 웹 브라우저는 HTTP 프로토콜을 기반으로 해당 url path 에 맞게 정적인 파일(HTML, CSS, JS, 이미지 등) 만 제공하는 목적이었습니다. 이를 웹 서버라고 합니다. Apache, Nginx 등이 웹 서버의 예시입니다.
반면 WAS 는 웹 서버와는 달리 동적 콘텐츠를 생성하고 제공하는 역할을 합니다. 데이터베이스와의 상호작용, 비즈니스 로직의 실행 등을 할 수 있습니다. WAS는 클라이언트의 요청에 따라 실시간으로 콘텐츠를 생성하고, 이를 웹 서버를 통해 클라이언트에게 전달합니다. Java EE, ASP.NET, PHP 등의 플랫폼에서 동작하는 Tomcat, JBoss, WebLogic 등이 WAS의 예시입니다.
물론 이 두 개념이 분리되어 운용되지 않습니다. 예를 들어 Tomcat 은 기본적으로 HTTP 서버 기능도 가지고 있어서, 웹 서버처럼 정적인 콘텐츠를 제공하는 역할도 수행할 수 있습니다. 이는 Tomcat이 HTTP 요청을 받아서 처리하고 응답을 보낼 수 있기 때문입니다. 따라서 Tomcat은 웹 서버와 WAS의 기능을 모두 가지고 있다고 볼 수 있습니다.
그러나 실제 대규모 프로덕션 환경에서는, 웹 서버(Apache HTTP Server, Nginx 등)와 WAS(Tomcat 등)를 분리하여 운영하는 경우가 많습니다. 이는 각각의 서버가 최적화된 작업을 수행하게 하여 전체 시스템의 성능을 향상시키기 위한 것입니다. 웹 서버는 정적 콘텐츠를 빠르게 제공하고, 동적 콘텐츠가 필요한 경우에만 WAS에 요청을 전달하는 방식으로 작동합니다. 이렇게 하면 WAS는 복잡한 애플리케이션 로직을 처리하는데 집중할 수 있습니다.
톰캣
Apache Tomcat 은 웹 서버 및 자바 서블릿 컨테이너로서, 자바 웹 애플리케이션을 실행하는데 필요한 환경을 제공하는 소프트웨어입니다. 웹 서버의 기능으로는 정적 컨텐츠를 제공하는 역할을 수행하고, 서블릿 컨테이너의 역할로는 자바 코드를 실행하며, HTTP 요청에 따른 동적 컨텐츠를 생성하는 역할을 수행합니다.
톰캣은 Coyote, Catalina, Jasper 로 이루어져있습니다.
- Coyote : Tomcat의 HTTP 커넥터 부분으로, 웹 브라우저와 같은 클라이언트로부터의 네트워크 연결을 처리합니다. Coyote 는 HTTP 프로토콜을 이해하고 HTTP 요청을 처리하여, 이를 “Catalina” 컴포넌트가 이해할 수 있는 형태(Request) 로 변환합니다. 그리고 Catalina 로부터의 응답(Response) 을 다시 HTTP 응답으로 변환하여 클라이언트에게 전달합니다.
- Catalina : Tomcat의 Servlet 컨테이너 부분입니다. Catalina 는 Servlet과 JSP 페이지를 처리하며, 웹 애플리케이션의 생명주기를 관리합니다. 또한, 클라이언트의 요청을 적절한 웹 애플리케이션으로 라우팅하는 역할도 담당합니다. 이 과정에서는 Coyote 컴포넌트에서 전달받은 요청을 처리하고, 응답을 생성하여 다시 Coyote 컴포넌트로 보냅니다.
- Jasper : Tomcat의 JSP 엔진 부분으로, JSP 페이지를 Servlet으로 변환하는 역할을 합니다. 이렇게 변환된 Servlet은 그 후 Catalina 에 의해 처리됩니다. (현재 JSP 는 잘 사용되지 않습니다.)
이러한 톰캣은 스프링부트에서 라이브러리로 내장되어 제공되고 있습니다.
서블릿
서블릿은 동적인 페이지를 생성하는 WAS 서버를 위한 기술입니다. 웹 브라우저에서 요청을 하면 해당 기능을 수행한 후 웹 브라우저에 결과를 전송합니다.
- 클라이언트가 HTTP 요청을 보냅니다. 이 요청은 먼저 톰캣의 Coyote 컴포넌트로 들어옵니다.
- Coyote 컴포넌트는 이 HTTP 요청을 파싱하여
org.apache.coyote.Request
객체와org.apache.coyote.Response
객체를 생성합니다. - 그 다음,
org.apache.coyote.Request
객체와org.apache.coyote.Response
객체는 서블릿 컨테이너인 Catalina 로 전달됩니다. - Catalina는
org.apache.coyote.Request
객체와org.apache.coyote.Response
객체를javax.servlet.http.HttpServletRequest
객체와javax.servlet.http.HttpServletResponse
객체로 변환합니다. - 그 후, Catalina는 클라이언트의 요청을 처리할 적절한 서블릿을 찾아 이
HttpServletRequest
객체와HttpServletResponse
객체를 해당 서블릿에 전달합니다. - 이렇게 전달받은
HttpServletRequest
객체와HttpServletResponse
객체는 서블릿에서 사용되어 클라이언트의 요청을 처리하고, 처리된 결과를HttpServletResponse
객체에 담아서 클라이언트에게 반환합니다. - 클라이언트에게 반환되기 전에
HttpServletResponse
객체는 다시org.apache.coyote.Response
객체로 변환되어 Coyote 에게 전달되고, Coyote 는 이를 HTTP 응답 형식으로 변환하여 클라이언트에게 반환합니다.
서블릿 구조
서블릿의 구조는 개략적으로 아래와 같이 생겼습니다.
public class MyServlet extends HttpServlet {
public void init(ServletConfig config) throws ServletException{
super.init();
}
public void destroy(){
super.destroy();
}
protected void service(HttpServletRequest request, HttpServletReponse response){
super.service(request, response);
}
}
서블릿 컨테이너에서 특정 reqeust 를 받으면 해당 요청에 맞는 서블릿을 생성(init) 합니다. 그리고 service 메서드로 HttpServletRequest, HttpServletReponse 받아서 동작합니다.
아래는 실제 HttpServlet 의 service 메서드를 개략적으로 나타낸 것입니다.
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 = Strings.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);
}
}
}
조건문을 통해 method.equals(METHOD_XXX)
로 어떤 메서드 요청인지 확인해서 doXXX(req, resp);
를 호출합니다. 즉 Get 요청이 들어오면 doGet 을 호출하고, Post 요청이 들어오면 doPost 를 호출합니다.
서블릿 등록/호출
서블릿을 등록하는 방법은 그게 두 가지로, web.xml
을 이용하는 전통적인 방법과 자바 기반의 구성을 사용하는 스프링 방식이 있습니다.
web.xml
먼저 web.xml
을 간단하게 보겠습니다. 다음과 같이 서블릿을 생성하고 해당 서블릿을 특정 url 에 매핑해줍니다.
<web-app>
<servlet>
<servlet-name>myServlet</servlet-name>
<servlet-class>com.example.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>myServlet</servlet-name>
<url-pattern>/my</url-pattern>
</servlet-mapping>
</web-app>
<servlet>
태그를 통해 서블릿 이름과 클래스를 틍록하고, <servlet-mapping>
태그를 통해 url 과 서블릿을 매핑합니다.
그리고 서블릿을 정의해주면 되겠죠. 아래는 MyServlet 클래스입니다.
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println("test");
}
}
해당 url 로 GET 메서드가 들어오면 test
를 출력합니다.
스프링 방식
스프링 방식으로 서블릿을 등록하는 방법은 2가지가 있는데요. @WebServlet
애노테이션을 사용하는 방식과 프로그래밍 방식이 있습니다. 여기서는 서블릿 등록방법을 알아보는 건 아니니 간단한 @WebServlet
방식만 확인해보겠습니다. 프로그래밍 방식은 스프링 부트 활용 웹 서버와 서블릿 컨테이너 포스팅을 확인해보세요.
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println("test");
}
}
xml 형식의 파일이 없고, @WebServlet(urlPatterns = "/my")
을 통해 어느 url 을 사용할지 결정합니다. 해당 url 로 GET 메서드가 들어오면 test
를 출력합니다.
DispatcherServlet
요청마다 servlet 을 정의하면 요청 처리 로직이 분산되고, 공통 로직을 처리하기 어려운 점이 있습니다. 따라서 프론트 컨트롤러 패턴을 구현하기 위해 DispatcherServlet 을 사용합니다. 이 패턴에서 모든 웹 요청은 단일 컨트롤러(여기서는 DispatcherServlet
)를 통해 처리되며, 이 컨트롤러는 요청을 적절한 핸들러(컨트롤러 메소드)로 라우팅합니다. 이렇게 하면 요청 처리 로직이 분산되는 것을 방지하고 중앙에서 요청 처리를 조정할 수 있습니다.
간단하게 표현하면 위 그림처럼 표현할 수 있습니다.
또한 DispatcherServlet 을 사용하면 스프링의 IoC(Inversion of Control) 컨테이너와 통합되어, 의존성 주입, 트랜잭션 관리, 보안 등 스프링의 핵심 기능을 사용할 수 있게 됩니다.
위 흐름은 DispatcherServlet 이후 핸들러를 호출하고 로직을 처리해서 View 를 응답해는 SpringMVC 구조입니다. 여기서는 서블릿까지만 알아봤습니다.
댓글남기기