상세 컨텐츠

본문 제목

Jakarta Struts 강좌 8 - Struts 프레임워크에 기반한 애플리케이션 개발 전략

프로그래밍/JAVA

by 라제폰 2009. 3. 6. 19:25

본문

Struts 프레임워크에 기반한 애플리케이션 개발 전략

스트럿츠 프레임워크에 기반한 애플리케이션 개발시 개발 생산성을 높이는 방법

내가 쓴 "스트럿츠 프레임워크 워크북"에서는 스트럿츠 프레임워크의 개발 생산성을 높이는 위한 방법들을 툴의 사용이나 Xdoclet의 사용을 통하여 해결하도록 이야기하고 있다. 그러나 기술의 발전과 다양한 시도로 지금 시점에서는 다른 방법을 제시하고자 한다.

스트럿츠 기반한 애플리케이션을 애플리케이션 서버에 배포하지 않은 상태에서 테스트가 가능하도록 지원하고 있는 Struts Test Case를 이용할 것을 권한다. 이것을 이용할 경우 struts-config.xml, Action, ActionForm, Validation등 스트럿츠를 이용할 경우 테스트해야하는 거의 대부분을 애플리케이션 서버에 배포하지 않은 상태에서 테스트하는 것이 가능하다.

최근에는 좋은 툴도 많지만 스트럿츠를 깊이 있게 사용해보지 않은 개발자들은 Struts Test Case를 이용하는 것이 스트럿츠를 깊이 있게 이해할 수 있고, 문제가 발생할 경우 문제해결 능력을 키울 수 있을 것으로 생각한다. 스트럿츠에 대한 경험이 많은 개발자라 할지라도 Struts Test Case를 사용할 경우 개발 생산성의 향상을 가져올 수 있다.

Introduction

스트럿츠 프레임워크를 이용할 경우 많은 개발자들의 한결 같은 불만중의 하나가 테스트하기 어렵다는 것이다. struts-config.xml에서 시시각각으로 바뀌는 설정 정보들이 정상적으로 작동하는지 일일이 테스트 하는 것 또한 여간 어려운 작업이 아니다. 더불어 struts-config.xml이 바뀔 때마다 서버를 재시작하는 반복적인 작업은 개발자들의 효율성과 생산성을 급격하게 저하시키기에 충분하다.

이 문서에서는 Mock Object, Cactus와 비슷한 성격을 가지는 테스트 프레임워크를 이용하여 개발 생산성을 증가시킬 수 있는 방법에 대하여 살펴볼 것이다.

스트럿츠 프레임워크 테스트를 위한 환경 세팅

  • http://strutstestcase.sourceforge.net/ 사이트에서 가장 최근에 배포한 Struts Test Case 바이너리 파일을 다운받는다. Struts Test Case는 Servlet 버전에 따라 다른 배포 파일을 가지므로 현재 자신이 개발하고 있는 환경의 Struts Test Case 바이너리 파일을 다운받는다.
  • 이 문서에 첨부되어 있는 파일을 다운 받으면 스트럿츠 프레임워크를 테스트하기 위하여 필요한 모든 jar 파일을 포함하고 있다. 그러나 개발자들이 자신만의 환경 세팅을 하고자 한다면 http://strutstestcase.sourceforge.net/에서 직접 파일을 다운받아 간단한 예제를 만들어 보는 것도 좋은 학습 효과를 발휘할 수 있을 것으로 생각한다.

스트럿츠 사용을 위한 Business, Persistence Layer 개발

이 문서의 주목적은 스트럿츠 프레임워크를 테스트하는데 있으므로 Business, Persistence Layer는 최대한 단순화시켰다. 또한 테스트의 편의성을 위해서 데이터베이스를 사용하지 않고 DAO 클래스에서 필요한 데이터들을 하드코딩 했다.
스트럿츠는 Business, Persistence Layer가 개발되어 있다는 가정하에서 개발되는 것이기 때문에 Business, Persistence Layer를 먼저 구현하였다.

User.java
package net.javajigi.user; /** * 사용자 관리를 위하여 필요한 도메인 클래스. * USERINFO 테이블의 각 칼럼에 해당하는 setter와 getter를 가진다. */ public class User extends BaseObject { private String userId = null; private String password = null; private String name = null; private String email = null; public String getEmail() { return email; } public String getName() { return name; } public String getPassword() { return password; } public String getUserId() { return userId; } public void setEmail(String string) { email = string; } public void setName(String string) { name = string; } public void setPassword(String string) { password = string; } public void setUserId(String string) { userId = string; } /** * 비밀번호가 일치하는지 여부를 결정하는 메써드. */ public boolean isMatchPassword(String inputPassword){ if ( getPassword().equals(inputPassword)){ return true; } else { return false; } } }
UserDAO.java
package net.javajigi.user; import org.apache.log4j.Logger; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; /** * 사용자 관리에서 데이터베이스와의 작업을 전담하는 클래스. * UserInfo 테이블에 사용자를 추가, 수정, 삭제, 검색등의 작업을 한다. */ public class UserDAO { /** * Logger for this class */ private static final Logger logger = Logger.getLogger(UserDAO.class); /** * 사용자 관리 테이블에 새로운 사용자 생성. */ public int create(User user) throws SQLException { if (logger.isDebugEnabled()) { logger.debug("userId : " + user.getUserId()); logger.debug("password : " + user.getPassword()); logger.debug("email : " + user.getEmail()); logger.debug("name : " + user.getName()); } return 0; } /** * 기존의 사용자 사용자 정보를 수정. */ public int update(User user) throws SQLException { if (logger.isDebugEnabled()) { logger.debug("userId : " + user.getUserId()); logger.debug("password : " + user.getPassword()); logger.debug("email : " + user.getEmail()); logger.debug("name : " + user.getName()); } return 0; } /** * 사용자 아이디에 해당하는 사용자를 삭제. */ public int remove(String userId) throws SQLException { if (logger.isDebugEnabled()) { logger.debug("userId : " + userId); logger.debug(userId + " removed in this systemh2."); } return 0; } /** * 사용자 아이디 정보를 데이터베이스에서 찾아 User 도메인 클래스에 저장하여 반환. */ public User findUser(String userId) throws SQLException { if (logger.isDebugEnabled()) { logger.debug("userId : " + userId); } User user = new User(); user.setUserId(userId); user.setPassword("password"); user.setName("박재성"); user.setEmail("javajigi@gmail.com"); return user; } /** * 사용자 리스트를 만들기 위한 부분으로 현재 페이지와 페이지당 카운트수를 이용하여 * 해당부분의 사용자만을 List콜렉션에 저장하여 반환. */ public List findUserList() throws SQLException { List userList = new ArrayList(); User user = new User(); user.setUserId("JavaJiGi"); user.setPassword("password"); user.setName("박재성"); user.setEmail("javajigi@gmail.com"); userList.add(user); User user2 = new User(); user2.setUserId("SanJiGi"); user2.setPassword("password"); user2.setName("박재성"); user2.setEmail("sanjigi@hotmail.com"); userList.add(user2); return userList; } /** * 인자로 전달되는 아이디를 가지는 사용자가 존재하는지의 유무를 판별. */ public boolean existedUser(String userId) throws SQLException { if (userId.equals("JavaJiGi")) { return false; } else { return true; } } }

데이터베이스에 종속적이지 않은 상태에서 테스트가 가능하도록 하기 위하여 DAO 클래스에서 데이터를 하드 코딩해 놓았다. 이번 문서에서 주목할 부분은 스트럿츠 프레임워크를 어떻게 테스트하느냐가 중요하기 때문에 DAO 클래스에서 전달되는 데이터는 중요하지 않다.

UserManager.java
package net.javajigi.user; import java.sql.SQLException; import java.util.List; /** * 사용자 관리 API를 사용하는 개발자들이 직접 접근하게 되는 클래스. * UserDAO를 이용하여 데이터베이스에 데이터 조작 작업이 가능하도록 하며, * 데이터베이스의 데이터들을 이용하여 비지니스 로직을 수행하는 역할을 한다. * 비지니스 로직이 복잡한 경우에는 비지니스 로직만을 전담하는클래스를 별도로 둘 수 있다. */ public class UserManager { private UserManager() { } public static UserManager instance() { return (new UserManager()); } public int create(User user) throws SQLException, ExistedUserException { if (getUserDAO().existedUser(user.getUserId())) { throw new ExistedUserException(user.getUserId() + "는 존재하는 아이디입니다."); } return getUserDAO().create(user); } public int update(User user) throws SQLException { return getUserDAO().update(user); } public int remove(String userId) throws SQLException { return getUserDAO().remove(userId); } public User findUser(String userId) throws SQLException, UserNotFoundException { User user = getUserDAO().findUser(userId); if (user == null) { throw new UserNotFoundException(userId + "는 존재하지 않는 아이디입니다."); } return user; } public List findUserList() throws SQLException { return getUserDAO().findUserList(); } public boolean login(String userId, String password) throws SQLException, UserNotFoundException, PasswordMismatchException { User user = findUser(userId); if (!user.isMatchPassword(password)) { throw new PasswordMismatchException("비밀번호가 일치하지 않습니다."); } return true; } private UserDAO getUserDAO() { return new UserDAO(); } }

페이지 흐름에 대한 분석과 테스트 케이스 클래스 작성

스트럿츠 프레임워크를 이용하여 View와 Control을 구현하기에 앞서 페이지의 흐름을 분석하여 간단하게 그릴 필요성이 있다. 이 예제는 상당히 단순하기 때문에 굳이 페이지 흐름을 분석하고 도식화할 필요가 없지만 복잡한 상태관리와 효율적인 페이지 흐름을 분석하기 위해서는 손쉽게 아무 종이에나 흐름을 도식화하는 것이 좋다.

페이지 흐름을 철저하게 분석하면 사용자의 편의성을 도모하고 개발의 효율성을 가져올 수 있다. 필자 또한 페이지를 개발할 때 주먹구구식으로(생각나는데로) 개발하는 경우가 많지만 직접 구현에 들어가기에 앞서 페이지 흐름과 상태들이 관리되는 과정을 도식화하는 과정이 필요하다. 특히나 스트럿츠 같은 프레임워크를 사용할 경우에는 이 같은 페이지 흐름도가 있다면 struts-config.xml을 만드는데도 많은 도움이 될 것이다.

이 문서에서는 테스트 주도 개발(TDD)에 근거하여 먼저 페이지 흐름과 각 페이지로부터 전달되는 데이터를 전달할 수 있도록하는 테스트 케이스 클래스를 만든다.

TDD에서 가장 먼저 만들어야 하는 것이 테스트 케이스 클래스이다. 그렇듯이 이 문서에서도 스트럿츠 프레임워크의 struts-config.xml과 Action 클래스를 테스트하기 위한 테스트 케이스 클래스를 먼저 생성해야 한다.

이 문서를 작성하기 위하여 TDD를 수행한 과정은 다음과 같다.

  • 테스트 케이스 클래스를 만든다.
  • 컴파일 후 테스트 케이스를 실행한다. 에러가 나는 것을 확인한다.
  • 테스트 에러를 없애기 위하여 struts-config.xml에 필요한 정보를 설정한다. 다시 테스트 실행한다.
  • Action 클래스가 없기 때문에 struts-config.xml만 정상적일지라도 에러가 발생한다. 그러므로 Action 클래스가 없다는 에러 메세지가 보일 때 Action 클래스를 작성한다.
  • 빨간 막대가 보이지 않을 때까지 Action 클래스를 수정하고 테스트하는 과정을 반복한다.
  • 마지막으로 녹색 막대를 보면서 Business Layer와의 통합을 시도하고 에러처리를 진행한다.
UserActionTest.java
package net.javajigi.web.action; import servletunit.struts.MockStrutsTestCase; /** * @author Administrator */ public class UserActionTest extends MockStrutsTestCase { private void createUserActionForm(String userId) { addRequestParameter("userId", userId); addRequestParameter("password", "password"); addRequestParameter("email", "javajigi@gmail.com"); addRequestParameter("name", "박재성"); } public void testAddUser() { setRequestPathInfo("/user_add"); createUserActionForm("JavaJiGi"); actionPerform(); verifyForward("user_list"); verifyNoActionErrors(); } public void testAddExistedUser() { setRequestPathInfo("/user_add"); createUserActionForm("SanJiGi"); actionPerform(); verifyForwardPath("/user_add.jsp"); verifyActionErrors(new String[] { "errors.user.existed" }); }

UserActionTest 테스트 케이스는 새로운 사용자를 추가하는 과정을 다루고 있다. Struts TestCase에서 제공하는 MockStrutsTestCase를 상속하여 구현을 시작한다. TestCase의 작성 규칙은 일반적으로 Junit을 이용할 경우와 같은 방법으로 처리하는 것이 가능하다. 위 예제에서는 사용하지 않았지만 setUp과 tearDown 메써드 또한 사용하는 것이 가능하다.

먼저 UserActionTest 클래스를 간략하게 살펴보자. 스트럿츠를 테스트하기 위해서는 먼저 테스트할 path가 필요하다. 이 path는 struts-config.xml에 설정된 path를 사용하게 된다. 위 예제에서는 user_add로 접근할 때를 테스트하는 것이 된다. 실 url은 보통 http://localhost:8080/ApplicationName/user_add.do가 될 것이다. 그 다음은 Action 클래스에 인자로 전달할 값들을 지정한다. 새로운 사용자를 추가하기 위해서 우리는 UserForm 클래스를 사용한다. 위 예제의 createUserActionForm을 통하여 전달된 데이터는 UserForm에 추가되어 Action 클래스에 전달되게 된다(9-14라인). 즉, 이렇게 전달되는 값이 우리들이 JSP에서 반복해서 입력해야 하는 값이다. 그러나 위 예제에서는 한번 테스트 데이터를 생성하면 다음부터는 반복적으로 사용하는 것이 가능하기 때문에 재사용성이 가능하다.

테스트는 추가하고자하는 사용자가 이미 존재할 경우와 그렇지 않을 경우로 나누어 테스트를 진행했다. 이미 존재하지 않는 사용자를 추가할 경우에는 에러 없이 처리가 되기 때문에 정상적인 흐름을 통해 처리되는지를 확인해 볼 수 있다. 또한 Action 클래스에서 모든 작업을 완료 후 다음으로 이동할 페이지에 대해서도 확인해 보는 것이 가능하다(22-23 라인).

testAddExistedUser 메써드는 사용자가 이미 존재할 경우 에러가 정상적으로 발생하는지를 확인하는 것이 가능하다(32-33라인).

사용자가 존재하는지에 대한 판단은 앞에서 살펴본 UserDAO 클래스의 existedUser() 메써드를 통해서 확인할 수 있다. 아이디가 JavaJiGi일 경우에는 존재하지 않는 사용자로 판단하고 그렇지 않을 경우 존재하는 사용자로 판단하도록 하드코딩 되어 있다.

물론 처음부터 위와 같은 테스트 케이스를 만드는 것이 힘들다. 첫 단계에서는 path와 전달해야할 인자만을 가지고 테스트를 진행해 가면서 struts-config.xml과 Action 클래스에 살을 붙여가는 방법으로 구현을 진행하는 것이 바람직하다. 물론 이미 구현되어 있는 struts-config.xml과 Action 클래스에 대해서는 위와 같은 테스트 케이스를 한번에 만드는 것이 가능할 것이다. 그러나 처음부터 개발하는 단계라면 한가지씩 추가해나가면서 점진적으로 완성해 가는 것이 좋다.

아래 예제에서는 앞에서 작성한 테스트 케이스 기반하에서 struts-config.xml와 InsertAction 클래스를 어떻게 작성했는지 확인해 볼 수 있다.

struts-config.xml 파일 생성

_struts-config.xml_

struts-config.xml
<form-beans> <form-bean name="userForm" type="net.javajigi.web.form.UserForm" /> </form-beans> <action path="/user_add" type="net.javajigi.web.action.InsertAction" name="userForm" scope="request" input="/user_add.jsp" unknown="false" validate="true" > <forward name="user_list" path="/user_list.do" redirect="true" /> </action> <message-resources parameter="net.javajigi.resources.MessageResources"/>
MessageResources.properties
# Errors errors.user.existed=User already exist. errors.password.mismatch=User password mismath. # Button user.registration=Registration user.list=List # Messages user.write.title=User Write Page Titleh2.

Action 클래스 작성

package net.javajigi.web.action; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.javajigi.user.ExistedUserException; import net.javajigi.user.User; import net.javajigi.user.UserManager; import net.javajigi.web.form.UserForm; import org.apache.commons.beanutils.PropertyUtils; import org.apache.log4j.Logger; 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; import org.apache.struts.action.ActionMessages; public class InsertAction extends Action { /** * Logger for this class */ private static final Logger logger = Logger.getLogger(InsertAction.class); /** * request에 저장되어 있는 인자값으로 User객체를 생성하여 * UserManager의 create메써드를 호출하여 새로운 게시물을 입력한다. */ public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { //UserForm클래스를 얻는다.  UserForm userForm = (UserForm) form; User user = new User(); //UserForm에 저장되어 있는 사용자 정보를 User클래스에 복사한다.  PropertyUtils.copyProperties(user, userForm); //모델을 이용하여 새로운 사용자를 생성.  UserManager manager = UserManager.instance(); ActionErrors errors = new ActionErrors(); try { manager.create(user); } catch (ExistedUserException e) { logger.error(e); errors.add(ActionMessages.GLOBAL_MESSAGE, new ActionError( "errors.user.existed")); } if (!errors.isEmpty()) { this.saveErrors(request, errors); return mapping.getInputForward(); } return mapping.findForward("user_list"); } }


관련글 더보기