[Spring] 톰캣(servlet) 에러페이지가 아닌 whitelabel Error Page가 뜨는 이유

신창호·2023년 3월 12일
2

Spring

목록 보기
1/9
post-thumbnail

에러페이지

  • 보통 에러페이지 쓰임은 크게 두가지로 나뉜다.
    • 개발자가 보게되는 에러페이지
    • 고객이 보게되는 에러페이지
  • 그래서 에러페이지는 상황에 따라 필요한 정보를 적절하게 노출시켜줘야하는데, Spring(내장톰캣사용)을 사용하면, 기본적인 에러페이지를 제공해준다.
    • whiteLable Error Page
    • Servlet Error Page
  • 이번 포스팅에서는 기본으로 제공해주는 에러페이지에 대해 알아보고자 한다.

whiteLable Error Page

  • 먼저 스프링으로 개발을 진행하다가, 아래와 같은 에러페이지를 본적이 있을 거다!

  • 해당 페이지는 WhiteLable Error Page라고 불리며, Spring 프레임워크에서 제공해주는 에러페이지 이다.

Servlet(톰캣) Error Page

  • 가끔씩은 아래와 같은 서블릿(톰캣) 에러페이지도 볼 수 있다.

=> 하지만, 실제 Spring을 동작하게되면 대부분 WhiteLadel 에러페이지만 줄줄이 나온다.

그럼 어떻게 해야 톰캣(servlet) 에러 페이지를 나올 수 있께 할까?


먼저 스프링 부트가 제공하는 Error Page가 어디있는지 찾아보고자 했다.

분석하기

ErrorPage 커스텀 코드 다시보기

  • WebServerFactoryCustomizer로 웹 에러페이지를 커스텀하게 바꿀 수 있다.
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    @Override
    public void customize(ConfigurableWebServerFactory factory) {
				// .. 위 생략

        factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
    }
}
  • ConfigurableWebServerFactory 에서, addErrorPages()메소드로 에러 메시지를 추가해줬기 때문에, ConfigurableWebServerFactory을 주목했다.

ConfigurableWebServerFactory 인터페이스

  • 스프링 부트가 기본적으로 사용하는 구현체AbstractConfigurableWebServerFactory 이며, 내부 코드를 보면

  • 에러페이지를 Set<> 형태로 중복되지않게 가지고 있음을 알 수 있다.

하지만, 해당 구현체에서 자료구조는 만들어 주지만, ErrorPage는 비어 있었다. 그럼 어디서 만들어서 넣어주는 것일까?

addErrorPages() 메소드

  • ErrorPage를 추가해주는 메소드이다.
    • 이 메소드를 쓰는 곳은 딱 한군데 있었는데, 바로 ErrorMvcAutoConfiguration 라는 class였다.

ErrorMvcAutoConfiguration

  • WebMvcAutoConfiguration.class 전에 실행이되는 클래스이다
  • 해당 클래스 안에 내부 클래스로 ErrorPageCustomizer가 있다.

내부클래스 ErrorPageCustomizer

static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

		private final ServerProperties properties;

		private final DispatcherServletPath dispatcherServletPath;

		protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
			this.properties = properties;
			this.dispatcherServletPath = dispatcherServletPath;
		}

		/**
		* addErrorPages() 부분
		**/
		@Override
		public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
			ErrorPage errorPage = new ErrorPage(
					this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
			errorPageRegistry.addErrorPages(errorPage);
		}
		/**
		* 위 부분 까지만 
		**/


		@Override
		public int getOrder() {
			return 0;
		}
}
  • 주요깊게 봐야할 것은 registerErrorPages() 메소드
    • 말 그대로 에러페이지 등록이다.

this.dispatcherServletPath.getRelativePath()

default String getRelativePath(String path) {
		String prefix = getPrefix();
		if (!path.startsWith("/")) {
			path = "/" + path;
		}
		return prefix + path;
}
  • 해당 디스패서서블릿의 경로를 받아올 수가 있다.

this.properties.getError().getPath()

  • getError()
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
// .. 중략
}
  • getPath()
private String path = "/error";
  • 결국 ErrorPage는 해당 에러가 발생하였을때, 리다이렉트해주는 기능뿐이다. 그럼 해당 url을 다시 Controller가 받아 실행하는 부분이 있을 것이다.

BasicErrorController

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

	private final 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 modelAndView = resolveErrorView(request, response, status, model);
		

		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}
  • resolverErrorView 가 Httpstatus에 따라, error model과 view를 정해주는 것을 알 수 있다.

    하지만, 이것만으로 TomCat(Servlet) Error Page가 나오고, WhiteLabelError가 나온다고 알 수 없다.

WhiteLabelErrorPage

  • 다시 처음으로 돌아가 기본적으로 ErrorMvcAutoConfiguration.class안에 WhiteLabelErrorPage이라는 내부 클래스가 있다.

    • (위에 있던 ErrorPageCustomizer도 내부클래스였다.)
  • 추가적으로 다른게 있다면, defaultErrorView , 그니까 기본적인 에러메시지를 WhiteLableError 메시지로 설정한다는 것을 알 수 있다.

  • StaticView()를 보면 WhiteLabel Error Page를 html로 일일이 만들어 준다는 것을 알 수 있다.

StaticView

private static class StaticView implements View {

		private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);

		private static final Log logger = LogFactory.getLog(StaticView.class);

		@Override
		public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
				throws Exception {
			if (response.isCommitted()) {
				String message = getMessage(model);
				logger.error(message);
				return;
			}
			response.setContentType(TEXT_HTML_UTF8.toString());
			StringBuilder builder = new StringBuilder();
			Object timestamp = model.get("timestamp");
			Object message = model.get("message");
			Object trace = model.get("trace");
			if (response.getContentType() == null) {
				response.setContentType(getContentType());
			}
			builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
					"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
					.append("<div id='created'>").append(timestamp).append("</div>")
					.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
					.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
			if (message != null) {
				builder.append("<div>").append(htmlEscape(message)).append("</div>");
			}
			if (trace != null) {
				builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
			}
			builder.append("</body></html>");
			response.getWriter().append(builder.toString());
		}

		private String htmlEscape(Object input) {
			return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
		}

		private String getMessage(Map<String, ?> model) {
			Object path = model.get("path");
			String message = "Cannot render error page for request [" + path + "]";
			if (model.get("message") != null) {
				message += " and exception [" + model.get("message") + "]";
			}
			message += " as the response has already been committed.";
			message += " As a result, the response may have the wrong status code.";
			return message;
		}

		@Override
		public String getContentType() {
			return "text/html";
		}

	}

정리

  • 만약, WhiteLabelError메시지를 하고 싶지않다면, application.properties 설정 파일에 아래와 같이 설정해주면 된다
	server.error.whitelabel.enabled=false
  • 더 자세히 들어가면(DefaultErrorWebExceptionHandler), 코드로 ErrorPage의 우선순위를 정하는 부분도 나오지만 거기까지는 파헤치진 않았다.(Flux 사용하고 난리남..)
  • 왜냐하면, 지금까지 코드만 보더라도 에러페이지의 우선순위는 1.CustomErrorPage > 2. whiteLabelErrorPage > 3. TomcatErrorPage 임을 알아냈기 때문이다.
  • 참고로 whiteLabelErrorPage 은 Spring Boot에서 제공하는 페이지이다.

좀더 알면 좋을 것 같은 내용

  • AutoConfiguration
  • Replication

참고자료

profile
한단계씩 올라가는 개발자

1개의 댓글

comment-user-thumbnail
2023년 3월 20일

Flux를 사용하는 부분은 1개가 아닌 여러개를 반환페이지를 구성하는 부분이 아닐까 하네요.
깊은 고찰글 잘 보았습니다.

탐킷 튜닝에 대해서도 학습해보시면 더욱 좋을 듯하네요

답글 달기