[펌] saveToken/ resetToken

프로그래밍/Framework 2007. 11. 28. 10:07 Posted by galad

펌: http://wiki.javajigi.net/pages/viewpage.action?pageId=77

Jakarta Struts 강좌 5 - RequestDispatcher를 이용할 경우 문제점과 해결방법

Summary : 지금까지 스트러츠의 전체적인 부분에 대하여 다루어왔다. 이번 강좌부터는 스트러츠의 세부항목들에 대하여 살펴보면서 스트러츠를 좀 더 효율적으로 활용해보도록 한다. 이번 강좌에서는 HttpServletResponse의 sendRedirect와 RequestDispatcher의 차이점에 대하여 살펴본다. RequestDispatcher를 사용할 경우 장점도 있지만 발생하는 문제점이 있다. 스트러츠를 활용하여 RequestDispatcher을 이용할 경우 발생하는 문제점을 해결하는 방법에 대하여 살펴본다.

HttpServletResponse의 sendRedirect와 RequestDispatcher의 차이점

JSP를 이용하여 프로그램할 경우 특정작업을 완료한 후 다음 페이지로 이동할 때 흔히 HttpServletResponse의 sendRedirect()를 이용한다. HttpServletResponse의 sendRedirect()는 두번의 Request를 통해 하나의 작업을 완료하는 방식이다. 즉, 첫번째 요청이 서버에서 처리된 다음 서버는 이동할 다음 페이지의 경로를 클라이언트에서 응답하면 클라이언트의 브라우저는 서버에게 새로운 페이지를 다시 요청하여 이동하는 방식이다. 이와 같이 할 경우 두번의 네크? Traffic이 발생하기 때문에 실행속도에 늦어질 수 밖에 없다. 또한 새로운 요청을 하기 때문에 요청하는 페이지에 객체를 전달하기 힘들다는 문제점이 있다.

이 같은 문제점을 해결하기 위하여 RequestDispatcher가 탄생하게 되었다. RequestDispatcher는 일련의 작업이 완료된 후 다음페이지로 이동할 경우 클라이언트에 응답을 한 후 다시 요청하도록 하는 방식이 아니라 서버에서 서버로 페이지를 요청하여 실행한 후 응답을 하도록 한다. 이럴 경우 네트? Traffic이 한번 발생하게 되며, 서버에서 서버로 페이지를 요청하기 때문에 String만이 아닌 객체의 전달도 가능하다.

다음 두개의 그림을 보면 두가지 방식의 차이점을 명확히 알 수 있을 것이다.

RequestDispatcher를 이용할 경우 한번의 요청으로 인해 모든 작업이 서버에서 처리되기 때문에 실행속도는 Response의 sendRedirect를 사용할 때보다 빠르다. 또한 Response의 sendRedirect을 이용할 경우에는 String밖에 전달 할 수 없다. 즉, 객체를 다른 페이지에 전달하는 것이 힘들다. 물론 세션을 이용하면 가능하지만 세션 또한 시스템의 메모리를 차지하기 때문에 매번 세션을 이용하는 것은 좋은 방법은 아니다.

이에 대한 대안으로 RequestDispatcher를 이용하면 된다. RequestDispatcher를 이용하면 서버에서 모든 작업이 처리되기 때문에 서버상에서의 객체의 전달이 가능하다. 하지만 RequestDispatcher를 이용할 경우 문제점이 있다. RequestDispatcher를 이용할 경우 응답이 이루어진 후의 URL이 처음 클라이언트가 요청한 URL로 지정되어 있기 때문에 문제가 되는 경우가 있다.

한가지 예로 사용자의 정보를 가져오거나 사용자의 정보를 볼 경우에는 문제가 되지 않는다. 하지만 사용자가 회원가입을 할 경우 똑같은 정보가 같은 페이지에 요청되기 때문에 같은 정보가 저장되는 경우가 있다.

개발자들이 게시판을 이용할 때 요청속도가 느려 Refresh를 계속해서 누를 경우 똑같은 글이 계속해서 저장되는 것을 경험한 적이 있을 것이다. RequestDispatcher를 이용할 경우에는 같은 문제가 발생하는 경우가 많다.

따라서 스트러츠에서는 이 문제점을 해결하기 위하여 자체적으로 해결방법을 제공하고 있다. 스트러츠에서 이 문제를 해결하는 방법에 대하여 살펴보자.

스트러츠 프레임워크에서 RequestDispatcher의 문제점 해결하는 방법.

RequestDispatcher의 문제점을 해결하기 위해서 스트러츠에서는 해결점을 제공하고 있다. Action클래스의 saveToken()과 resetToken()을 이용하여 요청(Request)이 한번만 실행할 수 있도록 지원하고 있다. 간단하게 요약하면 saveToken()을 이용하여 요청이 실행될 때 하나의 Token을 생성한다. 만약 앞에서 생성한 똑같은 Token의 요청이 실행되면 스트러츠에서는 에러를 발생시켜 요청이 실행되지 않도록 한다.

지난번 강좌에 이어 스트러츠를 이용하여 사용자를 관리하는 예제를 완성하면서 RequestDispatcher에서 발생하는 문제점을 해결하는 방법에 대하여 살펴본다. 단 데이터베이스를 사용하지 않고 예제를 단순화하기 위하여 콜렉션을 Static으로 선언하여 데이터베이스의 역할을 하도록 했다.

지금부터 스트러츠를 이용하여 사용자 관리를 하는 방법에 대하여 살펴보도록 하겠다.

package net.javajigi.mall.user;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class UserDatabase {
	//데이터베이스 대신 사용자 정보를 저장하고 있는 콜렉션을 선언하는 부분이다.
	protected static List users = new ArrayList();
	
	public static List getUsers() {
		return users;
	}
	
	public static UserVO getUser(String id) {
		Iterator userIter = users.iterator();
		
		while ( userIter.hasNext() ) {
			UserVO user = (UserVO)userIter.next();
			
			if ( id.equals(user.getId()) ) {
				return user;
			}
			
		}
		
		return null;
	}
}

데이터베이스 대신 사용자 정보를 저장하는 역할을 하는 클래스이다. 콜렉션을 Static으로 선언하여 메모리상에 사용자 정보를 저장하고 있다. 예제를 단순화하기 위하여 이처럼 처리했다. 다음 강좌에서는 이 부분을 데이터베이스를 이용하여 처리하도록 변경될 것이다.

다음에 보게될 클래스들은 MVC에서 model에 해당하는 부분을 처리하게 될 클래스들이다. 데이터베이스로 변경되더라도 다음 클래스만 변경하면 다른 부분에 대한 변경은 없어도 된다.

package net.javajigi.mall.user;

public class UserTO {
	private String id = null;
	private String password = null;
	private String name = null;
	private String email = null;
	
	public String getEmail() {
		return email;
	}

	public String getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	public String getPassword() {
		return password;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public void setId(String id) {
		this.id = id;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setPassword(String password) {
		this.password = password;
	}
}
package net.javajigi.mall.user;

public class UserVO {
	private String id = null;
	private String password = null;
	private String name = null;
	private String email = null;
	
	public UserVO(String id, String password, String name, String email){
		this.id = id;
		this.password = password;
		this.name = name;
		this.email = email;
	}
	
	public String getId() {
		return id;
	}

	public String getPassword() {
		return password;
	}

	public String getEmail() {
		return email;
	}

	public String getName() {
		return name;
	}

	public boolean equals(Object userVO) {
		return this.equals(userVO);
	}

	public String toString() {		
		return getId();
	}
}
package net.javajigi.mall.user;

import java.util.ArrayList;
import java.util.List;

public class UserList {
	private List users = null;
	
	public UserList() {
		users = new ArrayList();
	}

	public void addUser(UserVO userVO) {
		users.add(userVO);
	}
	
	public List getUsers() {
		return users;
	}	
}
package net.javajigi.mall.user;

import java.util.List;

public class UserDAO {

	public static UserVO getUser(String id) {
		/*
		데이터베이스에 접속하여 해당하는 User의 정보를 가져오는 코드가
		실제 Application에서는 있을 것이다.
		User의 정보를 가져와 User Value Object에 해당 정보를 저장하여 반환하는
		역할을 한다.
		
		이번 예제에서는 스트러츠에 집중하기 위하여 데이터베이스에 접속하여 데이터를
		가져오는 부분은 생략했다.
		
		id가 javajigi일 경우 UserVO를 생성하고, 
		그렇지 않을 경우 존재하지 않는 아이디로 간주하여 null을 반환하도록 
		구현했다.
		*/

		return UserDatabase.getUser(id);
	}

	public static int addUser(UserTO userTO) {
		UserVO userVO =
			new UserVO(
				userTO.getId(),
				userTO.getPassword(),
				userTO.getName(),
				userTO.getEmail());
		UserDatabase.users.add(userVO);
		
		return 1;
	}	
	
	public static List getUsers() {
		return UserDatabase.getUsers();
	}
	
	public static int updateUser(UserTO userTO) {
		UserVO userVO = getUser(userTO.getId());
		
		if ( userVO != null ) {
			int removeCount = removeUser(userTO.getId());
			
			if ( removeCount == 1) {
				return addUser(userTO);
			}		
		}
		
		return 0;
	}
	
	public static int removeUser(String id) {
		UserVO userVO = getUser(id);
		
		if ( userVO != null ) {
			UserDatabase.users.remove(userVO);
			
			return 1;
		}
		
		return 0;
	}	
}

지금까지 사용자 관리에서 Model에 해당되는 부분에 대하여 살펴보았다. 다음은 사용자 관리의 View와 Controller에 해당하는 부분에 대하여 살펴보자. 각 소스에서 설명이 필요한 부분은 주석으로 처리하였다.

메모리상에 저장된 사용자 정보가 없기 때문에 /user/login.jsp에서 회원가입을 한다. 회원가입을 하는 과정에서 RequestDispatcher에서 발생하는 문제점을 해결하는 방법에 대하여 살펴본다.

위 화면은 사용자가 로그인 하기 위한 화면이다. URL이 http://localhost:8080/zigimall/user/login.jsp임을 확인해 보기 바란다. 현재 메모리 상에 저장된 사용자가 없기 때문에 회원가입을 한 후에 로그인 과정을 진행하도록 한다.

<%@ page language="java" %>
<% response.setContentType("text/html"); %>

<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%> 
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html"%> 

<html:html locale="true">
<head>
    <title><bean:message key="login.title" /></title>
    <html:base/>
</head>
<body>
<html:errors />
<html:form action="/useradd">
    <bean:message key="prompt.id" />
    <html:text property="id" />
    <br />
    <bean:message key="prompt.password" />
    <html:password property="password" />
    <br />
    <bean:message key="prompt.name" />
    <html:text property="name" />
    <br />

    <bean:message key="prompt.email" />
    <html:text property="email" />
    <br />

	<html:submit/>
	
    <html:link page="/user/login.jsp">
        <bean:message key="useradd.cancel" />
    </html:link>  	
</html:form>
<body>
</html:html>
package net.javajigi.mall.user.action;

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

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

public class UseraddformAction extends Action {

 	public ActionForward execute(
		ActionMapping mapping,
		ActionForm form,
		HttpServletRequest request,
		HttpServletResponse response)
		throws Exception {
			
		//회원가입 Form으로 이동할 때 request에 새로운 Token을 생성하여 저장한다.
		//여기서 생성된 Token은 회원가입을 완료할 때 요청한 Request가 처음 요청한 Request인지
		//같은 Request가 요청되었는지를 확인하는데 사용된다.
		saveToken(request);
		
		return (mapping.findForward("useraddForm"));
	}
}
package net.javajigi.mall.user.action;

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

import net.javajigi.mall.user.form.UserAddForm;
import net.javajigi.mall.user.UserDAO;
import net.javajigi.mall.user.UserTO;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionError;
import org.apache.struts.action.ActionErrors;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

public class UserAddAction extends Action {
	public ActionForward execute(
		ActionMapping mapping,
		ActionForm form,
		HttpServletRequest request,
		HttpServletResponse response)
		throws Exception {
		UserAddForm useraddForm = (UserAddForm) form;

		ActionErrors errors = new ActionErrors();
		
		//요청된 Request의 Token이 유효한지를 처리하는 부분이다.
		//Token이 유효하지 않을 경우 ActionErrors에 에러를 저장한다.
		//따라서 같은 요청이 재요청될 경우 에러를 발생시키기 때문에 같은 요청이 
		//두번 반복해서 처리되지는 않는다.
        if (!isTokenValid(request)) {
            errors.add(ActionErrors.GLOBAL_ERROR,
                       new ActionError("error.transaction.token"));
        }
       	resetToken(request);		
		        
		if (!errors.isEmpty()) {
		    saveErrors(request, errors);
 	        saveToken(request);

	        return (mapping.getInputForward());
		}

		UserTO userTO = new UserTO();
		userTO.setId(useraddForm.getId());
		userTO.setPassword(useraddForm.getPassword());
		userTO.setName(useraddForm.getName());
		userTO.setEmail(useraddForm.getEmail());
				
		UserDAO.addUser(userTO);
		
		useraddForm.reset(mapping, request);
		
		return(mapping.findForward("loginForm"));
	}
}

회원가입이 완료된 후에 나타나는 로그인 화면이다. 앞의 login.jsp화면과 달라진 부분은 URL이 달라졌음을 알 수 있다. 이 URL은 사용자가 회원가입을 완료하기 위하여 요청한 URL이다. 즉, RequestDispatcher를 이용할 경우 첫번째 요청에 대한 URL이 바뀌지 않는다는 문제점이 있다. 따라서 여기서 위 화면에서 새로고침을 할 경우 똑같은 요청이 재요청된다는 문제점이 있다.

스트러츠에서 RequestDispatcher을 이용하기 위해서는 struts-config.xml에서 설정하면 된다.

struts-config.xml의 forward태그에 redirect attribute를 이용하면 된다. redirect의 Default값은 false이다. 따라서 만약 Response의 sendRedirect를 이용하고자 한다면 redirect를 true로 설정해 주어야 한다.

회원가입의 forward에서 와 같이 설정하였기 때문에 RequestDispatcher를 이용하고 있다.

위 화면에서 새로고침을 계속해서 누른다면 같은 사용자가 계속해서 저장되는 문제점이 있다. 하지만 위와 같이 처리할 경우 다음 화면과 같이 에러화면을 처리하면서 새로운 요청이 처리되지 않음을 볼 수 있다.

// Created by Xslt generator for Eclipse.
// XSL :  not found (java.io.FileNotFoundException:  (지정된 경로를 찾을 수 없습니다))
// Default XSL used : easystruts.jar$org.easystruts.xslgen.JavaClass.xsl

package net.javajigi.mall.user.action;

import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.javajigi.mall.user.UserDAO;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

public class UserListAction extends Action {

	public ActionForward execute(
		ActionMapping mapping,
		ActionForm form,
		HttpServletRequest request,
		HttpServletResponse response)
		throws Exception {

		List users = UserDAO.getUsers();
		
		//사용자의 정보를 얻어와 사용자의 정보를 담은 객체를 
		//request에 저장하여 전달하고 있다.
		//이것이 가능한 이유는 RequestDispatcher를 이용하기 때문이다.
		request.setAttribute("listuser", users);	
		
		return mapping.findForward("userlist");
	}
}
<%@ page language="java" %>
<% response.setContentType("text/html"); %>

<%@ page import="java.util.*"%>
<%@ page import="net.javajigi.mall.user.*"%>

<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>

<html:html locale="true">
<head>
    <title>사용자 리스트</title>
    <html:base/>
</head>

<body>
<form>
	<html:errors />
	<br/>
	<table border="1">
		<tr>
			<td><bean:message key="prompt.id" /></td>
			<td><bean:message key="prompt.name" /></td>
			<td><bean:message key="prompt.email" /></td>			
		</tr>
<%
	//UserListAction에서 전달한 List객체를 이용하여 사용자의 목록을 보여주는 부분
	List users = (List)request.getAttribute("listuser");

	for (int i=0; i < users.size(); i++) {
		UserVO userVO = (UserVO)users.get(i);
%>
		<tr>
			<td><%= userVO.getId() %></td>
			<td><%= userVO.getName() %></td>
			<td><%= userVO.getEmail() %></td>			
		</tr>
<%
	}
%>		
	</table>

    <html:link page="/useraddform.do">
        <bean:message key="user.useradd" />
    </html:link>
<form>
</body>
</html:html>