-
java.util.concurrent.FutureJAVA 2025. 8. 7. 18:07728x90반응형
1. Future란 무엇일까?
Java의 Future Interface는 비동기 연산의 결과를 나타내는 핵심 컴포넌트입니다.
Future는 Multi Thread 환경에서 비동기적으로 실행되는 작업의 결과를 관리하기 쉽게 해줍니다.
- 시간이 오래 걸리는 작업을 Background에서 실행 하는 경우
- 여러 작업을 병렬로 실행하고 결과를 기다리는 경우
- 작업의 진행 상태를 확인하거나 취소해야 하는 경우
2. Future의 기본 개념
2-1. 비동기 연산의 생명 주기
Future로 관리하는 비동기 작업은 다음과 같이 나타낼 수 있습니다.
생성 -> 실행 -> 완료 / 취소 / 예외
2-2. Generic Type
Future는 Generic Interface이기 때문에 반환될 결과의 타입을 지정할 수 있습니다.
Future<String> stringResult; // 문자열을 반환하는 Future Future<Integer> intResult; // 정수를 반환하는 Future Future<Void> nullResult; // 결과 값이 없는 비동기 작업 (get()은 null 반환) Future<?> wildcardResult; // 특정할 수 없는 타입의 결과를 반환하는 Future
3. Future 주요 메소드
Future Interface에는 5개의 핵심 메소드를 제공합니다.
3-1. 결과 조회
// 비동기 작업이 완료될 때까지 기다렸다가 결과를 반환합니다. V get() throws InterruptedException, ExecutionException // 비동기 작업이 완료될 때까지 최대 timeout 시간만큼 기다린 후 결과를 반환합니다. V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
3-2. 상태 확인
// 작업이 완료되기 전 cancel 메소드로 취소되었다면 true를 반환합니다. boolean isCancelled() // 작업이 정상 종료, 예외, 취소로 인해 완료되었다면 true를 반환합니다. boolean isDone()
3-3. 작업 취소
// 작업을 취소하려고 시도하는 메서드입니다. // 작업이 이미 완료되었거나 이미 취소된 경우에는 false를 반환합니다. // 작업이 실행 중이며 mayInterruptIfRunning이 true이면 실행 중인 스레드를 interrupt하여 중단을 시도합니다. // 취소에 성공하면 true, 실패하면 false를 반환합니다. boolean cancel(boolean mayInterruptIfRunning);
4. 기초 예제
4-1. 기본적인 Future 사용 예제
ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(new Callable<String>() { @Override public String call() throws Exception { Thread.sleep(2000); // 2초 작업 시뮬레이션 return "작업 완료!"; } }); System.out.println("작업 시작..."); try { String result = future.get(); System.out.println("결과: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown(); // 작업 시작... // 결과: 작업 완료!
4-2. Lamda 표현식을 사용한 예제
ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(() -> { Thread.sleep(1000); return "치킨 + 피자"; }); // 🎉 isDone 메소드가 논블로킹이라는걸 확인할 수 있습니다. while (!future.isDone()) { System.out.println("아직 다이어트 중..."); Thread.sleep(500); } try { String result = future.get(); System.out.println("저녁 메뉴는 " + result + "입니다."); } catch (Exception e) { e.printStackTrace(); } executor.shutdown(); // 아직 다이어트 중... // 아직 다이어트 중... // 아직 다이어트 중... // 저녁 메뉴는 치킨 + 피자입니다.
5. ExecutorService와 Future
5-1. ExecutorService를 활용한 Future 반환 예제
ExecutorService executor = Executors.newFixedThreadPool(3); // Callable: 작업이 직접 결과를 반환 (Future<String>) Future<String> callableResult = executor.submit(() -> { return "Callable 결과"; }); // Runnable + 결과값: 작업은 결과 없이 실행, 지정된 값 반환 (Future<String>) Future<String> runnableResult = executor.submit(() -> { System.out.println("Runnable 실행"); }, "Runnable 완료"); // Runnable만: 결과 값 없음, get()은 null 반환 (Future<?>) Future<?> voidResult = executor.submit(() -> { System.out.println("결과 없는 작업"); }); try { System.out.println(callableResult.get()); // 출력: Callable 결과 System.out.println(runnableResult.get()); // 출력: Runnable 완료 if (voidResult.get() == null) { System.out.println("null 반환"); // 출력: null 반환 } } catch (Exception e) { e.printStackTrace(); } executor.shutdown(); // submit() 호출 시 작업을 스레드 풀의 작업 큐에 추가합니다. // submit() 자체는 즉시 Future 객체를 반환하고 실제 작업의 실행은 스레드 풀의 스케줄링에 따라 비동기적으로 처리됩니다. // 즉, 출력은 스레드 풀이 작업을 처리하는 시점에서 발생합니다. // Runnable 실행 // Callable 결과 // Runnable 완료 // 결과 없는 작업 // null 반환
5-2. 병렬 처리 예제
ExecutorService executor = Executors.newFixedThreadPool(3); List<Future<Integer>> futures = new ArrayList<>(); // 여러 작업을 병렬로 시작 for (int i = 1; i <= 5; i++) { final int taskId = i; // 병렬적으로 3개의 스레드로 5번 작업을 합니다. Future<Integer> future = executor.submit(() -> { System.out.println("작업 >> " + taskId + " 시작됨 (소요시간: " + taskId + "초)"); Thread.sleep(taskId * 1000); return taskId * taskId; }); futures.add(future); } // 모든 결과 수집 System.out.println("모든 작업 결과:"); for (int i = 0; i < futures.size(); i++) { try { // Future의 get() 메소드는 블로킹이라는 걸 확인할 수 있습니다. Integer result = futures.get(i).get(); System.out.println("작업 " + (i + 1) + " 결과: " + result); } catch (Exception e) { System.out.println("작업 " + (i + 1) + " 실패: " + e.getMessage()); } } executor.shutdown(); // 모든 작업 결과: // 작업 >> 3 시작됨 (소요시간: 3초) // 작업 >> 1 시작됨 (소요시간: 1초) // 작업 >> 2 시작됨 (소요시간: 2초) // 작업 >> 4 시작됨 (소요시간: 4초) // 작업 1 결과: 1 // 작업 >> 5 시작됨 (소요시간: 5초) // 작업 2 결과: 4 // 작업 3 결과: 9 // 작업 4 결과: 16 // 작업 5 결과: 25
6. FutureTask
FutureTask는 Future Interface와 Runnable Interface를 구현합니다.
6-1. FutureTask 기본 사용 예제
FutureTask는 한번 실행되면 상태가 완료 or 취소로 결정되기 때문에 재 사용할 수 없습니다.
즉, 아래 예제를 섞는 경우 두번째 작업은 무시하게 됩니다.
// FutureTask 생성 FutureTask<String> futureTask = new FutureTask<>(() -> { System.out.println("작업 실행..."); Thread.sleep(2000); return "FutureTask 완료"; }); Thread thread = new Thread(futureTask); thread.start(); try { String result = futureTask.get(); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } // ----------------------------------------- // FutureTask 생성 FutureTask<String> futureTask = new FutureTask<>(() -> { System.out.println("작업 실행..."); Thread.sleep(2000); return "FutureTask 완료"; }); ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(futureTask); try { String result = futureTask.get(); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } executor.shutdown();
6-3. FutureTask 커스터마이징
public class FutureTaskCustom<V> extends FutureTask<V> { private final String taskName; public FutureTaskCustom(Callable<V> callable, String taskName) { super(callable); this.taskName = taskName; } @Override // 작업이 완료되면 FutureTask가 호출하는 Callback 메소드 입니다. // FutureTask 내부에서 이미 정의되어있습니다. protected void done() { if (isCancelled()) { System.out.println("==> " + taskName + " [취소됨]"); } else { try { get(); System.out.println("==> " + taskName + " [성공적으로 완료됨]"); } catch (Exception e) { System.out.println("==> " + taskName + " [예외 발생으로 완료됨]"); } } } } // --------------------- TTTs<Integer> task = new TTTs<>(() -> { Thread.sleep(1000); return 100; }, "계산 작업"); ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(task); try { Integer result = task.get(); System.out.println("결과: " + result); } catch (Exception e) { e.printStackTrace(); } executor.shutdown(); ==> 계산 작업 [성공적으로 완료됨] 결과: 100
7. 예외처리
7-1. ExecutionException 처리
ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(() -> { if (Math.random() > 0.5) { throw new RuntimeException("무작위 오류 발생!"); } return "성공!"; }); try { // 예외발생시 ExecutionException으로 감싸서 전달합니다. String result = future.get(); System.out.println("결과: " + result); } catch (ExecutionException e) { // ExecutionException에서 실제 Exception을 가져옵니다. Throwable cause = e.getCause(); // 실제 원본 메시지 추출합니다. System.out.println("작업 중 예외 발생: " + cause.getMessage()); } catch (InterruptedException e) { System.out.println("대기 중 인터럽트 발생"); // InterruptedException을 catch하면 현재 스레드의 인터럽트 상태는 false로 초기화됩니다. // 현재 메서드에서는 인터럽트를 완전히 처리할 수 없으므로 이 스레드를 호출한 상위 코드(호출한 곳)가 인터럽트 발생 사실을 알수 있도록 인터럽트 상태를 설정(true)해주는 것 입니다. Thread.currentThread().interrupt(); } executor.shutdown();
상위 코드에서 Thread.currentThread().isInterrupted()를 통해 Interrupt 상태를 확인할 수 있으며 true라면 실제로 종료된 것입니다.
7-3. 예외 래핑
ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(() -> { try { Thread.sleep(1000); throw new IOException("파일을 찾을 수 없습니다"); } catch (IOException e) { // IOException을 RuntimeException으로 감싸서 전달합니다. throw new RuntimeException(e); } }); try { future.get(); } catch (ExecutionException e) { // e.getCause()를 호출하면 작업 스레드가 던졌던 RuntimeException이 나옵니다. Throwable cause = e.getCause(); if (cause instanceof RuntimeException && cause.getCause() instanceof IOException) { // RuntimeException 안에 있던 진짜 원인(IOException)을 추출합니다. IOException originalException = (IOException) cause.getCause(); System.out.println("IO 예외: " + originalException.getMessage()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } executor.shutdown();
8. 다양한 예제
8-1. Timeout 취소
ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(() -> { Thread.sleep(5000); return "늦은 결과"; }); try { String result = future.get(3, TimeUnit.SECONDS); System.out.println("결과: " + result); } catch (TimeoutException e) { System.out.println("시간 초과! 작업을 취소합니다."); boolean cancelled = future.cancel(true); System.out.println("취소 성공: " + cancelled); } catch (Exception e) { e.printStackTrace(); } executor.shutdown();
8-2. 취소
ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(() -> { for (int i = 0; i < 10; i++) { // 인터럽트 상태를 직접 확인하는 부분 if (Thread.currentThread().isInterrupted()) { System.out.println("작업이 인터럽트되었습니다."); return "인터럽트됨"; } try { Thread.sleep(1000); // InterruptedException 발생 가능 } catch (InterruptedException e) { System.out.println("슬립 중 인터럽트 감지"); Thread.currentThread().interrupt(); // 인터럽트 상태 복원 return "인터럽트됨"; } System.out.println("작업 진행 중... " + i); } return "완료됨"; }); Thread.sleep(2000); // Interrupt를 발생시킵니다. boolean cancelled = future.cancel(true); System.out.println("취소 요청 결과: " + cancelled); try { String result = future.get(); System.out.println("최종 결과: " + result); } catch (CancellationException e) { System.out.println("작업이 취소되었습니다."); } catch (Exception e) { e.printStackTrace(); } executor.shutdown();
9. 실제 예제
9-1. 웹크롤링 병렬 예제
public class WebCrawlerExample { private final ExecutorService executor = Executors.newFixedThreadPool(5); public void crawlUrls(List<String> urls) { List<Future<CrawlResult>> futures = new ArrayList<>(); // 모든 크롤링 작업을 스레드 풀에 전달합니다. // submit은 작업을 맡기자마자 바로 다음 코드를 실행하므로 모든 작업이 거의 동시에 시작됩니다. System.out.println("모든 URL에 대한 크롤링 작업을 동시에 제출합니다..."); for (String url : urls) { Future<CrawlResult> future = executor.submit(() -> crawlSingleUrl(url)); futures.add(future); } // 전달된 작업들의 결과를 순서대로 확인합니다. System.out.println("각 작업의 결과를 확인합니다 (최대 10초 대기)..."); for (int i = 0; i < futures.size(); i++) { Future<CrawlResult> future = futures.get(i); String url = urls.get(i); try { // future.get()을 호출하되, 최대 10초까지만 결과를 기다립니다. CrawlResult result = future.get(10, TimeUnit.SECONDS); System.out.println("✅ URL " + url + " 크롤링 성공: " + result); } catch (TimeoutException e) { // 10초를 초과하면 TimeoutException이 발생합니다. System.out.println("❌ URL " + url + " 작업 시간 초과 (타임아웃)"); // 타임아웃된 작업에게 더 이상 실행할 필요가 없다고 인터럽트 신호를 보냅니다. future.cancel(true); } catch (Exception e) { // 그 외 작업 실행 중 발생한 예외 처리 System.out.println("🔥 URL " + url + " 처리 중 오류 발생: " + e.getMessage()); } } executor.shutdown(); } // 단일 URL을 크롤링하는 메서드 (시뮬레이션) private CrawlResult crawlSingleUrl(String url) throws InterruptedException { System.out.println("... " + url + " 크롤링 시작"); Thread.sleep((long) (Math.random() * 12000)); return new CrawlResult(url, "크롤링 완료"); } static class CrawlResult { final String url; final String content; CrawlResult(String url, String content) { this.url = url; this.content = content; } @Override public String toString() { return "CrawlResult{url='" + url + "', content='" + content + "'}"; } } public static void main(String[] args) { WebCrawlerExample crawler = new WebCrawlerExample(); List<String> urls = Arrays.asList( "https://www.google.com", "https://www.waug.com", "https://www.whitehouse.com", "https://www.naver.com", "https://www.daum.net" ); crawler.crawlUrls(urls); } }
10. Future 한계와 대안
10-1. Future 한계점
- 블로킹 방식: get() 메서드는 블로킹 방식으로만 동작합니다.
- 콜백 부재: 완료 시 자동으로 실행될 콜백 메서드가 없습니다.
- 조합 어려움: 여러 Future를 조합하기 복잡합니다.
- 예외 처리: ExecutionException으로 래핑되어 처리가 복잡합니다.
10-2. CompletableFuture(Java 8+)
CompletableFuture<String> future = CompletableFuture // 별도의 스레드에서 실행 (ForkJoinPool의 공용 스레드) .supplyAsync(() -> { System.out.println(" [1] supplyAsync: 'Hello'를 생성 중... " + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "Hello"; }) .thenApply(result -> { System.out.println(" [2] thenApply: '" + result + "'에 ' World'를 붙이는 중..." + Thread.currentThread().getName()); return result + " World"; }) // 순서에 맞게 위에 실행 후 실행되는 체이닝 메소드 입니다. .thenCompose(result -> { System.out.println(" [3] thenCompose: '" + result + "'에 '!'를 붙이는 새 작업을 시작..." + Thread.currentThread().getName()); return CompletableFuture.supplyAsync(() -> result + "!"); // 결과: "Hello World!" }) // 위에서 예외발생시 이 단계가 실행됩니다. .exceptionally(throwable -> { System.out.println(" [!] exceptionally: 오류 발생! " + throwable.getMessage()); return "Error: " + throwable.getMessage(); }); // 논 블로킹 방식 future.thenAccept(result -> { System.out.println("\n [4] thenAccept (콜백): 최종 결과가 나왔습니다! -> " + result + " :: " + Thread.currentThread().getName()); }); System.out.println("메인 스레드: 콜백을 등록했으니 다른 일을 계속할 수 있습니다." + Thread.currentThread().getName()); // 블로킹 방식 try { System.out.println("메인 스레드: 이제 get()으로 최종 결과가 올 때까지 기다립니다..." + Thread.currentThread().getName()); String finalResult = future.get(); System.out.println(" [5] get() (블로킹): 최종 결과를 받았습니다. -> " + finalResult + " :: " + Thread.currentThread().getName()); } catch (Exception e) { e.printStackTrace(); } // [1] supplyAsync: 'Hello'를 생성 중... ForkJoinPool.commonPool-worker-1 // 메인 스레드: 콜백을 등록했으니 다른 일을 계속할 수 있습니다.main // 메인 스레드: 이제 get()으로 최종 결과가 올 때까지 기다립니다...main // [2] thenApply: 'Hello'에 ' World'를 붙이는 중...ForkJoinPool.commonPool-worker-1 // [3] thenCompose: 'Hello World'에 '!'를 붙이는 새 작업을 시작...ForkJoinPool.commonPool-worker-1 // [4] thenAccept (콜백): 최종 결과가 나왔습니다! -> Hello World! :: ForkJoinPool.commonPool-worker-2 // [5] get() (블로킹): 최종 결과를 받았습니다. -> Hello World! :: main
10-3. 2개의 Future 조합
CompletableFuture<String> future1 = CompletableFuture .supplyAsync(() -> { System.out.println("야식 조회 중... " + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) {} return "닭발"; }); CompletableFuture<Integer> future2 = CompletableFuture .supplyAsync(() -> { System.out.println("가격 조회 중... " + Thread.currentThread().getName()); try { Thread.sleep(1500); } catch (InterruptedException e) {} return 900; }); // 두 Future의 결과를 조합 // 늦게 끝난 스레드가 해당 작업을 진행합니다. CompletableFuture<String> combined = future1.thenCombine(future2, (food, price) -> { System.out.println("결과 조합 중... " + Thread.currentThread().getName()); return String.format("야식: %s, 가격: %d세", food, price); }); try { String result = combined.get(); System.out.println("최종 결과: " + result); } catch (Exception e) { e.printStackTrace(); } // 야식 조회 중... ForkJoinPool.commonPool-worker-1 // 가격 조회 중... ForkJoinPool.commonPool-worker-2 // 결과 조합 중... ForkJoinPool.commonPool-worker-1 // 최종 결과: 이름: 닭발, 나이: 19000세
10-4. 모든 Future 완료 대기
// 병렬 처리 CompletableFuture<String> userService = CompletableFuture .supplyAsync(() -> { System.out.println("사용자 서비스 호출..."); try { Thread.sleep((long)(Math.random() * 2000)); } catch (InterruptedException e) {} return "사용자 데이터"; }); CompletableFuture<String> orderService = CompletableFuture .supplyAsync(() -> { System.out.println("주문 서비스 호출..."); try { Thread.sleep((long)(Math.random() * 2000)); } catch (InterruptedException e) {} return "주문 데이터"; }); CompletableFuture<String> paymentService = CompletableFuture .supplyAsync(() -> { System.out.println("결제 서비스 호출..."); try { Thread.sleep((long)(Math.random() * 2000)); } catch (InterruptedException e) {} return "결제 데이터"; }); // allOf는 전달된 모든 CompletableFuture가 완료되면 알려줍니다. // 단, 이건 이벤트이기 때문에 void로 써야합니다. CompletableFuture<Void> allTasks = CompletableFuture.allOf( userService, orderService, paymentService ); // 3개 서비스가 모두 완료되면 이 블록이 실행됩니다. allTasks.thenRun(() -> { try { // 이 시점에는 모든 Future가 완료되었기 때문에 get()을 호출해도 기다림 없이 즉시 결과를 가져올 수 있습니다. String user = userService.get(); String order = orderService.get(); String payment = paymentService.get(); System.out.println("=== 모든 데이터 수집 완료 ==="); System.out.println("- " + user); System.out.println("- " + order); System.out.println("- " + payment); } catch (Exception e) { e.printStackTrace(); } }); // 가장 오래 걸리는 작업이 1.5초이므로 시간 내에 완료될 것입니다. try { allTasks.get(5, TimeUnit.SECONDS); System.out.println("대시보드 로딩 완료!"); } catch (Exception e) { System.out.println("타임아웃 또는 오류 발생: " + e.getMessage()); }
10-5. 가장 빠른 Future 반환
CompletableFuture<String> server1 = CompletableFuture .supplyAsync(() -> { try { Thread.sleep((long)(Math.random() * 3000)); } catch (InterruptedException e) {} return "서버1 응답"; }); CompletableFuture<String> server2 = CompletableFuture .supplyAsync(() -> { try { Thread.sleep((long)(Math.random() * 3000)); } catch (InterruptedException e) {} return "서버2 응답"; }); CompletableFuture<String> server3 = CompletableFuture .supplyAsync(() -> { try { Thread.sleep((long)(Math.random() * 3000)); } catch (InterruptedException e) {} return "서버3 응답"; }); // allOf랑 다르게 무엇이든 하나가 완료되면 끝입니다. CompletableFuture<Object> fastest = CompletableFuture.anyOf(server1, server2, server3); try { Object result = fastest.get(4, TimeUnit.SECONDS); System.out.println("가장 빠른 응답: " + result); // 나머지 작업들은 interrupt를 발생시켜 종료 합니다 server1.cancel(true); server2.cancel(true); server3.cancel(true); } catch (TimeoutException e) { System.out.println("모든 서버가 응답하지 않음"); } catch (Exception e) { e.printStackTrace(); }
11. 비교
특성 Future CompletableFuture 실행 방식 블로킹 (get()) 논블로킹 콜백 지원 예외 처리 ExecutionException 래핑 exceptionally() 메서드 콜백 없음 thenAccept(), thenRun() 등 여러 Future 조합 수동 처리 필요 allOf(), anyOf(), thenCombine() 성능 스레드 블로킹 논 블로킹 + ForkJoinPool 728x90반응형'JAVA' 카테고리의 다른 글
java.util.concurrent.locks (0) 2025.07.17 [Java] Thread에 대해 알아보자 (3) 2025.06.26 [Java] I/O 작동 원리 HDD 구조부터 시작하기 (0) 2025.05.29 [Java] 인코딩 디코딩 다시 이해하기 (0) 2025.05.08 [Java 파먹기] 정렬 기준 : Comparable & Comparator (1) 2025.04.17