17 KiB
자바의 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
고정 크기의 스레드 풀을 생성하고 작업을 실행합니다.
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을 사용해 결과를 반환하는 작업을 실행합니다.
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로 주기적으로 작업을 실행합니다.
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 작업을 한 번에 실행하고 결과를 수집합니다.
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
- 특징: 스레드 풀을 활용하여 다중 스레드로 작업을 처리하며, 예외 발생 시 다른 작업에 영향을 주지 않습니다.
- 예시: 주기적으로 상태를 점검하는 작업.
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가 종료될 수 있습니다. - 예시: 간단한 알림 기능.
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
- 특징: 주기적 스케줄링은 직접 지원하지 않지만, 비동기 작업 조합과 예외 처리가 뛰어납니다.
- 예시: 작업 완료 후 후속 작업 실행.
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**의 차이점을 보여줍니다.
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는 주기적 스케줄링보다는 비동기 작업 조합에 강점을 가집니다. 요구사항에 따라 적절한 도구를 선택해 사용하세요!