[Java] Callable, Future 이해 및 사용예시

2025. 2. 26. 07:07·Java

Callable 이해하기

[Callable 은 왜 등장했는가?]

자바에서 멀티스레딩을 사용할 때 가장 기본적인 방법은 Runnable 인터페이스를 활용하는 것이다. 그러나 Runnable 인터페이스는 다음과 같은 한계를 가지고 있다.

  • 반환값을 가질 수 없음: Runnable의 run() 메서드는 반환값이 없는 void 타입이다. 따라서 실행 결과를 얻으려면 별도의 공유 변수 또는 콜백을 사용해야 한다.
  • 예외 처리가 어려움: Runnable은 checked exception을 명시적으로 던질 수 없다. run() 메서드는 throws 절을 가질 수 없으며, 내부에서 발생한 예외는 잡아서 처리해야 한다.

이러한 문제를 해결하기 위해 Java 5에서 Callable<T> 인터페이스가 도입되었다. java.util.concurrent.Callable 인터페이스는 별도의 스레드에서 실행할 수 있는 비동기 작업을 나타낸다. 예를 들어, Callable 객체를 ExecutorService에 submit 하면 비동기적으로 실행 후 반환값을 받아올 수 있다. (아래 사용 예시 참고)

 

[Callable 인터페이스는 무엇인가?]

Callable<T>는 멀티쓰레드 환경에서 쓰레드의 실행 결과값을 반환하기 위한 인터페이스다. java.util.concurrent 패키지에 속하며, Runnable과 유사하게 멀티스레드 환경에서 비동기 작업을 실행하기 위해 사용되지만, 다음과 같은 개선점을 제공한다.

  • 제네릭을 사용해 반환값을 가질 수 있다.
  • call() 메서드는 checked exception을 던질 수 있다.

[Callable 인터페이스 정의]

Java의 Callable 인터페이스는 함수형 인터페이스로 단 하나의 call() 메서드만을 포함하고 있다.

 

  • call() 메서드는 비동기 작업을 실행하는 데 사용되며, 결과값을 반환할 수 있고, 작업 실행 중 오류가 발생하면 예외를 던질 수 있다.
  • call() 메서드가 비동기적으로 실행될 경우, 결과값은 Java Future 객체를 통해 작업을 생성한 측에 전달된다.

 

[Callable 과 Runnable 은 각각 언제 사용해야 할까?]

Callable 과 Runnable은 모두 하나의 추상 메서드만 가지는 함수형 인터페이스이고, 별도의 스레드에서 실행될 작업을 나타낸다는 공통점이 있지만, 다음과 같은 차이점을 가진다.

  • Runnable 의 run() 메서드는 반환값이 없으며, 예외를 던질 수도 없다.
  • Callable은 call() 은 결과를 반환할 수 있으며, 예외 처리도 가능하다.

따라서 Callable은 단일 작업을 실행하고 결과를 반환해야 하는 경우 적합하며, Runnable은 결과값 반환 없이 장시간 실행되는 프로세스를 사용해야 할 경우 적합하다.

Future 이해하기

[Future 인터페이스란?]

Future 인터페이스는 비동기 작업의 결과를 나타내는 인터페이스다. 제공되는 메서드를 통해 작업이 완료되었는지 확인하고, 완료될 때까지 대기하며, 작업 결과를 가져올 수 있다.

 

Callable 에서 구현한 작업은 별도 쓰레드에서 비동기적으로 실행되기 때문에 언제 완료되어 값을 반환받을지 알 수 없다. 실행결과를 바로 받을 수 없으므로 미래에 완료될 Callable 의 반환값을 얻기 위해 Future 가 사용된다. 

  •  비동기 작업이 생성되면 Future 객체가 반환된다.
  • 비동기 작업이 완료되면 작업이 시작될 때 반환된 Future 객체를 통해 결과를 받아온다.

[Featue 인터페이스 정의]

Future 인터페이스에는 작업의 상태를 관리하고 결과를 가져오거나 취소할 수 있는 메서드들이 존재한다.

package java.util.concurrent;

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
  • get()
    • 연산이 완료될 때까지 블로킹 후 결과를 반환
    • 취소된 경우 CancellationException, 예외 발생 시 ExecutionException을 던짐
  • get(long timeout, TimeUnit unit)
    • timeout까지 기다렸다가 결과 반환 (초과 시 TimeoutException 발생)
  • isDone(),
    • 작업이 완료되었는지 여부를 확인
    • 작업이 취소되어 끝난 경우, 정상적으로 완료된 경우, 예외로 종료된 경우
  •  isCancelled()
    • cancel()이 호출된 후 작업이 성공적으로 취소되었는지 확인
    • 작업이 아직 실행 중이거나 정상 종료되었을 경우 false 반환
  • cancel(boolean mayInterruptIfRunning)
    • 작업을  취소하려고 시도
    • 작업이 이미 완료되었거나 취소된 경우 또는 다른 이유로 인해 취소할 수 없는 경우 아무 동작도 하지 않음
    • 작업이 이미 실행 중인 경우, mayInterruptIfRunning 매개변수는 현재 실행 중인 스레드를 인터럽트할지 여부를 결정
      • true일 경우: 작업을 실행하는 스레드가 인터럽트됨 (스레드를 중단하려고 시도)
      • false일 경우: 진행 중인 작업은 그대로 두고 완료될 때까지 실행
    • 작업이 실행되기 전 cancel 을 호출하면 해당 작업은 실행되지 않는다.
    • 반환 값은 작업이 실제로 취소되었는지를 보장하지 않기 때문에 작업이 취소되었는지 확인하려면 isCancelled()을 사용

해당 내용은 테스트를 통해 확인해보았다.

class FutureTest {

    ExecutorService executorService;

    @BeforeEach
    void setUp() {
        executorService = Executors.newSingleThreadExecutor();
    }

    @AfterEach
    void tearDown() {
        executorService.shutdown();
    }

    @Test
    void 작업이_지정된_시간_이내에_완료되지_않으면_TimeoutException이_발생한다() {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Future<String> future = executor.submit(() -> {
            Thread.sleep(5000);
            return "작업 완료";
        });

        assertThatThrownBy(() -> future.get(2, TimeUnit.SECONDS))
                .isInstanceOf(TimeoutException.class);

        executor.shutdown();
    }

    @Test
    void 취소된_작업에서_get을_호출하면_CancellationException이_발생한다() {
        Future<String> future = executorService.submit(callable());
        future.cancel(true);

        assertThatThrownBy(future::get)
                .isInstanceOf(CancellationException.class);
    }

    @Test
    void 초기_상태에서는_isCancelled가_false를_반환한다() {
        Future<String> future = executorService.submit(callable());

        assertThat(future.isCancelled()).isFalse();
    }

    @Test
    void 작업을_취소한_경우_isCancelled가_true를_반환한다() {
        Future<String> future = executorService.submit(callable());
        future.cancel(true);

        assertThat(future.isCancelled()).isTrue(); // 취소 후 isCancelled()는 true
    }

    @Test
    void 작업이_진행중이면_isDone이_false를_반환한다() {
        Future<String> future = executorService.submit(callable());
        assertThat(future.isDone()).isFalse(); // 실행 중이므로 false
    }

    @Test
    void 작업이_완료된_경우_isDone이_true를_반환한다() throws ExecutionException, InterruptedException {
        Future<String> future = executorService.submit(callable());
        future.get(); // 완료될 때까지 대기
        assertThat(future.isDone()).isTrue(); // 완료된 경우 true
    }

    @Test
    void 작업이_시작되기_전에_cancel_true를_호출하면_작업이_실행되지_않는다() throws InterruptedException {
        AtomicBoolean isRunning = new AtomicBoolean(false);

        Future<String> future = executorService.submit(() -> {
            isRunning.set(true); // 실행되었는지 확인용
            Thread.sleep(3000);
            return "Executed";
        });

        boolean isCanceled = future.cancel(true);

        Thread.sleep(500); // 작업이 실행되지 않았음을 보장하기 위해 대기
        assertThat(isCanceled).isTrue();
        assertThat(future.isCancelled()).isTrue();
        assertThat(future.isDone()).isTrue();
        assertThat(isRunning.get()).isFalse(); // 실행되지 않았음을 확인
    }

    @Test
    void 작업이_시작되기_전에_cancel_false를_호출하면_작업이_실행되지_않는다() throws InterruptedException {
        AtomicBoolean isRunning = new AtomicBoolean(false);

        Future<String> future = executorService.submit(() -> {
            isRunning.set(true); // 실행되었는지 확인용
            Thread.sleep(3000);
            return "Executed";
        });

        boolean isCanceled = future.cancel(false);

        Thread.sleep(500); // 작업이 실행되지 않았음을 보장하기 위해 대기
        assertThat(isCanceled).isTrue();
        assertThat(future.isCancelled()).isTrue();
        assertThat(future.isDone()).isTrue();
        assertThat(isRunning.get()).isFalse(); // 실행되지 않았음을 확인
    }

    @Test
    void 실행_중인_작업에_cancel_true를_호출하면_인터럽트된다() throws InterruptedException {
        AtomicBoolean isRunning = new AtomicBoolean(false);
        AtomicBoolean wasInterrupted = new AtomicBoolean(false);

        Future<String> future = executorService.submit(() -> {
            try {
                isRunning.set(true);
                while (!Thread.currentThread().isInterrupted()) {
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                wasInterrupted.set(true); // 인터럽트 감지
            }
            return "Executed";
        });

        Thread.sleep(500); // 작업이 시작되도록 대기
        future.cancel(true);
        Thread.sleep(500);

        assertThat(isRunning.get()).isTrue(); // 작업이 실행되었음을 확인
        assertThat(wasInterrupted.get()).isTrue(); // 인터럽트가 발생했는지 확인
        assertThat(future.isDone()).isTrue();
        assertThat(future.isCancelled()).isTrue();
    }

    @Test
    void 실행_중인_작업에_cancel_false를_호출하면_작업이완료될때까지_실행된다() throws InterruptedException {
        AtomicBoolean isRunning = new AtomicBoolean(false);
        AtomicBoolean wasInterrupted = new AtomicBoolean(false);

        Future<String> future = executorService.submit(() -> {
            try {
                isRunning.set(true);
                Thread.sleep(200);
            } catch (InterruptedException e) {
                wasInterrupted.set(true);
            }
            return "Executed";
        });

        Thread.sleep(500); // 작업이 시작되도록 대기
        future.cancel(false); // 실행 중이지만 취소 시도 (인터럽트 없음)
        Thread.sleep(500);

        assertThat(isRunning.get()).isTrue();
        assertThat(wasInterrupted.get()).isFalse();
        assertThat(future.isDone()).isTrue();
        assertThat(future.isCancelled()).isFalse();
    }

    Callable<String> callable() {
        return () -> {
            Thread.sleep(200);
            return "작업 완료";
        };
    }
}

 

새롭게 알게 된 내용으로, cancel(false) 의 경우 실행중인 작업을 취소하지 않고 완료될 때까지 실행하므로, 실행중인 작업에 cancel(true) 를 호출했다 하더라도 isCancelled() 가 false를 반환한다. cancel(false) 는 작업이 시작되기 전에 취소해야 isCancelled() 가 true 를 반환한다.

Callable 과 Feature 사용 예시

public class CallableSample implements Callable<Integer> {
    private final int value;

    public CallableSample(int value) {
        this.value = value;
    }

    @Override
    public Integer call() throws Exception {
        Thread.sleep(2000); // 2초 동안 대기
        return value * 100;
    }
}

Callable을 구현하려면 call() 메서드를 오버라이드하면 된다. 2초 동안 대기한 후, 전달받은 값에 100 을 곱한 값을 반환하도록 구현했다.

ExecutorService executor = Executors.newFixedThreadPool(2);

int initValue = 10;
System.out.println("초기값 = " + initValue);
Callable<Integer> task = new CallableSample(10);
Future<Integer> future = executor.submit(task);

System.out.println("작업 실행 중...");
Integer result = future.get();
System.out.println("결과 = " + result);
executor.shutdown();

 

Callable는 ExecutorService에 submit 되어 스레드 풀에서 작업을 수행하고 Future<T> 로 실행결과를 반환한다. future.get()을 호출하면 해당 작업이 완료될 때까지 현재 스레드가 블로킹(blocking) 상태가 되고, 작업이 완료되면 결과를 받을 수 있다.

 

Callable 또한 Runnable 과 마찬가지로 함수형 인터페이스이므로 동일한 작업을 람다식으로도 작성할 수 있다.

int initValue = 10;

Callable<Integer> task = () -> {
    Thread.sleep(2000);
    return initValue * 100;
};

'Java' 카테고리의 다른 글

[Java] 자바 객체의 Lock 과 Monitor 이해하기  (0) 2025.03.09
[Java] Blocking Queue 이해하고 사용해보기  (1) 2025.03.07
[Java] 열거형(Enum) 이해하고 사용하기  (0) 2025.02.22
[Java] Thread, Runnable 이해하고 사용하기  (0) 2025.02.21
자바의 정석 OOP(1)  (0) 2024.09.23
'Java' 카테고리의 다른 글
  • [Java] 자바 객체의 Lock 과 Monitor 이해하기
  • [Java] Blocking Queue 이해하고 사용해보기
  • [Java] 열거형(Enum) 이해하고 사용하기
  • [Java] Thread, Runnable 이해하고 사용하기
기억은 RAM, 기록은 HDD
기억은 RAM, 기록은 HDD
  • 기억은 RAM, 기록은 HDD
    적립식 개발
    기억은 RAM, 기록은 HDD
  • 전체
    오늘
    어제
    • 분류 전체보기 (45)
      • Gradle (1)
      • 알고리즘 (14)
        • 강한 연결 요소 (1)
        • BFS (1)
        • 다이나믹 프로그래밍 (2)
        • 그리디 (1)
        • 투 포인터 (2)
        • 비트마스크 (1)
        • 스택 (1)
        • 백트래킹 (1)
        • 유니온-파인드 (1)
        • 기초 기하학 (1)
        • 분할정복을 이용한 거듭제곱 (1)
        • 볼록 껍질 (1)
      • JPA (3)
      • Java (9)
      • Spring (9)
      • Git&GitHub (2)
      • Infra (4)
  • 최근 글

  • 인기 글

  • 태그

    투 포인터
    java synchronized
    java
    spring cors 해결
    enum정리
    완전탐색
    java 라이브러리 추가
    enum활용법
    자바 runnable
    기하학
    데몬쓰레드
    정렬
    자바future
    자바 callable
    비트마스킹
    githubworkflow
    자바synchronized키워드
    thread 와 runnable
    @embedded
    Github
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.1
기억은 RAM, 기록은 HDD
[Java] Callable, Future 이해 및 사용예시
테마상단으로

티스토리툴바