네, 아주 좋은 질문입니다. 콜백과 비동기 처리를 이해하는 데 있어 Runnable과 Callable의 차이를 아는 것은 매우 중요합니다.
Runnable이 어떤 특징을 가지고 있는지, 이해하기 쉬운 비유와 함께 핵심만 짚어드리겠습니다.
Runnable을 한마디로 정의하면: "결과 보고가 필요 없는 간단한 심부름"이해를 돕기 위해 간단한 비유를 들어보겠습니다.
Runnable: 부모님이 "방 청소 좀 해놓으렴" 하고 시키는 심부름과 같습니다. 부모님은 여러분이 청소를 했다는 '사실'이 중요하지, 청소 과정이나 결과물(예: '청소 완료 보고서')을 직접 돌려받을 필요는 없습니다. 그냥 묵묵히 할 일을 하면 됩니다.Callable: 부모님이 "마트 가서 우유 사 오고, 영수증 꼭 가져와" 라고 시키는 심부름입니다. 이 심부름은 '우유를 사 오는 행위'와 더불어, 반드시 '영수증'이라는 결과물을 가지고 돌아와야 합니다.이 비유를 기억하시고 Runnable의 주요 특징 4가지를 살펴보면 훨씬 이해가 잘 되실 겁니다.
Runnable 인터페이스의 주요 특징Runnable은 Java에서 가장 기본적이고 오래된 비동기 작업 단위이며, 그 구조는 매우 단순합니다.
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
run() 메서드의 반환 타입이 void입니다.Runnable이 맡은 작업이 끝난 후, 자신을 호출한 쪽으로 어떠한 결과값도 직접 돌려주지 않는다는 의미입니다.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();
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());
// 또는 로깅, 에러 리포팅 등을 수행
}
}
run() 하나뿐인 "함수형 인터페이스(@FunctionalInterface)"입니다.// 전통적인 익명 클래스 방식
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와 함께 쓰입니다.Runnable은 new Thread(runnable).start()처럼 아주 간단하게 사용할 수 있습니다. 하지만 Callable은 보통 이렇게 사용하지 않습니다. Callable의 진정한 힘은 스레드 풀(ExecutorService)과 만났을 때 발휘됩니다.
이 조합이 바로 Callable이 핵심적인 이유를 보여줍니다.
작업 제출 및 '결과 보증서' 받기
ExecutorService.submit(callable) 메서드를 사용해 스레드 풀에 Callable 작업을 제출합니다.submit 메서드는 작업을 시작시키고, 즉시 Future라는 객체를 반환합니다. 이 Future가 바로 "나중에 결과가 담길 약속 증서" 또는 "결과 보증서"입니다.결과가 필요할 때까지 기다리거나, 다른 작업 하기
Future 객체를 받아놓고, 당장 결과가 필요 없다면 다른 작업을 계속 진행할 수 있습니다.future.get() 메서드를 호출합니다.Callable의 작업이 이미 끝나있다면, 즉시 결과값을 반환합니다.get() 메서드에서 대기(blocking)합니다.예외 처리의 용이성
Callable의 call() 메서드 내에서 예외가 발생하면, 그 예외는 버려지지 않고 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을 반드시 사용해야 할까요?"비동기적으로 처리해야 하고, 그 작업의 '결과' 또는 '성공/실패 여부'가 필요한 모든 경우"에 사용합니다.
Callable을 동시에 실행시킨 후, 모든 Future의 결과가 모이면 이를 종합하여 최종 결과를 만들 때.Runnable vs. CallableRunnable: "그냥 이 작업 좀 해줘. 결과는 필요 없어."post하는 행위 자체)Callable: "이 작업 좀 하고, 끝나면 결과(또는 에러)를 나에게 알려줘."결론적으로, Callable은 드물게 쓰이는 것이 아니라, 의미 있는 결과를 만들어내는 대부분의 실용적인 비동기 프로그래밍에서 필수적인 핵심 요소라고 할 수 있습니다. 오히려 Runnable만으로는 구현하기 어려운 복잡하고 실용적인 시나리오들을 Callable이 가능하게 해줍니다.