빠른 비교
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(() -> 40 + 2);
int value = future.get();
pool.shutdown();갈리는 기준
어떤 비동기 실행 도구를 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 반환값 없는 작업 제출 | Runnable + execute() |
| 반환값 있는 작업 제출 | Callable + submit() |
| 결과를 나중에 기다리기 | Future.get() |
| 여러 작업을 한 번에 제출하고 모두 대기 | invokeAll(...) |
| 스레드 수를 제어하며 실행 | ExecutorService |
| 작업 종료 정리 | shutdown() + awaitTermination() |
Thread 직접 생성 vs ExecutorService
Thread를 직접 만들면 생성 비용, 개수 제한, 종료 시점을 모두 직접 다뤄야 합니다. ExecutorService는 이 문제를 thread pool로 관리합니다. 필요한 thread 수를 미리 정해 두고, 작업을 큐에 넣으면 빈 thread가 순서대로 처리합니다.
// ❌ 요청마다 새 Thread — 개수 제어 없음
for (Request req : requests) {
new Thread(() -> handle(req)).start(); // 요청이 많으면 thread가 폭발
}
// ✅ 고정 pool — 동시에 최대 4개 thread만 사용
ExecutorService pool = Executors.newFixedThreadPool(4);
for (Request req : requests) {
pool.submit(() -> handle(req)); // 나머지는 큐에서 대기
}
pool.shutdown();Callable과 Future: 결과를 받아오는 비동기 작업
Runnable은 반환값이 없지만, Callable<V>는 결과를 반환하거나 checked exception을 던질 수 있습니다. submit()은 Callable을 큐에 넣고 Future<V>를 반환합니다. Future는 나중에 결과를 기다리거나, 완료 여부를 확인하거나, 취소할 수 있는 핸들입니다.
Callable<String> task = () -> {
// 비동기 계산
return fetchDataFromDb();
};
Future<String> future = pool.submit(task);
// 다른 작업을 먼저 하고...
doOtherWork();
// 결과가 필요할 때 기다림
String result = future.get(); // 준비될 때까지 blockinvokeAll — 여러 작업을 병렬로 제출하고 전부 기다리기
submit()을 여러 번 반복해 Future 리스트를 관리하는 것보다, invokeAll()을 쓰면 작업 컬렉션을 한 번에 제출하고 모두 완료될 때까지 기다린 뒤 결과 리스트를 받을 수 있습니다.
List<Callable<String>> tasks = List.of(
() -> fetchFromServiceA(),
() -> fetchFromServiceB(),
() -> fetchFromServiceC()
);
// 모두 병렬 실행, 전부 완료될 때까지 block
List<Future<String>> futures = pool.invokeAll(tasks);
for (Future<String> f : futures) {
System.out.println(f.get()); // 이미 완료된 상태
}shutdown과 작업 완료 보장
shutdown()은 새 작업을 받지 않고 이미 제출된 작업이 모두 끝나면 pool을 정리합니다. shutdownNow()는 실행 중인 작업을 중단 시도합니다. shutdown() 없이 프로그램이 끝나면 pool thread가 종료되지 않아 프로그램이 멈추지 않을 수 있습니다.
pool.shutdown(); // 새 작업 접수 중단, 기존 작업 완료 대기
pool.awaitTermination(30, TimeUnit.SECONDS); // 최대 30초 대기Runnable vs Callable
결과가 필요 없으면 Runnable, 결과가 필요하거나 checked exception을 다뤄야 하면 Callable이 자연스럽습니다. 반환값이 필요한데 Runnable을 쓰면 바깥 공유 상태를 수정하는 식으로 코드가 틀어지기 쉽습니다.
// ✅ 결과가 필요 없는 작업
pool.execute(() -> sendMetric());
// ✅ 결과가 필요한 작업
Future<Report> reportFuture = pool.submit(() -> buildReport());
Report report = reportFuture.get();선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 반환값 없는 비동기 작업 | Runnable + execute() |
| 반환값 있는 비동기 작업 | Callable + submit() |
| 여러 작업 병렬 실행 + 전부 대기 | invokeAll(tasks) |
| 결과 대기 및 완료 확인 | Future.get() |
| 고정 thread 수 관리 | Executors.newFixedThreadPool(n) |
| pool 정리 | shutdown() + awaitTermination() |
주의할 점
Future.get()은 결과가 준비될 때까지 현재 thread를 block합니다. 모든 곳에서 즉시 get()을 호출하면 비동기 효과가 없습니다.
// ❌ 제출 직후 바로 get() — 결국 순차 실행과 동일
Future<String> f1 = pool.submit(() -> fetchA());
String a = f1.get(); // 여기서 block
Future<String> f2 = pool.submit(() -> fetchB());
String b = f2.get(); // 여기서 block
// ✅ 모두 제출 후 한꺼번에 결과 수집 — 병렬 실행
Future<String> f1 = pool.submit(() -> fetchA());
Future<String> f2 = pool.submit(() -> fetchB());
String a = f1.get(); // fetchA, fetchB 동시 진행 중
String b = f2.get();shutdown()을 빼먹으면 작업이 끝나도 pool thread가 살아 있어 프로그램이 안 내려갈 수 있습니다.
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> doWork());
// ❌ 종료 누락
// 프로그램이 계속 살아 있을 수 있음
// ✅ 종료 정리
pool.shutdown();
pool.awaitTermination(10, TimeUnit.SECONDS);참고 링크
3 sources