Runnable에서 예외를 던질 수 없는 이유

서버란·2024년 9월 19일

자바 궁금증

목록 보기
23/35

1. Runnable에서 예외를 던질 수 없는 이유

Runnable 인터페이스의 run() 메서드는 체크 예외(Checked Exception)를 던질 수 없습니다. 이는 Runnable 인터페이스가 자바 표준 라이브러리에서 정의될 때, 명시적으로 체크 예외를 던질 수 없는 구조로 설계되었기 때문입니다.

Runnable 인터페이스 정의:

public interface Runnable {
    void run();
}

위 정의에서 run() 메서드는 throws 절이 없습니다. 자바에서 체크 예외를 던지려면 반드시 throws 절이 있어야 하므로, run() 메서드에서 체크 예외를 직접 던질 수 없습니다.

이유:

  1. Runnable의 단순한 동작 모델: Runnable은 주로 멀티스레드 환경에서 사용됩니다. 자바 스레드 모델에서 예외 처리가 자주 사용되는 구조는 아닙니다. 멀티스레드 환경에서 예외가 발생하면 해당 스레드에서만 예외가 발생하고, 메인 스레드나 다른 스레드에는 예외가 전달되지 않습니다. 따라서 Runnable은 스레드가 수행할 단순한 작업 모델로 설계되었으며, 체크 예외를 던지지 않도록 간소화되었습니다.

  2. 스레드 풀과의 호환성: Runnable은 자바의 스레드 풀(ExecutorService) 같은 구조에서 널리 사용됩니다. 이러한 구조에서는 스레드 실행이 비동기적으로 처리되기 때문에, 체크 예외가 발생하면 이를 처리할 명확한 방법이 없습니다. 따라서 Runnable은 비동기 작업을 간단하게 처리하기 위해 예외를 처리하는 메커니즘을 내부적으로 처리하고, 예외를 던지지 않도록 설계되었습니다.

2. 체크 예외(Check Exception)와 언체크 예외(Unchecked Exception)

자바에서 예외는 크게 두 가지로 구분됩니다:

  • 체크 예외 (Checked Exception): 컴파일 시점에 반드시 처리하거나 던져야 하는 예외입니다. IOException, SQLException 등이 체크 예외에 속합니다. 메서드에서 체크 예외를 던지면 반드시 throws 키워드를 사용하여 선언하거나, try-catch로 처리해야 합니다.

  • 언체크 예외 (Unchecked Exception): 런타임 예외(Runtime Exception)라고도 불리며, 런타임 중에 발생하는 예외로, 컴파일러가 처리 여부를 강제하지 않습니다. NullPointerException, ArrayIndexOutOfBoundsException 등이 언체크 예외에 해당합니다.

3. 체크 예외의 재정의 규칙 (Checked Exception Overriding Rule)

체크 예외 재정의 규칙은 자바에서 메서드를 오버라이드할 때 체크 예외 처리에 관한 규칙입니다. 이는 부모 클래스의 메서드를 자식 클래스에서 오버라이드할 때 체크 예외 처리 방법을 제한하는 규칙입니다.

재정의 규칙:

  1. 자식 클래스는 부모 클래스가 던지는 체크 예외를 던질 수 있다: 부모 클래스의 메서드가 특정 체크 예외를 던지면, 자식 클래스에서 오버라이드된 메서드도 동일한 예외를 던질 수 있습니다.

  2. 자식 클래스는 부모 클래스가 던지는 체크 예외의 하위 타입만 던질 수 있다: 자식 클래스는 부모 클래스에서 던지는 예외보다 더 일반적인 예외를 던질 수 없습니다. 다만, 부모 클래스의 예외보다 더 구체적인 하위 예외를 던지는 것은 허용됩니다.

  3. 자식 클래스는 부모 클래스가 던지지 않은 예외를 새롭게 던질 수 없다: 부모 클래스의 메서드가 체크 예외를 던지지 않는다면, 자식 클래스도 체크 예외를 던질 수 없습니다. 즉, 부모 클래스에서 던지지 않는 체크 예외를 자식 클래스에서 던지는 것은 불가능합니다.

예시:

class Parent {
    public void method() throws IOException {
        // 부모 클래스 메서드
    }
}

class Child extends Parent {
    @Override
    public void method() throws FileNotFoundException {  // IOException의 하위 예외
        // 자식 클래스 메서드
    }
}

위 코드에서 Child 클래스의 method()는 IOException의 하위 클래스인 FileNotFoundException을 던질 수 있습니다. 하지만 만약 Exception과 같은 더 일반적인 예외를 던지려고 하면 컴파일 오류가 발생합니다.

4. Runnable에서 체크 예외 처리 방법

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를 사용하여 내부적으로 예외를 처리해야만 합니다.

5. Runnable 대신 Callable을 사용하는 경우

체크 예외를 처리할 수 없기 때문에, 체크 예외를 던져야 하는 작업이라면 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으로 포장되어 예외가 호출자에게 전달됩니다.


Q1. Runnable과 Callable의 가장 큰 차이점은 무엇인가요?

Runnable과 Callable의 가장 큰 차이점은 아래와 같습니다:

  1. 반환값 유무:
  • Runnable은 반환값이 없으며, run() 메서드가 실행되면 아무 결과도 반환하지 않습니다.
    Callable은 작업을 수행한 후 결과를 반환할 수 있습니다.
  • call() 메서드는 제네릭 타입을 사용하여 반환 타입을 지정할 수 있습니다.
  1. 예외 처리:
  • Runnable은 체크 예외를 던질 수 없습니다. run() 메서드에서 체크 예외가 발생할 경우 반드시 내부에서 try-catch로 처리해야 합니다.
  • Callable은 체크 예외를 던질 수 있습니다. call() 메서드에서 체크 예외를 던질 수 있고, 이 예외는 Future 객체를 통해 호출자에게 전달됩니다.
  1. 주요 메서드:
  • Runnable의 핵심 메서드는 run()이며, 반환값과 예외를 처리하지 않습니다.
  • Callable의 핵심 메서드는 call()이며, 결과를 반환하고 예외를 던질 수 있는 메서드입니다.

요약

인터페이스반환값예외 처리메서드
Runnable없음체크 예외 불가run()
Callable있음체크 예외 가능call()

Q2. 자식 클래스가 부모 클래스의 메서드를 오버라이드할 때, 체크 예외를 재정의할 수 있는 경우와 없는 경우는 무엇인가요?

체크 예외를 재정의할 때 자바에서는 몇 가지 규칙이 있습니다. 자식 클래스가 부모 클래스의 메서드를 오버라이드할 때, 체크 예외는 더 구체적이거나 던지지 않는 방향으로만 재정의할 수 있습니다.

체크 예외 재정의 규칙:

  1. 부모 클래스가 체크 예외를 던질 경우:
  • 자식 클래스는 부모 클래스가 던지는 예외와 동일하거나 그 예외의 하위 클래스를 던질 수 있습니다.
  • 자식 클래스는 체크 예외를 던지지 않을 수도 있습니다.

예시:

class Parent {
    public void method() throws IOException {
        // 부모 메서드
    }
}

class Child extends Parent {
    @Override
    public void method() throws FileNotFoundException {  // IOException의 하위 예외
        // 자식 메서드
    }
}
  1. 부모 클래스가 체크 예외를 던지지 않을 경우:
  • 자식 클래스는 체크 예외를 새로 추가해서 던질 수 없습니다. 부모 클래스의 메서드가 체크 예외를 던지지 않으면, 자식 클래스도 체크 예외를 던질 수 없습니다.

예시:

class Parent {
    public void method() {
        // 부모 메서드, 예외 없음
    }
}

class Child extends Parent {
    @Override
    public void method() throws IOException {  // 컴파일 에러 발생
        // 자식 메서드
    }
}

요약:

  • 부모 클래스가 체크 예외를 던지면, 자식 클래스도 동일한 예외나 그 하위 타입의 예외를 던질 수 있습니다.
  • 부모 클래스가 체크 예외를 던지지 않으면, 자식 클래스도 체크 예외를 던질 수 없습니다.

Q3. Callable을 사용하여 멀티스레딩 작업에서 예외를 처리할 때 어떤 방식으로 결과를 안전하게 받을 수 있나요?

Callable을 사용하면 체크 예외를 던질 수 있고, ExecutorService를 통해 Future 객체로 작업 결과를 받을 수 있습니다. Future 객체는 결과가 완료될 때까지 대기할 수 있으며, 예외가 발생한 경우 ExecutionException을 통해 예외를 확인할 수 있습니다.

Callable 예외 처리 방법:

  1. ExecutorService를 사용해 Callable을 제출합니다.
  2. Future.get() 메서드를 호출하여 작업 결과를 가져옵니다. 이때, 작업이 완료되지 않았다면 스레드는 결과가 나올 때까지 대기합니다.
  3. 예외가 발생하면, ExecutionException을 통해 예외가 전달됩니다. 이 예외는 getCause()를 사용해 원래 발생한 예외를 확인할 수 있습니다.

예시:

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();
        }
    }
}

설명:

  • Future.get() 메서드에서 ExecutionException이 발생하면, 원래 발생한 예외는 getCause()로 접근할 수 있습니다. 이를 통해 예외 처리를 안전하게 할 수 있습니다.
profile
백엔드에서 서버엔지니어가 된 사람

0개의 댓글