개요
Servlet은 자바로 개발된 웹 애플리케이션에서 클라이언트의 HTTP 요청에 대해 적절한 동작을 수행하고 응답을 작성하도록 설계된 자바 인터페이스입니다. 검색해보면 Servlet에 대해 대부분 이와 같이 정의합니다. 하지만, 요청에 대해 적절한 동작을 수행하고 응답한다라는 정의는 어딜가도 흔히 볼 수 있는 정의입니다. 실제로 오라클에서도 아래와 같이 Servlet을 정의합니다.
What Is a Servlet?
A servlet is a Java programming language class that is used to extend the capabilities of servers that host applications accessed by means of a request-response programming model. Although servlets can respond to any type of request, they are commonly used to extend the applications hosted by web servers. For such applications, Java Servlet technology defines HTTP-specific servlet classes.
The javax.servlet and javax.servlet.http packages provide interfaces and classes for writing servlets. All servlets must implement the Servlet interface, which defines life-cycle methods. When implementing a generic service, you can use or extend the GenericServlet class provided with the Java Servlet API. The HttpServlet class provides methods, such as doGet and doPost, for handling HTTP-specific services.javax.servlet 및 javax.servlet.http
이런 정의로는 Servlet이 무엇인지 정확히 이해하기 어렵습니다. 그래서 해당 글을 작성하면서 Servlet을 어떻게 이해하면 좋을까 고민하다가 "특정 자원(URI)에 대한 요청을 처리하는 CRUD 패키지(컴포넌트)"라고 개인적으로 정의했습니다. 먼저 Servlet은 설명한 것과 같이 인터페이스입니다. 따라서, Servlet이라는 인터페이스를 구현하는 여러 클래스가 존재할 수 있습니다. Servlet의 구현체는 특정 자원(URI)에 대해 일대다로 대응해 클라이언트의 요청을 처리합니다. 예를 들면, 아래 그림[1]과 같습니다. A라는 경로로 들어오는 요청은 ServletA가 처리하고, B라는 경로로 들어오는 요청에 대해서는 ServletB가 처리합니다.
다음으로 Servlet의 구현체는 HTTP Method(POST, GET, PUT, DELETE 등)에 대응하는 메서드를 최대 1개까지 가질 수 있습니다. 없어도 괜찮습니다. 그럼 클라이언트는 404를 응답받게 됩니다. 구체적인 프로세스는 이후 코드를 직접 보면서 알아보겠습니다. 아래 그림[2]와 같이 A라는 경로로 들어오는 모든 요청은 ServletA가 처리하게 되고 ServletA는 요청의 Http Method에 따라 요청을 분기처리한 후 정해진 로직을 실행합니다. 여기까지가 제가 생각한 Servlet의 간단한 메커니즘입니다. 이해하는데 도움이 되었으면 좋겠습니다.
서블릿 동작 흐름과 생명주기
그림[3]은 AI에게 질문한 Servlet의 동작 흐름과 생명주기이고, 그림[4]는 그림[3]을 기반으로 시각화 한 자료입니다. 위에서 정리한 Servlet의 정의와 같이 클라이언트로부터 특정 자원(URI)에 대한 요청이 들어오면 미리 정의된 Servlet 중 1개를 선택해 요청을 처리합니다. Serlvet 또한 인스턴스이기 때문에 여러 경로에 매핑해서 사용할 수 있습니다. 위에서 일대다라는 표현을 쓴 이유입니다. 물론 복수의 경로에 동일한 기능을 제공하는 Servlet을 중복 등록한다면 자원 낭비라고 생각합니다.
서블릿 예제 구현해보기
Servlet에 대해 이해가 잘 되지 않아 아주 간단하게 Servlet이 동작하는 과정을 구현해보았습니다. 먼저 클라이언트의 요청을 처리하기 위한 TCP 소켓 서버입니다. 이번 예제에서 Tomcat 서버의 역할을 한다고 생각하면 됩니다. Tomcat 서버는 Servlet 목록을 관리하고 조회하기 때문에 ServletContainer라는 개념에 포함되는 것 같습니다. 찾아보니 혼용해서 사용하는 것 같아 해당 글에서는 그냥 Tomcat이라고 표현하겠습니다. 클라이언트의 요청이 들어오면 가장 먼저 대응하는 부분이다 보니 앞서 보았던 Servlet의 동작 흐름을 가장 잘 파악할 수 있는 부분이라고 생각합니다.
public class Server {
private static final Logger logger = Logger.getLogger("Server");
private static final Map<String, Servlet> servletMapping = Map.of("/", new SimpleServlet());
private static final int PORT = 8080;
public static void main(String[] args) {
launch();
}
public static void launch() {
try(ServerSocket serverSocket = new ServerSocket(PORT)) {
logger.info("Server is running on port " + PORT + "\n");
while(true) {
// 1. 클라이언트 요청: 클라이언트가 특정 URL로 HTTP 요청을 전송합니다.
try(
Socket clientSocket = serverSocket.accept();
InputStream in = clientSocket.getInputStream();
OutputStream out = clientSocket.getOutputStream();
) {
String httpRequest = convertInputStreamToString(in);
if (httpRequest.isEmpty()) continue;
// 2. 요청 및 응답 처리를 위한 ServletRequest 및 ServletResponse 객체를 생성합니다.
HttpServletRequest request = new RequestFacade(new Request(httpRequest));
HttpServletResponse response = new ResponseFacade(new Response(out));
// 3. 요청 URL과 매핑된 Servlet이 있는지 확인합니다.
Servlet servlet = servletMapping.get(request.getRequestURI());
if (servlet != null) {
// 4. Servlet 객체를 초기화합니다.
servlet.init();
// 5. 클라이언트의 요청에 대해 정해진 로직을 수행합니다.
servlet.service(request, response);
// 7. Servlet 객체를 소멸시킵니다.
servlet.destroy();
} else {
String notFoundResponse = "HTTP/1.1 404 Not Found\r\n\r\n";
out.write(notFoundResponse.getBytes());
}
}
}
} catch (BindException e) {
logger.info("Port " + PORT + " is already in use.\n" + e.getMessage());
} catch (IOException e) {
logger.info("An error occurred while running the servlet example server.\n" + e.getMessage());
}
}
private static String convertInputStreamToString(InputStream in) throws IOException {
StringBuilder sb = new StringBuilder();
byte[] buffer = new byte[1024];
int bytesRead;
while((bytesRead = in.read(buffer)) != -1) {
sb.append(new String(buffer, 0, bytesRead));
if(sb.toString().contains("\r\n\r\n")) break;
}
return sb.toString();
}
}
다음은 Servlet 인터페이스와 구현체인 HttpServlet입니다. 아래 인터페이스와 같이 Servlet의 기본 설계는 매우 간단합니다. 실제로 자바에서 제공하는 Servlet 인터페이스에는 메서드가 2개 더 있지만 중요한 내용이 아닌 것 같아 생략했습니다. 구현체인 HttpServlet에서는 클라이언트 요청의 HttpMethod에 따라 어떤 로직을 수행할지 결정합니다. 실제 비즈니스 로직은 HttpServlet을 확장(상속)하는 클래스에서 구현되기 때문에 HttpServlet 클래스에서는 모든 HttpMethod에 대해 기본적으로 404 응답을 반환합니다.
public interface Servlet {
void init();
void service(ServletRequest req, ServletResponse res) throws IOException;
void destroy();
}
public class HttpServlet implements Servlet {
private final Logger logger = Logger.getLogger("HttpServlet");
private static final String METHOD_POST = "POST";
private static final String METHOD_GET = "GET";
private static final String METHOD_PUT = "PUT";
private static final String METHOD_DELETE = "DELETE";
@Override
public void init() {
logger.info("Servlet init");
}
@Override
public void service(ServletRequest req, ServletResponse res) throws IOException {
service((HttpServletRequest) req, (HttpServletResponse) res);
}
protected void service(HttpServletRequest req, HttpServletResponse res) throws IOException {
String method = req.getMethod();
switch (method) {
case METHOD_GET -> doGet(req, res);
case METHOD_POST -> doPost(req, res);
case METHOD_PUT -> doPut(req, res);
case METHOD_DELETE -> doDelete(req, res);
default -> setNotImplementedResponse(res);
}
}
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
setNotImplementedResponse(res);
}
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
setNotImplementedResponse(res);
}
protected void doPut(HttpServletRequest req, HttpServletResponse res) throws IOException {
setNotImplementedResponse(res);
}
protected void doDelete(HttpServletRequest req, HttpServletResponse res) throws IOException {
setNotImplementedResponse(res);
}
@Override
public void destroy() {
logger.info("Servlet destroy");
}
private void setNotImplementedResponse(HttpServletResponse res) throws IOException {
res.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
res.setContentType("text/plain");
res.write("Not Implemented");
}
}
마지막으로 HttpServlet을 확장(상속)하는 클래스입니다. 해당 클래스에서 클라이언트로부터 특정 자원(URI)에 대한 요청이 들어왔을때 동작하는 비즈니스 로직이 구현됩니다. 구현하지 않은 PUT, DELETE 등 요청에 대해서는 HttpServlet에서 404 응답을 반환합니다. 아래 코드는 학습을 위해 간단하게 구현해본 클래스이므로 자세한 코드를 보고싶으시면 해당 글에서 이해한 내용을 바탕으로 실제 자바 코드를 살펴본다면 도움이 될 것 같습니다.
public class SimpleServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
res.setStatus(HttpServletResponse.SC_OK);
res.setContentType("text/plain");
res.write("GET 요청이 성공적으로 처리되었습니다.");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
res.setStatus(HttpServletResponse.SC_OK);
res.setContentType("text/plain");
res.write("POST 요청이 성공적으로 처리되었습니다.");
}
}
실습에 사용된 전체 코드를 보고 싶으시다면 아래 깃헙을 참고해주세요. 실제 자바 라이브러리 코드를 보고싶으시다면 Tomcat 라이브러리를 참고하시면 됩니다. 문의와 지적은 감사히 받겠습니다. 감사합니다.
java-network-programming-exercise/src/servlet at main · zzzzseong/java-network-programming-exercise
자바 네트워크 프로그래밍 실습 레포지토리. Contribute to zzzzseong/java-network-programming-exercise development by creating an account on GitHub.
github.com
참고자료
What Is a Servlet? - The Java EE 5 Tutorial
What Is a Servlet? A servlet is a Java programming language class that is used to extend the capabilities of servers that host applications accessed by means of a request-response programming model. Although servlets can respond to any type of request, the
docs.oracle.com
[JSP] 서블릿(Servlet)이란?
1. Servlet(서블릿) 서블릿을 한 줄로 정의하자면 아래와 같습니다. 클라이언트의 요청을 처리하고, 그 결과를 반환하는 Servlet 클래스의 구현 규칙을 지킨 자바 웹 프로그래밍 기술 간단히 말해서,
mangkyu.tistory.com