Servlet, Spring의 Web MVC 예외처리

dev_314·2023년 3월 20일
0

Spring - Trial and Error

목록 보기
4/7

Servlet 예외처리

서블릿은 두 방식으로 예외 처리를 지원한다.

  1. Exception
  2. response.sendError(statuscode, message)

Exception

예외는 쓰레드의 call stack을 따라 전파된다.
그러므로 WAS에서 생성된 요청 쓰레드에서 발생한 예외를 catch하지 않으면 WAS까지 예외가 전파된다.

WAS <- Filter <- Servlet <- Interceptor <- Controller(Service, Repository, ...)
server.error.whitelabel.enalbed = false;
// 오류 처리 화면을 못 찾으면, 스프링의 whitelabel 페이지 사용하기 옵션
``` java
@Controller
public class TestController {
	
	@GetMapping("/server-error")
	public void serverError() {
    	throw new RuntimeException(); // WAS가 알아서 505을 리턴
    }
}

예외를 발생시키면 톰캣(WAS)가 예외 응답을 (404, 500, ...)발생시킨다.

response.sendError(code, msg)

HttpServletResponse의 sendError를 사용하면 명시적으로 예외 응답을 반환한다.

@Controller
public class TestController {
	
	@GetMapping("/server-error")
	public void serverError() {
    	throw new RuntimeException(); // WAS가 알아서 505을 리턴
    }
    
    @GetMapping("/not-found")
    public void notfound(HttpServletResponse response) throws IOException{
    	response.sendError(404, "not found"); // msg 생략 가능
    }
}

response.sendError는 기술적으로(?) 봤을 때, Exception을 발생시키는 것이 아니다.

Filter, DispatcherServlet, Interceptor, Controller에서는 정상적으로 처리한 것이고, 마지막 WAS단에서 response의 상태를 봤는데, sendError를 호출했음을 파악하고 WAS가 알아서 예외 응답을 반환하는 것이다.

참고

response.sendError에 정상 코드(EX. 200, 300 등)을 넣어도 정상 작동하긴 한다.

    @GetMapping("/exception")
    public void throwException(HttpServletResponse response) throws IOException {
        response.setHeader("location", "https://www.naver.com");
        response.sendError(302, "is it Error?");
    }

위 코드는 정상적으로 네이버로 redirect시킨다.

커스텀 예외 페이지 제공하기

톰캣이 제공하는 예외 페이지는 구리다. 개선해보자.

  1. web.xml에서 어떤 예외 code에, 어떤 페이지를 제공할지 지정할 수 있다. (검색해서 찾아보기)
  2. 스프링 부트를 사용하는 경우에는 다음처럼 할 수 있다.

스프링 부트 사용해서 개선하기

WebServerFactoryCustomizer

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {

	@Override
    public void customize(ConfigurableWebServerFactory factory) {
    	ErrorPage error404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
    	ErrorPage error500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
    	ErrorPage errorRX = new ErrorPage(RuntimeException.class, "/error-page/500");

		factory.addErrorPage(error404, error500, errorRX);
	}
}

new ErrorPage(예외로 인식할 status code, 에러 처리 handler url) 형식으로 예외로 처리할 status code와, 이를 처리할 handler url을 지정할 수 있다.

new ErrorPage(RuntimeException.class, "/error-page/500")처럼 예외로 처리할 Exception을 지정할 수 있다.
이때 Exception은 하위 Exception도 모두 커버한다.

@RequestMapping("/error-page/404")
void String errorPage404() {
	return "error-page/404"; // error-page/404.html 파일을 응답
}

WebServerCustomizer에서 매핑한 예외를 처리할 handler를 만들어야 한다.

만약 handler가 없다면 원래 Status code와 상관 없이 404 Not Found가 발생한다. 왜 그럴까?

작동 원리

response.sendError는 Exception을 발생시키는 것이 아니므로, response.sendError을 사용하면 WAS까지 정상적으로 응답이 나간다.

WAS에서 response.sendError이 호출된 것을 확인한 경우, 예외 매핑 정보를 확인 한 뒤, 매핑된 url이 있다면 새로 요청을 보낸다.

WAS는 WebServerCustomizer에 명시된 매핑 정보(WebServerCustomizer)에 따라, 새로운 에러 처리 요청을 발생시킨다.

// response.sendError를 호출
1. WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러

// response.sendError이 호출되었음을 파악하고 새로운 요청을 발생
2. WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> 뷰

이때 WAS는 request에 예외와 관련한 정보를 삽입해서 전달한다.

import static javax.servlet.RequestDispatcher.*;

@GetMapping("handleException")
public void exceptionHandler(HttpServletRequest request) {
	// 이거 외에도 여러가지 정보를 확인할 수 있다.
	request.getAttribute(ERROR_EXCEPTION)
	request.getAttribute(ERROR_EXCEPTION_TYPE)
	request.getAttribute(ERROR_MESSAGE)
	request.getAttribute(ERROR_REQUEST_URI)
	request.getAttribute(ERROR_SERVLET_NAME)
	request.getAttribute(ERROR_STATUS_CODE)
}

서블릿 예외 처리: Filter

ServletResponse의 sendError를 사용해서 예외를 처리하면 filter를 두 번(방향)으로 거치게 된다.

사용자가 원래 보낸 요청에서 한 번
sendError 호출
예외를 처리하기 위한 요청에서 한 번

총 2번 Filter를 거친다.

인증 등의 목적을 위해 필터, 인터셉터를 사용하는데, WAS에서 예외 처리를 위해 만든 요청에도 필터, 인터셉터를 거치게 하는건 비효율적이다.

요청의 성격에 따라 필터, 인터셉터를 탈지말지 결정하고 싶다.

DispatcherType

DispatcherType이라는 옵션을 통해 요청의 성격(?)을 구분할 수 있다.

public enum DispatcherType {
	FORWARD, // 서블릿에서 다른 서블릿, JSP를 호출
    INCLUDE, // 서블릿에서 다른 서블릿, JSP결과를 포함
    REQUEST, // 클라이언트 요청
    ASYNC, // 서블릿 비동기 호출
    ERROR // 오류 요청
}
public void doFilter(ServletRequest request, ...) {
	// 요청의 성격을 확인해 보자.
	request.getDispatcherType();
    ...
}

DispatcherType을 통해 filter가 어떤 요청일 때 반응할 지 설정할 수 있다.

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import javax.servlet.Filter;

@Configuration
public class Config {

	@Bean
    public FilterRegistrationBean myFilter() {
    	FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        // 사용할 필터를 설정
        filterRegistrationBean.setFilter(new MyFilter());
        // 필터 적용 순서를 지정
        filterRegistrationBean.setOrder(1);
        // 필터가 반응할 URL을 지정(모든 요청에 대해 반응)
        filterRegistrationBean.addUrlPatterns("/*");
        // 필터가 반응할 요청 타입을 지정
        // 해당 필터는 Request, Error 타입의 요청에 반응함
        filterRegistrationBean.setDispatcherType(DispatcherType.REQUEST, DispatcherType.ERROR);
       
        return filterRegistrationBean;
    }
}

따로 요청 타입을 설정하지 않으면 기본값 REQUEST에 대해서만 필터가 반응한다.

참고

  1. WAS에서 Filter를 거쳐 예외 처리 Controller로 요청을 보내는 과정에서, Filter 내부에서 chain.doFilter(req, res)를 통해 다음 필터(또는 컨트롤러)를 호출하지 않으면 500 Internal Server Error가 발생한다.
  2. Controller(또는 그 이하 layer)에서 발생한 Exception을 Filter에서 Catch한 뒤, WAS까지 다시 throw 하지 않으면 정상적으로 예외 처리 요청이 발생하지 않는다.

서블릿 예외 처리: Interceptor

인터셉터는 따로 요청 타입을 구분할 수 없다.
대신 요청 경로 패턴으로 구분해야 한다.

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

@Configuration
public class Config implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
        		.order(1)
                .addPathPatterns("/**") // 서블릿의 url 패턴과 약간 다르다 (모든 경로에 대해 적용)
                .excludePatterns("/error-page/**"); // 예외 요청은 인터셉터 제외
    }
}

예외 발생 여부에 따른 인터셉터 메서드들의 호출 여부를 잘 고려해야 한다.

스프링 부트로 개선하기

스프링 부트를 사용하면 WebServerFactoryCustomizer, 예외 처리 Controller를 생략할 수 있다.

스프링 부트는 다음의 기능을 수행한다.

  1. /error 경로로 ErrorPage 등록 (WebServerFactoryCustomizer 대체)
  2. /error 요청을 처리하는 BasicErrorController 등록
@Controller
// 환경 변수를 설정해서 예외 컨트롤러 경로를 수정할 수 있다.
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
	...
}

BasicErrorController는 아래의 우선순위 따라 예외시 보여줄 view를 찾는다.

1. 뷰 템플릿에서 찾기

resources/templates/error/500.html
resources/templates/error/5xx.html // 모든 종류의 500번대 예외 처리

2. 정적 리소스에서 찾기

resources/static/erorr/400.html
resources/static/erorr/404.html
resources/static/erorr/4xx.html // 모든 종류의 400번대 예외 처리

3. 적용 대상이 없으면 뷰 이름으로 찾기

resources/templates/error.html

참고

  1. Spring Boot는 더 specific한 이름 순서로 자원을 찾는다.
  2. BasicErrorController를 상속받아서 커스텀 예외 컨트롤러를 만들 수도 있다. 보통 그럴 일이 많지는 않다고 한다.
  3. 설정(server.error.whitelabel.enalbed = false;)을 통해 스프링의 whitelabel 페이지 사용하기 옵션을 끌 수 있다.
profile
블로그 이전했습니다 https://dev314.tistory.com/

0개의 댓글