본문 바로가기

예외 처리

오류에는 여러 가지가 있는데, 자바에서의 오류에는 크게 에러(error)예외(exception)가 있다.

에러는 응용프로그램 실행 시 하드웨어의 문제로 인해 발생하는 오류로, JVM 실행에 문제가 생겼다는 것을 의미한다. 자바 프로그램은 JVM 위에서 실행되기 때문에 프로그램을 아무리 잘 만들었어도 실행 불능이 되고, 개발자는 이에 대처할 방법이 없다.

예외는 사용자의 잘못된 조작이나 개발자의 실수로 인해 발생하는 오류를 말한다. 일단 발생하면 프로그램이 바로 종료된다는 점에서는 에러와 같지만, 예외는 예외 처리(exception handling)를 통해 실행 상태를 유지할 수 있다는 점에서 다르다.

 

예외는 다시 일반 예외(exception)실행 예외(runtime exception)로 나뉜다. 

일반 예외는 컴파일 과정에서 예외 처리 코드가 필요한지 검사하기 때문에 컴파일러 체크 예외(checked exception)이라고도 하며, 예외 처리 코드가 없다면 컴파일 오류가 발생한다.  

실행 예외는 컴파일 과정에서 예외 처리 코드를 검사하지 않는 예외다. 컴파일 시 검사 유무만 다를 뿐, 두 예외 모두 예외 처리는 필요하다. 

 

자바는 예외를 클래스로 관리한다. 프로그램 실행 중 예외가 발생하면 JVM은 해당 예외 클래스로 객체를 생성한 후, 예외 처리 코드에서 예외 객체를 사용할 수 있게 한다.

모든 예외 클래스는  java.lang.Exception  클래스를 상속한다. 일반 예외 클래스는

java.lang.Exception 클래스를 바로 상속하지만, 실행 예외 클래스의 경우   java.lang.RuntimeException  클래스가 java.lang.Exception 클래스를 상속하고 실행 예외 클래스가 java.lang.RuntimeException 클래스를 상속한다.

 

 

 

다음은 일반 예외의 예시다.

ClassNotFoundException: 컴파일된 클래스 파일(*.class)을 찾을 수 없는 경우

InterruptedException: 인터럽트가 발생한 경우

인터럽트는 다른 처리를 위해 실행을 멈추는 것을 의미한다.

 

 

다음은 실행 예외의 예시다.

 

NullPointerException: 객체 참조가 없는 변수, 즉 null 값을 갖는 변수로 객체를 참조하는 경우

ArrayIndexOutOfBoundsException: 배열에서 인덱스 범위를 초과해 접근하는 경우

NumberFormatException: 숫자로 변환할 수 없는 문자열을 숫자로 변환하는 경우

ClassCastException: 타입 변환(casting)이 불가능한 경우

 

NullPointerException은 가장 많이 발생하는 예외 중 하나다. 특히 객체 배열이나 컬렉션에서 발생하기 쉽다. 배열에서 발생하는 경우는 https://t0pli.tistory.com/98의 마지막 부분 참고.

 

타입 변환은 상위 클래스와 하위 클래스 간에서, 인터페이스와 구현 클래스 간에서만 가능하다. 이런 관계가 아닌데 타입 변환을 하면 ClassCastException이 발생한다.

 

 

예외는 다음과 같은 형식으로 처리할 수 있다. 

 

try {

    //예외가 발생할 수 있는 코드

} catch(EXCEPTION NAME) {

    //예외 처리

} finally {

    //예외 발생 유무에 관계 없이 무조건 실행

}

 

예를 들어  NumberFormatException 은 다음과 같이 처리할 수 있다.

 

String str = "Three";

 

try {

      int num = Integer.parseInt(str);

      System.out.println("Result: " + num); //변환 결과 출력

} catch(NumberFormatException e) {

      System.out.println("Failure"); //예외 발생 시 실패 메시지 출력

} finally {

      System.out.println("String: " + str); //원본 문자열은 무조건 출력

}

 

위와 같은 형식을  try-catch-finally block 이라 한다.

주의할 점은 try 블록이나 catch 블록에서 return 문을 쓰더라도 finally 블록은 실행된다는 것이다. 또한 finally 블록은 생략할 수 있다(catch 블록은 반드시 존재해야 한다).

 

 

여러 가지 예외를 각각 처리해야 하는 경우에는 다음과 같이 작성한다.

 

try {

    //예외가 발생할 수 있는 코드

} catch(EXCEPTION1 NAME1) {

    //예외 처리1

} catch(EXCEPTION2 NAME2) {

    //예외 처리2

finally {

 

}

 

이 경우 예외를 처리할 catch 블록은 위에서부터 차례대로 검색되기 때문에 상위 예외 처리 코드를 아래에 적어야 한다. 다시 말해 다음은 바람직하지 않다.

 

try {

 

catch(Exception e) {

    System.out.println("Unknown exception");

catch(NullPointerException e) { 

    System.out.println("NullPointerException");

} finally {

 

}

 

 NullPointerException  클래스는  Exception  클래스를 상속한다.  따라서 만약 Exception의 처리 코드를 먼저 작성한다면 NullPointerException이 발생하더라도 Exception의 처리 코드가 실행되므로 NullPointerException의 처리 코드는 무용지물이 된다.

 

 

여러 예외를 한 번에 처리할 경우 다음과 같이 작성한다.

 

 try {

 

catch(EXCEPTION1 | EXCEPTION2 | ... | EXCEPTIONn e) {

 

finally {

 

}

 

 

꼭 예외를 바로 처리할 필요는 없다. 다음과 같이 메소드를 호출한 곳으로 예외를 떠넘길 수도 있다.

 

반환형 메소드명(매개변수1, 매개변수2, ...) throws 예외1, 예외2, ... {

    ...

}

 

반환형 메소드명(매개변수1, 매개변수2, ...) throws Exception {

    ...

}

 

throws 키워드는 호출한 곳으로 예외를 떠넘긴다는 의미를 갖는다. 때문에 이 메소드는 반드시 try 블록에서 호출되어야 한다. try 블록에서 호출되어 예외가 발생하면 catch 블록에서 그 예외를 받아 처리하는 것이다. 키워드의 의미로 접근하면 다음과 같다.

 

① try: 예외가 발생하는지 발생하지 않는지 시도(try)한다.

② throws: 예외가 발생하면 떠넘긴다(throws).

③ catch: try 블록에서 떠넘긴 예외를 받아(catch) 처리한다.

 

 

예를 들어 다음 메소드를 다른 메소드에서 호출하려면

 

public void method1() throws Exception {

    ...

}

 

다음과 같이 호출해야 한다.

 

public void method2() {
    try {

        method1();

    } catch(Exception e) {

 

    }

}

 

 

main()에서도 throws 키워드를 사용해 예외를 떠넘길 수 있다. 이 경우 JVM이 예외를 처리하게 되는데, JVM은 예외를 모니터에 출력함으로써 예외를 처리한다. 

그런데 사용자의 입장에서는 프로그램을 잘 사용하다가 갑자기 알 수 없는 내용을 출력하고 프로그램이 종료되는 것이기 때문에 바람직한 방법이라고 할 수 없다. 그래서 main()에서 try-catch 블록으로 처리하는 것이 바람직하다. 

 

모든 예외 클래스에는  getMessage  메소드와  printStackTrace  메소드가 있다. 이름 그대로 에러 메시지 정보를 얻는 메소드와 스택 트레이스를 출력하는 메소드다. 스택 트레이스는 잠시 후에 설명한다.

 

먼저 getMessage 메소드는 에러 메시지를 String 객체로 반환하는 메소드다. 에러 메시지에는 예외 발생 원인에 대한 간단한 설명이 포함된다. 데이터베이스에서 발생하는 오류의 경우 원인을 세분화하기 위해 예외 코드를 포함하기도 한다. 이는 다음과 같이 catch 블록에서 얻을 수 있다.

 

public void method2() {
    try {

        method1();

    } catch(Exception e) {

        System.out.println(e.getMessage());

    }

}

 

 

스택 트레이스는 프로그램이 실행 중에 호출한 메소드의 목록이다. 다시 말해 printStackTrace 메소드는 예외 발생 경로를 출력하는 메소드다.

 

JVM은 메소드를 호출할 때마다 JVM 스택에 프레임(frame)을 추가(push)하고 메소드가 종료되면 프레임을 제거(pop)한다. 스택 트레이스는 이 JVM 스택을 추적(trace)한다는 의미를 포함한다. 

때문에 스택 트레이스를 관찰하면 어디서 어떤 메소드를 호출할 때 예외가 발생했는 지 알 수 있다. 이는 예외 처리의 기본이다.

 

printStackTrace 메소드는 다음과 같이 한 라인에 하나의 프레임을 표현한다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package example;
 
public class Example {
 
    public static void main(String[] args) {
        String str = "Three";
        
        try {
            int num = Integer.parseInt(str);
            System.out.println("변환 결과: " + num);
        } catch(NumberFormatException e) {
            e.printStackTrace();
        }
    }
}
cs

 

 

위의 출력 결과가 스택 트레이스다. 대략적인 내용은 다음과 같다.

 

Example.java의 12번째 줄에서 입력된 문자열 "Three"에 의해 NumberFoarmatException이 발생했다.

 

스택이기 때문에 

출력 결과에서 파란색 부분을 클릭하면 해당 위치로 이동한다.

예를 들어 NumberFormatException.java:65를 클릭하면 NumberFormatException.java의 65번째 줄로 이동한다.

 

이제 위에서부터 차례대로 읽어보며 원인을 파악하면 된다. 

우선 맨 윗줄을 읽어 무슨 예외가 발생했는 지 파악한다. 그리고 아래의 내용을 읽는다.

NumberFormatException.java의 65번째 줄이 실행되어 NumberFormatException이 발생했고,

NumberFormatException.java의 65번째 줄은 Integer.java의 580번째 줄에 의해 실행되었고,

Integer.java의 580번째 줄은 Integer.java의 615번째 줄에 의해 실행되었고,

Integer.java의 615번째 줄은 Example.java의 12번째 줄에 의해 실행되었음을 알 수 있다.

 

정리하면 다음과 같다.

 

Integer 클래스의 parseInt() 615번째 줄에서 같은 클래스의 parseInt()를 호출했는데, 그 메소드의 580번째 줄에서 NumberFormatExceptinon이 발생했다.

 

주의할 점은 '스택'이기 때문에 순서를 반대로 생각해야 한다는 것이다. 즉 호출 순서로 따지면 아래에서 위로 호출해나간 것이다.

 

예시에서는 스택 트레이스가 간단하기 때문에 읽어내는 데에 큰 어려움이 없지만, 규모가 큰 프로그램에서는 스택 트레이스가 수십 줄이 넘을 수도 있다. 때문에 스택 트레이스 해석에 익숙해지는 것이 좋다. 

 

 

참고: https://okky.kr/article/338405

 

'Programming > Java' 카테고리의 다른 글

압축파일탐색기  (0) 2021.03.11
사용자 정의 예외  (0) 2019.07.07
JVM  (0) 2019.07.04
Managed code  (0) 2019.07.04
IO 입출력  (0) 2019.05.23