스프링 개발을 하다보면 @Controller라는 어노테이션은 마법과 같다.
너무 당연하다고 느꼈었는데 어느 날 왜 이렇게 동작하는지에 대해 궁금증이 생겼다.
나는 그 요청의 메소드를 지정해주고 url을 매핑하고 요청과 응답에 대한 틀만 잘 정해줘서 비즈니스 로직에만 집중을 하고 있었는데
실제로 요청을 어떻게 받으며 응답은 어떻게 보내지는지에 대해 한번도 탐구해 보지 못한 것 같다.
이러한 역할을 하는 것이 서블릿이라는 것은 알겠는데 어떻게 동작하는지 궁금하게 되었다.
오늘은 이 주제에 대해서 탐구해보겠다.
어떠한 사용자가 들어와도 똑같은 페이지만 볼 수 있음.
정적인 페이지만 보여줬었음.
예) 회사소개글
CGI (Common Gateway Interface)란?
- 동적인 데이터를 제공하기 위한 규약
- Web Server와 CGI로 구현된 프로그램(C, PHP) 사이의 규약
CGI를 통해 사용자에 따른 동적인 웹페이지 구현 가능
예) My페이지
BUT!!!!! CGI의 문제점이 있었다!
Process : 실행중인 프로그램 인스턴스 (메모리에 적재된 프로그램)
Thread : 프로세스 내에서 실행되는 여러 흐름의 단위
Singleton : 객체의 인스턴스가 오직 1개만 생성되는 패턴(데이터 공유 가능)
이러한 문제를 보완한 것은?
public interface Servlet {
void init(ServletConfig config) throws ServletException;
void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
void destroy();
}
init()
:
- Servlet 생성시 호출된다.(Initialize)
- Parameter로
ServletConfig
Instance가 넘어온다.
ServletConfig 객체
는 Container가 서블릿을 생성할 때 생성되는 객체- Servlet을 초기화 하고 Servlet이 이용하는 자원을 할당하는 동작을 수행한다.
service()
:
- Servlet으로 요청이 전달 될때마다 호출된다.
- 실제 Service Logic을 수행한다.
- HTTP 메서드에 따라 doXXX메서드를 호출한다.
// GenericServlet이 Servlet Interface를 implements한다. abstract class HttpServlet extends GenericServlet HTTP Method (GET, POST, PUT, DELETE)에 따라 doGet(), doPost(), doPut(), doDelete()메서드 호출한다. doXXX() <- 개발자가 구현
destroy()
:
- Servlet Instance가 사라진다.
- Servlet에서 이용하는 자원을 해지하는 동작을 수행한다.
- 보통 Container가 종료되는 시점에 destroy()호출된다.
클라이언트의 요청이 들어오면 컨테이너는 해당 서블릿이 메모리에 있는지 확인한다.
없을 경우 init()
메소드를 호출하여 적재한다.
init()
메소드는 처음 한번만 실행되기 때문에, 서블릿의 쓰레드에서 공통적으로 사용해야하는 것이 있다면 오버라이딩하여 구현된다.
실행 중 서블릿이 변경될 경우, 기존 서블릿을 파괴하고 init()
을 통해 새로운 내용을 다시 메모리에 적재한다.
init()이 호출된 후
클라이언트의 요청에 따라서 service()
메소드를 통해 요청에 대한 응답을 준다.
doGet(), doPost(), ...등으로 분기된다.
이때 서블릿 컨테이너가 클라이언트의 요청이 오면 가장 먼저 처리하는 과정으로 생성된 HttpServletRequest, HttpServletResponse에 의해 request와 response객체가 제공된다.
컨테이너가 서블릿에 종료 요청을 하면 destroy()
메소드가 호출된다.
한번만 실행되며, 따로 처리해야하는 작업들은 destroy()
메소드를 오버라이딩하여 구현하면 된다.
각 메서드는 Servlet Container(Tomcat)이 호출해준다.
서블릿 객체를 생성하고 초기화하는 작업은 비용이 많은 작업이므로, 다음에 또 요청이 올 때를 대비하여 이미 생성된 서블릿 객체는 메모리에 남겨둡니다.
톰캣이 종료되기 전이나 reload 전에 모든 서블릿을 제거하게 된다.
이렇게 톰캣은 자원을 아끼면서 서블릿을 사용하고 있음.
개발자도 자원을 효과적으로 사용하기 위해서는 서블릿의 생명 주기를 알아야 한다. 이에 대한 방법으로는 초기화하는데 호출되는 init() 메서드를 활용한다. 즉, 요청이 매 번 똑같은 로직을 거쳐서 똑같은 결과를 산출하는 작업은 딱 한번만 수행 되도록 init() 에서 처리하는 것이다.
HttpServletRequest, HttpServletResponse
- WAS가 웹브라우져로부터 Servlet요청을 받으면 요청을 받을 때 전달 받은 정보를
HttpServletRequest
객체를 생성하여 저장- 웹브라우져에게 응답을 돌려줄
HttpServletResponse
객체를 생성(빈 객체)- 생성된
HttpServletRequest
(정보가 저장된)와HttpServletResponse
(비어 있는)를 Servlet에게 전달HttpServletRequest
- Http프로토콜의 request 정보를 서블릿에게 전달하기 위한 목적으로 사용
- Header정보, Parameter, Cookie, URI, URL 등의 정보를 읽어들이는 메소드를 가진 클래스
- Body의 Stream을 읽어들이는 메소드를 가지고 있음
HttpServletResponse
- Servlet은 HttpServletResponse객체에 Content Type, 응답코드, 응답 메시지등을 담아서 전송함
@WebServlet(name = "myServlet", value = "/lifecycle")
public class ServletLifeCycle extends HttpServlet {
private static final long serialVersionUID = 1L;
private final Logger log = LoggerFactory.getLogger(getClass());
public ServletLifeCycle() {
super();
log.error("================================================================== ServletLifeCycle 생성자 실행");
}
@Override
public void init(ServletConfig config) throws ServletException {
log.error("================================================================== init() 실행");
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
log.error("================================================================== service() 실행");
super.service(request, response);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
log.error("****** doGet() 실행");
// View
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head><title>Servlet Lifecycle Example</title></head>");
out.println("<body>");
out.println("<h1>Servlet Lifecycle Example</h1>");
out.println("<p>This is the doGet() method</p>");
out.println("</body></html>")
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response){
log.error("****** doPost() 실행");
}
@Override
public void destroy() {
log.error("================================================================== destroy() 실행");
}
}
@ServletComponentScan
어노테이션을 사용하면 서블릿 등록 가능하다.@ServletComponentScan // 서블릿 자동 등록
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
{host}:{port}/lifecycle
// 초기에 Bean이 로드될 때 생성자가 호출 됨.
================================================================== ServletLifeCycle 생성자 실행
// 처음 해당 페이지 url을 호출 할 때 호출 됨.
================================================================== init() 실행
================================================================== service() 실행
****** doGet() 실행
// 그다음 실행부터는 init()호출 없이 호출됨.
// GET method 호출
================================================================== service() 실행
****** doGet() 실행
================================================================== service() 실행
****** doGet() 실행
================================================================== service() 실행
****** doGet() 실행
.
.
.
// POST method 호출
================================================================== service() 실행
****** doPost() 실행
// 다시 GET method 호출
================================================================== service() 실행
****** doGet() 실행
// 어플리케이션 종료 직전에 실행 됨.
================================================================== destroy() 실행
서블릿 컨테이너는 개발자가 웹서버와 통신하기 위한 복잡한 일들(소켓 생성, 특정 포트 리스닝, 스트림 생성 등)을 할 필요가 없게 해준다. 컨테이너는 서블릿의 생성부터 소멸까지 일련의 과정을 관리한다. 서블릿 컨테이너는 요청이 들어올 때마다 새로운 자바 쓰레드를 만든다. 서블릿 컨테이너의 대표적인 예로는 Tomcat(WAS)이 있다.
서블릿 컨테이너는 서블릿과 웹서버가 손쉽게 통신할 수 있게 해준다 일반적으로 우리는 소켓을 만들고 listen, accept 등을 해야하지만 서블릿 컨테이너는 이러한 기능을 API로 제공하여 복잡한 과정을 생략할 수 있게 해줍니다. 그래서 개발자가 서블릿에 구현해야 할 비즈니스 로직에 대해서만 초점을 두게끔 도와줍니다.
서블릿 컨테이너는 서블릿의 탄생과 죽음을 관리합니다. 서블릿 클래스를 로딩하여 인스턴스화하고, 초기화 메소드를 호출하고, 요청이 들어오면 적절한 서블릿 메소드를 호출한다. 또한 서블릿이 생명을 다 한 순간에는 적절하게 Garbage Collection(가비지 컬렉션)을 진행하여 편의를 제공한다.
서블릿 컨테이너는 요청이 올 때 마다 새로운 자바 쓰레드를 하나 생성하는데, HTTP 서비스 메소드를 실행하고 나면, 쓰레드는 자동으로 죽게된다. 원래는 쓰레드를 관리해야 하지만 서버가 다중 쓰레드를 생성 및 운영해주니 쓰레드의 안정성에 대해서는 걱정하지 않아도 된다.
서블릿 컨테이너를 사용하면 개발자는 보안에 관련된 내용을 서블릿 또는 자바 클래스에 구현해 놓지 않아도 된다. 일반적으로 보안관리는 XML에 기록하므로, 보안에 대한 수정할 일이 생겨도 자바 소스 코드를 수정하여 다시 컴파일 하지 않아도 보안관리가 가능하다.
[자료 1]
[자료 2]
서블릿 컨테이너가 어떤 객체를 저장하고 어떻게 관리할 지에 대한 명세
간단한 예시
<web-app>
<!-- 1. aliases 설정 -->
<servlet>
<servlet-name>myServlet</servlet-name>
<servlet-class>servlets.ServletLifeCycle</servlet-class>
</servlet>
<!-- 2. 매핑 -->
<servlet-mapping>
<servlet-name>myServlet</servlet-name>
<url-pattern>/lifecycle</url-pattern>
</servlet-mapping>
</web-app>
서블릿 스펙에는 필터(filter)란 것도 존재한다.
필터는 요청과 응답에 추가적인 작업을 해야할 때 사용된다.
필터를 적용하면, 클라이언트의 요청이 서블릿에 도달하기 전에 필터를 먼저 거치게된다.
필터도 jakarta(javax).servlet 패키지에서 제공하는 인터페이스이다.
코드를 한번 살펴보자.
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
public default void destroy() {}
}
init()
과 destroy()
는 디폴트 메소드이므로 필수로 재정의할 필요는 없다. doFilter()
public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 필터 초기화 코드
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 요청 로깅
HttpServletRequest httpRequest = (HttpServletRequest) request;
System.out.println("LoggingFilter: Request URI - " + httpRequest.getRequestURI());
// 다음 필터로 요청과 응답 전달
chain.doFilter(request, response);
// 응답 로깅
HttpServletResponse httpResponse = (HttpServletResponse) response;
System.out.println("LoggingFilter: Response status - " + httpResponse.getStatus());
}
@Override
public void destroy() {
// 필터 종료 코드
}
}
<filter-mapping>
선언해서 매핑 할 수 있다.<web-app ...>
<filter>
<filter-name>LoggingFilter</filter-name>
<filter-class>LoggingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>LoggingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>ExampleServlet</servlet-name>
<servlet-class>ExampleServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ExampleServlet</servlet-name>
<url-pattern>/example</url-pattern>
</servlet-mapping>
</web-app>
결국 서블릿이란?
어떤객체?
어떻게?
어디서?
다음시간에
[참고]