Java 8 이전의 멀티스레딩에 대해 알아보자: Thread Pool

2026. 1. 29. 21:33·Java

계기

이에 대해 조사하게 된 계기는 바로 자바 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
'Java' 카테고리의 다른 글
  • Java 개념 장착하기 02 - Java의 변화(1)
  • Java 개념 장착하기 01 - Java의 시작과 철학
Kirbyyy
Kirbyyy
개인적인 일상과 회고를 기록하는 블로그입니다.
  • Kirbyyy
    커브볼의 생존일지
    Kirbyyy
  • 전체
    오늘
    어제
    • 분류 전체보기 (53)
      • 우아한테크코스 (8)
      • 프로덕트 빌드 (0)
      • Problem Solving (20)
      • C++ (0)
      • Kotlin (19)
      • Java (3)
      • CS (2)
        • AI (2)
      • 취미생활 (0)
        • 서평 (0)
        • 프라모델 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    백준 11123
    백준 파도반 수열
    그리디 알고리즘
    백준 RGB 거리
    우테코 8기
    C++
    백준 16173
    Problem Solving
    백준 1356
    다이나믹 프로그래밍
    백준 16174
    ProblemSolving
    분할 정복
    백준 연속 합
    너비 우선 탐색
    백준 33272
    BFS
    백준 31575
    백준 알고리즘
    백준
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Kirbyyy
Java 8 이전의 멀티스레딩에 대해 알아보자: Thread Pool
상단으로

티스토리툴바