ExceptionResolver를 이용한 API예외 처리
들어가기에 앞서
개발을 하면 할 수록 점점 중요하게 느껴지는 것이 예외처리 인 것 같다. 처음 개발을 시작할 때는 만들어서 동작하는것이 중요했지만 이제는 작게 만들더라도 문제가 생기지 않도록 만드는 것이 중요하다 라는 것을 느끼고 있다.
예외에 따른 view를 호출하는 것은 어렵지 않다. 이미 스프링에서 간단하게 사용할 수 있도록 모든 세팅이 되어있고 개발자는 간단하게 사용하기만 하면 된다. 하지만 API예외는 많이 신경을 써야한다. 단순히 에러코드나 에러종류에 따라서 veiw를 보여주는 것과는 다르게 API종류마다, 예외마다 다른 방식으로 보여줘야 하기 때문이다. view와는 다르게 json으로 반환해줘야 하는 경우도 있을 것이다. 결국 내부적으로 어떻게 할지 정하고 정한대로 만들어주면 되기때문에 각자의 방식대로 활용하면 될 것이다.
Spring MVC 예외처리
먼저 가장 기본적인 예외처리의 메커니즘을 알아보자.
서버에 요청이 들어오면 크게는 아래와 같이 전달이 된다.
- WAS - (필터) - 서블릿 - (인터셉터) - 컨트롤러
여기서 만약 컨트롤러에서 예외가 터지면 다시 반대로 WAS까지 돌아와서 500에러에 대한 내용을 보여주게 된다.
에러에 대한 설정을 해주면 WAS에서 바로 에러페이지를 보여주는 것이 아니라 설정된 내용에 따라서 다시 컨트롤러를 호출해서 원하는 페이지를 보여주거나 json으로 보여주거나 하는 등의 행동이 가능해진다. 그런데 이 과정은 너무 복잡하다. 요청도 한 번에 끝나는 것이 아니라 다시 WAS까지 왔다가 새로운 요청을 보내다보니 필터나 인터셉터도 동일하게 거치게 된다.
Spring MVC는 HandlerExcpetionResolver라는 것을 이용해서 다시 WAS까지 돌아가지 않고 컨트롤러에서 터진 예외를 바로 받아서 처리한 후 정상처리된 것처럼 마무리 할 수 있게 해준다. 이렇게 하면 예외를 들고 WAS까지 갔다가 다시 예외 관련 컨트롤러를 호출하는 비효율적인 움직임이 개선될 수 있다.
이런 과정으로 인해 예외를 정상적으로 처리할 수 있게 된다.
이제 사용하는 방법을 코드로 알아보자.
먼저 예외를 터뜨릴 컨트롤러를 만들어준다.
@Controller
public class ExceptionResolverController {
@GetMapping("/error/mvc")
public void errorMvcTest() {
throw new IllegalArgumentException("SpringMVC 예외처리");
}
}
IllegalStateException라는 예외를 발생시킨다.
이제 이 예외를 잡아줄 ExceptionResolver가 필요하다.
public class SpringMVCException implements HandlerExceptionResolver {
ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
String accept = request.getHeader("accept");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if (accept.equals("application/json")) {
HashMap<String, Object> errorResult = new HashMap<>();
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/500");
}
}
} catch (IOException e) {
e.getStackTrace();
}
return null;
}
}
HandlerExceptionResolver를 상속받아서 사용하면 된다.
코드를 간단하게 설명하자면
- 파라미터로 넘어온 Exception이 IllegalArgumentException인지 확인한다.
- HTTP헤더에서 accept에 대한 정보를 가져와서 json일때와 아닐때를 다르게 처리해준다.
- 응답 코드를 400에러로 변경한다.
- 예외의 대한 내용을 Map을 이용해서 저장한 뒤 json으로 변경해서 response에 담아준다.
- 빈 ModelAndView를 넘기면 지정된 view가 없기 때문에 바로 종료된다.
- 만약 accept가 json이 아니라면 원하는 view로 이동시킨다.
ExceptionResolver도 여러개를 설정할 수 있는데 return이 null일 경우에는 다음 ExceptionResolver로 이동시킨다.
이 처럼 발생한 예외의 종류에 따라서 원하는 대로 설정해서 예외를 처리하는 것이 가능하다. 하지만 아쉬운 부분들이 분명히 존재한다. 반드시 ModelAndView를 리턴해야하므로 우리가 흔히 사용하는 API의 형식과는 다르다는 점과 하나의 예외를 처리하는데 많은 코드를 작성해야한다는 불편한 점들이 있다. 이러한 불편한 점들이 SpringBoot에서 다시 한번 크게 개선되었다.
SpringBoot 예외처리
SpringBoot에서는 총 3가지의 예외처리 인터페이스를 제공한다.
- ExceptionHandlerExceptionResolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver
예외가 발생했을 때 위에서부터 우선순위를 가지고 구현체를 체크하고 예외를 처리할 수 있도록 해준다.
지금 알아볼 것은 가장 많이 사용하고 있는 ExceptionHandlerExceptionResolver에 대해서 알아보자.
이떄까지는 예외를 처리하기 위해서 따로 파일을 만들고 WebConfig에서 등록하고 또 response에 여러가지 설정들을 직접 입력해줘야 했지만 이번에 사용할 @ExceptionHandler 어노테이션을 사용하면 예외가 발생하는 컨트롤러 내부에 간단하게 선언해서 예외를 처리할 수 있다.
@RestController
public class ExceptionResolverController {
@ExceptionHandler(IllegalArgumentException.class)
public ExceptionDto exHandler(IllegalArgumentException e) {
return new ExceptionDto("IllegalArgumentException", e.getMessage());
}
@GetMapping("/error/mvc")
public void errorMvcTest() {
throw new IllegalArgumentException("SpringMVC 예외처리");
}
}
예외를 발생시키는 컨트롤러이다. 예외를 발생시키는 API위에 보면 @ExceptionHandler로 선언된 메소드가 있다.
@ExceptionHandler를 선언한 후 처리할 예외 클래스를 입력해주면 해당 컨트롤러에서 선언한 예외클래스가 발생하면 반드시 이 메소드를 사용해서 처리가 된다. 현재 선언된 컨트롤러가 RestController이기 때문에 객체를 반환해도 json으로 변환되서 반환된다.
이 외에도 컨트롤러가 가지는 기능들을 대부분 사용할 수 있기때문에 쉽고 유연하게 예외를 처리할 수 있게된다.
만약 여기서 해당 컨트롤러만 적용되는 것이 아니라 여러 컨트롤러를 통틀어서 적용시키고 싶다면 @ControllerAdvice를 사용하면 된다.
일반적인 @Controller와 비슷한 기능이지만 처리하고자 하는 예외가 발생했을 때 지정한 컨트롤러에 해당하는 모든 예외를 가져와서 처리할 수 있다. 특별하게 컨트롤러를 지정하지 않으면 모튼 컨트롤러에 적용이 된다.
@ControllerAdvice와 @RestControllerAdvice 두 종류가 있으며 @RestController와 마친가지로 @ResponseBody가 붙고 안붙고의 차이이다.
기존의 ExceptionResolverController에서 만든 @ExcpetionHandler를 새로만든 ExceptionControllerAdvice에 옮겨서 실행시켜보자.
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ExceptionDto> exHandler(IllegalArgumentException e) {
ExceptionDto exceptionDto = new ExceptionDto("IllegalArgumentException", e.getMessage());
return new ResponseEntity(exceptionDto, HttpStatus.BAD_REQUEST);
}
}
@RestControllerAdvice로 만들었기 때문에 json으로 변환되어서 return될 것이다.
코드를 이렇게 옮기고 다시 실행시키면 컨트롤러 내부에서 @ExcpetionHandler를 만들어서 실행시킨 것과 같은 결과가 나타나는 것을 알 수 있다.
앞에 잠시 설명했던 것처럼 모든 컨트롤러에 글로벌하게 적용되는 것이 아니라 원하는 컨트롤러만 지정해서 적용 시킬수도 있다.
컨트롤러의 범위를 지정하는 방법으로는 크게 3가지가 있다.
// 특정 어노테이션이 지정된 컨트롤러를 선택하는 방법
@RestControllerAdvice(annotations = RestController.class)
// 특정 패키지와 하위에 있는 패키지를 선택하는 방법
@RestControllerAdvice("hello.exception.servlet")
// 특정 컨트롤러 이름을 직접 선택하는 방법
@RestControllerAdvice(assignableTypes = {ErrorPageController.class, ExceptionResolverController.class})
처음 어노테이션을 지정할 떄 어노테이션 옆에 옵션으로 원하는 방법으로 선택해서 컨트롤러의 범위를 지정할 수 있다.
예외를 처리하는 작업은 절대 소홀히 하면 안되는 작업이기 때문에 이번에 공부하면서 간단하게 작성해보았다.
'Spring > Boot' 카테고리의 다른 글
스프링 설정 환경 분리 (0) | 2022.06.18 |
---|---|
스프링부트 검증기능 사용하기(Bean Validation) (0) | 2022.03.19 |
댓글
이 글 공유하기
다른 글
-
스프링 설정 환경 분리
스프링 설정 환경 분리
2022.06.18 -
스프링부트 검증기능 사용하기(Bean Validation)
스프링부트 검증기능 사용하기(Bean Validation)
2022.03.19