스레드 풀(Thread Pool)이란?
자바의 Executor 프레임워크는 스레드 풀(Thread Pool) 개념을 기반으로 설계되었습니다. 스레드 풀은 작업을 실행할 때마다 새로운 스레드를 생성하는 대신, 이미 생성된 스레드를 재사용함으로써 시스템 자원을 효율적으로 관리하고 성능을 향상시키는 기법입니다.
Executors 클래스는 스레드 풀을 손쉽게 생성할 수 있는 다양한 팩토리 메서드를 제공하며, ThreadPoolExecutor 클래스는 이 팩토리 메서드들이 반환하는 대부분의 Executor 구현체의 기반이 되는 핵심 클래스입니다.
고정 스레드 풀 vs 캐시 스레드 풀 핵심 차이점
| 구분 | 고정 스레드 풀 (Fixed Thread Pool) | 캐시 스레드 풀 (Cached Thread Pool) |
|---|---|---|
| 기본 동작 | 고정된 개수의 스레드가 공유 무제한 큐(unbounded queue)에서 작업을 처리하는 스레드 풀을 생성합니다. 동시에 활성화되어 작업을 처리하는 스레드는 최대 nThreads개이며, 모든 스레드가 바쁠 때 작업이 추가로 제출되면 큐에서 대기합니다. 종료(shutdown) 전에 스레드가 실행 실패로 종료되면, 이후 작업 처리를 위해 필요한 경우 새 스레드가 대체 생성됩니다. 풀의 스레드는 명시적으로 종료될 때까지 유지됩니다. | 필요할 때 새로운 스레드를 생성하되, 사용 가능한 기존 스레드가 있으면 이를 재사용하는 스레드 풀입니다. 수명이 짧은 비동기 작업을 대량으로 실행하는 프로그램의 성능을 크게 개선합니다. execute() 호출 시 유휴 상태의 스레드가 있으면 이를 재사용하고, 없으면 새 스레드를 생성해 풀에 추가합니다. |
| 내부 큐 | BlockingQueue를 사용합니다. | SynchronousQueue를 사용합니다. |
| 스레드 수명 | 명시적으로 종료하기 전까지 모든 스레드를 계속 유지합니다. | 60초 동안 사용되지 않은 스레드는 종료되고 캐시에서 제거됩니다. |
| 스레드 풀 크기 | 크기가 고정되어 있어 늘어나지 않습니다. | 0개부터 Integer.MAX_VALUE까지 스레드 수가 늘어날 수 있습니다. |
| 사용 사례 | 동시에 실행되는 작업 수를 제한해야 할 때 적합합니다. | 수명이 짧은 비동기 작업이 대량으로 발생할 때 적합합니다. |
고정 스레드 풀 예제 코드
다음은 Executors.newFixedThreadPool(4)로 고정 스레드 풀을 생성한 뒤, CountDownLatch를 사용해 두 개의 서비스 작업이 모두 완료될 때까지 대기하는 예제입니다.
Main.java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String args[]) throws InterruptedException {
ExecutorService executors = Executors.newFixedThreadPool(4);
CountDownLatch latch = new CountDownLatch(2);
executors.submit(new Service1(latch));
executors.submit(new Service2(latch));
latch.await();
System.out.println("Done");
}
}
Service1.java
import java.util.concurrent.CountDownLatch;
public class Service1 implements Runnable {
CountDownLatch latch;
public Service1(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
System.out.println("Service1 - 남은 카운트: " + latch.getCount());
}
}
Service2.java
import java.util.concurrent.CountDownLatch;
public class Service2 implements Runnable {
CountDownLatch latch;
public Service2(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
System.out.println("Service2 - 남은 카운트: " + latch.getCount());
}
}
정리: 어떤 스레드 풀을 선택해야 할까?
고정 스레드 풀은 동시 작업 수를 제한하여 서버나 시스템에 가해지는 부하를 통제해야 하는 경우에 적합합니다. 반면 캐시 스레드 풀은 작업량이 예측하기 어렵고 각 작업의 수명이 짧은 경우 유연하게 대응할 수 있습니다. 다만 캐시 스레드 풀은 스레드 수가 무제한으로 늘어날 수 있으므로, 과도한 스레드 생성으로 인한 자원 고갈(resource exhaustion)이 발생하지 않도록 주의해야 합니다.