2011년 6월 9일 목요일

iBATIS in Action (14/14)

14. 모두 종합해서 보기

14.1. 설계 컨셉
애플리케이션을 작성할 때는 몇 가지 방향을 사전에 정의하고 시작하는 것이 좋다. 애플리케이션으로 무엇을 하고자 하는지 총체적으로 살펴봐야 한다. 다시 말하면 ‘요구사항’을 정리해 보는 것을 뜻한다. 장바구니는 많은 책에서 다루어졌었기 때문에 요구사항을 정리하기가 쉽다. 지겹긴 하겠지만 이번에도 역시 장바구니를 할 것이다. (이봐, 최소한 애완동물 가게는 아니라고!)
우리는 장바구니의 설계를 단순하게 유지하면서 네 가지 주요 부분에 집중할 것이다. 여기서 말하는 네 가지 부분은 계정(account), 카탈로그(catalog), 장바구니(cart) 그리고 주문(order)이다. 관리 부분에 대해서는 그냥 넘어갈 것이다. 관리 부분은 이 책에서 그ㄷ지 핵심적인 부분은 아니면서도 많은 시간을 요하고 복잡성도 증가시킨다. 이제 애플리케이션 컴포넌트 그룹들의 역할을 알아보며, 각 요구사항의 세세한 부분들을 정하자.

14.1.1 계정
계정(account)에는 사용자와 관련된 정보를 저장할 것이다. 계정은 개인의 주소와 추가정보를 포함하고 있다. 사용자는 계정을 만들고 수정할 수 있어야만 한다. 계정은 또한 고객의 로그인을 위한 보안 관련 부분을 처리한다.

14.1.2 카탈로그
우리가 코딩해야 하는 많은 부분들이 카탈로그(catalog)에 포함된다. 카테고리(category), 제품(product), 항목(item) 등을 여기서 사용할 것이다. 카테고리, 제품, 항목들은 오직 1차 레벨만 있으며, 카테고리는 제품들을 자식으로 소유한다. 그리고 제품은 항목들을 소유한다. 항목은 제품의 변종이다. 예를 들어 Action이라는 카테고리에는 Doom과 같은 게임/제품이 있을 것이다. 게임/제품에는 PC, PlayStation, Xbox 그리고 이와 유사한 변형 항목들이 있을것이다.

14.1.3 장바구니
장바구니(cart)를 통해서 사용자가 선택한 제품을 관리한다. 장바구니는 현재 장바구니에 들어있는 항목들을 기록하여 주문을 준비할 때 사용할 수 있도록 한다.

14.1.4 주문
애플리케이션의 주문(order) 영역은 계산할 때 사용한다. 고객이 모든 항목을 선택하고서 장바구니에서 선택한 항목을 구매하고자 한다면, 장바구니에서 주문을 선택한다. 장바구니는 주문할 항목들을 확인, 지불, 계산서 작성, 배송, 최종 확인의 절차를 통해 처리할 것이다. 일단 주문이 완료되면 사용자는 주문 내역에서 이를 확인할 수 있을 것이다.

14.2. 기술 선택
이제 몇 가지 측면의 요구사항을 이끌어냈으니, 이떤 기술을 사용하여 필요한 기능을 구현할지를 결정할 때가 되었다. 우리가 만들고자 하는 것이 웹 애플리케이션이기 때문에, 각 계층별로 어떤 것을 선택할 수 있는지 살펴볼 것이다. 표준 웹 애플리케이션은 몇 가지로 나눠서 볼 수 있다.
 프레젠테이션(Presentation) 계층 – 웹에 한정되는 애플리케이션의 영역
 서비스(Service) 계층 – 대부분의 비즈니스 규칙들이 있는 곳
 퍼시스턴스(Persistence) 계층 – 데이터베이스 접근에 한정된 요소들만 다루는 곳.

14.2.1 프레젠테이션
프레젠테이션 계층에서는 선택 사양이 다양하다. 가장 유명한 프레임워크로는 스트럿츠(Struts), JSF, Spring과 WebWork 등이 있다. 이 모든 프레임워크들은 각각 지지층을 확보하고 있으며 다른 것에 의존하지 않고 잘 작동하는 것으로 알려져 있다. 여기서는 이 프레임워크들 중에서 스트럿츠를 사용한다. 스트럿츠는 매우 안정적이고 범용적이며 계속해서 점진적으로 발전하고 새로운 애플리케이션이나 기존 애플리케이션 모두에 충분히 사용 가능하다. 여기서는 스트럿츠 프레임워크에 대해 적당히 이해 하고 있다고 가정할 것이다. 혹시나 그렇지 않다면 테드 N. 허스테드, 세드릭 듀몰린, 조지 프랜시스커스와 데이비드 윈터펠트의 Struts In Action 책(Manning, 2002)을 구매해서 읽어보길 권한다.

14.2.2 서비스
서비스 계층은 선택하기가 다소 간단하다. 이 책은 iBATIS에 대한 것이기 때문에 서비스 클래스 내부에서는 iBATIS DAO를 사용할 것이다. iBATIS DAO를 통해서 데이터 접근 객체를 가져와서 이를 서비스 클래스의 인스턴스 변수로 저장할 것이다. 이렇게 하면 DAO 구현체를 서비스 클래스로부터 숨겨 둘 수 있다. iBATIS DAO를 사용하여 트랜잭션을 구분하고, 세세한 메서드 호출은 퍼시스턴스 계층에 모을 것이다.

14.2.3 퍼시스턴스
퍼시스턴스 계층에서 당연히 iBATIS SQL Maps를 사용할 것이다. iBATIS SQL Maps는 SQL, 퍼시스턴스 캐시를 관리하고 데이터베이스에 대한 호출을 책임진다. 이 책이 바로 그런 주제를 다루기 때문에 여기서 다시 상세하게 다루지는 않을 것이다.

14.3. 스트럿츠 최적화하기 : BeanAction
최근 웹 애플리케이션 프레임워크들은 약간의 변화를 겪고 있다. 상태 관리, 빈즈 기반의 프레젠테이션 클래스, 고급 GUI 컴포넌트 그리고 정교한 이벤트 모델과 같은 기능이 도입되어 개발이 좀 더 쉬워졌다. 이러한 차세대 프레임워크들의 한 가운데서도 스트럿츠는 여젼히 계속해서 강력한 위용을 과시하고 있다. JGameStore 애플리케이션의 가장 훌륭한 접근 방법이 무엇일까 평가해본 결과, 스트럿츠를 사용하면서 신세대 프레임워크들과의 앞으로의 관계도 유지하기로 결정하였다. 이러한 생각으로 우리가 BeanAction이라고 이름 붙인 접근 방법을 사용하기로 결정하였다. BeanAction을 사용하면 표준 스트럿츠 애플리케이션 개발자들이 쉽게 iBATIS를 표준 스트럿츠 애플리케이션에 통합할 수 있게 된다. 동시에 JSF, Wicket, 그리고 Tapestry 같은 차세대 프레임워크를 사용하는 개발자들도 BeanAction의 의미를 이해할 수 있을 것이다. 마지막으로 스트럿츠를 변형하려 들지는 않을 것이다. 우리는 단지 우리 애플리케이션을 더 많은 사람들에게 의미 있도록 만들고자 할 뿐이다.
BeanAction은 Action과 ActionForm의 역할을 한 개의 클래스에 성공적으로 집약시킨다. 이는 또한 세션(session)이나 요청(request)같은 웹에 종속적인 컴포넌트들에 직접적으로 접근하지 않도록 추상화 시켜준다. 이런 형태의 아키텍처는 WebWork나 JSF와 유사하다. 스트럿츠의 Action 클래스를 상속한 BeanAction과 BaseBean 그리고 ActionContext라는 몇 가지 핵심 컴포넌트들을 통해서 이러한 일을 할 수 있다. 이 컴포넌트들은 BeanAction이 어떻게 작동하는지 이해하는 데 매우 중요하다. 이들은 그림에서 볼 수 있다.

그림 14 1. BeanAction 아키텍처의 UML 다이어그램

14.3.1 BaseBean
ActionContext 와 BeanAction을 보기 전에 먼저 BaseBean의 목적을 이해하고 있어야 한다. BaseBean은 스트럿츠에서 유효성 검사에 사용하는 ValidatorActionForm을 확장한다. 그래서 ActionForm을 직접 상속하는 것 대신에 BaseBean을 상속한다. BaseBean은 ActionForm이 갖고 있는 일반적인 프로퍼티를 모두 포함한다. 게다가 BaseBean은 그 자체가 ActionForm이기 때문에 스트럿츠가 ActionForm을 만드는 것과 동일한 방식으로 만들어진다. 유일한 차이점은 상속받은 BaseBean이 public String methodName()라는 간략한 시그너처를 가진 행위 메서드를 포함한다는 것이다.

14.3.2 BeanAction
퍼즐의 다음 조각은 BeanAction이다. BeanAction은 두 가지 역할을 한다. 먼저 BeanAction은 ActionContext를 생성한다. 그 다음은 상속받은 BaseBean의 행위 메서드를 호출하고 행위 메서드가 반환한 문자열을 스트럿츠의 ActionForward로 변환한다. 이 덕분에 BaseBean은 행위 메서드를 스트럿츠에 종속적인 컴포넌트들이 하나도 없는 깔끔한 상태로 유지할 수 있다. BeanAction 클래스는 상속받은 BaseBean의 행위 메서드 중에서 무엇을 호출할지 결정할 때 서로 다른 두 곳을 살펴본다. 먼저 액션 매핑에 파라미터가 지정돼 있고, 명시적으로 어떤 메서드를 호출하라고 지정하고 있는지 확인한다. 파라미터가 *로 지정돼 있다면 메서드를 호출하지 않고 success(성공) 액션 포워드를 대신 사용한다. 만약 액션 매핑 파라미터 속성이 지정돼 있지 않거나 비어 있다면 ActionBean이 경로를 확인하고 확장자를 뺀 파일 이름을 메서드 이름으로 간주해서 메서드를 호출한다. 따라서 표준 .do 매핑을 사용하고 호출 경로가 /myMethod.do로 끝났다면 BaseBean을 상속받은 클래스의 myMethod라는 행위 메서드를 호출할 것이다.

14.3.3 ActionContext
마지막으로 ActionContext는 특정 웹 관련 정보를 추상화한다. 이를 사용하여 요청(request), 파라미터(parameter), 쿠키(cookie), 세션(session), 애플리케이션(application) 등 모든 스코프를 Map 인터페이스를 통해 접근할 수 있다. 이를 통해 웹 계층에 대한 직접적인 의존성을 줄일 수 있게 된다. 대부분의 경우 ActionContext는 개발자가 스트럿츠와 Servlet API에 신경 쓰지 않아도 되게 해준다. 하지만 ActionContext는 여전히 개발자가 꼭 필요한 경우에 대비해서 HttpServletRequest와 HttpServletResponse에 직접 접근할 수도 있도록 하고 있다.
이러한 접근 방법은 여러 가지 장점을 가지고 있다. 무엇보다도 ActionForm 객체를 ActionForm을 상속받은 타입으로 형 변환하느라 시간과 코드를 낭비할 필요가 없어진다. Action이 곧 ActionForm이기 때문에 그러한 일은 발생하지 않는다. 개발자는 오로지 BaseBean을 상속받은 빈즈 객체의 프로퍼티에 직접 접근하면 된다. 둘째로 행위메서드를 호출함으로써 복잡성을 줄일 수 있다. 일반적으로는 HttpServletRequest나 HttpServletResponse, ActionForm 그리고 ActionMapping을 받는 메서드 시그너처가 필요하다. 또한 Action의 수행 메서드는 ActionForward를 반환해야 한다. BeanAction은 이런 것들을 모두 줄여서 간단하게 시그너처는 비워두고 반환 값으로는 String만 있으면 된다. 셋째로 간단한 빈즈를 단위 테스트하는 것이 ActionForm이나 Action을 단위 테스트하는 것보다 훨씬 더 쉽다. MockObjects와 StrutsTestCase를 통해서 스트럿츠의 Action 클래스를 완벽하게 테스트할 수 있다. 하지만 간단한 빈즈를 테스트하는 것이 훨씬 더 쉽다. 마지막으로 BeanAction 아키텍처는 기존 스트럿츠 애플리케이션들과도 부드럽게 작동한다. 이 때문에 기존에 열심히 작업한 것들을 없애지 않고도 기존 애플리케이션 아키텍처를 현대적인 접근 방식을 사용하도록 이전할 수 있다.

14.4. 기초 닦기
이제 개발 환경을 구축해보자. iBATIS를 사용하는 애플리케이션을 개발하기 위해서 특정 개발 도구에 초점을 맞추지는 않을 것이다. 대신 우리에게 유용했던 기반 구조를 살펴보는데 시간을 할애할 것이다. 소스 트리를 조직화하는 것은 훌륭하고 깔끔하고 간결한 코드를 작성함에 있어서 매우 중요한 부분이다. 원한다면 iBATIS JGameStore 애플리케이션의 소스를 사용해서 14장의 나머지 부분을 따라 해도 된다. 앞어서 정한 모든 요구사항을 다 다루지는 못할 것이다. 하지만 iBATIS JGameStore 애플리케이션의 소스를 살펴보면 여기서 다루지 않은 코드를 보고 어떻게 작동하는지 이해할 수 있을 것이다.
jgamestore라는 기본 폴더를 만드는 것으로 시작해보자. 이 폴더는 프로젝트 폴더가 될 것이고 소스 트리를 포함하고 있다. 여러분이 선호하는 IDE에서 이 폴더를 구성하고 프로젝트 이름을 jgamestore라고 지어도 되고 혹은 간단히 운영체제에서 수작업으로 해도 된다.
프로젝트 폴더 하위에 src, test, web, build, devlib 그리고 lib라는 폴더를 생성한다.
/jgamestore
/src
/test
/web
/build
/devlib
/lib

각각의 폴더를 좀 더 자세히 살펴보자.

14.4.1 src
src라는 이름은 source의 줄임말이다. src폴더에 모든 자바 소스 코드와 클래스패스에 위치해야만 하는 property 및 xml 파일을 저장한다. 이 파일들은 분산 애플리케이션에서 사용될 것이다. 이 폴더는 단위 테스트와 같은 테스트 코드를 포함해서는 안 된다.
모든 소스는 org.apache.ibatis.jgamestore 라는 기본 패키지에 포함될 것이다. 기본 패키지 아래의 각각의 패키지는 애플리케이션 컴포넌트를 분류한다.
하위 패키지는 다음처럼 될 것이다.
 Domain – 이 패키지에는 애플리케이션에서 투명한 객체인 DTO/POJO 클래스들이 들어간다. 이 객체들은 애플리케이션의 각 계층 사이에서 전달/사용된다.
 Persistence – 데이터 접근 인터페이스와 구현체가 SQL Map xml 파일들과 함께 위치하는 곳이다. 데이터 접근 구현체는 iBATIS SQL Maps API를 사용할 것이다.
 Presentation – 이 패키지에는 프레젠테이션 빈즈들이 들어간다. 이 클래스들은 웹 애플리케이션의 서로 다른 화면들에 관련된 프로퍼티와 행위를 포함한다.
 Service – 이 패키지에는 비즈니스 로직이 들어간다. 이 포괄적인(coarse-grained) 클래스는 퍼시스턴스 계층의 세부적인 (fine-grained) 호출들을 함께 묶는 역할을 한다.

14.4.2 test
Test 디렉터리에는 모든 단위 테스트 코드를 저장한다. 패키지 구조는 src 디렉터리의 패키지 구조와 동일할 것이다. 각각의 패키지는 단위 테스트들을 저장한다. 단위 테스트는 src 디렉터리의 동일한 패키지에 있는 클래스들을 테스트한다. 이런 식으로 하는 데는 여러 가지 이유가 있는데 대부분의 이유는 코드를 안전하고 테스트 가능하게 관리하기 위해서이다.

14.4.3 web
Web 폴더에는 JSP, 이미지, 스트럿츠 설정 그리고 그 외의 유사한 파일 등 웹에 관련된 산출물을 모두 포함할 것이다.
Web 폴더의 구조는 다음과 같다.
 JSP 디렉터리들 – account, cart, catalog, common, order 디렉터리에는 애플리케이션에서 디렉터리 이름과 관련된 부분을 위한 JSP 파일들을 저장한다. 각각의 디렉터리 명은 해당 디렉터리 안의 JSP가 무엇을 위한 것인지 단어 자체가 설명하기 때문에 별도의 설명이 필요 없다.
 Css – 이 디렉터리에는 스타일 시트(CSS)를 저장한다. 스타일 시트에 친숙하지 않다면 웹 검색을 통해 많은 자료를 찾을 수 있다.
 Images – 이 디렉터리에는 사이트에 관련된 모든 이미지를 저장한다.
 WEB-INF – WEB-INF 디렉터리에는 서블릿과 스트럿츠 관련 설정 파일을 저장한다.

14.4.4 build
Build 디렉터리에는 빌드를 쉽게 실행할 수 있도록 하는 셸 스크립트와 윈도우 배치 파일 그리고 Ant 스크립트를 저장한다.

14.4.5 devlib
Devlib는 컴파일에 필요한 jar파일을 포함하지만 WAR에 포함되어 배포되지는 않는다.
개발을 위해 필요한 라이브러리(devlib)에는 다음이 있다 :
 Ant.jar
 Ant-junit.jar
 Ant-launcher.jar
 Cgilib-nodep-2.1.3.jar
 Emma_ant.jar
 Emma.jar
 Jmock-1.0.1.jar
 Jmock-cglib-1.0.1.jar
 Junit.jar
 Servlet.jar

14.4.6 lib
Lib 디렉터리는 컴파일에 필요한 모든 jar 파일을 포함하고, WAR에도 포함되어 배포된다.
실행과 배포를 위해 필요한 라이브러리(lib)는 다음과 같다 :
 Antlr.jar
 Beanaction.jar
 Commons-beanutils.jar
 Commons-digester.jar
 Commons-fileupload.jar
 Commons-logging.jar
 Commons-validator.jar
 Hsqldb.jar
 Ibatis-common-2.jar
 Ibatis-dao-2.jar
 Ibatis-sqlmap-2.jar
 Jakarta-oro.jar
 Struts.jar

이 기본적인 소스 트리 구조를 가지고, 실제로 작동하는 애플리케이션 코딩을 시작해보자. 애플리케이션의 카탈로그 부분이 고객이 처음으로 보게 되는 곳이기 때문에 그것에 집중해서 개발해보자.
14.5. Web.xml 설정하기
Web.xml을 설정하는 것은 지극히 간단하다. JSP 페이지에 직접 접근하는 것을 막는 간단한 보안 설정과 스트럿츠 ActionServlet 구성을 할 것이다.

action

org.apache.struts.action.ActionServlet


config
/WEB-INF/struts-config.xml


debug
2


detail
2

2


action
*.shtml


요소를 사용하여 요청을 처리하는 ActionServlet을 설정한다. ActionServlet을 위한 설정은 기본적인 스트럿츠 설정에서와 다를 바 없으며 특별한 것은 없다. 요소에 명시된 ActionServlet은 표준 ActionServlet이다. 우리는 표준 struts-config.xml 파일의 위치, debug 레벨로 2, detail 레벨을 2, 그리고 load-on-startup 값을 2로 지정하였다.
요소를 주의하라. 똑똑해 보이고 싶어서 ActionServlet에 요청을 매핑하는 확장자를 표준인 .do 말고 다른 것을 사용하기로 결정했다. 그래서 표준 .do 대신에 .shtml을 사용한다. 이렇게 하는 단 한가지 이유는 장난삼아 오래된 기술을 사용하는 것처럼 보이도록 하기 위해서이다. 누가 알겠는가? 이 때문에 사이트를 해킹(어림도 없다네!)하려던 사람들이 지쳐 포기하게 될지.
스트럿츠를 사용할 때는 JSP 페이지에 대한 직접적인 접근을 막는 것이 중요하다. JGameStore가 사용하는 모든 JSP 페이지는 pages 디렉터리 아래에 위치한다. 모든 JSP 페이지가 디렉터리 아래에 있기 때문에 그 디렉터리에 대한 직접 접근을 간단히 막을 수 있다. 아래 설정은 JSP 페이지에 대한 모든 접근이 스트럿츠 ActionServlet을 통해서 이루어짐을 보장한다.



Restrict access to JSP pages

/pages/*



With no roles defined, no access granted




Web.xml 을 다 설정하면 이제 우리는 스트럿츠의 프레젠테이션 계층에서 사용할 클래스와 설정을 만드는 데 집중할 수 있게 된다. 스트럿츠의 BeanAction 방식의 장점에 대해 좀 더 자세히 이야기해보자.

14.6. 프레젠테이션 설정하기
카탈로그는 애플리케이션에서 장바구니 사용자들이 가장 먼저 사용하는 부분이기 때문에 카탈로그의 프레젠테이션 부분 설정을 집중해서 알아보자.

14.6.1 첫 번째 단계
방문자들이 JGameStore에 방문하면 초기 페이지를 만나게 된다. 스트럿츠를 사용할 때는 항상 스트럿츠 컨트롤러 (ActionServlet)을 통과하도록 요청을 포워딩해야 한다는 것은 우리 웹 애플리케이션의 중요한 규칙이다. 스트럿츠 프레임워크를 통과해서 방문자들을 초기 페이지로 보내려면, 간단하게 포워딩을 수행하는 index.jsp 페이지를 만들고 struts-config.xml에서 포워딩 할 URL을 정의하고, titles-defs.xml 파일에서 타일을 정의하며 그러고 나서 우리의 방문자들이 보게 될 초기 JSP 페이지를 생성한다.
JSP 포워딩이 작동하려면 방문자들이 보게 될 초기 페이지를 구축해야 한다.
이 페이지는 소스 트리에서 web/catalog/ 디렉터리에 위치하게 되고 이름은 Main.jsp로 저장한다. 방문자들이 카탈로그를 제일 처음 보게 되기 때문에 우리는 메인 페이지를 카탈로그 디렉터리에 둘 것이다.
추가로 tiles-defs.xml 파일에서 이 페이지를 위한 정의를 살펴볼 필요가 있다. Tiles를 사용하면 공통 템플릿을 생성하고 재사용하는 것이 쉬워져서 JSP의 include를 중복해서 사용하지 않아도 된다. Tiles에 대해 더 배우고 싶다면 Struts in Action을 참조하라.



value="/pages/common/header.jsp" />
value="/pages/common/footer.jsp" />
value="/pages/common/left-blank.jsp" />

extends="layout.main" >
value="/pages/common/header.jsp" />
value="/pages/common/footer.jsp" />
value="/pages/common/left.jsp" />

extends="layout.catalog" >
value="/pages/catalog/Main.jsp" />




Tiles-defs.xml 파일에서는 index.tiles를 정의하기 전에 layout.main과 layout.catalog를 정의해야 한다. Index.tiles 정의는 layout.catalog 정의를 상속한다. 또한 layout.catalog도 layout.main 정의를 상속한다. Layout.main은 소스 트리에서 web/pages/main.jsp 에 위치해 있는 기본 템플릿을 정의한다.
일단 메인 페이지를 구축하고 나면, 그 페이지로의 포워딩을 정의해서 우리의 index.jsp 파일의 포워드가 작동할 수 있게 만들어야 한다. 매핑 설정에서 parameter 속성의 값을 index로 정의했음을 주의해서 보라. 결과적으로 /catalog/Main.jsp로 포워딩 되기 전에 catalogBean의 index() 메서드를 먼저 호출할 것이다.

type="org.apache.struts.beanaction.BeanAction"
name="catalogBean" parameter="index"
validate="false">




JGameStore 애플리케이션에서 CatalogBean의 index() 메서드는 CatalogService를 호출하여 각 카테고리의 신규 제품의 List를 생성한다. 조금 뒤에 이에 관하여 좀 더 나은 예제를 살펴볼 것이기 때문에, 설명은 이것으로 마친다. 지금 이 시점에서는 index() 메서드가 “success” 문자열을 반환하고 장바구니의 첫 번째 페이지로 포워딩되는 것을 확인하기만 하면 된다.
public String index() {

return SUCCESS;
}

다음으로 index.jsp 를 사용하여 스트럿츠 컨트롤러를 통해 메인 페이지에 성공적으로 포워딩을 할 수 있다. Index.jsp 페이지에서는 포워딩만 해 주면 된다. 포워딩 URL에 .shtml 확장자를 꼭 써야 됨을 명심하라. 그래야 서블릿 컨테이너가 요청을 스트럿츠 컨트롤러를 통해서 처리한다.


다음으로 프레젠테이션 빈즈를 구성하고 호출할 행위 메서드를 생성할 것이다.

14.6.2 프레젠테이션 빈즈 이용하기
방문자가 장바구니에 방문해서 확인하고 싶은 카테고리를 선택한다. 우리는 그 카테고리를 확인하고 그 다음 페이지에서 제품의 목록을 출력해준다. 이렇게 하려면 다음과 같이 만들어야 한다.
 Tiles-defs.xml 파일의 tiles 정의
 Category 도메인 객체
 Product 도메인 객체
 프로퍼티와 행위 메서드를 가지고 있고 사용자의 입력을 받아들이는 프레젠테이션 빈즈
 카테고리 제품을 나열하는 JSP 페이지
그러고 나서 스트럿츠 설정에 액션 매핑을 추가한다.
카테고리 사용에 관한 코드에 대해 이야기하고 있으니 org.apache.ibatis.jgamestore.domain 패키지에 Category 라는 이름으로 이 객체의 클래스를 생성하자. Category 도메인 객체는 간단한 객체로 오직 categoryId 와 name, description 그리고 image로만 구성돼 있다. 우리의 카테고리가 한 단계 이상의 깊이를 가지는 경우는 결코 없기 때문에, 부모 카테고리에 관한 사항은 고려하지 않을 것이다.

public class Category implements Serializable {
private String categoryId;
private String name;
private String description;
private String image;
// simple setters and getters


우리가 작성할 코드는 Product(제품) 도메인 객체의 사용과도 관련돼 있다. 제품 객체는 JSP 페이지에서 출력할 때 List에 저장하여 읽어 들일 것이다. 이는 현 시점에서는 어떤 자바 코드도 제품에 직접적으로 접근하지는 않을 것이라는 뜻이다. 어쨌든 철저하게 코딩하는 것이 현명할 것이다. 이 코드를 org.apache.ibatis.jgamestore.domain 패키지에 Product.java 파일로 추가하자. Product 객체는 productId, categoryId, name, description, image 로 구성된다. Product는 Product 도메인 객체가 관련된 categoryId를 포함하고 있다는 점만 빼고는 Category 도메인 객체와 별로 크게 다르지 않다.

public class Product implements Serializable {
private String productId;
private String categoryId;
private String name;
private String description;
private String image;
// simple setters and getters


이제 도메인 클래스를 모두 구성하였으니, 프레젠테이션 빈즈에 대해 알아보자. CatalogBean 프레젠테이션 클래스를 살펴보면 실세계에서 BeanAction이 어떻게 작동하는지 처음으로 슬쩍 확인할 수 있다. CatalogBean을 만들면서 viewCategory행위 메서드도 함께 만들어야 한다. 이 행위 메서드는 public String <행위이름>()와 같은 BeanAction 형태의 행위 메서드 시그너처를 사용한다. viewCategory 행위 메서드는 상당히 단순하다. 이것의 역할은 선택한 카테고리에 관련된 제품의 목록을 가져와서 완전한 Category 객체를 생성하고 그 다음에 뷰 페이지로 포워딩한다. viewCategory 메서드 안에서는 CatalogService 클래스를 호출하여 productList와 categoryList 객체를 생성한다. 서비스 클래스는 나중에 살펴볼 것이다. 지금 현재는 서비스 클래스가 객체들을 올바로 반환한다고 가정하는 것만으로도 족하다.

private String categoryId;
private Category category;

private PaginatedList productList;

public String viewCategory() {
if (categoryId != null) {
productList =
catalogService.getProductListByCategory(categoryId);
category = catalogService.getCategory(categoryId);
}
return SUCCESS;
}

// category setter/getter

// productList setter/getter


CatalogBean을 컴파일하기 위해 org.apache.ibatis.jgamestore.service 패키지에 CatalogService 인터페이스를 만들 것이다. 인터페이스에 public List getProductListByCategory(Integer categoryId)와 public Category getCategory(Integer categoryId) 라는 두 개의 메서드를 추가할 것이다. 이후에는 구현에 대해서는 걱정할 필요 없이 코딩을 계속적으로 진행할 수 있다.
viewCategory 메서드가 완료되면 SUCCESS라고 부르는 public static String을 반환 값으로 사용한다. SUCCESS 변수의 실제 값은 “success”이다. 반환된 String은 BeanAction에게 다음으로 호출할 액션 포워드의 이름을 제공해준다. 그 결과로 JSP 페이지가 출력된다.

value="${catalogBean.category}"/>
value="${catalogBean.productList}"/>









Return to Main Page





style="border-bottom: 1px solid #ccc">

paramName="product"
paramProperty="productId"
page="/viewProduct.shtml">
escapeXml="false"/>


paramName="product"
paramProperty="productId"
page="/viewProduct.shtml">
View Items


style="border-bottom: 1px solid #ccc">

paramId="productId"
paramName="product"
paramProperty="productId"
page="/viewProduct.shtml">







태그를 처음 부분에서 사용하여 Category 객체와 productList를 page 스코프에 저장한다. 일단 이 객체들을 page 스코프에 저장하면, 그 이후의 태그에서 그 객체들을 사용할 수 있다. JSTL core 태그와 스트럿츠 태그는 product와 category의 List를 사용하여 객체들을 출력한다. 태그를 사용하여 Category 객체의 name 프로퍼티를 표시한다. 태그를 사용하여 product의 List를 순회하여 List에 있는 각각의 product를 이 태그의 내용으로 노출시킨다. 그러면 가 노출된 product를 사용하여 제품을 보여주는 페이지에 대한 링크를 생성해낸다. 태그의 내부에서는 태그를 사용하여 제품의 이름을 보여준다.
스트럿츠의 태그들은 공통적인 명명 방법을 사용하여 객체를 처리한다. Name 속성은 특정 스코프에 노출된 객체를 가리키는 키의 역할을 한다. 예를 들어 page 스코프에 category라는 이름으로 객체를 저장했다고 하자. Name 속성은 바로 이 category를 참조하게 된다. Name 속성에 대응하는 속성으로 property 속성이 있다. 스트럿츠 태그는 이 속성을 통해서 name이 가리키는 객체의 특정 프로퍼티에 접근한다.
이와는 달리 JSTL 태그에는 스트럿츠 태그의 name과 property 속성보다 훨씬 더 강력한 EL(Expression Language, 표현식) 이라는 것이 있다. EL을 사용하여 스코프 안의 객체를 특정 JSTL속성의 값으로 지정할 수 있다. EL에 대해서는 더 이상 자세히 다루지 않을 것이다. 하지만 좀 더 공부해 보고 싶다면 숀 바이에른(Shawn Bayern)의 JSTL in Action(Manning, 2002)를 보라고 추천하겠다.
필요한 모든 컴포넌트들을 코딩하였다면, struts-config.xml에 액션 매핑을 추가하여 애플리케이션이 그 컴포넌트들을 사용할 수 있게 된 것이다. 우리는 먼저 CatalogBean을 catalogBean이라 이름 짓고 CatalogBean의 완전한 클래스 이름을 타입으로 제공하여 폼 빈으로 지정할 것이다. 이제 액션 매핑이 사용할 수 있는 폼 빈을 갖추게 되었다.
name="catalogBean"
type=
"org.apache.ibatis.jgamestore.presentation.CatalogBean"/>

path="/viewCategory"
type="org.apache.struts.beanaction.BeanAction"
name="catalogBean" scope="session" validate="false">



마무리로 위에서 볼 수 있는 것처럼 액션 매핑을 설정해보자. 액션 매핑은 path를 명시해야만 하고, 위의 경우에는 /viewCategory 이다. Type 속성에는 요청을 처리하기 위해 사용될 Action 클래스의 패키지 경로를 포함한 클래스명을 지정해준다. 이 예제에서는 type이 BeanAction이다. BeanAction은 액션 매핑이 사용하는 폼 빈에 있는 행위 메서드에 요청을 전달한다. 이는 액션 매핑의 name 속성에 지정된 폼 빈 이름에 기초하여 판단된다. 우리의 경우 앞서 설정한 catalogBean을 사용할 것이다. 그 다음에는 scope 속성을 사용하여 폼 빈을 세션 스코프에 저장하라고 지정하였다. 이 경우 유효성을 검사할 입력 값이 없기 때문에 validate 속성은 false로 지정한다.
마지막으로 태그를 사용하여 포워드될 페이지를 결정한다. Name 속성은 프레젠테이션 빈즈의 행위 메서드에 의해 반환되는 값으로 매핑된다. 우리의 경우에는 항상 success라는 반환 값을 가져오고 따라서 /catalog/Category.jsp로 포워딩한다.
이제부터는 서비스 계층을 만들어 보자.

14.7. 서비스 작성하기
서비스 계층은 서비스 인터페이스와 그 구현체, 단 두 개의 부분으로 구성할 것이다. 서비스 클래스는 좀 더 잘게 나뉜 데이터 접근 호출들을 포괄적으로 모아서 처리하는 클래스로 만들 것이다. 듣기에는 간단할 것 같지만, 실제로는 서비스 클래스의 구현은 몇 가지 어려운 문제로 우리를 괴롭힐 것이다. 서비스 클래스에 어떠한 데이터 베이스 종속적인 정보도 들어가서는 안 되기 때문에, 적절히 추상화할 수 있는 추가적인 대책을 마련해야 한다.
서비스 계층은 데이터 접근 계층을 호출하고 트랜잭션 구분을 처리해야 하기 때문에 간단히 데이터베이스 커넥션을 가져와서 그 커넥션을 가지고 트랜잭션 구분을 관리하는 것은 쉽게 할 수 있다. 하지만 이렇게 하면 JDBC 에 종속적인 내용들을 서비스 계층에 노출시키게 된다. 이는 서비스 계층이 우리가 사용할 데이터 저장 구현체를 인지하게 됨을 의미한다. 서비스 계층에 우리가 사용하는 데이터 저장 형태가 노출되는 순간 서비스 계층의 근본적인 목적을 손상시키게 된다.
10장에서 배웠듯이, iBATIS는 이러한 상황에서 사용할 수 있는 iBATIS DAO라는 작은 프레임워크를 제공한다. iBATIS DAO는 몇 가지 중요한 역할을 수행해 줄 것이다. 첫째로 데이터 접근 객체의 팩토리 역할을 할 것이다. 둘째로 iBATIS DAO를 사용하여 트랜잭션을 구분할 것이고, 따라서 서비스 계층이 내부적인 데이터 접근 기법에 의존하는 것을 줄여준다. 이번 절에서는 앞에서 본 예제를 계속 살펴보고 iBATIS DAO를 사용하여 서비스 계층을 어떻게 구축하는지 공부해 볼 것이다.

14.7.1 dao.xml 설정하기
프레젠테이션 계층에서와는 다르게, 먼저 iBATIS DAO 프레임워크를 설정하는 것으로 서비스 계층을 살펴보기로 한다. 이렇게 하면 서비스 계층에 관련된 컴포넌트들을 이해하기가 더 쉬워진다. iBATIS DAO 프레임워크를 사용하면 설정을 통해 필수적인 추상화를 관리할 수 있기 때문에 이것부터 살펴보는 것이 적절하다.
iBATIS DAO에서 처음으로 설정한 컴포넌트는 트랜잭션 관리자이다. 트랜잭션 관리자를 사용하여 데이터 접근 계층을 호출하면서 트랜잭션 구분 짓기를 처리한다. 우리의 예제에서는 SQLMAP 타입의 트랜잭션 관리자를 사용할 것이다. SQLMAP은 iBATIS SQL Maps 프레임워크와 통합시켜주는 타입이다. iBATIS DAO와 iBATIS SQL Maps를 구분하는 것이 어렵긴 하지만, 이 둘은 확실히 서로 다른 프레임워크이다. SQLMAP 트랜잭션 관리자는 뛰어나며 사용하기도 쉽다. 만약 iBATIS DAO 프레임워크를 다른 퍼시스턴스 계층과 함께 사용하지 않는다면, 대부분의 경우에 SQLMAP이 가장 적합한 트랜잭션 관리자일 것이다. SQLMAP 트랜잭션 관리자를 사용할 때는 SqlMapConfigResource 프로퍼티 단 하나만 지정해주면 된다. 이 프로퍼티는 태그 안에서 태그를 사용하여 지정해주면 된다. SqlMapConfigResource 프로퍼티에는 간단히 모든 필수적인 데이터베이스 접속 정보를 포함하고 있는 SQL Maps 설정 파일을 값으로 지정해주면 된다. 트랜잭션을 시작하고 커밋하고 종료하는 호출이 일어날 때마다, 필요한 요청을 SQL Maps 설정 파일에서 지정한 숨겨진 커넥션 객체에 투명하게 전달할 것이다.

value=
"org/apache/ibatis/jgamestore/persistence/sqlmapdao/sql/sql -mapconfig.
xml"/>


Dao.xml 설정의 다음 단계는 인터페이스를 구현체에 매핑하는 것이다. 이는 상당히 쉽게 할 수 있다. 요소의 interface 속성에 완전한 패키지 명을 포함하는 인터페이스 이름만 지정해주면 된다. Implementation 속성에는 해당 인터페이스의 완전한 패키지 명을 포함한 구현체 이름을 지정해주면 된다. 만약 지정된 인터페이스를 사용하지 않는 구현체를 설정하였다면, iBATIS DAO가 실행시간에 이에 관해 확실히 알려 줄 것이다.
interface=
"org.apache.ibatis.jgamestore.persistence.iface.ProductDao"
implementation=
"org.apache.ibatis.jgamestore.persistence.sqlmapdao.ProductSqlMapDao"/>

14.7.2 트랜잭션 구분하기
iBATIS DAO 프레임워크에서 SQLMAP 타입을 사용하면 암묵적인 트랜잭션 관리자와 명시적인 트랜잭션 관리자를 사용하게 된다. 기본적으로 트랜잭션을 명시적으로 지정하지 않으면 자동으로 트랜잭션이 시작될 것이다. 이를 피하는 방법이 여러 가지가 있는데 4장과 10장에서 이에 관해 읽어 볼 수 있다.
SQLMAP 타입에서 암묵적인 트랜잭션 관리는 간단하다. 우리가 해야 할 것이라고는 데이터 접근 객체의 메서드를 호출하는 것뿐이다. 트랜잭션 관리자는 자동으로 수행된다. Select 구문의 경우에는 트랜잭션이 꼭 필요한 것은 아니지만, 트랜잭션을 사용한다고 해서 나쁠 것도 없다.
public PaginatedList getProductListByCategory(
String categoryId
) {
return productDao.getProductListByCategory(categoryId);
}

명시적인 트랜잭션 관리는 좀 더 복잡하다. 이는 데이터 접근 객체에 하나 이상의 호출을 수행할 때만 필요하다. Try 블록 안에서 daoManager.startTransaction();을 처음으로 호출하고, 그 이후에 하나 혹은 그 이상의 데이터 접근 객체 호출을 수행한다. 데이터 접근 객체 호출이 모두 끝나면, daoManager.commitTransaction()을 호출하여 커밋한다. 만약 어떠한 이유에서라도 호출이 실패하면 finally 블록에 있는 daoManager.endTransaction()이 호출된 것이다. 이는 트랜잭션을 롤백하고 데이터 지장에 손상이 없도록 보호해준다. 예제에서 수행할 간단한 select 구문 처리는 이러한 수준의 트랜잭션 관리까지는 필요가 없다. 어쨌든 그래도 원한다면 이러한 방법을 사용할 수도 있다.
public PaginatedList getProductListByCategory(
String categoryId
) {
PaginatedList retVal = null;
try {
// Get the next id within a separate transaction
daoManager.startTransaction();
retVal = productDao
.getProductListByCategory(categoryId);
daoManager.commitTransaction();
} finally {
daoManager.endTransaction();
}
return retVal;
}

자 이제 간단한 카테고리 보기 예제에서 서비스 계층을 모두 살펴보았다. DAO계층의 나머지 조각들을 모두 조합하여 완성해보자.

14.8. DAO 작성하기

데이터 접근 계층은 데이터베이스를 호출하는 자바 코드가 있는 곳이다. SQL을 더 쉽게 처리하기 위해 iBATIS SQL Maps 프레임워크를 사용한다. iBATIS SQL Maps를 사용하는 데이터 접근 계층은 SQL Maps 설정파일, SQL Maps 파일들의 묶음, 그리고 데이터 접근 객체의 세 가지 기본적인 부분으로 나눌 수 있다.
카테고리 보기 예제에 이들을 어떻게 적용해서 제품 목록을 가져오는지 살펴보자.

14.8.1 SQL Maps 설정

Sql-map-config.xml 파일을 사용하여 데이터베이스 프로퍼티들을 지정하고 트랜잭션 관리자를 구성하고 SQL Map 파일들을 함께 묶을 것이다. 태그는 database.properties 파일을 가리킬 것이다. 이 파일에는 키/값 쌍이 저장돼 있는데, ${…}로 작성된 항목들을 이 파일에 있는 값으로 대체할 것이다. Database.properties 파일에는 사용할 데이터베이스에 적합한 드라이버와 URL, 사용자명 그리고 비밀번호 등이 확실하게 들어가 있어야 한다.










"org/apache/ibatis/jgamestore/persistence/sqlmapdao/sql/Product.xml"/>


다음으로 트랜잭션 관리자를 설정하자. 우리의 목적에 따라, 가장 쉬운 트랜잭션 관리자인 JDBC 타입을 사용할 것이다. JDBC 타입을 사용한다는 것은 SQL Maps가 표준 Connection 객체의 commit 과 rollback 메서드를 사용한다는 의미이다. 트랜잭션 구분은 서비스 계층에서 처리할 것이기 때문에 이 설정 파일이 더 중요해진다. 어쨌든 iBATIS DAO에서 트랜잭션 관리자를 제대로 작동시키려면 이 트랜잭션 관리자를 설정해야 한다.
transactionManager 안의 데이터 소스는 트랜잭션 관리자가 커넥션을 가져올 때 사용할 JDBC 데이터 소스를 정의한다. iBATIS가 데이터 소스 커넥션 풀을 처리하도록 할 것이기 때문에, SIMPLE 타입으로 지정하였다. 그 다음에 태그를 사용하여 드라이버와 접속 URL, 사용자명 그리고 비밀번호를 지정하였다. 각각의 태그는 ${…} 표기법을 사용하여 database.properties 파일에서 값을 가져다 쓴다.
마지막으로 설정할 요소는 요소이다. 이 요소로 SQL Map 파일들의 위치를 지정한다. iBATIS가 처음으로 호출될 때마다, 설정한 SQL Map 파일들과 그 내용을 메모리에 적재하여 필요할 때 SQL 구문을 실행할 수 있도록 한다.

14.8.2 SQL Map
카테고리 제품 목록을 가져오는 SQL 호출 구문을 저장하는 SQL Map 파일을 만들어야 한다. 이름은 Product.xml 이라고 짓고, Product 객체를 위한 typeAlias, 결과를 캐싱할 캐시 모델 그리고 실행할 select SQL을 저장하는 select 매핑 구문을 정의해야 한다.

alias="product"
type="org.apache.ibatis.jgamestore.domain.Product"/>





id="getProductListByCategory" resultClass="product"
parameterClass="string" cacheModel="productCache">
SELECT
PRODUCTID,
NAME,
DESCRIPTION,
IMAGE,
CATEGORYID
FROM PRODUCT
WHERE CATEGORYID = #value#



typeAlias 요소는 Product 도메인 클래스의 완전한 클래스 이름에 대한 별칭을 지정한다. 여기서 지정한 별칭은 product 이다. 이를 사용하면 Product 도메인 객체를 참조할 때마다 완전한 클래스 이름을 기입할 필요가 없어져서 타이핑이 줄어든다.
예제 캐시 모델은 간단하게 만들 것이다. 캐시 타입은 LRU이고 이름은 productCache라고 설정할 것이다. LRU 캐시는 잠재적으로 오랫동안 지속될 수 있기 때문에 요소를 사용하여 24시간 이상은 절대로 붙들려 있지 않게 하였다. 이렇게 하면 애플리케이션에서 상대적으로 새로운 데이터를 유지하게 할 수 있다. LRU의 크기는 productCache 캐시 모델이 100개의 서로 다른 결과를 저장할 수 있을 만큼으로 지정하였다. 사이트의 트래픽이 높아지면, 최소한 하루 한 번의 의무적인 캐시 비우기로 성능을 계속 유지할 수 있을 거라고 확신한다.
그 다음으로 select 요소가 나온다. Select 요소에 SQL 이 하는 일을 나타낼 만한 이름으로 id 속성값을 지정해주었다. Id가 길어지는 것을 두려워할 필요는 없다. 길면 select요소 내의 SQL이 하는 역할을 더욱 명확하게 나타낼 수 있다. 이번 경우에는 id 값으로 getProductListByCategory(“카테고리에 따라 제품의 목록을 가져오라”는 의미)를 지정하였다. 이 SQL은 틀림없이 지정된 카테고리를 기반으로 해서 List를 반환할 것이다.
위에서 정의한 typeAlias를 사용하여, select 구문의 resultClass를 product로 지정하였다. 비록 데이터 접근 객체에서 이 select 구문을 실행하여 목록을 얻어오지만, 반환 결과로 List를 지정하지는 않는다는 점에 주의하라. 이렇게 하는 이유는 동일한 select구문을 사용하여 한 개의 Product 객체를 반환받을 수도 있기 때문이다. 이름을 getProductListByCategory라고 지었기 때문에 이렇게 하는 것이 불합리하게 느껴질수 도 있다. 하지만 select 구문이 여러 목적을 가지고 단일 객체나 객체의 List를 반환하는 상황도 있을 수 있다.
이 select 구문의 parameterClass는 string 별칭(기본적으로 정의돼 있는 것이다)을 사용한다. 이미 예상했겠지만, 이 별칭은 String 객체를 나타낸다. 또한 사용자 정의 별칭을 사용해서 parameterClass 속성을 지정할 수도 있다.
Select 구문에서 사용한 마지막 속성은 cacheModel 이다. 이 속성은 앞에서 정의한 productCache 캐시 모델을 참조한다. cacheModel을 명시하여 쿼리 결과로 가져온 모든 카테고리 제품 목록이 캐싱될 것임을 보장할 수 있다. 이를 통해 속도 향상과 불필요한 데이터베이스 접근을 줄여주는 효과를 얻을 수 있다.
다음 단계는 SQL 문장으로 select 요소의 내용을 채우는 것이다. 예제의 select 구문은 select 요소에 설정한 대로 product 테이블에서 레코드의 결과섯을 가져다가 Product 객체의 목록에 매핑한다.
일단 sql-map-config.xml 파일, 별칭과 캐시 모델, select 구문 설정을 모두 마치고 나면 이제는 자바 코드에서 iBATIS API를 사용할 준비가 다 된 것이다. SQL Map을 사용하여 ProductDao 인터페이스의 구현체를 작성할 것이다.

14.8.3 인터페이스와 구현체
애플리케이션에서 계층 간의 작업을 할 때는 인터페이스를 기반으로 코딩하는 것이 좋은 방법이다. 이 예제에서는 서비스 계층과 데이터 접근 계층 사이에서 작업을 하고 있다. 서비스 계층은 항상 DAO 인터페이스와만 소통하고 DAO 구현체에 대해서는 몰라야 한다. 이 경우는 아래에서 보다시피 아무런 차이가 없다.
public interface ProductDao {
PaginatedList getProductListByCategory(
String categoryId);

}

CatalogService 클래스에서 사용할 ProductDao 인터페이스를 만들었다. CatalogService는 ProductDao 인터페이스와 소통하기 때문에, 실제 구현체가 어떤지에 관해서는 신경쓰지 않는다. ProductDao에서는 CatalogService가 사용할 수 있는 getProductListByCategory 메서드를 정의해야 한다. 반환타입은 PaginatedList이고 메서드의 시그너처는 String 타입의 categoryId로 구성돼 있다.
public class ProductSqlMapDao
extends BaseSqlMapDao
implements ProductDao {

public ProductSqlMapDao(DaoManager daoManager) {
super(daoManager);
}

public PaginatedList getProductListByCategory(
String categoryId
) {
return queryForPaginatedList(
"Product.getProductListByCategory",
categoryId, PAGE_SIZE);
}

}

Org.apache.ibatis.jgamestore.persistence.sqlmapdao 패키지의 ProductSqlMapDao가 ProductDao의 구현체이다. ProductSqlMapDao는 BaseSqlMapDao를 상속받고, BaseSqlMapDao는 SqlMapDaoTemplate을 상속받는다. SqlMapDaoTemplate은 iBATIS SQL Map의 기반 클래스이다. 이 클래스는 SQL Map XML 파일에 정의된 SQL을 호출할 때 사용하는 메서드들을 포함하고 있다. ProductSqlMapDao 클래스의 getProductListByCategory 메서드 구현에서 queryForPaginatedList 메서드를 사용한다. queryForPagenatedList를 호출할 때는 명명공간과 실행하고자 매핑 구문의 이름(예를 들어 Product.getProductListByCategory), 결과를 가져오고자 하는 카테고리의 categoryId, 반환 받아서 화면에 출력할 목록의 페이지 크기를 파라미터로 넘겨준다.

댓글 없음:

댓글 쓰기