[Spring] API 에러 처리하는 방법, ExceptionResolver 와 @ControllerAdvice

728x90

 

서블릿 API 에러 처리

에러 페이지는 단순히 에러 페이지만 랜더링 하면 되지만,

API 는 각 오류 상황에 맞는 응답 스펙을 정하고, 데이터를 JSON 으로 주어야 한다.

 

오류 페이지 컨트롤러에 JSON 응답을 해주는 메서드를 추가하면 된다.

@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));
}

produces = MediaType.APPLICATION_JSON_VALUE

  : 클라이언트가 요청하는 HTTP Header 의 Accept 값이 application/json 일 때 해당 메서드가 호출

 

스프링 부트 API 에러 처리

BasicErrorContoller

BasicErrorController 코드에는 errorHtml(), error() 두 메서드가 있다.

- errorHteml() : produces = MediaType.TEXT_HTML_VALUE 를 통해 클라이언트 요청의 Accept 헤더 값이 text/heml 인 경우에 호출되어 view 를 제공한다.

- error() : 그 외 경우에 호출되며 ResponseEntity 로 HTTP Body 에 JSON 데이터를 반환한다.

 

따라서 스프링 부트는 BasicErrorController 가 제공하는 기본 정보를 활용하여 오류 API 를 자동으로 생성해 준다.

 

HandlerExceptionResolver 구현하여 만들기

예외가 발생하여 WAS 까지 예외가 전파되면 HTTP 상태코드가 500으로 처리된다.

 

이렇게 HandlerExceptionResolver 는 예외가 발생했을 때

상태코드, 에러 메시지, 형식 등 다양하게 처리할 수 있도록 도와준다.

 

동작 흐름

            preHandle
         ↗ 
   preHandle
 ↗ 
   
클라이언트 HTTP 요청
→ → → →
Dispatcher
Servlet
 handle(handler)
→ → → → → →
← ← ← ← ← ←
 예외 전달
핸들러
어댑터
③  예외 발생
→ → → →
핸들러
(컨트롤러)
 
 render(model)
호출
 ↘    예외 해결 시도
       ExceptionResolver
       ↘
           ⑦ afterCompletion
              afterCompletion
   
HTML 응답
← ← ← ←
View        

예외가 발생하면 ExceptionResolver 가 호출되어 예외 해결을 시도한다.

이때 예외가 해결되어도 postHandle( ) 은 호출되지 않는다.

 

사용 예시 - response.sendError(xxx)

public class MyHandlerExceptionResolver implements HandlerExceptionResolver {

  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, 
                                       Object handler, Exception ex) {
                                       
    try {
      if (ex instanceof IllegalArgumentException) {
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
        return new ModelAndView();
      }
    } catch (IOException e) {
    }
    
    return null;
  }
}

응답에 response.sendError(xxx) 를 통해 원하는 HTTP 상태코드와 메시지를 담아준다.

 

사용 예시 - response.setStatus(xxx)

public class UserHandlerExceptionResolver implements HandlerExceptionResolver {

  private final ObjectMapper objectMapper = new ObjectMapper();

  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, 
                                       Object handler, Exception ex) {
                                       
    try {
      if (ex instanceof IllegalArgumentException) {
        String acceptHeader = request.getHeader("accept");
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        
        if ("application/json".equals(acceptHeader)) {
          Map<String, Object> errorResult = new HashMap<>();
          errorResult.put("ex", ex.getClass());
          errorResult.put("message", ex.getMessage());
          String result = objectMapper.writeValueAsString(errorResult);
          
          response.setContentType("application/json");
          response.setCharacterEncoding("utf-8");
          response.getWriter().write(result);
          
          return new ModelAndView();
        } else {
          return new ModelAndView("error/400");
        }
      }
    } catch (IOException e) {
    }
    
    return null;
  }
}

응답에 response.setStatus 를 통해 원하는 HTTP 상태코드를 담아준다.

 

HTTP 요청 헤더의 ACCEPT 값이 application/json 이면 JSON 으로 오류를 전달하고,

그 외의 경우는 HTML 에러 페이지를 보여준다.

 

@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
  resolvers.add(new MyHandlerExceptionResolver());
  resolvers.add(new UserHandlerExceptionResolver());
}

extendHandlerExceptionResolvers 를 통해 ExceptionResolver 를 등록한다.

 

> 참고 <

등록할 때, configureHandlerExceptionResolvers( ) 를 사용하면 스프링이 기본으로 등록하는 ExceptionResolver 가 제거되므로 extendHandlerExceptionResolver 를 사용해야 한다.

 

반환값에 따른 동작 방식

빈 ModelAndView : 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴

ModelAndView 지정 : 지정한 뷰를 렌더링

null : 처리할 수 있는 다음 ExceptionResolver 를 찾고, 없으면 기존에 발생한 예외를 서블릿 밖으로 던짐

 

활용

- 예외 상태 코드 변환

  : response.sendError(xxx) 사용

    > 이후 WAS 는 내부적으로 BasicErrorController 를 통해 에러 페이지를 찾아 호출하여 추가 프로세스가 실행된다.

  : response.setStatus(xxx) 사용

    > 스프링 MVC 에서 예외 처리가 끝이 나고, WAS 에서는 정상 처리되어 추가 프로세스가 실행되지 않는다.

- 뷰 템플릿 처리

  : ModelAndView 를 지정하여 예외에 따른 새로운 에러 페이지 뷰 렌더링 가능

- API 응답 처리

  : response.getWriter( ).println("hello") 처럼 HTTP 응답 바디에 직접 JSON 등 데이터를 넣어줄 수도 있음

 

스프링 ExceptionResolver

스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음 순서로 등록되어 사용된다.

1. ExceptionHandlerExceptionResolver

2. ResponseStatusExceptionResolver

3. DefaultHandlerExceptionResolver

 

ExceptionHandlerExceptionResolver

@ExceptionHandler 를 처리한다.

 

@ExceptionHandler

API 오류 응답의 경우에 발생하는 아래의 문제점을 해결한다.

1. ModelAndView 로 반환

2. response 에 직접 응답 데이터를 넣기

3. 동일한 예외를 컨트롤러 별로 처리하기 어려움

 

사용 방법

@Data
@AllArgsConstructor
public class ErrorResult {
  private String code;
  private String message;
}

예외가 발생했을 때 API 응답으로 사용하는 객체를 정의한다.

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
  return new ErrorResult("BAD", e.getMessage());
}

@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(Exception e) {
  ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
  return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}

@RestController 로, @ResponseBody 가 적용되어 HTTP 컨버터가 사용되어 응답이 JSON 으로 자동 변환된다.

 

@ExceptionHandler

  : 해당 컨트롤러에서 처리하고 싶은 예외를 지정

  : 예외가 발생하면 이 메서드가 호출되며, 지정한 예외 + 자식 클래스까지 잡을 수 있다.

  : @ExceptionHandler 에 예외를 생략하면, 메서드 파라미터의 예외가 지정된다. 

@ResponseStatus 

  : HTTP 상태 코드를 지정한다.

  : 애노테이션이므로 HTTP 상태 코드를 동적으로 변경할 수 없다.

ResponseEntity

  : HTTP 상태 코드와 메시지를 지정한다.

  : 프로그래밍해서 HTTP 상태 코드를 동적으로 변경할 수 있다.

 

ResponseStatusExceptionResolver

HTTP 상태 코드를 지정해 준다.

 

- @ResponseStatus 달려있는 예외

response.sendError(xxx) 로 HTTP 상태 코드를 변경하고, 메시지도 담는다.

이 때, response.sendError(xxx) 를 사용했기 때문에 WAS 에서 내부적으로 에러 페이지를 호출한다.

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

reason 은 MessageSource 에서 찾는 기능도 제공한다.

(ex. reason = "error.bad")

 

하지만 직접 변경할 수 없는 예외에는 적용할 수 없고, 조건에 따라 동적으로 변경하는 것이 어렵다.

 

- ResponseStatusException 예외

동적으로 HTTP 상태코드와, 에러 메시지, 실제 발생한 예외를 지정할 수 있다.

@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
  throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}

이때도 MessageSource 에서 찾는 기능이 제공된다.

 

DefaultHandlerExceptionResolver

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

 

예를 들어, 파라미터 바인딩 시점에 타입이 맞지 않으면 발생하는 TypeMismatchException

WAS 까지 전달되어 500 오류를 발생한다.

하지만 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이므로

DefaultHandlerExceptionResolver 가 400 오류로 변경한다.

 

이때, response.sendError(xxx) 를 사용했기 때문에 WAS 에서 내부적으로 에러 페이지를 호출한다.

 

@ControllerAdvice

@ControllerAdvice, @RestControllerAdvice 를 사용하여 @ExceptionHandler 와 컨트롤러 메서드를 분리할 수 있다.

 

@ControllerAdvice

- 대상으로 지정한 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여해 주는 역할

- 대상을 지정하지 않으면 모든 컨트롤러에 적용

- @RestControllerAdvice 는 @ResponseBody 가 추가된 것과 같다.

 

대상 컨트롤러 지정 방법

@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

출처 | 스프링 MVC 2(김영한) - 인프런

728x90