본문 바로가기
Spring/Boot

[Spring] SpringBoot 의 다중 요청 처리

by 기몬식 2024. 8. 22.

이전 글에서 자바의 스레드스레드풀에 대해 다루며 스레드를 이용해 여러 작업을 동시에 처리하는 방법 그리고 스레드풀을 통해 효율적으로 스레드를 관리하는 방법에 대해 알아보았습니다. 이제 스레드와 스레드풀의 개념을 조금 더 확장해서 스프링 부트를 사용하면서 서버에서 여러 사용자의 요청을 동시에 처리하는 상황에 대해서 살펴보도록 하겠습니다.

MVC

출처

위 이미지는 사용자의 요청이 서블릿을 통해 컨트롤러 그리고 서비스와 리포지토리로 전달된 후 다시 응답이 사용자에게 돌아가는 과정을 설명합니다. 사실 우리가 흔히 보는 MVC 구조는 한 사용자의 요청 흐름을 설명하는 데 중점을 둡니다. 하지만 실제 애플리케이션 환경에서는 많은 사용자가 동시에 요청을 보내며, 이러한 요청은 모두 서버에서 병렬로 처리되어야 합니다. 여기서 스레드와 스레드풀이 중요한 역할을 하게 됩니다.

 

자바의 스레드 풀 개념은 스프링 부트 내에서 서블릿 컨테이너를 통해 구현되며 스프링 부트는 여러 요청을 효율적으로 관리하기 위해 서블릿 컨테이너의 스레드 풀을 활용하여 다중 요청을 처리합니다. 서블릿 컨테이너와 스레드풀이 스프링 부트에서 어떻게 연관되어 작동하는지 그리고 다중 요청 처리 메커니즘이 어떻게 구현되는지에 대해 자세히 알아보겠습니다.

Servlet Container

출처

 

스프링 부트는 기본적으로 Tomcat 이 내장 서블릿 컨테이너로 설정됩니다.톰캣은 자바 서블릿 표준을 구현한 컨테이너로 웹 서버로서 역할을 하며 클라이언트로부터 들어오는 다중 요청을 효율적으로 처리하는 역할을 합니다. 이때 톰캣은 다중 요청 처리에 대한 서블릿 컨테이너로써 책임지며 스레드 풀을 생성하고 각각의 요청을 스레드에 할당합니다. 미리 정의된 수의 스레드를 만들어 두고 요청이 들어오면 해당 스레드 중 하나를 할당하여 요청을 처리할 수 있게 됩니다.

설정

스프링 부트는 Environment 인터페이스를 통해 Profile 과 Property 를 추상화하여 관리하며 이를 통해 애플리케이션 설정을 쉽게 제어할 수 있습니다. 이때 외부 파일, 시스템 환경 변수, 자바 시스템 속성 등 다양한 키-값(key-value) 형식의 속성을 통합하여 관리할 수 있습니다.

 

이 중 가장 흔히 사용되는 외부 파일을 이용한 resource 디렉토리 내의 application.yml 또는 application.properties 파일을 활용하여 아래와 같이 설정할 수 있습니다.

 

server:
  tomcat:
    threads:
      max: 200 # 생성할 수 있는 최대 thread 개수
      min-spare: 10 # 최소 idle thread 개수
    max-connections: 8192 # 최대 connection 개수
    accept-count: 100 # 작업 큐의 사이즈
    connection-timeout: 20000 # timeout 시간(20초)

 

이처럼 yml 파일을 활용하여 복잡한 서버 설정을 간단하게 정의할 수 있으며 이러한 설정이 제공되지 않더라도 스프링 부트의 AutoConfiguration 기능에 의해 org.springframework.boot.autoconfigure.web.ServerProperties 클래스에 정의된 기본 값으로 자동으로 설정됩니다. 이를 통해 개발자는 별도의 설정 없이도 최적화된 기본 구성을 사용할 수 있습니다.

Connector

출처

Connector 는 Java 기반 웹 애플리케이션 서버에서 사용되는 I/O 처리 방식으로 가장 먼저 클라이언트의 요청(socket)을 받게 되면 socket connection 을 통해 데이터 패킷을 얻습니다. 그 후 데이터 패킷을 파싱해서 HttpServletRequest 로 캡슐화하여 Servlet Container 로 요청을 전달합니다. 이때 Connector 는 blocking I/O 방법인 BIO Connector 와 non-blocking I/O 방법인 NIO Connector 두가지 방식을 사용합니다.

BIO Connector

BIO Connector 는 요청 시 하나의 스레드가 할당되어 요청을 처리하지만 Socket Connection 이 닫힐 때까지 blocking 된 상태로 대기하게 됩니다.


즉 BIO Connector 와 같은 처리 방식은 동시에 사용 가능한 스레드 개수가 동시 접속 가능한 사용자의 수로 한정이 되어 버립니다.

 

출처

 

위와 같이 하나의 요청 당 하나의 스레드가 할당되어 blocking 된 스레드는 유휴 상태로 리소스를 소모하며 효율적으로 자원을 활용하지 못하고 대부분의 스레드들은 idle 상태로 낭비되게 됩니다.

 

이를 해결하기 위해 등장한 Connector 방식이 non-blocking 방식으로 동작하는 NIO Connector 입니다. (BIO Connector 는 위와 같은 이유로 Tomcat 9.0부터 삭제되었습니다.)

NIO Connector

출처

NIoEndPoint에는 Acceptor, Poller, Worker라는 요소들이 있는데 각 요소들은 다음과 같은 역할을 합니다.

Acceptor

NIoEndPoint의 Acceptor는 Socket Connection 을 accept합니다. 소켓에서 Socket Channel 객체를 얻어서 톰캣의 NioChannel 객체로 변환하고 NioChannel 객체를 PollerEvent 객체로 한번 더 캡슐화해서 PollerEvent Queue 에 넣게 됩니다.

Poller

출처

NIO Connector 에선 Poller 라고 하는 별도의 스레드가 커넥션을 처리합니다. Poller 는 NIO의 Selector를 가지고 있는데 Selector 에는 다수의 채널이 등록되어 있고 select 동작을 수행하여 데이터를 읽을 수 있는 소켓을 얻습니다. 그리고 Worker ThreadPool 에서 이용할 수 있는 Worker Thread 를 얻어서 해당 소켓을 Worker Thread 에게 넘기게 됩니다.

 

Poller 는 Socket 들을 캐시로 들고 있다가 해당 Socket 에서 ㅇ에 대한 처리가 가능한 순간에만 스레드를 할당하는 방식을 사용하여 스레드가 idle 상태로 낭비되는 시간을 줄입니다.

Worker

Worker 스레드가 Poller 에 의해 Socket 을 넘겨 받게 되면 Socket 을 SocketProcessor 객체로 캡슐화합니다. 그 후 내부의 Http11NioProcessor 객체를 CoyoteAdapter 를 통해 HttpServletRequest 객체로 변환 후 Servelet 에게 해당 객체를 전달합니다.

오탈자 및 오류 내용을 댓글 또는 메일로 알려주시면, 검토 후 조치하겠습니다.

'Spring > Boot' 카테고리의 다른 글

[Spring] Executable JAR  (0) 2024.04.04