Spring Embedded Tomcat 튜닝

왜 Embedded Tomcat을 튜닝해야 하는가?

Spring Boot는 기본적으로 Embedded Tomcat을 내장하여 별도 WAS 설치 없이 애플리케이션을 실행합니다. 하지만 기본 설정은 개발 편의에 맞춰져 있어, 프로덕션 환경에서는 스레드 풀, 커넥터, 타임아웃, 커넥션 제한 등을 반드시 튜닝해야 합니다. 잘못된 설정은 요청 대기열 폭주, 커넥션 고갈, 느린 응답 등 심각한 성능 문제를 유발합니다.

스레드 풀 설정

Tomcat은 요청마다 스레드를 할당하는 thread-per-request 모델입니다. 스레드 풀 크기가 성능의 핵심입니다.

# application.yml
server:
  tomcat:
    threads:
      max: 200          # 최대 스레드 수 (기본: 200)
      min-spare: 20     # 유휴 최소 스레드 (기본: 10)
    max-connections: 8192  # 최대 동시 커넥션 (기본: 8192)
    accept-count: 100      # 대기열 크기 (기본: 100)
설정 기본값 의미
threads.max 200 동시 처리 가능한 최대 요청 수
threads.min-spare 10 요청 없어도 유지하는 최소 스레드
max-connections 8192 NIO 커넥터의 최대 동시 커넥션
accept-count 100 max-connections 초과 시 OS 레벨 대기열

튜닝 공식: threads.max = (CPU 코어 수) × (1 + I/O 대기 비율). CPU 바운드 앱은 코어 수와 비슷하게, I/O 바운드(DB 호출 많은) 앱은 코어 수의 5~10배로 설정합니다.

# 4코어 서버, I/O 비율 80%인 API 서버
# threads.max = 4 × (1 + 0.8/0.2) = 4 × 5 = 20 ... 이론값
# 실전에서는 부하 테스트로 최적값 찾기 (보통 100~400)

server:
  tomcat:
    threads:
      max: 150
      min-spare: 30
    max-connections: 10000
    accept-count: 200

타임아웃 설정

타임아웃 미설정은 Slow HTTP 공격이나 느린 클라이언트로 인한 스레드 고갈의 원인입니다.

server:
  # 커넥션 타임아웃: 클라이언트 연결 후 첫 요청까지 대기 시간
  connection-timeout: 10s     # 기본: 20s → 10s로 단축

  tomcat:
    # Keep-Alive 타임아웃: 유휴 커넥션 유지 시간
    keep-alive-timeout: 30s   # 기본: connection-timeout과 동일

    # Keep-Alive 최대 요청 수: 하나의 커넥션에서 처리할 최대 요청
    max-keep-alive-requests: 200  # 기본: 100, -1은 무제한

    # 요청 본문 읽기 타임아웃
    connection-timeout: 10000  # ms

Java 코드로 세밀하게 제어:

@Configuration
public class TomcatConfig {

    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory>
            tomcatCustomizer() {

        return factory -> {
            factory.addConnectorCustomizers(connector -> {
                var protocol = (Http11NioProtocol)
                    connector.getProtocolHandler();

                // 스레드 풀
                protocol.setMaxThreads(200);
                protocol.setMinSpareThreads(30);
                protocol.setAcceptCount(200);

                // 커넥션
                protocol.setMaxConnections(10000);
                protocol.setConnectionTimeout(10000);
                protocol.setKeepAliveTimeout(30000);
                protocol.setMaxKeepAliveRequests(200);

                // 요청 크기 제한
                protocol.setMaxHttpHeaderSize(16384);   // 16KB
                protocol.setMaxSwallowSize(2097152);    // 2MB

                // 압축
                connector.setProperty("compression", "on");
                connector.setProperty("compressionMinSize", "1024");
                connector.setProperty("compressibleMimeType",
                    "application/json,text/html,text/css,application/javascript");
            });
        };
    }
}

NIO2 커넥터와 비동기 처리

기본 NIO 대신 NIO2를 사용하면 비동기 I/O 성능이 향상됩니다.

@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory>
        nio2Customizer() {
    return factory -> {
        factory.setProtocol("org.apache.coyote.http11.Http11Nio2Protocol");
    };
}

// 또는 application.yml
server:
  tomcat:
    protocol: org.apache.coyote.http11.Http11Nio2Protocol

NIO2는 OS 레벨의 비동기 I/O(epoll/kqueue)를 직접 활용하여 높은 동시성 환경에서 NIO보다 나은 성능을 보입니다.

Access Log 설정

운영 환경에서 요청 로그는 필수입니다. Tomcat Access Log로 응답 시간, 상태 코드, 클라이언트 정보를 기록합니다.

server:
  tomcat:
    accesslog:
      enabled: true
      directory: /var/log/app
      prefix: access
      suffix: .log
      file-date-format: .yyyy-MM-dd
      # 커스텀 패턴: 클라이언트IP, 요청시간, 메서드, URL, 상태, 응답시간(ms)
      pattern: "%h %t "%r" %s %b %D"
      rotate: true
      max-days: 30
      condition-if: accessLogEnabled  # 조건부 로깅

JSON 포맷으로 구조화 로깅:

server:
  tomcat:
    accesslog:
      enabled: true
      pattern: >-
        {"ip":"%h","time":"%t","method":"%m",
         "uri":"%U","query":"%q","status":%s,
         "size":%b,"duration":%D,"ua":"%{User-Agent}i"}

Graceful Shutdown

Spring Boot 2.3+에서 Graceful Shutdown을 활성화하면, 종료 시 진행 중인 요청이 완료될 때까지 기다립니다.

server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s  # 최대 대기 시간

Graceful Shutdown 동작 순서:

1. SIGTERM 수신
2. 새 요청 거부 (503 반환)
3. 진행 중인 요청 완료 대기 (최대 30초)
4. Keep-Alive 커넥션 정리
5. 스레드 풀 종료
6. ApplicationContext 종료

Actuator로 Tomcat 메트릭 모니터링

# 주요 Tomcat 메트릭 (Micrometer)
tomcat.threads.current        # 현재 스레드 수
tomcat.threads.busy           # 사용 중인 스레드 수
tomcat.threads.config.max     # 최대 스레드 설정값
tomcat.connections.current    # 현재 커넥션 수
tomcat.connections.keepalive  # Keep-Alive 커넥션 수
tomcat.sessions.active        # 활성 세션 수

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    tags:
      application: my-api
// Grafana 대시보드용 PromQL 예시
// 스레드 사용률
tomcat_threads_busy_threads / tomcat_threads_config_max_threads * 100

// 스레드 풀 포화도 알림
// 사용률 80% 초과 시 경고
rate(tomcat_threads_busy_threads[5m]) > 0.8 * tomcat_threads_config_max_threads

환경별 권장 설정

# 소규모 API (2코어, 4GB)
server:
  tomcat:
    threads: { max: 100, min-spare: 10 }
    max-connections: 5000
    accept-count: 100
  connection-timeout: 10s

# 대규모 API (8코어, 16GB)
server:
  tomcat:
    threads: { max: 400, min-spare: 50 }
    max-connections: 20000
    accept-count: 500
  connection-timeout: 5s

# WebSocket 서버 (긴 커넥션)
server:
  tomcat:
    threads: { max: 50, min-spare: 10 }
    max-connections: 50000
    keep-alive-timeout: 120s
    max-keep-alive-requests: -1

관련 글

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux