스프링/4. 스프링 MVC-2

67. API 예외처리

sdafdq 2023. 9. 12. 21:02

html 같은 경우는 그냥 4xx, 5xx등의 오류 페이지만 있으면 거의 해결 가능하다.

 

근데 API는,

각 오류 상황에 맞는 오류 응답 스펙을 클라이언트 사이드와 서버 사이드에서 약속을 하고 데이터를 내려주어야 한다.

 

먼저, 그냥 가상으로 /api/members/ex 라는 uri로 호출되면 에러를 뿜게 해줬고, 그 외에는 그냥 오브젝트를 만들어서 return 해 줬다.

@RestController라 Json객체로 변환되어 들어간다.

@RestController
@Slf4j
public class ApiExceptionController {

    @GetMapping("/api/members/{id}")
    public MemberDto getMember(@PathVariable("id") String id){
        if(id.equals("ex")){
            throw new RuntimeException("잘못된 사용자");
        }

        return new MemberDto(id, "hello " + id);
    }

    @Data
    @AllArgsConstructor
    static class MemberDto{
        private String memberId;
        private String name;
    }
}

원래 컨셉대로라면 멤버조회라서 repository넣고 거기에서 {id}를 조회해서 찾아와야 하는데, 그냥 예시만 보여주기 위해 이렇게 한 거다.

 

 

 

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
        ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
        ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");

        factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
    }
}

우리가 스프링 구성에 등록해놨던 ErrorPage다. 에러를 뿌리면 /error-page/500 컨트롤러를 호출하게 되어 있다.

 

 

 

@Slf4j
@Controller
public class ErrorPageController {

    public static final String ERROR_EXCEPTION = "jakarta.servlet.error.exception";
    public static final String ERROR_EXCEPTION_TYPE = "jakarta.servlet.error.exception_type";
    public static final String ERROR_MESSAGE = "jakarta.servlet.error.message";
    public static final String ERROR_REQUEST_URI = "jakarta.servlet.error.request_uri";
    public static final String ERROR_SERVLET_NAME = "jakarta.servlet.error.servlet_name";
    public static final String ERROR_STATUS_CODE = "jakarta.servlet.error.status_code";

    @RequestMapping("/error-page/500")
    public String errorPage500(HttpServletRequest request, HttpServletResponse response){
        log.info("errorPage 500");
        printErrorInfo(request);        
        return "error-page/500";
    }

    @RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
    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));
    }

}

/error-page/500로 uri가 똑같다. 그러나, produces = MediaType.APPLICATION_JSON_VALUE로 얘는 json 전용이야. 라고 이야기 해 줘서 괜찮다. 구분되서 오류 안뜬다.

Accept가 */*면 그냥 위에꺼가 호출 되지만,

Accept가 application/json이면 아래 것이 우선순위를 가진다.

 

 

내용은 Key Value로 이루어진 컬렉션 HashMap을 만들어서,

거기에 상태코드, 에러를 뿜을 때 넣어놨던 메시지를 꺼내서 넣어놨다.

 

그리고, RequestDispatcher.ERROR_STATUS_CODE는 

우리가 위에 ERROR_STATUS_CODE 해서 정의해놨는데, 사실 RequestDispatcher 안에 다 있다.

에러에 관한 상수 뿐 아니라 여러 Request에 기본으로 들어가는 것들이 key가 다 상수로 정의되어 있다.

 

여튼 

return new ResponseEntity<>(객체, 상태코드)

넣어서, ResponseEntity니 HttpMessageConverter에 의해 객체가 Json 형태로 변환되고, 

또 거기에 상태코드까지 같이 넣어 반환해야 한다.

 

이렇게 응답 온다.