본문 바로가기
Language/Java

[JAVA] 비동기 프로그래밍의 완성 : Callable과 Future 인터페이스 심층 분석

by Papa Martino V 2026. 1. 21.
728x90

Callable과 Future 인터페이스
Callable과 Future 인터페이스

 

자바 멀티쓰레딩의 세계에서 Runnable은 가장 오래된 친구와 같습니다. 하지만 Runnable에는 치명적인 단점이 하나 있습니다. 바로 작업을 수행한 뒤 그 '결과값'을 반환할 수 없고, 체크 예외(Checked Exception)를 던질 수도 없다는 점입니다. 이러한 한계를 극복하고 현대적인 비동기 처리 모델을 완성하기 위해 JDK 5부터 도입된 것이 바로 CallableFuture입니다. 이 글에서는 단순히 두 인터페이스의 정의를 넘어, 왜 우리가 실무에서 이들을 사용해야 하는지, 그리고 비동기 연산의 결과를 어떻게 안전하게 수확할 수 있는지 전문가의 관점에서 상세히 설명하겠습니다.

1. Callable과 Future: 비동기 작업의 주문과 영수증

Callable과 Future의 관계는 식당에서의 주문 시스템과 매우 흡사합니다. Callable이 "주방에 전달할 요리법(작업 내용)"이라면, Future는 요리가 나올 때까지 들고 있는 "대기 번호표(영수증)"와 같습니다.

Callable 인터페이스

Runnable과 달리 call() 메서드를 사용하며, 제네릭 타입을 통해 반환 타입을 지정할 수 있습니다. 또한 메서드 시그니처에 throws Exception이 포함되어 있어 예외 처리가 훨씬 유연합니다.

Future 인터페이스

비동기 작업의 결과값을 담는 컨테이너입니다. 작업이 완료되었는지 확인하고, 진행 중인 작업을 취소하거나, 최종적으로 결과물을 가져오는 메서드들을 제공합니다.


2. Runnable vs Callable 한눈에 비교하기

두 인터페이스의 차이점을 명확히 이해하면 적재적소에 맞는 도구를 선택할 수 있습니다.

비교 항목 Runnable Callable
패키지 java.lang java.util.concurrent
실행 메서드 run() call()
반환값 (Return) 없음 (void) 있음 (Generic T)
예외 처리 체크 예외 처리 불가 예외 던지기 가능 (throws Exception)
도입 시점 JDK 1.0 JDK 1.5

3. Future 인터페이스의 주요 기능

작업을 던져놓고 잊어버리는 것이 아니라, 추후에 결과에 개입할 수 있는 다양한 수단을 제공합니다.

  • get(): 작업이 완료될 때까지 쓰레드를 블로킹(Blocking)하고 결과를 가져옵니다.
  • isDone(): 작업이 완료되었는지 여부를 확인합니다.
  • cancel(boolean): 작업을 취소합니다.
  • isCancelled(): 작업이 취소되었는지 확인합니다.

4. 실무 예제: 복잡한 계산 결과 받기 (Sample Example)

다음은 1부터 100까지의 합을 비동기적으로 계산하고 그 결과를 받아오는 코드입니다.

import java.util.concurrent.*;

public class CallableFutureDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        // 1. Callable 작업 정의
        Callable<Integer> task = () -> {
            System.out.println("계산 시작...");
            Thread.sleep(2000); // 복잡한 연산 시뮬레이션
            int sum = 0;
            for (int i = 1; i <= 100; i++) sum += i;
            return sum;
        };

        // 2. 작업 제출 및 Future 영수증 수령
        Future<Integer> future = executor.submit(task);

        System.out.println("메인 쓰레드는 다른 일을 할 수 있습니다.");

        try {
            // 3. 결과 가져오기 (결과가 나올 때까지 기다림)
            Integer result = future.get(); 
            System.out.println("계산 결과: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
    }
}

5. 전문가의 조언: get() 호출 시 주의사항

Future.get()은 결과가 반환될 때까지 현재 쓰레드를 멈추게(Blocking) 만듭니다. 만약 비동기 작업이 무한 루프에 빠지거나 응답이 없다면 메인 쓰레드도 영원히 멈출 수 있습니다. 이를 방지하기 위해 실무에서는 반드시 타임아웃이 설정된 get(long timeout, TimeUnit unit)을 사용할 것을 권장합니다. 또한, 더 복잡한 비동기 파이프라인(작업이 끝나면 다음 작업을 자동 실행 등)이 필요하다면 JDK 8부터 도입된 CompletableFuture를 학습해 보시는 것도 좋습니다.


내용 출처 및 참고 자료

  • Oracle Java SE 21 Documentation: Interface Callable<V>
  • Oracle Java SE 21 Documentation: Interface Future<V>
  • Java Concurrency in Practice (Brian Goetz) - Chapter 6. Executor Framework
  • Baeldung: Java Callable vs Runnable
728x90