Spring Thread Pool

김기현·2022년 7월 18일
0
post-thumbnail

Spring Thread Pool

다중요청 처리

위 사진은 스프링 부트의 flow를 찾을 때 흔히 보이는 MVC 흐름도입니다. 이 흐름에서 한 유저의 요청을 어떻게 처리하는지를 넘어 여러 유저의 요청이 어떻게 처리되는지를 살펴보겠습니다.

다중 요청은 스프링부트가 처리하는 것이 아닌, 스프링 부트에 내장되어있는 Servlet Container에서 다중요청을 처리합니다.

Servlet Container

Web Server

웹서버

Servlet Container를 파악하기 전에, 웹서버에 대한 이해가 필요한데요, 웹서버란 웹페이지를 사용자에게 전송하는 서버를 의미합니다.

웹서버는 웹페이지를 Client에게 전송하는 서버입니다. 데이터를 전송하기 위해 HTTP 프로토콜을 사용하며, 일반적으로 Client는 브라우저에게 URL(https://velog.ig)을 입력해 웹페이지를 얻습니다. 웹서버가 하는 일은 웹페이지를 사용자에게 전송하는 것입니다.

Servlet Container

Servlet Container

Servlet Container란 서버에서 만들어진 서블릿들의 생성, 실행, 파괴를 담당하는 서블릿들을 위한 Container입니다. 즉, 서블릿을 '요구사항 명세서'라고 표현한다면 서블릿 컨테이너는 해당 명세서를 보고 개발하는 '개발자'로 표현할 수 있습니다.

Servlet Container는 Client의 request를 받고 response를 할 수 있도록 웹서버와 소켓을 생성해 통신하며 대표적으로 Tomcat이 있습니다. Tomcat은 웹서버와 소켓을 만들어 통신하며 JSP(Java Server Page)와 Servlet이 작동할 수 있는 환경을 제공합니다.

일반적으로 사용자는 서버에서 오직 정적인 웹페이지만 요청할 수 있는데, Servlet Container는 서버 사이드에서 동적으로 웹페이지를 생성하기 위해 Java를 사용합니다. 그래서 웹서버와 Servlet이 상호작용할 때 Servlet Container는 필수입니다.

Servlet Container 역할

Servlet Container의 역할은 웹서버와의 통신 지원, 서블릿 생명주기 관리, 멀티쓰레드 지원 및 관리, 선언적인 보안 관리로 구분할 수 있습니다.

웹서버와의 통신 지원
Servlet Container는 서블릿과 웹서버가 손쉽게 통신할 수 있게 해주기 때문에, 소켓을 만들고 listen, accept 등을 API로 제공하기 때문에 복잡한 과정을 생략할 수 있습니다.

서블릿 생명주기(Life Cycle) 관리
Servlet Container는 서블릿의 생성과 소멸을 관리합니다. 1) 서블릿 클래스를 로딩하여 인스턴스화를 하며, 2) 초기화 메서드를 호출, 3) 요청이 들어오면 적절한 서블릿 메서드를 호출, 4) 서블릿 소멸 시 Garbage Collection을 진행합니다.

멀티스레드 지원 및 관리
Servlet Container는 요청이 올 때마다 새로운 Java Thread를 하나 생성합니다. HTTP 서비스 메서드를 실행하고 나면 Thread는 자동으로 소멸하는 특징이 있습니다. 이 때 원래는 Thread를 관리해야 하지만, 서버가 다중 쓰레드를 생성 및 운영하기 때문에 안정적으로 운영합니다.

여기서 Thread란 CPU의 자원을 이용하여 코드를 실행하는 하나의 단위입니다.

선언적인 보안관리
Servlet Container를 사용하면 개발자는 보안에 관련된 내용을 서블릿 또는 자바 클래스에 구현하지 않아도 됩니다. 일반적으로 보안 관리는 XML 배포 서술자에 기록하기 때문에, 보안에 대해 수정할 일이 생겨도 자바 소스 코드를 수정하여 다시 컴파일하지 않아도 보안관리가 가능합니다.

Servlet Container와 Web Server 요청 처리


Servlet Container와 Web Server 요청 처리과정은 사진과 같습니다.

  1. 웹서버가 HTTP 요청을 받으면 웹서버는 요청을 Servlet Container로 전달합니다.
  2. 서블릿이 컨테이너에 없다면 서블릿을 동적으로 검색하여 컨테이너의 주소 공간에 로드합니다.
  3. 컨테이너가 서블릿의 init() 메서드를 호출하면 서블릿이 초기화됩니다.
  4. 컨테이너가 서블릿의 service() 메서드를 호출하여 HTTP 요청을 처리합니다. 이때 서블릿은 컨테이너 주소에 남아있고, 다른 HTTP 요청들을 처리할 수 있습니다.
  5. 웹서버는 동적으로 생성된 결과를 올바른 위치에 반환합니다.

JVM의 역할
Servlet Container의 가장 중요한 기능은 요청을 올바른 서블릿에 전달해서 처리되도록 하며, JVM이 해당 요청을 처리한 후에는 생성된 결과를 올바른 장소에 동적으로 반환해줍니다.

Thread Pool

Tomcat


Spring Boot는 내장 서블릿 컨테이너인 Tomcat을 사용합니다. Tomcat이 다중요청을 처리하는 과정은 아래와 같습니다.

  1. Tomcat은 다중 요청을 처리하기 위해서 부팅할 때 쓰레드의 컬렉션인 Thread Pool을 생성합니다.
  2. 유저의 요청(HttpServletRequest)이 들어오면 Thread Pool에서 하나씩 Thread를 할당합니다.
  3. 해당 Thread에서 스프링부트에서 작성한 Dispatcher Servlet을 거쳐 유저 요청을 처리합니다.
  4. 작업을 모두 수행하고 나면 Thread는 Thread Pool로 반환됩니다.

Spring Boot와 내장 Tomcat

Spring과 Spring Boot의 차이점 중 하나는 Tomcat의 유무입니다. 스프링 부트에서는 내장 서블릿 컨테이너(Tomcat)을 지원합니다.

Spring Boot로 생성된 프로젝트에서 Spring Environment(application.yml 혹은 application.properties) 설정만으로 간편하게 Tomcat의 설정을 바꿀 수 있습니다.

아래는 웹서버의 설정을 바꾸어주는 예시이며 default값들입니다.

server:
  tomcat:
    threads:
      max: 200                # 생성할 수 있는 thread의 총 개수
      min-spare: 10           # 항상 활성화 되어있는(idle) thread의 개수
    max-connections: 8192     # 수립가능한 connection의 총 개수
    accept-count: 100         # 작업큐의 사이즈
    connection-timeout: 20000 # timeout 판단 기준 시간, 20초
  port: 8080                  # 서버를 띄울 포트번호

Thread Pool이란?

Thread Pool은 프로그램 실행에 필요한 Thread들을 미리 생성해놓는다는 개념입니다. Tomcat 3.2 이전의 버전은 유저의 요청이 들어올 때마다 Servlet을 실행할 Thread를 하나씩 생성하고 요청이 끝나면 소멸시켰습니다. 이는 두 가지의 문제점을 일으켰습니다.

  1. 모든 요청에 대해 스레드를 생성하고 소멸하는데에 OS와 JVM에 많은 부담을 준다.
  2. 동시에 일정 이상 다수의 요청이 올 경우 CPU와 메모리 자원의 소모가 심하다.

이 때문에 일시적으로 서버가 다운되거나 동시다발적인 요청을 처리하지 못해 생기는 문제가 발생하였습니다. 이를 해결하기 위해 Tomcat은 Thread Pool을 활용하기 시작했습니다.

Thread Pool Flow

Thread Pool 플로우는 사진과 같이 쓰레드를 미리 만들어놓고 필요한 작업에 할당했다가 돌려받습니다.

  1. 첫 작업이 들어오면 Core Size만큼의 쓰레드를 생성합니다.
  2. 유저 요청이 들어올 대마다 작업 queue에 담아둡니다.
  3. Core Size의 쓰레드 중 Idle(유휴 상태)인 쓰레드가 있다면 작업 queue에서 작업을 꺼내 쓰레드에 작업을 할당하여 처리합니다.
    3.1 만약 Idle(유휴 상태)인 쓰레드가 없다면 작업은 작업 queue에서 대기합니다.
    3.2 만약 그 상태가 지속되어 작업 queue가 꽉 찬다면 쓰레드를 새로 생성합니다.
    3.3 이 과정을 반복하다가 쓰레드의 최대 사이즈에 도달하고 작업 queue도 꽉 찬다면 추가 요청에 대해 Connection-refuesed오류를 반환합니다.
  4. 작업이 완료되면 쓰레드는 다시 Idle(유휴 상태)로 돌아갑니다.
  5. 만약 작업 queue가 비어있고 Core Size 이상의 쓰레드가 생성되어있다면 쓰레드를 소멸시킵니다.

쓰레드는 너무 많으면 해당 쓰레드가 CPU의 자원을 두고 경합하게 되어 처리속도가 늦어지고 너무 적으면 CPU 자원을 최적으로 활용하지 못하기 때문에 처리 속도가 늦어집니다. 적절한 수로 유지되는 것이 가장 효율적입니다.

Thread Pool 생성

Thread Pool을 자바에서 구현한 구현체가 ThreadPoolExecutor입니다. 위의 yml 설정 일부입니다.

server:
  tomcat:
    threads:
      max: 200        # 생성할 수 있는 thread의 총 개수
      min-spare: 10   # 항상 활성화 되어있는(idle) thread의 개수
    accept-count: 100 # 작업 큐의 사이즈

이 설정은 Thread의 최대 사이즈 및 Core Size를 변경할 수 있도록 해줍니다. Tomcat 9.0의 Default 옵션은 max 200개, min(core size 기본값) 25개이며 스프링 부트에서는 각각 200개, 10입니다.

accept-count는 작업 queue의 사이즈입니다. 스프링 부트에서 옵션을 주지 않으면 Integer.MAX값을 주는데, 이는 21억이 넘습니다. 이는 무한 대기열 전략으로 아무리 요청이 많이 들어와도 core size를 늘리지 않는다는 의미입니다. 무한 대기열 전략에선 작업 queue가 꽉 찰 일이 없으므로 쓰레드 풀의 Max사이즈가 의미없습니다.

profile
피자, 코드, 커피를 사랑하는 피코커

1개의 댓글

comment-user-thumbnail
2023년 8월 26일

혹시 “Servlet Container와 Web Server 요청 처리” 부분에서 앞단의 서블릿 영역의 서블릿이 dispatcher servlet이 아닐까요?
Dispatcher servlet 이 모든 요청을 대신 받고, handler view renderer 같은 스프링 컨테이너 요소들을 호출하는 형태가 아닐까요?

답글 달기