제리의 배움 기록
[Java] 의도적 Exception 호출의 처리 비용 본문
웹서비스 개발을 하다 보면 사용자의 요청이 비즈니스상 유효하지 않은 이벤트가 발생하였을때 Exception을 던지도록 할 때가 있습니다.
예를들어, 없는 ID로 로그인을 시도하거나
, 유효하지 않은 입력값으로 전송
하는 경우 등이 있습니다.
저같은 경우에도 웹서비스 개발시에 NotFoundUserException, DuplicateUserAccountException 등 비즈니스적인 예외를 별도로 정의하여 유효하지 않은 흐름에서는 Exception을 호출하고 ControllerAdvice에서 핸들링 하는 방식으로 주로 구현해왔습니다.
이렇게 하면 해당 로직에서 발생할 수 있는 예외가 분명히 명시되고 흐름도 깔끔해지는 장점이 있습니다.
그런데, 이러한 Exception 처리의 비용이 어떻게 될까가 궁금해져 알아보았습니다.
JVM의 Exception 처리 순서
프로그램 중간에 발생된 예외는 다음과 같은 순서로 처리됩니다.
- 예외 발생
- 예외를 핸들링 하는 핸들러 탐색(catch 블록)
- 예외 핸들러를 찾지 못했다면 default exception handler 실행 => 예외 출력과 함께 해당 thread 중단
처리 비용은 예외 핸들러 탐색과 관련 있습니다.
JVM은 예외를 처리할 수 있는 핸들러를 탐색합니다.
예외가 발생한 메서드에서 바로 처리된다면 베스트이지만, 바로 처리되지 못하면 JVM은 해당 예외를 처리할 수 있는 메서드를 찾을 때까지 메모리의 호출 스택(call stack)을 탐색하게 됩니다. 이 과정에서 비용이 발생합니다.
호출 스택을 탐색하는 과정 자체도 비용이지만, 여기에 덧붙여 fillInStackTrace 메서드는 호출 스택을 순회하며 클래스명, 메서드명, 코드 줄번호 등 정보를 차곡차곡 모아서 stacktrace로 정보를 만들어 비용을 크게 증가시킵니다.
fillInStackTrace 메서드는 Trowable 클래스의 구현 메서드로 생성자에서 호출되도록 되어있습니다.
모든 Exception은 Trowable을 상속하기 때문에 이 메서드를 가지고 있습니다.
비용을 줄이는 법
- 예외 발생대신 Empty 객체 리턴이나 다른 적절한 응답으로 처리가 가능한지 고려
- 커스텀 Exception을 정의할때 fillInStackTrace를 오버라이드하여 stacktrace 생성 비용을 줄임
=> 이미 인지하고 있는 예외라면 매번 stacktrace를 생성하는 비용을 발생하지 않아도 됨 - 예외 객체를 상수화 선언하여 한번만 생성하도록 함
적용해보기
현재 진행중인 토이프로젝트를 이용해 테스트해보았습니다.
기존 코드
중복 로그인 예외 클래스
package kancho.realestate.comparingprices.exception;
public class DuplicateLoginException extends RuntimeException {
public DuplicateLoginException(String message) {
super(message);
}
}
테스트 코드
@Test
@Transactional
@DisplayName("로그인한_사람이_같은_아이디로_로그인요청")
public void login_exception() throws Exception {
String serializedUser1 = serailizedTesterBody("tom ford", "12345678");
postSuccessTest("/join", serializedUser1, status().isCreated());
MvcResult loginResult = postSuccessTest("/login", serializedUser1, status().isCreated());
postExceptionTest("/login", serializedUser1, status().isBadRequest(),DuplicateLoginException.class,
loginResult.getResponse().getCookie("SESSION"));
}
public MvcResult postSuccessTest(String path, String content, ResultMatcher resultMatcher) throws Exception {
return mockMvc.perform(post(path)
.content(content)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(resultMatcher)
.andReturn();
}
public void postExceptionTest(String path, String content, ResultMatcher resultStatus,
Class exceptionClass, Cookie cookie) throws Exception {
mockMvc.perform(post(path)
.content(content)
.cookie(cookie)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(resultStatus)
.andExpect(result-> result.getResolvedException().printStackTrace())
.andExpect(result -> assertThat(result.getResolvedException()).isInstanceOf(exceptionClass));
}
printStackTrace로 출력한 결과
kancho.realestate.comparingprices.exception.DuplicateLoginException: 이미 로그인한 상태입니다.
at kancho.realestate.comparingprices.controller.UserController.validateDuplicateLogin(UserController.java:67)
at kancho.realestate.comparingprices.controller.UserController.login(UserController.java:44)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:72)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:167)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at
... 생략
fillInStackTrace 오버라이드 후 재실행
public class DuplicateLoginException extends RuntimeException {
public DuplicateLoginException(String message) {
super(message);
}
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}
결과
kancho.realestate.comparingprices.exception.DuplicateLoginException: 이미 로그인한 상태입니다.
해당 예외 클래스 정보 외 다른 정보들은 쌓이지 않는 것을 확인할 수 있습니다.
[참고]
Exceptional Performance
Throwing an exception in java is very slow
'자바' 카테고리의 다른 글
[Java] if, switch 누가 더 빠를까? (0) | 2021.12.06 |
---|---|
[Java] String + 연산 최적화 (0) | 2021.12.02 |
[테스트] PBT(Property Based Testing) (0) | 2021.11.12 |
[스프링] Spring Boot - AutoConfiguration (0) | 2021.11.12 |
[Java] 의존 라이브러리의 버전 관리를 편리하게 할 순 없을까? (0) | 2021.11.12 |