[Spring] 서블릿

보라보라·2024년 3월 12일
0

servlet

목록 보기
1/1

스프링 개발을 하다보면 @Controller라는 어노테이션은 마법과 같다.
너무 당연하다고 느꼈었는데 어느 날 왜 이렇게 동작하는지에 대해 궁금증이 생겼다.
나는 그 요청의 메소드를 지정해주고 url을 매핑하고 요청과 응답에 대한 틀만 잘 정해줘서 비즈니스 로직에만 집중을 하고 있었는데
실제로 요청을 어떻게 받으며 응답은 어떻게 보내지는지에 대해 한번도 탐구해 보지 못한 것 같다.
이러한 역할을 하는 것이 서블릿이라는 것은 알겠는데 어떻게 동작하는지 궁금하게 되었다.
오늘은 이 주제에 대해서 탐구해보겠다.

서블릿

서블릿이란

  • 자바 웹 어플리케이션 개발을 가능하게 한 기술 (인터페이스)
  • Spring Web MVC패턴에서 서블릿을 사용한다.
  • 클라이언트가 어떤 요청을 하면 그에 대한 결과를 다시 전송 해야함.
    예) 어떤 사용자 로그인
    1. 사용자(Client) : 아이디와 비번 입력 후 로그인 버튼(Request)
    2. 서버(Server) : 클라이언트의 아이디와 비번 확인 후 다음페이지 띄워주기(Response)

서블릿은 왜 나왔는가?

1. 정적 데이터만 전달하는 WebServer


어떠한 사용자가 들어와도 똑같은 페이지만 볼 수 있음.
정적인 페이지만 보여줬었음.
예) 회사소개글

2. 동적 데이터를 처리하는 CGI의 출현

CGI (Common Gateway Interface)란?

  • 동적인 데이터를 제공하기 위한 규약
  • Web Server와 CGI로 구현된 프로그램(C, PHP) 사이의 규약

CGI를 통해 사용자에 따른 동적인 웹페이지 구현 가능
예) My페이지

BUT!!!!! CGI의 문제점이 있었다!

  • 요청이 들어올 때마다 Process를 만듦. -> Thread로 바꿔야 하지 않을까?
  • 요청마다 GCI 구현체가 생기게 됨. -> 여러 Instance에서 Singleton으로 바꾸면 더 효율적이지 않을까?

    Process : 실행중인 프로그램 인스턴스 (메모리에 적재된 프로그램)
    Thread : 프로세스 내에서 실행되는 여러 흐름의 단위
    Singleton : 객체의 인스턴스가 오직 1개만 생성되는 패턴(데이터 공유 가능)

이러한 문제를 보완한 것은?

3. CGI를 보완한 Servlet

  • Request가 들어올 때마다 Thread가 생김
  • 그리고 같은 요청에는 Singleton으로 생성된 구현체로 응답되어짐.
  • tomcat과 같은 WebApplicationServer(WAS)에서 동적 데이터를 처리할 수 있음.
  • WebContainer : 요청이 들어오면 Thread를 생성하고, Servlet을 실행시킨다. Servlet interface에 따라 Servlet을 관리한다.

Servlet Interface

코드보기

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()호출된다.

서블릿의 생명주기

  1. 클라이언트의 요청이 들어오면 컨테이너는 해당 서블릿이 메모리에 있는지 확인한다.
    없을 경우 init() 메소드를 호출하여 적재한다.
    init()메소드는 처음 한번만 실행되기 때문에, 서블릿의 쓰레드에서 공통적으로 사용해야하는 것이 있다면 오버라이딩하여 구현된다.
    실행 중 서블릿이 변경될 경우, 기존 서블릿을 파괴하고 init()을 통해 새로운 내용을 다시 메모리에 적재한다.

  2. init()이 호출된 후
    클라이언트의 요청에 따라서 service()메소드를 통해 요청에 대한 응답을 준다.
    doGet(), doPost(), ...등으로 분기된다.
    이때 서블릿 컨테이너가 클라이언트의 요청이 오면 가장 먼저 처리하는 과정으로 생성된 HttpServletRequest, HttpServletResponse에 의해 request와 response객체가 제공된다.

  3. 컨테이너가 서블릿에 종료 요청을 하면 destroy() 메소드가 호출된다.
    한번만 실행되며, 따로 처리해야하는 작업들은 destroy()메소드를 오버라이딩하여 구현하면 된다.



https://hudi.blog/servlet-and-servlet-container/

각 메서드는 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, 응답코드, 응답 메시지등을 담아서 전송함

서블릿 생명주기 구현

  • SpringBoot 2.x 환경
  • 해당 로그 레벨은 눈에 띄게 error 레벨로 함.
@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() 실행");
    }
}
  • Spring Boot는 내장된 서블릿 컨테이너를 사용하기 때문에 web.xml 파일을 사용하지 않습니다.
  • 대신에 @ServletComponentScan어노테이션을 사용하면 서블릿 등록 가능하다.
@ServletComponentScan // 서블릿 자동 등록
@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
  • 결과 : Postman으로 요청함.
    {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)이 있다.

1) 웹서버와의 통신 지원

서블릿 컨테이너는 서블릿과 웹서버가 손쉽게 통신할 수 있게 해준다 일반적으로 우리는 소켓을 만들고 listen, accept 등을 해야하지만 서블릿 컨테이너는 이러한 기능을 API로 제공하여 복잡한 과정을 생략할 수 있게 해줍니다. 그래서 개발자가 서블릿에 구현해야 할 비즈니스 로직에 대해서만 초점을 두게끔 도와줍니다.

2) 서블릿 생명주기 관리

서블릿 컨테이너는 서블릿의 탄생과 죽음을 관리합니다. 서블릿 클래스를 로딩하여 인스턴스화하고, 초기화 메소드를 호출하고, 요청이 들어오면 적절한 서블릿 메소드를 호출한다. 또한 서블릿이 생명을 다 한 순간에는 적절하게 Garbage Collection(가비지 컬렉션)을 진행하여 편의를 제공한다.

3)멀티쓰레드 지원 및 관리

서블릿 컨테이너는 요청이 올 때 마다 새로운 자바 쓰레드를 하나 생성하는데, HTTP 서비스 메소드를 실행하고 나면, 쓰레드는 자동으로 죽게된다. 원래는 쓰레드를 관리해야 하지만 서버가 다중 쓰레드를 생성 및 운영해주니 쓰레드의 안정성에 대해서는 걱정하지 않아도 된다.

쓰레드 풀 방식 (요청 마다 쓰레드 생성의 단점 보완)

특징
  • 필요한 쓰레드를 쓰레드 풀에 보관하고 관리한다.
  • 쓰레드 풀에 생성 가능한 쓰레드 최대치를 관리한다. 톰캣은 max 200개가 기본 설정이다.
사용
  • 쓰레드가 필요하면, 이미 생성되어 있는 쓰레드를 쓰레드 풀에서 꺼내서 사용한다.
  • 사용을 종료하면 쓰레드 풀에 사용 끝난 쓰레드 반납
  • 쓰레드 풀에 사용 가능한 쓰레드가 없다면, 기다리는 요청은 거절하거나 특정 숫자 만큼 대기하도록 설정이 가능하다.
쓰레드 풀 관리 팁
  • WAS의 주요 튜닝 포인트는 최대 쓰레드 설정 개수이다
  • 이 값이 너무 낮게 설정되면, 서버 리소스는 널널하지만 클라이언트는 응답 지연 현상을 겪게 된다. (비효율)
  • 이 값이 너무 높게 설정되면, 서버 리소스 임계점 초과로 서버 다운
WAS의 멀티 쓰레드 지원 정리
  • 멀티 쓰레드에 대한 부분은 WAS가 처리해준다
    • 개발자가 멀티 쓰레드 관련 코드를 신경쓰지 않아도 된다.
      -> 개발자는 싱글 쓰레드 프로그래밍 하는 것 처럼 편리하게 소스 개발 가능
    • 멀티 쓰레드 환경이므로 싱글톤 객체(서블릿, 스프링 빈)는 주의해서 사용해야 한다.

4) 선언적인 보안 관리

서블릿 컨테이너를 사용하면 개발자는 보안에 관련된 내용을 서블릿 또는 자바 클래스에 구현해 놓지 않아도 된다. 일반적으로 보안관리는 XML에 기록하므로, 보안에 대한 수정할 일이 생겨도 자바 소스 코드를 수정하여 다시 컴파일 하지 않아도 보안관리가 가능하다.

서블릿 동작 과정

[자료 1]

[자료 2]

  • 상태를 가진다는 것은 서블릿 내부에서 멤버 변수를 가지고 이를 변경하는 것을 의미

web.xml

서블릿 컨테이너가 어떤 객체를 저장하고 어떻게 관리할 지에 대한 명세

  • web application의 설정을 위한 설정 파일
  • Deploy할 때 Servlet정보를 설정해 준다.
  • 즉, 서블릿을 서블릿 컨테이너에 등록하고, URL과 매핑을 시켜주어야 하는데, 이것을 진행하는 곳

간단한 예시

<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>
  1. aliases 설정
  • 서블릿 이름을 실제 서블릿 클래스에 연결
  • myServlet과 아래 매핑 설정에서의 servlet-name은 반드시 같아야 한다.
  • servlets.ServletLifeCycle은 개발자에 의해 작성된 실제 클래스 이름으로 설정해야 한다.
  • Ex. (패키지 이름).(서블릿 클래스 이름)
  1. 매핑
  • URL을 서블릿 이름에 연결
  • /lifecycle은 클라이언트(browser)의 요청 URL에서 앱(프로젝트) 이름 뒤에 오는 부분으로, 슬래시(‘/’)로 시작해야 한다.

서블릿 필터

서블릿 스펙에는 필터(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()
    파라미터로 들어오는 Request와 Response를 통해 추가적인 작업을 할 수 있으며, FilterChain을 사용하여 다음 필터에 넘겨주는 형태로 사용할 수 있다.

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() {
        // 필터 종료 코드
    }
}
  • web.xml 파일에 아래처럼 <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>

서블릿의 단점

  • 화면에 표현될 HTML 코드를 프로그램적으로 작성해야 한다.
  • 서비스하기 전에 반드시 컴파일을 해야 한다.

결론

결국 서블릿이란?

  • 객체입니다.

어떤객체?

  • 동적 컨텐츠를 가능하게 하는

어떻게?

  • 자바 기반으로!!!!

어디서?

  • WAS에서!!

그리하여 MVC패턴의 세계가 펼쳐졌으니...

다음시간에

[참고]


profile
쉽게쓰려고 노력하는 블로그

0개의 댓글