[Spring MVC] 스프링 API 에러처리
- [ Backend ]/Spring Web
- 2022. 8. 22.
저번에 HTML 페이지를 이용한 에러 처리를 공부했다.
API 예외처리
그런데 앱 서버를 만드는 등 HTML을 사용할 수 없고 API를 사용해야 하는 경우에는 어떻게 해결할 수 있을까? 이 경우 HTML 오류 페이지를 만드는 단순한 방법으로는 문제를 해결할 수 없기 때문에 단순히 고객에게 오류 화면을 보여주는 것 이외에, 각 오류 상황에 맞는 오류 응답 스펙을 정하고 JSON으로 응답해주어야 한다.
1. BasicErrorController
스프링은 이러한 처리를 위해서 BasicErrorController라는 컨트롤러를 제공한다.(HTTP 에러 처리시에 이용했던 컨트롤러를 그대로 API 예외처리에 사용할 수 있다.) HTTP 예외처리시에 했듯이 json으로 요청해주면 알아서 오류API를 json으로 적절하게 생성해준다.
이렇게 BasicErrorController를 이용하면 매우 간단하게 오류를 처리할 수 있다. 그런데 단순히 예외상황에 맞는 뷰를 리턴해주면 되는 HTTP예외처리와는 다르게, API 오류처리는 각각의 예외마다 다른 응답을 출력하고, 상황에 따라 다르게 대응해야 하는 경우가 많다. 이를 위해서 @ExceptionHandler 어노테이션을 사용하자.
2. ExceptionResolver
원래, 컨트롤러에서 예외가 처리되지 않으면 바로 서블릿을 통해서 WAS로 예외가 전달되었다. 이러한 방법으로는 오류 메세지를 상세하게 컨트롤 할 수 없다. 그래서 스프링은 HandlerExceptionResolver라는 것을 이용해서 컨트롤러에서 예외를 처리하지 못하고 다시 서블릿으로 예외가 던져진 경우 적당한 ExceptionResolver가 존재하는지를 확인하여 예외 처리를 위임한다.
예를 들어, 클라이언트에게 특정 스펙은 사용하지 않도록 합의가 된 상황에서, 클라이언트가 실수로 그 스펙을 사용했다면 그것은 논리적으로는 클라이언트의 잘못(4xx error)이다. 하지만 실제로는 서버에서 예외를 발생시키기 때문에 서버 에러(5xx error)로 표시되게 된다. 이런 경우에 ExceptionResolver를 이용하여 5xx error를 4xx error로 바꿀 수 있다.
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()); //400error로 변경
return new ModelAndView();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
HandlerExceptionResolver를 구현하는 클래스를 만들고, 그 안에 있는 resolveException을 오버라이딩 해주면 된다. 그러면 컨트롤러에서 에러가 해결되지 않고 WAS로 던져질 경우 서블릿은 예외가 이 리졸버를 거치게 한다. 위 코드에서는 exception을 받아서 특정 에러일 경우 그 에러를 먹고, response.sendError로 새로 에러를 생성하고 ModelAndView객체를 반환한다.
----------------------------------------------------------------------------------------------------------------------------------------------------
비슷하게 특정 예외가 발생했을 때 별도의 json형태로 예외정보를 받고 싶을때도 HandlerExceptionResolver를 구현해서 만들면 된다. 그렇게 하면 정의해둔 UserException이 발생했을 때, ObjectMapper와 같은 json 파싱 라이브러리를 이용해서 원하는 형태의 json형태로 만들어 반환해줄 수 있다.
이렇게 ExceptionResolver를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver에서 예외를 처리해준다. 따라서 서블릿 컨테이너에는 예외가 전달되지 않고, 정상 처리로 인식해주지만, 그만큼 코드가 복잡해진다는 단점이 있다. 그러므로 ExceptionResolver를 직접 구현하는 대신 스프링이 제공하는 리졸버를 이용하자.
3. @ExceptionHandler
스프링이 제공하는 ExceptionResolver는 크게 세가지가 있고, 다음과 같은 우선순위를 가진다.
1. ExceptionHandlerExceptionResolver
> @ExceptionHandler 어노테이션이나 ExceptionHandlerException자체를 처리
2. ResponseStatusExceptionResolver
> ExceptionHandlerExceptionResolver가 동작하지 않을 경우 작동
> @ResponseStatus 어노테이션이나 ResponseStatusException 처리
> @ResponseStatus(code = ...)를 지정해주어서 어떤 예외를 발생시킬지 처리 가능
3. DefaultHandlerExceptionResolver
> 스프링 내부 기본 예외 (파라미터 바인딩시 발생하는 TypeMismatchException 등)에 반응하여 동작
> 서버에서 발생했지만 원인이 클라이언트에 있을 경우 5xx error가 아닌 4xx error 발생하도록 바꾸어줌
그 중에서도 @ExceptionHandler 어노테이션을 이용하여 ExceptionHandlerExceptionResolver를 사용하는 경우가 가장 많다. 이 경우 컨트롤러에서 예외가 던져지면 서블릿에서 ExceptionResolver에 예외 해결을 시도하고, 그 결과 @ExceptionHandler 어노테이션을 해결하기 위해서 ExceptionHandlerExceptionResolver가 실행되어 동작한다.
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
@RestController
public class ApiExceptionController {
//해당 클래스에서 발생하는 IllegalArgumentException 에러에 대해서 처리
@ResponseStatus(HttpStatus.BAD_REQUEST) //404 에러로 처리
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) {
return new ErrorResult("BAD", e.getMessage());
}
...
위와 같이 컨트롤러 안에 @ExceptionHandler 어노테이션과 받을 예외 종류를 선택해주면 (@RestController이므로) ExceptionHandlerExceptionResolver가 해당 컨트롤러 안에서 발생하는 예외들에 대해서 작동하여 정상적인 json형태로 변환하여 리턴한다. 따라서 에러가 발생했음에도 서버에서 그 에러를 처리했으므로 상태코드는 200이 된다.
4. @ControllerAdvice
@ExceptionHandler 어노테이션을 이용하면 예외를 깔끔한 로직으로 처리할 수 있지만, 컨트롤러 단위로 동작하기 때문에 컨트롤러마다 Exception Handler를 만들어주어야 한다는 문제가 있다.
이 대신 컨트롤러에 존재하는 ExceptionHandler들을 @ControllerAdvice(@RestControllerAdvice)로 선언된 클래스 밑으로 옮겨주면, 핸들러들이 전역성을 갖게 된다.
@RestControllerAdvice
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) {
return new ErrorResult("BAD", e.getMessage());
}
...
}
'[ Backend ] > Spring Web' 카테고리의 다른 글
[Spring MVC] 스프링 HTTP 에러 처리 (0) | 2022.08.18 |
---|---|
[Spring MVC] 서블릿 필터, 스프링 인터셉터 (0) | 2022.08.14 |
[Spring MVC] 쿠키+세션을 사용한 로그인 (0) | 2022.08.09 |
[Spring MVC] 쿠키를 사용한 로그인 (0) | 2022.08.08 |
[Spring MVC] 검증 2 - Bean Validation (0) | 2022.07.14 |