Runnable 인터페이스의 run() 메서드는 체크 예외(Checked Exception)를 던질 수 없습니다. 이는 Runnable 인터페이스가 자바 표준 라이브러리에서 정의될 때, 명시적으로 체크 예외를 던질 수 없는 구조로 설계되었기 때문입니다.
Runnable 인터페이스 정의:
public interface Runnable {
void run();
}
위 정의에서 run() 메서드는 throws 절이 없습니다. 자바에서 체크 예외를 던지려면 반드시 throws 절이 있어야 하므로, run() 메서드에서 체크 예외를 직접 던질 수 없습니다.
이유:
Runnable의 단순한 동작 모델: Runnable은 주로 멀티스레드 환경에서 사용됩니다. 자바 스레드 모델에서 예외 처리가 자주 사용되는 구조는 아닙니다. 멀티스레드 환경에서 예외가 발생하면 해당 스레드에서만 예외가 발생하고, 메인 스레드나 다른 스레드에는 예외가 전달되지 않습니다. 따라서 Runnable은 스레드가 수행할 단순한 작업 모델로 설계되었으며, 체크 예외를 던지지 않도록 간소화되었습니다.
스레드 풀과의 호환성: Runnable은 자바의 스레드 풀(ExecutorService) 같은 구조에서 널리 사용됩니다. 이러한 구조에서는 스레드 실행이 비동기적으로 처리되기 때문에, 체크 예외가 발생하면 이를 처리할 명확한 방법이 없습니다. 따라서 Runnable은 비동기 작업을 간단하게 처리하기 위해 예외를 처리하는 메커니즘을 내부적으로 처리하고, 예외를 던지지 않도록 설계되었습니다.
자바에서 예외는 크게 두 가지로 구분됩니다:
체크 예외 (Checked Exception): 컴파일 시점에 반드시 처리하거나 던져야 하는 예외입니다. IOException, SQLException 등이 체크 예외에 속합니다. 메서드에서 체크 예외를 던지면 반드시 throws 키워드를 사용하여 선언하거나, try-catch로 처리해야 합니다.
언체크 예외 (Unchecked Exception): 런타임 예외(Runtime Exception)라고도 불리며, 런타임 중에 발생하는 예외로, 컴파일러가 처리 여부를 강제하지 않습니다. NullPointerException, ArrayIndexOutOfBoundsException 등이 언체크 예외에 해당합니다.
체크 예외 재정의 규칙은 자바에서 메서드를 오버라이드할 때 체크 예외 처리에 관한 규칙입니다. 이는 부모 클래스의 메서드를 자식 클래스에서 오버라이드할 때 체크 예외 처리 방법을 제한하는 규칙입니다.
재정의 규칙:
자식 클래스는 부모 클래스가 던지는 체크 예외를 던질 수 있다: 부모 클래스의 메서드가 특정 체크 예외를 던지면, 자식 클래스에서 오버라이드된 메서드도 동일한 예외를 던질 수 있습니다.
자식 클래스는 부모 클래스가 던지는 체크 예외의 하위 타입만 던질 수 있다: 자식 클래스는 부모 클래스에서 던지는 예외보다 더 일반적인 예외를 던질 수 없습니다. 다만, 부모 클래스의 예외보다 더 구체적인 하위 예외를 던지는 것은 허용됩니다.
자식 클래스는 부모 클래스가 던지지 않은 예외를 새롭게 던질 수 없다: 부모 클래스의 메서드가 체크 예외를 던지지 않는다면, 자식 클래스도 체크 예외를 던질 수 없습니다. 즉, 부모 클래스에서 던지지 않는 체크 예외를 자식 클래스에서 던지는 것은 불가능합니다.
예시:
class Parent {
public void method() throws IOException {
// 부모 클래스 메서드
}
}
class Child extends Parent {
@Override
public void method() throws FileNotFoundException { // IOException의 하위 예외
// 자식 클래스 메서드
}
}
위 코드에서 Child 클래스의 method()는 IOException의 하위 클래스인 FileNotFoundException을 던질 수 있습니다. 하지만 만약 Exception과 같은 더 일반적인 예외를 던지려고 하면 컴파일 오류가 발생합니다.
Runnable에서 체크 예외를 던질 수 없기 때문에, 예외 처리가 필요한 경우 예외를 try-catch로 처리해야 합니다. 직접 예외를 던질 수 없기 때문에, Runnable 구현에서 발생하는 체크 예외는 모두 내부적으로 처리하거나, 적절한 방식으로 전달해야 합니다.
예시:
class MyRunnable implements Runnable {
@Override
public void run() {
try {
// 체크 예외가 발생할 수 있는 작업
throw new IOException("입출력 오류");
} catch (IOException e) {
System.out.println("예외 처리: " + e.getMessage());
}
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
이 경우, try-catch를 사용하여 내부적으로 예외를 처리해야만 합니다.
체크 예외를 처리할 수 없기 때문에, 체크 예외를 던져야 하는 작업이라면 Runnable 대신 Callable 인터페이스를 사용하는 것이 적합합니다. Callable은 체크 예외를 던질 수 있으며, 작업의 결과를 반환할 수 있습니다.
예시:
import java.util.concurrent.*;
class MyCallable implements Callable<String> {
@Override
public String call() throws IOException {
// 예외를 던질 수 있음
throw new IOException("입출력 오류");
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<String> future = executor.submit(new MyCallable());
try {
String result = future.get();
} catch (ExecutionException | InterruptedException e) {
System.out.println("Callable 예외 처리: " + e.getCause());
} finally {
executor.shutdown();
}
}
}
위 코드에서 Callable은 체크 예외를 던질 수 있으며, 예외가 발생하면 ExecutionException으로 포장되어 예외가 호출자에게 전달됩니다.
Runnable과 Callable의 가장 큰 차이점은 아래와 같습니다:
요약
| 인터페이스 | 반환값 | 예외 처리 | 메서드 |
|---|---|---|---|
| Runnable | 없음 | 체크 예외 불가 | run() |
| Callable | 있음 | 체크 예외 가능 | call() |
체크 예외를 재정의할 때 자바에서는 몇 가지 규칙이 있습니다. 자식 클래스가 부모 클래스의 메서드를 오버라이드할 때, 체크 예외는 더 구체적이거나 던지지 않는 방향으로만 재정의할 수 있습니다.
체크 예외 재정의 규칙:
예시:
class Parent {
public void method() throws IOException {
// 부모 메서드
}
}
class Child extends Parent {
@Override
public void method() throws FileNotFoundException { // IOException의 하위 예외
// 자식 메서드
}
}
예시:
class Parent {
public void method() {
// 부모 메서드, 예외 없음
}
}
class Child extends Parent {
@Override
public void method() throws IOException { // 컴파일 에러 발생
// 자식 메서드
}
}
요약:
Callable을 사용하면 체크 예외를 던질 수 있고, ExecutorService를 통해 Future 객체로 작업 결과를 받을 수 있습니다. Future 객체는 결과가 완료될 때까지 대기할 수 있으며, 예외가 발생한 경우 ExecutionException을 통해 예외를 확인할 수 있습니다.
Callable 예외 처리 방법:
예시:
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws IOException {
if (true) { // 예외 발생 조건
throw new IOException("입출력 오류 발생");
}
return 42;
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new MyCallable());
try {
// 결과를 얻거나 예외 처리
Integer result = future.get(); // 예외 발생 시 ExecutionException
} catch (ExecutionException e) {
System.out.println("예외 처리: " + e.getCause());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
설명: