백엔드/스프링, 스프링부트, JPA, Spring Webflux

[Spring] 스프링부트 다중유저 처리 (Tomcat- Thread Pool, Connector)

Kangjieun11 2023. 5. 11. 21:59
728x90

 

 

✅ 개요

 

스프링부트가 어떻게 다중요청을 처리하는지에 대해 알아보자. 

 

 

 

 

나는 이전에 스프링 내부 구조와 동작원리, 요청은 어떻게 처리되는가?에 대해 전체적으로 큰그림을 그린적이 있다.

 

그 그림은 요청이 들어왔을때,

WAS, 필터와 필터체인(시큐리티필터) , Dispatcher servlet부터 Controller까지 가는것,  JSON과 DTO의 변환, Mock요청의 경우는 어떻게 되는가? 인터셉터 등등 굉장히 다양한 주제를 한개의 그림에 함축하고 있었다. 

(지식을 확실하게 쌓고 나서 나중에 더 업데이트 해놔야지 ㅎㅎ)

 

 

 

당시 내가 다중유저 처리에 관해 알았던 것은 (그림중 오른쪽의 파란 부분을 보면 된다.)
1) 톰캣 내부에 스레드풀이 존재하고, 미리 스레드를 만들어둔 다음 

2) 요청이 들어올 때마다 1개씩 매칭해서 스레드 단위로 처리가 된다. 

 

 

이정도였는데, 좀더 확실하게 정리하고, 알고자 이 글을 적는다. 

 

 

✅ 다중유저 처리 관점으로 보는   '요청 과정'

1) 스프링부트는 내장된 WAS, Tomcat이 있다.

2) Tomacat안에는 스레드 풀이 있다.  

3) 다중요청을 처리하기 위해 스레드풀에 스레드를 미리 만들어둔다

4) 유저의 요청이 들어오면 스레드 풀에서 스레드를 하나 할당하고, 이 스레드가 필터부터 디스패처서블릿을 거쳐가며 요청을 처리한다. 

5) 작업이 끝나면, 스레드는 스레드풀로 반환된다. 

 

⏺ 스레드와 스레드풀은 무엇일까??

Thread : 실행중인 프로그램(프로세스)에서 실제로 작업을 수행하는 단위 

Thread pool : 프로그램실행에 스레드가 필요하고, 이 스레드를 미리 생성하고 관리하여 필요한 곳에서 사용할 수 있도록 제공한다.

 

 

⏺ 왜 스레드풀에 미리 스레드를 생성해두는걸까?

미리 생성해둔다는것부터 느낌상 재사용의 목적이 강하다.

실제로 스레드를 생성하는것에는 "비용"이 드는데, 비용이 많이 들어서 미리 만들어두고, 재사용하기 위해 스레드풀에 스레드를 생성해두는것이다. 

 

DB Connection pool과도 비슷한 개념이라고 보면 된다.

 

 

⏺ 스레드를 만드는 "비용"이란 ?

비용이라고 하면 무엇을 의미할까?

바로 운영체제 시간에 배웠던 시스템자원을 의미한다.

 

스레드를 만들 때 일정량의 메모리, CPU 시간을 필요로 하게 되는데 이걸 비용이라고 하는것이다. 

 

스레드가 운영체제 수준에서 관리되는 자원이라, 스레드를 생성하면 스레드의 상태를 추적, 스케줄링을 위한 추가 작업이 필요한데 

이런 작업들에서 오버헤드가 생기고, 스레드 생성하는데 시간과 자원(메모리 CPU 시간)이 소비된다.

 

 

✅ 이제 좀더 깊게 들어가보며 공부해보자.

 

⏺ 스레드풀은 스레드를 몇개 보유할까?

 스프링부트에 내장된 톰캣은 애플리케이션을 처리하기 위해 스레드 풀을 사용하고, 

스프링부트 2.x버전에서 기본 스레드 풀의 크기는 200이다. 

 

이 값은 변경 가능하지만, 스레드를 생성하는것은 아까도 말했듯이 "비용"이 들기 때문에 

몇개를 만들어두고 사용하는게 합리적인지 고민해봐야한다.

 

application.properties

server.tomcat.threads.max=200

 

application.yml

server:
  tomcat:
    threads:
      max: 200

 

 

⏺ 스레드풀의 FLOW

 

스레드풀의 동작 흐름은 다음과 같다.

  1. 스레드 풀 초기화 : 애플리케이션 시작하면, 정의된 스레드를 생성하고 초기화 한다. (스레드의 개수는 설정된 수로)
  2. 작업 요청 발생 : 특정 요청이 발생할경우, 스레드풀에 제출되고, 대기열에 들어가게 된다. (아래 Queue
  3. 큐에서 나오면, 작업이 수행된다 : Task를 가져와서 스레드를 할당하고, 스레드에게 실행을 요청한다. 할당된 스레드는 작업을 수행하고, 완료되면 다음 작업을 수행할 준비를 한다. 
  4. 스레드 반환 : 작업이 완료되었기 때문에 스레드는 다시 사용될 수 있게 스레드 풀로 반환된다. 대기큐에 있는 새로운 작업을 가져와서 다시 실행하며 스레드를 재사용하게 된다.
  5. 스레드 풀의 종료 : 애플리케이션이 종료되면, 스레드 풀은 사용한 스레드들을 안전하게 종료시키고, 자원을 해제한다.

* 스레드가 너무 많으면 너무 많은 스레드가 CPU의 자원을 두고 경합하게 됨 -> 처리속도가 느려질 수 있음

* 스레드가 너무 적으면 CPU 자원을 최적으로 활용하기 어렵게 됨 -> 처리속도가 느려질 수 있음

 

따라서 적정 스레드의 수를 유지하도록 해야한다.

 

 

 


 

 

✅ 커넥터 (Connector)

 

커넥터의 개념은 내가 오늘 처음 보는거라 확실하게 설명은 못하지만 일단 개념을 다루기 위해 추가한다. 

 

 

 

✍️ 커넥터란?

네트워크 계층과 웹 서버/애플리케이션 서버 사이에서 동작하며,

다양한 프로토콜과 방식으로 클라이언트와의 연결을 관리

 

웹서버와 HTTP요청을 처리 , TCP/IP소켓연결을 설정하고, 클라이언트의 요청을 받아들이는 역할을 수행함

 

 

  • 커넥터는 클라이언트와의 연결을 관리
  • 커넥터는 스레드풀에서 관리되는 스레드를 사용하여 클라이언트 요청을 처리함. 커넥터는 스레드풀에서 스레드를 가져와 작업을 위임하고, 작업이 완료되면 스레드를 풀로 반환.
  • 스레드는 커넥터에 의해 사용되며, 커넥터는 스레드의 생성 및 소멸을 관리하지는 않는다. (이부분은 스레드풀의 역할)

 

 

📣 커넥터의 방식 2가지

커넥터는 블로킹방식과 넌블로킹방식으로 클라이언트와의 연결, 요청을 처리할 수 있다고 한다.

  • BIO Connector   (Blocking I/O)
  • NIO Connector   (Non-Blocking I/O)

 

1️⃣ Blocking I/O Connector

  • Java의 기본적인 I/O기술을 사용
  • 각 클라이언트 연결마다 새로운 스레드가 생성되어 해당 연결을 처리 - 연결 종료시 스레드는 풀에 반환
  • 동시접속 가능한 사용자수만큼 스레드가 사용되며, 스레드가 idle상태로 대기하는 시간이 많이 발생해 리소스가 낭비될 수 있다.

* idle : 스레드가 아무것도 하지 않고 대기하는 상태, CPU시간 다른 리소스를 소비하지 않는 상태

 

2️⃣ Non-Blocking I/O Connector

BIO의 문제를 해결하기 위해 나온 방식

  • 별도의 스레드인 Poller를 사용해 소켓을 관리한다. (커넥션관리)
  • 데이터 처리가 가능한 순간에만 스레드를 할당한다
  • Poller 는 Selector를 사용해 데이터처리가 가능한 소켓을 생성하고, 해당 소켓을 Worker Thread에게 넘겨 처리한다. 
  • 이방식을 이용하여 idle 상태의 스레드를 줄인다.

 

 

 

 

좀더 자세하게는 봐야겠지만, 커넥터에 이런 것들이 있다 정도만 알고 넘어가자. 

 

 

 

 

개념만 살펴보아도, BIO가 NIO에 비해 나은 점이 솔직히 잘 안보인다.

아마 톰캣 예전 버전에선 BIO가 사용되었겠지만 이젠 NIO위주로 되는 것으로 보인다.

https://tomcat.apache.org/tomcat-8.5-doc/config/http.html

 

 

 



References 

 

 

내가 그린 스프링 큰그림 중

https://sihyung92.oopy.io/spring/1

https://velog.io/@jihoson94/BIO-NIO-Connector-in-Tomcat

https://velog.io/@cjh8746/%EC%95%84%ED%8C%8C%EC%B9%98-%ED%86%B0%EC%BA%A3%EC%9D%98-NIO-Connector-%EC%99%80-BIO-Connector%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90