본문 바로가기

프로그래밍 공부/Java

Java - 예외 처리

오류(Error)와 예외(Exception)

Java 프로그램을 작성할 때 Java 문법에 맞지 않게 코드를 작성하면 문법 오류(Syntax error)가 발생한다. 또 문법에 맞게 작성되었다 하더라도 프로그램이 실행되면서 예상치 못한 오류가 발생할 수도 있다. 이렇게 시스템이 동작하는 도중에 예상치 못한 사태가 발생해 실행중인 프로그램이 영향을 받는 것을 오류와 예외 두 가지로 구분한다.

오류(Error) : 시스템 레벨에서 프로그램에 심각한 문제를 야기해 실행 중인 프로그램을 종료시킨다. 이런 오류는 개발자가 미리 예측해 처리할 수 없는 것이 대부분이기 때문에 오류에 대한 처리는 할 수 없다.

예외(Exception) : 오류와 마찬가지로 실행 중인 프로그램을 비정상적으로 종료시키지만 발생할 수 있는 상황을 미리 예측해 처리할 수 있다. 따라서 개발자는 예외 처리(Exception handling)를 통해 예외 상황을 처리할 수 있도록 코드의 흐름을 바꿀 필요가 있다.

예외 처리(Exception handling)

JavaScript나 PHP에도 존재하는 try / catch / finally 문을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
// 예외 처리를 위한 try / catch / finally 문
try{
    예외를 처리하길 원하는 실행 코드;
}catch(e1){
    e1 예외가 발생할 경우에 실행될 코드;
}catch(e2){
    e2 예외가 발생할 경우에 실행될 코드;
}
...
finally{
    예외 발생 여부와 상관없이 무조건 실행될 코드;
}
cs

1. try : 기본적으로 맨 먼저 실행되는 코드로 여기에서 발생한 예외는 catch 블록에서 처리
2. catch : try에서 발생한 예외 코드나 예외 객체를 인수로 전달받아 그 처리를 담당
3. finally : try에서 예외가 발생하건 안 하건 맨 마지막에 무조건 실행

catch와 finally는 선택적인 옵션이기 때문에 반드시 사용할 필요는 없다. 따라서 예외 처리의 적합한 문법은 세 가지가 있다.

1. try / catch
2. try / finally
3. try / catch / ... / finally

다른 제어문과는 달리 예외 처리문은 중괄호를 생략하면 안된다.

예외 처리의 순서

1. try에 도달한 프로그램의 제어는 try 내의 코드를 실행한다. 이때 만약 예외가 발생(throw)하지 않고, finally가 존재하면 프로그램의 제어는 finally 처리로 이동한다.
2. try에서 예외가 발생하면 catch 핸들러는 다음과 같은 순서로 적절한 catch를 찾게된다.
2-1. 스택에서 try와 가장 가까운 catch부터 차례대로 검사
2-2. 만약 적절한 catch를 못찾으면 바로 다음 바깥쪽 try 다음에 위치한 catch를 차례대로 검사
3. 적절한 catch를 찾게되면 throw 문의 피 연산자는 예외 객체의 형식 매개변수로 전달
4. 모든 예외 처리가 끝나면 프로그램의 제어는 finally로 이동
5. finally가 모두 처리되면 프로그램의 제어는 예외 처리문 바로 다음으로 이동

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 예외 처리의 메커니즘
try{
    ...
    try{
                            // ①
    }catch(IOException e){    // ②
        ...
    }catch(SQLException e){    // ③
        ...
    }
}catch(ClassNotFoundException e){    // ④
    ...
}catch(IllegalAccessException e){    // ⑤
    ...
}
...
finally{
...    // ⑥
}
cs

만약 번(5행) try에서 예외가 발생하지 않고, 바깥쪽 try에서도 예외가 발생하지 않는다면 ⑥번(18행) finally가 바로 실행이 된다.

하지만 ①번(5행) try에서 예외가 발생하면, 번(6행)과 번(8행) catch에서 해당 예외를 처리할 수 있는지 검사한다. 만약 적절한 catch를 찾지 못하면 바깥쪽 try의 번(11행)과 번(13행) catch도 차례대로 검사하게 된다.

이 때 해당 예외를 처리할 수 있는 catch를 찾으면 해당 catch를 실행한 후 번(18행) finally를 실행한다. 하지만 모든 catch가 해당 예외를 처리할 수 없다면 예외는 처리되지 못하고 해당 프로그램은 강제 종료가 된다.

Exception 클래스

Java에서 모든 예외의 조상 클래스가 되는 Exception 클래스는 아래와 같이 구분한다.

1. RuntimeException 클래스
2. 그 외의 Exception 클래스의 자식 클래스

Exception 클래스
Object
Throwable
Error Exception
IOError IOException
... ...
... ...
... RuntimeException




ArithmeticException
...
NullPointException

RuntimeException 클래스를 상속받는 자식 클래스들은 주로 치명적인 예외 상황을 발생시키지 않는 예외들로 구성된다. 따라서 try / catch문을 사용하기보다는 프로그램을 작성하면서 예외가 발생하지 않도록 주의를 하는게 좋다.

하지만 그 외의 Exception 클래스에 속하는 자식 클래스들은 치명적인 예외 상황을 발생시키므로, 반드시 try / catch문을 사용해 예외를 처리해야한다. 따라서 Java 컴파일러는 RuntimeException 클래스 이외의 Exception 클래스의 자식 클래스에 속하는 예외가 발생할 가능성이 있는 구문에는 반드시 예외를 처리하게 강제한다. 만약 이러한 예외가 발생할 가능성이 있는 구문을 예외 처리를 하지 않았을 때는 컴파일 시 오류를 발생시킨다.

1
2
3
4
5
6
public class Exception {
    public static void main(String[] args){
        byte[] list = {'a''b''c'};
        System.out.write(list);
    }
}
cs

위의 코드는 PrintStream 클래스의 write() 메서드를 사용해 byte 타입의 배열의 모든 요소를 출력하는 코드이다. 하지만 위의 코드에서는 write() 메서드에서 발생할 수 있는 IOException에 대한 예외를 처리하지 않았기 때문에 컴파일시 오류가 발생된다.

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

1
2
3
4
5
6
7
8
9
10
11
public class Exception {
    public static void main(String[] args){
        byte[] list = {'a''b''c'};
        
        try{
            System.out.write(list);
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}
cs

실행 결과는 abc 이다.

예외 처리의 계층 관계

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

1
2
3
4
5
6
7
try{
    System.out.write(list);
}catch(Exception e){
    e.printStackTrace();
}catch(IOException e){
    e.printStackTrace();
}
cs

위의 코드에서 IOException이 발생하면 Java는 첫번째 catch부터 순서대로 해당 예외를 처리할 수 있는지 검사하는데 IOException은 Exception의 자식 클래스이기 때문에 첫 번째 catch에서도 IOException을 처리할 수 있다. 따라서 IOException을 비롯해 Exception 클래스의 자식 클래스에 해당하는 예외가 발생하면 언제나 첫번째 catch에서만 처리될 것이다.

즉 catch의 순서를 위의 예제처럼 작성하면 두 번째 catch는 영원히 실행되지 못한다.

따라서 IOException만을 따로 처리하고자 한다면 아래와 같이 작성해야 한다.

1
2
3
4
5
6
7
try{
    System.out.write(list);
}catch(IOException e){
    e.printStackTrace();
}catch(Exception e){
    e.printStackTrace();
}
cs

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 일반적인 예외 타입 처리
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();
}
cs

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

Throwable 클래스

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

Throwable 클래스의 메서드 설명
String getMessage() 해당 throwable 객체에 대한 자세한 내용을 문자열로 리턴
void printStackTrace() 해당 throwable 객체와 표준 오류 스트림에서 해당 객체의 스택 트레이스를 출력
String toString() 해당 throwable 객체에 대한 간략한 내용을 문자열로 리턴

위는 Throwable 클래스에서 예외나 오류에 관한 다양한 정보를 확인할 수 있는 메서드들이다.

아래의 코드는 일부러 숫자를 0으로 나눠 ArithmeticException 오류를 발생시키는 코드이다. 이렇게 발생한 오류에 대해 Throwable 메서드를 사용해 발생한 오류에 대한 정보를 출력한다.

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

자주 사용되는 예외(Exception) 클래스

클래스명 설명
ClassCastException 수행할 수 없는 타입 변환이 진행될 경우
ArrayIndexOutOfBoundsException 배열에 잘못된 인덱스를 사용해 접근할 경우
NullPointerException null 객체의 인스턴스 메서드를 호출하는 등의 경우
ArithmeticException 산술 연산에서 정수를 0으로 나누는 등의 불가능한 연산을 수행하는 경우

예외 발생 및 회피

Java에서는 throw 키워드로 강제로 예외를 발생시킬 수 있다.

1
2
3
Exception e = new Exception("오류 메세지");
...
throw e;
cs

위의 코드처럼 생성자에 전달된 문자열은 getMessage() 메서드를 사용해 오류 메세지로 출력할 수 있다.

예외 회피

메서드 선언부에 throw 키워드를 사용해 해당 메서드를 사용할 때 발생할 수 있는 예외를 미리 명시할 수도 있다. 이렇게 하면 해당 메서드를 사용할 때 발생할 수 있는 예외를 사용자가 충분히 인지할 수 있고, 그에 대한 처리까지도 강제할 수 있다.

따라서 더욱 안정성 있는 프로그램을 손쉽게 작성할 수 있도록 도와줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Exception {
    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() 메소드에서 예외가 처리됨!");
        }
    }
}
cs

실행 결과는 호출된 메서드에서 예외가 처리됨! 이 출력된다.

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

아래의 코드는 throw 키워드를 사용해 호출된 메서드에서 발생한 예외를 호출한 메서드로 넘기는 코드이다.

1
2
3
4
5
6
7
8
9
10
public class Exception {
    static void handlingException() throws Exception { throw new Exception(); }
        public static void main(String[] args) {
            try {
                handlingException();
            } catch (Exception e) {
                System.out.println("main() 메소드에서 예외가 처리됨!");
        }
    }
}
cs

실행 결과는 main() 메서드에서 예외가 처리됨! 이 출력된다.

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

사용자 정의 예외 클래스

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

1
2
3
4
5
class MyException extends RuntimeException {
    MyException(String errMsg) {
        super(errMsg);
    }
}
cs

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

try-with-resources 문

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

1
2
3
try (파일을열거나자원을할당하는명령문) {
     ...
}
cs

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

1
2
3
4
5
6
7
8
9
10
static String readFile(String filePath) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(filePath));
    try {
        return br.readLine();
    } finally {
        if (br != null){
            br.close();
        }
    }
}
cs

위와 같이 Java SE 7 이전에는 finally 를 사용해 사용한 파일을 닫아줘야 했다. 하지만 try-with-resource 문을 사용하면 아래와 같이 자동으로 파일의 닫기를 수행할 수 있다.

1
2
3
4
5
static String readFile(String filePath) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
        return br.readLine();
    }
}
cs