Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

참새의 이야기

[MVC2] API 예외 처리 본문

Spring

[MVC2] API 예외 처리

참새짹짹! 2023. 8. 15. 19:41

MVC의 처리 방법

WebServerCustomizer

이전 글에서 다뤘던 WebServerCustomizer 를 이용한 예외 처리를 살펴보자.

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

위와 같이 Exception을 사용하여 WebServerCustomizer 에게 처리를 요청하면, 정상적인 요청은 JSON 형식으로 반환하지만, 예외 발생 시에는 Html이 반환된다.

WebServerCustomizererror-page/500 로 보내는 요청을 Html이 아닌 JSON으로 반환하려면 아래와 같은 API가 필요하다.

@RequestMapping(value = "error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(HttpServletRequest request, HttpServletResponse response) {

    Map<String, Object> result = new HashMap<>();
    Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
    result.put("status", request.getAttribute(ERROR_STATUS_CODE));
    result.put("message", ex.getMessage());

    Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);

    return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
}

BasicErrorController

아래는 BasicErrorController 가 제공하는 두 가지 메서드이다.

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
   HttpStatus status = getStatus(request);
   Map<String, Object> model = Collections
      .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
   response.setStatus(status.value());
   ModelAndView modelAndView = resolveErrorView(request, response, status, model);
   return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
   HttpStatus status = getStatus(request);
   if (status == HttpStatus.NO_CONTENT) {
      return new ResponseEntity<>(status);
   }
   Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
   return new ResponseEntity<>(body, status);
}

클라이언트 요청의 Accept 헤더 값이 text/html인 경우에는 errorHtml()이 호출되고 나머지 경우에는 error()가 호출된다.

코드만 봐도 알 수 있지만, 전자는 view를 제공하고 후자는 JSON을 반환한다.

이 방법의 단점은 응답이 유연하지 못하다는 점이다.

필요한 형태로 확장할 수 있지만, 이후 설명할 @ExceptionHandler 를 이용하는 편이 좋다.

API를 위한 처리

HandlerExceptionResolver

ExceptionResolver를 적용하지 않으면 예외 발생 시 DispatcherServlet은 postHandler를 호출하지 않고 afterCompletion을 호출한다.

이후 WAS에게 예외가 전달된다.

이때 ExceptionResolver를 적용하면 DispatcherServlet은 ExceptionResolver를 호출한 다음 afterCompletion을 호출한다.

ExceptionResolver가 예외를 해결하더라도 postHandler는 호출되지 않는다.

ExceptionResolver는 아래와 같이 구현하여 활용할 수 있다.

public class MyHandlerExceptionResolver implements org.springframework.web.servlet.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;
    }
}

try에서 예외를 해결하기 위한 시도를 하고 성공 시에 빈 ModelAndView를 반환한다.

예외를 해결하지 못한 경우에는 null을 반환하고 다음 ExceptionResolver를 실행한다.

다음 ExceptionResolver가 없을 경우에는 기존에 발생한 예외를 서블릿 밖으로 보낸다.

스프링이 제공하는 ExceptionResolver

스프링 부트는 ExceptionHandlerExceptionResolver, ResponseStatusExceptionResolver, DefaultHandlerExceptionResolver 를 ExceptionResolver로 제공한다.

ResponseStatusExceptionResolver

예외에 따라 상태코드를 지정해 주는 ExceptionResolver다.

@ResponseStatus 에노테이션이 달려있거나 ResponseStatusException인 경우에 처리한다.

@ResponseStatus는 아래와 같이 활용한다.

@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}

아래는 ResponseStatusExceptionResolver의 코드다.

sendError()를 호출하는 것을 확인할 수 있다.

protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
      throws IOException {

   if (!StringUtils.hasLength(reason)) {
      response.sendError(statusCode);
   }
   else {
      String resolvedReason = (this.messageSource != null ?
            this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
            reason);
      response.sendError(statusCode, resolvedReason);
   }
   return new ModelAndView();
}

@ResponseStatus는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다.

이런 경우에는 ResponseStatusException을 사용하면 된다.

@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
    throw new ResponseStatusException(HttpStatus.NOT_FOUND, "잘못된 요청입니다.");
}

DefaultHandlerExceptionResolver

스프링 내부에서 발생하는 예외를 처리하는 Resolver다.

대표적으로 파라미터 바인딩 시점에 타입이 맞지 않을 때 활용된다.

@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
    return "ok";
}

위와 같은 경우에 파라미터로 문자가 들어가면 내부에서는 TypeMismatchException이 발생할 것이다.

이 예외를 처리하지 않으면 400이 아닌 500번 오류가 발생한다.

이런 경우에는 400번이 적절하기 때문에 DefaultHandlerExceptionResolver가 적절한 상태 코드로 변경해 준다.

ExceptionHandlerExceptionResolver

@ExceptionHandler 애노테이션을 선언하고 해당 컨트롤러에서 처리하고자 하는 예외를 지정해 주면 된다.

@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
    log.error("[exceptionHandle] ex", e);
    return new ErrorResult("BAD", e.getMessage());
}

이 경우에는 IllegalArgumentException이나 그 하위 클래스 모두를 처리할 수 있다.

애노테이션의 괄호에 들어가는 예외를 생략하는 경우에는 메서드 파라미터의 예외가 지정된다.

실행 흐름을 정리해 보자면, IllegalArgumentException 예외가 발생하면 컨트롤러 밖으로 예외가 던져진다.

예외 발생으로 인해 ExceptionResolver가 작동하고 우선순위대로 실행된다.

우선순위가 가장 높은 ExceptionHandlerExceptionResolver는 해당 컨트롤러에 IllegalArgumentException을 처리할 수 있는 @ExceptionHandler가 있는지 확인하고 illegalExHandler()를 실행한다.

reference

이 글은 김영한님의 '스프링 MVC 2편'을 듣고 작성했습니다.

'Spring' 카테고리의 다른 글

[Spring Security] Authentication Architecture  (0) 2023.11.05
[Spring Security] Spring security Architecture  (0) 2023.11.05
[MVC2] MVC 예외 처리  (0) 2023.08.12
[MVC2] ArgumentResolver  (0) 2023.08.10
[MVC2] 로그인 처리 - Filter, Interceptor  (0) 2023.08.10