Skip to content

16. 예외 처리(Exception Handling)


1. 키워드

  • 오류(Error)와 예외(Exception)
  • 예외 처리(Exception Handling)
  • try / catch / finally
  • Exception 클래스와 RuntimeException 클래스
  • 예외 발생 및 회피, throw, throws
  • try-with-resources


2. 예외 처리(Exception Handling)

1) 오류(Error)와 예외(Exception)

  • 자바 프로그램을 작성할 때 자바 문법에 맞지 않게 코드를 작성하고 컴파일하려고 하면, 자바 컴파일러는 문법 오류(Syntax Error)를 발생시킨다.
  • 또한, 자바 문법에는 맞게 작성되었다고 하더라도 프로그램이 실행되면서 예상하지 못한 오류가 발생할 수 있다.
  • 이렇게 컴퓨터 시스템이 동작하는 도중에 예상하지 못한 사태가 발생하여 실행 중인 프로그램이 영향을 받는 것을 오류(Error)와 예외(Exception) 두 가지로 구분할 수 있다.
  • 오류는 시스템 레벨에서 프로그램에 심각한 문제를 야기하여 실행 중인 프로그램을 종료시킨다.
  • 이러한 오류는 개발자가 미리 예측하여 처리할 수 없는 것이 대부분이므로, 오류에 대한 처리는 할 수 없다.
  • 하지만 예외는 오류와 마찬가지로 실행 중인 프로그램을 비정상적으로 종료시키지만, 발생할 수 있는 상황을 미리 예측하여 처리할 수 있다.
  • 따라서 개발자는 예외 처리(Exception Handling)를 통해 예외 상황을 처리할 수 있도록 코드의 흐름을 바꿀 필요가 있다.


2) 예외 처리

  • 자바에서는 프로그램이 실행되는 도중 발생하는 예외를 처리하기 위해 try / catch / finally 문을 사용할 수 있다.


try {
 예외를 처리하길 원하는 실행 코드;
} catch (e1) {
 e1 예외가 발생할 경우에 실행될 코드;
} catch (e2) {
 e2 예외가 발생할 경우에 실행될 코드;
}

...

finally {
 예외 발생 여부와 상관없이 무조건 실행될 코드;
}


1] try 블록

  • 기본적으로 맨 먼저 실행되는 코드로 여기에서 발생한 예외는 catch 블록에서 처리된다.

2] catch 블록

  • try 블록에서 발생한 예외 코드나 예외 객체를 인수로 전달받아 그 처리를 담당한다.

3] finally 블록

  • 이 블록은 try 블록에서 예외가 발생하든 안 하든 맨 마지막에 무조건 실행된다.


  • catch 블록과 finally 블록은 선택적인 옵션으로 반드시 사용할 필요는 없다.
  • 따라서 사용할 수 있는 모든 적합한 try 구문은 다음과 같다.


try / catch
try / finally
try / catch / ... / finally


3) 예외 처리 메커니즘

  • 자바에서 예외 처리는 다음과 같은 순서로 진행된다.


1] try 블록에 도달한 프로그램의 제어는 try 블록 내의 코드를 실행한다. 이때 만약 예외가 발생(Throw)하지 않고, finally 블록이 존재하면 프로그램의 제어는 바로 finally 블록으로 이동한다.

2] try 블록에서 예외가 발생하면 catch 핸들러는 다음과 같은 순서로 적절한 catch 블록을 찾게 된다.

2-1] 스택에서 try 블록과 가장 가까운 catch 블록부터 차례대로 검사한다.

2-2] 만약 적절한 catch 블록을 찾지 못하면, 바로 다음 바깥쪽 try 블록 다음에 위치한 catch 블록을 차례대로 검사한다.

2-3] 이러한 과정을 가장 바깥쪽 try 블록까지 계속 검사하게 된다.

2-4] 그래도 적절한 catch 블록을 찾지 못하면, 예외는 처리되지 못한다.

3] 만약 적절한 catch 블록을 찾게 되면, throw 문의 피연산자는 예외 객체의 형식 매개변수로 전달된다.

4] 모든 예외 처리가 끝나면 프로그램의 제어는 finally 블록으로 이동한다.

5] finally 블록이 모두 처리되면, 프로그램의 제어는 예외 처리문 바로 다음으로 이동한다.


  • 다음 그림은 위에서 설명한 예외 처리 메커니즘을 그림으로 표현한 것이다.


001


  • 만약 ①번 try 블록에서 예외가 발생하지 않고, 바깥쪽 try 블록에서도 예외가 발생하지 않으면, ⑥번 finally 블록이 바로 실행될 것이다.
  • 하지만 ①번 try 블록에서 예외가 발생하면, ②번과 ③번 catch 블록에서 해당 예외를 처리할 수 있는 검사하게 된다.
  • 만약 적절한 catch 블록을 찾지 못하면, 바깥쪽 try 블록의 ④번과 ⑤번 catch 블록도 차례대로 검사하게 된다.
  • 이때 해당 예외를 처리할 수 있는 catch 블록을 찾게 되면, 해당 catch 블록을 실행한 후 ⑥번 finally 블록을 실행한다.
  • 하지만 모든 catch 블록이 해당 예외를 처리할 수 없으면, 예외는 처리되지 못한 채 해당 프로그램은 강제 종료될 것이다.


3. 예외 클래스

1) Exception 클래스

  • 자바에서 모든 예외의 조상 클래스가 되는 Exception 클래스는 크게 다음과 같이 구분할 수 있다.


1] RuntimeException 클래스

2] 그 외의 Exception 클래스의 자식 클래스


002


  • RuntimeException 클래스를 상속받는 자식 클래스들(주황색 박스)은 주로 치명적인 예외 상황을 발생시키지 않는 예외들로 구성된다.
  • 따라서 try / catch 문을 사용하기보다는 프로그램을 작성하면서 예외가 발생하지 않도록 주의를 기울이는 편이 좋다.
  • 하지만 그 외의 Exception 클래스에 속하는 자식 클래스들(파란색 박스)은 치명적인 예외 상황을 발생시키므로, 반드시 try / catch 문을 사용하여 예외를 처리해야만 한다.
  • 따라서 자바 컴파일러는 RuntimeException 클래스 이외의 Exception 클래스의 자식 클래스에 속하는 예외가 발생할 가능성이 있는 구문에는 반드시 예외를 처리하도록 강제하고 있다.
  • 만약 이러한 예외가 발생할 가능성이 있는 구문을 예외 처리하지 않았을 때는 컴파일 시 오류를 발생시킨다.


  • 다음 예제는 PrintStream 클래스의 write() 메서드를 사용하여 byte 타입 배열의 모든 요소를 출력하는 예제이다.


byte[] list = { 'a', 'b', 'c' };

System.out.write(list);


  • 하지만 위의 예제에서는 write() 메서드에서 발생할 수 있는 IOException에 대한 예외를 처리하지 않았으므로, 컴파일 시 오류가 발생한다.


  • 따라서 다음 예제와 같이 try / catch 문을 사용하여 IOException에 대한 예외 처리까지 해줘야만 컴파일을 할 수 있다.


byte[] list = { 'a', 'b', 'c' };

try {
  System.out.write(list);
} catch (IOException e) {
  e.printStackTrace();
}

// abc


2) 예외 처리의 계층 관계

  • 자바에서는 예외가 발생하면, try 블록과 가장 가까운 catch 블록부터 순서대로 검사한다.
  • 따라서 여러 개의 catch 블록을 사용할 때는 Exception 클래스의 계층 관계에도 주의를 기울여야만 한다.


try {
 System.out.write(list);
} catch (Exception e) {
 e.printStackTrace();
} catch (IOException e) {
 e.printStackTrace();
}


  • 위의 예제에서 IOException이 발생하면, 자바는 첫 번째 catch 블록부터 순서대로 해당 예외를 처리할 수 있는지를 검사한다.
  • 그런데 IOExceptionException의 자식 클래스이므로, 첫 번째 catch 블록에서도 IOException을 처리할 수 있다.
  • 따라서 IOException을 비롯한 Exception 클래스의 자식 클래스에 해당하는 예외가 발생하면, 언제나 첫 번째 catch 블록에서만 처리될 것이다.
  • 즉, catch 블록의 순서를 위의 예제처럼 작성하면, 두 번째 catch 블록은 영원히 실행되지 못할 것이다.


  • 따라서 IOException만을 따로 처리하고자 한다면, 다음 예제처럼 catch 블록의 순서를 변경해야 한다.


try {
 System.out.write(list);
} catch (IOException e) {
 e.printStackTrace();
} catch (Exception e) {
 e.printStackTrace();
}


  • 위의 예제에서 IOException이 발생하면, 첫 번째 catch 블록에서 해당 예외를 처리할 것이다.
  • 또한, IOException 외의 Exception 클래스의 자식 클래스에 해당하는 예외가 발생하면, 두 번째 catch 블록에서 처리될 것이다.
  • 이처럼 범위가 더 좁은 예외를 처리하는 catch 블록을 먼저 명시하고, 범위가 더 넓은 예외를 처리하는 catch 블록은 나중에 명시해야만 정상적으로 해당 예외를 처리할 수 있다.


3) 여러 예외 타입의 동시 처리

  • Java SE 7부터는 | 기호를 사용하여 하나의 catch 블록에서 여러 타입의 예외를 동시에 처리할 수 있다.


try {
 this.db.commit();
} catch (IOException e) {
 e.printStackTrace();
} catch (SQLException e) {
 e.printStackTrace();
}


  • 즉, Java SE 7부터는 위의 예제를 다음 예제처럼 표현할 수 있게 되었다.


try {
 this.db.commit();
} catch (IOException | SQLException e) {
 e.printStackTrace();
}


  • 하지만 둘 이상의 예외 타입을 동시에 처리하는 catch 블록에서 매개변수로 전달받은 예외 객체는 묵시적으로 final 제어자를 가지게 된다.
  • 따라서 catch 블록 내에서 해당 매개변수에는 어떠한 값도 대입할 수 없다.


4) Throwable 클래스

  • 자바에서 Throwable 클래스는 모든 예외의 조상이 되는 Exception 클래스와 모든 오류의 조상이 되는 Error 클래스의 부모 클래스이다.
  • Throwable 타입과 이 클래스를 상속받은 서브 타입만이 JVM이나 throw 키워드에 의해 던져질 수 있다.


  • 다음 예제는 일부러 숫자를 0으로 나눠 ArithmeticException 오류를 발생시키는 예제이다.
  • 이렇게 발생한 오류에 대해 Throwable 메서드를 사용하여 발생한 오류에 대한 정보를 출력한다.


try {
 System.out.println(5 / 0);
} catch (ArithmeticException e) {
 System.out.println("현재 발생한 예외 정보: " + e.getMessage());
}

// 현재 발생한 예외 정보: / by zero


4. 예외 발생 및 회피

1) 예외 발생시키

  • 자바에서는 throw 키워드를 사용하여 강제로 예외를 발생시킬 수 있다.


Exception e = new Exception("오류메시지");

...

throw e;


  • 위의 예제처럼 생성자에 전달된 문자열은 getMessage() 메서드를 사용하여 오류 메시지로 출력할 수 있다.


2) 예외 회피하기

  • 메서드 선언부에 throws 키워드를 사용하여 해당 메서드를 사용할 때 발생할 수 있는 예외를 미리 명시할 수도 있다.
  • 이렇게 하면 해당 메서드를 사용할 때 발생할 수 있는 예외를 사용자가 충분히 인지할 수 있으며, 그에 대한 처리까지도 강제할 수 있다.
  • 따라서 더욱 안정성 있는 프로그램을 손쉽게 작성할 수 있도록 도와줄 수 있다.


  • 다음 예제는 호출된 메서드에서 발생한 예외를 호출된 메서드에서 처리하는 예제이다.


class Test {

  static void handlingException() {
    try {
      throw new Exception();
    } catch (Exception e) {
      System.out.println("호출된 메서드에서 예외가 처리됨");
    }
  }

  public static void main(String[] args) {
    try {
      handlingException();
    } catch (Exception e) {
      System.out.println("main() 메서드에서 예외가 처리됨!");
    }
  }
}

// 호출된 메서드에서 예외가 처리됨


  • 이때 호출된 메서드의 try / catch 문을 생략하면 컴파일 오류가 발생한다.
  • 또한, 이 메서드를 호출한 main() 메서드는 호출된 메서드에서 예외가 발생한 사실을 알 수 없다.


  • 다음 예제는 throws 키워드를 사용하여 호출된 메서드에서 발생한 예외를 호출한 메서드로 넘기는 예제이다.


class Test {

  static void handlingException() throws Exception {
    throw new Exception();
  }

  public static void main(String[] args) {
    try {
      handlingException();
    } catch (Exception e) {
      System.out.println("main() 메서드에서 예외가 처리됨!");
    }
  }
}

// 호출된 메서드에서 예외가 처리됨!


  • 이렇게 함으로써 호출된 메서드에는 try / catch 문을 생략할 수 있다.
  • 그리고 호출된 메서드에서 발생한 예외를 해당 메서드를 호출한 main() 메서드에서 처리할 수 있게 된다.


3) 사용자 정의 예외 클래스

  • 자바에서는 Exception 클래스를 상속받아 자신만의 새로운 예외 클래스를 정의하여 사용할 수 있다.
  • 사용자 정의 클래스에는 생성자뿐만 아니라 필드 및 메서드도 원하는 만큼 추가할 수 있다.


class MyException extends RuntimeException {

  MyException(String errMsg) {
    super(errMsg);
  }
}


  • 요즘에는 위와 같이 Exception 클래스가 아닌 예외 처리를 강제하지 않는 RuntimeException 클래스를 상속받아 작성하는 경우가 많다.


4) try-with-resources

  • Java SE 7부터는 사용한 자원을 자동으로 해제해 주는 try-with-resources 문을 사용할 수 있다.


try (파일을열거나자원을할당하는명령문) {

 ...

}


  • 위와 같이 try 블록에 괄호를 추가하여 파일을 열거나 자원을 할당하는 명령문을 명시하면, 해당 try 블록이 끝나자마자 자동으로 파일을 닫거나 할당된 자원을 해제해 준다.


  • 다음 예제는 파일에서 문자열을 한 줄 읽어오는 예제이다.


static String readFile(String filePath) throws IOException {
  BufferedReader br = new BufferedReader(new FileReader(filePath));

  try {
    return br.readLine();
  } finally {
    if (br != null) {
      br.close();
    }
  }
}


  • 위와 같이 Java SE 7 이전에서는 finally 블록을 사용하여 사용한 파일을 닫아줘야 했다.


  • 하지만 try-with-resources 문을 사용하면 다음과 같이 자동으로 파일의 닫기를 수행할 수 있다.


static String readFile(String filePath) throws IOException {
  try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
    return br.readLine();
  }
}

References