스레드와 스레드 풀

동동주·2025년 12월 4일

jscode 자바 스터디

목록 보기
3/5

✔ Java에서 스레드를 만드는 방법, 스레드 풀 개념, 그리고 스프링이 스레드 풀을 많이 사용하는 이유

멀티쓰레딩은 현대 애플리케이션에서 필수적인 개념이며, 특히 웹 서버·백엔드 개발에서는 요청을 병렬로 처리하기 위해 스레드 활용이 매우 중요합니다. 아래에서 Java의 스레드 생성 방식부터 스레드 풀과 스프링이 대규모 스레드 풀을 사용하는 이유까지 정리해보겠습니다.


✅ 1. Java에서 스레드를 만드는 방법

Java에서 스레드를 만드는 방법은 크게 세 가지입니다.

1) Thread 클래스를 상속

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running");
    }
}

new MyThread().start();

특징

  • 간단하지만, 이미 다른 클래스를 상속받고 있다면 사용 불가(단일 상속 제한)
  • 재사용성이 낮음

2) Runnable 인터페이스 구현 (가장 일반적인 방식)

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Task running");
    }
}

new Thread(new MyTask()).start();

특징

  • 스레드 실행 로직(run)을 캡슐화
  • Thread와 작업(task)을 분리하여 더 유연하게 사용 가능
  • ThreadPoolExecutor 같은 스레드 풀에서 사용되는 기본 구조

3) Callable + Future (값 반환 + 예외 처리 가능)

Callable<Integer> task = () -> {
    return 42;
};

Future<Integer> result = Executors.newSingleThreadExecutor().submit(task);
System.out.println(result.get());

특징

  • Runnable과 달리 결과 반환예외 전파가 가능
  • 비동기 작업에서 자주 사용

✔ 정리하면

방식장점단점
Thread 상속가장 단순재사용성 낮음, 단일상속 제한
Runnable유연함, 스레드 풀에서 사용결과 반환 불가
Callable결과 반환 가능, 예외 처리구현 복잡도 ↑

✅ 2. 스레드 풀(Thread Pool)이란 무엇인가?

스레드 풀은 미리 여러 개의 스레드를 만들어 놓고 필요할 때 가져다 쓰는 구조입니다.

Java에서는 ExecutorService, ThreadPoolExecutor로 구현되어 있습니다.

장점

1) 스레드 생성 비용 절감

  • 스레드를 새로 만드는 비용은 생각보다 매우 무겁다

    • OS 레벨에서 메모리 할당(Stack)
    • 커널에 스레드 등록
    • 스케줄러 관리 대상 추가
      → 요청마다 새로 스레드를 만드는 것은 비효율적

2) 과도한 스레드 생성 방지

  • 스레드를 계속 만들면?

    • 메모리 초과
    • CPU 문맥 교환(Context Switching) 폭증
    • 성능 급락

스레드 풀은 동시에 실행 가능한 스레드 개수를 제한하여 이를 제어한다.

3) 안정적인 시스템 운영

  • 요청이 몰려도 스레드 수가 일정하게 유지되므로 시스템 과부하 방지
  • Queue에 쌓아두고 순차적으로 처리 가능

✔ 스레드 풀의 종류 (Executors)

  • newFixedThreadPool(n) : 고정 개수 스레드
  • newCachedThreadPool() : 필요할 때 늘렸다가 비면 제거
  • newSingleThreadExecutor() : 1개 스레드
  • ThreadPoolExecutor : 직접 정책 설정(실무에서 가장 많이 사용)

✅ 3. 스프링(서버 프레임워크)은 왜 스레드 풀을 수백 개 이상으로 설정할까?

스프링 기반 웹 서버(Tomcat, Netty 등)는 보통 200~300개 이상의 스레드 풀을 사용합니다.
이는 문맥 교환(Context Switching)이 발생함에도 불구하고 선택하는 구조인데, 이유는 다음과 같습니다.

✔ 이유 1. 서버의 대기 시간이 CPU를 거의 사용하지 않기 때문

웹 서버의 대부분 시간은 I/O 대기 시간입니다.

  • DB 응답 기다림
  • Redis 대기
  • 외부 API 응답 대기
  • 파일/네트워크 I/O 대기

➡ CPU를 쓰는 시간이 매우 적다.

즉, 스레드는 일하는 시간이 짧고 기다리는 시간이 길다.

따라서 스레드가 많다고 해서 CPU를 과도하게 사용하지 않는다.

✔ 이유 2. I/O Bound 작업은 많은 스레드를 둘수록 처리량(Throughput)이 증가됨

CPU Bound 작업이면 스레드가 많으면 오히려 느려지지만,
웹 서버는 대부분 I/O Bound.

즉, 스레드 300개가 모두 일을 하는 것이 아니라

  • 대부분 대기 상태(Waiting)
  • 일부만 CPU를 점유

그래서 문맥 교환 비용보다
동시성(concurrency) 증가로 얻는 이익이 훨씬 크다.

✔ 이유 3. 웹 요청 자체가 동시성이 매우 높음

하나의 웹 서버에서 수백 개~수천 개의 요청이 동시에 들어온다.

만약 스레드가 50개뿐이라면?

  • 나머지 요청은 전부 대기
  • 응답 지연 증가
  • 서버 처리량 감소

➡ 실무에서는 최소 200~300개 스레드를 둠

✔ 이유 4. 스프링 MVC는 요청당 스레드 1개가 필요 (Tomcat worker thread)

스프링 MVC는 Blocking I/O 모델이다.

  • 요청 1개 → 스레드 1개 고정 점유
  • DB 응답 기다리는 동안에도 스레드는 점유됨

따라서 처리량을 확보하려면 스레드 수를 많이 둘 수밖에 없다.

스프링 WebFlux(Non-blocking)는 스레드 수가 매우 적게 필요하지만
전통적인 MVC는 request-per-thread 구조라 스레드 풀을 크게 잡아야 한다.


✔ 정리: 문맥 교환 비용보다 더 큰 이득이 있기 때문

Context Switching 비용은 분명 존재한다.
하지만 서버 환경에서는 다음이 더 중요하다.

  • I/O 대기 시간이 훨씬 크다
  • 동시 접속 요청이 많다
  • 스레드가 대부분 Waiting 상태다
  • 스레드가 부족하면 처리량이 급격히 떨어진다

그래서 스레드 수백 개 운영이 훨씬 효율적


📌 정리

Java에서 스레드는 Thread 상속, Runnable 구현, Callable을 통해 생성할 수 있다. 하지만 요청마다 스레드를 새로 만드는 것은 비용이 커서, 보통 스레드 풀(Thread Pool)로 스레드를 재사용한다. 스프링을 포함한 대부분의 서버 프레임워크는 수백 개 이상의 스레드를 운영하는데, 이는 웹 서버의 대부분 로직이 CPU 작업이 아닌 DB·외부 API·네트워크 I/O 대기로 이루어져 있기 때문이다. 즉, 스레드는 CPU를 거의 사용하지 않고 대부분 대기 상태라 많은 스레드를 두어도 오버헤드보다 동시 처리량 증가의 이점이 훨씬 크다. 요청당 스레드를 하나씩 점유하는 스프링 MVC 구조에서는 특히 많은 스레드를 둘수록 안정적인 처리량을 확보할 수 있다.

0개의 댓글