API 예외 처리 - HandlerExceptionResolver
예외가 발생해서 서블릿을 넘어 WAS까지 예외가 전달되면 HTTP 상태코드가 500으로 처리된다.
발생하는 예외에 따라서 400, 404 등등 다른 상태코드로 처리하고 싶다.
오류 메시지, 형식등을 API마다 다르게 처리하고 싶다.
예를 들어서 IllegalArgumentException 을 처리하지 못해서 컨트롤러 밖으로 넘어가는 일이 발생하면
HTTP 상태코드를 400으로 처리하고 싶다. 어떻게 해야할까?
(IllegalArgumentException면 클라이언트가 데이터를 잘못넘겨서 발생하는 예외이다.
그런데 예외가 발생하면 500(Internal Server Error)의 상태코드로 되기 때문에
응답을 받은 클라이언트는 서버에 문제라고 판단할 수 있다. 그러므로 400(Bad Request)로 바꿔주는것이 좋다.)
ApiExceptionController - 수정
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable String id) {
if (id.equals("ex")) { // ex라는 아아디가 반환되면
throw new RuntimeException("잘못된 사용자"); // 예외를 발생시켲고
}
//아래부분이 추가됨.
if (id.equals("bad")) { //bad라고 들어오면
throw new IllegalArgumentException("잘못된 입력값"); //IllegalArgumentException를 발생시킨다.
}
return new MemberDto(id, "hello" + id); // 아니라면 멤버를 반환해준다.
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
api에 bad라는 값이 들어오면 IllegalArgumentException를 던져주는 부분을 추가하였다.
(IllegalArgumentException이 발생하게끔해서 테스트하기위함.)
실행 결과
스프링부트의 기본 JSON 에러 응답에 몇가지 정보를 추가해줘서 자세하게 보이는것이다.
(실제 개발할때는 정보를 추가하지않고 로그로 확인해야함)
오류정보추가 설정 (application.properties)
server.error.include-exception=true
server.error.include-message=always
server.error.include-stacktrace=on_param
server.error.include-binding-errors=always
JSON응답의 결과를 보면 상태코드가 500이다.
WAS까지 예외가 올라가면 WAS는 예외가 발생했으니 500!으로 처리해준다.
(WAS는 예외(Exception)이 올라오면 상태코드를 500으로 처리해준다.
그런데 예외를 던진게 아니고, response.sendError(HTTP 상태 코드, 오류 메시지)로 처리됬다면 예외(exception)이 발생되는것은 아니고, 오류가 발생했다는 사실을 서블릿컨테이너의 서블릿(Dispatcher Servlet을 호출한)에게 알리는것이므로 호출된 response.sendError(HTTP 상태 코드, 오류 메시지)를 참고해서 상태코드를 내려준다.)
HandlerExceptionResolver
스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우
예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다.
컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 HandlerExceptionResolver 를 사용하면 된다.
줄여서 ExceptionResolver 라 한다.
ExceptionResolver 적용 전
ExceptionResolver 적용 후
컨트롤러(핸들러)에서 예외가 발생하면 DisPatcher Servlet(서블릿)은 postHandle()을 실행시키지않는것 까지는 동일하다.
그런데 ExcpetionResolver(HandlerExceptionResolver)가 존재한다면 ExcpetionResolver를 호출해서 예외 처리를 시도한다. ExcpetionResolver가 예외처리에 성공하면 ModelAndView를 반환해주고, 그 이후는 정상로직과 동일하다(뷰를 렌더링해주고 http응답메시지에 담기고, was에 가서 서블릿컨테이너의 서블릿(DisPatcherServlet을 호출한)이 응답을 클라이언트에게 내려주고)
(ExcpetionResolver에서 정상적으로 리턴이 되면 예외는 처리됬다고 간주한다.)
HandlerExceptionResolver - 인터페이스
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
Object handler : 핸들러(컨트롤러) 정보
Exception ex : 핸들러(컨트롤러)에서 발생한 발생한 예외
ExcpetionResolver는 ModelAndView를 반환해준다.
MyHandlerExceptionResolver
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
log.info("call resolver ",ex);
try {
if (ex instanceof IllegalArgumentException) { // 발생한 예외가 IllegalArgumentException이라면
log.info("IllegalArgumentException resolver to 400");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
// DisPatch Servlet은 HandlerExceptionResolver의 반환값으로 예외가 처리됬는지 안됬는지를 판단한다.
return new ModelAndView(); // ModelAndView 객체가 반환되면 예외처리 성공으로 간주시켜 예외를 먹어버린다. (예외가 WAS로 못올라가게)
// ModelAndView가 비어있으면 DisPatch Servlet은 뷰렌더링을 하지않고 WAS로 정상응답을 보낸다.
// ModelAndView내용이 있으면 DisPatch Servlet은 뷰렌더링을 하고, WAS로 정상응답을 보낸다.
// null이 반환되면 DisPatch Servlet은 다음 ExceptionResolver를 찾아서 실행한다.
//만약 처리할 수 있는 ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.
//response.sendError를 이용하면 에러에 대한 상태코드와 , 에러에 대한 메시지를 지정해줄 수 있다.
//WAS는 예외를 안올라왔고, response.sendError()가 호출된 기록이 있으므로 response.sendError()에 대한 내용을 보고 에러 처리를 해준다.
//WAS에 예외가 안올라온 이유는 ExceptionResolver에서 ModelAndView가 반환 됬기 때문이다.
}
} catch (IOException e) { //sendError을 사용하려면 throws IOException를 클래스에 붙여주던가 try catch문을 이용해야한다.
log.error("resolver ex", e); //혹시나 예외가 발생할 수 있으므로 로그로 찍어본다.
}
return null; //null로 리턴을 하게되면 예외가 처리됬다고 보지않고 , 다시 예외가 던져진다.
}
}
ExceptionResolver 가 ModelAndView 를 반환하는 이유는 마치 try, catch를 하듯이,
Exception 을 처리해서 정상 흐름 처럼 변경하는 것이 목적이다.
(원래는 컨트롤러에서 예외가 발생하지않았더라면 컨트롤러의 반환으로 ModelAndView가 반환됬었을거니까,
예외를 처리했다면 ModelAndView를 반환해서 나머지 과정을 정상적으로 진행하기 위해서이다)
(이름 그대로 Exception 을 Resolver(해결)하는 것이 목적이다.)
위 코드에서는 IllegalArgumentException이 발생하면 response.sendError(400) 를 호출해서
HTTP 상태 코드를 400으로 지정하고, 빈 ModelAndView 를 반환한다. (빈 ModelAndView == 뷰렌더링 x)
그렇게 정상흐름으로 WAS까지 정상응답이 도착하고 WAS는 response.sendError()가 호출됬는지 체크한다.
response.sendError(상태코드,메시지)가 호출됬으므로 파라미터내용을 이용해서 에러처리를 준비한다.
기본 에러 페이지 경로 (/error)를 request(요청)해서 BasicErrorController의 HandlerMethod가 호출된다.
postman으로 request했다면 Request Header의 Accept는 */*이므로 매핑조건이 없는 기본매핑 HandlerMethod가 호출되면서 응답을 JSON으로 보내준다. 그때 JSON의 형식은 스프링부트가 기본제공하는 형식이며 BasicErrorController가 생성해준다.
HandlerExceptionResolver 반환 값에 따른 동작 방식
HandlerExceptionResolver 의 반환 값에 따른 DispatcherServlet 의 동작 방식은 다음과 같다.
빈 ModelAndView
new ModelAndView() 처럼 빈 ModelAndView 를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.
ModelAndView 지정
ModelAndView 에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 한다.
null
null 을 반환하면, 다음 ExceptionResolver 를 찾아서 실행한다.
만약 처리할 수 있는 ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.
ExceptionResolver 활용
1. 예외 상태 코드 변환
예외를 response.sendError(xxx) 호출로 변경해서 서블릿(Dispatcher Servlet을 호출한 서블릿)에서 상태 코드에 따른 오류를 처리하도록 위임
이후 WAS는 서블릿 오류 페이지를 찾아서 내부 호출, 예를 들어서 스프링 부트가 기본으로 설정한 / error 가 호출됨
2. 뷰 템플릿 처리
ModelAndView에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링 해서 고객에게 제공
3. API 응답 처리
response.getWriter().println("hello");
HttpServletResponse를 이용해서 HTTP 응답 바디에 직접 데이터를 넣어주는 것도 가능하다.
response.getWriter().println("{userID : 1234}");
여기에 JSON 으로 응답하면 API 응답 처리를 할 수 있다.
https://keeeeeepgoing.tistory.com/178
https://keeeeeepgoing.tistory.com/162
ExceptionResolver 등록
@Configuration
public class WebConfig implements WebMvcConfigurer { //implements WebMvcConfigurer는 인터셉터등록,ExceptionResolver때문에 추가
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
ExceptionResolver 등록하는 두가지 방법이 있다.
1. configureHandlerExceptionResolvers()
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
사용하면 스프링이 기본으로 등록하는 ExceptionResolver 가 제거되므로 주의 ( 사용 X )
2. extendHandlerExceptionResolvers()
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
extendHandlerExceptionResolvers() 를 사용하면 된다.
결과 확인
IllegalArgumentException 발생시 ExceptionHandler로 인해 status가 400으로 처리된것을 확인가능하다.
RuntimeException 발생시
call resolver 로그가 찍히는것을 보면
어떤 예외가 발생하더라도 DisPatcher Servlet은 ExceptionHandler를 실행시키는것을 확인할 수 있다.
하지만 우리가만든 ExceptionHandler내부 동작상 IllegalArgumentException가 아니면 null을 반환하므로
예외가 처리되지않아서 서블릿(DispatcherServlet을 호출한 서블릿)까지
(서블릿까지 == 서블릿컨테이너까지 == WAS 까지)
예외가 전달된것을 확인할 수 있다. 그래서 WAS에서 Exception이 왔으니 상태코드를 500으로 처리한것이다.
API 예외 처리 - HandlerExceptionResolver 활용
예외를 여기서 마무리하기
예외가 발생하면 WAS까지 예외가 던져지고, WAS에서 오류 페이지 정보를 찾아서 다시 /error 를 호출하는 과정은
생각해보면 너무 복잡하다.
ExceptionResolver 를 활용하면 예외가 발생했을 때 이런 복잡한 과정 없이 여기에서 문제를 깔끔하게 해결할 수 있다.
예제로 알아보자.
먼저 사용자 정의 예외(커스텀예외)를 하나 추가하자.
UserException
public class UserException extends RuntimeException { //RuntimeException를 상속받는다.
//RuntimeException의 모든 메소드를 오버라이드한다. Ctrl + O
//오버라이드하고 아무것도 건들지않음
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
public UserException(String message, Throwable cause) {
super(message, cause);
}
public UserException(Throwable cause) {
super(cause);
}
protected UserException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
호출시 UserException 이 발생하도록 해두었다.
이제 이 예외를 처리하는 UserHandlerExceptionResolver 를 만들어보자.
UserHandlerExceptionResolver
@Slf4j
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 UserException) {
log.info("UserException resolver to 400");
String acceptHeader = request.getHeader("accept");// http요청헤더의 Accept가 application/json이냐 아니냐에 따라 처리가 달라진다.
response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 상태코드를 400으로 바꾼다.
if ("application/json".equals(acceptHeader)) { //"application/json"는 상수로 뽑아서 어디다가 정의해서 사용하는게 좋다.
Map<String, Object> errorResult = new HashMap<>(); //데이터를 담아줄 MAP
errorResult.put("ex", ex.getClass());
errorResult.put("message", ex.getMessage());
//그리고 넣어준 데이터를 HttpServletResponse에 넣어줘야한다. 그리고 http응답메시지에 필요한 설정들도 HttpServletResponse에 설정한다.
//그래야 서블릿에 가서 (Dispatcher Servlet을 호출한 서블릿) HttpServletResponse의 내용을 가지고 http응답메시지를 구성할테니까
//왜냐면 HandlerExceptionResolver는 반환이 ModelAndView이니까 http응답메시지를 설정할 수 있는 방법은 HttpServletResponse를 이용하는방법뿐이다.
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
//Map에 있는 데이터를 JSON으로 변환해야한다. HttpEntity를 사용할 수 있거나 그러면 좋은데 그럴수없으므로 ObjectMapper를 이용해서 변환한다.
String result = objectMapper.writeValueAsString(
errorResult); // 객체를 JSON으로 바꾸고, JSON을 String형식으로 바꿔주는 메소드를 이용한다.
response.getWriter().write(result); //http응답메시지 바디에 String 데이터를 넣는다.
return new ModelAndView(); //아무것도 보내지않아야 뷰 렌더링이 되지않는다. + ModelAndView가 반환되므로 Dispatcher Servlet은 예외는 처리됬다고 간주한다.
//was까지 정상 응답이 도착하고, was에 올라온 예외도 없고, response.sendError()호출된것도 없으니까
//서블릿이 HttpServletResponse 내용을 가지고 http응답메시지를 구성해서 응답해준다.
} else { // Accept가 application/json이 아닌 경우에는
return new ModelAndView("error/500"); //ModelAndView에 논리 뷰이름을 추가해서 반환해준다. (뷰가 렌더링될것이다.)
}
}
} catch (IOException e) { // HttpServletResponse 때문에 catch로 IOException를 잡아줘야함
log.error("resolver ex", e);
}
return null;
}
}
HTTP 요청 해더의 ACCEPT 값이 application/json 이면 JSON으로 오류를 내려주고,
그 외 경우에는 error/500에 있는 HTML 오류 페이지를 보여준다
WebConfig에 UserHandlerExceptionResolver 추가
ExceptionResolve를 만들었으니까 등록해줘야한다.
@Configuration
public class WebConfig implements WebMvcConfigurer { //implements WebMvcConfigurer는 인터셉터등록,ExceptionResolver때문에 추가
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
resolvers.add(new UserHandlerExceptionResolver());
}
실행 결과
1. accept가 application/json인 경우
로그를 보면 Exception이 터졌으니까 MyHandlerExceptionResolver도 실행된다.
하지만 MyHandlerExceptionResolver에서 처리 못했기 때문에 (null이 리턴됬음)
UserHandlerExceptionResolver가 호출되었다.
Exception이 WAS에 올라가거나 또는 sendError(상태코드,메시지)가 호출되거나하면
WAS에서 다시 /error 경로 Request하게된다
그러면 필터,인터셉터에서 중복처리안되게 설정해줘야하고,
그다음 BasicErrorController에서 처리하게 된다.
이런 길고 복잡한 과정보다는
HandlerExceptionResolver에서 예외를 던지지않게 먹어버리니까 WAS까지 정상응답이 올라가고
서블릿에서는 정상응답으로 왔으니까 HttpServletResponse의 내용을 가지고 http응답메시지를 만들어 응답해준다
(응답에 대한 값은 HandlerExceptionResolver에서 HttpServletResponse에다가 세팅해준다)
2. accept가 application/json가 아닌경우
UserHandlerExceptionResolver에서 ModelAndView 객체에 논리 뷰이름을 담아 반환해줬기에
DisPatcher Servlet은 해당 뷰를 찾아 렌더링해준 결과이다.
정리
ExceptionResolver 를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver에서 예외를 처리해버린다.
따라서 예외가 발생해도 서블릿 컨테이너까지 예외가 전달되지 않고, 스프링 MVC에서 예외 처리는 끝이 난다.
결과적으로 WAS 입장에서는 정상 처리가 된 것이다.
이렇게 예외를 이곳에서 모두 처리할 수 있다는 것이 핵심이다.
서블릿 컨테이너까지 예외가 올라가면 복잡하고 지저분하게 추가 프로세스가 실행된다.
반면에 ExceptionResolver 를 사용하면 예외처리가 상당히 깔끔해진다.
그런데 직접 ExceptionResolver 를 구현하려고 하니 상당히 복잡하다.
지금부터 스프링이 제공하는 ExceptionResolver 들을 알아보자.
API 예외 처리 - 스프링이 제공하는 ExceptionResolver (1)
스프링 부트가 기본으로 제공하는 ExceptionResolver는 다음과 같다.
HandlerExceptionResolverComposite 에 다음 순서로 등록되어있다.
1. ExceptionHandlerExceptionResolver
2. ResponseStatusExceptionResolver
3. DefaultHandlerExceptionResolver (우선 순위가 가장 낮다.)
Dispatcher Servlet이 순서대로 ExceptionResolver를 찾아서 실행한다.
현재 ExceptionResolver가 null을 반환하면 다음 ExceptionResolver로 이동...을 반복
모든 ExceptionResolver를 호출했는데 null이면 WAS에 예외가 전달된다.
ExceptionHandlerExceptionResolver
@ExceptionHandler 을 처리한다. (@ExceptionHandler를 처리한다해서 ExceptionHandler + ExceptionResolver)
API 예외 처리는 대부분 이 기능으로 해결한다. 조금 뒤에 자세히 설명한다.
ResponseStatusExceptionResolver
HTTP 상태 코드를 지정해준다.
원하는 Exception에 @ResponseStatus를 붙여놓고 상태코드를 지정했을때
해당 Exception가 발생하면 ResponseStatusExceptionResolver가
@ResponseStatus에 설정한 상태코드를 보고 해당 상태코드로 처리해준다.
(알아서 response.sendError(상태코드,메시지) 메소드를 이용해서 처리해준다. )
예) @ResponseStatus(value = HttpStatus.NOT_FOUND)
DefaultHandlerExceptionResolver
스프링 내부 기본 예외를 처리한다.
스프링 부트가 기본으로 제공하는 ExceptionResolver를 자세히 알아보자.
ResponseStatusExceptionResolver
ResponseStatusExceptionResolver 는 예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 한다.
다음 두 가지 경우를 처리한다.
1. @ResponseStatus 가 달려있는 예외
2. ResponseStatusException 예외
ResponseStatusExceptionResolver 테스트용 Exception
@ResponseStatus(code = HttpStatus.BAD_REQUEST,reason = "잘못된 요청오류") // @ResponseStatus를 이용해서 해당 예외가 발생했을때 상태코드를 지정할 수 있다.
// reason속성을 이용해서 Exception의 메시지또한 정할 수 있다.
public class BadRequestException extends RuntimeException {
}
BadRequestException 예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver가
@ResponseStatus 애노테이션을 확인해서 오류 코드를 HttpStatus.BAD_REQUEST (400)으로 변경하고,
메시지도 담는다.
@ResponseStatus는 클래스레벨에도 붙일수있고, 메소드레벨에도 붙일 수 있다.
(참고는 아래링크)
https://keeeeeepgoing.tistory.com/181
reason 을 MessageSource(메시지파일) 에서 찾는 기능도 제공한다. reason = "error.bad"
이전에 messages.properties에 메시지들을 적어놓고 사용했던 그것을 이용
messages.properties
error.bad = 잘못된 요청입니다.
@ResponseStatus(code = HttpStatus.BAD_REQUEST,reason = "error.bad")
public class BadRequestException extends RuntimeException {
}
https://keeeeeepgoing.tistory.com/203
테스트용 HandlerMethod
@GetMapping("/api/response-status-ex1")
public String responseStatusEx1() {
throw new BadRequestException();
}
실행결과
BadRequestException가 발생하면 ExceptionHandlerExceptionResolver가 호출되었다가 null을 반환하고,
ResponseStatusExceptionResolver가 호출된다.
(예외가 발생하면 순차적으로 ExceptionResolver가 호출된다.)
ResponseStatusExceptionResolver의 내부 코드를 보면
@Override
@Nullable
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
if (ex instanceof ResponseStatusException) {
return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
}
ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (status != null) {
return resolveResponseStatus(status, request, response, handler, ex);
}
if (ex.getCause() instanceof Exception) {
return doResolveException(request, response, handler, (Exception) ex.getCause());
}
}
catch (Exception resolveEx) {
if (logger.isWarnEnabled()) {
logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", resolveEx);
}
}
return null;
}
doResolveException()이라는 예외를 처리하는 메소드가 있다.
거기 내부코드를 보면 @ResponseStatus 어노테이션이 있는지 체크하고 있으면
resolveResponseStatus()라는 메소드를 호출한다.
ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (status != null) {
return resolveResponseStatus(status, request, response, handler, ex);
}
response.sendError()를 통해 상태코드만있으면 상태코드를, Exception메시지도 있으면 같이 전달하는 코드로 구성되어있다.
이렇게하면 WAS에서 예외는 처리되서 WAS에서 예외가 있었음을 인지하지못하지만,
response.sendError()가 호출됬기에 그 내용(에러정보)를 기본메시지에러(객체)의 경로 /error를 request하면서 전달해주고
BasicErrorController에서 에러정보와 Request의 Accept헤더를 보고 응답을 처리해준다.
ResponseStatusExceptionResolver 코드를 확인해보면 결국 response.sendError(statusCode, resolvedReason) 를
호출하는 것을 확인할 수 있다. sendError(400) 를 호출했기 때문에 WAS에서 다시 오류 페이지( /error )를 내부 요청한다.
ResponseStatusException
@ResponseStatus는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다.
(애노테이션을 직접 넣어야 하는데, 내가 코드를 수정할 수 없는 라이브러리의 예외 코드 같은 곳에는 적용할 수 없다.)
추가로 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하는 것도 어렵다.
이때는 ResponseStatusException 예외를 사용하면 된다
테스트용 핸들러메소드
@GetMapping("/api/response-status-ex2")
public void responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}
첫번째 파라미터는 상태코드, 두번째는 예외 메세지, 세번째는 Exception이 발생한 진짜 Exception을 넣는다.
(Exception이 발생한 진짜 Exception는 선택사항)
코드를 작성하다가 예를들어 검증을 하고 검증에 실패하면 예외를 발생시켜야할때
그 예외가 외부 라이브러리의 예외라면 상태코드라던지, 메시지를 수정해줄수 없으니까 ResponseStatusException를 생성하고 그 안에 상태코드,에러메시지를 적고 선택사항으로 원래 발생해야하는 예외를 넣어주면된다.
ResponseStatusException는
1.
예외를 던져야할때 외부라이브러리의 예외는 @ResponseStatus를 넣어서 원하는 상태코드,메시지로 수정하기 어려우니까 대신 ResponseStatusException를 만들어 상태코드와 메시지를 넣어서 예외를 발생시켜주는역할도 할 수 있고
2.
매번 파일을 생성해서 새롭게 예외를 만드는 과정을 줄일 수 있다.
(ResponseStatusException를 만들어서 그냥 상태코드랑 메시지넣어서 처리해버린다면)
https://mangkyu.tistory.com/204
결과확인
API 예외 처리 - 스프링이 제공하는 ExceptionResolver (2)
이번에는 DefaultHandlerExceptionResolver 를 살펴보자.
DefaultHandlerExceptionResolver 는 스프링 내부에서 발생하는 스프링 예외를 해결한다
대표적으로 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException이 발생하는데,
이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가고, 결과적으로 500 오류가 발생한다.
그런데 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이다.
HTTP에서는 이런 경우 HTTP 상태 코드 400을 사용하도록 되어 있다.
DefaultHandlerExceptionResolver는 이것을 500 오류가 아니라 HTTP 상태 코드 400 오류로 변경한다.
스프링 내부 오류를 어떻게 처리할지 수 많은 내용이 정의되어 있다.
( Http URL을 request할때 없는 URL을 request하면 404 상태코드가 나오는것도 DefaultHandlerExceptionResolver가 바꿔준것이다)
테스트용 핸들러 메소드
@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
return "ok";
}
TypeMismatchException를 발생하기 위한 메소드이다.
결과
Exception이 발생하였는데 상태코드가 500이 아니라 400이다.
TypeMismatchException은 DefaultHandlerExceptionResolver가 처리하는 예외이므로 그렇다.
DefaultHandlerExceptionResolver 내부의 handleTypeMismatch()메소드를 보면 다음과 같다.
response.sendError(HttpServletResponse.SC_BAD_REQUEST)
결국 response.sendError() 를 통해서 문제를 해결한다.
sendError() 를 호출했기 때문에 WAS에서 다시 오류 페이지( /error )를 내부 요청한다.
정리
지금까지 HTTP 상태 코드를 변경하고, 스프링 내부 예외의 상태코드를 변경하는 기능도 알아보았다.
(ResponseStatusExceptionResolver가 http 상태코드를 바꿔주었고
(DefaultHandlerExceptionResolver가 스프링내부의 예외의 상태코드도 바꿔주었다.)
그런데 HandlerExceptionResolver를 직접 사용하기는 복잡하다.
API 오류 응답의 경우 response에 직접 데이터를 넣어야 해서 매우 불편하고 번거롭다.
(우리가 위에서 UserHandlerExceptionResolver를 만들때 HttpServletResponse를 이용해서 http응답메시지를 세팅하고, 객체를 Objectmapper로 JSON으로 변환해서 다시 String으로 바꿔서 HttpServletResponse에 넣고 이런과정을 겪었다.)
ModelAndView를 반환해야 하는 것도 API에는 잘 맞지 않는다.
( implements HandlerExceptionResolver를 하면 resolveException()를 오버라이드해야하는데 반환이 ModelAndView였다. UserHandlerExceptionResolver 코드를 참고)
스프링은 이 문제를 해결하기 위해 @ExceptionHandler 라는 매우 혁신적인 예외 처리 기능을 제공한다.
그것이 아직 소개하지 않은 ExceptionHandlerExceptionResolver 이다.
ExceptionHandlerExceptionResolver 는 다음글로
'인프런 > 스프링 MVC 2편' 카테고리의 다른 글
19) 스프링 타입 컨버터 (0) | 2023.02.12 |
---|---|
18) API 예외처리 (3) (0) | 2023.02.12 |
16) API 예외 처리 (0) | 2023.02.09 |
15) 예외 처리와 오류 페이지 (0) | 2023.02.09 |
14) 로그인 처리2 - 인터셉터 , ArgumentResolver 활용 (0) | 2023.02.08 |
댓글