본문 바로가기

Springあるある

API 오류 처리 시의 BasicErrorController의 한계점(Feat. @ExceptionHandler)

스프링 부트는 기본적으로 예외/sendError() 발생 시, 개발자가 에러 처리 컨트롤러를 등록하지 않게 되면

자동적으로 WAS에서 "/error"(수정 가능)을 재요청하여 BasicExceptionController에서 처리하게 만들어 져 있다. 

BasicErrorController 컨트롤러는 기본적으로 아래와 같은 메서드를 제공한다. 

BasicErrorController 일부

위 코드를 분석해 보면 응답 에러 발생시 

1] HTML 오류 페이지 

2] JSON 방식

위 2가지 방식으로 오류 상태를 클라이언트에게 보내 준다. 

아래의 코드는 ResponseEntity<>를 이용하여 API 응답 오류를 JSON으로 반환하는 예시이다. 



@Slf4j
@Controller
public class ErrorPageController {

    // RequestDispatcher 클래스에 final static으로 정의돼 있음

    public final static String FORWARD_REQUEST_URI = "jakarta.servlet.forward.request_uri";

    public final static String FORWARD_CONTEXT_PATH = "jakarta.servlet.forward.context_path";

    public final static String FORWARD_MAPPING = "jakarta.servlet.forward.mapping";

    public final static String FORWARD_PATH_INFO = "jakarta.servlet.forward.path_info";

    public final static String FORWARD_SERVLET_PATH = "jakarta.servlet.forward.servlet_path";

    public final static String FORWARD_QUERY_STRING = "jakarta.servlet.forward.query_string";

    public final static String INCLUDE_REQUEST_URI = "jakarta.servlet.include.request_uri";

    public final static String INCLUDE_CONTEXT_PATH = "jakarta.servlet.include.context_path";

    public final static String INCLUDE_PATH_INFO = "jakarta.servlet.include.path_info";

    public final static String INCLUDE_MAPPING = "jakarta.servlet.include.mapping";

    public final static String INCLUDE_SERVLET_PATH = "jakarta.servlet.include.servlet_path";

    public final static String INCLUDE_QUERY_STRING = "jakarta.servlet.include.query_string";

    public final static String ERROR_EXCEPTION = "jakarta.servlet.error.exception";

    public final static String ERROR_EXCEPTION_TYPE = "jakarta.servlet.error.exception_type";

    public final static String ERROR_MESSAGE = "jakarta.servlet.error.message";

    public final static String ERROR_REQUEST_URI = "jakarta.servlet.error.request_uri";

    public final static String ERROR_SERVLET_NAME = "jakarta.servlet.error.servlet_name";

    public final static String ERROR_STATUS_CODE = "jakarta.servlet.error.status_code";

	
    // 1] HTML 오류 페이지
    @RequestMapping("/error-page/500")
    public String errorPage500(HttpServletRequest request, HttpServletResponse response){

        log.info("errorPage 500");
        printErrorInfo(request);
        return "error-page/500";

    }
	
    
    // 2] API 응답 오류 
    @RequestMapping(value="/error-page/500",produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Map<String,Object>> errorPage500Api(HttpServletRequest request,HttpServletResponse response){


        log.info("API Eroor Page : 500");

        HashMap<String, Object> result = new HashMap<>();

        Exception ex = (Exception)request.getAttribute(ERROR_EXCEPTION);
        result.put("message",ex.getMessage());
        result.put("status",request.getAttribute(ERROR_STATUS_CODE));

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

    }


    private void printErrorInfo(HttpServletRequest request){

        log.info("FORWARD_REQUEST_URI  : {}",request.getAttribute(FORWARD_REQUEST_URI ));
        log.info("FORWARD_CONTEXT_PATH : {}",request.getAttribute(FORWARD_CONTEXT_PATH));
        log.info("FORWARD_MAPPING :{}",request.getAttribute(FORWARD_MAPPING));
        log.info("FORWARD_PATH_INFO : {}",request.getAttribute(FORWARD_PATH_INFO));
        log.info("FORWARD_SERVLET_PATH : {}",request.getAttribute(FORWARD_SERVLET_PATH));
        log.info("FORWARD_QUERY_STRING : {}",request.getAttribute(FORWARD_QUERY_STRING));
        log.info("INCLUDE_REQUEST_URI  : {}",request.getAttribute(INCLUDE_REQUEST_URI ));
        log.info("INCLUDE_CONTEXT_PATH : {}",request.getAttribute(INCLUDE_CONTEXT_PATH));
        log.info("INCLUDE_PATH_INFO  : {}",request.getAttribute(INCLUDE_PATH_INFO ));
        log.info("INCLUDE_MAPPING  : {}",request.getAttribute(INCLUDE_MAPPING ));
        log.info("INCLUDE_SERVLET_PATH : {}",request.getAttribute(INCLUDE_SERVLET_PATH));
        log.info("INCLUDE_QUERY_STRING : {}",request.getAttribute(INCLUDE_QUERY_STRING));
        log.info("ERROR_EXCEPTION: {}",request.getAttribute(ERROR_EXCEPTION ));
        log.info("ERROR_EXCEPTION_TYPE : {}",request.getAttribute(ERROR_EXCEPTION_TYPE));
        log.info("ERROR_MESSAGE  : {}",request.getAttribute(ERROR_MESSAGE ));
        log.info("ERROR_REQUEST_URI : {}",request.getAttribute(ERROR_REQUEST_URI));


        log.info("Dispatcher Type : {}",request.getDispatcherType());


    }






}

 

PostMan을 이용하여 위 url로 API 요청(Accept : application/json) 시, 컨트롤러는 ResponseEntity<>를 사용하여 아래의

그림과 같이 API 응답 오류 메시지를 전달한다. 

 

BasicErrorController 또한 API 응답 메시지에 대한 지원을 위와 같이 지원을 하기는 한다. 

그러나 실무에서는 API 오류 응답에 대해서는 BasciErrorController를 사용하면 안 되고 @ExceptionHandler를 사용해야

한다.

스프링 부트가 제공하는  BasicErrorController 는 HTML 페이지를 제공하는 경우에는 매우 편리하다.

4xx, ` ` 5xx 등등 모두 잘 처리해준다.

그런데 API 오류 처리는 다른 차원의 이야기이다.

API 마다, 각각의 컨트롤러나 예외마 다 서로 다른 응답 결과를 출력해야 할 수도 있다.

예를 들어서 회원과 관련된 API에서 예외가 발생할 때 응답과, 상품과 관련된 API에서 발생하는 예외에 따라 그 결과가 달

라질 수 있다.(각 API에 대한 메서드가 API 응답 메시지를 만들 때 비지니스 상황에 맞게 서로 다른 형태의 API 응답 메세

지를 만들어야만 한다. 구체적인 예시로는 API 마다 상태 코드를 다르게 설정을 해야 하거나, API 각 사용자마다 API 응답

메세지 스펙을 다르게 요구할 경우 등. 또한 API 사용자가 매개변수 타입을 잘못 보내어 서버의 컨트롤러에서

IllegalException 예외가 발생하면 Tomcat은 상태 코드 500, 즉 서버 문제로 인식한다. 이런 경우 상태 코드 400으로 바꾸

고 주고 싶은 경우 등의 경우도 존재. 

그러나 BasicErrorController는 유연한 API 응답 메시지를 만들지 못한다. BasicErrorController의 

ResponseEntity<> error 메서드 중 getErrorAttributes()가 있는데 이 부분을 오버라이딩을 하여 API 응답 메세지를 개발자

입맛에 맞게 고칠 수가 있지만, 이렇게 하면 API 마다 해당 부분을 오버라이딩해야 하는 번거로움이 존재)

결과적으로 매우 세밀하고 복잡하다.

따라서 이 방법 은 HTML 화면을 처리할 때 사용하고, API 오류 처리는 뒤에서 설명할 ` @ExceptionHandler `를 사용해야 

한다.