반응형
예외 처리랑 오류 페이지를 만드는 작업을 왜 해야 하나요?
- 웹 사이트 이용시 비정상적인 접근을 할 경우 서버에서 오류 페이지를 반환받은 경험이 있을것이다.
- 위와 같은 상황에서 개발자는 사용자에게 정상적인 요청이 이루어지지 않은 이유를 알려줘야 할 필요가 있다.
- Spring은 이러한 오류처리를 편리하게 제공한다.
- 이 글에서는 Spring의 예외 처리와 오류페이지에 대해 설명하며 이러한 동작을 할 때 내부원리의 동작과정에 대해서 설명한다.
스프링의 예외처리는 어떻게 진행되나요?
- 스프링은 ErrorPage를 자동으로 등록한다.
- src/main/resources/error 경로의 HTML 파일을 오류 반환 HTML로 설정한다.
- 파일 이름을 오류 번호대로 해야한다.
- ex:)300.html, 3xx.html, 400.html, 500.html
- 3xx의 xx는 해당 맨 앞 번호대의 오류 번호일 경우 응답한다.
- 파일 이름을 오류 번호대로 해야한다.
- 기본 경로를 기반으로 오류 처리번호가 구체적인 HTML를 View에 반환한다.
- 예를 들어 4xx, 404가 html이 있을 경우 404 홈페이지가 요청될 경우 더 구체적인 404 에러페이지를 반환한다.
- 정적 HTML이면 정적리소스 Directory에 넣으면 된다.
- ex:)resources/static/error/400.html
- 뷰 템플릿을 사용해서 동적으로 오류 화면을 만들고 싶다면 뷰 템플릿 경로에 오류페이지를 넣으면 된다.
- ex:)1. resources/templates/error/500.html
- 이러한 로직들은 Spring의 BasicErrorController가 제공하는 기능이다.
BasicErrorControler
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.web.servlet.error;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
/**
* Basic global error {@link Controller @Controller}, rendering {@link ErrorAttributes}.
* More specific errors can be handled either using Spring MVC abstractions (e.g.
* {@code @ExceptionHandler}) or by adding servlet
* {@link AbstractServletWebServerFactory#setErrorPages server error pages}.
*
* @author Dave Syer
* @author Phillip Webb
* @author Michael Stummvoll
* @author Stephane Nicoll
* @author Scott Frederick
* @since 1.0.0
* @see ErrorAttributes
* @see ErrorProperties
*/
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @param errorProperties configuration properties
*/
public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
this(errorAttributes, errorProperties, Collections.emptyList());
}
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @param errorProperties configuration properties
* @param errorViewResolvers error view resolvers
*/
public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties,
List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
}
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ResponseEntity<String> mediaTypeNotAcceptable(HttpServletRequest request) {
HttpStatus status = getStatus(request);
return ResponseEntity.status(status).build();
}
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
if (this.errorProperties.isIncludeException()) {
options = options.including(Include.EXCEPTION);
}
if (isIncludeStackTrace(request, mediaType)) {
options = options.including(Include.STACK_TRACE);
}
if (isIncludeMessage(request, mediaType)) {
options = options.including(Include.MESSAGE);
}
if (isIncludeBindingErrors(request, mediaType)) {
options = options.including(Include.BINDING_ERRORS);
}
return options;
}
/**
* Determine if the stacktrace attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the stacktrace attribute should be included
*/
protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
switch (getErrorProperties().getIncludeStacktrace()) {
case ALWAYS:
return true;
case ON_PARAM:
return getTraceParameter(request);
default:
return false;
}
}
/**
* Determine if the message attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the message attribute should be included
*/
protected boolean isIncludeMessage(HttpServletRequest request, MediaType produces) {
switch (getErrorProperties().getIncludeMessage()) {
case ALWAYS:
return true;
case ON_PARAM:
return getMessageParameter(request);
default:
return false;
}
}
/**
* Determine if the errors attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the errors attribute should be included
*/
protected boolean isIncludeBindingErrors(HttpServletRequest request, MediaType produces) {
switch (getErrorProperties().getIncludeBindingErrors()) {
case ALWAYS:
return true;
case ON_PARAM:
return getErrorsParameter(request);
default:
return false;
}
}
/**
* Provide access to the error properties.
* @return the error properties
*/
protected ErrorProperties getErrorProperties() {
return this.errorProperties;
}
}
- BasicErrorControlerr는 아래와 같은 정보를 Model에 담아 뷰에 전달한다.
- timeStamp: 에러 발생시간
- status : 상태
- error : error 발생 원인
- exception : 예외 정보
- trace : 예외 trace
- meesgae: 검증이 실패했을 경우의 object, 숫자등 정보들
- errors : 에러 종류
- path : 클라이언트 요청 경로
- 뷰 템플릿을 활용해 이러한 값들을 출력할 수 있다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>500 오류 화면 스프링 부트 제공</h2></div>
<div>
<p>오류 화면 입니다.</p>
</div>
<ul>
<li>오류 정보</li>
<ul>
<li th:text="|timestamp: ${timestamp}|"></li>
<li th:text="|path: ${path}|"></li>
<li th:text="|status: ${status}|"></li>
<li th:text="|message: ${message}|"></li>
<li th:text="|error: ${error}|"></li>
<li th:text="|exception: ${exception}|"></li>
<li th:text="|errors: ${errors}|"></li>
<li th:text="|trace: ${trace}|"></li>
</ul>
</li>
</ul>
<hr class="my-4">
</div> <!-- /container -->
</body>
</html>
- 이러한 오류 관련 정보는 고객들에게 노출하는것이 좋지 않다.
- 보안상 문제가 될 수 있기 때문이다.
- 그렇기 때문에 BasicErrorController는 오류 정보를 model에 포함할 지에 대한 여부를 사용자에게 맡긴다.
- 아래와 같은 옵션을 활용하여 Model에 데이터를 전할지 말지 결정한다./
server.error.include-exception=false //기본 화이트라벨 기반 오류 페이지
server.error.include-message=on_param//오류 message여부
server.error.include-stacktrace=on_param//오류 stacktrace 여부
server.error.include-binding-errors=on_param//오류 errrors 종류 여부
속성 종류
- never : 사용하지 않음
- always :항상 사용
- on_param : 파라미터가 있을 때(message나 statrace옵션이 on_param이고 URL 쿼리 파라미터가 존재할 경우 오류를 출력한다는 의미이다.)
중간정리
- 스프링의 에러처리는 BindingErrorController가 자동으로 처리해주기 때문에 개발자는 편리하게 error처리 홈페이지만 등록해서 사용하면 된다는것을 알 수 있다.
- 그렇다면 그 내부 동작을 스프링이 아닌 순수 Servlet 컨테이너부터 살펴보자
서블릿 예외처리 방법
- 서블릿은 Exception과 response.sendError(Http 오류메시지)를 활용해서 에러를 WAS에게 전달할 수 있다.
- 만약 Exception이 Servlet 밖까지 나갈 경우에는 WAS에서 예외를 반환한다.
- response.sendError()또한 예외처리가 안될 경우 WAS에서 최종적으로 예외처리를 진행한다.
서블릿의 오류 페이지 작동 원리
- 서블릿은 Exception (예외)이 발생해서 서블릿 밖으로 전달되거나 또는 response.sendError()가 호출되었을 때 설정된 오류 페이지를 찾는다.
- 사용자 요청이 controller에 와서 예외가 발생하기까지의 흐름은 아래와 같다.
요청 → WAS → 필터 → Servlet → 인터셉터 → Controller(예외 발생
- 예외가 발생한 이후 response.sendError()의 흐름은 아래와 같다.
WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 (response.sendError())
- 이때 WAS는 미리 등록한 해당 예외를 처리하는 오류 페이지 정보를 확인한다.
- 과거에는 아래와 같이 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>
- Spring Boot를 사용하면 아래와 같이 간편하게 등록이 가능하다.
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;
/**
* WebServerFactoryCustomizer를 사용하게 되면 Spring의 기본 옵션인 BasciErrorController에서 처리하는 옵션대로 진행이 안된다.
* 에러 경로를 커스터 마이징하고 싶을때 사용하자
*/
//@Component
//errorPage를 등록하는 과정
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);
}
}
- 오류 페이지 정보를 확인하고 다시 오류 페이지를 출력하기 위해 controller에게 오류를 재요청하게 된다.
WAS /error-page/500 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/ 500) -> View
- 지금까지의 방식을 보면 비효율적인 부분이 한 가지 보인다.
- 이미 한 번 요청이 일이나서 필터와 인터셉터를 걸치면서 검증된 호출 로직이 오류 페이지를 출력하기 위해 다시 한 번 필터와 인터셉터 호출이 일어나는점은 굉장히 비효율적이다.
- 이러한 문제를 해결하기 위해 필터는 DispatchrType, 인터셉터는 excludePathPatterns를 지원하여 자체적으로 error관련 URL을 제외할 수 있다.
- DispatchrType은 해당 타입의 요청일 경우에만 필터가 동작하도록 설정하는것을 의미한다.
- 그렇기 때문에 Servlet은 기본값으로 Request로 설정되어있다. (ERROR는 동작할 이유가 없으니까!)
- 필터의 DispatchrType의 종류는 아래와 같다.
- REQUEST : 클라이언트 요청
- ERROR : 오류 요청
- FORWARD : MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때
RequestDispatcher
.forward(request, response); - INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
RequestDispatcher.include(request, response);
- ASYNC : 서블릿 비동기 호출
- 인터셉터는 http 요청이 올 경우 아래와 같이 excludePatterns를 활용해 에러페이지는 검증을 안하도록 제외 해야한다.
package hello.exception;
import hello.exception.filter.LogFilter;
import hello.exception.interceptor.LogInterceptor;
import hello.exception.resolver.MyHandlerExceptionResolver;
import hello.exception.resolver.UserHandlerExceptionResolver;
import org.apache.catalina.User;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
//error-page를 추가해주어 중복을 제거하였음
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "*.ico", "/error-page/**");
}
}
정리
- Spring은 우리들의 예외처리 및 오류 페이지를 View에 반환해주작업을 BasicErrorController를 활용하여 자동으로 처리해준다.
- 필터는 DispatchType을, 인터셉터는 excluedePatterns를 사용해 오류 페이지 반환시 불필요한 호출을 하지 않는다.
- 인터셉터는 excludePatterns를 통해 항상 설정해주는것을 잊지말자
예제의 모든 코드
https://github.com/NamHyeop/Spring_Boot_Study/tree/master/4.spring-MVC2/exception
REFERENCE.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
반응형
'Spring' 카테고리의 다른 글
Spring의 API 예외 처리 (0) | 2022.07.05 |
---|---|
Spring의 필터와 인터셉터 (0) | 2022.07.01 |
Spring의 쿠키,세션 (0) | 2022.06.29 |
Spring의 Bean Validation (0) | 2022.06.28 |
Spring 메시지, 국제화 (0) | 2022.06.27 |