서블릿은 다음 2가지 방식으로 예외를 처리한다.
- Exception
- response.sendError
Exception (예외)
자바 직접 실행의 경우
자바의 메인 메서드를 직접 실행하는 경우 main이라는 이름의 쓰레드가 실행된다. 실행 도중에 예외를 잡지 못하고 main()을 넘어서 예외가 던져지면 예외 정보를 출력하고 해당 쓰레드는 강제 종료된다.
웹 애플리케이션
웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다. 애플리케이션에서 예외가 발생했는데, try-catch문으로 잡지 못하고 서블릿 밖으로까지 예외가 전달되면 WAS까지 이동된다. 이동되는 단계는 다음과 같다.
WAS(여기까지 전달된다) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외 발생)
그렇다면 예외를 전달 받은 WAS에서는 예외를 어떻게 처리할까?
WAS는 전달받은 예외를 자체적으로 가지고 있는 예외 매커니즘을 통해 예외를 처리한다.
<톰캣의 경우>
- 예외가 발생할 경우, 해당 예외의 스택 트레이스를 로그에 출력하고 사용자에게 기본적인 오류페이지(일반적으로 HTTP상태코드 500 + 서버내부오류)를 보여준다.
- 하지만, 이러한 기본 페이지는 사용자 친화적이지 않고 보안 측면에서도 시스템 정보를 외부에 노출하는 위험성이 있기 때문에 애플리케이션 레벨에서 예외를 처리하도록 하는 것이 좋다.
response.sendError(HTTP 상태코드, 오류메시지)
오류가 발생했을 때 HttpServletResponse가 제공하는 .sendError() 메서드를 사용할 수 있다. 이 메서드는 당장 예외가 발생하는 것은 아니지만 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있다. 또한 HTTP상태코드와 오류메시지도 추가할 수 있다.
- response.sendError (HTTP 상태코드)
- response.sendError (HTTP 상태코드, 오류메시지)
sendError 흐름
WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(response.sendError())
- response.sendError() 를 호출하면 response 내부에는 오류가 발생했다는 상태를 저장한다.
- 서블릿 컨테이너는 사용자에서 응답하기 전에 response에 sendError()가 호출되었는지 확인한다.
- 호출이 되었다면 설정한 오류 코드에 맞추어 기본 오류 페이지를 보여준다.
서블릿 예외처리 - 오류화면 제공
서블릿은 Exception (예외)가 발생해서 서블릿 밖으로 전달되거나 또는 response.sendError() 가 호출 되었을 때 각각의 상황에 맞춘 오류 처리 기능을 제공한다.
web.xml을 통한 오류 페이지 출력 방식
<web-app>
<error-page>
<error-code>404</error-code>
<location>/error-page/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error-page/500.html</location>
</error-page>
<error-page>
<exception-type>java.lang.RuntimeException</exception-type>
<location>/error-page/500.html</location>
</error-page>
</web-app>
- 각각의 에러코드에 해당되는 오류가 발생하면 매핑된 해당 페이지를 보여주는 식의 web.xml 파일을 설정해서 보여준다.
스프링 부트를 통해 서블릿 오류 페이지 등록
package hello.exception;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
/**
* 웹 서버를 커스터 마이징할 수 있는 방식
*/
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
//에러 페이지 생성 (스프링 부트 제공)
//받은 예외를 확인하고 해당 예외에 대한 페이지를 WAS -> 컨트롤러로 설정한 경로를 호출하면서 해당 오류페이지를 호출한다.
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404"); // 400번 에러
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500"); // 500번 에러
//런타임 에러 페이지 생성 -> 런타임에러의 자식 예외 또한 다 적용된다.
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
//에러 페이지 등록
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
- 오류가 발생했을 때 처리할 수 있는 컨트롤러가 필요하다. 예를 들어서 RuntimeException 예외가 발생하면 errorPageEx에서 지정한 /error-page/500 이 호출된다.
[컨트롤러 - 오류 페이지 출력 컨트롤러]
package hello.exception.servlet;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Controller
public class ErrorPageController {
//404 예외 발생 시 해당 오류 페이지 출력.
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response){
log.info("errorPage 404");
return "error-page/404";
}
//500 예외 발생 시 해당 오류 페이지 출력.
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response){
log.info("errorPage 500");
return "error-page/500";
}
}
[컨트롤러 - 예외 발생시키는 컨트롤러]
package hello.exception.servlet;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Controller
public class ServletExController {
//런타임 에러 발생 (Exception)
@GetMapping("/error-ex")
public void errorEx(){
throw new RuntimeException("예외 발생!!");
}
//404 에러 발생(response.sendError)
@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
response.sendError(404, "404 오류!!");
}
//400 에러 발생(response.sendError)
@GetMapping("/error-400")
public void error400(HttpServletResponse response) throws IOException {
response.sendError(400, "400 오류!!");
}
//500 에러 발생(response.sendError)
@GetMapping("/error-500")
public void error500(HttpServletResponse response) throws IOException {
response.sendError(500) ;
}
}
오류페이지 작동 원리
1. 서블릿은 Exception (예외)가 발생해서 서블릿 밖으로 전달되거나 또는 response.sendError() 가 호출되었 을 때 설정된 오류 페이지를 찾는다.
예외 발생 (Exception) 흐름
예외가 발생하면 try-catch 문 등을 통한 예외 처리가 없는 한 WAS까지 전달된다.
컨트롤러(예외 발생!) -> 인터셉터 -> 서블릿 -> 필터 -> WAS(여기까지 전파)
sendError 흐름
sendError의 경우도 마찬가지로 WAS까지 전달된다.
컨트롤러(response.sendError) -> 인터셉터 -> 서블릿 -> 필터 -> WAS(sendError 호출 기록 확인)
2. WAS는 해당 예외를 처리하는 오류페이지 정보를 확인한다.
- 만약, RuntimeException이 발생하면, 위의 웹서버커스터마이저에서 설정한 RuntimeException에 대한 오류정보페이지를 확인한다.
- new ErrorPage(RuntimeException.class, "/error-page/500")
3. WAS는 다시 설정한 해당 경로(/error-page/500)로 요청을 보내 오류 페이지를 호출한다.
- [오류 페이지 요청 흐름]
WAS ('/error-page/500') 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 (/error-page/500) 실행 -> View 호출
정리
[예외 발생 시]
1. 컨트롤러(예외 발생!!) -> 인터셉터 -> 서블릿 -> 필터 -> WAS 로 전달
2. WAS에서 해당 예외 정보 확인 후, 오류페이지 호출 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> View 호출
- 이처럼 예외가 발생해서 WAS까지 전달되면 다시 WAS에서 해당 오류페이지를 출력하기 위해 서버 내에서 요청을 보낸다.
- 이러한 과정은 서버 내부에서 일어나기 때문에 클라이언트(웹 브라우저)는 전혀 알 수 없다.
- WAS는 오류페이지를 단순히 다시 요청만 하는 것이 아니라, 오류 정보를 request에 담아서 넘겨준다.
- 따라서, request에 담긴 오류정보들을 활용할 수 있다.
[ request.attribute에 담긴 오류정보 - RequestDispatcher 상수로 정의되어 있음. ]
- javax.servlet.error.exception : 예외
- javax.servlet.error.exception_type : 예외 타입
- javax.servlet.error.message : 오류 메시지
- javax.servlet.error.request_uri : 클라이언트 요청 URI
- javax.servlet.error.servlet_name : 오류가 발생한 서블릿 이름
- javax.servlet.error.status_code : HTTP 상태 코드
서블릿 예외 처리 시 발생하는 필터, 인터셉터 중복 호출 제거
위에서 서블릿 예외처리 동작 순서를 보면 필터와 인터셉터가 중복하여 다시 호출되는 것을 알 수 있다. 이미 한번 필터나 인터셉터를 거쳐 처리가 완료된 후 다시 또 호출되는 것을 의미 없고 매우 비효율적이다.
[예외 발생 시]
1. 컨트롤러(예외 발생!!) -> 인터셉터 -> 서블릿 -> 필터 -> WAS 로 전달
2. WAS에서 해당 예외 정보 확인 후, 오류페이지 호출 요청 -> 필터(재호출) -> 서블릿 -> 인터셉터(재호출) -> 컨트롤러 -> View 호출
=> 매우 비효율적!!!
- 이러한 문제를 해결하기 위해서는 서블릿이 요청이 들어왔을 때, 클라이언트가 보낸 정상 요청인지, 오류로 인한 오류페이지 출력 요청인지 구분할 수 있어야 한다.
필터 중복 제거 - DispatcherType
필터의 경우, 이 같은 요청을 구분하기 위해 'DispatcherType'의 옵션을 제공한다. DispatcherType은 의미 그대로 요청이 올때 어떤 요청인지를 분별해준다. DispatcherType의 종류는 다음과 같다.
[ DispatcherType ]
- REQUEST : 클라이언트 요청
- ERROR : 오류 요청
- FORWARD : 서블릿에서 다른 서블릿이나 JSP를 호출할 때
( RequestDispatcher.forward(request, response); )
- INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
( RequestDispatcher.include(request, response); )
- ASYNC : 서블릿 비동기 호출
필터에 DispatcherType 적용
필터에 DispatcherType 설정은 사용자 필터를 작성하여 등록하는 WebConfig 파일에 필터를 등록할 때, DispatcherType을 설정해주면 된다.
package hello.exception;
import hello.exception.filter.LogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
@Configuration
public class WebConfig{
@Bean
public FilterRegistrationBean logFilter(){
//로그 필터(사용자 정의 필터) 등록
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
//해당 필터는 DispatcherType이 REQUEST와 ERROR 두 가지의 경우에만 호출.
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return filterRegistrationBean;
}
}
- .setDispatcherTypes() 메서드를 사용하여 해당 필터를 어떤 요청 타입일 때만 적용하게 끔 설정할 수 있다.
- 해당 메서드에 아무것도 넣지 않으면, 기본 값으로 REQUEST 타입에만 필터가 적용된다.
- 따라서, 오류 페이지 처리 요청에 대한 필터 적용을 하지 않으려면 REQUEST 타입에만 필터가 적용되게끔 설정하면 된다.
인터셉터 중복 제거 - .excludePathPatterns()
인터셉터의 경우, 서블릿이 아닌 스프링이 제공하는 기능이기 때문에 DispatcherType를 활용할 수 없다. 대신, 인터셉터는 요청 경로에 따라 경로를 추가하거나 제외하기 쉽게 설계되어 있기 때문에 이를 활용한 방식을 이용한다.
package hello.exception;
import hello.exception.interceptor.LogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.DispatcherType;
@Configuration
public class WebConfig implements WebMvcConfigurer {
//인터셉터는 필터와 달리 dispatcherType를 지정할 수 없기 때문에 .excludePathPatterns()에 해당 에러 페이지 경로를 등록하면 된다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/*")
.excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**"); //해당 경로들로 요청이 들어오면 인터셉터 실행X
}
}
- 인터셉터를 등록할 때 .excludePathPatterns()메서드로 제외할 경로를 설정해두면 인터셉터가 실행되지 않기 때문에 이를 활용해서 중복을 제거할 수 있다.
전체 흐름 정리
- /hello (정상 요청의 경우)
WAS(/hello, DispatcherType = REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> View
- /error-ex (오류 요청의 경우)
- 필터 => DispatcherType으로 중복 호출 제거
- 인터셉터 => .excludePathPatterns로 중복 호출 제거
1. WAS(/error-ex, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
2. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
3. WAS 오류 페이지 확인
4. WAS(/error-page/500, dispatchType=ERROR) -> 필터(호출x) -> 서블릿 -> 인터셉터(호출x) -> 컨트 롤러(/error-page/500) -> View
'Spring > SpringMVC' 카테고리의 다른 글
API 예외 처리 (1) | 2024.02.27 |
---|---|
스프링 부트 - 오류페이지 처리 (0) | 2024.02.19 |
필터, 인터셉터 (0) | 2024.02.11 |
쿠키, 세션을 활용한 로그인, 로그아웃 (0) | 2024.02.04 |
검증 - Bean Validation (0) | 2024.01.22 |