GitHub - kyukong/jwp-dashboard-mvc at step3

MVC 프레임워크를 구현하는 미션 중에서 로그인 요청(POST) 를 보내면 Cannot forward after response has been committed 의 에러가 발생하였다. 에러 메시지를 그대로 해석해보면 이미 Commit 된 Response는 forward 할 수 없다고 한다. 그래서 forward 를 하는 위치의 코드를 살펴보았다.

LoginController.java

@Controller
public class LoginController {

    private static final Logger log = LoggerFactory.getLogger(LoginController.class);

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public ModelAndView login(final HttpServletRequest request, final HttpServletResponse response) {
        if (UserSession.isLoggedIn(request.getSession())) {
            return new ModelAndView("redirect:/index.jsp");
        }

        final String viewName = InMemoryUserRepository.findByAccount(request.getParameter("account"))
            .map(user -> {
                log.info("User : {}", user);
                return login(request, user);
            })
            .orElse("redirect:/401.jsp");
        return new ModelAndView(viewName);
    }

    private String login(final HttpServletRequest request, final User user) {
        if (user.checkPassword(request.getParameter("password"))) {
            final var session = request.getSession();
            session.setAttribute(UserSession.SESSION_KEY, user);
            return "redirect:/index.jsp";
        } else {
            return "redirect:/401.jsp";
        }
    }
}

JspView.java

public class JspView implements View {

    private static final Logger log = LoggerFactory.getLogger(JspView.class);

    private static final String REDIRECT_PREFIX = "redirect:";

    private final String viewName;
    private final boolean redirect;

    public JspView(final String viewName) {
        validateJsp(viewName);
        if (viewName.startsWith(REDIRECT_PREFIX)) {
            this.viewName = viewName.substring(REDIRECT_PREFIX.length());
            this.redirect = true;
            return;
        }
        this.viewName = viewName;
        this.redirect = false;
    }

    private void validateJsp(final String viewName) {
        if (!viewName.endsWith(".jsp")) {
            throw new IllegalArgumentException(String.format("JSP 형식이 아닙니다. [%s]", viewName));
        }
    }

    @Override
    public void render(final Map<String, ?> model, final HttpServletRequest request, final HttpServletResponse response) throws Exception {
        if (redirect) {
            response.sendRedirect(viewName); // redirect 관련 설정
        }
        model.keySet().forEach(key -> {
            log.debug("attribute name : {}, value : {}", key, model.get(key));
            request.setAttribute(key, model.get(key));
        });

        final RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewName);
        requestDispatcher.forward(request, response); // 요청 forward
    }
}

이미 Commit 된 이후에 또 다른 설정이 되는 것이 안된다는 의미였으므로 sendRedirect() 와 forward() 에 찾아보았다.

HttpServletResponse#sendRedirect

If the response has already been committed, 
this method throws an IllegalStateException. 
After using this method, the response should be considered 
to be committed and should not be written to.

이미 Commit 된 이후에 sendRedirect 를 호출하면 IllegalStateException 가 발생한다.

HttpServletResponse#sendRedirect vs RequestDispatcher#forward

Untitled

애초에 redirect 와 forward 는 동시에 설정될 수 없다. redirect 는 사용자에게 원하는 URL 로 바로 응답하고, forward 는 다른 로직을 수행 후 사용자에게 응답한다.

그래서 다음과 같이 코드를 수정하였다.

JspView.java

public class JspView implements View {

		...

    @Override
    public void render(final Map<String, ?> model, final HttpServletRequest request, final HttpServletResponse response) throws Exception {
        if (redirect) {
            response.sendRedirect(viewName); // redirect 관련 설정
						return; // forward 를 실행하지 않기 위해 return
        }
        model.keySet().forEach(key -> {
            log.debug("attribute name : {}, value : {}", key, model.get(key));
            request.setAttribute(key, model.get(key));
        });

        final RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewName);
        requestDispatcher.forward(request, response); // 요청 forward
    }
}