서버가 느리면 스레드를 늘리라고요?
입사 초기, 내가 맡았던 스프링 부트(Spring Boot) API 서버가 트래픽이 몰릴 때마다 느려지는 현상이 있었다. 원인을 모르니 일단 구글링부터 시작했다.
“Spring Boot server slow response” “Tomcat performance tuning”
검색 결과, 블로그나 커뮤니티에서 가장 많이 보이는 조언은 단순했다. ‘톰캣(Tomcat)의 스레드 풀(Thread Pool) 사이즈를 늘리세요. 일꾼이 부족해서 요청이 대기 중인 겁니다.’
나는 “아하, 일꾼이 부족하구나!” 하고 단순하게 생각했다. 바로 application.yml 설정을 열어 기본값 200개였던 스레드를 2,000개로 확 늘려버렸다. 내 계산대로라면 일꾼이 10배 늘어났으니 처리 속도도 빨라져야 했다.
하지만 배포 후 모니터링 화면을 본 나는 얼어붙었다. 서버는 오히려 더 굼뜨게 움직였고, CPU 사용률은 치솟는데 정작 처리되는 요청 수는 줄어들었다. 마치 작업자들이 일을 안 하고 허공에 삽질만 하고 있는 것 같았다.
도대체 왜 일꾼을 늘렸는데 공장은 더 느려진 걸까? 그 이유를 파헤치다가 나는 운영체제의 가장 비싼 비용, ‘컨텍스트 스위칭(Context Switching)’을 마주하게 되었다.

복습: 스레드, 기억하시나요?
이 글을 처음 접하는 분들을 위해, 혹은 지난 글(4편: 프로세스와 스레드)의 내용이 가물가물한 분들을 위해 잠시 기억을 되감아보자.
우리의 ‘디지털 물류 센터’ 세계관에서:
- 프로세스: 공장 건물 그 자체. (독립된 공간)
- 스레드: 공장 안에서 실제로 일을 하는 작업자(일꾼).
스프링 부트는 기본적으로 멀티 스레드 방식이다. 요청(Request)이 들어올 때마다 일꾼(스레드)을 하나씩 배정해서 일을 시킨다. 그래서 나는 단순하게 “일꾼이 많으면 더 많은 요청을 동시에 처리하겠지?”라고 생각했던 것이다.
하지만 내가 간과한 것이 있었다. 우리 공장의 핵심 작업자, CPU 코어의 개수다.
디지털 물류 센터의 교대 근무
사실 컴퓨터의 핵심 작업자인 CPU 코어는 한 번에 딱 하나의 일만 할 수 있다. (싱글 코어 기준) 그런데 우리는 노래도 듣고, 코딩도 하고, 카톡도 동시에 한다. 어떻게 가능한 걸까?
공장장(OS)이 작업자(CPU)에게 엄청나게 빠른 속도로 ‘작업 교대’를 시키기 때문이다. “0.001초 동안 노래 재생하고, 멈춰! 다음 0.001초 동안 카톡 전송하고, 멈춰!”
이것이 바로 ‘시분할(Time Sharing)’이고, 이 과정에서 작업자가 도구를 내려놓고 새로운 도구를 집어 드는 과정이 ‘컨텍스트 스위칭’이다.
작업 전환의 대가: 옷 갈아입는 시간
내가 스레드를 2,000개로 늘렸을 때 벌어진 일은 이런 것이다.
CPU는 몸이 하나인데, 2,000명의 스레드 일꾼들이 “제 거 처리해 주세요!”라고 아우성친다. CPU는 공평하게 일을 처리하기 위해 2,000명을 쉴 새 없이 번갈아 가며 만난다.
문제는 A 일꾼의 일을 하다가 B 일꾼의 일로 넘어갈 때 ‘준비 시간’이 필요하다는 점이다.
- A가 하던 작업 내용(State)을 장부에 기록한다.
- 작업대(Register)를 싹 치운다.
- B가 지난번에 어디까지 했는지 장부를 읽어온다(Load).
- B 작업을 시작한다.
이 ‘기록하고 치우고 읽어오는 시간’을 ‘컨텍스트 스위칭 비용(Overhead)’이라고 한다. 일꾼이 적당할 땐 이 비용이 무시할 만하다. 하지만 일꾼이 너무 많아지면? CPU는 하루 종일 작업자 장부만 정리하다가 정작 ‘실제 일(계산)’은 하나도 못 하는 상황에 빠진다. 이게 내 서버가 느려진 진짜 이유였다.

[Code Verification] 스레드가 많다고 무조건 빠를까?
백문이 불여일견이다. 코드로 증명해보자. 똑같은 양의 덧셈 작업을 수행하는데, 스레드 1개로 할 때와 스레드 100만 개로 쪼개서 할 때의 속도를 비교해보자. 상식적으로는 100만 개가 빨라야 할 것 같지만, 현실은 다르다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ContextSwitchingTest {
private static final int TASK_COUNT = 1_000_000;
public static void main(String[] args) throws InterruptedException {
// 1. 싱글 스레드로 처리 (교대 근무 없음)
long start = System.currentTimeMillis();
for (int i = 0; i < TASK_COUNT; i++) {
simpleTask();
}
System.out.println("싱글 스레드 소요 시간: " + (System.currentTimeMillis() - start) + "ms");
// 2. 엄청나게 많은 스레드로 처리 (컨텍스트 스위칭 유발)
// 스레드 풀을 무제한으로 생성 (주의: 컴퓨터 멈출 수 있음)
ExecutorService executor = Executors.newCachedThreadPool();
start = System.currentTimeMillis();
for (int i = 0; i < TASK_COUNT; i++) {
executor.submit(() -> simpleTask());
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.HOURS);
System.out.println("다중 스레드 소요 시간: " + (System.currentTimeMillis() - start) + "ms");
}
private static void simpleTask() {
int a = 1 + 1; // 아주 가벼운 작업
}
}
결과 예시 (환경마다 다름):
- 싱글 스레드: 약 5ms
- 다중 스레드: 약 3,000ms (혹은 OOM 발생)
분석: 작업 자체(1+1)는 너무 간단해서 눈 깜짝할 새면 끝난다. 하지만 다중 스레드 방식에서는 100만 개의 스레드를 만들고, 운영체제가 이들 사이를 왔다 갔다 하는(스위칭) 비용이 작업 시간보다 수천 배 더 들어간 것이다. 배보다 배꼽이 더 큰 상황이다.
실무에서의 교훈: 적정선 찾기
그렇다면 스프링 부트 서버의 스레드는 몇 개가 적당할까? 정답은 “서버가 무슨 일을 하느냐”에 따라 다르다.
- CPU를 많이 쓰는 작업 (동영상 인코딩, 암호화):
- 스레드를 많이 늘려봤자 소용없다. 어차피 CPU 코어 개수만큼만 동시에 돌 수 있다.
- 적정 개수: CPU 코어 수 + 1
- 대기 시간이 긴 작업 (DB 조회, 외부 API 호출):
- 대부분의 웹 서버가 여기에 해당한다. 스레드가 DB 응답을 기다리는 동안(Blocking) 다른 스레드가 CPU를 쓰면 된다.
- 적정 개수: CPU 코어 수보다 훨씬 많아도 된다. (톰캣 기본값이 200인 이유)
하지만 내가 겪었던 상황처럼 2,000개로 무작정 늘리는 건 과유불급이다. 스레드가 늘어날수록 메모리(Stack)도 많이 잡아먹고, 컨텍스트 스위칭 비용 때문에 CPU가 과부하 걸리기 때문이다.
최근에는 이 문제를 해결하기 위해 Node.js나 스프링의 WebFlux 같은 비동기(Non-blocking) 기술들이 주목받고 있다. 얘네들은 “스레드를 늘리지 말고, 한 명이 쉬지 않고 빠르게 처리하자”는 전략을 쓴다.

마치며: 공짜 점심은 없다
우리는 흔히 “동시에 처리하면 빠르다”라고 착각한다. 하지만 컴퓨터의 세계에서 ‘동시’란, 사실 눈속임에 가까운 초고속 교대 근무일 뿐이다.
컨텍스트 스위칭을 이해하고 나면, 왜 서버 튜닝이 단순히 “숫자 늘리기”가 아닌지 알게 된다. 무턱대고 늘린 스레드는 오히려 서버의 숨통을 조이는 독이 될 수 있다.
자, 이제 컴퓨터 내부(CPU, RAM, Process) 공장은 꽤 잘 돌아가는 것 같다. 그렇다면 이제 공장 문을 열고 밖으로 나가보자. 우리 공장에서 만든 데이터를 저 멀리 다른 공장(고객)에게 보내려면 어떻게 해야 할까?
다음 시간에는 네트워크와 HTTP, 그 보이지 않는 도로망에 대해 이야기해 보겠다.
네트워크와 HTTP 글은 언제 올라오나요?
업로드 되었습니다. 시리즈를 기대해주셔서 감사합니다. ^^