Spring에서 동시에 요청을 처리하는 방법과 Thread Pool

준하·2024년 12월 12일
0

스레드는 Unit of Execution으로 불리며, CPU 코어의 실행단위이다.
즉, 하나의 프로세스에서 두 개 이상의 스레드를 사용함으로써 두 가지 이상의 작업을 동시에 실행할 수 있다.

하지만 단순히 Thread만 사용해서 동시에 여러 작업을 처리하는 프로그램을 만든다면 문제가 발생한다.

만약 작업 요청이 들어올때마다 스레드를 생성하여 처리한다면 어떤 문제가 발생할까?

스레드 생성비용 문제

Java의 경우 One-To-One Threading 모델로 스레드를 생성한다.

즉, User Thread 생성 시, OS Thread와 연결해야하며,
이는 새로운 스레드를 생성할 때마다 오버헤드가 크게 발생함을 의미한다.

작업 요청에 대해 매번 새롭게 스레드를 생성하여 처리한다면, 결과적으로 최종적인 요청 처리시간이 증가하는 문제가 발생한다.

과도한 스레드 생성 문제

만약 프로세스의 요청 처리 속도보다 더 빠른 속도로 요청이 들어온다면 어떻게 될까?

새로운 스레드가 무제한적으로 계속 생성되며, 스레드가 많아질 수록 메모리를 차지하고, Context-Switching이 더 자주 발생한다.

또한 CPU 자원을 경합하는 경우가 발생할 수 있으며,
이는 하나 이상의 스레드가 데이터를 기록하려고 할 때 다른 스레드가 해당 데이터를 읽으려고 하는 경우이다.

이에 따라 메모리 문제가 발생할 수 있고, CPU 오버헤드가 증가한다.


Thread Pool

이러한 문제를 해결하기 위해 Thread Pool(스레드풀)을 사용한다.
스레드풀은 스레드를 허용된 수 만큼만 사용하도록 제한하는 시스템이다.

스레드풀의 기본 플로우는 다음과 같다.

  1. 처음에는 core size만큼의 스레드를 생성한다.

  2. 유저 요청(Connection)이 들어올때마다 작업 큐에 담는다.

  3. 유휴상태(idle)인 스레드가 있다면 작업 큐에서 작업을 꺼내 스레드에 작업을 할당하여 처리한다.
    3.1 만약 유휴상태인 스레드가 없다면 작업은 작업 큐에서 대기한다.
    3.2 작업 큐가 가득 차면 스레드를 생성한다.
    3.3 max size 만큼의 스레드가 존재하고, 작업 큐도 가득차면 connection-refused 오류를 반환한다.

  4. 작업이 완료되면 스레드는 다시 유휴상태로 돌아간다.

위와 같은 방식으로 생성될 수 있는 스레드의 개수를 제한하고, 한 번 생성된 스레드를 없애지 않고 재사용함으로써, 스레드 생성에 따른 오버헤드를 없앨 수 있다.

즉, 여러 개의 작업을 동시에 처리하면서도 안정적으로 처리하고 싶을 때 Thread Pool은 효과적이다.


Web Server

웹서버의 특성 상 동시에 여러 요청을 처리해야하며, 앞서 설명한 Thread Pool을 사용하기 매우 적합하다.

Tomcat

Tomcat은 Spring Boot의 내장 Servlet Container 중 하나이며, Java 기반의 WAS이며,
Java의 Thread Pool과 매우 유사한 자체 스레드풀 구현체를 가지고 있다.

톰캣의 스레드풀에서는 두 가지 추가적인 요소가 존재한다.

  1. Max-Connections
    : 톰캣이 최대로 동시에 처리할 수 있는 Connection의 개수를 의미한다. Web 요청이 들어오면 톰캣의 Connector가 Connection을 생성하면서 요청된 작업을 ThreadPool의 Thread와 연결한다.

  2. Accept-Count
    : Max-Connections 이상의 요청이 들어왔을때 사용하는 대기열 Queue 사이즈이다. 이 대기열이 꽉 찼을때 들어오는 요청은 거절될 수 있다.

톰캣의 스레드풀 관련 설정은 application.yml 혹은 application.properties 같은 설정파일을 통해 가능하다.

server:
  tomcat:
    threads:
      max: 200
      min-spare: 10
    max-connections: 8192
    accept-count: 100 # Task queue size
    connection-timeout: 20000

위 설정을 하나하나 살펴보자면,

  • max
    : Thread Pool에 생성될 수 있는 스레드의 최대 개수를 의미한다. 기본값은 200이다.

  • min-spare
    : 최소한으로 유지할 스레드의 수를 의미한다.

  • max-connections
    : 한 번에 처리할 수 있는 최대 연결 수이며, 이는 Keep-Alive 상태도 포함한다. 기본값은 8192이다. 사실 상 서버의 실질적인 동시요청 처리 개수라고도 생각할 수 있다.

  • accept-count
    : max-connections를 초과하는 요청이 들어올때 대기할 수 있는 최대 수. 기본 값은 100이다. 너무 작게 설정한다면, 요청이 몰렸을 때 들어오는 요청들을 모두 거절할 수도 있다.

Tomcat 8 버전 이후부터는 Non-Blocking I/O 방식을 채택하여, 기존의 1 connection - 1 thread 방식에서 벗어나 N connection - 1 thread 방식으로 전환되었다.
이를 통해 하나의 스레드가 여러 연결의 작업을 관리할 수 있어, 더 적은 스레드로도 높은 동시성을 처리할 수 있다.

따라서 Non-Blocking I/O 방식의 최신 버전 톰캣을 사용한다면, 최대 스레드의 개수보다 적거나 같은 수의 max-connections를 설정하는 것은 비효율적이라고 한다.

Thread Pool의 설정들은 TPS와 요청에 대한 응답시간을 결정하는 하나의 요소이며, 이러한 설정들이 적절하지 않다면, 병목현상 및 CPU 오버헤드와 메모리 문제를 유발할 수 있다.

profile
A Sound Code in a Sound Body💪

0개의 댓글