티스토리 뷰

Spring

[SpringBoot] 올바른 스프링 부트 톰캣 애플리케이션 설정 가이드(SpringBoot Tomcat Configuration)

망나니개발자 2025. 4. 15. 10:00
반응형

 

 

 

1. 올바른 스프링 부트 톰캣 애플리케이션 설정 가이드
(SpringBoot Tomcat Configuration) 


[ 다양한 스프링 부트 톰캣 애플리케이션 설정들 ]

이전 포스팅에서 설명하였듯 ThreadPoolExecutor는 자바 버전과 톰캣 버전 두 종류가 존재하는데, 실질적으로 큰 차이는 없다.

톰캣의 ThreadPoolExecutor가 생성되는 코드는 다음과 같은데, 스프링 역시 이에 맞춰 해당 값을 설정할 수 있는 속성들을 제공하고 있다.

 

 

 

스프링 부트에서 조정 가능한 설정값은 크게 4가지가 존재하는데 각각 다음과 같다

  • server.tomcat.max-connections
    • 설명: 서버가 동시에 수락하고 처리할 수 있는 최대 연결의 수로, 이 한도에 도달하면 운영체제가 "acceptCount" 설정에 따라 연결을 계속 수락할 수도 있음
    • 기본값: 8192
  • server.tomcat.accept-count
    • 설명: 모든 요청 처리 스레드가 사용 중일 때, 추가 연결 요청을 대기시킬 수 있는 최대 큐 길이
    • 기본값: 100
  • server.tomcat.threads.min-spare
    • 설명: 항상 유지하려는 최소 작업(워커) 스레드의 수로, 마찬가지로 가상 스레드가 활성화된 경우에는 적용되지 않음
    • 기본값: 10
  • server.tomcat.threads.max
    • 설명: 생성 가능한 최대 작업(워커) 스레드의 수로, 가상 스레드(Virtual Threads)가 활성화되어 있는 경우에는 이 설정이 적용되지 않음
    • 기본값: 200

 

 

 

[ 스프링 부트 톰캣 애플리케이션 설정들이 갖는 의미 ]

모든 값이 1로 설정되어 있는 경우

각각의 설정값이 갖는 의미가 무엇인지 실제 동작을 바탕으로 자세히 살펴보도록 하자.

6명의 사용자가 0.1초 간격으로 요청을 보내는데, 해당 요청을 처리하는 데 100초가 걸리는 상황이라고 가정하자. 이때 다음과 같이 4가지 설정이 모두 1로 되어 있는 상황이라면 어떤 상황에 직면하게 될까?

server.tomcat.max-connections=1
server.tomcat.accept-count=1
server.tomcat.threads.min-spare=1
server.tomcat.threads.max=1

 

 

다음과 같이 컨트롤러를 생성하고, 요청이 들어오는 로그를 확인해볼 수 있을 것이다.

@Slf4j
@RestController
@RequiredArgsConstructor
public class TestController {

    @GetMapping("/test")
    public void test() throws InterruptedException {
        log.info("test");
        Thread.sleep(1000L);
    }
}

 

 

테스트를 해 보면 첫 번째 요청은 서버가 요청을 받아서 처리가 진행되는 중이지만, 나머지 요청들은 모두 대기 상태임을 확인할 수 있다. 그렇다면 각각의 요청들은 구체적으로 어떤 상황이길래 대기 상태에 걸린 것일까?

HTTP 역시 TCP를 기반으로 하기 때문에, 우리가 보내는 API 요청은 SYN-SYN/ACK-ACK 의 3-way handshake 과정으로 연결이 맺어지므로 wireshark를 통해 주고 받은 패킷을 분석해볼 수 있다.

 

 

이를 정리하면 다음과 같은 상황임을 알 수 있다.

  • 첫 번째 요청은 정상적으로 연결이 맺어졌고, 요청이 처리중임
  • 두 번째 요청은 정상적으로 연결이 맺어졌지만, 요청이 처리되지는 않음
  • 나머지 요청들은 모두 서버로부터 SYN/ACK를 전달 받지 못해서 연결 수립에 실패했고, 클라이언트가 재연결(SYN 재전송)을 시도함

 

 

이러한 상황이 발생한 이유는 먼저 max-connections 값이 1로 설정되었기 때문이다. 앞서 설명하였듯 max-connections는 서버가 동시에 수락하고 처리할 수 있는 최대 연결 수이다. 기본적으로 톰캣에서 연결을 수락하는 컴포넌트는 Acceptor 컴포넌트임을 앞선 포스팅에서 살펴보았는데, 해당 컴포넌트의 로직을 살펴보면 다음과 같다.

 

 

 

Acceptor 컴포넌트는 serverSocket.accept()를 통해 요청을 수락한다. 하지만 요청 받은 연결의 수가 max-connections를 초과하려고 하면, Acceptor 컴포넌트는 대기 상태가 되어 serverSocket.accept()를 수행할 수 없다. 스레드 덤프를 통해 현재 Acceptor의 상태를 확인해보면 다음과 같이 countUpOrAwaitConnection 부분에서 블로킹중임을 확인할 수 있다.

"http-nio-8080-Acceptor" #37 daemon prio=5 os_prio=31 cpu=8.08ms elapsed=10.79s tid=0x0000000134ca2000 nid=0x9703 waiting on condition  [0x0000000313532000]
   java.lang.Thread.State: WAITING (parking)
	at jdk.internal.misc.Unsafe.park(java.base@17.0.12/Native Method)
	- parking to wait for  <0x0000000401182678> (a org.apache.tomcat.util.threads.LimitLatch$Sync)
	at java.util.concurrent.locks.LockSupport.park(java.base@17.0.12/LockSupport.java:211)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(java.base@17.0.12/AbstractQueuedSynchronizer.java:715)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(java.base@17.0.12/AbstractQueuedSynchronizer.java:1047)
	at org.apache.tomcat.util.threads.LimitLatch.countUpOrAwait(LimitLatch.java:117)
	at org.apache.tomcat.util.net.AbstractEndpoint.countUpOrAwaitConnection(AbstractEndpoint.java:1461)
	at org.apache.tomcat.util.net.Acceptor.run(Acceptor.java:116)
	at java.lang.Thread.run(java.base@17.0.12/Thread.java:840)

 

 

그렇다면 두 번째 요청의 연결이 맺어진 이유는 무엇일까? 그 이유는 accept-count 변수에 있다. 앞선 포스팅에서 살펴보았듯 네트워크 연결은 기본적으로 운영체제 계층에서 수행되며, 수립된 요청들은 운영체제의 백로그 큐(backlog queue)에 저장된다.

 

 

그리고 serverSocket.accept()를 하면 백로그 큐에 쌓인 연결 정보를 바탕으로 소켓을 반환받는데, 이때 백로그 큐에 쌓아둘 연결의 수를 관리하는 변수가 바로 accept-count 이다. 백로그 큐에 쌓인 연결의 수가 accept-count를 초과하면 운영체제는 추가적인 연결을 맺지 않는데, accept-count 변수는 NioEndpoint 라는 톰캣의 연결 구현체에서 ServerSocketChannel.bind()의 파라미터로 전달되고 있음을 확인할 수 있다.

 

 

현재 설정을 기준으로 accept-count는 1이기 때문에 두 번째 요청까지는 연결에 성공하고, 이후의 요청들은 연결에 실패한 것이다. 따라서 현재 상황을 보다 구체적으로 정리하면 다음과 같다.

  • 첫 번째 요청은 정상적으로 연결이 맺어졌고, 요청이 처리중임
  • 두 번째 요청은 정상적으로 연결이 맺어졌지만, max-connections에 도달하여 Acceptor 컴포넌트가 꺼내지 못하고 운영체제의 백로그 큐에 쌓여 있음
  • 나머지 요청들은 운영 체제의 백로그 큐의 수가 accept-count를 초과하여 서버로부터 SYN/ACK를 전달 받지 못하고 연결 수립에 실패했고, 클라이언트가 재연결(SYN 재전송)을 시도함

 

 

따라서 클라이언트가 서버와 추가적인 TCP 연결을 수립하지 못하는 상황이라면, accept-count를 늘려서 연결을 더 수립시키고 운영 체제의 백로그 큐에 연결을 저장해둘 수 있다.

 

 

 

accept-count를 2로 늘린 경우

위와 같은 상황임을 인지하고 accept-count를 2로 늘렸다고 가정해보자.

server.tomcat.max-connections=1
server.tomcat.accept-count=2
server.tomcat.threads.min-spare=1
server.tomcat.threads.max=1

 

 

이제는 다음과 같은 상황이 되었음을 알 수 있다.

  • 첫 번째 요청은 정상적으로 연결이 맺어졌고, 요청이 처리중임
  • 두 번째, 세 번째 요청은 정상적으로 연결이 맺어졌지만, max-connections에 도달하여 Acceptor 컴포넌트가 꺼내지 못하고 운영체제의 백로그 큐에 쌓여 있음
  • 나머지 요청들은 운영 체제의 백로그 큐의 수가 accept-count를 초과하여 서버로부터 SYN/ACK를 전달 받지 못하고 연결 수립에 실패했고, 클라이언트가 재연결(SYN 재전송)을 시도함

 

 

실제로 wireshark를 통해 연결이 맺어진 정보를 보면 총 3개 맺어졌음을 확인할 수 있다.

 

 

즉, 클라이언트의 연결을 대기시킬 수 있는 큐의 길이를 조정하는 것이 accept-count 변수인 것이다. 하지만 일반적인 애플리케이션에서는 요청을 대기시키기 보다는 동시 처리 성능을 보다 높이는 것이 바람직하다. 따라서 max-connections를 조정해보도록 하자.

 

 

 

max-connections를 4로 늘린 경우

현재 애플리케이션의 동시성은 1에 불과하다. 여러 가지 이유가 있지만, 당장은 max-connections이 1로 설정되어 있는 부분에서 Acceptor 컴포넌트가 추가적인 연결을 백로그 큐에서 가져오지 못하기 때문이다. 따라서 이번에는 max-connections를 4로 설정하고 상황을 분석해보도록 하자.

server.tomcat.max-connections=4
server.tomcat.accept-count=2
server.tomcat.threads.min-spare=1
server.tomcat.threads.max=1

 

 

이제는 모든 요청들이 연결이 맺어진 상황이 되었는데, 현재 상황을 정리하면 다음과 같다.

  • 첫 번째 요청은 정상적으로 연결이 맺어졌고, 요청이 처리중임
  • 두 번째 ~ 네 번째 요청은 정상적으로 연결이 맺어졌고, Acceptor 컴포넌트를 통해 연결을 가져왔지만, 처리 가능한 작업 스레드가 없어서 대기중임
  • 나머지 요청들은 정상적으로 연결이 맺어졌지만, max-connections에 도달하여 Acceptor 컴포넌트가 꺼내지 못하고 운영체제의 백로그 큐에 쌓여 있음

 

 

모든 요청들이 연결 맺어진 상황임에도 불구하고 테스트를 해보면 현재 애플리케이션의 동시성은 1에 그친다는 것을 알 수 있다. 그 이유는 현재 가용 가능한 스레드가 1로 제한되어 있기 때문이다. 따라서 이제 스레드 관련 설정을 변경할 차례이다.

 

 

 

threads.min-spare와 threads.max를 2로 늘린 경우

threads.min-spare는 보장되는 최소 작업 스레드의 수에 해당하므로, 이를 통해 최소 동시성을 조정할 수 있다. 따라서 threads.min-spare를 2로 높여줘 보도록 하자. threads.max는 생성 가능한 최대 작업 스레드의 수에 해당하므로 반드시 threads.min-spare보다 크거나 같아야 한다. 따라서 threads.max도 2로 함께 높여주도록 하자.

server.tomcat.max-connections=4
server.tomcat.accept-count=2
server.tomcat.threads.min-spare=2
server.tomcat.threads.max=2

 

 

이제는 동시성이 2로 확보되어 다음과 같은 상황이 되었음을 확인할 수 있다.

  • 첫 번째, 두 번째 요청은 정상적으로 연결이 맺어졌고, 요청이 처리중임
  • 세 번째 ~ 네 번째 요청 역시 정상적으로 연결이 맺어졌고, Acceptor 컴포넌트를 통해 연결을 가져왔지만, 처리 가능한 작업 스레드가 없어서 대기중임
  • 나머지 요청들은 정상적으로 연결이 맺어졌지만, max-connections에 도달하여 Acceptor 컴포넌트가 꺼내지 못하고 운영체제의 백로그 큐에 쌓여 있음

 

 

현재 서비스의 평균 TPS는 2에 불과하지만 가끔씩 4까지 높아지는 경우가 있다고 하자. threads.min-spare는 보장되는 최소 작업 스레드의 수이기 때문에 해당 값을 4로 높이면 시스템 리소스가 낭비되는 경우가 생길 수 있다. 따라서 이러한 경우에는 threads.max를 조정하는 것이 바람직하다.

 

 

 

threads.max를 4로 늘린 경우

톰캣의 작업 스레드 풀에는 기본적으로 threads.min-spare 만큼의 스레드(기본 스레드)가 존재하는데, max-connections의 기본값은 8192로 통상적으로 threads.min-spare보다 큰 값을 갖는 경우가 대부분이다. 즉, 맺어질 수 있는 연결에 비해 처리 가능한 스레드는 적은 값을 갖는데, 이러한 상황을 보완하기 위해 threads.max가 존재한다.

threads.max는 생성 가능한 최대 작업 스레드의 수에 해당하는데, 기본 스레드가 모두가 사용중임에도 불구하고 연결이 맺어져서 Acceptor 컴포넌트를 통해 백로그 큐에서 꺼내진 요청들이 존재하는 경우에 톰캣은 threads.max 만큼의 초과 스레드를 생성하여 요청을 처리하도록 한다. 이렇게 생성된 초과 스레드는 일정 시간이 지나도 계속해서 활용되지 않으면 소멸되고, 스레드의 수는 최종적으로 threads.min-spare으로 유지된다. 만약 여기서 기본 스레드와 초과 스레드 그리고 스레드 풀에 대한 내용이 이해가 가지 않는다면 이전 포스팅을 참고하도록 하자.

 

이번에는 threads.min-spare는 2로 고정된 상태에서 threads.max를 4로 변경해보도록 하자.

server.tomcat.max-connections=4
server.tomcat.accept-count=2
server.tomcat.threads.min-spare=2
server.tomcat.threads.max=4

 

 

이렇게 변경된 설정을 적용하면 요청 처리는 최종적으로 다음과 같은 상황이 된다.

  • 첫 번째, 두 번째 요청은 정상적으로 연결이 맺어졌고, 기본 스레드를 통해 요청이 처리중임
  • 세 번째 ~ 네 번째 요청 역시 정상적으로 연결이 맺어졌고, 초과 스레드를 통해 요청이 처리중임
  • 다섯 번째 ~ 여섯 번째 요청은 대기하다가, 임의의 두 요청이 처리 완료되면 반환된 스레드로 요청을 처리함
  • 여섯 번째 요청이 완료되고, 초과 스레드들이 일정 시간 사용되지 않으면 소멸되어 스레드 풀에는 기본 스레드만 남게 됨

 

 

 

max-connections를 8192로 늘린 경우

마지막으로 max-connections를 8192로 늘린 경우를 살펴보도록 하자.

server.tomcat.max-connections=8192
server.tomcat.accept-count=2
server.tomcat.threads.min-spare=2
server.tomcat.threads.max=4

 

 

앞선 포스팅에서 살펴보았듯 톰캣 애플리케이션의 ThreadPoolExecutor 동작을 정리하면 다음과 같다.

  1. 작업 요청이 들어오면, core 사이즈 만큼 스레드를 생성함
  2. core 사이즈를 초과하면 max 사이즈 만큼 스레드(초과 스레드)를 만들어 실행시키는데, 이는 설정된 시간을 넘어서도 활용되지 않으면 소멸됨
  3. 스레드의 수가 max 사이즈를 큐에 작업을 넣음
  4. 큐가 꽉 차면 정책에 따라 처리함(기본적으로 예외를 발생시킴)

 

 

ThreadPoolExecutor가 작업을 보관하는 큐의 구현에 따라, 작업 큐가 꽉 차고 나면 max 만큼의 초과 스레드를 생성하거나 혹은 max 만큼의 초과 스레드가 생성된 후에 작업 큐를 채우는 방식으로 동작이 달라질 수 있는데, 톰캣은 자체적인 TaskQueue를 구현하여 사용하기에 처리 방식이 위와 같음을 이전 포스팅에서 살펴보았다.

따라서 이러한 정보를 바탕으로 현재의 상황을 정리하면 다음과 같다고 볼 수 있다.

  • 첫 번째 ~ 두 번째 두 번째 요청은 정상적으로 연결이 맺어졌고, 기본 스레드를 통해 요청이 처리중임
  • 세 번째 ~ 네 번째 요청 역시 정상적으로 연결이 맺어졌고, 초과 스레드를 통해 요청이 처리중임
  • 다섯 번째 요청이 들어온다면 연결이 정상적으로 맺어지고 Acceptor 컴포넌트를 통해 백로그 큐에서 꺼내질 것이지만, 초과 스레드까지 모두 생성된 상태이므로 ThreadPoolExecutor 내부의 작업 큐에서 요청이 보관 및 대기됨

 

 

이제 각각의 설정이 갖는 의미에 대해 분석을 완료했는데, 각각을 그렇다면 어떻게 설정하면 좋을지 살펴보도록 하자.

참고로 초과 스레드가 생성되어 활용되는 경우에는 ThreadPoolExecutor 구현에 따라 WAITING 상태의 스레드가 줄어들고, TIMED-WAITING 상태의 스레드가 증가함을 확인할 수 있음을 참고해두도록 하자. 관련 내용은 해당 포스팅을 통해 자세히 참고하도록 하자.

 

 

 

 

[ 올바른 스프링 부트 톰캣 애플리케이션 설정 가이드 ]

먼저 max-connections는 스프링 부트에서 기본값 8192로 되어 있으며, 이는 충분히 넉넉하게 설정 가능한 클라이언트 연결의 수에 해당한다. 따라서 해당 값을 특별히 조정할 필요는 거의 없을 것이다.

accept-count는 백로그 큐에 쌓아둘 요청의 크기에 해당하는데, 스프링 부트의 기본값은 100으로 되어 있다. 서비스 상황에 따라 다르겠지만, 한 번 거절된 요청은 다시 맺어지기 어렵기 때문에 해당 값을 보다 넉넉히 조정하여 요청을 잠깐 대기시키고, 현재 처리중인 요청이 완료되면 이어서 처리해주는 것이 좋다. 따라서 시스템의 리소스와 서비스 상황 등을 고려하여 해당 값을 1024 내지는 4096 정도로 늘려주는 것도 고려할법하다.

마지막으로 스레드 관련 설정 threads.min-spare와 threads.max 관련 부분인데, 대부분의 서비스에서는 동시 처리되는 요청이 100을 넘는 경우가 많지 않다. 따라서 threads.min-spare를 100 정도로 설정해두되, 특정 시간에 급증하는 트래픽에도 유연하게 대응할 수 있도록 threads.max를 200 정도로 설정해두면 적당할 것이다. 그러다가 동시 요청의 수가 200으로도 감당하기 어려운 경우에는 threads.min-spare과 threads.max를 각각 2배씩 설정해주는 방식으로 커스터마이징해주면 적당할 것이다.

  • server.tomcat.max-connections=8192 (기본값)
  • server.tomcat.accept-count=1024
  • server.tomcat.threads.min-spare=100
  • server.tomcat.threads.max=200

 

 

물론 위의 내용은 서비스 특성과 하드웨어 스펙 및 배포 환경 등 다양한 고려 요인을 생략하고 통상적인 환경을 기준으로 지나치게 일반화한 값으로 볼 수 있다. 따라서 본인의 서비스 특성에 맞게 반드시 성능 테스트 등을 거쳐서 올바르게 조정해주도록 하자.

 

 

 

관련 포스팅

  1. 멀티 스레드 기반으로 다중 요청을 처리하는 톰캣(Tomcat)의 구조와 동작 방식
  2. 올바른 스프링 부트 톰캣 애플리케이션 설정 가이드(SpringBoot Tomcat Configuration)
  3. 스프링 부트 톰캣 애플리케이션의 Graceful Shutdown 동작 방식(Spring Boot Tomcat Graceful Shutdown)</a
  4. 요청량이 급증하여 톰캣의 초과 스레드가 활용될 때, TIMED_WAITING 상태의 스레드가 급증하는 이유

 

 

 

 

참고 자료

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함