본문 바로가기
programming/JAVA

Runnable과 Callable의 차이

by buddev 2025. 1. 18.
특징 Runnable Callable
반환값 없음 (void) 값 반환 (V)
예외 처리 검사 예외(Checked Exception) 명시 불가 검사 예외 명시 가능 (throws Exception)
주요 메서드 void run() V call()
사용 목적 단순히 작업 실행 작업 실행 후 결과 반환, 예외 처리 필요 시

RunnableCallable예외 처리 관점에서의 차이는 두 인터페이스의 메서드가 검사 예외(Checked Exception)를 어떻게 처리하는지와 관련이 있습니다.


1. 검사 예외와 비검사 예외

검사 예외 (Checked Exception)

  • Java 컴파일러가 명시적으로 처리하도록 요구하는 예외.
  • 예: IOException, SQLException, InterruptedException.
  • 메서드에서 던지려면 throws 키워드를 사용해야 하며, 호출하는 쪽에서 반드시 try-catch로 처리하거나 throws로 위임해야 합니다.

비검사 예외 (Unchecked Exception)

  • Java 컴파일러가 처리 여부를 강제하지 않는 예외.
  • 예: NullPointerException, ArrayIndexOutOfBoundsException.
  • RuntimeException을 상속받는 예외들이 해당되며, 명시적인 처리가 없어도 던질 수 있습니다.

2. Runnable과 Callable의 예외 처리 차이

Runnable: 검사 예외를 명시적으로 던질 수 없음

Runnablerun() 메서드는 throws 절이 없기 때문에 검사 예외를 직접 던질 수 없습니다.

@FunctionalInterface
public interface Runnable {
    void run(); // throws Exception 불가능
}

결과:

  • 검사 예외를 run() 메서드에서 던지고 싶다면, 반드시 try-catch로 내부에서 처리해야 합니다.
  • 호출하는 쪽에서는 Runnable 내부에서 발생한 예외를 직접 알 수 없습니다.
예제: Runnable에서 예외 처리
Runnable task = () -> {
    try {
        throw new Exception("예외 발생!"); // 검사 예외
    } catch (Exception e) {
        System.out.println("Runnable에서 예외 처리: " + e.getMessage());
    }
};

new Thread(task).start(); // 출력: Runnable에서 예외 처리: 예외 발생!

Callable: 검사 예외를 명시적으로 던질 수 있음

Callablecall() 메서드는 throws Exception 절이 포함되어 있어, 검사 예외를 명시적으로 던질 수 있습니다.

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception; // throws Exception 가능
}

결과:

  • 검사 예외를 call() 메서드에서 던질 수 있으며, 호출하는 쪽에서 try-catch로 처리하거나 예외를 위임할 수 있습니다.
  • 호출하는 쪽에서는 작업 도중 발생한 예외를 명확히 알 수 있습니다.
예제: Callable에서 예외 처리
import java.util.concurrent.Callable;

public class CallableExample {
    public static void main(String[] args) {
        Callable<Integer> task = () -> {
            throw new Exception("예외 발생!"); // 검사 예외
        };

        try {
            task.call(); // 예외가 호출한 곳으로 전달됨
        } catch (Exception e) {
            System.out.println("Callable에서 예외 처리: " + e.getMessage());
        }
    }
}

3. Runnable과 Callable의 예외 처리 흐름 비교

(1) Runnable의 예외 처리 흐름

  • 예외는 run() 메서드 내부에서만 처리 가능합니다.
  • 호출하는 쪽에서는 내부에서 발생한 예외를 직접 알 수 없습니다.
Runnable task = () -> {
    try {
        // 검사 예외를 처리
        throw new Exception("예외 발생!");
    } catch (Exception e) {
        System.out.println("Runnable에서 처리: " + e.getMessage());
    }
};
new Thread(task).start(); // 호출한 곳에서는 예외를 알 수 없음

(2) Callable의 예외 처리 흐름

  • call() 메서드는 예외를 던질 수 있으며, 호출하는 쪽에서 예외를 직접 처리할 수 있습니다.
  • 예외는 Future.get() 호출 시 확인할 수도 있습니다.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

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

        Callable<Integer> task = () -> {
            throw new Exception("예외 발생!");
        };

        Future<Integer> future = executor.submit(task);

        try {
            future.get(); // Callable의 예외는 여기에 전달됨
        } catch (Exception e) {
            System.out.println("Callable 예외 처리: " + e.getCause().getMessage());
        }

        executor.shutdown();
    }
}

4. 예외 처리 관점에서의 선택 기준

특징 Runnable Callable
검사 예외 처리 내부에서 처리해야 하며, 외부로 전달 불가 검사 예외를 던지고 호출한 쪽에서 처리 가능
비검사 예외 처리 내부에서 처리하거나 그대로 던질 수 있음 내부에서 처리하거나 그대로 던질 수 있음
사용 시점 결과 반환이나 예외 처리가 필요 없는 단순 작업 반환값이 필요하거나, 예외 처리가 작업의 중요한 부분일 때 사용

5. 결론

  • Runnable:
    • 검사 예외는 내부에서 처리해야 하며, 호출하는 쪽에서는 이를 알 수 없습니다.
    • 단순히 실행만 필요한 작업에서 적합합니다.
  • Callable:
    • 검사 예외를 명시적으로 던질 수 있으며, 호출하는 쪽에서 이를 처리하거나 위임할 수 있습니다.
    • 반환값이 필요하거나, 예외 처리가 중요한 작업에 적합합니다.

요약:

Runnable은 간단한 작업에 적합하며, Callable은 예외 처리와 결과 반환이 필요한 경우 유용합니다.

 

호출하는 위치에서 예외를 알 수 있는지

이 두 부분을 명확히 이해하려면, RunnableCallable의 예외 처리 흐름과 호출하는 쪽에서의 동작 차이를 중심으로 설명하겠습니다.


1. Runnable의 예외 처리 흐름

Runnablerun() 메서드는 검사 예외(Checked Exception)를 명시적으로 던질 수 없습니다. 따라서, Runnable 내부에서 발생한 예외는 반드시 try-catch로 처리해야 합니다. 호출하는 쪽에서는 Runnable 내부에서 예외가 발생했는지 직접 알 수 없습니다.

예제: Runnable에서 예외 처리

Runnable task = () -> {
    try {
        throw new Exception("Runnable에서 예외 발생!");
    } catch (Exception e) {
        System.out.println("Runnable 내부에서 처리: " + e.getMessage());
    }
};

// Runnable을 스레드에서 실행
Thread thread = new Thread(task);
thread.start();

출력 결과:

Runnable 내부에서 처리: Runnable에서 예외 발생!

특징

  1. 내부 처리 필수:
    • Runnable에서 발생한 예외는 run() 내부에서 처리해야 합니다.
    • 예외가 발생했는지 호출하는 쪽에서는 알 수 없습니다.
  2. 호출하는 쪽의 한계:
    • 호출한 코드(thread.start() 실행 위치)에서는 Runnable 내부의 예외를 감지하거나 처리할 수 없습니다.

2. Callable의 예외 처리 흐름

Callablecall() 메서드는 검사 예외(Checked Exception)를 명시적으로 던질 수 있습니다. ExecutorService.submit()을 통해 실행한 후, 반환된 Future 객체를 사용하면, 결과값뿐만 아니라 발생한 예외를 호출하는 쪽에서 직접 확인할 수 있습니다.

예제: Callable에서 예외 처리

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

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

        Callable<Integer> task = () -> {
            throw new Exception("Callable에서 예외 발생!");
        };

        Future<Integer> future = executor.submit(task); // Callable 실행

        try {
            future.get(); // 결과를 가져오는 시점에서 예외 발생 여부 확인
        } catch (Exception e) {
            System.out.println("Callable 예외 처리: " + e.getCause().getMessage());
        }

        executor.shutdown();
    }
}

출력 결과:

Callable 예외 처리: Callable에서 예외 발생!

Future.get()에서 예외 확인

  • future.get() 메서드를 호출하면:
    1. Callable 작업이 반환한 결과를 가져옴.
    2. 작업 도중 예외가 발생했다면, ExecutionException으로 래핑되어 예외가 던져짐.
    3. 호출한 쪽에서 예외를 확인하고 처리 가능.

특징

  1. 명시적으로 던질 수 있음:
    • Callablecall() 메서드에서 발생한 예외는 호출하는 쪽(future.get())으로 전달됩니다.
  2. 호출하는 쪽에서 알 수 있음:
    • Future.get()을 호출하면, 작업 도중 발생한 예외를 감지하고 처리할 수 있습니다.

3. Runnable과 Callable의 예외 처리 비교

특징 Runnable Callable
검사 예외 던지기 내부에서만 처리 가능 (try-catch 필수) call() 메서드에서 명시적으로 던질 수 있음
호출하는 쪽에서 예외 확인 불가능 (start() 호출 위치에서는 알 수 없음) Future.get() 호출 시 예외를 확인하고 처리 가능
예외 처리 방식 내부에서만 처리 (run() 메서드 안에서) 호출하는 쪽에서 처리 가능 (future.get() 사용)

4. 핵심 차이 요약

Runnable

  • 호출하는 쪽에서는 내부에서 예외가 발생했는지 알 수 없습니다.
  • 예외를 처리하려면 run() 메서드 내부에서 try-catch를 사용해야 합니다.
  • 호출하는 쪽은 작업이 성공했는지 실패했는지 알 수 없고, 결과값도 받을 수 없습니다.

Callable

  • 호출하는 쪽에서는 Future.get()을 통해 예외 발생 여부를 확인할 수 있습니다.
  • call() 메서드에서 던진 예외ExecutionException으로 래핑되어 전달됩니다.
  • 호출하는 쪽은 작업 결과와 예외 상태를 모두 확인할 수 있습니다.

5. 비유를 통한 이해

  • Runnable: "내부에서 문제가 생기면 알아서 해결하세요. 호출한 사람은 그게 뭔지 신경 쓰지 않아요."
    • 호출자는 예외가 발생했는지 알 방법이 없습니다.
  • Callable: "문제가 생기면 나중에 보고서를 줄게요. 보고서를 읽고 어떻게 처리할지 결정하세요."
    • 호출자는 Future.get()을 통해 예외와 결과를 확인할 수 있습니다.

6. 어떤 상황에서 사용해야 할까?

  • Runnable:
    • 단순히 실행만 필요한 작업.
    • 결과값이나 예외 처리가 필요 없는 경우.
    • 예: 로그 기록, UI 업데이트, 단순 반복 작업.
  • Callable:
    • 결과값을 반환해야 하거나, 예외를 호출하는 쪽에서 처리해야 하는 작업.
    • 예: 파일 다운로드, 데이터베이스 쿼리, 계산 작업.

댓글