Runnable과 Callable의 차이 (Gemini 답변)

Philipy (윤상필)·2025년 9월 15일

네, 아주 좋은 질문입니다. 콜백과 비동기 처리를 이해하는 데 있어 RunnableCallable의 차이를 아는 것은 매우 중요합니다.

Runnable이 어떤 특징을 가지고 있는지, 이해하기 쉬운 비유와 함께 핵심만 짚어드리겠습니다.

Runnable을 한마디로 정의하면: "결과 보고가 필요 없는 간단한 심부름"

이해를 돕기 위해 간단한 비유를 들어보겠습니다.

  • Runnable: 부모님이 "방 청소 좀 해놓으렴" 하고 시키는 심부름과 같습니다. 부모님은 여러분이 청소를 했다는 '사실'이 중요하지, 청소 과정이나 결과물(예: '청소 완료 보고서')을 직접 돌려받을 필요는 없습니다. 그냥 묵묵히 할 일을 하면 됩니다.
  • Callable: 부모님이 "마트 가서 우유 사 오고, 영수증 꼭 가져와" 라고 시키는 심부름입니다. 이 심부름은 '우유를 사 오는 행위'와 더불어, 반드시 '영수증'이라는 결과물을 가지고 돌아와야 합니다.

이 비유를 기억하시고 Runnable의 주요 특징 4가지를 살펴보면 훨씬 이해가 잘 되실 겁니다.


Runnable 인터페이스의 주요 특징

Runnable은 Java에서 가장 기본적이고 오래된 비동기 작업 단위이며, 그 구조는 매우 단순합니다.

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

1. 결과를 반환하지 않습니다. (No Return Value)

  • run() 메서드의 반환 타입이 void입니다.
  • 이는 Runnable이 맡은 작업이 끝난 후, 자신을 호출한 쪽으로 어떠한 결과값도 직접 돌려주지 않는다는 의미입니다.
  • 앞서 비유한 "방 청소"처럼, 어떤 값을 계산해서 반환하는 목적이 아니라, 특정 동작을 수행하는 것 자체가 목적일 때 사용합니다. "Fire-and-forget"(실행하고 잊어버리기) 방식에 적합합니다.

2. 매개변수를 받지 않습니다.

  • run() 메서드는 아무런 매개변수도 받지 않습니다.
  • 만약 작업에 데이터가 필요하다면, 보통 Runnable을 구현하는 클래스의 생성자나 다른 메서드를 통해 외부에서 데이터를 전달받아 멤버 변수에 저장해두고 run() 메서드 내에서 사용합니다.
// Runnable에 데이터를 전달하는 일반적인 방법
class MyDataProcessor implements Runnable {
    private String data;

    public MyDataProcessor(String data) {
        this.data = data; // 생성자를 통해 데이터를 받아 멤버 변수에 저장
    }

    @Override
    public void run() {
        System.out.println("전달받은 데이터 처리: " + this.data);
        // ... 데이터를 사용하는 로직 ...
    }
}

// 사용 예시
MyDataProcessor processor = new MyDataProcessor("중요한 데이터");
new Thread(processor).start();

3. 체크 예외(Checked Exception)를 던질 수 없습니다.

  • run() 메서드 시그니처에는 throws 구문이 없습니다.
  • 따라서 IOException이나 SQLException처럼 반드시 처리해야 하는 체크 예외를 메서드 밖으로 던질 수 없습니다.
  • 만약 run() 메서드 내에서 이런 예외가 발생할 수 있는 코드가 있다면, 반드시 run() 메서드 내부에서 try-catch 블록으로 감싸서 직접 처리해야 합니다.
@Override
public void run() {
    try {
        // 파일 쓰기와 같이 IOException을 발생시킬 수 있는 작업
        Files.write(Paths.get("test.txt"), "hello".getBytes());
    } catch (IOException e) {
        // 예외는 run() 메서드 안에서 자체적으로 처리해야 함
        System.err.println("파일 쓰기 실패: " + e.getMessage());
        // 또는 로깅, 에러 리포팅 등을 수행
    }
}

4. 구현이 매우 간단합니다.

  • 추상 메서드가 run() 하나뿐인 "함수형 인터페이스(@FunctionalInterface)"입니다.
  • 덕분에 Java 8 이상의 람다(Lambda) 표현식을 사용하면 매우 간결하게 코드를 작성할 수 있습니다.
// 전통적인 익명 클래스 방식
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello from Anonymous Class!");
    }
};

// 람다 표현식 (훨씬 간결함)
Runnable r2 = () -> System.out.println("Hello from Lambda!");

new Thread(r2).start();

Runnable vs Callable 핵심 비교

특징Runnable (간단한 심부름)Callable<V> (결과 보고가 필요한 심부름)
핵심 메서드void run()V call()
결과 반환없음 (void)있음 (Generic Type V)
예외 처리run() 내부에서 try-catch로 직접 처리call() 메서드 밖으로 Exception을 던질 수 있음
결과 확인(결과 없음)Future<V> 객체를 통해 나중에 결과 확인/대기 가능

결론적으로 Runnable은 별도의 결과값이나 예외 처리를 외부로 넘길 필요 없이, 독립적으로 어떤 동작을 수행하고 끝나야 할 때 사용하는 가장 단순하고 기본적인 비동기 작업 명세서라고 할 수 있습니다.


아니요, 전혀 그렇지 않습니다! 오히려 현대 자바 애플리케이션, 특히 서버나 안드로이드 같은 비동기 처리가 필수적인 환경에서는 Callable이 매우 중요하고 광범위하게 사용됩니다.

Runnable이 '단순 실행'의 대표주자라면, Callable'결과를 돌려받는 비동기 작업'의 표준이라고 할 수 있습니다.

Callable이 별로 쓰이지 않는 것처럼 보일 수 있는지, 그리고 실제로는 얼마나 중요한지 알려드리겠습니다.

Callable은 혼자 쓰이지 않고, ExecutorService & Future와 함께 쓰입니다.

Runnablenew Thread(runnable).start()처럼 아주 간단하게 사용할 수 있습니다. 하지만 Callable은 보통 이렇게 사용하지 않습니다. Callable의 진정한 힘은 스레드 풀(ExecutorService)과 만났을 때 발휘됩니다.

이 조합이 바로 Callable이 핵심적인 이유를 보여줍니다.

  1. 작업 제출 및 '결과 보증서' 받기

    • ExecutorService.submit(callable) 메서드를 사용해 스레드 풀에 Callable 작업을 제출합니다.
    • submit 메서드는 작업을 시작시키고, 즉시 Future라는 객체를 반환합니다. 이 Future가 바로 "나중에 결과가 담길 약속 증서" 또는 "결과 보증서"입니다.
  2. 결과가 필요할 때까지 기다리거나, 다른 작업 하기

    • 호출한 쪽은 Future 객체를 받아놓고, 당장 결과가 필요 없다면 다른 작업을 계속 진행할 수 있습니다.
    • 나중에 결과가 필요해지면, future.get() 메서드를 호출합니다.
      • 이때 Callable의 작업이 이미 끝나있다면, 즉시 결과값을 반환합니다.
      • 아직 작업이 진행 중이라면, 작업이 끝날 때까지 get() 메서드에서 대기(blocking)합니다.
  3. 예외 처리의 용이성

    • Callablecall() 메서드 내에서 예외가 발생하면, 그 예외는 버려지지 않고 Future 객체 안에 저장됩니다.
    • 그리고 future.get()을 호출하는 시점에, 저장되어 있던 예외가 ExecutionException으로 래핑(wrapping)되어 발생합니다. 덕분에 비동기 작업에서 발생한 예외를 호출한 쪽에서 try-catch로 깔끔하게 잡아낼 수 있습니다.

코드로 보는 Callable의 전형적인 사용 패턴

import java.util.concurrent.*;

public class CallableExample {
    public static void main(String[] args) {
        // 1. 스레드 풀 생성 (2개의 스레드를 가진)
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 2. 결과를 반환하는 Callable 작업 정의 (1~100까지 더하기)
        Callable<Integer> sumTask = () -> {
            System.out.println("계산 시작... (스레드: " + Thread.currentThread().getName() + ")");
            int sum = 0;
            for (int i = 1; i <= 100; i++) {
                sum += i;
                Thread.sleep(10); // 시간이 걸리는 작업을 시뮬레이션
            }
            // 계산 결과를 반환
            return sum;
        };

        // 3. 스레드 풀에 작업을 제출하고 '결과 보증서(Future)'를 받음
        System.out.println("스레드 풀에 작업을 제출합니다.");
        Future<Integer> future = executorService.submit(sumTask);

        // 4. 다른 작업을 수행할 수 있음
        System.out.println("결과를 기다리는 동안 다른 작업을 수행합니다...");

        try {
            // 5. 결과가 필요할 때 .get()으로 결과를 가져옴 (작업이 끝날 때까지 대기)
            System.out.println("결과를 가져옵니다.");
            Integer result = future.get(); // 작업이 끝날 때까지 여기서 멈춤
            System.out.println("계산 결과: " + result);

        } catch (InterruptedException | ExecutionException e) {
            // InterruptedException: .get()으로 대기 중에 스레드가 중단될 경우
            // ExecutionException: Callable의 call() 메서드에서 예외가 발생한 경우
            e.printStackTrace();
        }

        // 6. 스레드 풀 종료
        executorService.shutdown();
    }
}

언제 Callable을 반드시 사용해야 할까요?

"비동기적으로 처리해야 하고, 그 작업의 '결과' 또는 '성공/실패 여부'가 필요한 모든 경우"에 사용합니다.

  • 네트워크 요청: API를 호출하고 응답(JSON, XML 등)을 받아 파싱하는 작업. 결과는 파싱된 데이터 객체, 예외는 네트워크 오류.
  • 데이터베이스 조회: DB에서 데이터를 조회하고, 그 결과를 객체 리스트로 받아오는 작업.
  • 복잡한 계산: 대규모 데이터를 처리하거나 암호화/복호화 같이 CPU를 많이 사용하는 계산 후 결과값을 얻는 작업.
  • 파일 읽기/쓰기: 대용량 파일을 읽어 그 내용을 반환하거나, 파일 쓰기 성공 여부를 반환하는 작업.
  • 여러 비동기 작업의 결과 종합: 여러 개의 Callable을 동시에 실행시킨 후, 모든 Future의 결과가 모이면 이를 종합하여 최종 결과를 만들 때.

결론: Runnable vs. Callable

  • Runnable: "그냥 이 작업 좀 해줘. 결과는 필요 없어."
    • 예: 백그라운드에서 로그 파일 기록, UI 업데이트 요청(UI 스레드에 post하는 행위 자체)
  • Callable: "이 작업 좀 하고, 끝나면 결과(또는 에러)를 나에게 알려줘."
    • 예: 서버에서 사용자 프로필 데이터를 가져와 화면에 표시하기

결론적으로, Callable은 드물게 쓰이는 것이 아니라, 의미 있는 결과를 만들어내는 대부분의 실용적인 비동기 프로그래밍에서 필수적인 핵심 요소라고 할 수 있습니다. 오히려 Runnable만으로는 구현하기 어려운 복잡하고 실용적인 시나리오들을 Callable이 가능하게 해줍니다.

profile
Tech Phase-smith, Karax wannabe

0개의 댓글