계기
이에 대해 조사하게 된 계기는 바로 자바 8의 멀티스레딩의 변화를 찾아보면서, 자바 8 이전의 멀티스레딩은 어땠길래 자바 8에 새로운 멀티스레딩 방법을 제공하게 되었는지 의문이 생기게 되었다. 때문에 해당 방법들에 대해 자세히 정리하고, 고찰해보려고 한다.
Thread Pool
스레드 풀은 자바만의 개념은 아니다. 스레드 풀은 사실 소프트웨어 디자인 패턴 중 하나이다. 이름에서 어느 정도 유추해볼 수 있듯이 스레드가 모여있는 "스레드 풀"을 만들어 놓고, 여기에 처리할 작업을 넘겨주면 해당 스레드 풀에서 유휴 스레드가 작업을 수행하는 방식이다.

이러한 구조는 직관적으로 봤을 때도 꽤 장점이 있을 것이라 생각됐다, 실제로도 그러했는데 나열하면 다음과 같다.
스레드를 생성하고 파괴하는 오버헤드가 크게 줄어든다.
스레드 풀의 스레드 개수는 마음대로 조정할 수 있으므로 하드웨어 상황에 따라 유연하게 맞출 수 있다.
수동 스레드 관리보다 쉽게 작업을 큐에 넣을 수 있고, 병행성 제어, 동기화 수준을 높일 수 있다.
(이는 직접적으로 와닿지 않을 수 있지만, 과거 뛰어난 프로그래머들이 이러한 목표를 달성한 구현을 해뒀다, 예시: C++ 스레드풀 라이브러리https://github.com/vit-vit/ctpl)
Java 5에서의 Thread Pool과 Executor
자바 5의 스레드 풀을 좀 더 깊게 이해하기 스레드 풀이 상속하고있는 인터페이스, 클래스들을 짚고 넘어가도록 하겠다.
1. Executor
public interface Executor
메서드
void execute(Runnable command)
이름에서도 알 수 있듯이 전달된 Runnable 작업을 실행하는 객체이다. 처음에는 그냥 실행하면 되지 않나? 라고 의문이 들긴 했다.
공식 문서에 따르면 다음과 같이 기술되어있다.
This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc. An Executor is normally used instead of explicitly creating threads.
이 인터페이스는 작업 제출을 각 작업이 실행되는 메커니즘으로부터 분리하는 방법을 제공합니다. Executor는 일반적으로 명시적 스레드 생성 대신 사용됩니다.
같이 기술되어있는 예시를 보니 왜 이를 도입했는지 알 수 있었다.
실행해야 하는 작업 집합에 대해 이전 문법으로는 다음과 같이 사용했을 것이다.
// 각각의 작업들에 대해 아래 코드를 호출한다.
new Thread(new(RunnableTask())).start()
사실 이렇게만 보면 뭐가 문제인지 알기 쉽지 않다. 때문에 위같은 상황에 Executor 객체를 사용했을 때를 보도록 하겠다.
// anExecutor는 이전에 언급했던 executor를 구현한다
// 때문에 anExecutor.execute는 구현하기 나름!
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
...
이를 보고 가장 먼저 든 생각은 단일 책임 원칙이였다. 공식 문서에도 언급되어있듯이 직접 스레드를 생성하고 작업을 던지는 이전 코드는 작업 제출과 스레드 사용, 스케줄링 즉 제출과 실행이 한데 얽혀있다. 즉 유지보수가 매우 어렵다라고 할 수 있겠다.
때문에 Executor의 사용 예시를 보면, execute를 따로 구현되게 함으로써, 작업 제출과 작업 수행 매커니즘 로직을 분리했다. 공식 문서의 예시를 계속 살펴보면 다음과 같은 예시들을 제공한다.
// 받은 작업을 호출자의 스레드에서 즉시 실행
class DirectExecutor implements Executor {
public void execute(Runnable r) {
r.run();
}
}
// 받은 작업을 호출자 스레드가 아닌 다른 스레드에서 실행
class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
}
}
결론적으로 위와 같이 변경됨으로써, 작업 제출 단계에서는 execute()의 구현 즉 복잡한 스레딩 정책은 몰라도 되게 만든다.
2. ExecutorService
public interface ExecutorService extends Executor
메서드
boolean awaitTermination(long timeout, TimeUnit unit)
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
boolean isShutdown()
boolean isTerminated()
void shutdown()
List<Runnable> shutdownNow()
<T> Future<T> submit(Callable<T> task)
Future<?> submit(Runnable task)
<T> Future<T> submit(Runnable task, T result)
메서드가 상당히 많은데 일단 타입명에 Service가 들어가는 것으로 보아, 작업 수행을 마치 서비스처럼 다루고자 하는 것으로 생각해볼 수 있겠다. 공식 문서에는 이렇게 나와있다.
An Executor that provides methods to manage termination and methods that can produce a Future for tracking progress of one or more asynchronous tasks.
종료를 관리하기 위한 메서드와 하나 이상의 비동기 작업 진행 상황을 추적하기 위해 Future를 생성할 수 있는 메서드를 제공하는 Executor이다.
이를 통해 단순히 작업 수행 매커니즘을 구현한 Executor객체에서 다음 기능을 추가했다고 볼 수 있겠다
작업 묶기(invokeAll, invokeAny)
생명 주기 관리(shutdown, isShutdown, awaitTerminate, isTerminated)
작업 제출 + 결과 추적(Future<T> submit)
3. AbstractExecutorService
public abstract class AbstractExecutorService extends Object implements ExecutorService
해당 클래스는 이름에서도 알 수 있듯이 좀 더 라이트하게 사용가능한 ExecutorService 인터페이스라고 할 수 있겠다. 굳이 해당 클래스로 중간 다리를 왜 놓았을지 의문이 들었는데, 다시 보니 ExecutorSevice 인터페이스에 구현해야할 메소드가 많아도 좀 많아보이긴 한다...

때문에 공식 문서에서도 다음과 같이 나와있다
Provides default implementations of ExecutorService execution methods.
ExecutorService 실행 메서드들의 기본 구현을 제공합니다
그래서 어떤 메서드를 어떻게 구현했는지 살펴보면 submit(), invokeAny(), invokeAll() 메서드를 구현했다고 나와있다. 구체적인 각각의 구현은 나와있지 않으나, newTaskFor() 메서드에 의해 반환된 RunnableFuture로 감싸진 작업을 이용하여 구현한다고 나와있다. 자바 문법에 아직 익숙하지 않아, 이부분에 대한 설명과 조사는 추후에 진행하도록 하겠다.
결론적으로 던져본 질문은 이거였다.
"왜 얘네만 구현했을까? ". 나름대로 내려본 생각은, 당연한 말이지만, 웬만하면 동일한 동작이기 때문이다.
위 메서드들의 공통점은 주로 작업을 어떤 단위로 감싸서 실행하고, 결과를 어떻게 추적할지 등의 구현을 기대하는 메서드이므로, 공통된 부분이 많이 존재할 것이다.
또 다른 이유는 이러한 내용들은 특히 개발자가 구현하기 어려운 내용이므로 개발자의 구현 부담을 크게 덜어준다는 이유가 있을 것 같다.
4. ThreadPoolExecutor
public class ThreadPoolExecutor
extends AbstractExecutorService
결론적으로 해당 클래스는 위에서 설명한 AbstractExecutorService를 상속한다. 결국 우리가 처음 탐구하고자 했던 부분이라고 할 수 있겠다. 이름에서도 예측할 수 있듯이 이제서야 스레드풀을 다루는 로직들에 대해 정의해둔 클래스로 생각된다.
공식 문서에는 다음과 같이 나와있다.
An ExecutorService that executes each submitted task using one of possibly several pooled threads, normally configured using Executors factory methods.
풀링된 여러 스레드 중 하나를 사용하여 제출된 각 작업을 실행하는 ExecutorService입니다. 보통 Executors 팩토리 메서드를 통해 설정됩니다.
정말 놀합게도, 마지막 부분에서 알 수 있듯이. 자바에서는 이를 직접 사용하는 것을 디폴트로 설계하지 않고, Executors 팩토리 메서드를 통해 알맞게 설정된 ThreadPoolExecutor 객체를 사용하는 것을 디폴트로 상정함을 알 수 있었다.
때문에 해당 클래스의 생성자를 살펴보면 상당히 설정해줘야 하는 값이 많다.
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
5. Executors
public class Executors
extends Object
위에 소개한 인터페이스를 구현하지는 않는다. 위에서 소개했듯 ThreadPoolExecutor를 다양한 용도에 맞게 사전 설정해둔 팩토리 메서드를 제공한다. 제공하는 메서드들 중에서 이에 해당하는 팩토리 메서드 중 일부를 나열해보면 다음과 같다.
static ExecutorService newCachedThreadPool()
static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
static ExecutorService newFixedThreadPool(int nThreads)
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
static ExecutorService newSingleThreadExecutor()
static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
static ExecutorService newWorkStealingPool()
static ExecutorService newWorkStealingPool(int parallelism)
'Java' 카테고리의 다른 글
| Java 개념 장착하기 02 - Java의 변화(1) (0) | 2026.01.28 |
|---|---|
| Java 개념 장착하기 01 - Java의 시작과 철학 (0) | 2026.01.27 |