Java에서 병렬 처리나 비동기 작업을 구현할 때 Thread를 직접 생성해 사용하는 방식은 더 이상 일반적이지 않습니다. 요즘 Java에서는 ExecutorService를 사용하여 스레드 풀(Thread Pool)을 효율적으로 관리하는 것이 표준입니다. 특히 Executors 클래스에서 제공하는 팩토리 메서드들을 활용하면 다양한 유형의 스레드 풀을 간편하게 생성할 수 있습니다.
이번 글에서는 Executors가 제공하는 대표적인 스레드 풀 4종류를 살펴보고, 각각의 사용 시나리오와 주의할 점을 예제 중심으로 정리해보겠습니다.
newFixedThreadPool(int nThreads)
이 메서드는 정해진 개수만큼의 스레드를 미리 생성하고, 그 스레드들이 재사용되며 작업을 처리합니다. 더 많은 작업이 들어오면 내부 큐에 쌓이고, 기존 스레드들이 작업을 처리하면 순차적으로 소비됩니다.
ExecutorService executor = Executors.newFixedThreadPool(4);
✅ 사용 시점
- 처리할 작업량은 많지만, 동시에 처리해야 하는 작업 수를 제한하고 싶은 경우
- CPU 자원을 고르게 사용하며, 안정적인 처리량을 원하는 경우
🧪 예시
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
Thread.sleep(1000); // 모의 작업
});
}
⚠️ 주의
- 과도하게 큰 작업 큐가 쌓일 수 있음 → 메모리 사용량 증가
- 스레드 수는 CPU 코어 수나 애플리케이션 특성에 맞게 설정해야 함
newCachedThreadPool()
이 스레드 풀은 요청이 많으면 새로운 스레드를 계속 생성하고, 특정 기간 동안 사용되지 않으면 제거합니다. 캐시된 스레드를 재사용하므로 빠르고 유연합니다.
ExecutorService executor = Executors.newCachedThreadPool();
✅ 사용 시점
- 작업이 짧고 빠르게 끝나는 I/O 중심의 작업일 때
- 짧은 burst가 자주 발생하는 상황에서 유용
🧪 예시
for (int i = 0; i < 100; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
});
}
⚠️ 주의
- 요청이 많을 경우 스레드가 무제한으로 늘어나 과부하가 될 수 있음
→ 실제 운영에서는 ThreadPoolExecutor로 커스텀 제어 필요
newSingleThreadExecutor()
항상 단 하나의 스레드만 사용하여 작업을 처리합니다. 작업은 FIFO 순서로 실행되며, 순서가 중요한 로직에 적합합니다.
ExecutorService executor = Executors.newSingleThreadExecutor();
✅ 사용 시점
- 작업 순서가 중요할 때
- 하나의 리소스(DB, 파일 등)에 대해 직렬화된 접근이 필요할 때
🧪 예시
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Processing task " + taskId);
Thread.sleep(500);
});
}
⚠️ 주의
- 하나의 스레드에 의존하기 때문에 처리량은 낮음
- 스레드가 죽으면 새로운 스레드로 대체되므로 중단되지는 않음
newScheduledThreadPoool(int corePoolSize)
지연 실행(delay)이나 주기적인 작업(interval) 실행에 특화된 스레드 풀입니다. ScheduledExecutorService를 반환하며, Java의 타이머 역할도 수행할 수 있습니다.
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
✅ 사용 시점
- 주기적인 백업 작업, 모니터링, 알림 전송 등
- 특정 시간 후 작업을 실행하고 싶을 때
🧪 예시
scheduler.scheduleAtFixedRate(() -> {
System.out.println("Heartbeat sent by " + Thread.currentThread().getName());
}, 1, 5, TimeUnit.SECONDS); // 1초 후 시작, 5초마다 반복
⚠️ 주의
- 작업이 너무 오래 걸리면 다음 주기 작업이 지연될 수 있음
→ 처리 시간이 일정하지 않다면 scheduleWithFixedDelay()를 고려
🔍 Thread Pool 선택 가이드
목적 | 추천 스레드 풀 |
CPU 바운드 작업 (병렬 처리) | newFixedThreadPool(n) |
I/O 바운드, 짧은 작업 많음 | newCachedThreadPool() |
순차 실행 보장 | newSingleThreadExecutor() |
타이머 / 주기 작업 | newScheduledThreadPool(n) |
🧠 마무리
Java에서 Executors 팩토리 메서드는 다양한 형태의 스레드 풀을 간단히 생성할 수 있게 도와줍니다. 하지만, 운영 환경에서는 ThreadPoolExecutor를 직접 생성하여 작업 큐, 최대 스레드 수, 거부 정책 등을 세밀하게 조절하는 것이 더욱 안전하고 확장성 있는 선택입니다.
'Java & Spring Boot' 카테고리의 다른 글
Java Redis 직렬화 삼총사: Json, String, JDK (1) | 2024.11.10 |
---|---|
SSE와 WebSocket, 그들은 왜 실시간 통신의 라이벌이 되었을까? (6) | 2024.10.25 |
Spring Boot의 MongoDB 연동: MongoTemplate vs MongoRepository (2) | 2024.10.10 |
자바의 기본 트랜잭션 매니저와 그 구현체들 (0) | 2024.04.13 |
멀티모듈에서 공통 모듈의 의존성이 포함이 안돼요 (0) | 2024.02.21 |