예외처리... HTTP API 응답
요약 정리
- HTML / TEXT 형식의 예외 처리
- BasicErrorController() 사용
- API 형식의 예외처리
- ExceptionHandlerExceptionResovler 사용 (@ExceptionHandler)
사전 준비
@Component
public class MyCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
ErrorPage errorPageException = new ErrorPage(RuntimeException.class, "/error-page/500");
factory.addErrorPages(errorPage404, errorPageException);
}
}
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/exception/{id}")
public String apiExceptionTest(@PathVariable("id") String id){
if (id.equals("ex"){
throw new RuntimeException("잘못된 사용자"); // 예외 던짐 (RuntimeException)
}
return new MemberDto(id, "hello" + id);
}
}
@DATA
@AllArgsConstructors
public class MemberDto{
private String memberId;
private String name;
}
- Customizing을 통해서 Error 발생시 error-page 경로 설정
- RuntimeException 예외 던짐
예외 API 응답 처리
1. 직접 Error 제어 Controller 추가
- produces = MediaType.APPLICATION_JSON_VALUE 를 이용하여 api 요청을 Mapping 함
- ResponseEntity 를 사용하므로 , 메시지 컨버터를 이용하여 json 형식으로 반환됨
@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500(HttpServletRequest request, HttpServletResponse response) {
Map<String, Object> result = new HashMap<>();
Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
// result에 api 응답을 담음
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));
}
2. Spring boot 기본 제공 Error 제어 (BasicErrorController)
- 기본값으로, /templates/error/ 하위에 에러페이지를 생성하면 Mapping 시켜준다.
- application.properties 에 "server.error.path"부분을 추가해주면 경로를 변경할 수 있다
- errorHtml()의 경우 accept헤더 값이 "text/html"인 경우에 호출된다. (view 페이지 제공)
- error()의 경우 accept헤더 값이 "application/json"인 경우에 호출된다 (API 제공)
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
//...
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}
//...
}
- application properties 설정을 통해 더 자세한 오류 결과값 출력 가능
[application.properties]
server.error.include-binding-errors=always
server.error.include-exception=true
server.error.include-message=always
server.error.include-stacktrace=always
3. Handler Exception Resolver
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex);
}
- resolveException()메서드를 이용해서, exception 발생시 원하는 응답코드와 메시지를 ModelAndView형태로 반환이 가능
- 반환 형태
- null : null을 반환할경우, 다음 resolveException을 찾고 없으면 servlet 밖으로 예외처리가 됨
- ModelAndView 빈 값 : view 를 반환하지않고, 서블릿에 정상흐름으로 반환 됨
- ModelAndView 값 : view를 렌더링한다.
public class MyHandlerException implements HandlerExceptionResolver {
@Override
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex){
try{
if (ex instanceOf IllegalArgumentException){
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); // 400번대 오류발생 (Illegal 예외시)
return new ModelAndView();
}
}catch (IoException e){
log.error("error");
}
return null; // 다음 resovleException
}
- HandlerExceptionResolver 등록 방법 (WebConfig)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
}
4. spring 제공 Exception Resolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver
- ExcetpionHandlerExceptionResolver -> 우선순위가 가장 높고, 자주 사용됨
ResponseStatusExceptionResolver
- 역할
- 처리방법
- @ResoposeStatus 예외
- 직접 Exception을 만들때에 사용이 가능
- 개발자가 직접 추가를 해줘야하는 부분이므로, 직접 구현이 어렵다면 ResponseStatusException을 활용
- 추가적으로, reason에 messages.properties를 이용하여 작성이 가능
- ResponseStatusException 예외
- ResponseStatusException클래스를 이용하여, 직접 추가하지 못한 @ResponseStatus 대신하여 상태코드 지정
/*
** @ResponseStatus 사용 **
*/
//messages.properties
error.bad="잘못된 요청 오류"
// BadRequestException.class
//@ResponseStatus( code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
@ResponseStatus( code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException{
}
// TestController.class
@Controller
public class TestController{
@GetMapping("/api/responseStatus")
public String testException(){
throw new BadRequestException;
}
}
/*
** ResponseStatusException 사용 **
*/
//messages.properties
error.bad="잘못된 요청 오류"
//ResponseExceptionResolver
//ResponseResovlerController
@Controller
public class ResponseResolverController{
@GetMapping("/api/responseResolver")
public String testException(){
throw new ResponseStatusException(HttpStatus.NOT_FOUND,"error.bad", new IllegalArgumentException());
}
}
DefaultHandlerExceptionResolver
@Controller
public class DefaultController{
@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
//data -> typeMismatch ( data ==> String )
return "ok";
}
}
- 스프링 내부적으로 자동으로 발생시켜주는 error Resolver
- 주로, TypeMismatch 로 인해 발생한다
[응답결과]
{ "status": 400, "error": "Bad Request", "exception": "org.springframework.web.method.annotation.MethodArgumentTypeMismatchException", "message": "Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: \"hello\"", "path": "/api/default-handler-ex" }
ExceptionHandlerExceptionResolver
- HttpServletResponse에 .sendError() 적용할 필요 없음
- ExceptionResolver를 이용하여 ModelAndView를 반환했어야 했지만, 그럴 필요가 없어짐
- 컨트롤러마다 다르게 예외 처리 방식 적용이 가능 ( 기존에는 RuntimeException을 예외처리 해놓으면 어떠한 컨트롤러이든지 같은예외 처리가 이루어짐)
@RestControllerAdvice
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
return new ErrorResult("EX", "내부 오류");
}
}
- @RestControllerAdvice를 이용하여, 기존의 Controller와 분리하여 관리가 가능해짐
- @RestControllerAdvice 또는 @ControllerAdvice 사용 가능
- @RestControllerAdvice는 @ResponseBody가 더 추가 된 것임
- @ResponseStatus를 이용하여 HttpStatus 상태코드 지정 가능
- @ExceptionHandler를 사용하여, 예외 처리 지정이 가능
- 예외처리 지정을 하지 않았을 경우, 파라미터를 기준으로 적용됨
- @ExceptionHandler의 경우 여러 Exception들을 적용할 수 있음
- ( ex @ExceptionHandler(@ExceptionHandler.class, @BadRequestException.class) )
- ResponseEntity를 사용하여 errorResult 및 Http 상태코드를 지정 가능
대상 컨트롤러 지정 방법
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}