Callable 및 FutureTask 인터페이스를 활용한 Java 스레드 구현

JDK 5.0부터 Java는 Runnable 인터페이스의 한계를 보완하고 스레드 작업의 결과 반환 및 예외 처리를 강화하기 위해 Callable 인터페이스와 FutureTask 클래스를 도입했습니다. 이들을 활용하면 스레드가 비동기적으로 작업을 수행하고, 작업 완료 후 결과를 호출 스레드로 전달할 수 있는 보다 유연한 멀티스레드 프로그래밍이 가능해집니다.

Callable 및 FutureTask를 사용한 스레드 생성 절차

  1. Callable 구현: 수행할 비동기 작업을 정의하는 Callable 인터페이스를 구현하는 클래스를 작성합니다. 이 클래스는 제네릭 타입으로 작업의 반환 타입을 지정하며, call() 메서드를 오버라이드하여 실제 스레드 로직을 구현합니다. call() 메서드는 작업 결과를 반환하고 예외를 던질 수 있습니다.
  2. FutureTask 캡슐화: 생성된 Callable 객체를 FutureTask 객체로 캡슐화합니다. FutureTaskRunnable 인터페이스를 구현하므로, 표준 Thread 클래스의 생성자에 전달될 수 있습니다. 또한, FutureTask는 스레드 작업의 상태를 관리하고, 작업 완료 후 get() 메서드를 통해 결과를 조회할 수 있는 핵심 기능을 제공합니다.
  3. Thread 생성 및 시작: FutureTask 객체를 Thread 클래스의 생성자에 전달하여 새로운 스레드를 생성합니다. 이후 Thread 객체의 start() 메서드를 호출하여 스레드를 시작하고 call() 메서드에 정의된 작업을 실행합니다.
  4. 결과 조회: 스레드 작업이 완료된 후, FutureTask 객체의 get() 메서드를 호출하여 call() 메서드가 반환한 결과를 가져옵니다. get() 메서드는 작업이 완료될 때까지 호출 스레드를 블록(대기)합니다.

주요 특징 및 고려사항

  • 결과 반환: Callablecall() 메서드를 통해 특정 타입의 결과를 반환할 수 있어, 스레드 간의 데이터 교환이 용이합니다.
  • 예외 처리: call() 메서드에서 발생한 예외는 FutureTaskget() 메서드 호출 시 ExecutionException으로 래핑되어 전달됩니다.
  • 유연성: Callable은 인터페이스이므로, 구현 클래스가 다른 클래스를 상속받거나 여러 인터페이스를 구현할 수 있어 코드의 확장성이 좋습니다.
  • 복잡성: 단순한 Runnable 방식에 비해 CallableFutureTask를 함께 사용해야 하므로 초기 코딩의 복잡도가 다소 증가할 수 있습니다.

코드 예제: 단일 작업 실행

다음은 주어진 상한까지의 합을 계산하는 Callable 구현과 이를 메인 스레드에서 실행하고 결과를 받는 예제입니다.

1. 계산 작업을 정의하는 Callable 구현체

package com.example.concurrency;

import java.util.concurrent.Callable;

// 제네릭으로 call() 메서드의 반환 타입을 Long으로 지정합니다.
public class SumCalculatorCallable implements Callable<Long> {

    private final int upperLimit;

    public SumCalculatorCallable(int upperLimit) {
        this.upperLimit = upperLimit;
    }

    /**
     * 스레드에서 실행될 실제 계산 로직을 정의하고 합계 결과를 반환합니다.
     */
    @Override
    public Long call() throws Exception {
        long currentSum = 0;
        for (int i = 1; i <= upperLimit; i++) {
            currentSum += i;
        }
        // 필요시 인위적인 작업 지연을 추가할 수 있습니다.
        // Thread.sleep(50); 
        return currentSum;
    }
}

2. 메인 스레드에서 Callable 작업 실행 및 결과 대기

package com.example.concurrency;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ExecutionException;

public class SingleTaskExecutionDemo {

    public static void main(String[] args) {
        // 1. Callable 작업 객체를 생성합니다.
        Callable<Long> task = new SumCalculatorCallable(100);

        // 2. Callable 작업을 FutureTask로 감쌉니다.
        // FutureTask는 Runnable 인터페이스를 구현하므로 Thread에 전달 가능합니다.
        // 또한, 비동기 작업의 결과를 조회하는 get() 메서드를 제공합니다.
        FutureTask<Long> futureCalculation = new FutureTask<>(task);

        // 3. FutureTask를 이용하여 새로운 스레드를 생성합니다.
        Thread computationThread = new Thread(futureCalculation, "Sum-Computer-1");

        // 4. 스레드를 시작하여 작업을 비동기적으로 실행합니다.
        computationThread.start();

        System.out.println("백그라운드 스레드에서 합계 계산 중...");

        try {
            // 5. FutureTask의 get() 메서드를 호출하여 작업 결과를 기다리고 가져옵니다.
            // get()은 작업이 완료될 때까지 현재 스레드를 블록(대기)합니다.
            Long result = futureCalculation.get();
            System.out.println("계산 스레드의 최종 결과: " + result);
        } catch (InterruptedException e) {
            System.err.println("메인 스레드가 인터럽트되었습니다: " + e.getMessage());
            Thread.currentThread().interrupt(); // 인터럽트 상태를 복원합니다.
        } catch (ExecutionException e) {
            System.err.println("비동기 작업 실행 중 예외 발생: " + e.getCause().getMessage());
        }
    }
}

코드 예제: 여러 작업 병렬 실행 및 결과 대기

여러 FutureTask 인스턴스를 생성하여 다수의 스레드를 시작하고, 각 스레드의 결과를 개별적으로 기다릴 수 있습니다.

package com.example.concurrency;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ExecutionException;

public class ParallelTaskExecutor {

    public static void main(String[] args) {
        // 첫 번째 Callable 작업 및 FutureTask 생성
        Callable<Long> longTask = new SumCalculatorCallable(50000); // 더 큰 수 계산
        FutureTask<Long> futureLongResult = new FutureTask<>(longTask);
        Thread threadA = new Thread(futureLongResult, "Heavy-Calculator");
        threadA.start();

        // 두 번째 Callable 작업 및 FutureTask 생성
        Callable<Long> shortTask = new SumCalculatorCallable(1000); // 더 작은 수 계산
        FutureTask<Long> futureShortResult = new FutureTask<>(shortTask);
        Thread threadB = new Thread(futureShortResult, "Light-Calculator");
        threadB.start();

        System.out.println("두 개의 계산 스레드가 백그라운드에서 동시에 실행 중입니다.");

        try {
            // 첫 번째 (무거운) 작업의 결과 대기 및 출력
            // futureLongResult.get()은 threadA가 완료될 때까지 현재 스레드를 블록합니다.
            Long resultA = futureLongResult.get();
            System.out.println("스레드 '" + threadA.getName() + "' 결과: " + resultA);
        } catch (InterruptedException | ExecutionException e) {
            System.err.println("첫 번째 작업 처리 중 오류: " + e.getMessage());
            Thread.currentThread().interrupt();
        }

        try {
            // 두 번째 (가벼운) 작업의 결과 대기 및 출력
            // futureShortResult.get()은 threadB가 완료될 때까지 현재 스레드를 블록합니다.
            Long resultB = futureShortResult.get();
            System.out.println("스레드 '" + threadB.getName() + "' 결과: " + resultB);
        } catch (InterruptedException | ExecutionException e) {
            System.err.println("두 번째 작업 처리 중 오류: " + e.getMessage());
            Thread.currentThread().interrupt();
        }

        System.out.println("모든 병렬 작업이 완료되었습니다.");
    }
}

태그: java Callable FutureTask Multithreading concurrency

7월 26일 12:48에 게시됨