본문 바로가기
프로그래밍/WAS

단일 WAS 환경에서 WAS별 비교 feat. Tomcat, Undertow, Netty, Jetty

by 더나개 블로그 2025. 8. 26.

문득 궁금해서 해보는 WAS별 비교 🧐

서블릿 환경에서 흔히들 쓰는 tomcat과 reactive 프로그래밍에서 사용하는 netty와 undertow, jetty 등 WAS별 비교해 보자.

 

1. 톰캣(tomcat)

기본적으로 tomcat을 사용할 때 스레드 개수를 설정하지 않으면 http-nio-9194-1@exec ~ http-nio-9194-10@exec까지 10개의 스레드가 대기 상태로 놓이게 된다.

 

그 이유는 내장 톰캣의 최소 쓰레드 수는 10개에서 최대 쓰레드 수는 200개로 10개의 쓰레드가 초기화된 상태로 존재하며 이를 기반으로 요청이 들어오면 기존 스레드를 사용하고, 기존 쓰레드를 초과하면 추가적인 쓰레드를 생성하게 된다.

 

주요 옵션은 다음과 같다.

- minThreads(minimum pool size): 기본값은 `10`. 톰캣이 처음 시작될 때 준비되는 스레드 수.
- maxThreads(maximum pool size): 기본값은 `200`. 요청을 처리하기 위해 생성할 수 있는 최대 스레드 수.
- acceptCount: 기본값은 `100`. 스레드 풀이 가득 찼을 때 대기열에서 허용할 수 있는 최대 요청 수.
- keepAliveTimeout: 스레드가 사용되지 않을 때 얼마나 오래 기다릴지(기본값 20초).

 

springboot 환경에서 다음과 같이 설정할 수 있음

server:
  tomcat:
    threads:
      min: 10   # 최소 스레드 수
      max: 200  # 최대 스레드 수
    accept-count: 100 # 대기열 크기

 

그렇다면 동시 요청이 200개가 초과되는 경우에는 어떻게 될까? 🧐

accept-count는 대기열 크기로 accept-count만큼 대기열에 쌓이게 된다. accpet-count의 default는 100개로 추가 설정이 가능하다.

만약 대기열도 초과하게 된다면, 톰캣이 연결을 거부하거나 요청에 대한 타임아웃이 발생한다.

maxThreads의 설정이 200개이고 accept-count의 설정이 100개일 때, 만약 동시에 300건 이상의 요청이 발생하면 클라이언트는 연결 실패 혹은 503 Service Unavailable 상태 코드를 받게 된다.

물론 하드웨어 스펙이 허락하는 한 maxThreads의 설정이 가능하다.

 

그럼 스레드를 어떻게 설정하는 게 베스트 프랙티스일까? 🧐

기본적으로 쓰레드 수의 튜닝은 하드웨어 스펙(CPU, 메모리)에 따른다. 

minThreads와 maxThreads는 CPU 코어 수 * 2 ~ 4 정도로, CPU 코어 수가 8개라면 16개 ~ 32개로 설정하자.

다만 부하 테스트를 통해 좀 더 정밀한 최적의 값을 찾도록 하는게 좋다.

동시에 대기열 크기를 조정해 가면서 서비스 니즈에 맞도록 지연 시간을 수용할 수 있을 만큼 대기열 크기를 조정하며 설정하도록 하자.

2. undertow

undertow는 논블로킹 I/O 기반으로 요청을 이벤트 루프에서 처리한다.

I/O 스레드는 CPU 코어 수 * 2로 작업 분배 및 연결 처리를 담당한다.

작업 쓰레드는 CPU 코어 수 * 8으로 I/O 스레드에서 넘어온 요청의 처리를 담당한다.

작업 대기열의 크기는 unbounded로 무제한으로 알려져 있다. 즉 메모리 한계까지 요청이 쌓일 수 있으므로 필요에 따라 제한하는 편이 좋다.

 

작업 대기열의 크기는 springboot properties에서 직접적으로 지원하지 않고 undertow는 XNIO 라이브러리를 사용하므로, 워커 스레드의 태스크 대기열 크기를 Options.QUEUE.SIZE로 설정할 수 있다.

다음과 같이 설정할 수 있다.

import io.undertow.UndertowOptions;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.Xnio;

@Configuration
public class UndertowConfig {

    @Bean
    public UndertowServletWebServerFactory undertowFactory() {
        UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();

        factory.setIoThreads(4);
        factory.setWorkerThreads(32);

        OptionMap options = OptionMap.builder()
            .set(Options.WORKER_TASK_CORE_THREADS, 4)
            .set(Options.WORKER_TASK_MAX_THREADS, 32)
            .set(Options.QUEUE_SIZE, 1000)
            .getMap();

        Xnio xnio = Xnio.getInstance();
        factory.setWorker(xnio.createWorker(options));

        return factory;
    }
}

 

그렇다면 tomcat에서 undertow로 WAS를 변경하게 되면 default 스레드의 수가 바뀌는 걸까? 🧐

그렇다.

별도의 설정이 없는 한 서버 사양에 맞춰서 CPU 코어 수 * 8개로 자동 설정된다.

 

springboot 환경에서 다음과 같이 설정할 수 있다.

# I/O 스레드 수
server.undertow.io-threads=4 

# 작업 스레드 수
server.undertow.worker-threads=32

 

그렇다면 undertow는 비동기/논블로킹으로 동작한다고 하였는데 tomcat을 사용하던 동일한 코드에서 undertow로 바꾸게 되면 webflux framework를 사용해서 리액티브 프로그래밍을 한 것과 동일한 효과를 낼 수 있을까? 🧐

그렇지 않다.

undertow가 비동기/논블로킹 방식으로 동작한다고 해서 tomcat에서 undertow로 변경하는 것만으로 WebFlux와 동일한 리액티브 프로그래밍 효과를 얻을 수 있는 것은 아니다.

이유는 undertow의 비동기/논블로킹 특성과 리액티브 프로그래밍이 해결하는 문제의 범위가 다르기 때문인데,

undertow가 해결하는 비동기/논블로킹은 WAS 레벨에서 비동기/논블로킹 IO 기반의 스레드 모델을 사용하기 때문이다.

즉 요청이 들어오고 응답이 처리될 때 이벤트 루프를 사용해서 비동기/논블로킹을 처리하는 것이지, 애플리케이션 레벨(controller, service 등)에서 논블로킹이 이루어지는 게 아니기 때문이다.

WebFlux의 경우에는 Refactor, Mono, Flux 등의 리액티브 스트림을 사용해서 리액티브 패러다임을 구현할 수 있기 때문에 완전한 논블로킹이 가능하다고 하는 것이다.

 

그렇다면 tomcat 또는 undertow를 쓰고 webflux를 쓴다면? 🧐

tomcat을 쓴다면 WAS 레벨에선 블로킹이겠지만 webflux를 통해 논블로킹 애플리케이션 코드를 구현할 수 있겠고, undertow는 전부 논블로킹/비동기로 구현할 수 있다.

결국 리액티브 프로그래밍의 효과를 보려면 애플리케이션 레벨에서 WebFlux와 같은 비동기/논블로킹 IO 기반의 리액티브 스택을 사용해야 한다.

 

그렇다면 어떤 상황에 undertow를 쓰는 게 적합할까? 🧐

다음과 같은 상황을 가정해 보자.

  1. 동시 접속이 200건 이상 꾸준히 유지될 상황
  2. 코드 레벨에서의 변경은 불가하다는 가정
  3. 로드밸런싱도 안 한다는 가정

위의 경우에 tomcat을 undertow로 바꾸게 되면 좀 더 나은 성능을 낼 것으로 보인다.

 

이유는 tomcat과 달리 undertow는 이벤트 루프 기반으로 요청을 처리하기 때문에 컨텍스트 전환을 최소화하여 레이턴시를 줄이고 처리량을 높인다.

tomcat의 경우 동시 요청마다 하나의 스레드가 필요하므로, 200개 이상의 요청이 꾸준히 유지되면 스레드 리소스 소모가 많아지고 컨텍스트 전환 오버헤드가 커질 수 있다.

 

그렇다면 tomcat과 마찬가지로 동시 요청이 200개가 초과되는 경우에는 어떻게 될까? 🧐

CPU 코어 수가 8개인 것을 기준으로 I/O 스레드는 16개, 작업 쓰레드는 64개이다.

요청이 발생하면 I/O 스레드는 논블로킹 방식으로 요청을 처리 대기열에 전달하며 대기열에서 작업 쓰레드가 요청을 처리한다.

즉 64개의 요청을 동시에 처리할 수 있으며, 나머지는 작업 큐에 대기하고 작업 큐가 초과된 경우 연결 실패 혹은 503 Service Unavailable 상태 코드를 받게 된다.

3. jetty

jetty의 경우는 기본적으로 쓰레드 풀에서 스레드를 할당하여 요청을 처리한다. 블로킹 I/O와 비동기 I/O 모두를 지원한다.

최소 스레드(minThreads)는 톰캣과 동일하게 10개, 최대 쓰레드(maxThreads)는 톰캣과 동일하게 200개이다.

Idle 타임아웃은 60초로 쓰레드 풀이 초과되면 요청은 대기하거나 거부한다.

Jetty는 경량화와 비동기 지원에 특화되어 있어 동시성 요청이 많아도 적은 스레드로 안정적인 처리가 가능하다.

4. netty

netty는 완전 비동기/논블로킹 I/O를 지원하며 이벤트 루프 그룹으로 동작한다.

이벤트 루프 그룹엔 boss 그룹과 worker 그룹이 존재하는데, boss 그룹은 쓰레드 1개로 요청이 발생했을 때 연결 수락을 담당한다.

worker 그룹의 스레드는 CPU * 2개로 boss 그룹으로부터 넘겨받은 요청을 처리한다.

별도의 요청 대기열은 없으며 이벤트 루프에서 직접 처리한다.

연결을 수락하는 boss 그룹과 요청 처리를 담당하는 worker 그룹이 분리되어 고성능 처리가 가능하다.

대기열 대신 처리되지 않은 요청은 타임아웃이 발생한다.

댓글