티스토리 뷰

1. 개요

서비스를 운영하게 되면 가장 많이 하는 작업 중 하나는 로그 분석입니다. 예외가 발생하면 여러 줄의 로그가 출력되는데, 이 많은 로그 중에서 근본 원인(Root Cause) 을 빠르고 정확하게 찾는 것이 무엇보다 중요합니다.

이 글에서는

  • 스택 트레이스가 무엇인지
  • 로그가 어떤 규칙으로 출력되는지
  • 그리고 어디서부터, 어떤 순서로 분석해야 하는지

를 단계적으로 설명합니다.

2. 스택 트레이스 기본 개념

2.1 스택 트레이스란 무엇인가?

예외 발생 시 출력되는 로그를 스택 트레이스(Stack Trace)라고 합니다. Stack Trace를 문자 그대로 해석하면 Stack에 쌓인 정보를 Trace(추적한) 기록입니다.

2.2 스택 트레이스의 구성

스택 트레이스는 크게 다음 두 가지로 구성됩니다.

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Example.methodC(Example.java:17)
    at Example.methodB(Example.java:13) 
    at Example.methodA(Example.java:9)
    at Example.main(Example.java:5)

1. 예외 정보 (첫 번째 줄)

  • Exception in thread "main": 어느 스레드에서 발생했는지
  • java.lang.ArithmeticException: 예외 클래스명 (어떤 종류의 예외인지)
  • / by zero: 예외 메시지

2. 호출 메서드 정보 (at으로 시작하는 줄들)

  • 예외가 발생하기까지 어떤 메서드들이 어떤 순서로 호출되었는지를 보여줍니다.
  • 각 줄은 클래스명.메서드명(파일명:라인번호) 형태로 출력됩니다.2.3 호출 흐름과 출력 순서

예제 코드를 통해 호출 메서드 정보가 어떻게 출력되는지 알아보겠습니다.

코드 실행 순서

main() → methodA() → methodB() → methodC() (예외 발생)
public class Example {
    public static void main(String[] args) {
        methodA();
    }

    static void methodA() {
        methodB();
    }

    static void methodB() {
        methodC();
    }

    static void methodC() {
        int x = 10 / 0; // 예외 발생
    }
}

스택 상태

+----------+
| methodC  | ← 현재 실행 중 (가장 안쪽)
+----------+
| methodB  |
+----------+
| methodA  |
+----------+
| main     |
+----------+

출력되는 스택 트레이스
예외 발생 시 JVM은 스택의 가장 윗 부분(methodC)부터 출력합니다.

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Example.methodC(Example.java:17)  ← 예외 발생 지점 (스택 최상위)
    at Example.methodB(Example.java:13)  ← methodC 호출 지점
    at Example.methodA(Example.java:9)   ← methodB 호출 지점
    at Example.main(Example.java:5)      ← methodA 호출 지점 (스택 최하위)

3. 예외 유형별 분석 방법

앞에서 스택 트레이스가 예외 정보와 호출 경로로 구성되어 있다는 것을 알았습니다. 그렇다면 로그는 가장 윗 부분만 또는 위에서 아래로 분석하면 될까요? 실제로는 그렇지 않습니다. 스택 트레이스의 형태에 따라 읽는 방향이 달라집니다.

3.1 기본 스택 트레이스 분석

기본 스택 트레이스란? 예외가 한 번만 발생해서 다른 예외로 감싸지지 않은 형태입니다. 예외 정보와 호출 경로만 표시됩니다.

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Example.methodC(Example.java:17)  ← 예외 발생 지점
    at Example.methodB(Example.java:13)
    at Example.methodA(Example.java:9)
    at Example.main(Example.java:5)

분석 방법

  • 예외 발생 지점 확인
    • 스택 트레이스 상단의 첫 번째 at 라인
    • → methodC()가 예외 발생 지점
  • 호출 흐름 분석
    • 예외 발생 지점을 기준으로 아래에서 위로
    • → main → methodA → methodB → methodC

3.2 복합 스택 트레이스 분석

실제 운영 환경에서는 예외가 여러 번 감싸지는 경우가 훨씬 많습니다. 이때 스택 트레이스에는 Caused by가 함께 출력됩니다.

예제 코드

public class ExceptionWrappingExample {  
    public static void main(String[] args) {  
       startApplication();  
    }  

    static void startApplication() {  
       try {  
          loadConfig();  
       } catch (Exception e) {  
          throw new RuntimeException("애플리케이션 시작 실패", e);  
       }  
    }  

    static void loadConfig() {  
       try {  
          readFile();  
       } catch (Exception e) {  
          throw new IllegalStateException("설정 파일 로딩 실패", e);  
       }  
    }  

    static void readFile() throws Exception {  
       throw new Exception("파일을 읽을 수 없습니다.");  
    }  
}

출력되는 스택 트레이스

Exception in thread "main" java.lang.RuntimeException: 애플리케이션 시작 실패
    at ExceptionWrappingExample.startApplication(ExceptionWrappingExample.java:11)
    at ExceptionWrappingExample.main(ExceptionWrappingExample.java:5)

Caused by: java.lang.IllegalStateException: 설정 파일 로딩 실패
    at ExceptionWrappingExample.loadConfig(ExceptionWrappingExample.java:20)
    ...

Caused by: java.lang.Exception: 파일을 읽을 수 없습니다.  ← 근본 원인
    ...

분석 방법

  1. Caused by가 있는지 확인
  2. 있다면 가장 마지막 Caused by로 이동
  3. 이게 바로 Root Cause(근본 원인)입니다.
  4. 마지막 Caused by의 첫 번째 at 라인예외가 처음 발생한 지점입니다.

Root Cause 블록 안에 내가 작성한 클래스/메서드가 없다면, 바로 위 단계의 Caused by나 상위 스택으로 올라가며 내가 작성한 패키지명(예: com.myapp.*)이 처음 등장하는 곳을 찾습니다.

4. Caused by는 왜 생기는가?

Caused by는 예외 래핑 과정에서 생성되는 출력 형식입니다.

예외가 발생했을 때 두 가지 방법이 있습니다.

  1. 그대로 전파: 원본 예외를 그대로 위로 던짐
  2. 예외 래핑: 새로운 예외로 감싸서 던짐 (이때 Caused by 생성)
// 방법 1: 그대로 전파 (Caused by 없음)
public void method1() throws SQLException {
    throw new SQLException("DB 오류");  // 그대로 던짐
}

// 방법 2: 예외 래핑 (Caused by 생성)  
public void method2() {
    try {
        throw new SQLException("DB 오류");
    } catch (SQLException e) {
        throw new ServiceException("서비스 오류", e);  // 새 예외로 감쌈
    }
}

실제 코드에서는 내가 직접 래핑하지 않아도 Caused by가 자주 나타납니다.

Spring, JPA, Hibernate 등 실제 애플리케이션을 구성하는 많은 요소들이 내부적으로 예외를 래핑하기 때문입니다.

Spring Data JPA 예시

@Service  
public class UserService {
    public void saveUser(User user) {
        userRepository.save(user);  // 이것만 호출했는데...
    }
}

userRepository.save() 호출 시 예외가 발생했을 때, 실제 출력되는 로그는 아래와 같이 장황합니다.

org.springframework.dao.DataIntegrityViolationException: could not execute statement
    ...
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement  
    ...
Caused by: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'user@test.com'
    ...

내 코드: userRepository.save(user) 한 줄
실제 결과: 3단계 Caused by 체인!

최종적으로 발생한 예외는 DataIntegrityViolationException이고 이 에러 발생의 원인은 SQLIntegrityConstraintViolationException인 것을 알 수 있습니다.

5. 실무 로그 분석 가이드

로그가 아무리 길어도 당황하지 말고 다음의 3단계 프로세스를 따르세요.

1. 무엇이 문제인가? (Root Cause)

  • 로그의 가장 마지막 Caused by 섹션의 예외명과 메시지를 읽습니다.
  • 예: $java.sql.SQLIntegrityConstraintViolationException$ (중복 키 에러)

2. 어디서 터졌는가? (Origin Point)

  • 해당 섹션의 첫 번째 at 라인의 파일명과 라인 번호를 확인합니다.
  • 예: UserMapper.xml:45 또는 UserRepository.java:12

3. 내가 고칠 곳은 어디인가? (Action Point)

  • 만약 Origin Point가 외부 라이브러리라면, 내 프로젝트 패키지명(com.myapp.*)이 나타날 때까지 위로 스캔합니다.
  • 내가 작성한 코드와 에러가 만나는 지점이 바로 수정 포인트입니다.

스택 트레이스 분석의 핵심은 예외가 어디서 처음 발생했는지를 찾고, 그 지점을 기준으로 어떤 호출 흐름을 통해 도달했는지를 이해하는 것이다. 이 단계를 분리해서 접근하면, 로그의 길이나 복잡도와 관계없이 근본 원인을 빠르게 파악할 수 있을것이다.

'언어 > Java' 카테고리의 다른 글

TDD 알아보기  (0) 2026.02.06
CompletableFuture 가이드  (0) 2026.01.01
Record 살펴보기  (0) 2025.09.14
Optional 바르게 사용하기  (0) 2025.08.26
JVM 메모리 구조 정리  (0) 2025.08.16
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함