Exception handling

Ajisai·2023년 7월 26일
0

Java

목록 보기
10/17

크게 둘로 나뉜다.

  • try-catch 블록으로 바로 처리하기
  • 떠넘겨서 다른 쪽(예외 발생 가능 부분을 호출한 쪽)에서 처리하도록 하기

바로 처리하기

try {
    //예외가 발생할 수 있는 코드
} catch(EXCEPTION_NAME e) {
    //예외 처리
} 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라 한다.
주의할 점은 try 블록이나 catch 블록에서 return 문을 쓰더라도 finally 블록은 실행된다는 것이다. 말 그대로 끝내는 실행이 되는 것이다.
이런 특성을 활용해 리소스 처리에 응용할 수도 있는데, 이건 여기를 참고하기 바란다.

여러 가지 예외를 각각 처리해야 하는 경우

try {
    //예외가 발생할 수 있는 코드
} catch(EXCEPTION1 e1) {
    //예외 처리1
} catch(EXCEPTION2 e2) {
    //예외 처리2
} finally {

}

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

try {
    //...
} catch(Exception e) {
    System.out.println("Unknown exception");
} catch(NullPointerException e) { 
    System.out.println("NullPointerException");
} finally {

}

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

애초에 Unreachable catch block이라고 컴파일 에러 뜸.

여러 예외를 한 블록에서 처리하는 경우

try {

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

} finally {

}

예외 떠넘기기

예외를 꼭 바로 처리할 필요는 없다

다음과 같이 메소드를 호출한 곳으로 예외를 떠넘길 수도 있다.

RETURN_TYPE1 method1(param1, param2, ...) throws EXCEPTION1, EXCEPTION2, ... {
    ...
}

RETURN_TYPE2 method2(param1, param2, ...) throws Exception {
    ...
}

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

  1. throws: 예외가 발생하면 떠넘긴다(throws).
  2. try: 예외가 발생하는지 발생하지 않는지 시도(try)한다.
  3. catch: try 블록에서 떠넘긴 예외를 받아(catch) 처리한다.
public void method1() throws Exception {
    ...
}

예를 들어 이런 메소드를 다른 메소드에서 호출하려면
다음과 같이 호출해야 한다.

public void method2() {
    try {
        method1();
    } catch(Exception e) {

    }
}

main()에서 예외 떠넘기기

main()에서도 throws 키워드를 사용해 예외를 떠넘길 수 있다. 이 경우 JVM이 예외를 처리하게 되는데, JVM은 예외를 모니터에 출력함으로써 처리한다.
그런데 사용자의 입장에서는 프로그램을 잘 사용하다가 갑자기 알 수 없는 내용을 출력하고 프로그램이 종료되는 것이므로 바람직한 방법이라고 할 수 없다.
그래서 main()에서는 throw하지 않고 try-catch 블록으로 처리하는 것이 바람직하다.

누구한테 떠넘김?

method에서도 예외를 떠넘길 수 있고, 생성자에서도 떠넘길 수 있다.
여기서 떠넘긴 예외는 호출한 쪽이 받아야 한다.

A가 B를 호출하고, B가 C를 호출하고 있다고 하자.
여기서 B와 C는 메소드든 생성자든 상관없다.
C 내부에서 발생한 예외는 B가 처리할 수도 있고 떠넘길 수도 있다.
B가 처리하지 않고 떠넘긴다면 A에서 처리해야 한다.
A에서 떠넘긴다면 A를 호출한 곳(main 메소드든 뭐든)에서 처리해야 한다.

계속 떠넘기다가 main 메소드에서마저 떠넘기면 JVM이 처리한다.
근데 이게 말이 처리지 사실상 프로그램이 지 혼자 숨는 거라 main 메소드에서는 절대 throw하지 않는다.
정확히는 그냥 이상한 에러 메시지 띄우고 종료돼버린다.

아무튼 예외를 떠넘기는 일련의 과정을 Exception chaining이라 한다.
Exception chaining에 관한 내용은 여기를 참고할 것.

예외 클래스의 메소드

모든 예외 클래스는 다음의 두 메소드를 갖는다.

  • getMessage()
    error message(String)를 얻는 메소드.
  • printStackTrace()
    stack trace를 출력하는 메소드다.
    자세한 내용은 후술한다.

getMessage()

  • 에러 메시지를 String 객체로 반환하는 메소드.
  • 에러 메시지에는 예외 발생 원인에 대한 간단한 설명이 포함된다.
  • 데이터베이스에서 발생하는 오류의 경우 원인을 세분화하기 위해 예외 코드를 포함하기도 한다.
  • 이는 다음과 같이 catch 블록에서 호출해 메시지를 얻을 수 있다.
public void method2() {
    try {
        method1();
    } catch(Exception e) {
        System.out.println(e.getMessage());
    }
}

printStackTrace()

  • stack trace는 프로그램 실행 중에 호출된 메소드의 목록이다.
  • 다시 말해 printStackTrace 메소드는 예외 발생 경로를 추적해 출력하는 메소드다.
  • JVM은 메소드를 호출할 때마다 JVM 스택에 method frame을 추가(push)하고, 메소드가 종료되면 프레임을 제거(pop)한다.
  • 스택 트레이스는 이 JVM 스택의 method frame을 추적(trace)한다는 의미를 포함한다.
  • 때문에 스택 트레이스를 관찰하면 어디서 어떤 메소드를 호출할 때 예외가 발생했는 지 알 수 있다. 이는 예외 처리의 기본이다.

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

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

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

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

스택이기 때문에 출력 결과에서 파란색 부분을 클릭하면 해당 위치로 이동한다.
예를 들어 NumberFormatException.java:65를 클릭하면 NumberFormatException.java의 65번째 줄로 이동한다.

이제 위에서부터 차례대로 읽어보며 원인을 파악하면 된다.
우선 맨 윗줄을 읽어 무슨 예외가 발생했는 지 파악한다. 그리고 아래의 내용을 읽는다.
NumberFormatException.java65번째 줄이 실행되어 NumberFormatException이 발생했고,
NumberFormatException.java65번째 줄은 Integer.java580번째 줄에 의해 실행되었고,
Integer.java의 580번째 줄은 Integer.java의 615번째 줄에 의해 실행되었고,
Integer.java의 615번째 줄은 Example.java의 12번째 줄에 의해 실행되었음을 알 수 있다.

요약하면 다음과 같다.

Integer 클래스의 parseInt() 내부(Integer 클래스 615번째 줄)에서 같은 클래스의 overload된 parseInt()를 호출했는데, 
그 메소드 내부(Integer 클래스 580번째 줄)에서 NumberFormatExceptinon이 발생했다.
이는 문자열 "Three"가 주어졌기 때문이다.

주의할 점은 '스택'이기 때문에 순서를 반대로 생각해야 한다는 것이다. 즉 호출 순서로 따지면 아래에서 위로 호출해나간 것이다.
다시 말해 가장 아래부분이 최초의 호출 부분이므로, 거기부터 위로 타고 들어감으로써 예외가 발생한 경위를 파악할 수 있다.

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

이 부분에 관해서는 여기를 참고하면 좋을 것이다.

profile
Java를 하고 싶었지만 JavaScript를 하게 된 사람

0개의 댓글

관련 채용 정보