서블릿의 예외 처리와 오류 페이지
스프링이 아닌 순수 서블릿 컨테이너에서의 예외처리 방법은 2가지 방식이 있다.
1. Exception (예외)
2. response.sendError(HTTP 상태 코드, 오류 메세지)
Exception
예외는 특정 조건이나 오류가 발생했을 때 이를 처리하기 위해 사용된다.
만약에 에플리케이션에서 예외를 잡지 못하고 서블릿 밖으로 예외가 전달되면
컨트롤러 (예외 발생) -> 인터셉터 -> 서블릿 -> 필터 -> WAS 순서로 WAS까지 예외가 전달된다.
@GetMapping("/error-ex")
public void errorEx() {
throw new RuntimeException("예외 발생!");
}
response.sendError(HTTP 상태 코드, 오류 메세지)
주로 클라이언트에게 잘못된 요청이나 서버의 문제 등을 알릴 때 사용된다.
sendError를 호출하면 response 내부에서 오류 발생 상태를 저장해두고 서블릿 컨테이너는 고객에게 응답 하기 전 sendError 여부를 확인한 이후 설정한 오류 코드에 맞게 오류 페이지를 보여준다. 흐름은 Exception과 같다.
@GetMapping("/error-ex")
public void errorEx(HttpServletResponse response) throws IOException {
response.sendError(500,"500 에러 발생!!");
}
1. 서블릿 오류 페이지 등록
@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);
}
}
2. 오류 처리 컨트롤러 등록
public class ErrorPageController {
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 404");
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 500");
return "error-page/500";
}
}
서블릿 예외 페이지 작동 원리
예외가 발생하면 WAS에서는 해당 예외를 처리하는 오류 페이지 정보를 확인한다. 그러기위해서는 다시 컨트롤러를 호출해야한다.
컨트롤러 (예외 발생) -> 인터셉터 -> 서블릿 -> 필터 -> WAS -> "/error-ex" 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> view 반환
순으로 예외가 처리되면서 컨트롤러에서 다시 컨트롤러까지 가는 비효율적인 상황이 발생한다.
결국 클라이언트로부터 발생한 정상 요청인지, 오류 페이지를 출력하기 위한 내부 요청인지를 구분할 수 있어야 한다.
서블릿은 이러한 문제를 해결하기 위해 DispatcherType이라는 추가 정보를 제공한다.
DispatcherType
- REQUEST : 클라이언트 요청
- ERROR : 오류 요청
필터 중복 호출
기본적으로 DispatcherType.REQUEST가 적용되어 클라이언트 요청에만 필터가 적용되고, 서블릿에서 예외 처리가 발생할 때는 필터를 거치지 않는다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return filterRegistrationBean;
}
}
인터셉터 중복 호출
필터의 경우에는 DispatcherType에 따라 필터 적용 여부를 결정했지만 인터셉터에 경우 DispatcherType과 무관하게 항상 호출된다. 대신 인터셉터는 요청 경로에 따라 제외하기 쉽기 때문에 excludePathPatterns를 사용해서 제외하면 된다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns(
"/css/**", "/*.ico"
, "/error", "/error-page/**" //오류 페이지 경로
);
}
서블릿에서의 예외 처리와 오류 페이지 정리
정상 요청 시
WAS(/hello, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> View
오류 시
WAS(/hello, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
WAS(/error-page/500, dispatchType=ERROR) -> 필터(x) -> 서블릿 -> 인터셉터(x) -> 컨트 롤러(/error-page/500) -> View
스프링에서의 예외 처리와 오류 페이지
서블릿에서는 WebServerCustomizer를 만들고 예외 종류에 따라 ErrorPage를 추가해야 했다.
스프링 부트에서는 이런 과정을 기본적으로 제공해준다.
1. ErrorPage 자동 등록 ( /error 라는 경로로 기본 오류 페이지를 설정해준다. )
2. BasicErrorController라는 스프링 컨트롤러 자동 등록
즉 개발자는 /error 라는 경로 안에 오류 페이지만 만들면 된다. !
ex) 400 에러는 /error/400.html 파일을 만들면 알아서 400 오류 시 해당 페이지로 간다.
BasicController 가 제공하는 기본 정보
해당 컨트롤러는 에러 정보를 model에 담아 뷰에 전달한다.
* timestamp: Fri Feb 05 00:00:00 KST 2021
* status: 400
* error: Bad Request
* exception: org.springframework.validation.BindException
* trace: 예외 trace
* message: Validation failed for object='data'. Error count: 1
* errors: Errors(BindingResult)
* path: 클라이언트 요청 경로 (`/hello`)
스프링에서의 예외 처리와 오류 페이지 정리
스프링 부트가 기본적으로 제공하는 오류 페이지를 활용하면 아주 쉽게 예외 처리 및 오류 페이지 문제를 해결할 수 있다.
서블릿에서의 API 예외 처리
WebServerCustomizer를 등록한 이후 response.sendError()를 호출하면 등록한 예외 페이지 경로가 호출된다.
@GetMapping("/error-ex/{id}")
public MemberDto errorEx(@PathVariable("id") String id){
if(id.equlas("ex"){
throw new RuntimeException("잘못된 사용자");
}
return new MemberDto(id,"hello");
}
이때 API로 요청을 하였는데 오류가 발생하면 미리 만들어 놓은 오류 페이지 HTML이 반환된다. 이는 원하는 바가 아니다.
즉 클라이언트는 정상 요청이든, 아니든 무조건 JSON으로 반환해야 하는 상황에서 이런 문제를 해결하기 위해 오류 페이지 컨트롤러도 JSON 응답을 할 수 있도록 수정해야 한다.
@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) {
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));
}
WebServerCustomizer 로 인해 RuntimeException 시 "/error-page/500" 경로가 가게 되고 produces = MediaType.APPLICATION_JSON_VALUE (HTTP 헤더의 Accept 값이 application/json 일 때)로 인해 미디어 타입이 json인 경우 해당 메서드가 호출되게 된다.
스프링에서의 API 예외 처리
스프링 부트는 BasicErrorController가 제공하는 기본 정보들을 활용하여 API 예외 처리를 기본적으로 처리해준다.
즉 자동으로 HTML 요청 시 -> HTML 반환 , JSON 요청 시 -> json 반환을 해준다.
API 예외 처리
HandlerExceptionResolver
만약 500 상태코드 예외를 400 이나 404 상태코드로 처리하고 싶다면 어떻게 해야할까
스프링 MVC는 컨트롤러 밖으로 예외가 던져진 경우 예외를 해결하고 동작을 정의할 수 있는 방법을 제공해준다.
바로 HandlerExceptionResolver를 사용하면 된다. 줄여서 ExceptionResolver 이라고 한다.
ExceptionResolver 적용 전
ExceptionResolver 적용 후
예외를 처리해서 정상 처리해준다.
HandlerExceptionResolver - 인터페이스
public interface HandlerExceptionResolver {
ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
MyHanlderExceptionResolver 생성
IllegalArgument 예외 발생 시 400 오류 코드로 반환
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);
}
return null;
}
}
- 빈 ModelAndView 반환 이유 : try, catch 하듯이 Exception을 처리해서 정상 흐름으로 변경하기 위해
반환 값에 따른 동작 방식
- 빈 ModelAndView: new ModelAndView() 처럼 빈 ModelAndView 를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴됨
- ModelAndView 지정: ModelAndView 에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링
- null: null 을 반환하면, 다음 ExceptionResolver 를 찾아서 실행한다. 만약 처리할 수 있는 ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.
ExceptionResolver 활용
- 예외 상태 코드 변환 :예외를 response.sendError(xxx) 호출로 변경해서 서블릿에서 상태 코드에 따른 오류를 처리 하도록 위임 이후 WAS는 서블릿 오류 페이지를 찾아서 내부 호출한다.
- 뷰 템플릿 처리 : ModelAndView 에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링 해서 고객에게 제공
- API 응답 처리 : response.getWriter().println("hello"); 처럼 HTTP 응답 바디에 직접 데이터를 넣어 주는 것도 가능하다. 여기에 JSON 으로 응답하면 API 응답 처리를 할 수 있다.
WebMvcConfigurer에 등록
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
API - 예외 처리 - HandlerExceptionResolver 활용
예외가 발생하면 WAS 까지 예외를 던지고, WAS에서 오류 페이지를 다시 찾아 호출하는 과정은 매우 복잡하다 ExceptionResolver를 활용하면 예외가 발생하였을 때 이런 복잡한 과정없이 문제를 해결할 수 있다.
직접 ExceptionResolver 구현
@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;
}
}
WebMvcConfigurer에 등록
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
resolvers.add(new UserHandlerExceptionResolver());
}
API - 예외 처리 - ExceptionResolver
이렇게 직접 ExceptionResolver를 구현하면 매우 복잡하다. 스프링에서는 이러한 복잡한 문제를 모두 해결해 주는 ExceptionResolver을 제공해준다.
스프링에 제공하는 ExceptionResolver 우선 순위
- ExceptionHanlderExceptionResolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver
ResponseStatusExceptionResolver
예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 한다. 또한 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다.
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
@ResponseStatus 애노테이션을 적용하면 해당 오류의 HTTP 상태 코드를 변경해준다.
해당 클래스 이름의 예외가 발생하면 ResponseStatusExceptionResolver에서 해당 애노테이션을 확인하여 오류코드를 (BAD_REQUEST) 400으로 상태 코드가 변경해준다.
DefaultHandlerExceptionResolver
스프링 내부에서 발생하는 스프링 예외를 해결한다.
ExceptionHanlderExceptionResolver
API 오류 응답의 경우 response에 직접 데이터를 넣어야 해서 매우 불편하고 번거롭다. 또한 ModelAndView를 반환하는 과정 또한 효율적이지 못하다. 스프링은 이러한 문제를 해결하기 위해 @ExceptionHandler라는 혁신적인 예외 처리 기능을 제공한다.
ExceptionHandler
웹 브라우저에 HTML 화면을 제공할 때는 BasicController를 사용하여 4xx , 5xx 화면을 보여주면 됐다.
하지만 API 의 경우 예외에 따라 각각 다른 데이터를 출력해야 할 수도 있으며, 어떤 컨트롤러에서 발생하냐에 따라에서도 다른 예외처리가 필요할 때가 있다. 앞서 다룬 HandlerException을 직접 구현하는 방식이나 BasicController를 통해 API를 다루기엔 쉽지않다.
이러한 여러 API 예외 문제에서 발생하는 문제를 해결하기 위해 @ExceptionHandler 라는 애노테이션이 등장하였다.
예외 발생 시 응답 객체 정의
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
ApiExceptionController
@ExceptionHandler 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주기만 하면 된다.
지정한 예외 또는 그 예외의 자식 클래스까지 모두 잡아준다.
@RestController
public class ApiExceptionController {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandle] 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 exHandle(Exception e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("EX", "내부 오류");
}
@GetMapping("/api2/response-status-ex1")
public String responseStatusEx1() {
throw new BadRequestException();
}
...
- @ExceptionHandler에 예외를 생략할 수 있다. 생략 시 메서드 파라미터의 예외가 지정된다.
- ResponseStauts 를 통해 해당 메서드에서의 오류는 지정 오류코드로 응답한다.
- @RestController 이므 HTTP 컨버터가 사용되고 응답이 json으로 반환된다.
- html 화면 반환시 ModelAndView를 사용하면 된다.
API 예외처리 - ControllerAdvice
@ExceptionHandler를 통해 예외를 깔끔하게 처리할 수 있지만 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여있으며 하나의 컨트롤러에밖에 적용이 안된다. @ControllerAdvice 또는 @RestControllerAdvice를 사용하면 이러한 문제를 해결할 수 있다.
@RestControllerAdvice
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandle] 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 exHandle(Exception e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("EX", "내부 오류");
}
}
- 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler , @InitBinder 기능을 부여해준다.
- @ControllerAdvice에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다.
대상 컨트롤러 지정 방법
- 패키지를 기준으로 지정 : @RestControllerAdvice(basePackages = "com.example.controller")
- 애노테이션을 기준으로 지정 : @RestControllerAdvice(annotations = {MyCustomAnnotation.class})
'웹 개발' 카테고리의 다른 글
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 파일 업로드 (0) | 2024.06.01 |
---|---|
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 스프링 타입 컨버터 (0) | 2024.06.01 |
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 로그인 처리 (0) | 2024.05.30 |
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 검증 (0) | 2024.05.30 |
intellij 단축키 (0) | 2024.04.16 |