본문 바로가기
Spring/MVC

API 예외 처리

by o3oppp 2024. 7. 21.
정상 응답과 오류 응답
@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);
    }
    
    @Data
    @AllArgsConstructor
    static class MemberDto{
        private String memberId;
        private String name;
    }
}

정상 호출
예외 발생 호출

  • 정상 호출 시 JSON 형식으로 데이터가 정상 반환
  • 그러나 예외 발생 호출 시 JSON 형식이 아닌 오류 페이지 HTML이 반환

오류 응답 변환
@Slf4j
@Controller
public class ErrorPageController {
	...
 
    @RequestMapping("/error-page/500")
    public String errorPage500(HttpServletRequest request, HttpServletResponse response){
        log.info("errorPage 500");
        return "error-page/500";
    }
	
    // 추가
    @RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE) // 해당 타입인 경우 url이 같으면 해당 메소드가 우선순위
    public ResponseEntity<Map<String, Object>> errorPage500Api(HttpServletRequest request, HttpServletResponse response){

        log.info("API errorPage 500");

        Map<String, Object> result = new HashMap<>();
        Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
        result.put("status", request.getAttribute(ERROR_STATUS_CODE));
        result.put("message", ex.getMessage());

        Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
    }
}

예외 발생 호출

  • produces = MediaType.APPLICATION_JSON_VALUE 추가
  • HTTP Header의 Accept의 값이 application/json일 때 해당 Method 호출
  • HTTP Header의 Accept의 값이 application/json이 아닌 경우 기존 오류 응답인 HTML 반환
  • ResponseEntity를 사용하여 JSON 반환

HandlerExceptionResolver
public interface HandlerExceptionResolver {

	@Nullable
	ModelAndView resolveException(
    	    HttpServletRequest request,
            HttpServletResponse response,
            @Nullable Object handler,
            Exception ex);

}
  • 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하도록 Spring MVC에서 제공하는 인터페이스
  • 줄여서 ExceptionResolver
  • handler : 핸들러(컨트롤러) 정보
  • ex : 핸들러(컨트롤러)에서 발생한 예외

사용 예시

@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("IllegalArgumentException resolver to 400");
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                return new ModelAndView();
            }

        } catch (IOException e) {
            log.error("resolver ex");
        }

        return null;
    }
}
...
@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");
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);

                if("application/json".equals(acceptHeader)){
                    Map<String, Object> errorResult = new HashMap<>();
                    errorResult.put("ex", ex.getClass());
                    errorResult.put("message", ex.getMessage());

                    String result = objectMapper.writeValueAsString(errorResult); // Json to String

                    response.setContentType("application/json");
                    response.setCharacterEncoding("utf-8");
                    response.getWriter().write(result);
                    return new ModelAndView();

                } else{
                    // TEXT, HTML 등
                    return new ModelAndView("error/500");
                }
            }
        } catch(IOException e){
            log.error("resolver ex", e);
        }

        return null;
    }
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new MyHandlerExceptionResolver());
        resolvers.add(new UserHandlerExceptionResolver());
    }
}
  • IllegalArgumentException이 발생 시
    • HandlerExceptionResolver 적용 전 : HTTP 상태 코드 500 반환
    • HandlerExceptionResolver 적용 후 : response.sendError(400)을 호출해서 HTTP 상태 코드를 400으로 지정하고 빈 ModelAndView를 반환
  • ModelAndView를 반환하는 이유는 try - catch를 하듯이 Exception을 처리해서 정상 흐름처럼 변경
    • 빈 ModelAndView 반환 시 : View를 렌더링 하지 않고 정상 흐름으로 서블릿이 return
    • ModelAndView 반환 시 : View를 렌더링
    • null 반환 시 : 다음 ExceptionResolver를 찾아서 실행. 처리할 수 있는 ExceptionResolver가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던짐
  • 활용 예시
    • 예외를 response.sendError(xxx) 호출로 변경해서 서블릿에서 상태 코드에 따른 오류를 처리하도록 위임
    • ModelAndView에 값을 채워서 예외에 따른 새로운 오류 화면 View 렌더링 제공
    • HTTP 요청 헤더의 ACCEPT 값에 따라서 오류 지정 및 오류 페이지 지정 가능
    • response.getWrite().println(xxx) 처럼 HTTP 응답 Body에 직접 데이터를 넣어주어 API 응답 처리 가능

  • ExceptionResolver로 예외를 해결해도 postHandle()은 호출되지 않음

'Spring > MVC' 카테고리의 다른 글

파일 업로드  (0) 2024.08.05
스프링 API 예외 처리  (0) 2024.07.21
HttpEntity  (0) 2024.07.21
서블릿 예외 처리  (0) 2024.07.20
스프링 인터셉터  (0) 2024.07.11