프로그램 개발 시, 우리는 우리의 예상대로 동작하지 않는 수많은 상황을 마주치게 됩니다.
그중에서도 예외 발생 시, StackTrace를 통해 디버깅을 하곤 하는데요.
StackTrace를 더 잘보기 위해 StackTrace에 대해 알아봅시다!
Java 예외는 어떻게 터질까?
JVM은 예외가 발생했을 경우, 예외를 처리할 수 있는 try-catch 블록을 찾습니다.
try-catch 블록을 사용하여 코드 내에서 명시적으로 처리되지 않을 경우, 예외는 전파되는데요.
쓰레드 내에서 처리시킬 try-catch 블록가 없어 끝까지 처리되지 않은 예외는 결국 스레드의 실행을 중단시키는 예외를 말합니다.
해당 처리되지 않은 예외를 처리하기 위해 JVM은 아래의 Thread 클래스의 dispatchUncaughtException() 메서드를 호출합니다.

해당 메서드의 설명을 보면 "처리되지 않는 메서드를 Handler에게 전송한다"라고 작성되어 있습니다. 또, 해당 메서드는 JVM만 호출할 수 있다고 합니다.
즉 다시 말해, 잡히지 않은 예외가 발생했을 때 JVM은 dispatchUncaughtException() 메서드를 통해 예외를 처리할 핸들러에게 예외를 전달합니다.
UncaughtExceptionHandler
예외를 전달받은 핸들러는 UncaughtExceptionHandler 라는 핸들러인데요.
마찬가지로 Thread 클래스 내부에서 선언이 되어있습니다.

해당 핸들러는 스레드가 잡히지 않은 예외로 인해 갑자기 종료될 때 호출되는 핸들러용 인터페이스입니다.
스레드에 UncaughtExceptionHandler가 명시적으로 설정되어 있지 않은 경우, 해당 스레드의 ThreadGroup 객체가 UncaughtExceptionHandler 역할을 합니다.

Handler로 지정된 ThreadGroup의 uncaughException() 메서드를 보면 다음의 과정으로 실행이 됩니다.
- 부모 thread group을 확인하고 없다면, 기본 uncaught exception 핸들러가 설치되어 있는지 확인합니다.
- 그것도 아니라면, 이 메서드는 Throwable 인자가 ThreadDeath의 인스턴스인지 확인합니다.
- 여기서 ThreadDeath라는 것은 Thread.stop() 메서드가 호출된 상태인 Thread를 말합니다.
- 만약 ThreadDeath라면 멈춰버린 쓰레드임으로 특별한 처리를 하지 않습니다.
- 아니라면, 드디어 발생된 예외의 스레드의 이름과 함께 StackTrace를 호출합니다
-
- 스레드의 이름 (thread의 getName 메서드에서 반환된 값)
- 스택 백트레이스 (Throwable의 printStackTrace 메서드를 사용)
StackTrace는 어떻게, 무엇을 출력할까?
Throwable의 printStackTrace에 대해 알아보기 전에, StackTrace에 대해 알아보면 좋을 것 같은데요.
StackTrace와 StackTraceElement
StackTrace는 Stack + Trace로 스택 추적을 말합니다. 즉, 호출된 함수들의 경로를 추적할 수 있도록 하는 것인데요.
Java에는 StackTraceElement 라는 클래스가 있습니다.
해당 클래스는 조금 있다가 알아볼 Throwable의 printStackTrace에서 사용되는 getStackTrace 반환값이기도 한데요.

가장 기본적인 스택 프레임 표현 클래스인 StackTraceElement는 Stack Frame인 실행 지점을 의미합니다.
따라서 메소드 이름, 파일 이름, 라인 번호 등 기본 정보를 제공합니다.
StackTraceElement은 보통 Throwable 클래스에서 생성됩니다.
StackTraceElement외에도 Frame을 나타내는 또 다른 StackWalker.StackFrame 인터페이스와 그 구현체 StackFrameInfo 클래스 등이 있습니다.
각각의 구현체마다 제공하는 기능은 조금씩 다르기 때문에 더 자세하게 보고싶다면 클래스 파일을 참고하시면 좋을 것 같습니다.
일반적인 예외 처리와 로깅: StackTraceElement
실행 중인 스택에 대한 세부적인 분석: StackWalker.StackFrame 및 StackFrameInfo
Throwable의 printStackTrace
자바의 StackTraceElement에 대해 알아보았으니, 다시 돌아와서 예외 핸들링 중 최종적으로 실행된 Throwable의 printStackTrace에 대해 알아봅시다.

printStackTrace() 메서드를 보면 타고타고 들어가서 PrintStreamOrWriter라는 인자를 받는 메서드로 이동합니다.
해당 메서드는 다음의 과정을 거치는데요.
- Set을 생성하여 이미 처리된 Throwable 객체를 추적하고 현재 Throwable 객체를 이 Set에 추가
- 출력 스트림(s)에 대한 동기화된 블록을 시작
- 현재 예외 정보 및 스택 트레이스 출력
- 억제된 예외(Suppressed Exceptions) 출력
- 원인 예외(Cause) 출력
즉, 예외 출력 시 현재 예외 트레이스 + Suppressed Exceptions + Cause Exceptions 의 내용들이 출력된다는 것을 알 수 있습니다.
Suppressed Exceptions과 Cause Exceptions
Suppressed Exceptions와 Cause Exceptions는 무엇일까요?
Suppressed Exceptions
억제된 예외는 발생했지만 어떻게든 무시되는 예외를 말합니다. 원래는 무시되었기 때문에 Stack Trace에도 찍히지 않는데요.
예제로 보자면, try문 안의 FileInputStream이 실행되는 과정에서 FileNotFoundException이 발생된다면 FileNotFoundException이 Suppressed Exception 라고 볼 수 있습니다.

이런 경우에 FileNotFoundException에 대한 프레임은 확인할 수 없기 때문에 사용자가 진짜 문제를 확인하기 어려운데요,
때문에 무시된 예외라고 하더라도 StackTrace에 추가하는 메서드인 Throwable.addSuppressed()를 통해 원인을 출력해줄 수 있습니다.
Exception in thread "main" java.lang.Exception: Something happened
at Foo.bar(Foo.java:10)
at Foo.main(Foo.java:5)
Suppressed: Resource$CloseFailException: Resource ID = 0
at Resource.close(Resource.java:26)
at Foo.bar(Foo.java:9)
... 1 more
+) 자바 try-wth-resource는 자동으로 Suppressed Exception들를 addSuppressed 해줍니다.
Cause Exceptions
TestApplicationTests > contextLoads() FAILED
36 java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
37 Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1788
38 Caused by: jakarta.persistence.PersistenceException at AbstractEntityManagerFactoryBean.java:421
39 Caused by: org.hibernate.exception.JDBCConnectionException at SQLStateConversionDelegate.java:100
40 Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException at SQLError.java:165
41 Caused by: com.mysql.cj.exceptions.CJCommunicationsException at Constructor.java:499
42 Caused by: java.net.ConnectException at Net.java:-2
개발을 하다보면 위와 같은 스택 트레이스를 굉장히 많이 보셨을 겁니다. Caused by 라는 이름에서 알 수 있듯 어떤 예외의 원인이 되는 예외 정도만 생각했었는데요.

맞습니다. Cause는 원인이 존재하지 않거나 알 수 없는 경우 이 Throw 가능 객체의 Cause를 반환합니다.
만약 아래처럼 중첩된 예외가 발생한다면,
public class Junk {
public static void main(String args[]) {
try {
a();
} catch(HighLevelException e) {
e.printStackTrace();
}
}
static void a() throws HighLevelException {
try {
b();
} catch(MidLevelException e) {
throw new HighLevelException(e);
}
}
static void b() throws MidLevelException {
c();
}
static void c() throws MidLevelException {
try {
d();
} catch(LowLevelException e) {
throw new MidLevelException(e);
}
}
static void d() throws LowLevelException {
e();
}
static void e() throws LowLevelException {
throw new LowLevelException();
}
}
class HighLevelException extends Exception {
HighLevelException(Throwable cause) { super(cause); }
}
class MidLevelException extends Exception {
MidLevelException(Throwable cause) { super(cause); }
}
class LowLevelException extends Exception {
}
Caused by 구문을 통해 중첩된 에러들이 모두 발생하는 것을 알 수 있습니다.
HighLevelException: MidLevelException: LowLevelException
at Junk.a(Junk.java:13)
at Junk.main(Junk.java:4)
Caused by: MidLevelException: LowLevelException
at Junk.c(Junk.java:23)
at Junk.b(Junk.java:17)
at Junk.a(Junk.java:11)
... 1 more
Caused by: LowLevelException
at Junk.e(Junk.java:30)
at Junk.d(Junk.java:27)
at Junk.c(Junk.java:21)
... 3 more
지금까지 자바의 명시적으로 처리되지 않는 예외를 처리하는 방법과 스택 트레이스가 어떻게 구성되는지를 알 수 있었습니다.
에러 상황 디버깅하실 때 도움이 되셨다면 좋겠네요 !
파이팅🔥
'JAVA 🎻' 카테고리의 다른 글
Collection과 Collections의 차이 (2) | 2024.03.17 |
---|---|
[Java] StackTrace란 (0) | 2023.10.12 |
내가 보려고 정리한 Map의 메서드 (0) | 2023.08.18 |
Function Interface과 Lambda Expression (0) | 2023.08.17 |
[JAVA] Reactive Streams, Back Pressure란? (0) | 2023.08.05 |
프로그램 개발 시, 우리는 우리의 예상대로 동작하지 않는 수많은 상황을 마주치게 됩니다.
그중에서도 예외 발생 시, StackTrace를 통해 디버깅을 하곤 하는데요.
StackTrace를 더 잘보기 위해 StackTrace에 대해 알아봅시다!
Java 예외는 어떻게 터질까?
JVM은 예외가 발생했을 경우, 예외를 처리할 수 있는 try-catch 블록을 찾습니다.
try-catch 블록을 사용하여 코드 내에서 명시적으로 처리되지 않을 경우, 예외는 전파되는데요.
쓰레드 내에서 처리시킬 try-catch 블록가 없어 끝까지 처리되지 않은 예외는 결국 스레드의 실행을 중단시키는 예외를 말합니다.
해당 처리되지 않은 예외를 처리하기 위해 JVM은 아래의 Thread 클래스의 dispatchUncaughtException() 메서드를 호출합니다.

해당 메서드의 설명을 보면 "처리되지 않는 메서드를 Handler에게 전송한다"라고 작성되어 있습니다. 또, 해당 메서드는 JVM만 호출할 수 있다고 합니다.
즉 다시 말해, 잡히지 않은 예외가 발생했을 때 JVM은 dispatchUncaughtException() 메서드를 통해 예외를 처리할 핸들러에게 예외를 전달합니다.
UncaughtExceptionHandler
예외를 전달받은 핸들러는 UncaughtExceptionHandler 라는 핸들러인데요.
마찬가지로 Thread 클래스 내부에서 선언이 되어있습니다.

해당 핸들러는 스레드가 잡히지 않은 예외로 인해 갑자기 종료될 때 호출되는 핸들러용 인터페이스입니다.
스레드에 UncaughtExceptionHandler가 명시적으로 설정되어 있지 않은 경우, 해당 스레드의 ThreadGroup 객체가 UncaughtExceptionHandler 역할을 합니다.

Handler로 지정된 ThreadGroup의 uncaughException() 메서드를 보면 다음의 과정으로 실행이 됩니다.
- 부모 thread group을 확인하고 없다면, 기본 uncaught exception 핸들러가 설치되어 있는지 확인합니다.
- 그것도 아니라면, 이 메서드는 Throwable 인자가 ThreadDeath의 인스턴스인지 확인합니다.
- 여기서 ThreadDeath라는 것은 Thread.stop() 메서드가 호출된 상태인 Thread를 말합니다.
- 만약 ThreadDeath라면 멈춰버린 쓰레드임으로 특별한 처리를 하지 않습니다.
- 아니라면, 드디어 발생된 예외의 스레드의 이름과 함께 StackTrace를 호출합니다
-
- 스레드의 이름 (thread의 getName 메서드에서 반환된 값)
- 스택 백트레이스 (Throwable의 printStackTrace 메서드를 사용)
StackTrace는 어떻게, 무엇을 출력할까?
Throwable의 printStackTrace에 대해 알아보기 전에, StackTrace에 대해 알아보면 좋을 것 같은데요.
StackTrace와 StackTraceElement
StackTrace는 Stack + Trace로 스택 추적을 말합니다. 즉, 호출된 함수들의 경로를 추적할 수 있도록 하는 것인데요.
Java에는 StackTraceElement 라는 클래스가 있습니다.
해당 클래스는 조금 있다가 알아볼 Throwable의 printStackTrace에서 사용되는 getStackTrace 반환값이기도 한데요.

가장 기본적인 스택 프레임 표현 클래스인 StackTraceElement는 Stack Frame인 실행 지점을 의미합니다.
따라서 메소드 이름, 파일 이름, 라인 번호 등 기본 정보를 제공합니다.
StackTraceElement은 보통 Throwable 클래스에서 생성됩니다.
StackTraceElement외에도 Frame을 나타내는 또 다른 StackWalker.StackFrame 인터페이스와 그 구현체 StackFrameInfo 클래스 등이 있습니다.
각각의 구현체마다 제공하는 기능은 조금씩 다르기 때문에 더 자세하게 보고싶다면 클래스 파일을 참고하시면 좋을 것 같습니다.
일반적인 예외 처리와 로깅: StackTraceElement
실행 중인 스택에 대한 세부적인 분석: StackWalker.StackFrame 및 StackFrameInfo
Throwable의 printStackTrace
자바의 StackTraceElement에 대해 알아보았으니, 다시 돌아와서 예외 핸들링 중 최종적으로 실행된 Throwable의 printStackTrace에 대해 알아봅시다.

printStackTrace() 메서드를 보면 타고타고 들어가서 PrintStreamOrWriter라는 인자를 받는 메서드로 이동합니다.
해당 메서드는 다음의 과정을 거치는데요.
- Set을 생성하여 이미 처리된 Throwable 객체를 추적하고 현재 Throwable 객체를 이 Set에 추가
- 출력 스트림(s)에 대한 동기화된 블록을 시작
- 현재 예외 정보 및 스택 트레이스 출력
- 억제된 예외(Suppressed Exceptions) 출력
- 원인 예외(Cause) 출력
즉, 예외 출력 시 현재 예외 트레이스 + Suppressed Exceptions + Cause Exceptions 의 내용들이 출력된다는 것을 알 수 있습니다.
Suppressed Exceptions과 Cause Exceptions
Suppressed Exceptions와 Cause Exceptions는 무엇일까요?
Suppressed Exceptions
억제된 예외는 발생했지만 어떻게든 무시되는 예외를 말합니다. 원래는 무시되었기 때문에 Stack Trace에도 찍히지 않는데요.
예제로 보자면, try문 안의 FileInputStream이 실행되는 과정에서 FileNotFoundException이 발생된다면 FileNotFoundException이 Suppressed Exception 라고 볼 수 있습니다.

이런 경우에 FileNotFoundException에 대한 프레임은 확인할 수 없기 때문에 사용자가 진짜 문제를 확인하기 어려운데요,
때문에 무시된 예외라고 하더라도 StackTrace에 추가하는 메서드인 Throwable.addSuppressed()를 통해 원인을 출력해줄 수 있습니다.
Exception in thread "main" java.lang.Exception: Something happened
at Foo.bar(Foo.java:10)
at Foo.main(Foo.java:5)
Suppressed: Resource$CloseFailException: Resource ID = 0
at Resource.close(Resource.java:26)
at Foo.bar(Foo.java:9)
... 1 more
+) 자바 try-wth-resource는 자동으로 Suppressed Exception들를 addSuppressed 해줍니다.
Cause Exceptions
TestApplicationTests > contextLoads() FAILED
36 java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
37 Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1788
38 Caused by: jakarta.persistence.PersistenceException at AbstractEntityManagerFactoryBean.java:421
39 Caused by: org.hibernate.exception.JDBCConnectionException at SQLStateConversionDelegate.java:100
40 Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException at SQLError.java:165
41 Caused by: com.mysql.cj.exceptions.CJCommunicationsException at Constructor.java:499
42 Caused by: java.net.ConnectException at Net.java:-2
개발을 하다보면 위와 같은 스택 트레이스를 굉장히 많이 보셨을 겁니다. Caused by 라는 이름에서 알 수 있듯 어떤 예외의 원인이 되는 예외 정도만 생각했었는데요.

맞습니다. Cause는 원인이 존재하지 않거나 알 수 없는 경우 이 Throw 가능 객체의 Cause를 반환합니다.
만약 아래처럼 중첩된 예외가 발생한다면,
public class Junk {
public static void main(String args[]) {
try {
a();
} catch(HighLevelException e) {
e.printStackTrace();
}
}
static void a() throws HighLevelException {
try {
b();
} catch(MidLevelException e) {
throw new HighLevelException(e);
}
}
static void b() throws MidLevelException {
c();
}
static void c() throws MidLevelException {
try {
d();
} catch(LowLevelException e) {
throw new MidLevelException(e);
}
}
static void d() throws LowLevelException {
e();
}
static void e() throws LowLevelException {
throw new LowLevelException();
}
}
class HighLevelException extends Exception {
HighLevelException(Throwable cause) { super(cause); }
}
class MidLevelException extends Exception {
MidLevelException(Throwable cause) { super(cause); }
}
class LowLevelException extends Exception {
}
Caused by 구문을 통해 중첩된 에러들이 모두 발생하는 것을 알 수 있습니다.
HighLevelException: MidLevelException: LowLevelException
at Junk.a(Junk.java:13)
at Junk.main(Junk.java:4)
Caused by: MidLevelException: LowLevelException
at Junk.c(Junk.java:23)
at Junk.b(Junk.java:17)
at Junk.a(Junk.java:11)
... 1 more
Caused by: LowLevelException
at Junk.e(Junk.java:30)
at Junk.d(Junk.java:27)
at Junk.c(Junk.java:21)
... 3 more
지금까지 자바의 명시적으로 처리되지 않는 예외를 처리하는 방법과 스택 트레이스가 어떻게 구성되는지를 알 수 있었습니다.
에러 상황 디버깅하실 때 도움이 되셨다면 좋겠네요 !
파이팅🔥
'JAVA 🎻' 카테고리의 다른 글
Collection과 Collections의 차이 (2) | 2024.03.17 |
---|---|
[Java] StackTrace란 (0) | 2023.10.12 |
내가 보려고 정리한 Map의 메서드 (0) | 2023.08.18 |
Function Interface과 Lambda Expression (0) | 2023.08.17 |
[JAVA] Reactive Streams, Back Pressure란? (0) | 2023.08.05 |