멀티쓰레딩은 현대 애플리케이션에서 필수적인 개념이며, 특히 웹 서버·백엔드 개발에서는 요청을 병렬로 처리하기 위해 스레드 활용이 매우 중요합니다. 아래에서 Java의 스레드 생성 방식부터 스레드 풀과 스프링이 대규모 스레드 풀을 사용하는 이유까지 정리해보겠습니다.
Java에서 스레드를 만드는 방법은 크게 세 가지입니다.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running");
}
}
new MyThread().start();
특징
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("Task running");
}
}
new Thread(new MyTask()).start();
특징
Callable<Integer> task = () -> {
return 42;
};
Future<Integer> result = Executors.newSingleThreadExecutor().submit(task);
System.out.println(result.get());
특징
Runnable과 달리 결과 반환 및 예외 전파가 가능| 방식 | 장점 | 단점 |
|---|---|---|
| Thread 상속 | 가장 단순 | 재사용성 낮음, 단일상속 제한 |
| Runnable | 유연함, 스레드 풀에서 사용 | 결과 반환 불가 |
| Callable | 결과 반환 가능, 예외 처리 | 구현 복잡도 ↑ |
스레드 풀은 미리 여러 개의 스레드를 만들어 놓고 필요할 때 가져다 쓰는 구조입니다.
Java에서는 ExecutorService, ThreadPoolExecutor로 구현되어 있습니다.
스레드를 새로 만드는 비용은 생각보다 매우 무겁다
스레드를 계속 만들면?
스레드 풀은 동시에 실행 가능한 스레드 개수를 제한하여 이를 제어한다.
newFixedThreadPool(n) : 고정 개수 스레드newCachedThreadPool() : 필요할 때 늘렸다가 비면 제거newSingleThreadExecutor() : 1개 스레드ThreadPoolExecutor : 직접 정책 설정(실무에서 가장 많이 사용)스프링 기반 웹 서버(Tomcat, Netty 등)는 보통 200~300개 이상의 스레드 풀을 사용합니다.
이는 문맥 교환(Context Switching)이 발생함에도 불구하고 선택하는 구조인데, 이유는 다음과 같습니다.
웹 서버의 대부분 시간은 I/O 대기 시간입니다.
➡ CPU를 쓰는 시간이 매우 적다.
즉, 스레드는 일하는 시간이 짧고 기다리는 시간이 길다.
따라서 스레드가 많다고 해서 CPU를 과도하게 사용하지 않는다.
CPU Bound 작업이면 스레드가 많으면 오히려 느려지지만,
웹 서버는 대부분 I/O Bound.
즉, 스레드 300개가 모두 일을 하는 것이 아니라
그래서 문맥 교환 비용보다
동시성(concurrency) 증가로 얻는 이익이 훨씬 크다.
하나의 웹 서버에서 수백 개~수천 개의 요청이 동시에 들어온다.
만약 스레드가 50개뿐이라면?
➡ 실무에서는 최소 200~300개 스레드를 둠
스프링 MVC는 Blocking I/O 모델이다.
따라서 처리량을 확보하려면 스레드 수를 많이 둘 수밖에 없다.
스프링 WebFlux(Non-blocking)는 스레드 수가 매우 적게 필요하지만
전통적인 MVC는 request-per-thread 구조라 스레드 풀을 크게 잡아야 한다.
Context Switching 비용은 분명 존재한다.
하지만 서버 환경에서는 다음이 더 중요하다.
➡ 그래서 스레드 수백 개 운영이 훨씬 효율적
Java에서 스레드는 Thread 상속, Runnable 구현, Callable을 통해 생성할 수 있다. 하지만 요청마다 스레드를 새로 만드는 것은 비용이 커서, 보통 스레드 풀(Thread Pool)로 스레드를 재사용한다. 스프링을 포함한 대부분의 서버 프레임워크는 수백 개 이상의 스레드를 운영하는데, 이는 웹 서버의 대부분 로직이 CPU 작업이 아닌 DB·외부 API·네트워크 I/O 대기로 이루어져 있기 때문이다. 즉, 스레드는 CPU를 거의 사용하지 않고 대부분 대기 상태라 많은 스레드를 두어도 오버헤드보다 동시 처리량 증가의 이점이 훨씬 크다. 요청당 스레드를 하나씩 점유하는 스프링 MVC 구조에서는 특히 많은 스레드를 둘수록 안정적인 처리량을 확보할 수 있다.