티스토리 뷰
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: 파일을 읽을 수 없습니다. ← 근본 원인
...
분석 방법
- Caused by가 있는지 확인
- 있다면 가장 마지막 Caused by로 이동
- 이게 바로 Root Cause(근본 원인)입니다.
- 마지막 Caused by의 첫 번째 at 라인이예외가 처음 발생한 지점입니다.
Root Cause 블록 안에 내가 작성한 클래스/메서드가 없다면, 바로 위 단계의
Caused by나 상위 스택으로 올라가며 내가 작성한 패키지명(예:com.myapp.*)이 처음 등장하는 곳을 찾습니다.
4. Caused by는 왜 생기는가?
Caused by는 예외 래핑 과정에서 생성되는 출력 형식입니다.
예외가 발생했을 때 두 가지 방법이 있습니다.
- 그대로 전파: 원본 예외를 그대로 위로 던짐
- 예외 래핑: 새로운 예외로 감싸서 던짐 (이때 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 |
