반응형

Interceptor에서 예외를 응답 해주는 방법

  • postman 요청은 성공? 다른 서버에서는 실패?
// /error 는 스프링 부트가 제공하고 있기 때문에 커스텀 해도 스프링을 타게 되기 때문에(BasicErrorController.java)
// /api/error 와 같이 커스텀 해야한다.
request.setAttribute("message", "로그인이 필요한 서비스");
request.getRequestDispatcher("/error").forward(request, response);

문제


HandlerInterceptor에서 예외를 RestControllerAdvice가 핸들링하지 못한다.

상황이 어떤가 살펴보자.

  1. 소셜 로그인을 시도한다.
  2. authorization code가 유효하지 않으면, 예외를 발생 시킨다.
    500으로 응답한다.
public abstract class SessionLogin implements HandlerInterceptor {
        ...
        if (accessToken == null) {
                throw new IllegalArgumentException("로그인이 필요합니다.");
        }
        ...
}

위의 코드는 인터셉터에서 예외를 발생 시킨다.
하지만 이렇게 끝내면 500에러를 응답하게 된다.

스프링은 예외가 발생하면, WAS로 예외를 다시 전달한다.
그리고 WAS는 /error로 재요청한다. /error는 스프링이 기본으로 제공하는 api이다.
BasicErrorController.java가 정의한 컨트롤러이다.
이 컨트롤러를 타면, 스프링이 제공하는 기본 페이지를 응답하고, 500으로 반환하게 되는 것이다.

RestControllerAdvice가 핸들링 하지 못하는 이유

  • HandlerInterceptorHandlerExceptionResolver의 위치를 살펴보자.

출처 - 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

  • HandlerInterceptor, DispatcherServlet, ExceptionResolver의 동작 순서는 아래와 같다.
    DispatcherServletpreHandler(HandlerInterceprot)ControllerpostHandler(HandlerInterceprot)
    DispatcherServletafterCompletion응답
  • 즉, afterCompletion 전에 발생한 예외는 모두 DispatcherServlet으로 반환된다.
    afterCompletion은 모든 처리가 끝난 후 예외 정보, 요청, 응답 정보를 모두 받는 위치에 있기 때문이다.
  • 그럼 결국 핸들링이 되어야 한다. 그런데 왜 안된걸까?

핸들러가 존재할 때와 존재하지 않을 때로 구분한다.

HandlerExceptionResolver은 핸들러에 연관된 예외를 핸들링 하기 위한 것이다.

  • 핸들러가 존재할 때
    1. preHandle에서 예외 발생
    2. 핸들러가 존재하는지 확인(@Controller, @RestController)
    3. 존재한다면, ExceptionResolver가 예외 해결 시도
  • 핸들러가 존재하지 않을 때
    1. preHandler에서 예외 발생
    2. 핸들러가 존재하는지 확인
    3. 존재하지 않으면, 그대로 WAS까지 예외 전달
    4. 이후 /error로 재요청하며, 500 페이지 제공

결국 핸들러가 존재하지 않았기 때문에 핸들링이 되지 않는 범위였던 것이었다.

해결


그렇다면, 핸들러가 존재하지 않을 때는 어떻게 해야 HandlerExceptionResolver의 핸들링 대상이 될까?

ServletRequest의 RequestDispatcher로 forwarding하기

  • getRequestDispatcher를 사용하여 forward를 사용한다. response.sendRedirect를 사용해도 되지만,
    forward를 선택했다.
public abstract class SessionLogin implements HandlerInterceptor {
        ...
        private boolean isAccessToken(HttpServletRequest request, HttpServletResponse response, String accessToken) throws ServletException, IOException {
                if (accessToken != null) {
                    return true;
                }

                request.setAttribute("message", "로그인이 필요합니다.");
                request.setAttribute("exception", "AuthenticationException");
                request.getRequestDispatcher("/api/error").forward(request, response);
                System.out.println("request = " + request);
                return false;
                }
        }
        ...
}
  • 그리고 에러를 처리할 컨트롤러를 만들었다.
@RestController
public class ErrorController {
    @GetMapping("/api/error")
    public void error(HttpServletRequest request) throws AuthenticationException {
        String message = (String) request.getAttribute("message");
        String exception = (String) request.getAttribute("exception");

        if ("AuthenticationException".equals(exception)) {
            throw new AuthenticationException(message);
        }
    }
}

이렇게 되면, 결국 컨트롤러에서 예외가 발생되기 때문에 HandlerExceptionResolver가 핸들링할 수 있게된다.

forward를 선택한 이유

상황에 따라 다르겠지만, 현재 나의 상황에서 이유는 2가지가 있다.

  • 브라우저로 리다이렉트를 해서 재요청을 하여 네트워크를 두 번 탈 이유가 없다.
    그럴 이유가 없다면, 굳이 두 번 네트워크를 탈 필요가 없다고 생각한다.
  • request에 담은 message, exceptionErrorController에서 사용해야 한다.
    재요청을 하게되면, request가 초기화 되기 때문에 사용할 수 없다.

JSP를 사용해봤다면 forward를 봤을수도 있고, 보지 못했을 수도 있다.
JSP에서 우리는 모델에 담긴 객체를 그대로 사용하게 되는데 이는 request.setAttribute로 서블릿이 담는 것이고,
이를 JSP로 forwarding해서 url 변경없이 JSP화면이 제공되고, JSP 페이지는 이를 request.getAttribte해서
사용할 수 있는 것이다.

→ JSP 내용 참고

반응형
복사했습니다!