본문 바로가기

스프링/Spring MVC

[API 예외 처리] - HandlerExceptionResolver 사용하기

서버에서 예외가 발생하고, 서블릿을 넘어 WAS까지 예외가 전달되면 HTTP 상태 코드가 500으로 처리됩니다.

발생하는 예외에 따라서 400, 404 등등 다른 상태 코드도 처리하고 싶은 경우 어떻게 하면 될까요?

오류 메시지, 형식등을 API마다 다르게 처리하고 싶으면 어떻게 하면 될까요?

 

위와 같은 목적을 위해서 HandlerExceptionResolver를 사용하면 처리 가능합니다.

 

상태 코드 변환

상황: IllegalArgumentException 을 처리하지 못해서 컨트롤러 밖으로 넘어가는 일이 발생하면 HTTP 상태 코드를 400으로 처리하고 싶습니다.

@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
        if (id.equals("ex")) {
            throw new RuntimeException("잘못된 사용자");
        }
        if (id.equals("bad")) {
            throw new IllegalArgumentException("잘못된 입력 값");
        }
        return new MemberDto(id, "hello " + id);
}

http://localhost:8080/api/members/bad라고 호출하면 IllegalArgumentException발생하도록 했습니다.

실행해보면 상태 코드가 500인 것을 확인할 수 있다.

{
    "status": 500,
    "error": "Internal Server Error",
    "exception": "java.lang.IllegalArgumentException",
    "path": "/api/members/bad"
}

 

HandlerExceptionResolver

스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공합니다. 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 HandlerExceptionResolver를 사용하면 됩니다. 줄여서 ExceptionResolver라고 합니다.

 

HandlerExceptionResolver - 인터페이스

public interface HandlerExceptionResolver {

    ModelAndView resolveException(

    HttpServletRequest request, HttpServletResponse response,

    Object handler, Exception ex);

}
  • handler : 핸들러(컨트롤러) 정보
  • Exception ex : 핸들러(컨트롤러)에서 발생한 발생한 예외

 

MyHandlerExceptionResolver

@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                log.info("IllegalArgumentException resolver to 400");
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                return new ModelAndView();
            }
        } catch (IOException e) {
            log.error("resolver ex", e);
        }
        return null;
    }
}
  • ExceptionResolver가 ModelAndView를 반환하는 이유는 마치 try, catch를 하듯이, Exception을 처리해서 정상 흐름처럼 변경하는 것이 목적입니다. 이름 그대로 Exception 을 Resolver(해결)하는 것이 목적입니다.

 

여기서는 IllegalArgumentException 이 발생하면 response.sendError(400) 를 호출해서 HTTP 상태 코드를 400으로 지정하고, 빈 ModelAndView를 반환합니다.

 

반환 값에 따른 동작 방식

HandlerExceptionResolver의 반환 값에 따른 DispatcherServlet의 동작 방식은 다음과 같습니다.

  • 빈 ModelAndView: new ModelAndView()처럼 빈 ModelAndView를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴됩니다.
  • ModelAndView 지정: ModelAndView에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 ㅎ바니다.
  • null: null을 반환하면, 다음 ExceptionResolver를 찾아서 실행합니다. 만약 처리할 수 있는 ExceptionResolver가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던집니다.

 

ExceptionResolver 활용

  • 예외 상태 코드 변환
    • 예외를 response.sendError(xxx) 호출로 변경해서 서블릿에서 상태 코드에 따른 오류를 처리하도록 위임
    • 이후 WAS는 서블릿 오류 페이지를 찾아서 내부 호출, 예를 들어서 스프링 부트가 기본으로 설정한 /error 가 호출됨
  • 뷰 템플릿 처리
    • ModelAndView에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링 해서 고객에게 제공
  • API 응답 처리
    • response.getWriter(). println("hello");처럼 HTTP 응답 바디에 직접 데이터를 넣어주는 것도 가능하다. 여기에 JSON으로 응답하면 API 응답 처리를 할 수 있다.

 

WebMvcConfigurer에 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new MyHandlerExceptionResolver());
    }
}
configureHandlerExceptionResolvers(..)를 사용하면 스프링이 기본으로 등록하는 ExceptionResolver 가 제거되므로 주의, extendHandlerExceptionResolvers를 사용하자.

 

Postman으로 실행

  • http://localhost:8080/api/members/ex HTTP 상태 코드 500
  • http://localhost:8080/api/members/bad HTTP 상태 코드 400

 

API 예외 처리 - HandlerExceptionResolver 활용

예외를 ExceptionResolver에서 마무리하기

예외가 발생하면 WAS까지 예외가 던져지고, WAS에서 오류 페이지 정보를 찾아서 다시/error를 호출하는 과정은 복잡합니다.

ExceptionResolver를 활용하면 예외가 발생했을 때 이런 복잡한 과정 없이 여기에서 문제를 깔끔하게 해결할 수 있습니다.

실행

POSTMAN 실행,

http://localhost:8080/api/members/user-ex

ACCEPT : application/json

{
  "ex": "hello.exception.exception.UserException",
  "message": "사용자 오류"
}

ACCEPT : text/html

<!DOCTYPE HTML>

<html>

...

</html>

결론 

ExceptionResolver를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver에서 예외를 처리합니다.

따라서 예외가 발생해도 서블릿 컨테이너까지 예외가 전달되지 않고, 스프링 MVC에서 예외 처리는 끝이 납니다.

결과적으로 WAS 입장에서는 정상 처리가 된 것입니다. 이렇게 예외를 이곳에서 모두 처리할 수 있다는 것이 핵심입니다.