API 예외처리 - HandlerExceptionResolver
웹 개발에서 오류는 사용자 요청에 의한 페이지오류가 있고, API로 요청을 왔을때 발생하는 API오류가 있습니다.
사용자 요청에 의한 오류페이지 제공은 스프링에서 제공되는 BasicErrorController를 사용하면 따로 로직을 개발하지 않고도 에러페이지를 만들수 있는데 API요청에 의한 오류는 많은 경우에서 발생이 가능하기 때문에 세밀한 관리가 필요합니다.
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값");
}
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello " + id);
}
}
해당 REST API에 ACCEPT를 application/json으로 요청을 한다면
BasicErrorController에서 기본적으로 json요청에 맞는 응답 데이터를 반환해주고,
ACCEPT를 text/html로 요청을 하게 되면 /resource/templates/error 하위에 에러코드에 맞는 html 파일을 반환해 줄 것입니다.
API 요청이라고 한다면 대부분 JSON을 이용하여 데이터를 주고 받게 됩니다. 그렇다면 누군가 api요청을 했을때 위와같은 에러가 노출이 된다면 좋은 api라고 할 수 없을것 입니다. api오류를 관리하기 위해서 스프링에서는 HandlerExceptionResolver를 사용하여 오류 다룰수 있습니다.
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if(ex instanceof IllegalArgumentException) {
log.info("IllegalAccessException resolver to 400");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
} catch (IOException e) {
log.error("resolver ex,",e);
}
return null;
}
}
HandlerExceptionResolver를 상속받은 클래스에서 resolveException을 오버라이드한 뒤 예외 종류를 비교하여 sendError에 담은 뒤 modelAndView로 보내는 코드입니다.
이제 해당 예외코드를 등록하시면 됩니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
}
등록까지 한 뒤 다시 실행을 해보면
분명 500에러코드가 나와야 하는 상황이지만 404에러코드로 바뀐 모습입니다.
그림으로 보시게 되면 요청이 컨트롤러에서 예외가 발생하면 등록했던 ExceptionResolver에서 예외를 받아 변환을 한 뒤 WAS로 정상 응답을 할수도 HTML로 응답할수도 있게 됩니다. ExceptionResolver는 예외코드 변환, 뷰 템플릿 처리, API응답 처리도 가능합니다.
- 예외 상태 코드 변환
- response.sendError(xxx)호출로 변경한 뒤 서블릿에서 상태 코드에 따른 오류 처리 가능 - 뷰 템플릿 처리
- ModelAndView에 값을 넣어 새로운 오류 화면 뷰 렌더링 가능 - API 응답 처리
- response.getWriter().println("hello"); 와 같이 HTTP응답 바디에 직접 데이터를 넣기 가능.
HandlerExceptionResolver 활용
방금까지의 예시는 예외가 발생하면 WAS에까지 간 뒤 WAS에서 다시 오류에 대해 처리하는 복잡한 과정이였습니다. 그런 복잡한 과정을 없이 한번에 예외를 처리하는 예제를 만들어보겟습니다.
먼저 RuntimeException을 상속받은 새로운 UserException 클래스를 만들어줍니다.
public class UserException extends RuntimeException{
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);
}
}
그런 다음 해당 예외를 처리할 resolveException을 만들어줍니다.
@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("user Exception");
String accept = request.getHeader("accept");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if("application/json".equals(accept)){
Map<String,Object> errorResult = new HashMap<>();
errorResult.put("ex",ex.getClass());
errorResult.put("message",ex.getMessage());
String s = objectMapper.writeValueAsString(errorResult);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(s);
return new ModelAndView();
}else{
return new ModelAndView("/error/500");
}
}
}catch (IOException e){
log.error("resolver e");
}
return null;
}
}
예외가 발생하면 예외의 타입을 새로만든 UserException과 비교하여 accpect가 application/json인지 확인한 뒤 예외클래스와 메세지를 Map에 담고 ObejctMapper객체에 담습니다. 그런 다음 응답(Response) 설정을 한 다음 response.getWrite().write()에 담은 뒤 반환을 해줍니다. ELSE로 그렇지 않은 경우는 모두 /resource/templates/error/500.html 파일을 반환해 줍니다.
이제 예외처리 코드를 모두 완성 했다면 등록을 해줍니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
resolvers.add(new UserHandlerExceptionResolver());
}
}
위에서 만든 Resvoler아래에 넣어준 코드입니다.
결과 모습을 보면 정상 작동한 모습을 확인할 수 있습니다.
정리
ExceptionResolver를 사용하면 컨트롤러에서 예외가 발생해도 서블릿 컨트롤러까지 예외를 전달하지 않아도 스프링MVC에서 처리가 가능합니다. 그런데 직접 구현을 하기에는 상당히 복잡한 코드가 될 수 있기 때문에 스프링에서 제공하는 ExceptionResolver를 사용하는 방법을 이어서 작성하겠습니다.