프로필사진
KiwiK
KiwiK Dev.
Java 예외를 추적하는 Stack Trace 읽는 방법
Java 예외를 추적하는 Stack Trace 읽는 방법

2023. 3. 1. 18:15Java

오늘은 Java 학습을 위해 여러 리서치를 하던 도중 평소에 대략적으로 알고는 있었지만 모호했던 개념에 대해 알아본다.

 

개발을 하다 보면 수많은 예외(Exception)들과 마주하게 되는데, 초보 개발자라면 수십 줄의 에러 로그를 보고서 어디서부터 원인파악을 해나가야 할지 꽤나 난감했던 경험들이 한 번쯤은 있을 것이다.

 

예외를 읽을 줄 아는 능력은 개발자로 살아감에 있어 필수 중의 필수 덕목이다.

 

예외를 읽을 줄 알면 단 몇 초만에 해결할 수 있는 문제를 예외를 읽을 줄 모른다면 무의미한 인터넷 검색과 무작위 수정 등으로 시간을 허비하게 되고 이는 곧 생산성의 문제로 직결된다.

Java Stack Trace Example.

 

Java Throwable 클래스에서는 프로그램에 의해 호출되는 모든 메서드의 정보를 수집하고 기본적으로 콘솔에 Stack Trace를 출력한다.  JVM은 예외가 발생하면 자동으로 Stack Trace를 생성하고 Stack Trace에서 각 요소는 메서드 호출부를 나타내 예외가 발생한 위치를 추적하고 또한, Throwable 클래스에서는 콘솔에서 Stack Trace를 출력하는 printStackTrace() 메서드를 제공한다.

 

Stack Trace 출력 예시

try {
    Exception e = new Exception();
    e.initCause(new IOException("No space memory"));
    throw e;
} catch(Exception e) {
    e.printStackTrace();
}

 

 

Stack Trace 출력 결과

java.lang.Exception
	at example.StackTrace$AAA.ccc(StackTrace.java:21)
	at example.StackTrace$AAA.bbb(StackTrace.java:14)
	at example.StackTrace$AAA.aaa(StackTrace.java:10)
	at example.StackTrace.main(StackTrace.java:47)
Caused by: java.io.IOException: No space memory
	at example.StackTrace$AAA.ccc(StackTrace.java:22)
	... 3 more

 

 

 

 

 

Stack Trace 읽는 방법

 

아래는 널포인터 예외(NullPointerException)의 예시이다. 대부분의 예외 로그는 굉장히 길기 때문에 보기 전부터 거부감이 생기기 십상이다. 하지만 Stack Trace를 읽을 줄 안다면 핵심 코드만 빠르게 분석하여 쉽게 원인을 분석할 수 있다.

 

java.lang.reflect.InvocationTargetException
     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
     at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
     at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
     at java.lang.reflect.Method.invoke(Unknown Source)
     at com.mylibrary.ap.xul.builder.handler.MethodCallback.invokeCallback(MethodCallback.java:32)
     at com.mylibrary.ap.xul.builder.handler.ViewHandler.invokeActionResult(ViewHandler.java:581)
     at com.mylibrary.ap.xul.context.impl.ViewContextImpl$2.onActionResult(ViewContextImpl.java:283)
     at com.mylibrary.ap.xul.context.impl.ActionContextImpl.setResult(ActionContextImpl.java:90)
     at com.mylibrary.ap.xul.action.stock.DialogAction.handleDialogClose(DialogAction.java:156)
     at com.mylibrary.ap.xul.action.stock.DialogAction$1.windowClosed(DialogAction.java:142)
     at com.mylibrary.api.Window.fireWindowClosedEvent(Unknown Source)
     at com.mylibrary.api.Window.onClose(Unknown Source)
     at com.mylibrary.api.platform.WindowBase.BaseClose(Native Method)
     at com.mylibrary.api.platform.WindowBase.BaseClose(Unknown Source)
     at com.mylibrary.api.Window.close(Unknown Source)
     at com.mylibrary.ap.xul.ui.MessageDialog.handleButtonClick(MessageDialog.java:145)
     at com.mylibrary.ap.xul.ui.MessageDialog$1.handleClick(MessageDialog.java:134)
     at com.mylibrary.api.ClickableSupport.fireClickEvent(Unknown Source)
     at com.mylibrary.api.ClickableSupport.handleAction(Unknown Source)
     at com.mylibrary.api.Button.actionHook(Unknown Source)
     at com.mylibrary.api.Component.action(Unknown Source)
Caused by: java.lang.NullPointerException
     at com.mycompany.service.impl.PortalManagerImpl.deleteMenuItem(PortalManagerImpl.java:603)
     at com.mycompany.service.impl.PortalManagerImpl.deletePortal(PortalManagerImpl.java:358)
     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
     at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
     at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
     at java.lang.reflect.Method.invoke(Unknown Source)
     at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:307)
     at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:182)
     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:149)
     at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:106)
     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
     at org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:66)
     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
     at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
     at $Proxy54.deletePortal(Unknown Source)
     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
     at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
     at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
     at java.lang.reflect.Method.invoke(Unknown Source)
     at com.mycompany.util.SpringSecurityContextInvocationHandler.invoke(SpringSecurityContextInvocationHandler.java:62)
     at $Proxy84.deletePortal(Unknown Source)
     at com.mycompany.ui.binding.PortalDataProvider.doDelete(PortalDataProvider.java:81)
     at com.mycompany.ui.binding.PortalDataProvider.doDelete(PortalDataProvider.java:12)
     at com.mycompany.ui.binding.AbstractEISDataProvider.delete(AbstractEISDataProvider.java:105)
     at com.mylibrary.ap.xul.binding.dataset.impl.DatasetImpl.doCommit(DatasetImpl.java:90)
     at com.mylibrary.ap.xul.binding.dataset.impl.AbstractDataset.commit(AbstractDataset.java:251)
     at com.mylibrary.ap.xul.binding.dataset.impl.AbstractDataset.deleteRow(AbstractDataset.java:201)
     at com.mylibrary.ap.xul.action.dataset.DeleteDataRowAction.execute(DeleteDataRowAction.java:22)
     at com.mylibrary.ap.xul.context.impl.ViewContextImpl.execute(ViewContextImpl.java:294)
     at com.mycompany.ui.portal.PortalInfoHandler.onPostConfirmDeleteAction(PortalInfoHandler.java:192)
     ... 21 more

 

오류가 발생하게 되면 무조건 오류 메시지부터 완벽히 이해해야 한다. 검색은 그다음 순서다.

예외처리를 제대로 작성한 코드라면 대부분의 문제는 Stack Trace안에 답이 있기 때문이다.

 

위의 예시를 살펴보면 총 두 가지의 예외 내용을 포함하고 있다는 것을 알 수 있다.

 

java.lang.reflect.InvocationTargetException
...
Caused by: java.lang.NullPointerException
...
  • InvocationTargetException
  • NullPointerException

그리고 눈여겨봐야할 곳은 위쪽의 Trace가 아닌 Caused By: 로 시작되는 NullPointerException 부분임을 인지할 수 있어야 한다.

 

이제 Caused by: java.lang.NullPointerException 밑 두 줄을 유심히 살펴보자.

 

Caused by: java.lang.NullPointerException
     at com.mycompany.service.impl.PortalManagerImpl.deleteMenuItem(PortalManagerImpl.java:603)
     at com.mycompany.service.impl.PortalManagerImpl.deletePortal(PortalManagerImpl.java:358)

 

위의 구문을 해석해 보면,

'com.mycompany.service.impl.PortalManagerImpl' 클래스의 'deletePortal' 메서드 358번째 줄에서 같은 클래스의 'deleteMenuItem'메서드를 호출했는데 해당 메서드 603번째 줄에서 널 포인터 예외가 발생했다' 라고 해석할 수 있다.(추적하는 것이기 때문에 대부분 아래에서 위로 해석하는 게 맞다. ex. 분해는 조립의 역순)

 

그리고 PortalManagerImpl 클래스의 관련된 소스는 다음과 같다.

 

if (item == null) {
    throw new NullArgumentException("item");
}

//중간 생략
List<PortalMenu> children = getMenuItems(item.getPortal().getId(), item.getId()); // 603번째 줄

for (PortalMenu child : children) {
    deleteMenuItem(child);
 }

 

그렇다면 어디가 NullPointerException의 원인일까?

 

대부분의 사람들은 children이나 item.getId()가 널 포인터 예외의 원인 같다고 생각한다.(나 포함)

 

이론적으로 위 소스 코드에서 Null 값이 들어갈 수 있는 모든 경우의 수는,

  1. List <PortalMenu> children
  2. item
  3. item.getPortal()
  4. item.getPortal().getId()
  5. item.getId()

이렇게 다섯 가지가 있다. 하나하나씩 따져보자.

 

따져보기에 앞서, 널 포인터 예외는 단순히 변수에 null 값이 들어가서 발생하는 오류가 아니다.

널 포인터 예외는 명확하게 객체의 Null Reference에 대한 메서드 호출이나 필드 참조 등의 작업을 했을 때

발생하는 문제라는 것을 짚고 넘어가야 한다.

 

먼저 1번의 경우 단순히 children이라는 변수에 null 값을 할당하는 것 만으론 절대 NullPointerException이 발생할 수 없다. 그리고 4번5번의 경우도 item.getPortal().getId()item.getId() 이 null이라면 이는 Null Reference에 대한 호출이 아닌 null 값을 getMenuItems라는 메서드의 인자로 넘기는 것뿐이기 때문에 역시 NullPointerException이 발생할 수 없다.

 

물론 getMenuItems메서드 안에서 해당 인자에 대한 null check 없이 값을 사용하다가 예외가 발생할 수도 있겠지만

이 경우에는 절대로 Stack Trace상에서 굵은 글자로 표시된 603번째 예외를 뿌리지는 않을 것이다.

 

그렇다면 남은 선택지는 2번과 3번뿐인데, 2번item 변수는 위 if문을 통해 null check가 이루어지기 때문에

603번째 줄에서 절대 null 값을 가질 수 없다. 그렇기 때문에 답은 3번이다.

 

 

마치며..

 

오늘은 Java에서 예외를 추적하는 Stack Trace를 읽는 방법에 대해 알아보았다.

개발은 문제해결의 연속이고 해결을 위해서는 원인 파악이 우선시된다.

그러나 원인을 파악하는 방법조차 모른다면 과연 개발자라고 부를 수 있을까라는 의문이 든다.(부끄)

Java 공부를 더 열심히 해야겠다는 생각을 하면서... 끝

 

참조 : https://okky.kr/articles/338405

          https://www.javatpoint.com/java-stack-trace

'Java' 카테고리의 다른 글

Java Stream API  (0) 2023.03.09