HTML 페이지와 다르게, API의 경우는 각 오류 상황에 맞는 오류 응답 스펙을 정하고 JSON으로 데이터를 보내주어야 한다.
이 장에서 배운 Response Http Body에 JSON으로 오류 데이터를 반환하는 방법은 2가지가 있다. (나머지는 View, ModelView 반환)
1) BasicErrorController : ResponseEntity
- view를 기준으로 JSON으로 반환을 한다.
- ResponseEntity에 에러 메시지를 넣어주는 것이 불편하다.
2) HandlerExceptionResolver : response.getWriter().println("...")
- 반환 타입인 ModelAndView는 Api 응답에는 필요하지 않다.
- 동일한 (Runtime)Exception 예외를 서로 다른 방식으로 처리하기 어렵다.
- Response에 직접 응답 데이터를 넣어주는 것이 불편하다.
하지만 다음 장에 배울 @ExceptionHandler의 편의성이 막강해 이를 사용하면 좋다.
1. API 예외 처리 - 직접 (HashMap)
클라이언트가 정상∙오류 요청 상관없이 반환값이 JSON이기를 기대한다면 ResponseEntity<>()를 사용할 수 있다.
ResponseEntity<>()
스프링이 ResponseEntity의 body로 지정한 객체를 사용해 JSON으로 변환한다.
이 때 추가적으로 헤더 정보와 HTTP 상태 코드를 함께 전달할 수 있다.
// 클라이언트가 받고 싶은 타입, 즉 Accept 헤더가 JSON_VALUE 일 때 이 메소드가 호출된다.
@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));
}
2. Error View 제공 → 기본 오류 처리 (BasicErrorController)
[BasicErrorController]
- Spring MVC에서 제공하는 기본 에러 컨트롤러
- '/error' 경로를 처리하는 두 개의 메소드가 있다.
1. errorHtml() : 클라이언트 요청의 Accept 헤더 값이 text/html 인 경우 호출되어 view를 반환한다. (정의한 뷰 or 기본 뷰)
2. error() : 그 외에 호출되어 ResponseEntity로 Http Body에 JSON 데이터를 반환한다. → @ExceptionHandler 사용
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {}
}
3. Error JSON 제공 → API 예외 처리 (HandlerExceptionResolver)
→ 결국 ModelView() 반환 엔딩..
WAS까지 예외가 전달되면, HTTP 상태코드는 무조건 500으로 처리된다.
사용자 입력 오류와 같이 클라이언트 예외는 다른 상태코드로 처리할 수 있다.
또한 제공하는 API마다 오류 메시지, 형식 등을 다르게 처리할 수 있다.
1) HandlerExceptionResolver
컨트롤러에서 발생한 exception을 먹고,
응답에 ①sendError, ②response.getWriter().println() 를 포함하여 WAS에 정상 응답을 보낸다.
public interface HandlerExceptionResolver {
// handler : 핸들러(컨트롤러) 정보
// Exception ex : 핸들러(컨트롤러)에서 발생한 예외
@Nullable
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
2) ExceptionResolver
ExceptionResolver 적용 전
- WAS까지 예외가 전달이 되고, WAS에서 '/error' 경로로 내부 요청을 하여 에러 뷰를 띄운다.
ExceptionResolver 적용 후
- 컨트롤러에서 예외가 발생하면 ExceptionResolver가 해당 예외를 해결할 수 있으면(...) 해결을 한다.
해결을 하면서 ModelAndView를 반환을 하고, WAS에는 정상 응답(그러나 'response.sendError()' 포함)을 전달한다.
(WAS에서 respons에 포함된 error를 확인하면서 여전히 '/error' 내부 요청이 있다.)
(예외를 해결해도 postHandle()은 호출되지 않는다.)
- 'response.getWriter().println("...")' 으로 response body에 직접 데이터를 넣어줄 수 있다.
(response.sendError()가 없이, WAS에서 '/error' 내부 요청 없이 바로 JSON으로 오류를 반환할 수 있다.)
(그러나 다음 장에 배울 '@ExceptionHandler' 를 사용하는 것이 좋다.)
3) MyHandlerExceptionResolver
[return 값에 따른 동작 방식]
1. 빈 ModelAndView
: View를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.
2. ModelAndView 지정
: View를 렌더링 한다.
3. null
: 다음 ExceptionResolver를 찾아서 실행한다.
@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", e);
e.printStackTrace();
}
return null;
}
}
// ExceptionResolver() 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
}
4. Spring 기본 제공 → ExceptionResolver(1)
1) 우선순위
1. ExceptionHandlerExceptionResolver
: @ExceptionHandler 처리
2. ResponseStatusExceptionResolver
: HTTP 상태 코드 지정
3. DefaultHandlerExceptionResolver
: 스프링 내부 기본 예외 처리 (ex. TypeMismatch)
2) ResponseStatusExceptionResolver
1. @ResponseStatus 예외
: 컨트롤러 내에서 발생한 예외가 컨트롤러 밖으로 넘어가면, ResponseStatusExceptionResolver가 @ResponseStatus을 확인해서 오류 코드를 HttpStatus.BAD_REQUEST(400)으로 변경하고, 메시지를 담는다.
- sendError(statusCode, reason)으로 보낸다. → '/error' 내부 요청
- reason을 메시지화 가능하다.
- 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다.
(ex. 라이브러리 예외 코드)
GetMapping("/api/response-status-ex1")
public String responseStatusEx1() {
throw new BadRequestException();
}
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
2. ResponseStatusException 예외
- 조건에 따라 예외 동적으로 변경 가능
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}
3) DefaultHandlerExceptionResolver
// 파라미터를 아무것도 넣지 않았다.
// http://localhost:8080/api/default-handler-ex
@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer number) {
return "ok";
}
4. Spring 기본 제공 → ExceptionResolver(2)
*ExceptionHandlerExceptionResolver
: @ExceptionHandler 처리
@ExceptionHandler 어노테이션으로, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다.
그러면 해당 컨트롤러에서 예외가 발생하면 @ExceptionHandler 메서드가 호출된다.
(예외를 지정하지 않으면 메서드 파라미터 예외를 사용한다.)
1) 실행흐름
1. 컨트롤러에서 IllegalArgumentException 예외가 발생해, 컨트롤러 밖으로 던져진다.
2. 예외가 발생했으므로 ExceptionResolver가 작동한다.
(그 중 가장 우선순위가 높은 'ExceptionHandlerExceptionResolver' 실행)
3. IllegalArgumentException 를 처리할 수 있는 @ExceptionHandler이 있는지 확인한다.
4. ExControllerAdvice에서 illegalExeptionHandler()를 실행한다.
(@RestControllerAdvice이므로, @ResponseBody가 적용되어 HTTP 컨버터가 JSON으로 변환하여 반환된다.)
5. ResponseStatus() 상태 코드에 맞춰 응답한다.
2) @ControllerAdvice
특정 컨트롤러에서 발생할 수 있는 예외를 잡아 처리하는 기능을 한다.
(대상을 지정하지 않으면, 글로벌 적용한다.)
대상 컨트롤러 지정
// 공식문서 : https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann- controller-advice
// @RestController 어노테이션이 붙은 모든 컨트롤러
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// "org.example.controllers" 경로 하위에 있는 모든 컨트롤러
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// "ControllerInterface.class", "AbstractController.class" 구체적인 컨트롤러
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
[ExControllerAdvice]
@Slf4j
@RestControllerAdvice(assignableTypes = {ApiExceptionV2Controller.class})
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
// @ResponseStatus를 지정해주지 않으면 200 OK으로 나간다.
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExceptionHandler(IllegalArgumentException e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage()); // 그대로 정상 응답, JSON으로 반환된다.
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExceptionHandler(UserException e) {
log.error("[exceptionHandler] ex ", e);
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exceptionHandler(Exception e) {
log.error("[exceptionHandler] ex ", e);
return new ErrorResult("EX", "내부 오류");
}
}
'Spring > MVC 2' 카테고리의 다른 글
11. 파일 업로드 (0) | 2024.05.07 |
---|---|
10. 스프링 타입 컨버터 (0) | 2024.05.06 |
8. 예외 처리와 오류 페이지 (0) | 2024.05.05 |
7. 로그인 처리2 - 필터, 인터셉터 (0) | 2024.05.04 |
6. 로그인 처리1 - 쿠키, 세션 (1) | 2024.05.02 |