
현대적인 서버 사이드 애플리케이션에서 수많은 요청을 동시에 처리하는 능력은 필수적입니다. 자바 개발자가 멀티쓰레딩을 구현할 때 가장 먼저 배우는 것은 new Thread()이지만, 실제 운영 환경에서 이 방식을 사용하는 것은 매우 위험할 수 있습니다. 무분별한 쓰레드 생성은 메모리 부족(OOM)과 컨텍스트 스위칭 오버헤드로 인해 시스템을 마비시킬 수 있기 때문입니다. 이러한 문제를 우아하게 해결해주는 것이 바로 쓰레드 풀(Thread Pool)과 이를 관리하는 ExecutorService입니다. 본 포스팅에서는 자바의 동시성 프레임워크인 java.util.concurrent 패키지를 중심으로, 효율적인 쓰레드 관리 기법을 심층적으로 다루어 보겠습니다.
1. 쓰레드 풀(Thread Pool)의 개념과 도입 배경
쓰레드 풀은 미리 일정 수의 쓰레드를 생성해 두고, 작업(Task)이 들어올 때마다 대기 중인 쓰레드에 할당하여 처리하는 방식입니다. 작업이 완료된 쓰레드는 소멸하지 않고 다시 풀로 돌아가 다음 작업을 기다립니다.
왜 쓰레드 풀을 사용해야 하는가?
- 리소스 재사용: 쓰레드 생성 및 제거에 따르는 시스템 비용(시간 및 메모리)을 절감합니다.
- 응답 속도 향상: 작업 요청 시 이미 생성된 쓰레드가 즉시 투입되므로 대기 시간이 줄어듭니다.
- 부하 제어: 동시에 실행되는 쓰레드의 최대치를 제한하여 시스템 자원 고갈을 방지합니다.
2. ExecutorService: 자바 쓰레드 관리의 표준 인터페이스
ExecutorService는 비동기적으로 작업을 실행할 수 있는 메커니즘을 제공하는 인터페이스입니다. 쓰레드 생성, 작업 할당, 종료 절차 등 복잡한 생명주기를 개발자가 직접 관리할 필요 없이 선언적으로 처리할 수 있게 돕습니다.
| 종류 (Factory Method) | 특징 및 용도 | 권장 상황 |
|---|---|---|
| newFixedThreadPool(n) | 고정된 개수의 쓰레드를 유지함 | 예측 가능한 부하가 발생하는 서버 |
| newCachedThreadPool() | 필요에 따라 쓰레드를 생성하며, 유휴 쓰레드는 회수함 | 작업 부하의 변동폭이 큰 단기 작업 |
| newSingleThreadExecutor() | 단 하나의 쓰레드만 사용하여 순차 실행함 | 작업의 순서 보장이 필요한 경우 |
| newScheduledThreadPool(n) | 지정한 시간에 작업을 실행하거나 주기적으로 반복함 | 타이머, 스케줄링 작업 |
3. 실무 예제: ExecutorService 활용하기 (Sample Example)
다음은 FixedThreadPool을 사용하여 5개의 작업을 동시에 처리하는 예시 코드입니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 최대 3개의 쓰레드를 사용하는 풀 생성
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) {
int taskId = i;
executor.submit(() -> {
String threadName = Thread.currentThread().getName();
System.out.println("Task " + taskId + " is running on " + threadName);
try { Thread.sleep(1000); } catch (InterruptedException e) {}
});
}
// 2. 더 이상 작업을 받지 않고 안전하게 종료
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
4. 주의사항: 실무에서 Executors를 지양해야 하는 이유
많은 전문가들은 Executors.newFixedThreadPool() 대신 ThreadPoolExecutor를 직접 생성하여 사용할 것을 권장합니다. 그 이유는 기본 생성 방식에서 사용하는 LinkedBlockingQueue가 무제한(Unbounded) 크기를 가지기 때문입니다.
요청이 폭주할 경우 대기 큐에 작업이 무한정 쌓여 OutOfMemoryError를 유발할 수 있습니다. 따라서 실무에서는 아래와 같이 큐의 크기와 거부 정책(Rejection Policy)을 명시적으로 설정하는 것이 좋습니다.
// 실무 권장 방식: 직접 생성
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // Core Pool Size
20, // Max Pool Size
60L, TimeUnit.SECONDS, // Keep Alive Time
new ArrayBlockingQueue<>(100), // Bounded Queue (큐 크기 제한)
new ThreadPoolExecutor.CallerRunsPolicy() // 거부 정책 설정
);
5. 결론: 쓰레드 관리의 미학
자바의 ExecutorService는 멀티쓰레딩의 복잡성을 추상화하여 개발자가 비즈니스 로직에 집중할 수 있게 해줍니다. 하지만 '공짜 점심'은 없습니다. 사용하는 쓰레드 풀의 특성을 이해하고, 적절한 쓰레드 개수와 큐 크기를 산정하는 성능 테스트 과정이 수반되어야만 진정으로 견고한 애플리케이션을 완성할 수 있습니다.
내용 출처 및 참고 문헌
- Oracle Java SE 21 Documentation: java.util.concurrent Interface ExecutorService
- Joshua Bloch, "Effective Java 3rd Edition" - Item 80: 쓰레드보다는 실행자, 태스크, 스트림을 애용하라
- Brian Goetz, "Java Concurrency in Practice" - Chapter 6. Task Execution
'Language > Java' 카테고리의 다른 글
| [JAVA] 쓰레드 로컬(ThreadLocal)의 마법 : 쓰레드별 독립적인 데이터 관리 (0) | 2026.01.21 |
|---|---|
| [JAVA] 비동기 프로그래밍의 완성 : Callable과 Future 인터페이스 심층 분석 (0) | 2026.01.21 |
| [JAVA] 가시성 문제의 해결사, volatile 키워드의 완벽 이해와 실무 활용 (0) | 2026.01.21 |
| [JAVA] 자바 쓰레드 제어의 한 끗 차이 : sleep() vs wait() 완벽 분석 (0) | 2026.01.21 |
| [JAVA] 쓰레드 간의 효율적인 대화 : wait(), notify(), notifyAll() 완벽 가이드 (0) | 2026.01.21 |