화면과 달리 오류 응답 스펙을 정의해야하고, JSON으로 데이터를 처리해야함.
1XX: Informational(정보 제공)
임시 응답으로 현재 클라이언트의 요청까지는 완료, 계속 진행하라는 의미
2XX: Success(성공)
클라이언트의 요청이 서버에서 성공적으로 처리되었다는 의미
3XX: Redirection(리다이렉션)
완전한 처리를 위해서 추가 동작이 필요한 경우, 주로 서버의 주소 또는 요청한 URI의 웹 문서가 이동되었으니 그 주소로 다시 시도하라는 의미
4XX: Client Error(클라이언트 에러)
없는 페이지를 요청하는 등 클라이언트의 요청 메시지 내용이 잘못된 경우를 의미
5XX: Server Error(서버 에러)
서버 사정으로 메시지 처리에 문제가 발생한 경우로, 서버의 부하, DB 처리 과정 오류, 서버에서 익셉션이 발생하는 경우를 의미
@GetMapping(("/api/members/{id}")) public MemberDTO getMember(@PathVariable("id") String id) { if (id.equals("RuntimeExcecption")) { throw new RuntimeException(); } return new MemberDTO(id, "welcome to " + id); }
성공시 다음과 같은 정상적인 JSON형태의 데이터가 처리됨.
또한 실패시 기존의WebServerCustomizer나, 기본 Spring의 오류 제공 기능 덕분에templates/error/경로에 오류 페이지 HTML이 있다면, 해당 오류 화면을 제공해준다.
하지만, API에서 우리가 클라이언트에 처리해줘야 할 데이터는 JSON 형태의 값이므로 이것을 공부한다.API 처리 컨트롤러
@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)); }코드를 보면
Request= /error-page/500, produces가 JSON으로 되어 있다. 즉 클라이언트에서 요청하는 Accept의 값이 application/json이면 해당 메서드가 호출되는 것으로 API 통신일 경우 처리 되는 컨트롤러다.위의 작성된 코드를 보면 RuntimeException이라는 아이디 값이 입력된 경우 예외를 던진다. 해당 RuntimeException이 발생하면
ErrorPageerrorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");에러 페이지로 등록해둔 이 URL의 경로로 호출하게 되는데, 바로 이때, 에러 페이지 컨트롤러에서 작성한produces=MediaType.APPLICATION_JSON_VALUE가 있는 컨트롤러를 통해 해당 요청이 JSON 인걸 파악하고 이 컨트롤러가 호출된다. 그러면 클라이언트에게 HTML이 아닌 JSON 형태로 응답을 처리할 수 있게 된다.
@RequestMapping(produces=Media.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {} @RequestMapping public ResponseEntity<Map<String,Object>> error(HttpServletRequest request) {}
errorHtml():produces = MediaType.TEXT_HTML_VALUE: 클라이언트 요청의 Accept 해더 값이text/html인 경우에는errorHtml()을 호출해서 view를 제공한다.error(): 그외 경우에 호출되고ResponseEntity로 HTTP Body에 JSON 데이터를 반환한다.
BasicErrorController를 사용하면, 기본적으로 예외발생시 페이지를 처리해주는데
이 때의 경로인templates/error를 기본으로 받음.
BasicErrorController 자체에서 오류 HTML 페이지를 제공하는 경우는 매우 편리하지만, API 오류는 회원, 상품등의 관련된 API스펙을 정의하기도 까다롭기 때문에, @ExceptionHandler를 사용하는 것이 좋음. public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex);
}
handler : 핸들러(컨트롤러) 정보
Exception ex : 핸들러(컨트롤러)에서 발생한 발생한 예외
다음 코드를 보자
@RestController
@Slf4j
public class ExceptionController {
@GetMapping("/controller/{id}")
public MemberDTO getMember(@PathVariable("id") String id) {
if (id.equals("wrongUser")) {
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("Types")) {
throw new IllegalArgumentException("잘못 입력된 값 ");
}
if (id.equals("error")) {
throw new UserException("사용자 오류 ");
}
return new MemberDTO(id, "welcome to" + id);
}
해당 컨트롤러에서 오류를 설정해놨지만, 당연하게도 스프링 부트 기본 설정에 의해 /templates/error로 들어가 오류 페이지를 찾아서 제공했다. 하지만, 이 통신에서는 JSON으로 오류를 통신받기를 원하기 때문에 UserException 사용자 오류에 대한 핸들러를 작성해야한다.
@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);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(result);
return new ModelAndView();
} else {
//TEXT/HTML
return new ModelAndView("error/400");
}
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver>
resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
resolvers.add(new UserHandlerExceptionResolver());
}
결과를 보면 다음과 같이 JSON형태로 code와 message를 처리함.
ExceptionHandlerExceptionResolverResponseStatusExceptionResolverDefaultHandlerExceptionResolverResponseStatusExceptionResolver
@ResponseStatus@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류") public class BadRequestException extends RuntimeException { }이렇게 어노테이션을 사용하여 해당 코드의 Exception을 지정해두면됨.
@GetMapping("/api/response-status-ex1") public String responseStatusEx1() { throw new BadRequestException(); }@GetMapping을 통해 예외가 던져지면, 해당 ResponseStatusExceptionResolver에서 ResponseStatus어노테이션을 찾아 해당 예외를 처리함.
추가로messages.properties에 에러를 작성하고,reason = "error.bad"등의 코드를 작성하면, 메시지소스를 찾아 제공해주는 기능도 있다.
@ResponseStatusException@GetMapping("~~~") public String responseStatus2(){ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException()); }위의
ResponseStatusException의 HttpStatus와 메시지, 예외를 호출하여 조건에 따라 예외를 변경하고 처리해줄 수도 있음.
DefalutHandlerExceptionResolver
- 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서
TypeMismatchException이 발생. 결과적으로 500오류가 발생한다.- 오류가 발생하면, 클라이언트가 파라미터에 타입을 잘못 입력한 값이므로, 500오류가 발생하여, 서블릿 컨테이너까지 오류가 전달됨.
문제는 서블릿 컨테이너까지 오류가 전달되면 다시 추가적인 프로세스가 늘어나면서 처리에 있어서 소요가 크다. 따라서,DefalutHandlerExceptionResolver는 HTTP 상태 코드 400오류로 변경해준다.- 결국
response.sendError()를 통해서 문제를 해결한다.
sendError(400)를 호출했기 때문에 WAS에서 다시 오류 페이지(/error)를 내부 요청한다
@ExceptionHandler
- 우선, ExceptionHandler는 restful API에서 예외처리를 할 때 위의 코드들보다 훨씬 편리함.
- 컨트롤러 클래스에 대해 지정해서 관리도 가능.
- 동일한 예외 처리 로직을 여러 컨트롤러에서 재사용할 수 있으며,@ControllerAdvice와 함께 사용하여, 애플리케이션 전역에서 공통된 예외 처리 방식을 정의할 수 있어 일관성을 유지
- 오류를 관리할 때도 개별적으로 상태코드, 응답 메시지, 헤더등을 자유롭게 설정할 수 있음.
코드
예외 발생시 API 응답 객체를 따로 만들어줬을 경우
@Data @AllArgsConstructor public class ErrorResult { private String code; private String message; }@ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(IllegalArgumentException.class) public ErrorResult illegalExHandle(IllegalArgumentException e) { log.error("[exceptionHandle] ex", e); return new ErrorResult("BAD", e.getMessage()); }
- 기본적으로,JSON형태로 반환해주기 때문에 편리하다.
- 서블릿에 예외가 도달하지 않기 때문에, 예외를 컨트롤러에서 처리하고 정상흐름으로 200이 나오기 때문에, @ResponseStatus를 통해 예외 상태 코드를 바꿔준다. (어찌됐건, 오류기 때문)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler public ErrorResult exHandle(Exception e) { log.error("[exceptionHandle] ex", e); return new ErrorResult("EX", "내부 오류"); }
- 위의 코드는 다른 예외 코드를 작성했는데도 불구하고, 예외를 처리하지 못했을 경우 최상위인 Exception에서 예외를 다 잡아버려서 처리해준다.
public ErrorResult exHandle(Exception e)
@ExceptionHandler(Exception.class)로 지정해줘도 OK
ControllerAdvice
@ControllerAdvice는 대상으로 지정한 여러 컨트롤러에@ExceptionHandler,@InitBinder기능을 부여해주는 역할을 한다.@ControllerAdvice에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다. (글로벌 적용)@RestControllerAdvice는@ControllerAdvice와 같고,@ResponseBody가 추가되어 있다.@Controller,@RestController의 차이와 같다.코드
@Slf4j @RestControllerAdvice(basePackages = "hello.exception.api") public class ExControllerAdvice { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(IllegalArgumentException.class) public ErrorResult illegalExHandler(IllegalArgumentException e) { log.error("[exceptionHandler] ex", e); return new ErrorResult("BAD", e.getMessage()); } @ExceptionHandler public ResponseEntity<ErrorResult> userExHandler(UserException e) { log.error("[exceptionHandler] ex", e); ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage()); return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST); }지정 방법
// 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 {}