[Java 응용] Exception (예외 계층 & try문)

Kyung Jae, Cheong·2024년 9월 2일
0
post-thumbnail

예외 처리 (예외 계층 & try문)

0. 예외 처리가 필요한 이유

  • 자바에서 예외 처리(Exception Handling)는 프로그램이 실행 중에 발생할 수 있는 오류를 처리하여 프로그램의 비정상적인 종료를 방지하고, 안정적으로 동작하도록 하기 위해 필수적인 요소입니다.

1. 프로그램의 비정상 종료 방지

  • 프로그램이 실행되는 동안 예기치 않은 오류가 발생할 수 있습니다. 예외 처리를 하지 않으면 이러한 오류가 발생할 때 프로그램이 즉시 종료될 수 있으며, 이는 사용자의 불편을 초래하고, 프로그램의 신뢰성을 저하시킵니다.
  • 예시: 사용자가 입력한 데이터가 기대한 형식이 아닐 때, 네트워크 연결이 갑자기 끊겼을 때, 또는 파일이 존재하지 않을 때 등.

2. 에러 발생 시의 적절한 대응

  • 예외 처리를 통해 프로그램은 오류가 발생했을 때 적절한 대응을 할 수 있습니다. 예를 들어, 오류 메시지를 출력하거나, 로그를 기록하여 문제를 파악하고, 사용자에게 안내할 수 있습니다. 이렇게 하면 프로그램의 안정성을 높이고, 문제를 빠르게 해결할 수 있습니다.
  • 예시: 파일을 읽는 도중 파일이 존재하지 않으면, 사용자에게 파일이 존재하지 않는다는 메시지를 출력하고, 파일 경로를 다시 입력하도록 요청할 수 있습니다.

3. 코드의 가독성 및 유지보수성 향상

  • 예외 처리는 오류 처리 로직을 명확하게 분리하여 코드의 가독성을 높이고, 유지보수를 쉽게 만듭니다. try-catch 블록을 통해 정상적인 흐름과 예외적인 흐름을 분리할 수 있습니다.
  • 예시: 파일 입출력 작업을 수행할 때, 정상적인 처리 로직과 파일이 없을 때의 예외 처리 로직을 분리함으로써 코드의 가독성을 높일 수 있습니다.

4. 프로그램의 신뢰성 향상

  • 예외 처리를 통해 예상치 못한 상황에서도 프로그램이 정상적으로 동작할 수 있도록 함으로써, 사용자에게 신뢰할 수 있는 프로그램을 제공할 수 있습니다. 예외를 적절하게 처리하면 프로그램이 중단되지 않고, 대신 적절한 대안을 제공하거나 복구 작업을 수행할 수 있습니다.
  • 예시: 네트워크 통신 중에 발생한 오류로 인한 데이터 손실을 최소화하고, 재전송을 시도하거나 로컬에 임시로 데이터를 저장하는 등의 대안을 제시할 수 있습니다.

5. 디버깅 및 문제 해결에 도움

  • 예외 처리를 통해 프로그램이 어떤 상황에서 오류가 발생했는지에 대한 정보를 수집할 수 있습니다. 예외의 스택 트레이스(stack trace)를 로그로 남기거나, 예외 메시지를 사용자에게 전달하여 문제를 더 쉽게 파악하고 해결할 수 있게 합니다.
  • 예시: 특정 메서드에서 NullPointerException이 발생했을 때, 예외 처리로 인해 어느 부분에서 예외가 발생했는지 확인할 수 있어 디버깅이 용이합니다.

6. 프로그램 흐름 제어

  • 예외 처리를 통해 프로그램의 흐름을 제어할 수 있습니다. 특정 조건에서 예외를 발생시키고, 이를 처리하여 프로그램이 올바른 방향으로 진행되도록 할 수 있습니다.
  • 예시: 입력된 값이 특정 범위를 벗어났을 때, 예외를 발생시키고 이를 처리하여 사용자에게 다시 입력을 요구하거나 기본값으로 설정할 수 있습니다.

1. 예외 계층 & 처리 규칙

1.1. 예외 계층 구조

  • 자바에서 예외는 Throwable 클래스를 중심으로 계층 구조를 형성하고 있습니다. 이 계층 구조는 크게 두 가지 주요 하위 클래스인 ErrorException으로 나눌 수 있으며, 각각의 용도와 처리 방법이 다릅니다.
    • 참고로 아래에서 다룰 Throwable, Error, Exception, RuntimeException, 등 자바에서 기본적으로 제공하는 예외 클래스들은 java.lang에 소속되어 있기 때문에, 별도의 import구문 없이 사용하실 수 있습니다.

자바 예외 처리 관련 클래스

  • Throwable 클래스
    • 모든 예외와 오류의 최상위 클래스입니다. 자바에서 예외 처리 가능한 모든 객체는 이 클래스에서 파생됩니다.
    • 주요 하위 클래스: Error, Exception
  • Error 클래스
    • 프로그램에서 복구할 수 없는 심각한 문제를 나타냅니다. 주로 JVM의 상태를 나타내며, 일반적으로 애플리케이션 코드에서 직접 처리하지 않습니다.
    • 예: OutOfMemoryError, StackOverflowError
    • 처리 규칙
      • Error는 프로그램의 비정상적인 상태를 나타내므로, 이를 처리하기 위해 예외 처리 코드를 작성하는 것은 권장되지 않습니다.
      • 대부분의 경우, Error가 발생하면 프로그램을 종료하고 로그를 남기는 것이 일반적입니다.
  • Exception 클래스
    • 프로그램에서 발생할 수 있는 예외적인 상황을 나타냅니다. Exception 클래스는 일반적으로 프로그램 코드에서 처리할 수 있으며, 애플리케이션의 정상적인 흐름에서 발생할 수 있는 오류를 나타냅니다.
    • 주요 하위 클래스: RuntimeException, IOException, SQLException, 등
    • Exception의 하위 클래스는 크게 두 종류로 분류할 수 있습니다.
      • 언체크 예외 (Unchecked Exception) : RuntimeException 클래스
      • 체크 예외 (Checked Exception) : RuntimeException을 제외한 모든 하위 클래스
  • RuntimeException 클래스
    • RuntimeException과 그 하위 클래스들은 체크되지 않은 예외(unchecked exception)입니다. 컴파일러가 예외 처리 여부를 검사하지 않으며, 개발자가 선택적으로 처리할 수 있습니다.
    • 예: NullPointerException, ArrayIndexOutOfBoundsException, 등
    • 처리 규칙
      • RuntimeException은 주로 프로그래머의 실수에 의해 발생하는 예외로, 예외 처리를 하지 않아도 컴파일 오류가 발생하지 않습니다.
      • 필요에 따라 예외 처리 코드를 작성하거나, 코드 수정으로 예외 발생을 방지하는 것이 좋습니다.
  • 체크 예외 (Checked Exception)
    • RuntimeException을 제외한 Exception 클래스의 모든 하위 클래스는 체크 예외입니다. 컴파일러는 이러한 예외가 발생할 가능성이 있는 메서드에 대해 예외 처리를 강제합니다.
    • 예: IOException, SQLException, ClassNotFoundException, 등
    • 처리 규칙
      • 체크 예외는 반드시 예외 처리 코드(try-catch 블록)로 처리하거나, 메서드 시그니처에 throws 키워드를 사용해 상위 메서드로 예외를 전달해야 합니다.
      • 체크 예외는 예상 가능한 예외적인 상황에 대해 명시적으로 처리하는 것이 일반적입니다.

1.2. 예외 처리 규칙

예외 처리 방법

  • try-catch 블록: 예외 발생 가능성이 있는 코드를 try 블록 내에 작성하고, 발생한 예외를 catch 블록에서 처리합니다.
    • 여러 종류의 예외를 처리하기 위해 여러 개의 catch 블록을 사용할 수 있습니다.
    • 참고로 파이썬에서는 "try-except" 문이 이와 비슷한 기능을 수행합니다.
try {
    // 예외 발생 가능 코드
} catch (IOException e) {
    // IOException 처리
} catch (SQLException e) {
    // SQLException 처리
}
  • finally 블록: try 또는 catch 블록이 완료된 후 반드시 실행되는 블록입니다.
    • 예외 발생 여부와 상관없이 리소스를 정리하거나, 중요한 작업을 수행하는 데 사용됩니다.
try {
    // 예외 발생 가능 코드
} catch (Exception e) {
    // 예외 처리
} finally {
    // 리소스 해제 또는 정리 작업
}

예외 던지기

  • 예외가 발생했을 때, 해당 예외를 호출한 메서드로 전달하려면 throw 키워드를 사용합니다.
    • 쉽게 말해 throw 키워드를 이용하면 오류를 발생시킬 수 있습니다. (주로 조건문과 함께 쓰입니다.)
    • 참고로 파이썬에서는 "raise" 문이 이와 비슷한 기능을 수행합니다.
  • 체크 예외는 메서드 시그니처에 throws 키워드를 사용해 명시해야 합니다.
    • 아래에서 다시 설명하겠지만, 자신을 호출한 다른 클래스에 발생한 오류를 던져주겠다는 뜻으로 해석할 수 있습니다.
    • 참고로 파이썬에서는 "모든 예외가 언체크 예외로 간주"되기 때문에 비슷한 기능을 하는 문법이 없습니다.
public void readFile(String filePath) throws IOException {
    if (filePath == null) {
        throw new IOException("파일 경로가 null입니다.");
    }
    // 파일 읽기 작업
}

예외의 재처리 (Re-throwing)

  • catch 블록에서 예외를 처리한 후, 같은 예외 또는 새로운 예외를 다시 던질 수 있습니다.
    • 이를 통해 예외의 원인을 상위 계층으로 전달하고, 호출자가 적절히 처리할 수 있게 합니다.
try {
    // 예외 발생 가능 코드
} catch (IOException e) {
    // 예외 로깅 등 추가 작업
    throw e; // 동일한 예외 다시 던지기
}

커스텀 예외(Custom Exception)

  • 필요에 따라 개발자는 특정 상황에 맞는 사용자 정의 예외를 생성할 수 있습니다.
    • 일반적으로 Exception 또는 RuntimeException을 상속하여 구현합니다.
public class InvalidUserInputException extends Exception {
    public InvalidUserInputException(String message) {
        super(message);
    }
}

2. 체크 예외 (Checked Exception)

  • 체크 예외(Checked Exception)는 자바에서 예외 처리 시스템의 중요한 부분을 차지하며, 발생 가능성이 있는 예외를 명시적으로 처리하도록 강제하는 예외입니다.
    • 체크 예외는 컴파일 시점에서 반드시 처리되어야 하며, 이를 무시하면 컴파일 오류가 발생합니다.
    • 체크 예외는 자바 프로그램이 예상치 못한 상황에서도 안정적으로 동작하도록 보장하는 중요한 역할을 합니다.
  • 특징
    • 컴파일러에 의해 강제됨: 체크 예외는 컴파일러에 의해 강제로 예외 처리 코드가 작성되도록 요구됩니다. 만약 체크 예외를 처리하지 않으면, 컴파일 오류가 발생합니다.
    • 일반적으로 외부 요인에 의해 발생: 체크 예외는 주로 파일 입출력, 네트워크 통신, 데이터베이스 연결 등 외부 자원에 의존하는 작업에서 발생합니다.
    • 예외 처리 필수: 예외가 발생할 가능성이 있는 메서드는 해당 예외를 처리하거나, 호출자에게 예외를 넘겨주어야 합니다.

예제: 체크 예외의 발생과 처리

import java.io.*;

public class FileManager {

    // 파일을 읽는 메서드
    public void readFile(String filePath) throws IOException {
        File file = new File(filePath);

        // 파일이 존재하지 않으면 IOException 발생
        if (!file.exists()) {
            throw new IOException("파일이 존재하지 않습니다: " + filePath);
        }

        // 파일 읽기 작업
        BufferedReader reader = new BufferedReader(new FileReader(file));
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
        reader.close();
    }

    public static void main(String[] args) {
        FileManager fileManager = new FileManager();

        try {
            // 체크 예외 처리: IOException을 처리하기 위해 try-catch 사용
            fileManager.readFile("example.txt");
        } catch (IOException e) {
            // 예외 처리: 오류 메시지 출력
            System.err.println("파일을 읽는 동안 오류가 발생했습니다: " + e.getMessage());
        }
    }
}
  • 위 예제는 FileManager 클래스에서 파일을 읽는 readFile 메서드를 구현한 것입니다.
    • 이 메서드는 IOException이라는 체크 예외를 던질 수 있습니다.
    • 이 체크 예외는 파일이 존재하지 않거나 읽기 작업 중 오류가 발생할 때 던져집니다.
  • throws 키워드: readFile 메서드는 throws 키워드를 사용해 IOException을 호출자에게 넘깁니다.
    • 이는 이 메서드를 호출하는 쪽에서 해당 예외를 처리하도록 강제하는 것입니다.
  • 예외 처리: main 메서드에서 readFile을 호출할 때, try-catch 블록을 사용해 IOException을 처리합니다. catch 블록에서는 오류 메시지를 출력하여 예외를 처리합니다.

체크 예외 처리 방법

  • throws를 통한 예외 전파
    • 메서드 시그니처에 throws 키워드를 사용하여 발생할 수 있는 예외를 호출자에게 넘겨줍니다. 이를 통해 예외 처리를 메서드를 호출하는 쪽에서 처리할 수 있게 합니다.
    • 사용 예: public void readFile(String filePath) throws IOException
  • try-catch 블록을 통한 예외 처리
    • 메서드 내부에서 발생할 수 있는 체크 예외를 try-catch 블록을 사용해 처리합니다.
    • 이를 통해 프로그램이 예외 발생 시에도 정상적으로 흐름을 유지하도록 합니다.
    • 사용 예: try { /* 예외 발생 가능 코드 */ } catch (IOException e) { /* 예외 처리 */ }

3. 언체크 예외 (Unchecked Exception)

  • 언체크 예외(Unchecked Exception)는 자바에서 발생할 수 있는 예외 중 컴파일러에 의해 예외 처리 여부가 강제되지 않는 예외를 말합니다.
    • 언체크 예외는 주로 프로그래머의 실수나 잘못된 코드 작성으로 인해 발생하는 예외로, 자바 런타임 시에 발생할 수 있습니다.
    • 이러한 예외는 컴파일러가 체크하지 않기 때문에, 개발자가 필요에 따라 처리할 수 있습니다.
  • 특징
    • 컴파일러에 의해 강제되지 않음: 언체크 예외는 컴파일 시 예외 처리가 강제되지 않습니다. 따라서, 예외 처리를 하지 않아도 컴파일 오류가 발생하지 않습니다.
    • RuntimeException 및 그 하위 클래스: 언체크 예외는 RuntimeException 클래스를 상속하는 모든 예외를 포함합니다. RuntimeExceptionException 클래스의 서브클래스이므로, 언체크 예외도 Throwable 계층 구조의 일부입니다.
    • 프로그래머의 실수로 인해 발생: 언체크 예외는 주로 잘못된 코딩으로 인해 발생하는 예외로, 예외가 발생하지 않도록 코드를 수정하는 것이 바람직한 경우가 많습니다.

예제: 언체크 예외의 발생과 처리

public class UncheckedExceptionExample {

    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};

        try {
            // ArrayIndexOutOfBoundsException 발생 가능
            System.out.println(numbers[5]);
        } catch (ArrayIndexOutOfBoundsException e) {
            // 예외 처리
            System.err.println("배열 인덱스가 잘못되었습니다: " + e.getMessage());
        }

        // NullPointerException 발생 가능
        String text = null;
        try {
            System.out.println(text.length());
        } catch (NullPointerException e) {
            // 예외 처리
            System.err.println("널 포인터 예외 발생: " + e.getMessage());
        }
    }
}
  • 위 예제는 ArrayIndexOutOfBoundsExceptionNullPointerException이라는 두 가지 언체크 예외를 보여줍니다.
    • 이 두 예외 모두 RuntimeException의 하위 클래스이며, 발생 시점에 예외를 처리하지 않아도 컴파일러가 이를 강제하지 않습니다.
    • ArrayIndexOutOfBoundsException: 배열의 잘못된 인덱스에 접근하려 할 때 발생합니다. 예제에서는 배열의 길이를 초과하는 인덱스에 접근하려고 시도하여 예외가 발생합니다.
    • NullPointerException: 객체가 null일 때 해당 객체의 메서드를 호출하려 할 때 발생합니다. 예제에서는 null인 문자열 변수에 대해 length() 메서드를 호출하려고 시도하여 예외가 발생합니다.

언체크 예외의 처리 방법

  • 예외 처리 선택적: 언체크 예외는 컴파일러에 의해 강제되지 않으므로, 필요에 따라 예외를 처리할 수 있습니다.
    • 개발자가 예외가 발생하지 않도록 코드를 작성하는 것이 일반적이지만, 예외가 발생할 수 있는 상황을 명시적으로 처리하기 위해 try-catch 블록을 사용할 수도 있습니다.
  • 코드 수정으로 예외 방지: 언체크 예외는 주로 프로그래머의 실수로 인해 발생하므로, 코드에서 예외가 발생하지 않도록 하는 것이 가장 좋습니다.
    • 예를 들어, 배열에 접근하기 전에 인덱스 범위를 체크하거나, null 여부를 먼저 확인하는 등의 방어적인 코드를 작성하는 것이 좋습니다.
  • 프로그램의 정상적인 흐름 보장: 특정 상황에서 언체크 예외가 발생할 수 있다고 예상되면, 이를 처리함으로써 프로그램이 비정상적으로 종료되지 않도록 할 수 있습니다.
    • 이는 특히 사용자 입력이나 외부 데이터 처리와 관련된 경우 유용할 수 있습니다.

4. 리소스 반환 문제 해결 (finally, try-with-resources)

  • 자바에서 파일, 네트워크 소켓, 데이터베이스 연결과 같은 리소스는 사용 후 반드시 반환해야 합니다.
    • 그렇지 않으면 리소스가 불필요하게 점유되어 메모리 누수와 같은 심각한 문제를 일으킬 수 있습니다.
  • 리소스를 안전하게 반환하기 위해 자바는 finally 블록과 try-with-resources 구문을 제공합니다.

finally 블록

  • finally 블록은 예외가 발생하더라도 반드시 실행되는 블록입니다.
    • try-catch 구문과 함께 사용되며, 주로 리소스를 해제하거나 중요한 정리 작업을 수행하는 데 사용됩니다.
    • 예외가 발생하든 발생하지 않든 상관없이 finally 블록은 항상 실행됩니다.
import java.io.*;

public class FileManager {

    public void readFile(String filePath) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(filePath));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("파일을 읽는 동안 오류가 발생했습니다: " + e.getMessage());
        } finally {
            // 리소스 해제
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    System.err.println("리소스를 해제하는 동안 오류가 발생했습니다: " + e.getMessage());
                }
            }
        }
    }

    public static void main(String[] args) {
        FileManager fileManager = new FileManager();
        fileManager.readFile("example.txt");
    }
}
  • try 블록: 리소스를 사용해 파일을 읽습니다.
  • catch 블록: 파일 읽기 중 발생한 예외를 처리합니다.
  • finally 블록: 파일을 읽은 후 BufferedReader 리소스를 닫아야 합니다. 이 블록은 예외 발생 여부와 상관없이 항상 실행되므로, 리소스 해제에 적합합니다.
  • 하지만 finally 블록을 사용할 때는 개발자가 리소스를 직접 관리해야 하며, 코드가 복잡해질 수 있습니다.

try-with-resources 구문

  • try-with-resources 구문은 자바 7에서 도입된 기능으로, AutoCloseable 인터페이스를 구현한 리소스(예: 파일, 소켓, 데이터베이스 연결)를 자동으로 닫아주는 기능을 제공합니다.
    • 이 구문을 사용하면 finally 블록을 사용하지 않고도 리소스를 자동으로 해제할 수 있어 코드가 더 간결하고 안전해집니다.
    • AutoCloseable 인터페이스는 java.lang에 포함되어 있는 인터페이스이기 때문에, 별도의 import구문 없이 사용하실 수 있습니다.

예시 : try-with-resources를 사용한 리소스 반환

import java.io.*;

public class FileManager {

    public void readFile(String filePath) {
        try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("파일을 읽는 동안 오류가 발생했습니다: " + e.getMessage());
        }
        // try-with-resources 구문은 자동으로 리소스를 해제합니다.
    }

    public static void main(String[] args) {
        FileManager fileManager = new FileManager();
        fileManager.readFile("example.txt");
    }
}
  • try-with-resources 구문: try 블록에 선언된 리소스는 블록이 종료되면 자동으로 닫힙니다.
    • 예외가 발생하더라도 BufferedReader가 자동으로 닫히므로, finally 블록을 사용할 필요가 없습니다.
  • 코드 간결성: 리소스 해제 코드가 필요 없으므로, 코드가 더 간결하고 읽기 쉬워집니다.

try-with-resources의 커스텀 인터페이스 구현 예제

  • try-with-resources 구문을 사용하려면 리소스가 AutoCloseable 인터페이스를 구현(implements)해야 합니다.
  • 또한 try 구문 바로 뒤에 괄호()를 통해 try-with-resources구문을 이용할 수 있습니다.
// CustomResource.java
public class CustomResource implements AutoCloseable {

    public CustomResource() {
        System.out.println("CustomResource가 열렸습니다.");
    }

    public void doSomething() {
        System.out.println("CustomResource에서 작업을 수행 중입니다.");
    }

    @Override
    public void close() {
        System.out.println("CustomResource가 닫혔습니다.");
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        // try-with-resources 구문에서 커스텀 리소스 사용
        try (CustomResource resource = new CustomResource()) {
            resource.doSomething();
        } catch (Exception e) {
            System.err.println("예외 발생: " + e.getMessage());
        }
    }
}
  • 실행 결과
CustomResource가 열렸습니다.
CustomResource에서 작업을 수행 중입니다.
CustomResource가 닫혔습니다.
  • CustomResource 클래스: 이 클래스는 AutoCloseable 인터페이스를 구현하여 close() 메서드를 제공합니다. 이 메서드는 리소스를 닫을 때 자동으로 호출됩니다.
  • doSomething() 메서드: CustomResource 클래스의 메서드로, 리소스를 사용하여 어떤 작업을 수행하는 역할을 합니다.
  • try-with-resources 구문: CustomResource 객체를 try 블록 내에서 생성하고 사용합니다. 블록이 종료되면 자동으로 close() 메서드가 호출되어 리소스가 안전하게 닫힙니다.

5. 실무에서의 예외 처리 방식 (언체크 예외 및 예외 계층 활용)

  • 자바가 처음 설계될 당시에는 체크 예외(Checked Exception)가 프로그램의 신뢰성을 높이는 데 유용할 것이라 믿어졌습니다.
    • 당시 개발자들은 예상 가능한 예외를 명시적으로 처리하도록 강제함으로써, 프로그램의 안정성을 확보할 수 있다고 생각했습니다.
    • 체크 예외는 프로그램이 외부 환경에 의존하는 작업(예: 파일 I/O, 네트워크 연결 등)에서 발생할 수 있는 예외 상황을 명확하게 다루도록 도와줍니다.
  • 그러나 시간이 지나면서 체크 예외가 가진 단점도 명확해졌습니다.
    • 코드가 복잡해지거나, 개발자가 불필요한 예외 처리를 강제당해 코드의 가독성과 유지보수성이 떨어지는 경우가 발생했습니다.
    • 이에 따라 실무에서는 언체크 예외(Unchecked Exception)와 예외 계층을 적절히 활용하는 방향으로 예외 처리 전략이 진화해왔습니다.

언체크 예외의 활용 (Unchecked Exceptions)

  • 언체크 예외의 장점: 언체크 예외는 RuntimeException을 상속하는 예외로, 컴파일러가 예외 처리를 강제하지 않습니다.
    • 개발자는 필요에 따라 예외를 처리하거나, 이를 상위 호출자로 전파할 수 있습니다.
    • 이는 코드의 간결함을 유지하고, 불필요한 예외 처리 코드를 줄여 가독성과 유지보수성을 높입니다.
  • 사용 사례: 실무에서는 프로그래머의 실수로 인해 발생할 수 있는 논리 오류, 예를 들어 NullPointerException, IllegalArgumentException, IndexOutOfBoundsException 등을 처리할 때 언체크 예외를 주로 사용합니다.
  • 전략: 실무에서는 예외 상황을 발생시키기 전에 방어적인 코드를 작성하는 것이 일반적입니다.
    • 예를 들어, 메서드에 전달된 인자가 null이 아닌지 확인하고, 컬렉션의 인덱스 범위를 미리 검사하는 방식입니다.

체크 예외와 언체크 예외의 균형

  • 체크 예외의 단점: 체크 예외는 발생 가능성이 낮은 예외를 매번 처리해야 하므로 코드가 지나치게 장황해지고, 예외 처리가 중복될 수 있습니다.
    • 특히 상위 호출 메서드로 계속 예외를 전파할 때, 많은 메서드에 throws 선언을 추가해야 하는 불편함이 있습니다.
  • 실무에서의 접근법: 실무에서는 체크 예외가 필요 이상으로 사용되지 않도록 주의합니다.
    • 필요할 경우, 체크 예외를 언체크 예외로 변환하여 처리하기도 합니다.
    • 예를 들어, 외부 라이브러리에서 체크 예외를 던지지만, 이를 언체크 예외로 감싸서 애플리케이션 내에서 사용하기도 합니다.

커스텀 예외(Custom Exceptions)

  • 커스텀 예외의 장점: 실무에서는 애플리케이션의 도메인에 맞는 커스텀 예외를 정의하여, 예외 상황을 더욱 명확하게 표현할 수 있습니다.
    • 이를 통해 예외 상황에 대한 의미를 명확히 하고, 문제를 쉽게 파악할 수 있습니다.
  • 예외 계층 구조 활용: 예외의 계층 구조를 설계하여, 특정 범주에 속하는 예외들을 그룹화할 수 있습니다.
    • 예를 들어, 데이터베이스 관련 예외는 DatabaseException이라는 부모 클래스를 만들고, 이를 상속하는 ConnectionException, QueryException 등을 정의할 수 있습니다.
    • 이를 통해 예외 처리를 일관되게 수행할 수 있습니다.
  • 실무 사례: 금융 애플리케이션에서 InsufficientFundsException과 같은 커스텀 예외를 정의하여, 특정 비즈니스 로직에서 발생하는 예외를 처리할 수 있습니다.

예외를 통한 흐름 제어는 피하기

  • 안티 패턴: 예외를 사용해 프로그램의 흐름을 제어하는 것은 일반적으로 안티 패턴으로 간주됩니다.
    • 예외는 예외적인 상황에서 사용되어야 하며, 프로그램의 정상적인 흐름을 제어하는 데 사용해서는 안 됩니다.
  • 대안: 조건문을 사용하여 문제가 될 수 있는 상황을 미리 검사하고, 예외를 통해서는 정말로 예외적인 상황만을 처리하도록 합니다.

런타임 예외 및 Exception을 try-catch문에서 활용

  • 아래 예제에서는 RuntimeExceptionException 같은 상위 클래스를 사용하여 다양한 예외를 포괄적으로 처리하는 방법을 보여줍니다.
    • RuntimeException은 언체크 예외를, Exception은 체크 예외와 언체크 예외를 모두 처리할 수 있습니다.
public class ExceptionHandlingExample {

    public static void main(String[] args) {
        try {
            processInput(null); // NullPointerException 발생 가능
            divideNumbers(10, 0); // ArithmeticException 발생 가능
            accessArrayElement(new int[]{1, 2, 3}, 5); // ArrayIndexOutOfBoundsException 발생 가능
            simulateCheckedException(); // IOException 발생 가능 (체크 예외)
        } catch (RuntimeException e) {
            // RuntimeException과 그 하위 예외를 포괄적으로 처리
            System.err.println("런타임 예외 발생: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        } catch (Exception e) {
            // Exception과 그 하위 예외를 포괄적으로 처리
            System.err.println("예외 발생: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        }
    }

    public static void processInput(String input) {
        if (input == null) {
            throw new NullPointerException("입력 값이 null입니다.");
        }
    }

    public static int divideNumbers(int a, int b) {
        return a / b; // 0으로 나누면 ArithmeticException 발생
    }

    public static int accessArrayElement(int[] array, int index) {
        return array[index]; // 잘못된 인덱스 접근 시 ArrayIndexOutOfBoundsException 발생
    }

    public static void simulateCheckedException() throws Exception {
        throw new Exception("체크 예외 발생!");
    }
}
  • 실행 결과
런타임 예외 발생: NullPointerException - 입력 값이 null입니다.
  • 첫 번째 메서드 processInput(null)에서 NullPointerException이 발생하여 RuntimeException 블록에서 처리됩니다.
    • 이후의 코드는 실행되지 않고 예외가 처리됩니다.
  • try 블록
    • 다양한 예외를 발생시킬 수 있는 메서드들을 호출합니다.
    • processInput(null)NullPointerException을 발생시킬 수 있습니다.
    • divideNumbers(10, 0)ArithmeticException을 발생시킬 수 있습니다.
    • accessArrayElement(new int[]{1, 2, 3}, 5)ArrayIndexOutOfBoundsException을 발생시킬 수 있습니다.
    • simulateCheckedException()Exception을 던질 수 있습니다.
  • catch 블록
    • RuntimeException e: 모든 언체크 예외(RuntimeException과 그 하위 클래스)를 처리합니다.
      • 여기서 NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException을 포괄적으로 처리할 수 있습니다.
    • Exception e: RuntimeException으로 처리되지 않은 모든 예외를 처리합니다.
      • 여기서는 Exception을 직접 던지는 체크 예외(simulateCheckedException에서 발생)와 모든 체크 예외를 처리할 수 있습니다.

마무리

  • 자바의 예외 처리는 프로그램의 안정성을 높이고, 예외적인 상황에서도 정상적인 흐름을 유지하기 위해 필수적인 요소입니다.
  1. 예외 계층: 자바의 예외는 Throwable가 최상위 클래스이며, ErrorException으로 나뉩니다. Error는 복구 불가능한 시스템 오류를, Exception은 프로그램에서 처리 가능한 예외를 나타냅니다.

  2. 체크 예외: 컴파일러가 예외 처리를 강제하는 예외로, 파일 I/O나 네트워크 연결 등 외부 자원에 의존하는 작업에서 주로 발생합니다. throws 키워드를 통해 상위 메서드로 전파하거나, try-catch 블록으로 처리해야 합니다.

  3. 언체크 예외: RuntimeException을 상속하는 예외로, 컴파일러가 예외 처리를 강제하지 않습니다. 주로 프로그래머의 실수로 발생하며, 필요에 따라 예외 처리를 하거나 방어적인 코드를 작성해 예방할 수 있습니다.

  4. 리소스 반환: 리소스 누수를 방지하기 위해 finally 블록이나 try-with-resources 구문을 사용해 리소스를 안전하게 반환해야 합니다. try-with-resources는 자바 7 이후 도입된 기능으로, AutoCloseable을 구현한 리소스를 자동으로 닫아줍니다.

  5. 실무에서의 예외 처리: 실무에서는 체크 예외와 언체크 예외를 상황에 맞게 조합해 사용하며, 필요에 따라 커스텀 예외를 정의해 예외 계층을 설계합니다. 예외를 통해 프로그램 흐름을 제어하는 것은 피해야 하며, 예외는 예외적인 상황에서만 사용해야 합니다.

profile
일 때문에 포스팅은 잠시 쉬어요 ㅠ 바쁘다 바빠 모두들 화이팅! // Machine Learning (AI) Engineer & BackEnd Engineer (Entry)

0개의 댓글