Spring MVC : HTTP API 예외처리
- Spring/Spring MVC
- 2022. 1. 6.
이 포스팅은 인프런의 김영한님의 강의를 복습하며 정리한 내용입니다.
HTTP API 예외 처리
앞선 포스팅에서 요청 파라미터에 대한 예외처리를 하는 방법(Spring MVC : Error 관련)을 살펴봤다. 요청 파라미터를 예외처리하는 방법은 간단했다. 왜냐하면 Spring MVC가 제공하는 BasicErrorController에서 어떤 ErrorPage를 사용할지 정해져있기 때문에 개발자들은 사용할 에러 페이지만 만들어서 등록하면 됐다. 즉, 페이지 단위로 처리하기 때문에 고려할 내용들이 많지 않았다.
하지만 HTTP API 예외처리는 요청 파라미터 처리와는 다르다. API는 각 오류 상황에 맞는 API 스펙을 설정하고, 그에 대한 JSON 스펙을 내려주어야 하기 때문이다. 혹자는 요청 파라미터처럼 HTML을 내려줘도 괜찮을 것이라고 이야기하겠지만, HTML은 웹 브라우저가 아니면 사용할 수 있는 곳이 많지 않다. 따라서 JSON 형태의 데이터를 내려줘야한다.
이번 포스팅에서는 JSON 형태의 데이터를 내려주기 위해서, 그리고 잘 내려주기 위해서 발전해왔던 눈물겨운 과정들을 하나하나 정리하고자 한다.
Servlet의 ErrorPage + ErrorController + Produce 활용으로 구현
API 컨트롤러 구현
@GetMapping("/api/members/{id}")
public MemberDao getMember(@PathVariable String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 예외입니다. ");
}
return new MemberDao(id, "hello " + id);
}
- API 컨트롤러를 구현했다.
- 'ex'라는 값이 들어오게 되면 RuntimeException을 throw한다.
Exception은 처리해주는 것이 없기 때문에 서블릿 컨테이너까지 가게 된다. 서블릿 컨테이너에서는 WebCustomizer를 통해 등록해둔 ErrorPage가 ErrorController를 맵핑해준 상태가. 그렇지만 HTTP API에 대한 Error Controller는 없기 때문에 이것을 구현해줘야 한다.
HTTP API Error Controller 구현하기
@GetMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(HttpServletRequest request, HttpServletResponse response) {
log.info("error Page 500 API");
Map<String, Object> errors = new HashMap<>();
Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
errors.put("ex", ex.getClass());
errors.put("message", ex.getMessage());
Integer statusCode = (Integer) request.getAttribute(ERROR_STATUS_CODE);
return new ResponseEntity<>(errors, HttpStatus.valueOf(statusCode));
}
- Error Controller를 구현해준다.
- 클라이언트는 요청을 할 때 Header의 Accept에 받고자 하는 Media-Type을 설정해서 보낸다.
- 컨트롤러는 Produce를 통해서 Media-Type이 application/json일 때, 맵핑될 수 있도록 설정한다.
- Map을 만들고, 이것을 ResponseEntity에 담아서 Return해준다.
- ResponseEntity는 HTML 메시지의 Body에 직접 들어가게 된다.
결과
POSTMAN으로 테스트를 하면 다음과 같은 응답이 나오는 것을 볼 수 있다.
- 메세지는 JSON 형태로 잘 나왔다.
- 우측 상단의 상태 코드도 500으로 잘 나왔다.
그런데 약간의 문제가 있다. 우리는 Spring MVC의 BasicErrorController를 사용해서 에러 메세지를 처리해왔다. 그런데 이 서블릿 기술을 사용하면, 서블릿 Error Page를 내가 직접 하나하나 만들어야한다. 그럴 수 있냐? 그럴 수 없다! 어떻게 찾아온 광명인데... 그렇다면 어떻게 Spring MVC의 BasicErrorController를 사용할 수 있을지 살펴보자.
BasicErrorController를 활용한 JSON 에러 메세지 받기
API 컨트롤러 구성하기
@GetMapping("/api/members/{id}")
public MemberDao getMember(@PathVariable String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 예외입니다.");
}
if (id.equals("bad")) {
throw new IllegalArgumentException("잘못된 요청입니다.");
}
return new MemberDao(id, "hello " + id);
}
- API 컨트롤러에 "bad"가 들어오면, IllegalArgumentException이 발생하도록 했다.
- 이 Exception은 서블릿 컨테이너까지 타고 올라가서, BasicErroController의 힘을 빌린다.
이렇게까지 구현하면 우리는 HTTP API 응답을 내려주는데, 더 해야할 일이 없다. 이유는 BasicErrorController를 살펴보면 알 수 있다.
BasicErrorController 내부 코드 보기
- 위는 BasicErroController의 내부 구현 모습이다.
- Produce 옵션을 통해서 TEXT/HTML의 값이 들어오면 BasicErrorController는 ModelAndView에 값을 넣어서 View를 랜더링해준다.
- JSON 값이 들어오면 ResponseEntity를 반환해준다.
BasicErrorController는 아주 기본적인 형태의 JSON 응답을 지원해준다. 따라서 이 기능을 사용해도 무방하다.
BasicErrorController HTTP API 응답의 한계
BasicErrorContrller HTTP API 응답은 한계가 있다. 상태 코드를 설정할 수가 없다는 점이다. 항상 Exception이 터져서 서블릿 컨테이너로 전달되고, 그 서블릿 컨테이너는 에러 코드를 Request에 담아서 전달해준다. 그런데 서버 내부적으로 터진 것이기 때문에 항상 500으로 전달이 된다.
그렇지만 HTTP API는 항상 500이 아닐 수 있다. 왜냐하면 API 스펙을 정하기 나름이기 때문이다. 예를 들어서 어떤 값이 들어왔을 때는, 400으로 내려줘야하고 어떤 값으로 내려줬을 때는 500으로 내려줘야한다. 또 어떤 경우에는 404로 내려줘야한다. 이렇게 여러가지 형태의 응답을 하기 위해서는 BasicErrorController만으로는 한계가 있다.
이걸 극복하기 위해 HandlerExceptionResolver를 이용해본다.
HandlerExceptionResolver
개념
HandlerExceptionResolver는 컨트롤러 밖으로 예외가 던져졌을 경우, 동작을 새로 정의할 수 있는 방법을 제공한다. 이 때 HandlerExceptionResolver를 사용하면 동작을 새로 정의할 수도 있고, 다른 형태로 응답 코드를 변경할 수도 있다.
또 좋은 점은 Exception 처리가 간단해진다는 것이다. 기존의 BaiscErrorController는 반드시 서블릿 컨테이너까지 Exception을 던진 다음에 다시 한번 요청이 나가는 형태로 Exception을 처리했다. 그렇지만 HandlerExceptionResolver를 사용하면 도중에 Exception을 처리해서 다시 한번 재요청없이 Exception을 갈무리 할 수 있다.
Handler가 끝나고 난 뒤, DisPathcer Servlet은 등록되어있는 모든 HandlerExceptionResolver에 대해서 검사를 시작한다. HandlerExcpetionResolver에서 다루는 Error가 있는지를 확인하고, 그 에러가 있으면 HandlerExcpetionResolver에 의해서 재정의 된 동작을 수행한다.
Dispatcher Servlet에는 다음과 같이 HandlerExceptionResolver를 살펴보는 코드가 포함되어있다. 즉, Exception이 있건 없건 항상 체크를 한다는 점을 인지하면 된다.
HandlerExcpectionResolver를 사용하기 위해서는 1) HandlerExceptionResolver의 구체를 만들고 2) 구체를 MVC Config에 등록해주면 된다. 이렇게 등록해주면 Dispatcher Servlet은 값이 들어올 때 마다 체크하고 처리를 해준다.
HandlerExceptionResolver의 사용 방법
HandlerExceptionResolver의 오버라이드한 메서드의 Type은 항상 ModelAndView다. 이 때, Return 값이 어떤 값이 되느냐에 따라서 HandlerExcpetionResolver 이후에 동작은 달라진다.
- new ModelAndView()
- 빈 ModelAndView를 return 해주기 때문에 ViewName이 없다. 따라서 바로 서블릿 컨테이너까지 넘어간다.
- new ModelAndView("viewName)
- View 이름을 지정해주었기 때문에 ViewResolver를 통해 View를 만든다. 그리고 View가 Rendering된다.
- null
- 다음 ExceptionResolver를 찾아서 실행한다. 만약, 다 찾았는데 없으면 Exception을 서블릿 컨테이너까지 바로 던진다.
HandlerExceptionResolver의 활용처
HandlerExcpetionResolver는 아래 세 가지 형태로 활용해서 Exception을 없앨 수도 있고, 상태 코드를 바꿔 응답할 수도 있다.
- 예외 상태 코드 변환
- response.sendError(...)를 통해 서블릿 컨테이너에 상태 코드에 따른 오류를 처리하도록 위임한다.
- 서블릿 컨테이너는 BasicErrorController의 경우 "/error"로 맵핑된 컨트롤러가 호출된다.
- View Template 처리
- ModelAndView에 ViewName을 넣어서 반환해주면, ViewResolver가 View를 만들어 랜더링 해준다.
- API 응답 처리
- response.getWriter().write()를 통해서 Response HTML Body에 직접 데이터를 넣어주는 것이 가능하다.
HandlerExceptionResolver로 상태코드만 바꿔서 내려주기
API 컨트롤러 구현
@GetMapping("/api/members/{id}")
public MemberDao getMember(@PathVariable String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 예외입니다.");
}
if (id.equals("bad")) {
throw new IllegalArgumentException("잘못된 요청입니다.");
}
return new MemberDao(id, "hello " + id);
}
- API 컨트롤러는 "bad"가 오면 IllegalArgumentException이 던져지도록 구현되었다.
API 컨트롤러를 구현했다. 이 API 컨트롤러에서 던지는 Exception을 중간에서 처리해 줄 HandlerExcpetionResolver를 구현해본다.
HandlerExceptionResolver 구현
@Slf4j
public class MyHandlerExcpetionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
log.info("call resolver", ex);
try {
if (ex instanceof IllegalArgumentException) {
log.info("MyHandlerExcpetionResolver resolve to 400");
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
String requestURI = request.getRequestURI();
return new ModelAndView();
}
} catch (Exception e) {
log.error("e",e);
}
return null;
}
}
- HandlerExceptionResolver를 implements 했다.
- 목표는 단순히 상태 코드만 바꾸고, BasicErrorController를 활용해서 에러 처리를 하고자 한다.
- response.sendError를 활용해 상태 코드만 바꿔준다.
HandlerExceptionResolver의 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExcpetionResolver());
}
- WebMvcConfigurer의 구현체로 가서 extendHandlerExceptionResolver를 오버라이드 해준다.
- resolvers.add를 통해서 구현한 HandlerExceptionResolver를 등록해주기만 하면 된다.
결과
결과는 다음과 같이 BasicErrorController를 활용해서 "400" 상태 코드를 받을 수 있다는 점이었다. 기존의 BasicErrorController는 Produce를 통해서 단순히 '500' 코드를 반환해주기만 했다. 그렇지만 HandlerExceptionResolver를 이용해서 상태 코드를 설정했고, 그것을 받아볼 수 있게 했다.
이렇게 하다보면 욕심이 난다. 상태코드를 바꿨는데, 그럼 응답 받는 값도 바꿀 수 있는 거 아니야? 물론이다. 바꿀 수 있다. 아래에서는 응답 받는 Body를 바꾸는 것을 해보겠다.
HandlerExceptionResolver를 활용한 HTTP Body 메세지 전달
앞서 이야기 한 것처럼 HandlerExceptionResolver를 활용한 HTTP Body 메세지를 전달할 수 있다. 왜냐하면 HandlerExcpetrionResolver에는 HttpServletResponse가 있기 때문이다! Response에 getWriter()를 통해서 OutputStream을 작성해서 에러 메시지를 던져주면 된다!
이 때 HTTP BODY를 전달할 때는 하나 간과하면 안되는 것이 있다. 항상 JSON으로 줄 필요가 없다는 것이다. 클라이언트는 요청할 때 마다 필요한 응답 형태를 서버에 알려준다. 이것은 HEADER의 ACCEPT를 보고 알 수 있다. HEADER의 ACCEPT를 확인하고 필요한 형태의 값을 뿌려줄 수 있어야 한다.
HandlerExceptionResolver 구현
@Slf4j
public class UserHandlerExcpetionResolver implements HandlerExceptionResolver {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
log.info("UserHandlerExcpetionResolver");
String acceptHeader = request.getHeader("accept");
try {
if (ex instanceof UserException) {
if (acceptHeader.equals("application/json")) {
// aplication/json
Map<String, Object> resultError = new HashMap<>();
resultError.put("ex", ex.getClass());
resultError.put("message", ex.getMessage());
String result = objectMapper.writeValueAsString(resultError);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(result);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return new ModelAndView();
} else {
// html/text
return new ModelAndView("error/500");
}
}
} catch (Exception e) {
log.error("ex",e);
}
return null;
}
}
- ACCEPT HEADER를 먼저 확인한다.
- HEADER가 application/json이면 BODY에 응답을 준다.
- HEADER가 다른 것이면 500.html 페이지로 응답을 준다.
- setStatus를 이용해 상태 코드만 설정해준다.
- ModelAndView에는 Body값을 작성할 수 없다.
- response.getWriter().writer를 통해서 Body에 값을 적어주었다.
- 바이트 코드를 String으로 작성해줄 때는 항상 어떤 값을 인코딩 했는지 알려줘야한다.
- new ModelAndView()로 View 없이 controller로 돌아간다.
setStatus를 통해서 현재의 응답의 상태만 설정을 해준다. sendError를 치거나 Exception이 서블릿 컨테이너까지 올라가지 않았기 때문에 발생했던 Exception은 없어진 것이나 다름이 없다. 따라서 서블릿 컨테이너는 Exception 처리를 위해서 어떤 것도 수행하지 않는다.
이렇게 만들어 둔 HandlerExceptionResolver를 등록해주면 사용할 수 있다.
결과
다음과 같이 HTTP API 형식으로 응답을 받았고, 상태 코드 역시 400으로 받을 수 있었다. 위에서 구현한 것을 다시 한번 정리하면 다음과 같다.
- Handler에서 Error가 발생한다.
- Error가 Dispatcher Servlet으로 넘어온다.
- Dispatcher Servlet은 등록된 HandlerExcpetionResovler를 하나씩 살펴본다.
- MyHandlerExceptionResolver의 application/json Header를 보고 HTML Body에 값을 적어주었다.
- 이 때, setStatus로 상태값만 설정해주었기 때문에 서블릿 컨테이너는 어떤 Exception도 발생하지 않은 것으로 이해한다
- ModelAndView()로 반환해주었기 때문에 서블릿 컨테이너까지 바로 나간다.
- 서블릿 컨테이너에서 Exception 체크 시 문제 없기 때문에 응답이 내려진다.
위 과정을 통해 BasicErrorController를 거치지 않고 HandlerExceptionResolver에서 Exception을 처리해버리고, HTML BODY에 데이터를 내려줄 수 있었다. 좋은 방법이었다.
그런데 여기서 다시 한번! 한 가지 고민이 남는다. 바로 하나하나 언제 이 HandlerExceptionResolver를 등록하는 것이냐는 것이다. 그게 가장 큰 문제다. 따라서 스프링은 또 좋은 방안을 하나 제안해준다. 바로 스프링부트가 자동으로 등록해주는 HandlerExceptionResolver다.
스프링이 등록해주는 HandlerExceptionResolver
스프링 부트는 개발자의 편의를 위해서 HandlerExceptionResolver를 미리 만들어서 제공을 해준다. 아래는 우선순위 기준으로 정렬되어있다.
- ExceptionHandlerExceptionResolver → 항상 가장 높은 우선순위를 가진다.
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionREsolver
기본적으로 스프링이 등록해서 사용해주기 때문에 어떻게 사용 되고 있는지, 사용할 수 있는지를 살펴보고 쓰면 된다.
스프링이 등록해 준 ResponseStatusExceptionResolver
ResponseStatusExcpetionResolver는 아래 두 가지 경우에 발생하는 Exception들에 대해서만 처리를 해주는 Resolver다.
- @ResponseStatus
- ResponseStatusException
@ResponseStatus 어노테이션이 달려져있는 Exception Class가 발생했을 때, ResponseStatusExceptionResolver는 처리를 해준다. 또한 ReponseStatusException(...)으로 감싸져있는 Exception을 봤을 때도 ResponseStatusExceptionResolver가 처리를 해준다.
주의 사항
ResponseStatusExceptionResolver는 sendError를 해준다. 따라서 BasicErrorController에게 Exception 처리를 맡긴다는 점을 이해해야한다.
사용방법
@ResponseStatus로 사용하기
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException{
}
- @ResponseStatus에 code로 상태를 넣어주고, reason에 에러 메세지를 넣어준다.
- 이 때 "error.bad"를 하게 되면 message.properties에서 가장 먼저 메세지가 있는지 뒤진다.
- 없으면 error.bad를 그대로 출력해준다.
ResponseStatusExcpetion으로 감싸기
@GetMapping("/api/response-status-ex1")
public void ex2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "실패 실패",new RuntimeException());
ResponseStatusExcpetion으로 감싸서 에러를 던져주면 된다.
스프링이 등록해 준 DefaultHandlerExceptionResolver
DefaultHandlerException은 Type Mismatch Error처럼 스프링 내부에서 발생하는 예외를 해결해주는 HandlerExceptionResolver다. 이런 Type Mismatch Error는 사실 서버 내부에서 발생하는 것이라기보다는 클라이언트가 잘못된 값을 입력했다고 이해할 수 있다.
따라서 500의 값을 내려주는 것보다는 400의 값을 내려주는 것이 합당하다. DefaultHandlerExceptionResolver는 이런 것들을 내부적으로 열심히 수행해준다.
위의 코드를 보면 DefaultHandlerExceptionResolver도 sendError를 통해서 BasicErrorController를 통해서 에러를 처리하도록 해준다.
DefaultHandlerExceptionResolver 코드로 확인해보기
@GetMapping("/api/response-status-ex3")
public String ex3(@RequestParam Integer data) {
return "ok";
}
다음 코드를 작성하고 쿼리 파라미터에 문자값을 넣어주면 된다.
쿼리 파라미터를 넣어주고 클라이언트가 요청을 보내면 다음과 같이 DefaultHandlerExceptionResolver가 내부적으로 Bad Request를 띄운 것을 확인할 수 있다.
스프링이 제공하는 ExcepionHandlerExceptionResolver
스프링이 제공하는 궁극의 HandlerExceptionResolver다. 어노테이션 기반으로 동작을 하기 때문에 상당히 유연하게 사용을 할 수 있다. 뿐만 아니라 기존에는 사용하기 위해서 WebMvcConfigurer에 등록을 했어야 했는데, 어노테이션을 붙여주는 것만으로 사용이 완료된다.
또한, 기존에는 항상 ModleAndView를 반환받았기 때문에 HTTP API 응답을 넣어주는게 매우 곤란했었다. 그렇지만 이 어노테이션을 사용하게 되면 @RestController나 ResponseEntity를 사용할 수 있기 때문에 한결 더 수월하게 사용할 수 있다.
사용하는 방법
- @ExcpetionHandler를 예외처리 메서드에 달아주면 된다. 또한, Exception 클래스를 넣어주면 그 Exception 클래스에 대해서만 자동으로 맵핑되어 동작하게 된다.
- @ExceptionHandler가 표시되어있는 컨트롤러 내에서만 Exception을 캐치하고 동작한다.
- @ExcptionHandler에 기입된 예외의 자식까지 모두 처리해준다.
- @ExcpetionHandler에 Exception이 기입되어있지 않다면, 매개변수로 받는 Exception Type이 자동으로 기입된다.
- Return Type은 Object, ResponseEntity 등을 받을 수 있다.
- 이 때 Object를 받는 경우 @ReponseStatus를 통해 상태 코드를 설정할 수 없다. 설정하지 않을 경우 200 OK 나감
- ResponseEntityt는 HTTP 응답 코드까지 모두 설정할 수 있다.
- ExceptionHandlerExceptionResolver는 여기서 Exception을 처리해준다. 즉, Exception이 서블릿 컨테이너까지 올라가지 않는다.
보충학습 @ResponseStatus + @ExcpetionHandler
앞서 @ResponseStatus를 이용하면 @ResponseStatusHandlerExceptionResolver가 동작한다고 했다. 그리고 이 Resolver는 sendError를 통해 400 에러를 보내주고, BasicErrorController를 에러를 처리한다고 했다. 그렇지만 @ExceptionHandler를 사용할 때는 @ResponseStatus는 BasicErrorController로 넘어가게 처리를 하지는 않는다.
이유는 @ExceptionHandler가 스프링이 등록해주는 HandlerExceptionResolver 중에서 더 높은 우선순위를 가지고 동작하기 때문이다. 따라서 이 때, @ResponseStatus는 Resolver를 통한 동작은 무시되고 단순히 상태 코드를 설정하는 용도로 사용된다.
사용예시1
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExResolver(HttpServletRequest request, HttpServletResponse response, IllegalArgumentException e) {
log.info("illegalExResolver Start!");
return new ErrorResult((Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE), e.getMessage());
}
- 객체를 바로 HTML BODY로 반환받았다.
- 따라서 @ResponseStatus를 이용해서 상태코드를 지정해주었다.
- @ExcpetionHandler에 반영할 Exception 객체를 설정해주었다.
@ExceptionResolver 사용 예시는 다음과 같다.
사용예시2
@ExceptionHandler
public ResponseEntity<ErrorResult> userExResolver(HttpServletRequest request, HttpServletResponse response, UserException exception) {
ErrorResult errorResult = new ErrorResult((Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE), exception.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.NOT_FOUND);
}
- @ExceptionResolver를 사용한다.
- @ExcpetiionResolver에 반영할 Exception을 설정하지 않았다. 따라서 매개변수에 있는 Exception을 반영한다.
- ResponseEntity로 값을 반환받는다. ResponseEntity에 Header값을 넣을 수 있다.
실행 결과는 위와 같다.
정리
@ExceptionResolver를 사용하면 앞서 직접 HandlerExceptionResolver를 만들고 등록했던 옛 시절이 기억이 날 것이다. 그 때를 생각하면 너무나 좋다. 왜냐하면 어노테이션만 달아주면 바로바로 사용이 가능하기 때문이다. 그렇지만 인간의 욕심은 끝이 없다. 한 가지 불편한 점이 또 생각났다!
바로 Exception을 처리하는 영역과 API Controller가 같은 영역에서 처리가 되고 있다는 점이다. 즉, 관심사의 분리가 필요할 것 같다. 그런데 앞서 이야기했지만 @ExceptionResolver는 @ExceptionResolver가 있는 Controller 내에서만 동작을한다. 그렇다면 분리를 하기 위해서는 어떤 것이 필요할까?
@ControllerAdvice, @RestControllerAdvice
@ControllerAdvice가 앞서 발생했던 문제를 해결하는데 많은 도움을 준다!
- @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여해주는 역할을 한다. 즉, 클래스를 분리해서 따로 관리할 수 있다!
- @ControllerAdvice는 대상을 지정하지 않으면 모든 컨트롤러에 적용된다(Global)
- @RestControllerAdvice = @ControllerAdvice + @ResponseBody
@ControllerAdvice를 활용해서 Exception 처리 로직을 다른 클래스로 분리해서 처리를 해줄 수 있다.
@ControllerAdvice로 Exception 처리 로직 분리 코드
@Slf4j
@RestControllerAdvice(assignableTypes = ApiExceptionControllerV2.class)
public class ApiControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExResolver(HttpServletRequest request, HttpServletResponse response, IllegalArgumentException e) {
log.info("illegalExResolver Start!");
return new ErrorResult((Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE), e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExResolver(HttpServletRequest request, HttpServletResponse response, UserException exception) {
ErrorResult errorResult = new ErrorResult((Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE), exception.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.NOT_FOUND);
}
}
- 다음과 같이 기존의 Exception 처리 로직을 @RestControllerAdvice 클래스로 분리할 수 있다.
- 이 때 @RestControllerAdvice에 BasePackage나 assignableType을 이용해서 적용할 곳을 선별할 수 있다.
기타 기능 : Message 기능 사용하기
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException{
}
다음과 같이 @ResponseStatus에 reason을 넣을 수 있다. 이 때, reason에는 출력될 에러 메세지를 넣는 곳이다. 그런데 약간의 꿀팁은 reason에 넣는 값은 messages.properties에서 사용할 수 있는 값을 먼저 뒤져본다는 것이다. 다음과 같이 동작한다.
- messages.properties에서 reason과 동일하게 맵핑되는 값을 찾는다. → 있으면 출력
- 없으면 error.bad 그 자체를 출력한다.
'Spring > Spring MVC' 카테고리의 다른 글
Spring MVC : @Value 어노테이션 (0) | 2022.01.08 |
---|---|
Spring MVC : Converter, Formatter 알아보기 (0) | 2022.01.07 |
Spring MVC : 로그인 처리하기 (0) | 2022.01.02 |
Spring MVC : Bean Validation 활용하기 (0) | 2022.01.02 |
Spring MVC : Validation (0) | 2021.12.26 |