본문 바로가기

Java

Java - Executor

Executor를 이해하기 위해 이해해야 할 몇 가지 개념들이 존재합니다. 이를 먼저 정리한 뒤 Executor를 설명하도록 하겠습니다.

 

목차 

1.Thread

2.Task

3.Executor

 

 

 

Thread


프로그래밍 세계에서 클라이언트의 요청을 수행하기 위해서는 누군가 일을 해야합니다. 프로그래밍 세계에서 일(Task)을 하는 누군가를 쓰레드(Thread)라고 합니다.

 

컴퓨터 공학 서적을 찾아보면 보통 쓰레드를 프로세스 내에서 실행되는 흐름의 단위라고 합니다. 참고로 쓰레드의 어원은 입니다. 아마도 프로그래밍의 세상에서 일(Task)이란, 코드를 실행하는 것입니다. 추측컨데 위에서 아래로 코드를 읽어내려가는 모습이 마치 실과 같아서 이러한 어원이 생기게 된 것이 아닐까요? :)

 

하여튼 프로그래밍 세계에서는 어떠한 일을 수행하기 위해서는 스레드를 생성해야 합니다. 코드를 통해 스레드를 생성해보겠습니다.

 

쓰레드 생성 및 작업 수행 예시 코드

위 코드에서 new 를 통해 쓰레드를 생성하거 자신의 이름을 소개하는 작업을 수행하도록 해보았습니다. 하지만 이 코드에는 몇 가지 문제가 있습니다. 대표적으로 스레드를 직접 생성해야 한다는 것입니다. 이외에도 스레드 종료 및 스레드가 많아졌을 때의 관리 등을 직접해야 어려움이 있습니다.

 

이에 대한 해결책으로 풀(Pool)이란 것을 많이 사용합니다. Spring boot 환경에서 개발을 해본 경험이 있다면 내장 Tomcat WAS, HikariCP 라는 단어를 들어보셨을 수도 있을텐데요. 이들 모두 풀이라는 개념을 사용하고 있습니다. 풀이란 간단히 요약해서 일을 하는 주체들을 미리 생성해두고 필요할 때 꺼내쓰는 공간이라고 생각하시면 됩니다.

 

풀이라는 개념을 이해한다면 Executor가 등장한 배경을 이해하는데 도움이 될 것입니다.

 

Task


앞서 Task란 일이라고 표현했는데요. 앞으로는 작업이라고 표현하겠습니다. Java 세계에서는 Runnable, Callable 인터페이스가 작업을 의미합니다. 인터페이스기 때문에 이들을 구현하면 그것이 작업이 되는 것입니다.

 

두 인터페이스 모두 함수형 인터페이스로 하나의 추상 메서드를 구현하면 됩니다. 차이점은 반환 값의 여부예외를 던질 수 있는가입니다.

 

Runnable

Runnable 경우 리턴 타입이 void 입니다.

 

Callable

Callable의 경우에는 제네릭을 통해 반환 값을 정의하고 예외 또한 던질 수 있습니다.

 

여기서 Java의 역사를 조금 알고 가면 좋을 것 같아요. Runnable의 경우에는 Java의 원년멤버에요. 하지만 Callable의 경우에는 Java5에서 합류하게 되었습니다. Runnable을 사용함에 있어 무언가 불편했을 거에요. 예외 처리나 반환값이 존재하지 않는 이유로요. 그래서 Callable이 등장했다는 것을 추측할 수 있겠죠?

 

Task를 설명하다보니 Runnable/Callable 차이점을 비교하고 되었는데요. 지금 당장 더 중요한 것은 Runnable/Callable 이 Task라는 사실입니다.

 

 

Executor


Executor는 Java5에 추가된 인터페이스입니다. 아래 해당 명세의 설명을 일부 발췌한 내용입니다.

 

An object that executes submitted Runnable tasks. 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는 제출된 작업을 실행하는 객체이다. 일반적으로 스레드를 만드는 것 대신해서 이용한다. 앞서 작업을 실행하는 객체는 Thread라고 했는데요. 이를 통해 Executor가 Thread와 매우 밀접한 연관이 있다는 것을 추측할 수 있습니다. 그리고 스레드를 만드는 것 대신해서 이용한다는 것을 통해 풀이라는 것을 사용한다는 것 또한 추측해볼 수 있습니다.

 

그러면 Executor를 만들어보겠습니다. 정확히는 ExecutorService 구현체를 만들건데요. ExecutorService는 Executor를 상속하는 인터페이스입니다.

 

ExecutorService executorService = Executors.newFixedThreadPool(5);

 

Executors는 Executor와 관련된 클래스들을 생성할 수 있는 팩터리 클래스라고 생각하시면 편합니다.

그러면 한 번 newSingleThreadExecutor라는 정적 팩터리 메서드를 살펴보겠습니다.

 

 

ThreadPoolExecutor 객체를 생성하고 있습니다. 명세에서 발췌한 부분 중 An Executor is normally used instead of explicitly creating threads. 을 명확하게 이해할 수 있을 것입니다. 스레드를 만드는 것 대신 스레드 풀을 사용하겠다는 의미입니다.

 

그러면 한 번 ExecutorService 를 생성해서 작업을 제출하고 실행해보도록 하겠습니다. 

 

 

위를 통해 현재 블록에서 실행되는 쓰레드의 이름은 Test worker 인 것을 확인할 수 있습니다. 그리고 나머지 쓰레드는 ExecutorService 내부 쓰레드 풀에 있는 스레드가 사용된 것을 확인할 수 있습니다.

 

코드가 위에서 아래로 순차적으로 실행되지 않았다는 것이 의아하지 않으신가요? 비동기처리가 되었기 때문입니다. 

추가로 Executor 명세에서 제공하는 설명을 보도록 하겠습니다.

 

An Executor is normally used instead of explicitly creating threads. 

For example, rather than invoking new Thread(new RunnableTask()).start() for each of a set of tasks, you might use:

 Executor executor = anExecutor();  
 executor.execute(new RunnableTask1());  
 executor.execute(new RunnableTask2());


However, the Executor interface does not strictly require that execution be asynchronous. 

In the simplest case, an executor can run the submitted task immediately in the caller's thread:

 class DirectExecutor implements Executor {
    public void execute(Runnable r) {     
       r.run();    // 주목
    }  
}

 

여기서 주목할 것은 start(), run() 그리고 볼드체로 강조한 문구입니다. 먼저 볼드체로 나타낸 부분에서는 Executor 인터페이스에서는 비동기처리를 강제로 요구하지 않는다. 라고 말합니다. 그 예시로 아래 코드를 제공하고 있습니다.

 

run() 메서드는 새로운 스레드를 만들지 않고 현재 스레드에서 작업을 동기적으로 처리합니다. 반면 start() 메서드는 새로운 스레드를 만들며 작업을 비동기적으로 처리합니다.

 

위 코드에서는 Executor 구현체가 새로운 스레드를 생성해서 작업을 비동기적으로 처리하고 있습니다. 더 정확히는 비동기와 더불어 I/O에 대해 논블로킹 상태입니다. 블로킹/논블로킹에 대한 더 자세한 내용은 Callable, Future, CompletableFutre 를 다룰 때 다시 정리하겠습니다.

 

정리하면 Executor는 쓰레드 풀을 활용해서 제출된 작업을 실행(execute)하는 것에 대한 명세를 규정해놓은 인터페이스입니다. 더불어 이에 대한 구현체들은 일반적으로 작업을 비동기/논블로킹으로 작업을 처리합니다.