[Spring] 서블릿 컨테이너 개념과 쓰레드 풀

Loopy·2023년 3월 7일
0
post-thumbnail

1️⃣ 웹 어플리케이션 이해

☁️ 웹 시스템 구성

웹 시스템은 주로 웹 서버, 웹 어플리케이션 서버, DB로 구성된다. 정적 리소스는 웹 서버가 처리하고, 애플리케이션 로직 같은 동적 처리는 WAS에 요청을 위임하는 방식이다.

🔖 웹 서버 VS 웹 애플리케이션 서버(WAS)

  • 웹 서버 : HTTP 기반 동작, 정적 리소스 제공한다. ex) Nginx
  • 웹 애플리케이션 서버(WAS) : 웹 서버 기능 포함 + 애플리케이션 로직/코드 수행한다. ex) Tomcat

그렇다면 왜 웹 서버를 앞단에 두는 것이 필요할까?

WAS 가 정적 리소스 처리와 애플리케이션 로직 모두 실행이 가능하지만, 너무 많은 역할을 담당하면 서버에 과부화가 올 수 있으며 WAS 에 장애가 나면 오류 화면 조차 제공하지 못하게 된다.

따라서 둘을 분리한다면 효율적인 리소스 관리 가능하며 (각 WEB 서버 / WAS 증설) WAS와 DB 장애 시 웹 서버가 오류 화면 제공이 가능해진다.

☁️ 서블릿(Servlet)

서블릿이란, 웹 기반의 요청에 대한 동적인 처리가 가능한 Server Side에서 돌아가는 자바 프로그램이다. 즉, 개발자는 비즈니스 로직에만 집중할 수 있도록 웹 애플리케이션 서버를 직접 구현할때 필요한 모든 기능들을 제공하는 것을 말한다.

만약 서블릿이 없다면? 다음과 같이 서버 TCP/IP 연결대기, HTTP 요청 메시지와 바디 내용 파싱, 비즈니스 로직 실행, HTTP 응답 메시지 생성, TCP/IP 응답 전달 및 소켓 종료 작업을 직접 구현해줘야 할 것이다.

서블릿 제공 기능

@WebServlet(name = " helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
   
   @Override
   protected void service(HttpServletRequest request, HttpServletResponse response){
     //애플리케이션 로직
   }
}
  • HttpServletRequest : HTTP 요청 정보 사용 가능하다.
  • HttpServletResponse : HTTP 응답 정보 제공 가능하다. WASResponse 객체에 담긴 내용으로 HTTP 응답 정보를 생성한다.

서블릿 동작 과정

HTTP 요청 시 웹 어플리케이션 서버는 Request, Response 객체를 새로 만들어서 서블릿 객체 호출한다. 개발자는 요청 객체에 있는 정보를 사용하고, 응답 객체를 활용해 응답 정보를 입력하면 웹 어플리케이션 서버는 응답 객체에 담겨있는 정보로 실제 HTTP 응답을 생성한다.

서블릿 컨테이너

🔖 서블릿 컨테이너 특징
1. 싱글톤으로 관리
2. 동시 요청을 위한 멀티 스레드 제공

☁️ 서블릿 컨테이너와 싱글톤

톰캣처럼, 서블릿을 지원하는 WAS이며 서블릿 객체의 생명주기(생성/초기화/호출/종료)를 관리해주는 것을 서블릿 컨테이너라고 한다.

중요한 점은 서블릿 객체는 싱글톤으로 관리된다는 점이다.
HTTP 요청/응답 정보는 클라이언트 마다 다른 정보를 가지므로 매 요청마다 새로운 객체가 생성되는 것이 맞지만, 어플리케이션 로직이 들어 있는 서블릿 객체는 하나만 생성해두고 여러 클라이언트가 재사용하는것이 효율적이다.

따라서 최초 로딩 시점에 서블릿 객체 하나만 생성해두고 재활용한다면, 고객의 요청은 동일한 서블릿 객체 인스턴스에 접근할 수 있을 것이다.(공유 변수 사용에 주의하자.)

☁️ 서블릿 컨테이너와 멀티 스레드

서블릿 객체는 그렇다면 누가 실행시키는 것일까? 답은 스레드에 존재한다.

스레드는 애플리케이션 코드 하나하나를 순차적으로 실행한다. 하지만 다수의 요청에 대해 동시 처리가 필요하다면, 하나의 스레드가 지연되어도, 다른 스레드는 정상 동작 할 수 있도록 반드시 요청 시 마다 신규 쓰레드를 생성 해야 한다. 하나의 스레드로 다수의 요청을 처리한다면 데드락 현상이 발생할 수 있기 때문이다.

🔖 데드락 현상
멀티 프로그래밍 환경에서 한정된 자원을 얻기 위해 서로 경쟁하는 상황 발생한다. 이 떄 한 프로세스가 자원을 요청했을 때, 동시에 그 자원을 사용할 수 없는 상황이 발생할 수 있는데 이때 프로세스는 대기 상태로 들어간다. 대기 상태로 들어간 프로세스들이 실행 상태로 변경될 수 없을 때 '교착 상태' 발생한다.

요청 마다 스레드 생성의 단점

  1. 쓰레드 생성 비용은 매우 비싸므로, 응답 속도가 늦어진다.
  2. 컨텍스트 스위칭 비용이 발생
  3. 쓰레드 생성에 제한이 없으므로, CPU와 메모리 임계점을 넘어 서버가 죽을 수 있다.

따라서 우리는 미리 스레드를 생성해놓고 재사용 하는 방식을 고려하게 되는데, 이를 스레드 풀이라고 한다.

쓰레드 풀

스레드 풀은 필요한 쓰레드를 쓰레드 풀에 보관하고 관리하며, 쓰레드가 필요한 경우 풀에서 꺼내서 사용하고 종료시 풀에 다시 반납하면 된다.

또한 생성 가능한 쓰레드의 최대치를 설정 가능하다. 톰캣과 같은 경우 기본 최대치는 200 이다. 만약 최대 쓰레드가 모두 사용 중인 경우에는 요청을 거절하거나 특정 숫자만큼 대기 하도록 설정할 수 있다.

쓰레드 풀 장점

쓰레드 생성/종료하는 비용(CPU)이 절약 되며, 응답 시간이 빨라진다. 또한, 개발자가 멀티 스레드 관련 코드를 신경쓰지 않아도 되어 생산성이 증가한다.

🔖 WAS 의 주요 튜닝 포인트

  1. 최대 쓰레드(MAX THREAD)
    동시 요청이 많은데 너무 낮게 설정하는 경우, 서버 리소스(CPU)는 여유롭지만 클라이언트 응답이 지연되는 문제가 발생한다. 너무 높게 설정한다면, CPU와 메모리 리소스 임계점 초과로 서버가 다운 될 수 있다.

  2. 쓰레드 풀의 적정 숫자
    적정 숫자는 애플리케이션 로직의 복잡도, CPU, 메모리, IO 리소스 상황에 따라 모두 다르므로 nGrinder와 같은 툴을 사용해 성능 테스트를 해보는 것이 필요하다.

☁️ SSR과 CSR

SSR

서버 사이드 렌더링(SSR) 이란 HTML 최종 결과를 서버에서 만들어서, 웹 브라우저에 전달하는 방식을 의미한다. 주로 정적인 화면에 사용한다. ex) JSP, 타임리프

CSR

클라이언트 사이드 렌더링(CSR) 이란 HTML 결과를 자바스크립트 이용해 웹브라우저에서 동적으로 생성해 적용하는 것을 의미한다. 주로 복잡하고 동적인 화면에 사용한다. ex)React, Vue.js

2️⃣ 서블릿(Servlet) 구현을 통한 이해

사실 스프링과 서블릿은 전혀 관련이 없지만, 스프링 부트는 톰캣 서버를 내장하고 있으므로 스프링 부트 환경에서 서블릿 컨테이너를 사용할 수 있다.

@ServletComponentScan  // 서블릿 자동 등록
@SpringBootApplication
public class ServletApplication {
}
@WebServlet(name = "helloServlet", urlPatterns = "/hello") // 서블릿 애노테이션
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

	      // 1. 요청
        String username = request.getParameter("username");
        System.out.println("username = " + username);

	      // 2. 응답
        response.setContentType("text/plain");      //헤더 정보(content type)
        response.setCharacterEncoding("utf-8");     //헤더 정보(content type)
        response.getWriter().write("hello " + username);    //http body
    }
}

HTTP 요청을 통해 매핑된 URL 호출되면, 서블릿 컨테이너는 다음의 service() 메서드를 호출한다.

☁️ HttpServletRequest

HTTP 요청 메시지 형태는 다음과 같다.

  POST /save HTTP/1.1           //start line
  Host: localhost:8080          //header
  Content-Type: application/x-www-form-urlencoded   //header
  username=kim&age=20        //body
  • START LINE : HTTP 메소드, URL, 쿼리 스트링, 스키마/프로토콜
  • HEADER : 헤더 조회
  • BODY : form 파라미터 형식 조회 / message body 데이터 직접 조회

이외에도 Request에는 부가 기능을 제공한다.
임시 저장소 기능은, 해당 HTTP 요청이 시작부터 끝날 때 까지 유지되는 임시 저장소 기능이다. 저장은 request.setAttribute(name, value), 조회는 request.getAttribute(name) 로 한다. 또한 세션 관리 기능이 존재한다. request.getSession(create: true)

☁️ HTTP 요청 데이터 전달 방법

1. GET 쿼리 파라미터

메세지 바디 없이, URL쿼리 파라미터에 데이터를 포함해서 전달하는 방식이다. request.getParameter 로 쿼리 파라미터를 조회할 수 있다. content-type 은 지정할 필요가 없기 때문에 null이 된다.
1) 모든 파라미터 조회

request.getParameterNames().asIterator()
            .forEachRemaining(paramName -> System.out.println(paramName +  "=" + request.getParameter(paramName)));

2) 단일 파라미터 조회

String username = request.getParameter("username");
String age = request.getParameter("age");

3) 이름이 같은 복수 파라미터 조회

String[] usernames = request.getParameterValues("username"); //여러개 있을때 사용

2. POST HTML Form

메시지 바디에 쿼리 파리미터 형식으로 전달하는 방식이다. content-typeapplication/x-www-form-urlencoded 로 설정된다. 따라서, 마찬가지로 request.getParameter() 이용해 Form 을 통해 전송한 데이터를 가져올 수 있다.

3. API 메시지 바디-단순 텍스

HTTP message body직접 데이터를 담아서 요청하는 방식이다.

ServletInputStream inputStream = request.getInputStream();    //바이트 코드
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);     // 문자열로 변환(인코딩 종류 지정)
response.getWriter().write("ok");
  • inputStream : byte 코드를 반환하기 때문에 이후 문자로 변환하는 작업이 필요하다.(Charset 지정 필요)

4. API 메시지 바디-JSON

HTTP message body 에 JSON 형식으로 데이터를 보내는 방식이다.(REST API) content-typeapplication/json 이 되며 JSON 형식을 담기 위한 객체가 필요하다.

문자열을 JSON 타입으로 변환해주는 라이브러리 ObjectMapper 를 사용한다면, 자동으로 객체가 값을 넣어주는 작업까지 완료한다.

private com.fasterxml.jackson.databind.ObjectMapper objectMapper = new ObjectMapper();

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
   ServletInputStream inputStream = request.getInputStream();   //바이트 코드
   String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);//문자열로 변환

   HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);

   response.getWriter().write("ok");
}

☁️ HTTPServletResponse

HTTP 응답 메시지를 생성하는 기능을 제공한다. HTTP 응답코드 지정, 헤더 생성, 바디 생성등을 담당한다.

기본 사용법

@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header")
public class ResponseHeaderServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //[status-line]
        response.setStatus(HttpServletResponse.SC_OK);

        //[response-headers]
        response.setHeader("Content-Type", "text/plain;charset=utf-8");
        response.setHeader("Cache-Control", "no-cache ,no-store, must-revalidate" );
        response.setHeader("Pragma", "no-cache");
        response.setHeader("my-header", "hello");

        //[header 편의 메서드]
        content(response);
        cookie(response);
        redirect(response);

        //[message body]
        PrintWriter writer = response.getWriter();
        writer.println("ok");
    }
}

부가 기능

  1. Content 편의 메서드
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
  1. 쿠키 편의 메서드
//response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600");
Cookie cookie = new Cookie("myCookie", "good");
cookie.setMaxAge(600); //600초
response.addCookie(cookie);
  1. redirect 편의 메서드
//response.setHeader("Location", "/basic/hello-form.html");
response.sendRedirect("/basic/hello-form.html");

☁️ HTTP 응답 데이터 - HTML/JSON

  1. HTML 형식
response.setContentType("text/html")
  1. JSON 형식
response.setContentType("application/json")

objectMapper.writeValueAsString 를 이용해서 객체를 JSON 문자로 변환해준다. 이후 Spring MVC를 사용한다면 HelloData 객체를 반환하기만 하면 자동으로 처리해준다.

private ObjectMapper objectMapper = new ObjectMapper();

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    response.setContentType("application/json");
    response.setCharacterEncoding("utf-8");

    HelloData helloData = new HelloData();
    helloData.setUsername("kim");
    helloData.setAge(20);

   String result = objectMapper.writeValueAsString(helloData);
   response.getWriter().write(result);
}

참고 자료
servletcontainer VS springcontainer
급하게 알아보는 스프링 기반 기술
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

profile
개인용으로 공부하는 공간입니다. 피드백 환영합니다 🙂

0개의 댓글

관련 채용 정보