Post View

Spring ExceptionHandler를 이용한 Exception 발생 시 오류 화면 출력

블로그에서 가끔씩 오류가 발생했었습니다.
정해진 경로로 접속하는 경우에는 문제가 없었지만 오류가 나는 페이지를 접속했던 이력들이 있더라구요.

물론 저도 이런 오류가 발생하는 화면을 자주 보았습니다.
보안상의 문제가 있을 수도 있고 보기에도 좋지 않으니 오류가 발생하는 경우 별도의 오류 페이지가 보이도록 수정하는 작업을 해보겠습니다.

package net.kurien.blog.exception.handler;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseStatus;

import net.kurien.blog.common.template.Template;
import net.kurien.blog.common.template.metadata.TemplateCss;
import net.kurien.blog.common.template.metadata.TemplateJs;
import net.kurien.blog.common.template.metadata.TemplateMeta;
import net.kurien.blog.exception.DuplicatedKeyException;
import net.kurien.blog.exception.NotFoundDataException;
import net.kurien.blog.module.category.service.CategoryService;
import net.kurien.blog.util.HtmlUtil;

@ControllerAdvice
public class BasicExceptionHandler {
    Logger logger = LoggerFactory.getLogger(BasicExceptionHandler.class);

    @Inject
    private Template template;
    
    @Inject
    CategoryService categoryService;

    @ExceptionHandler({NotFoundDataException.class})
    @ResponseStatus(value=HttpStatus.NOT_FOUND)
    public String notFoundExceptionHandler(HttpServletRequest request, HttpServletResponse response, Model model, Exception nfe) {
        String requestURI = (String)request.getAttribute("javax.servlet.forward.request_uri");
        String referer = request.getHeader("referer");

        model.addAttribute("referer", referer);
        
        try {
            logger.info("exception requestURI: " + requestURI, nfe);
    
            model.addAttribute("template", this.setTemplate(request));
            model.addAttribute("exceptionMsg", nfe.getMessage());
            model.addAttribute("exceptionDescription", "페이지가 이동되었거나 삭제되었습니다.<br>카테고리를 선택하시거나 아래에 표시된 버튼을 통해 이동하시기 바랍니다.");

            return "error/exception";
        } catch(Exception e) {
            logger.error("exception requestURI: " + requestURI, e);
            return "error/fatalError";
        }
    }
    
    private Template setTemplate(HttpServletRequest request) {
        String contextPath = request.getContextPath();
        
        template.setLang("ko");
        template.setCharset("utf-8");
        template.setMainTitle("Kurien's Blog");
        template.setSubTitle("오류 페이지");
        template.setDescription("Kurien's Blog는 프로그래밍과 개발 전반에 대한 내용을 다루는 블로그입니다.");
        
        template.setMeta(new TemplateMeta());
        template.setCss(new TemplateCss());
        template.setHeadJs(new TemplateJs());
        template.setFootJs(new TemplateJs());
        
        template.getCss().add("<link rel=\"stylesheet\" href=\"/css/module/error.css\">");
        
        template.setCategoryHTML(categoryService.getCategoryHTML(contextPath));
        
        // subTitle이 있다면 subTitle을 포함하여 표시한다. 
        String title = template.getMainTitle(); 
        
        if(template.getSubTitle().equals("") == false) {
            title = template.getSubTitle() + " | " + title;
        }
        
        template.setTitle(title);
        
        return template;
    }
}

@ControllerAdvice은 @Controller에서 발생된 Exception들을 catch하는 역할을 합니다.
class 상단에 @ControllerAdvice를 지정해주고 실제 Exception을 handling 해주는 notFoundExceptionHandler 메서드의 상단에는 @ExceptionHandler({NotFoundDataException.class})을 지정했습니다.
이 경우 Controller로부터 NotFoundDataException이 발생했을 때 notFoundExceptionHandler 메서드가 실행됩니다.

@ResponseStatus는 해당 메서드가 실행됐을 때 클라이언트에서 보여줄 HTTP 상태코드를 지정하는 어노테이션입니다.
HTTP 상태코드를 지정하지 않거나 잘못 지정하는 경우 오류 페이지를 정상 페이지로 인식하여 구글 검색 등에서 나타나는 경우도 있으니 상태코드는 실제 발생된 오류에 맞게 처리해야합니다.

메서드에서는 로그작성, 사용자에게 보여줄 메시지 지정 등을 처리한 후 Controller처럼 실제 사용자에게 보여질 view 파일의 경로를 리턴합니다.
그런데 ExceptionHandler를 이용한 처리 시 Interceptor를 타지 않는 문제가 있어 일단은 handler 내부에서 private 메서드인 setTemplate를 호출하여 처리하는 방식으로 마무리 하였습니다.

코드를 보시면 복잡해보일 수 있겠지만 실제 동작은 로그 작성, 템플릿 지정, 오류 메시지 설정, 오류 상세설명 설정, view 화면 설정 정도가 전부입니다.

그렇게 작업해서 나온 결과입니다.
HTML은 아래의 형태로 처리했습니다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<section id="exception" class="error">
    <h3 class="section_subject">Error page</h3>

    <div id="exception_box">
        <h4 id="exception_header">${exceptionMsg}</h4>
        
        <div id="exception_description">
            ${exceptionDescription}
        </div>
        
        <div id="exception_button_wrap">
            <c:if test="${referer != null}"><a class="kre_btn reverse_btn signin_btn" href="${referer}"><span class="material-icons">arrow_back</span> Previous Page</a></c:if><a class="kre_btn reverse_btn signin_btn" href="${contextPath}/"><span class="material-icons">home</span> Kurien's Blog</a>
        </div>
    </div>
</section>

HTML 부분도 그렇게 복잡하지 않습니다. 간단한 HTML에 CSS를 적용했습니다.

그런데 생각을 해보니 notFoundExceptionHandler에서도 오류가 발생할 수 있을 것 같아 추가적으로 try ~ catch를 걸었습니다.

try {
	logger.info("exception requestURI: " + requestURI, nfe);

	model.addAttribute("template", this.setTemplate(request));
	model.addAttribute("exceptionMsg", nfe.getMessage());
	model.addAttribute("exceptionDescription", "페이지가 이동되었거나 삭제되었습니다.<br>카테고리를 선택하시거나 아래에 표시된 버튼을 통해 이동하시기 바랍니다.");

	return "error/exception";
} catch(Exception e) {
	logger.error("exception requestURI: " + requestURI, e);
	return "error/fatalError";
}

여기서 Exception이 발생된다면 더 이상 핸들링을 해주는 부분이 없기 때문에 오류페이지 개선 전의 화면이 나타납니다.
그래서 여기서 오류가 발생하면 fatalError라는 화면으로 이동하도록 처리하였습니다.
fatalError 화면에서는 로그 작성과 최소한의 화면만 출력하도록 처리하여 추가적인 Exception이 발생되지 않도록 했습니다.

이렇게 ExceptionHandler를 이용하여 Exception 발생 시 원하는 형태의 화면으로 출력되도록 수정했습니다.
해당 수정사항은 https://github.com/kurien92/kreBlog/commit/a945fc558892f27c09dc40dead74818f27c92b1c에서 확인 가능하며,
web.xml의 error-page 입력을 통한 오류 처리는 다음 포스팅에서 작성하도록 하겠습니다.

Comments