Circular view path [error] 원인과 해결방법
Programming/Spring Boot

Circular view path [error] 원인과 해결방법

상황

운영 서버의 그라파나를 확인해보니 API 500 에러와 함께 아래 로그가 발생하고 있었다.

javax.servlet.ServletException: Circular view path [error]: would dispatch back to the current handler URL [/error] again. Check your ViewResolver setup!

 

문제 원인

구글링을 해보니 에러가 발생한 원인은 크게 2가지로 나뉜다.

  1. @Controller 사용 시 @GetMapping으로 매핑된 url과 view의 이름이 같은 경우
  2. 클라이언트에서 잘못된 url을 요청했을 때 에러를 핸들링할 ErrorController를 만들어주지 않았을 경우

나같은 경우 2번에 해당했다.

Nginx log를 보니 rooturl("/")로 접속했을 때 해당 에러가 발생한 것을 확인할 수 있었다. 즉, 스프링부트에서 rooturl("/")에 대응하는 핸들러를 찾을 수 없어서 발생한 문제였다.

 

에러로그 마지막을 보니 InternalResourceView에서 에러가 발생했다. InternalResourceView의 prepareForRendering  메서드 내 if문에서 DispatchLoop를 감지하여 ServletException을 발생시키고 있었다.

public class InternalResourceView extends AbstractUrlBasedView {

    private boolean alwaysInclude = false;

    private boolean preventDispatchLoop = false;

    @Override
    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

		// Determine the path for the request dispatcher.
		String dispatcherPath = prepareForRendering(request, response);

        //(중략)
    }

    protected String prepareForRendering(HttpServletRequest request, HttpServletResponse response) throws Exception {

        String path = getUrl();
        Assert.state(path != null, "'url' not set");

        if (this.preventDispatchLoop) { // 에러 발생 지점!!!
            String uri = request.getRequestURI();
            if (path.startsWith("/") ? uri.equals(path) : uri.equals(StringUtils.applyRelativePath(uri, path))) {
                throw new ServletException("Circular view path [" + path + "]: would dispatch back " +
                        "to the current handler URL [" + uri + "] again. Check your ViewResolver setup! " +
                        "(Hint: This may be the result of an unspecified view, due to default view name generation.)");
            }
        }
        return path;
    }
}

스프링부트에서는 클라이언트가 요청한 url과 매핑되는 핸들러가 없을 경우 기본적으로 "/error"에 대응하는 에러핸들러로 매핑된다. 이 때 반환타입이 html 페이지라면 error라는 뷰페이지를 보여주게 되는데 이때 Circular view path라는 이름에서도 유추할 수 있듯이 url과 view의 이름이 "/error"이어서 Circular view path 에러가 발생하게 된다.

 

해결 방법

해결 방법은 간단하다. "/error"에 대응하는 에러페이지를 만들어주면 된다.

@Controller
public class CustomErrorController implements ErrorController {

    @RequestMapping("/error")
    public String error(HttpServletRequest request) {
        return "에러 페이지입니다";
    }
}

ErrorController의 구현체를 작성하여 빈으로 등록하면 Spring Boot는 에러 처리를 위해 BasicErrorController 대신 해당 빈의 메서드를 사용하게 된다.

 

그러나 이것은 400, 500번대 에러에서도 200 OK 응답을 보내주기 때문에 좋지 않은 방법이다. 이를 해결하기 위해 아래처럼 ResponseEntity에 StatusCode를 함께 내려줄 수도 있다. BasicErrorController의 코드를 조금 변형했다.

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

    public CustomErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes);
    }

    @RequestMapping
    public ResponseEntity<String> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(status.toString(), status);
    }
}

결과 페이지

참고 자료