LogstashEncoder로 StackTrace 최적화하기: ShortenedThrowableConverter 완전 정복
들어가며
시스템 운영 로그를 관리하다 보면, 예외 로그의 StackTrace가 너무 길어져서 Logstash의 버퍼 크기를 초과하거나 스토리지 비용이 증가하는 문제를 겪게 됩니다. 특히 Spring Boot 애플리케이션에서 중첩된 예외가 발생할 때, StackTrace가 수십 줄에서 수백 줄까지 길어질 수 있습니다.
이번 글에서는 logstash-logback-encoder의 ShortenedThrowableConverter를 활용해 StackTrace를 효과적으로 줄이고 최적화하는 방법을 실제 예제와 함께 알아보겠습니다.
문제 상황
일반적인 StackTrace의 문제점
// 중첩된 예외 발생 시
java.lang.RuntimeException: Service layer failed: Unable to process business logic
at ExceptionTestService.testNestedExceptions(ExceptionTestService.java:17)
at ExceptionTestController.testNestedException(ExceptionTestController.java:22)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
... 30+ more lines
Caused by: java.lang.RuntimeException: Deep service method failed: Data processing error
at ExceptionTestService.deepServiceMethod(ExceptionTestService.java:45)
at ExceptionTestService.testNestedExceptions(ExceptionTestService.java:15)
... 32 common frames omitted
Caused by: java.lang.RuntimeException: Repository method failed: Data access error
at ExceptionTestService.repositoryMethod(ExceptionTestService.java:52)
... 34 common frames omitted
Caused by: java.lang.RuntimeException: External API call failed: Connection timeout
at ExceptionTestService.externalApiCall(ExceptionTestService.java:58)
... 35 common frames omitted
이런 긴 StackTrace는:
- 스토리지 비용 증가 (하루에 수백만 건의 로그가 쌓이면 엄청난 용량)
- Logstash 버퍼 크기 초과 (기본 65536 bytes)
- 로그 분석 시 가독성 저하
ShortenedThrowableConverter란?
ShortenedThrowableConverter는 logstash-logback-encoder에서 제공하는 강력한 StackTrace 최적화 도구입니다.
주요 기능
- StackTrace 길이 제한
- 불필요한 프레임 제외
- 클래스명 축약
- Root Cause First 출력
- 공통 프레임 생략
설정 방법
기본 설정
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<!-- 각 예외당 최대 스택 깊이 -->
<maxDepthPerThrowable>30</maxDepthPerThrowable>
<!-- 전체 StackTrace 최대 문자 수 -->
<maxLength>2048</maxLength>
<!-- 클래스명 축약 길이 -->
<shortenedClassNameLength>20</shortenedClassNameLength>
<!-- 불필요한 프레임 제외 패턴 -->
<exclude>sun\.reflect\..*\.invoke.*</exclude>
<exclude>net\.sf\.cglib\.proxy\.MethodProxy\.invoke</exclude>
<exclude>\$SpringCGLIB\$</exclude>
<exclude>^org\.springframework\.cglib\.</exclude>
<!-- Root Cause를 먼저 출력 -->
<rootCauseFirst>true</rootCauseFirst>
<!-- 해시값 인라인 표시 -->
<inlineHash>true</inlineHash>
</throwableConverter>
</encoder>
핵심 설정 옵션 상세 분석
1. maxDepthPerThrowable
각 개별 예외당 최대 스택 프레임 수를 제한합니다.
<maxDepthPerThrowable>10</maxDepthPerThrowable>
적용 전:
java.lang.RuntimeException: Service failed
at method1(...)
at method2(...)
at method3(...)
... 25 more frames
적용 후:
java.lang.RuntimeException: Service failed
at method1(...)
at method2(...)
... 23 frames truncated
2. maxLength
전체 StackTrace의 총 문자 수를 제한합니다.
<maxLength>1024</maxLength>
전체 길이가 1024자를 넘으면 자동으로 잘립니다.
3. exclude 패턴
불필요한 스택 프레임을 제거합니다.
<!-- Spring CGLIB 프록시 제거 -->
<exclude>\$SpringCGLIB\$</exclude>
<!-- Reflection 관련 제거 -->
<exclude>^sun\.reflect\..*\.invoke</exclude>
<!-- 여러 패턴을 한 번에 -->
<exclusions>
\$SpringCGLIB\$,
^sun\.reflect\..*\.invoke,
^org\.springframework\.web\.filter\.
</exclusions>
4. rootCauseFirst - "Wrapped by"의 비밀
가장 흥미로운 기능 중 하나입니다!
rootCauseFirst=false (기본값):
RuntimeException: Service layer failed
at testMethod(...)
Caused by: RuntimeException: Connection failed
at deepMethod(...)
Caused by: RuntimeException: Timeout occurred // Root Cause
at apiCall(...)
rootCauseFirst=true:
RuntimeException: Timeout occurred // Root Cause가 먼저!
at apiCall(...)
Wrapped by: RuntimeException: Connection failed
at deepMethod(...)
Wrapped by: RuntimeException: Service layer failed
at testMethod(...)
rootCauseFirst=true일 때만 **"Wrapped by"**가 나타납니다!
rootCauseFirst 적용전
rootCauseFirst 적용후
5. shortenedClassNameLength
클래스명을 축약하여 공간을 절약합니다.
<shortenedClassNameLength>25</shortenedClassNameLength>
적용 전:
global.colosseum.colo.api.test.service.ExceptionTestService.method()
적용 후:
g.c.c.a.t.s.ExceptionTestService.method()
실제 테스트 코드로 확인하기
테스트 컨트롤러 생성
@RestController
@RequestMapping("/api/test")
public class ExceptionTestController {
@GetMapping("/nested-exception")
public String testNestedException() {
try {
exceptionTestService.testNestedExceptions();
} catch (Exception e) {
log.error("Controller error occurred", e);
return "Exception logged";
}
}
}
서비스 로직
@Service
public class ExceptionTestService {
public void testNestedExceptions() {
try {
deepServiceMethod();
} catch (Exception e) {
throw new RuntimeException("Service layer failed", e);
}
}
private void deepServiceMethod() {
try {
repositoryMethod();
} catch (Exception e) {
throw new RuntimeException("Data processing error", e);
}
}
private void repositoryMethod() {
// Root Cause
throw new RuntimeException("Database connection timeout");
}
}
1. 과도한 최적화 주의
- maxLength를 너무 작게 설정하면 중요한 디버깅 정보가 손실될 수 있습니다
2. 제외 패턴 신중하게 선택
- 비즈니스 로직과 관련된 스택은 제외하지 마세요
- 주로 프레임워크, 프록시, JDK 내부 호출만 제외
3. 환경별 차별화
- 개발: 상세한 로그
- 스테이징: 중간 수준
- 운영: 최적화된 로그
마무리
ShortenedThrowableConverter는 단순한 로그 줄이기 도구가 아닙니다. 스토리지 비용 절약, 성능 향상, 가독성 개선을 동시에 달성할 수 있는 강력한 최적화 도구입니다.
특히 rootCauseFirst 설정을 통해 가장 중요한 정보(Root Cause)를 먼저 보여주는 것은 디버깅 효율성을 크게 향상시킵니다. "Wrapped by"와 "Caused by"의 차이를 이해하고 적절히 활용한다면, 로그 분석 시간을 대폭 단축할 수 있을 것입니다.
대규모 서비스에서는 작은 최적화가 큰 비용 절약으로 이어집니다. 오늘부터 당신의 애플리케이션에도 ShortenedThrowableConverter를 적용해보세요!
참고 자료