Spring에서 요청을 어떻게 처리하고 응답하는 과정에 대해서 이해하기 위해선 Servlet이라는 것에 대해서 알아야 한다.
그 이유는 동적인 요청에 대해서 처리하는 역할을 하는 것이 Servlet이기 때문이다.
이 포스트에서는 Servlet가 무엇인지에 대해서 알아보자.
Servlet이 생긴 이유에 대해서 알기 위해서는 기존에 웹 통신이 어떻게 이루어졌는 지 알아보아야 한다.

기존의 웹 통신은 요청이 오면 index.html 같은 이미 만들어진 정적 파일들을 응답으로 보내주었다.
그래서 다른 사용자들이 요청을 해도 똑같은 index.html을 응답으로 보내주었다.
그런데 시간이 지날 수록 사용자마다 다르게 페이지를 보여주고 싶게 되고 페이지를 동적으로 처리하기를 바라게 된다.
CGI는 Common Gateway Interface의 약자로 요청을 동적으로 처리하기 위한 인터페이스이다.
CGI는 인터페이스이다. 인테페이스는 어떤 규약인데 즉, 웹 서버와 프로그램이 통신을 하기 위한 규약이다.
동적인 페이지를 만들기 위해선 웹 서버가 프로그램과 통신을 주고 받아야 하는데 언어가 다른 경우도 있고 환경이 다른 경우도 있으니 규약이 필요해진다. 이 규약을 인테페이스로 만든이 것이 CGI이다.
(인터페이스이므로 다양한 언어로 구현이 가능하다.)

클라이언트에서 동적인 요청을 보내게 되면 웹 서버는 CGI에게 위임을 한다.
그러면 CGI가 동적인 처리를 하고 이를 html의 형태로 웹 서버에게 보내주면 웹 서버는 받아서 클라이언트에게 전달을 해준다.
CGI를 통해서 사용자들은 자신의 이름이 들어간 페이지라던가 오늘 방문한 방문자 수 같은 동적인 페이지를 받을 수 있게 되었다.
하지만 CGI에는 여러 가지 문제가 있었다.

첫 번째로 요청이 올 때마다 프로세스를 생성하는 것이다.
프로세스는 메모리를 많이 차지한다. 즉, 요청이 올 때마다 쓸데 없이 메모리를 많이 차지하게 되고 이는 메모리 낭비로 이어지는 것이다.
두 번째는 같은 객체를 사용하더라고 요청이 다르면 새로 객체를 생성이 되는 것이다.
이도 마찬가지로 객체가 차지하는 메모리가 많기 때문에 메모리 낭비로 이어지게 된다.
위에 말한 CGI의 문제를 해결하기 위해서 나온 것이 Servlet이다.
서블릿은 CGI와 다르게 스레드를 사용해서 들어온 요청을 처리한다.

Servlet는 Java는 단일 인스턴스를 사용을 하고 있기 때문에 객체를 여러 개 생성할 필요가 없다.

Servlet은 개발자가 관리를 하지 않는다.
Servlet은 WAS, Web Application Server가 관리를 한다.
WAS에는 Servlet을 관리하는 Web Container(이하 웹 컨테이너)가 있고 이 Web Container 안에는 Servlet를 관리하는 Servlet Container(이하 서블릿 컨테이너)가 존재한다.
그래서 웹 서버에서 동적인 요청이 들어오면 WAS로 넘기면 웹 컨테이너에서 Servlet을 관리하면서 동적인 처리를 한다.
이제 Servlet에 대해서 파헤져보자.
먼저, Servlet의 생명 주기를 알아보고 동작원리에 대해서 알아보자.
Servlet의 생명주기에는 초기화, 작업 수행, 종료가 있다.
init() - 초기화 : 서블릿 인스턴스를 생성되는 메소드
service() - 작업 수행 : 실제 기능이 실행이 되는 메소드
destory() - 종료 : 서블릿 인스턴스를 메모리에서 지우는 메소드
그러면 이제 요청이 들어왔을 때 서블릿이 어떻게 동작을 하는 지 알아보자.

브라우저에서 요청을 보내면 Http Request를 WAS에 보내고 WAS의 웹 컨테이너는 서블릿 컨테이너에 요청을 보내주면 서블릿 컨테이너는 Request 객체와 Response 객체를 만든다. 그리고 서블릿에게 넘겨준다.
서블릿 컨테이너는 이 요청이 어느 서블릿에 대한 요청인지를 판단을 한다. 만약 해당 서블릿이 존재하지 않는다면 서블릿의 인스턴스를 생성을 하고 init() 메소드를 실행한다.
다음으로 service()를 실행을 한다. service() 메소드를 통해서 doGet(), doPost() 같은 메소드로 요청을 처리한다.
요청 처리가 완료가 되면 Response를 통해서 응답을 작성을 하고 반환을 한다.
이제 Servlet의 동작원리에 대해서 알아보았으니 Servlet의 코드에 대해서 알아보자.

Servlet은 인터페이스 Servlet와 ServletConfig를 구현한 GenericServlet를 HttpServlet가 상속을 받고 있고 이를 사용해서 Servlet를 사용을 한다.
Servlet는 우리가 위에서 이야기 했던 init, service 등 메소들이 있고 ServletConfig는 servlet을 초기화할 때, 필요한 설정들을 관리하기 위해서 필요한 것이다.
이 두 인터페이스를 구현한 것이 GenericServlet이고 이 GenericServlet를 상속 받아서 HttpServlet을 사용을 하고 있다.
우리는 Servlet 인터페이스와 HttpServlet 클래스를 살펴볼 것이다.
// Servlet 인터페이스
package jakarta.servlet;
import java.io.IOException;
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
Servlet 인터페이스를 보면 위에서 살펴 본 init(), service(), destory() 메소드들이 있다.
이제 이 후에 구현체에서 이 메소드들을 구현을 해서 servlet 구현체에서 사용을 할 수 있다.
public abstract class HttpServlet extends GenericServlet {
// 생략
public void init(ServletConfig config) throws ServletException {
super.init(config);
this.cachedUseLegacyDoHead = Boolean.parseBoolean(config.getInitParameter("jakarta.servlet.http.legacyDoHead"));
}
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
if (ifModifiedSince < lastModified / 1000L * 1000L) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
} catch (ClassCastException var6) {
throw new ServletException(lStrings.getString("http.non_http"));
}
this.service(request, response);
}
// 생략
}
Http Servlet에는 많은 메소들이 있지만 우리는 init(), service()를 살펴볼 것이다.
init() 메소드에 경우에는 말 그대로 부모 클래스의 init()를 불러와서 초기화를 진행을 하고 초기화 파라미터 값을 boolean 값으로 변경을 해서 변수 저장을 해줌으로서 요청이 다시 들어올 때 초기화 하는 과정을 생략을 해주는 역할을 해준다.
service()는 두 개가 있다.
먼저, 아래에 있는 service()를 보면 HttoServletRequest와 HttpServletResponse로 형 변환을 해준 뒤에 위에 있는 service()로 넘겨주면서 service()를 실행을 한다.
두 번쨰 service()에서는 request에서 method를 꺼내와서 메소드의 종류에 따라 그에 맞는 do 메소드들을 실행을 시켜준다.
우리는 Servlet이 어떻게 나왔고 어떻게 동작을 하고 내부는 어떻게 구성이 되어 있는 지 알아보았다.
이제 이 HttpServlet를 상속을 받아서 실제로 동작을 하는 원리 한 번 구현을 해보자.
먼저, 설정을 해주고 실행이 되는 지 확인을 해보자
@SpringBootApplication
@ServletComponentScan
public class ServletApplication {
public static void main(String[] args) {
SpringApplication.run(ServletApplication.class, args);
}
}
Application class에 @ServletComponentScan이라는 어노테이션을 통해서 @WebServlet 어노테이션이 붙은 클래스를 스캔할 해서 등록을 할 수 있도록 해준다.
package sample.servlet.study;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("----- 실행 -----");
}
}
@WebServlet를 통해서 Servlet의 이름과 url을 맵핑을 시켜주고 요청이 들어왔을 때 잘 실행이 되는 지를 확인을 하기 위해서 실행이라는 글을 출력을 시켜보자

localhost:8080/hello로 요청을 보냈을 때 실행이 잘 나오는 것을 알 수 있다.
package sample.servlet.study;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("request = " + req);
System.out.println("response = " + resp);
}
}
다음으로는 request와 response를 출력을 해보았다.

request와 response는 객체이다.
위에서 서블릿 컨테이너가 HttpRequest를 통해서 request, response 객체를 만들어서 servlet에 넘겨준다고 했는데 그 request, response가 바로 여기 사진에 나와 있는 객체이다.
이제 파라미터를 받아보자.
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
System.out.println("username = " + username);
}
}
request에 getParameter() 메소드를 사용을 해서 값을 받아서 오면

이렇게 받을 수 있는 것을 확인을 할 수 있다.
그러면 이 username을 받아서 간단하게 동적으로 페이지를 반환을 해보자.
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
System.out.println("username = " + username);
resp.setContentType("text/plain");
resp.setCharacterEncoding("utf-8");
resp.getWriter().write("hello" + username);
}
}
response를 이용을 해서 반환을 해줄 내용을 작성을 하고 먼저, 이상혁이라는 이름으로 요청을 보내보자.

이상혁이라는 이름으로 잘 나온다. 그러면 이번에는 김철수라는 이름을 파라미터로 넣어서 요청을 보내보자.

파라미터를 김철수로 변경을 해서 보내보니 이름이 변경이 되어서 잘 나오는 것을 알 수 있었다.
우리가 username이라는 파라미터를 넣어서 우리가 원하는 이름이 나올 수 있도록 하였다.
이처럼 서블릿은 우리가 원하는 값을 통해서 사용자마다 화면이 다르게 혹은 화면에서 일부분이 다르게 보일 수 있도록 동적으로 페이지 처리를 도와주는 역할을 한다.
또 요청에 따라 서블릿을 다르게 해서 원하는 형태로 보여줄 수도 있다.
이러한 서블릿으로 인해서 JSP와 MVC 패턴으로 이어지기도 하게 된다.
[10분 테코톡] 리오, 밀리의 Servlet & Spring MVC
[10분 테코톡] 👨🎨규동의 Servlet & Spring
[10분 테코톡] 🌻타미의 Servlet vs Spring
Servlet (서블릿)이해하기 + 실습예제
Servlet