개요
기본 의존성 중 하나인 spring-boot-starter-web 모듈을 사용하면, 내장 톰캣을 사용하는 스프링 MVC 구조를 기반으로 동작한다. 일반적으로 하나의 프로세스에 하나의 스레드가 작업하는 것과 달리, 하나의 프로세스 내에서 여러 스레드가 동시에 작업을 수행하는 것이다. Spring boot에 내장된 tomcat를 비롯한 대부분의 웹서버는 멀티스레드를 통해, 모든 사용자가 자신이 원하는 작업을 원활히 할 수 있다.
그러나 요청하는 client 수가 많아지면 그만큼 thread 생성, 소멸 비용 및 오버헤드가 발생한다.
이와 같은 문제점을 해결하기 위해 스레드 풀을 사용한다. 서블릿 컨테이너인 tomcat에서 스레드 풀을 활용하여 다중 요청 처리해준다.
또한 tomcat은 NIO Connector와 BIO Connector로 두가지가 존재하는데, 9.0부턴 NIO 기반의 커넥터가 Default가 되었다.
또한 스프링 부트 2.x부턴 톰캣 9.0이 적용되었다.
→ NIO 기반의 커넥터지만, SpringMVC에서 사용하는 Dispatcher Servlet의 HttpServletRequest, HttpServletResponse가 결과적으로 ServletInputStream, ServletOutputStream을 사용하여 결국 IOStream을 상속받는 구조다. 결국 동기적으로 수행한다.
톰캣 프로세스
server:
tomcat:
threads: -> worker thread?
max: 200 # 생성할 수 있는 thread의 총 개수
min-spare: 10 # 항상 활성화 되어있는(idle) thread의 개수
max-connections: 8192 # 수립가능한 connection의 총 개수 -> connector 내부 poll event queue
accept-count: 100 # 작업큐의 사이즈 -> thread pool 내 작업 큐
connection-timeout: 20000 # timeout 판단 기준 시간, 20초
port: 8080 # 서버를 띄울 포트번호
스프링 부트에서 @RequestMapping을 통해 handlerMapping, handlerAdapter 자동 등록해준다.
- 우선, tomcat은 port listen을 통해 Socket 을 생성하여 client로부터 연결 요청을 대기함.
- 요청을 acceptor에 accept를 통해 가져옴.
- accept를 통해 소켓에서 socket channel 객체를 얻어서 pollerEvent 객체로 캡슐화해줌.
- acceptor는 event queue에 해당 객체를 저장해둠. pollerEvent는 NIO Channel로 등록됨.
- poller스레드에는 NIO selector를 가지고 있음 selector에는 다수의 채널이 등록되어있음.
- select 동작을 수행하여 데이터 읽을 수 있는 poller event객체를 얻음.
- 그리고 worker thread pool에서 유휴상태의 worker thread를 얻어서 poller event를 worker thread에게 넘기게 됨.
→ worker thread는 tomcat에서 관리하는 thread pool에서 가져온 thread인 듯???
→ 데이터를 읽을 수 있을 때만 thread를 사용하므로 낭비되는 스레드가 줄어들게 됨. - poller event는 작업 큐에 저장됨.
- 첫 작업이 들어오면, core size 만큼의 스레드 생성
- core size의 스레드 중 유휴상태(idle)인 스레드가 있다면 작업 큐에서 작업을 꺼내 스레드에 작업을 할당
- 만약 유휴상태인 스레드가 없다면, 작업은 작업 큐에서 대기
- 그 상태가 지속되어 작업 큐가 꽉 찬다면, 스레드를 새로 생성
- 스레드 최대 사이즈 에 도달하고 작업큐도 꽉 차게 되면, 추가 요청에 대해선 connection-refused 오류를 반환
- 해당 스레드에서 서블릿 컨테이너는 DispatcherServlet이 메모리에 있는지 확인, 없다면 init()메소드 호출하여 메모리에 적재.
- 서블릿 컨테이너는 HttpServletRequest, HttpServletResponse를 생성한다.
- 그리고 로드된 DispatcherServlet에서 service() 메소드를 호출하여, HTTP 메소드에 따라 내부의 doGet 이나 doPost 등을 호출한다.
- DispatcherServlet은 핸들러 매핑을 통해 요청 URL에 매핑된 controller를 탐색한다.
- 조회한 컨트롤러를 실행할 수 있는 핸들러 어댑터를 조회한다.
- 핸들러 어댑터(HandlerAdapter)를 통해 Controller를 호출한다.
- Controller를 실행하여 요청을 처리하고, 응답을 다시 핸들러 어댑터로 반환한다.
- 핸들러 어댑터는 이 응답을 ModelAndView로 가공하여 반환
- view를 반환해야 하여 @Cotroller 사용했을 때 흐름
- view Resolver를 찾고 실행
- View Resolver는 View의 논리 이름을 물리 이름으로 바꾸고, 랜더링 역할을 담당하는 View 객체를 반환
- View를 랜더링 하여 클라이언트에 반환
- view 반환 하지 않아 @RestController 사용했을 때 흐름
- View와 ViewResolver를 거치지 않는다.
- Controller로 부터 반환 받은 데이터를 MessageConverter를 거쳐서 Json 형식으로 변환
- Json을 ResponseBody로 응답.
- 응답을 반환하면 HttpServletRequest, HttpServletResponse 두 객체를 소멸시킴.(서블릿은 싱글톤 객체이므로 서블릿을 소멸시키지는 않음)
- 작업을 모두 수행하고 나면 스레드는 스레드 풀로 반환된다.
- 작업큐가 비어있고 core size이상의 스레드가 생성되어있다면 스레드를 destory함.
스레드 풀 특징
- 요청마다 쓰레드 생성의 단점을 보완한다.
- 필요한 스레드를 스레드 풀에 저장하고 관리한다.
- tomcat은 최대 200개가 기본 설정
- 너무 낮게 설정할 경우 : 응답 지연 발생
- 너무 높게 설정할 경우 : CPU, 메모리 리소스 임계점 초과로 서버 다운
- 보통 CPU 사용률 확인 후, 최소 50%는 사용할 수 있도록 설정함.(성능 테스트는 필수)
- 최대 스레드가 모두 사용중이라 풀에 스레드가 없으면?
- 기다리는 요청 거절
- 특정 수 만큼 대기하도록 설정 가능
- 장점
- 스레드가 미리 생성되어있어, 스레드 생성과 종료 비용이 절약되며, 응답시간이 빠름
- 생성 가능한 스레드의 최대치가 있어 많은 요청이 들어올 때 기존 요청은 안전하게 처리 가능
- 단점
- 작업 큐 사이즈, max-connections 수치보다 더 많은 요청이 들어올 때, 들어오는 요청에 대해 받지 못함
스레드 세이프
- 스프링에서는 기본적으로 멀티 스레드 방식을 사용하며, 싱글톤 패턴을 이용해 모든 빈을 공유함.
- 스프링은 기본적으로 스레드 세이프하지 않은 환경이다.
- 따라서 스프링 빈을 불변 객체로 관리하도록 개발할 필요성이 있다.
서블릿 생명주기
- 서블릿은 싱글톤 객체로, 스레드끼리 공유되며, 따라서 불변성 객체를 유지하여 개발해야 함.
public abstract class HttpServlet extends GenericServlet {
...
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
}
...
- 클라이언트 요청이 들어오면, 컨테이너는 해당 서블릿이 메모리에 있는지 확인, 없다면 init()메소드 호출하여 메모리에 적재.
- 서블릿 컨테이너는 클라이언트 요청이 올때마다 HttpServletRequest, HttpServletResponse를 생성한다. 그리고 service() 메소드 내에서 doGet 이나 doPost 등이 호출된다.
- 응답을 브라우저에 전송한다.
- 서버가 종료되거나, 서블릿이 수정되면 destroy 메서드 호출을 통해 DB 커넥션을 닫고,서블릿을 종료시켜서 GC 대상이 되게 한다.
서블릿
- 클라이언트가 어떤 요청을 했을 때 그에 대한 결과를 전송해주는 자바 프로그램
- 웹서버가 동적인 페이지를 제공할 수 있도록 도와주는 어플리케이션
- java thread를 이용하여 동작
- HTTP 프로토콜 서비스를 지원하는 javax.servlet.http.HttpServlet 클래스 상속
- HTML을 사용하여 요청에 응답함.
서블릿 컨테이너
- 서블릿을 관리해주는 역할
- 서블릿 객체는 싱글톤으로 관리됨.
- 최초 로딩 시점에 서블릿 객체를 미리 만들고 재활용 함.
- 따라서 공유 변수 사용에 주의해야 함.
- 웹 서버와 통신 지원
- 웹서버와 통신할 수 있도록 소켓 만들고 listen, accept하는 기능을 API로 제공
- 서블릿 생명주기 관리(생성, 초기화, 호출, 종료)
- 서블릿 생명 주기를 관리하여 생명이 다 한 순간엔 GC를 진행함
- 사용자 요청에 대한 멀티 스레드 지원 및 관리
- 요청이 올 때마다 새로운 스레드를 가져갔다가 http 서비스 메소드를 실행하고 나면 스레드를 반납함.
- 선언적인 보안 관리
핸들러 매퍼
@Controller
@RequestMapping("/accounts")
public class AccountController {
}
- 설정된 url을 통해 컨트롤러를 선택하는 기준이 되는 인터페이스
- HandlerMapping이란 Http 요청 정보를 이용해서 핸들러(컨트롤러)를 찾아주는 기능을 수행
핸들러 어댑터
@Controller
@RequestMapping("/accounts")
public class AccountController {
@GetMapping("/{id}")
public String hello(@PathVariable Long id) {
//...
}
}
- 핸들러 매핑에서 결정된 핸들러를 직접 실행하는 기능을 하는 인터페이스
- 핸들러 매핑으로 가져온 핸들러를 Handler Adapter들을 돌면서 각 support()메서드에 대입하고 부합하는 어댑터의 handle() 메서드를 통해 핸들러를 실행