제리의 배움 기록

[Java] 의도적 Exception 호출의 처리 비용 본문

자바

[Java] 의도적 Exception 호출의 처리 비용

제리92 2021. 11. 12. 21:53

웹서비스 개발을 하다 보면 사용자의 요청이 비즈니스상 유효하지 않은 이벤트가 발생하였을때 Exception을 던지도록 할 때가 있습니다.

예를들어, 없는 ID로 로그인을 시도하거나 , 유효하지 않은 입력값으로 전송 하는 경우 등이 있습니다.

저같은 경우에도 웹서비스 개발시에 NotFoundUserException, DuplicateUserAccountException 등 비즈니스적인 예외를 별도로 정의하여 유효하지 않은 흐름에서는 Exception을 호출하고 ControllerAdvice에서 핸들링 하는 방식으로 주로 구현해왔습니다.

이렇게 하면 해당 로직에서 발생할 수 있는 예외가 분명히 명시되고 흐름도 깔끔해지는 장점이 있습니다.

그런데, 이러한 Exception 처리의 비용이 어떻게 될까가 궁금해져 알아보았습니다.

JVM의 Exception 처리 순서

프로그램 중간에 발생된 예외는 다음과 같은 순서로 처리됩니다.

  1. 예외 발생
  2. 예외를 핸들링 하는 핸들러 탐색(catch 블록)
  3. 예외 핸들러를 찾지 못했다면 default exception handler 실행 => 예외 출력과 함께 해당 thread 중단

처리 비용은 예외 핸들러 탐색과 관련 있습니다.

JVM은 예외를 처리할 수 있는 핸들러를 탐색합니다.
예외가 발생한 메서드에서 바로 처리된다면 베스트이지만, 바로 처리되지 못하면 JVM은 해당 예외를 처리할 수 있는 메서드를 찾을 때까지 메모리의 호출 스택(call stack)을 탐색하게 됩니다. 이 과정에서 비용이 발생합니다.

호출 스택을 탐색하는 과정 자체도 비용이지만, 여기에 덧붙여 fillInStackTrace 메서드는 호출 스택을 순회하며 클래스명, 메서드명, 코드 줄번호 등 정보를 차곡차곡 모아서 stacktrace로 정보를 만들어 비용을 크게 증가시킵니다.

fillInStackTrace 메서드는 Trowable 클래스의 구현 메서드로 생성자에서 호출되도록 되어있습니다.
모든 Exception은 Trowable을 상속하기 때문에 이 메서드를 가지고 있습니다.

비용을 줄이는 법

  1. 예외 발생대신 Empty 객체 리턴이나 다른 적절한 응답으로 처리가 가능한지 고려
  2. 커스텀 Exception을 정의할때 fillInStackTrace를 오버라이드하여 stacktrace 생성 비용을 줄임
    => 이미 인지하고 있는 예외라면 매번 stacktrace를 생성하는 비용을 발생하지 않아도 됨
  3. 예외 객체를 상수화 선언하여 한번만 생성하도록 함

적용해보기

현재 진행중인 토이프로젝트를 이용해 테스트해보았습니다.

기존 코드

중복 로그인 예외 클래스

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

Comments