예전에 Exception에 대해 포스팅했었는데, 이번엔 Exception이 성능에 미치는 영향에 대해 알아보겠습니다.
예외 처리 장점
- 프로그램 실행 완료를 위한 제공
- 프로그램 코드 및 오류 처리 코드를 쉽게 식별
- 오류 전파
- 의미 있는 오류 보고
- 오류 유형 식별
JVM 예외 처리 순서
예외가 발생하게 되면, JVM은 발생한 예외를 처리할 수 있는 exception handler(try-catch)가 포함된 메서드를 찾기 위해 call stack(호출 스택: 호출된 메서드 목록)을 검색합니다.
예외가 발생한 메서드부터 검색을 시작하고, 바로 handling 하지 못하면 JVM은 해당 예외를 처리할 수 있는 메서드를 찾을 때까지 메서드가 호출된 역순으로 메모리의 call stack을 탐색합니다.
만약 적절한 handler(발생한 예외 객체와 타입이 일치하는 handler)를 찾으면 발생한 예외를 handler에 전달합니다.
하지만 call stack의 모든 메서드를 탐색하고 적절한 handler를 찾을 수 없는 경우 JVM은 예외 객체를 JVM의 일부인 default exception handler로 전달합니다. 이 handler는 다음 형식으로 예외 정보를 출력하고 해당 Thread를 비정상적으로 종료합니다.
Exception in thread "xxx" Name of Exception : Description
... ...... .. // Call Stack
예외 처리 비용
보통 예외를 적절하게 쓰면 비즈니스 명확성을 높여주기 때문에 각 서비스 성격에 맞게 Custom Exception(User-Defined Exception)을 생성하기도 하고, Spring의 경우 어디에서든 throw exception 하더라도 전역적으로 잡아주는 @ControllerAdvice를 사용하여 예외를 처리합니다.
그런데 Java에서 예외가 성능에 미치는 영향 포스팅을 확인해보면 call stack을 탐색하면서 비용이 발생합니다.
call stack을 탐색하는 과정 자체도 비용이지만, fillInStackTrace() 메서드는 call stack을 순회하면서 클래스명, 메서드명, 코드 줄번호 등 정보를 수집하여 stackTrace로 정보를 만들기 때문에 비용을 크게 증가시킵니다.
fillInStackTrace() 메서드는 Trowable 클래스의 구현 메서드로 생성자에서 호출되도록 되어있습니다. 모든 Exception은 Trowable을 상속하기 때문에 이 메서드를 가지고 있습니다.
일반적으로 stackTrace를 생성하는 시간은 예외가 발생한 환경, stack dept, StackFrame의 메서드 호출 수, JVM의 버전 및 설정 등에 다르기 때문에 몇 밀리초에서 몇 초까지 다양합니다. 하지만 성능테스트를 해보면 stack dept가 깊을수록 비용이 많이 발생하는 것은 분명합니다.
비용을 줄이는 법
1. Return object
예외 발생대신 empty 객체를 리턴하거나 다른 적절한 응답으로 처리합니다.
public ResponseEntity<Object> method() {
try {
return ResponseEntity.ok(...);
} catch(BadRequestException e) {
return ResponseEntity.badRequest().body(...);
}
}
2. Overriding fillInStackTrace method
NullPointException이나 OutOfMemory와 같이 자바에서 기본적으로 제공하는 예외를 제외한, 우리가 만드는 Custom Exception은 에러의 추적보다는 유효하지 않는 값일 때 하위 비즈니스 로직을 수행하지 못하도록 하기 위한 용도로 주로 사용됩니다.
이러한 Custom Exception이나 이미 인지하고 있는 예외라면 매번 stackTrace를 생성하는 비용은 필요하지 않기 때문에 단순히 try-catch로 이후 flow를 제어하거나 spring환경에서 @ControllerAdvice로 예외를 처리하는 경우에는 불필요한 성능 저하를 막기 위해 fillInStackTrace()를 오버라이드하여 stackTrace 생성 비용을 줄일 수 있습니다.
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
3. Caching an exception
stackTrace를 가지지 않도록 오버라이딩한 Exception이라면 static final로 선언하여 일종의 상수 값 형태로 예외를 캐싱하고 쓰는 것이 매번 같은 종류의 예외를 new로 생성하는 것보다 효율적입니다.
public class CustomException extends RuntimeException {
public static final CustomException INVALID_PARAMETER = new CustomException(ResponseType.INVALID_PARAMETER);
public static final CustomException INVALID_TOKEN = new CustomException(ResponseType.INVALID_TOKEN);
// ...
}
위와 같이 CustomException 클래스에 예외 상황에 대한 적당한 응답 메세지나 코드를 담도록 한 뒤, 아래처럼 예외 발생 상황에서 new 키워드 없이 throw를 수행하도록 합니다.
if (StringUtils.isBlank(parameter)) {
throw CustomException.INVALID_PARAMETER;
}
이런 이슈가 있음에도 불구하고 Java에서 Checked Exception보다 Unchecked Exception을 추천하는 이유는 무엇일까? 예상컨대
1. 예외는 말 그대로 예외일 뿐, 자주 발생하는 것이 아니다.
2. 발생할 만한 예외는 개발자가 미리 핸들링 하기 때문에 괜찮다.
3. 성능 이슈를 일으킬 만큼의 비용을 많이 쓰지 않는다.
이지 않을까 추측해 봅니다.
[Reference]
https://www.geeksforgeeks.org/exceptions-in-java/
https://docs.oracle.com/javase/tutorial/essential/exceptions/index.html
https://stackoverflow.com/questions/299068/what-are-the-effects-of-exceptions-on-performance-in-java
https://meetup.nhncloud.com/posts/47
'java' 카테고리의 다른 글
[JAVA] JDK 11에서 JDK 21로 변경하기 (LTS버전) (0) | 2024.05.08 |
---|---|
[Java21] Virtual Thread 요약 (0) | 2023.05.15 |
[Java11] Customizing redisCodec for redisCommands (0) | 2023.02.09 |
[JAVA] UUID 중복 확률 (0) | 2022.08.09 |
[Java] Handler dispatch failed; nested exception is java.lang.NoSuchFieldError: (0) | 2022.07.21 |
댓글