JDK 5.0부터 Java는 Runnable 인터페이스의 한계를 보완하고 스레드 작업의 결과 반환 및 예외 처리를 강화하기 위해 Callable 인터페이스와 FutureTask 클래스를 도입했습니다. 이들을 활용하면 스레드가 비동기적으로 작업을 수행하고, 작업 완료 후 결과를 호출 스레드로 전달할 수 있는 보다 유연한 멀티스레드 프로그래밍이 가능해집니다.
Callable 및 FutureTask를 사용한 스레드 생성 절차
- Callable 구현: 수행할 비동기 작업을 정의하는
Callable인터페이스를 구현하는 클래스를 작성합니다. 이 클래스는 제네릭 타입으로 작업의 반환 타입을 지정하며,call()메서드를 오버라이드하여 실제 스레드 로직을 구현합니다.call()메서드는 작업 결과를 반환하고 예외를 던질 수 있습니다. - FutureTask 캡슐화: 생성된
Callable객체를FutureTask객체로 캡슐화합니다.FutureTask는Runnable인터페이스를 구현하므로, 표준Thread클래스의 생성자에 전달될 수 있습니다. 또한,FutureTask는 스레드 작업의 상태를 관리하고, 작업 완료 후get()메서드를 통해 결과를 조회할 수 있는 핵심 기능을 제공합니다. - Thread 생성 및 시작:
FutureTask객체를Thread클래스의 생성자에 전달하여 새로운 스레드를 생성합니다. 이후Thread객체의start()메서드를 호출하여 스레드를 시작하고call()메서드에 정의된 작업을 실행합니다. - 결과 조회: 스레드 작업이 완료된 후,
FutureTask객체의get()메서드를 호출하여call()메서드가 반환한 결과를 가져옵니다.get()메서드는 작업이 완료될 때까지 호출 스레드를 블록(대기)합니다.
주요 특징 및 고려사항
- 결과 반환:
Callable은call()메서드를 통해 특정 타입의 결과를 반환할 수 있어, 스레드 간의 데이터 교환이 용이합니다. - 예외 처리:
call()메서드에서 발생한 예외는FutureTask의get()메서드 호출 시ExecutionException으로 래핑되어 전달됩니다. - 유연성:
Callable은 인터페이스이므로, 구현 클래스가 다른 클래스를 상속받거나 여러 인터페이스를 구현할 수 있어 코드의 확장성이 좋습니다. - 복잡성: 단순한
Runnable방식에 비해Callable과FutureTask를 함께 사용해야 하므로 초기 코딩의 복잡도가 다소 증가할 수 있습니다.
코드 예제: 단일 작업 실행
다음은 주어진 상한까지의 합을 계산하는 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("모든 병렬 작업이 완료되었습니다.");
}
}