본문 바로가기
개인공부

★ 스택 트레이스(Stack Trace ) 읽는 법 정리

by 리승우 2023. 3. 8.

- 스택 트레이스란?

· 프로그램이 시작된 시점부터 현재 위치까지의 메서드 호출 목록

· 이를 통해 예외가 어디서 발생했는지 알 수 있음

 

- 왜 스택 트레시를 볼 줄 알아야 하나?

· 오류 발생 시, 문제의 정확한 원인이 무엇인지 알아야 문제상황을 해결할 수 있기 때문

· 정확한 문제 상황을 인지하지 못하면, 애꿎은 코드 및 환경설정만 손보다가 우연찮게 문제를 해결할 순 있어도, 장기적으로 보면 실력이 발전없이 멈추게된다.

 

- 스택 트레이스 예시

public class StackTraceTest 
{
	public static void main(String[] args) 
	{one();}
	
	public static void one()
	{two();}

	public static void two()
	{three();}
	
	public static void three()
	{Integer.parseInt("abcde");}
}

위 메소드를 실행 시, 에러가 발생한 시점부터 프로그램이 시작된 시점까지 거슬러 올라가면서 출력됨

-> 먼저 실행된 메서드가 아래쪽에 위치함

 

- 스택 트레이스 읽는법

아래는 스택 트레이스 예시이다.

이전에는 읽기 전에 지레 겁을 먹고 주춤했었지만, 이제 읽는 법을 정확히 알아볼 예정이다

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

 

위 예제를 보면 이제부터는 단박에 알아야한다.

> 우선 위 트레이스는 두 가지 예외 내용을 포함하고 있다

> 문제의 진정한 원인은 윗쪽의 트레이스가 아니라 'Caused By:'로 표시되는 널포인터 예외 부분이다

 

그럼 이제 문제의 진정한 원인인 'Caused By:' 쪽의 트레이스를 읽어보자

아래 5줄까지만 우선 가져와보겠다. (사실 아래로 3줄만 가져와도 충분함)

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으로 시작하는 첫 번째 메시지에 집중하면 된다.

왜냐하면 해당 메서드를 호출한 직후에 NullPointerException이 발생했기 때문이다!

 

해당 메시지는 아래와 같다

'com.mycompany.service.impl.PortalManagerImpl' 클래스의 deleteMenuItem 메서드를 호출할 때

603번째 줄에서 NullPointerException이 발생했습니다

 

오케이, 그럼 이 오류를 해결하기 위해 위 클래스의 603번째 줄로 이동해보자

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

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

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

 

여기서 이제 어디가 문제인지 진단해야 한다.

이곳에서 NULL 값이 들어갈 수 있는 모든 경우의 수는 아래 5가지이다

  1. children
  2. item
  3. item.getPortal()
  4. item.getPortal().getId()
  5. item.getId()

참고로 나는 깊은 고심을 한 다음 children으로 인해 문제가 발생했다고 판단했으나,

이는 NullPointerException을 정확히 이해하지 못한 큰 오판이였다.

 

왜냐하면 NullPointException은 단순히 변수에 NULL값이 들어가서 생기는 오류가 아니라,

NULL 레퍼런스에 대해 메서드 호출이나 필드 참조, 배열 처리, throw, 동기화 등 작업을 했을 때 생기는 문제이기 때문이다.

 

위 이유를 기반으로 하나씩 근거를 대며 보기를 소거해보겠다

 

1번 children

> 단순히 변수에 NULL값을 할당하는 것만으론 NullPointerException이 생기지 않는다

 

4번, 5번 item.getPortal().getId() / item.getId()

> 위 2개가 NULL이라면 이는 NULL 레퍼런스에 대한 호출이 아니라 NULL값을 'getMenuItems' 라는 메소드의 인자로 넘기는 것이기에 NullPointerException가 생기지 않는다

 

> 혹여나, 'getMenuItem' 메소드 안에서 해당 인자에 대한 NULL 체크없이 값을 사용하다가 예외가 발생할 순 있다! 하지만 그렇게되면 스택 트레이스의 'Caused By:'바래 아래의 at은 603이 아니라 다른 곳을 가리키고 있을 것이다

 

2번 item

> item은 위에서 별도로 NULL 체크를 하기 때문에 603번째 줄에서 절대로 NULL값을 가질 수 없다

 

그래서 결국 문제의 원인은

item.getPortal()인 3번이다.

 

스택 트레이스는 이런 식으로 보아야한다.

스택 트레이스를 자주 봄과 동시에 여러 오류에 대해 명확하게 알고 있어야겠다고 느낀 하루다.

 

 

 

참조링크

https://okky.kr/articles/338405

 

OKKY - 초보 개발자를 위한 스택트레이스 읽는 법

몇 년 전에 네이버 카페에 썼던 글인데, 답변을 달려다 보니 링크가 안되서 이 곳에 옮겨 적습니다. 초보 개발자분들이 제대로 된 디버그 방법을 배우지 못해 오류가 나면 무턱대고 검색부터 하

okky.kr

 

댓글