Files
java-examples/docs/Concurrency/05_Executor.md

401 lines
17 KiB
Markdown

### 자바의 `Executor`와 관련된 설명
자바의 `Executor` 프레임워크는 스레드 풀을 관리하고 작업을 비동기적으로 실행하기 위한 강력한 도구로, `java.util.concurrent` 패키지에 포함되어 있습니다. `Executor` 인터페이스를 기반으로, 스레드 생성과 관리를 직접 다루는 대신 작업 단위로 실행을 위임하여 코드의 가독성과 효율성을 높입니다. 주요 클래스와 메서드를 표로 정리하고, 예시를 통해 사용 방법을 설명하겠습니다.
---
### `Executor`와 관련된 클래스 및 메서드 표
#### `Executor` 인터페이스
| 메서드명 | 반환 타입 | 설명 |
|-------------------------|-----------|---------------------------------------------------|
| `execute(Runnable)` | `void` | `Runnable` 작업을 실행하도록 스레드 풀에 위임 |
#### `ExecutorService` 인터페이스 (확장 인터페이스)
| 메서드명 | 반환 타입 | 설명 |
|-----------------------------------|-------------------|----------------------------------------------------------------------|
| `submit(Runnable)` | `Future<?>` | `Runnable` 작업을 실행하고 결과를 추적할 수 있는 `Future` 반환 |
| `submit(Callable<T>)` | `Future<T>` | `Callable` 작업을 실행하고 결과를 반환하는 `Future` 반환 |
| `shutdown()` | `void` | 새로운 작업을 받지 않고, 기존 작업 완료 후 종료 |
| `shutdownNow()` | `List<Runnable>` | 즉시 종료 시도하고 실행 대기 중인 작업 목록 반환 |
| `awaitTermination(long, TimeUnit)` | `boolean` | 지정된 시간 동안 종료를 기다리며, 완료 여부 반환 |
| `invokeAll(Collection<Callable<T>>)` | `List<Future<T>>` | 모든 `Callable` 작업을 실행하고 결과 목록 반환 |
| `invokeAny(Collection<Callable<T>>)` | `T` | 주어진 `Callable` 중 하나가 완료되면 그 결과를 반환 |
#### `Executors` 유틸리티 클래스 (정적 팩토리 메서드)
| 메서드명 | 반환 타입 | 설명 |
|-----------------------------------|-------------------|----------------------------------------------------------------------|
| `newFixedThreadPool(int n)` | `ExecutorService` | 고정 크기의 스레드 풀 생성 |
| `newSingleThreadExecutor()` | `ExecutorService` | 단일 스레드로 작업을 순차적으로 실행하는 풀 생성 |
| `newCachedThreadPool()` | `ExecutorService` | 필요에 따라 스레드를 생성/재사용하는 동적 풀 생성 |
| `newScheduledThreadPool(int n)` | `ScheduledExecutorService` | 주기적 작업을 스케줄링할 수 있는 스레드 풀 생성 |
#### `ScheduledExecutorService` 인터페이스 (확장 인터페이스)
| 메서드명 | 반환 타입 | 설명 |
|-----------------------------------|-------------------|----------------------------------------------------------------------|
| `schedule(Runnable, long, TimeUnit)` | `ScheduledFuture<?>` | 지정된 지연 시간 후 작업 실행 |
| `scheduleAtFixedRate(Runnable, long, long, TimeUnit)` | `ScheduledFuture<?>` | 고정 주기로 작업 반복 실행 |
| `scheduleWithFixedDelay(Runnable, long, long, TimeUnit)` | `ScheduledFuture<?>` | 작업 완료 후 고정 지연을 두고 반복 실행 |
#### 관련 클래스
| 클래스명 | 설명 |
|-------------------------|----------------------------------------------------------------------|
| `ThreadPoolExecutor` | 스레드 풀의 세부 설정(코어 풀 크기, 최대 풀 크기 등)을 커스터마이징 가능 |
| `Future<T>` | 비동기 작업의 결과를 추적하거나 취소할 수 있는 인터페이스 |
| `Callable<T>` | `Runnable`과 유사하나 결과를 반환하며 예외를 던질 수 있는 인터페이스 |
---
### `Executor` 설명 및 예시
`Executor` 프레임워크는 스레드 관리의 복잡성을 줄이고, 작업 실행을 추상화하여 재사용 가능한 스레드 풀을 제공합니다. 아래에서 주요 사용 사례를 예시로 설명합니다.
#### 1. 기본 사용: `ExecutorService`와 `newFixedThreadPool`
고정 크기의 스레드 풀을 생성하고 작업을 실행합니다.
```java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2); // 2개 스레드 풀
for (int i = 0; i < 5; i++) {
int taskId = i;
executor.execute(() -> {
System.out.println("작업 " + taskId + " 실행 중: " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executor.shutdown(); // 작업 제출 후 종료 요청
}
}
```
- **출력 예시**:
```
작업 0 실행 중: pool-1-thread-1
작업 1 실행 중: pool-1-thread-2
(1초 후)
작업 2 실행 중: pool-1-thread-1
작업 3 실행 중: pool-1-thread-2
(1초 후)
작업 4 실행 중: pool-1-thread-1
```
- **설명**: 2개의 스레드가 최대 2개 작업을 동시에 처리하며, 나머지는 대기 후 실행됨.
#### 2. `Callable`과 `Future`로 결과 받기
`submit`을 사용해 결과를 반환하는 작업을 실행합니다.
```java
import java.util.concurrent.*;
public class CallableExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
Future<Integer> future = executor.submit(task);
System.out.println("작업 제출 완료");
Integer result = future.get(); // 작업 완료까지 대기 후 결과 가져오기
System.out.println("결과: " + result);
executor.shutdown();
}
}
```
- **출력 예시**:
```
작업 제출 완료
(1초 후)
결과: 42
```
- **설명**: `Future.get()`은 작업이 완료될 때까지 블록하며, 결과를 반환받음.
#### 3. 주기적 실행: `ScheduledExecutorService`
`scheduleAtFixedRate`로 주기적으로 작업을 실행합니다.
```java
import java.util.concurrent.*;
public class ScheduledExecutorExample {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> System.out.println("작업 실행: " + System.currentTimeMillis());
executor.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS); // 1초 후 시작, 2초 주기
try {
Thread.sleep(5000); // 5초 동안 실행
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
}
}
```
- **출력 예시**:
```
(1초 후)
작업 실행: 1678321234567
(2초 후)
작업 실행: 1678321236567
(2초 후)
작업 실행: 1678321238567
```
- **설명**: 작업이 고정된 2초 주기로 실행됨.
#### 4. `invokeAll`로 여러 작업 실행
여러 `Callable` 작업을 한 번에 실행하고 결과를 수집합니다.
```java
import java.util.concurrent.*;
import java.util.Arrays;
import java.util.List;
public class InvokeAllExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(2);
List<Callable<String>> tasks = Arrays.asList(
() -> { Thread.sleep(1000); return "작업 1 완료"; },
() -> { Thread.sleep(500); return "작업 2 완료"; },
() -> { Thread.sleep(1500); return "작업 3 완료"; }
);
List<Future<String>> futures = executor.invokeAll(tasks);
for (Future<String> future : futures) {
System.out.println(future.get());
}
executor.shutdown();
}
}
```
- **출력 예시**:
```
작업 1 완료
작업 2 완료
작업 3 완료
```
- **설명**: 모든 작업이 완료될 때까지 대기한 후 결과를 순차적으로 출력.
---
### `Executor`의 장점과 주의점
- **장점**:
- 스레드 풀을 재사용하여 리소스 낭비 감소.
- 작업 실행과 스레드 관리를 분리하여 코드 간소화.
- 주기적 작업 및 결과 처리가 용이.
- **주의점**:
- `shutdown()`을 호출하지 않으면 프로그램이 종료되지 않을 수 있음.
- 스레드 풀 크기와 작업 부하를 적절히 조정해야 성능 최적화 가능.
---
### 결론
`Executor` 프레임워크는 스레드 관리의 복잡성을 줄이고, 다양한 실행 패턴(단일 실행, 주기적 실행, 병렬 실행 등)을 지원합니다. `ExecutorService`, `ScheduledExecutorService`, `Executors`를 활용하면 효율적인 멀티스레드 애플리케이션을 쉽게 구현할 수 있습니다. 위 예시를 참고하여 실제 프로젝트에 적용해 보세요!
---
### `ScheduledExecutorService`와 다른 스케줄링 도구 비교
자바에서 주기적이거나 지연된 작업을 스케줄링할 때 `ScheduledExecutorService`는 강력한 도구로 사용됩니다. 하지만 `java.util.Timer`와 `CompletableFuture`와 같은 대안도 있으며, 각각의 특징과 사용 사례가 다릅니다. 아래에서 `ScheduledExecutorService`를 `Timer`와 `CompletableFuture`와 비교하여 표로 정리하고, 예시와 함께 설명하겠습니다.
---
### 비교 표
| **특징** | **`ScheduledExecutorService`** | **`Timer`** | **`CompletableFuture`** |
|---------------------------|-----------------------------------------------------|------------------------------------------|------------------------------------------|
| **패키지** | `java.util.concurrent` | `java.util` | `java.util.concurrent` |
| **스레드 모델** | 스레드 풀 기반 (다중 스레드 지원) | 단일 스레드 기반 | 기본 풀(`ForkJoinPool`) 또는 사용자 지정 |
| **작업 타입** | `Runnable`, `Callable` | `TimerTask` | `Runnable`, `Supplier` |
| **스케줄링 메서드** | `schedule`, `scheduleAtFixedRate`, `scheduleWithFixedDelay` | `schedule`, `scheduleAtFixedRate` | `supplyAsync` + `then` 조합으로 간접 지원 |
| **고정 주기 지원** | `scheduleAtFixedRate` (고정 주기), `scheduleWithFixedDelay` (고정 지연) | `scheduleAtFixedRate` (고정 주기) | 직접 지원 없음, 별도 로직 필요 |
| **예외 처리** | 개별 작업 예외가 전체에 영향 없음 | 예외 발생 시 `Timer` 종료 가능 | `exceptionally` 등으로 명시적 처리 가능 |
| **취소 기능** | `ScheduledFuture.cancel()` | `TimerTask.cancel()`, `Timer.cancel()` | `complete()`, `cancel()` |
| **복잡한 작업 조합** | 제한적 | 없음 | `thenApply`, `allOf` 등으로 가능 |
| **성능 및 확장성** | 높은 부하와 복잡한 작업에 적합 | 간단한 작업에 적합 | 비동기 작업 조합에 강력 |
| **사용 용도** | 주기적 작업, 다중 스레드 스케줄링 | 간단한 단일 스레드 스케줄링 | 비동기 작업 흐름 관리 |
---
### 상세 설명 및 예시
#### 1. `ScheduledExecutorService`
- **특징**: 스레드 풀을 활용하여 다중 스레드로 작업을 처리하며, 예외 발생 시 다른 작업에 영향을 주지 않습니다.
- **예시**: 주기적으로 상태를 점검하는 작업.
```java
import java.util.concurrent.*;
public class ScheduledExecutorExample {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
Runnable task = () -> System.out.println("실행: " + System.currentTimeMillis());
executor.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
}
}
```
- **출력 예시**:
```
실행: 1678321234567
실행: 1678321236567
실행: 1678321238567
```
#### 2. `Timer`
- **특징**: 단일 스레드로 동작하며, 작업 중 예외가 발생하면 `Timer`가 종료될 수 있습니다.
- **예시**: 간단한 알림 기능.
```java
import java.util.*;
public class TimerExample {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("실행: " + System.currentTimeMillis());
}
};
timer.scheduleAtFixedRate(task, 1000, 2000);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
timer.cancel();
}
}
```
- **출력 예시**:
```
실행: 1678321234567
실행: 1678321236567
실행: 1678321238567
```
#### 3. `CompletableFuture`
- **특징**: 주기적 스케줄링은 직접 지원하지 않지만, 비동기 작업 조합과 예외 처리가 뛰어납니다.
- **예시**: 작업 완료 후 후속 작업 실행.
```java
import java.util.concurrent.*;
public class CompletableFutureExample {
public static void main(String[] args) throws Exception {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("실행: " + System.currentTimeMillis());
}).thenRun(() -> System.out.println("후속 작업"));
future.get();
}
}
```
- **출력 예시**:
```
실행: 1678321235567
후속 작업
```
#### 비교 예시: 예외 발생 시 동작
- **`ScheduledExecutorService`**와 **`Timer`**의 차이점을 보여줍니다.
```java
import java.util.*;
import java.util.concurrent.*;
public class ExceptionComparison {
public static void main(String[] args) {
// Timer 예시
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
int count = 0;
@Override
public void run() {
count++;
System.out.println("Timer 실행 " + count);
if (count == 2) throw new RuntimeException("Timer 예외");
}
}, 0, 1000);
// ScheduledExecutorService 예시
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
int count = 0;
count++;
System.out.println("Executor 실행 " + count);
if (count == 2) throw new RuntimeException("Executor 예외");
}, 0, 1, TimeUnit.SECONDS);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
timer.cancel();
executor.shutdown();
}
}
```
- **출력 예상**:
```
Timer 실행 1
Timer 실행 2
(예외 발생 후 Timer 종료, 더 이상 실행 안 됨)
Executor 실행 1
Executor 실행 2
(예외 발생 후에도 다른 작업은 계속 실행 가능)
```
- **설명**: `Timer`는 예외로 종료되지만, `ScheduledExecutorService`는 영향을 받지 않음.
---
### 선택 가이드
- **`ScheduledExecutorService`**:
- 다중 스레드와 안정성이 필요한 주기적 작업.
- 복잡한 작업 관리와 확장성 요구 시.
- **`Timer`**:
- 간단한 단일 스레드 작업.
- 경량 애플리케이션에서 최소한의 스케줄링 필요 시.
- **`CompletableFuture`**:
- 비동기 작업 흐름을 조합하거나 단일 작업의 후속 처리가 필요할 때.
- 주기적 스케줄링보다는 결과 기반 작업에 적합.
---
### 결론
`ScheduledExecutorService`는 스레드 풀 기반으로 안정성과 유연성을 제공하며, `Timer`는 간단한 작업에 적합하지만 취약점이 있습니다. `CompletableFuture`는 주기적 스케줄링보다는 비동기 작업 조합에 강점을 가집니다. 요구사항에 따라 적절한 도구를 선택해 사용하세요!