위 사진은 스프링 부트의 flow를 찾을 때 흔히 보이는 MVC 흐름도입니다. 이 흐름에서 한 유저의 요청을 어떻게 처리하는지를 넘어 여러 유저의 요청이 어떻게 처리되는지를 살펴보겠습니다.
다중 요청은 스프링부트가 처리하는 것이 아닌, 스프링 부트에 내장되어있는 Servlet Container
에서 다중요청을 처리합니다.
웹서버
Servlet Container
를 파악하기 전에, 웹서버에 대한 이해가 필요한데요, 웹서버
란 웹페이지를 사용자에게 전송하는 서버를 의미합니다.
웹서버는 웹페이지를 Client에게 전송하는 서버입니다. 데이터를 전송하기 위해 HTTP 프로토콜을 사용하며, 일반적으로 Client는 브라우저에게 URL(https://velog.ig)을 입력해 웹페이지를 얻습니다. 웹서버가 하는 일은 웹페이지를 사용자에게 전송하는 것입니다.
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는 서블릿과 웹서버가 손쉽게 통신할 수 있게 해주기 때문에, 소켓을 만들고 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 요청 처리과정은 사진과 같습니다.
init()
메서드를 호출하면 서블릿이 초기화됩니다.JVM의 역할
Servlet Container의 가장 중요한 기능은 요청을 올바른 서블릿에 전달해서 처리되도록 하며, JVM이 해당 요청을 처리한 후에는 생성된 결과를 올바른 장소에 동적으로 반환해줍니다.
Spring Boot는 내장 서블릿 컨테이너인 Tomcat
을 사용합니다. Tomcat
이 다중요청을 처리하는 과정은 아래와 같습니다.
Thread Pool
을 생성합니다.HttpServletRequest
)이 들어오면 Thread Pool
에서 하나씩 Thread를 할당합니다.Thread Pool
로 반환됩니다.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들을 미리 생성해놓는다는 개념입니다. Tomcat 3.2 이전의 버전은 유저의 요청이 들어올 때마다 Servlet을 실행할 Thread를 하나씩 생성하고 요청이 끝나면 소멸시켰습니다. 이는 두 가지의 문제점을 일으켰습니다.
- 모든 요청에 대해 스레드를 생성하고 소멸하는데에 OS와 JVM에 많은 부담을 준다.
- 동시에 일정 이상 다수의 요청이 올 경우 CPU와 메모리 자원의 소모가 심하다.
이 때문에 일시적으로 서버가 다운되거나 동시다발적인 요청을 처리하지 못해 생기는 문제가 발생하였습니다. 이를 해결하기 위해 Tomcat은 Thread Pool을 활용하기 시작했습니다.
Thread Pool 플로우는 사진과 같이 쓰레드를 미리 만들어놓고 필요한 작업에 할당했다가 돌려받습니다.
Core Size
만큼의 쓰레드를 생성합니다.Core Size
의 쓰레드 중 Idle
(유휴 상태)인 쓰레드가 있다면 작업 queue에서 작업을 꺼내 쓰레드에 작업을 할당하여 처리합니다.쓰레드는 너무 많으면 해당 쓰레드가 CPU의 자원을 두고 경합하게 되어 처리속도가 늦어지고 너무 적으면 CPU 자원을 최적으로 활용하지 못하기 때문에 처리 속도가 늦어집니다. 적절한 수로 유지되는 것이 가장 효율적입니다.
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사이즈가 의미없습니다.
혹시 “Servlet Container와 Web Server 요청 처리” 부분에서 앞단의 서블릿 영역의 서블릿이 dispatcher servlet이 아닐까요?
Dispatcher servlet 이 모든 요청을 대신 받고, handler view renderer 같은 스프링 컨테이너 요소들을 호출하는 형태가 아닐까요?