[Servlet] 4. Forward와 Redirect
Forward
1. foward()의 역할
- 컨테이너 내에서 처음 요청 받은 페이지가 요청 데이터(HttpServletRequest, HttpServletResponse)를 다른 페이지에 전송하여 처리를 요청하고, 자신이 처리한 것처럼 응답한다.
- 클라이언트가 요청한 url주소(페이지)가 변경되지 않는다.
2. forward()의 구조
1) 서버(톰캣)를 실행하면 내부 서블릿 컨테이너에 작성한 서블릿이 다 올라가고, doGet, doPost 등 매핑된 url로 연결된다.
2) HTTP에 의한 요청을 전달하면 헤더의 문자열을 파싱해서 헤더, 데이터, 응답 대상 브라우저 등을 request와 response 객체로 쪼개어 doGet()/doPost() 메소드로 보낸다.
3) 이 때 요청받은 서블릿에서 다른 서블릿으로 request, response 객체를 담아 forward하면 동일한 속성을 가지고 처리 권한을 위임한다. (전달된 req, resp의 모든 정보를 이용해 새로운 req, resp를 깊은 복사를 통해 만들어 전달하므로 요청 방식(post -> post) 등 데이터가 그대로 유지된다. 즉, 데이터 공유가 가능하다.)
4) 서버 내부에서 다른 서블릿에 위임했지만 요청받은 서블릿이 응답하는 것처럼 처리하므로 위임한 경로를 노출하지 않는다. 즉, 처리하는 서블릿이 변경되었어도 url이 변경되지 않는다.
* 새로고침(재요청) 시 동일 요청을 반복하게 되는데, 이때 DB에 INSERT 등의 행위가 있으면 중복 행 삽입의 문제가 발생한다.
-> 이런 경우에는 redirect 방식 응답을 해야 한다.
* 예제
<!--index.jsp-->
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>Forward</title>
</head>
<body>
<h1>forward</h1>
<form action="forward" method="post">
<table>
<tr>
<td>아이디 : </td>
<td><input type="text" name="userId"></td>
</tr>
<tr>
<td>비밀번호 : </td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="로그인"></td>
</tr>
</table>
</form>
</body>
</html>
@WebServlet("/print")
public class PrintLoginSuccessServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// forward 받은 서블릿에서 속성값 꺼내기
String username = (String) req.getAttribute("userName");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<h1>" + username + "님 환영합니다." + "</h1>");
out.flush();
out.close();
}
}
@WebServlet("/forward") // 이 서블릿을 '/forward'에 매핑한다.
// /forward로 요청이 오면 이 서블릿의 doPost 메소드가 호출된다.
public class ReceiveInformationServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String userId = req.getParameter("userId");
String password = req.getParameter("password");
// id와 pwd에 해당하는 ;user의 정보를 select 하고 오는 비즈니스 로직이 수행되어야 한다.
// 해당 로직이 정상 수행되었다는 가정 하에 'XXX님 환영합니다' 와 같은 메세지 출력 화면을 응답한다.
// attribute도 map 형식의 일종으로, key-value 방식으로 값을 저장할 수 있다.
req.setAttribute("userName", "홍길동");
/* <다른 서블릿으로 forward>
print라는 URL 패턴으로 요청을 forward하며 이 URL 패턴에 매핑된 서블릿이 요청을 처리한다.
이 예제에서는 PrintLoginSuccessServlet아 '/print'에 매핑되어 있으므로
이 서블릿에서 포워딩된 요청이 처리된다.
RequestDispatcher는 서블릿 위임 시 어디로 보낼 지 결정하는 역할을 한다.
*/
RequestDispatcher rd = req.getRequestDispatcher("print"); // print는 printLoginSuccessServlet에서 WebServlet에 매핑된 거?
rd.forward(req, resp);
}
}
3. cf) 405 error
: 서버에서 제공하지 않는 방식으로 클라이언트가 요청을 했을 때 발생
- ex. 서버에서는 doGet만 오버라이딩 했는데 post 요청을 보낼 때
Redirect
1. redirect()의 역할 및 특징
- 클라이언트 브라우저에게 "(매개변수로 등록한) 페이지를 재요청하라"고 응답한다. (응답 상태 코드: 301, 302)
- encodeRedirectURL은 매개변수(URL)에 Session ID 정보를 추가하여 재요청 처리한다.
- 클라이언트가 별도로 다른 페이지 요청을 하지 않아도 URL 주소(페이지)가 변경된다. (브라우저 요청에 따라 서버가 알아서 해당 페이지를 요청하며, 쿼리스트링으로 별도의 데이터를 전송하지 않으면 요청 데이터가 없다.)
- 이전 요청에 포함된 정보는 남아있지 않고 URL이 변경된다.
- 첫 요청 시의 request와 redirect된 페이지의 request는 서로 다른 객체이므로, redirect를 사용하면 이전 서블릿의 request 객체 속성 값을 공유해서 사용할 수 없다. -> 이를 해결하기 위해 쿠키 및 세션 객체를 활용한다.
- 301/302 응답 코드인 경우 요청에 대한 처리를 완료하였고, 사용자의 URL을 강제로 redirect 경로로 이동시키라는 의미이다.
- redirect 요청은 무조건 get 방식이다. (주소를 변경하라는 요청이기 때문)
2. redirect()의 구조
1) 서버(톰캣)를 실행하면 내부 서블릿 컨테이너에 작성한 서블릿이 다 올라가고, doGet, doPost 등 매핑된 url로 연결된다.
2) HTTP에 의한 요청을 전달하면 헤더의 문자열을 파싱해서 헤더, 데이터, 응답 대상 브라우저 등을 request와 response 객체로 쪼개어 doGet()/doPost() 메소드로 보낸다.
3) 이때 요청받은 서블릿에서 재요청할 URL를 담아 sendRedirect로 응답하면 처리하는 서블릿으로 브라우저가 재요청을 보내도록 한다. (301/302 코드를 보냄으로써 요청 URL을 바꿔 다시 요청하라는 의미를 전달한다.)
* 예제
<!--index.jsp-->
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>JSP - Hello World</title>
</head>
<body>
<h1>redirect</h1>
<ul>
<li><a href="othersite">다른 웹 사이트로 redirect 테스트</a></li>
<li><a href="otherservlet">다른 서블릿으로 redirect 테스트</a></li>
</ul>
</body>
</html>
@WebServlet("/othersite")
public class OtherSiteRedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("get 요청 후 naver site로 redirect");
resp.sendRedirect("https://www.naver.com");
}
}
@WebServlet("/otherservlet")
public class OtherServletRedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("get 요청 정상 수락");
// 비즈니스 로직 처리 후
resp.sendRedirect("redirect");
}
}
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("이 servlet으로 redirect 완료!");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<h1>이 서블릿으로 redirect 완료!");
out.flush();
out.close();
}
}
forward vs redirect
1. 구조 비교
- forward(): 서버 내부에서 요청을 동일한 파라미터 객체로 위임하여 응답하고, 마치 요청받은 서블릿이 응답한 것처럼 보여준다.
- redirect(): 요청한 서블릿이 301/302 status로 응답을 보내주고, 브라우저가 응답받은 방향으로 다시 요청을 보내 페이지를 반환받는 구조이다.
2. CRUD 로직에서의 활용
(1) forward()
- 새로고침을 반복할 때마다 동일한 요청이 반복되어 주로 조회 기능에 사용한다.
- 서버 내부에서 데이터를 전달하므로 select 처리한 조회 값 데이터가 많으면 한 번에 많은 값을 전달할 수 있다.
ex. 사용자가 DB에서 여러 레코드를 조회하고 이를 JSP 페이지에서 표시하는 경우 유용
- 새로고침하면 재조회해서 추가/삭제된 데이터를 반영해서 조회할 수 있다.
(2) redirect()
- 새로고침을 하면 재요청된 페이지에 대한 요청이 반복되어 주로 삽입, 수정, 삭제 기능에 사용한다. 해당기능이 중복 수행되지 않도록 하는게 바람직하다.
- cf. 로그인 기능은 select 기능이지만, redirect 방식을 활용해 로그인 횟수 제한 등을 설정하기도 한다.
ㄴ> 아래는 ChatGPT를 이용해 어떤 식으로 redirect 방식을 활용해 로그인 횟수 제한을 설정할 수 있는지 알아보았다.
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
private static final int MAX_LOGIN_ATTEMPTS = 3; // 최대 로그인 시도 횟수
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String userId = req.getParameter("userId");
String password = req.getParameter("password");
// 사용자 인증 로직
boolean isAuthenticated = authenticateUser(userId, password);
// 세션에서 로그인 시도 횟수 조회
HttpSession session = req.getSession();
Integer loginAttempts = (Integer) session.getAttribute("loginAttempts");
if (loginAttempts == null) {
loginAttempts = 0;
}
if (isAuthenticated) {
// 로그인 성공
session.setAttribute("user", userId);
session.removeAttribute("loginAttempts"); // 로그인 성공 시 시도 횟수 초기화
resp.sendRedirect("/welcomePage"); // 로그인 성공 후 리디렉션
} else {
// 로그인 실패
loginAttempts++;
session.setAttribute("loginAttempts", loginAttempts);
if (loginAttempts >= MAX_LOGIN_ATTEMPTS) {
// 최대 시도 횟수 초과
resp.sendRedirect("/lockedPage"); // 계정 잠금 페이지로 리디렉션
} else {
// 로그인 실패 시 로그인 페이지로 리디렉션 및 오류 메시지 전달
resp.sendRedirect("/loginPage?error=Invalid credentials");
}
}
}
private boolean authenticateUser(String userId, String password) {
// 사용자 인증 로직 구현
return false; // 단순 예시로 인증 실패 처리
}
}
객체별 공유 데이터 설정하기
- 공유 데이터는 Map 형식의 key-value 방식으로 저장되는데, 이런 공유 데이터를 포함한 ServletContext, ServletRequest, HttpSession에서 사용하는 메소드는 아래 표와 같다.
메소드 | 내용 |
setAttribute(String,Object) | 공유 데이터 저장 |
getAttribute(String) | 공유 데이터 가져옴 |
getAttributeNames() | 공유 데이터 전체의 명칭 가져옴 |
removeAttribute(String) | 공유 데이터 자체를 삭제 |