본문 바로가기

java

[230827] Exception에 관해서 (2) - StackTrace

예외는 JVM에서 발생하거나, 프로그램에서 throw문에 의해 발생할 수 있다. 예외 상황이 발생하였을 때 예외 클래스의 인스턴스가 생성되며, 스택트레이스 데이터나 오류에 대한 메시지 문자열도 이 시점에 생성된다. 

 

이번 글에서는 스택트레이스가 무엇인지, 그리고 스택트레이스 요소를 저장하는 클래스인 StackTraceElement에 대해 정확히 알아보려고 한다. 


StackTrace


스택트레이스란 예외 발생 시점 전까지 호출된 메서드의 목록을 말한다. 

 

예외를 강제 발생시키는 테스트 코드를 작성해서 스택트레이스를 찍어보았다. 

public static void main(String[] args) {
    // 존재하지 않는 파일 경로를 설정해서 예외를 강제로 발생시킴 
    File file = new File("C:\\notExistFile.txt");

    try {
        FileInputStream in = new FileInputStream(file);
        int i;
        
        in.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }

}

위 코드를 실행하면 예외가 발생하며 아래처럼 지정된 파일을 찾을 수 없다는 스택트레이스 로그가 남는다. 

위 로그는 printStackTrace()를 호출했을 때의 기본 포맷 형식을 따르고 있다. 

첫 줄에는 Exception명과 발생한 이유가 적혀있고, 두번째 줄부터 스택트레이스가 찍힌다. 

스택트레이스는 에러가 발생한 메서드부터 시작해서 실행 역순으로 찍힌다. (스택이므로 당연하다.) 

위 예시대로면, main 함수의 10라인에서 FileInputStream객체를 생성했을 때, 

FileInputStream 객체의 생성자 안에서 open 메서드가 실행되었을 때 네이티브 메서드 안에서 예외가 발생한 것이다. 

 

우리 입장에서는 예외를 추적할 때 스택트레이스의 맨 아래부터 위로 올라가면서 예외가 발생한 원인을 추적하면 된다.

스텍트레이스는 예외를 추적하는 데에 가장 핵심적인 요소다. 

 

 

StackTraceElement


이름에서 알 수 있듯이 StackTraceElement는 스택트레이스 각각의 요소를 의미하는 클래스다.

Throwable.getStackTrace() 를 사용해서 예외에 대한 스택트레이스를 StackTraceElement[]로 반환받을 수 있다. 

 

public static void main(String[] args) {

    File file = new File("C:\\notExistFile.txt");

    try {
        FileInputStream in = new FileInputStream(file);

        in.close();
    } catch (FileNotFoundException e) {
        // FileNotFoundException의 이름, 예외 발생 이유를 출력 
        System.out.println(e.getClass().getName() + ": " + e.getMessage());
        
        // StackTraceElement 배열을 가져옴 
        StackTraceElement[] stackTraceElements = e.getStackTrace();

        for(StackTraceElement element : stackTraceElements){
            // StackTraceElement에 toString이 재정의되어있어서 그냥 출력해도 정리된 포맷으로 정보를 볼 수 있다.
            System.out.println(element);
        }

    } catch (IOException e) {
        e.printStackTrace();
    }

}

위 코드의 실행결과는 아래와 같다. 

보기에 예쁘지는 않지만 printStackTrace()를 수동으로 구현한 결과가 나온다. 

printStackTrace도 내부적으로 유사하게 구현되어있을 걸 예상할 수 있다. 

 

 

정리


- java의 모든 예외의 상위클래스는 Throwable인데, Throwable은 예외의 이름이나 예외가 발생한 원인, 스택트레이스 데이터를 가지고 있다. 

- 스택트레이스는 예외가 발생하기 전까지 호출된 메서드의 목록으로, StackTraceElement 라는 클래스로 저장된다. 

-  Throwable.getStackTrace() 를 사용해서 스택트레이스 데이터에 직접 접근할 수도 있다. 

 

 

주의할 점


그렇기 때문에, 프로그램에서 예외가 여러 번 발생해도 스택 트레이스는 가장 마지막에 발생한 예외에 대해서만 찍힌다. 

"first"에 대한 예외는 catch단에서 후처리가 되지 않았고, "second"에 대한 예외의 스택트레이스에는 "first"에 대한 정보가 없어서 "second"에 대한 예외만 로그로 남은 걸 볼 수 있다. 

 

따라서 실제로 개발할 때 try 블럭 안에서 예외가 발생하고 catch 블럭 안에서 또 예외가 발생하는 상황이 일어나지 않도록 주의해야 한다. 예를 들면 아래와 같은 상황이 있다. 

public static void main(String[] args) throws Exception {
    BufferedReader br = null;
    try {
        br = new BufferedReader(new FileReader("path"));
        // 1. 여기서 예외가 발생했다고 가정할 때 
        br.readLine();
    } catch (Exception e) {
        if(br != null){
            // 2. 여기서도 예외가 발생한다면 1에 대한 예외는 체크할 수 없다. 
            br.close();
        }
        e.printStackTrace();
    }
}

입출력 스트림 객체를 사용한 후에는 객체를 close()로 닫아야 하는데, close()를 할 때 예외가 발생한다면 br.readLine()에서 발생한 예외는 스택트레이스에 안 남아서 추적이 불가능하다. (물론 위와 같은 경우는 try-with-resources를 사용해서 해결할 수 있다.) 이러면 개발자 입장에서는 예외가 발생했는데도 모르는 상황이 발생한다. 

 

스택트레이스를 잘 남겨야 이슈가 발생했을 때 추적하기 쉽다. try-catch를 사용할 때 catch 단에서도 예외가 발생하지 않도록 주의해야 한다. 

 

 

참고자료


https://stackoverflow.com/questions/3988788/what-is-a-stack-trace-and-how-can-i-use-it-to-debug-my-application-errors

https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html#getStackTrace--

https://docs.oracle.com/javase/8/docs/api/java/lang/StackTraceElement.html

이펙티브 자바 item 9