Spring, Struts 통합

다른 웹 프레임워크들과의 통합





14.1. 소개



Spring은 어떠한 자바 기반의 웹 프레임워크와도 쉽게 통합된다. 당신은 ContextLoaderListener 를 당신의 web.xml에 선언하고, 어떤 컨텍스트 파일을 로드할 것인지를 세팅하기 위해 contextConfigLocation <context-param>를 사용하기만 하면 된다.


The <context-param>:

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>

The <listener>:

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

노트: Listeners는 Servlet API 2.3 버전에 추가되었다. 만약 당신이 Servlet 2.2 컨테이너를 사용한다면, 당신은 동일한 기능을 얻기 위해 ContextLoaderServlet 를 사용할 수 있다.


만약 당신이 contextConfigLocation 컨텍스트 파라미터를 명시하지 않는다면, ContextLoaderListener는 로드할 /WEB-INF/applicationContext.xml 파일을 찾을 것이다. 일단 컨텍스트 파일이 로드되면, Spring은 빈 정의에 기반하여 WebApplicationContext 객체를 생성하고 이것을 ServletContext에 담아둔다.


모든 자바 웹 프레임워크들은 Servlet API에 기반하여 만들어졌기 때문에, 당신은 Spring이 생성한 ApplicationContext를 얻기 위해 다음의 코드를 사용할 수 있다.

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

WebApplicationContextUtils 클래스는 편의를 위해 만들어진 것인데, 때문에 당신은 ServletContext 속성의 이름을 기억할 필요가 없다. 그것의 getWebApplicationContext() 메써드는 만약 (ApplicationContext) 객체가 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 키로 존재하지 않는다면 null을 반환할 것이다. 어플리케이션에서 NullPointerExceptions를 받는 위험을 감수하는 것보다는 getRequiredWebApplicationContext() 메써드를 사용하는 것이 낫다. 이 메써드는 ApplicationContext를 찾지 못할 경우, 예외를 던진다.


일단 당신이 WebApplicationContext를 참조하게 되면, 당신은 이름 혹은 타입으로 빈들을 가져올 수 있다. 대부분의 개발자들은 빈들을 이름으로 가져와서 그것의 구현된 인터페이스들 중 하나로 캐스팅한다.


운 좋게도 이번 장에서 다루는 대부분의 프레임워크들은 빈들을 룩업하는 방식이 매우 간단하다. 빈들을 BeanFactory로부터 가져오는 것이 쉬울 뿐만 아니라, 컨트롤러에 의존성 삽입(dependency injection)을 사용할 수 있도록 해준다. 각각의 프레임워크 섹션에서 그것의 특화된 통합 전략들에 기반하여 보다 세부적인 사항들을 설명할 것이다.






14.2. JavaServer Faces



JavaServer Faces (JSF) 는 컴포넌트 기반, 이벤트 드리븐 웹 프레임워크이다. Sun Microsystem의 JSF Overview 에 따르면, JSF 기술은 다음을 포함하고 있다.





  • UI 컴포넌트들을 표현하고 그것들의 상태를 관리하며, 이벤트들을 핸들링하고, 밸리데이션을 삽입하고, 페이지 네비게이션을 정의하고, 국제화와 접근성을 지원하는 API들의 세트



  • JSP 페이지 내에서 JavaServer Faces 인터페이스를 표현하기 위한 JavaServer Pages (JSP) 커스텀 태크 라이브러리






14.2.1. DelegatingVariableResolver



당신의 Spring 미들티어를 JSF 웹 레이어와 통합하는 가장 쉬운 방법은 DelegatingVariableResolver 클래스를 사용하는 것이다. 이 변수 처리자(variable resolver)를 당신의 어플리케이션에 설정하려면, faces-context.xml를 수정해야 한다. <faces-config> 요소를 연 이후에, <application> 요소를 추가하고 그 안에 <variable-resolver> 요소를 추가하면 된다. 변수 처리자의 값은 Spring의 DelegatingVariableResolver를 참조해야만 한다.

<faces-config>
<application>
<variable-resolver>org.springframework.web.jsf.DelegatingVariableResolver</variable-resolver>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>en</supported-locale>
<supported-locale>es</supported-locale>
</locale-config>
<message-bundle>messages</message-bundle>
</application>

Spring의 변수 처리자를 명시함으로써, 당신은 Spring 빈들을 당신이 관리하는 빈들의 프라퍼티들로 설정할 수 있다. DelegatingVariableResolver는 처음엔 값을 룩업하는 것을 기반하는 JSF 구현의 디폴트 처리자에 위임한다. 그리고나서 Spring의 루트 WebApplicationContext에 위임한다. 이것은 당신이 JSF에 의해 관리되는 빈들에 의존성을 쉽게 주입할 수 있도록 해준다.


관리되는 빈들은 faces-config.xml 파일에 정의된다. 아래는 Spring의 BeanFactory로부터 가져온 #{userManager} 빈을 어디에 정의하느냐를 보여준다.

<managed-bean>
<managed-bean-name>userList</managed-bean-name>
<managed-bean-class>com.whatever.jsf.UserList</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>userManager</property-name>
<value>#{userManager}</value>
</managed-property>
</managed-bean>

DelegatingVariableResolver는 JSF와 Spring을 통합할 때 적극 추천되는 전략이다. 만약 당신이 보다 튼튼한 통합 예시를 찾는다면, JSF-Spring 프로젝트를 볼 수 있다.






14.2.2. FacesContextUtils



커스텀 VariableResolver는 당신의 프라퍼티들을 faces-config.xml 내의 빈들과 매핑할 때 매우 잘 동작한다. 그러나, 종종 당신은 빈을 명시적으로 가로챌 필요가 있을 것이다. FacesContextUtils 클래스는 그것을 쉽게 해준다. 이 클래스는 WebApplicationContextUtils과 비슷한데, ServletContext 파라미터가 아니라 FacesContext 파라미터를 가진다는 점만 다르다.

ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());





14.3. Struts



Struts 는 자바 어플리케이션을 위한 사실상의 웹 프레임워크 그 자체이다. 이것은 주되게 Struts이 가장 먼저 릴리즈된(2001년 6월) 것 중 하나라는 사실에 기인한다. Craig McClanahan에 의해 창안된 Struts은 아파치 소프트웨어 재단에 의해 후원되는 오픈 소스 프로젝트이다. 처음부터, Struts는 JSP/Servlet 프로그래밍 패러다임을 획기적으로 간소화했고 개인 프레임워크들을 사용하던 많은 개발자들을 끌어들였다. 이것은 프로그래밍 모델을 간단하게 했으며 오픈소스였다. 그리고 이것은 이 프로젝트가 성장하고 자바 웹 개발자들 사이에 대중화될 수 있도록 하는 거대한 커뮤니티를 가졌다.


Struts 어플리케이션을 Spring과 통합하는 데는 두 가지 방법이 있다.





  • ContextLoaderPlugin를 사용하여 Spring이 Action들을 빈들로 관리하도록 설정하고 Action들의 의존성을 Spring 컨텍스트 파일에 세팅하는 방법



  • Spring의 ActionSupport 클래스를 상속해서 getWebApplicationContext() 메써드를 사용하여 Spring 관리되는 빈들을 명시적으로 가로채는 방법






14.3.1. ContextLoaderPlugin



ContextLoaderPlugin 은 Struts ActionServlet을 위해 Spring 컨텍스트 파일을 로드하는 Struts 1.1 이상 버전의 플러그인이다. 이 컨텍스트는 ContextLoaderListener에 의해 로드된 WebApplicationContext를 그것의 부모 클래스로 참조한다. 컨텍스트 파일의 디폴트 이름은 매핑된 서블릿의 이름에 -servlet.xml을 더한 것이다. 만약 web.xml에서 ActionServlet<servlet-name>action</servlet-name>라고 정의되었다면, 디폴트는 /WEB-INF/action-servlet.xml이 될 것이다.


이 플러그인을 설정하기 위해서는 다음의 XML을 struts-config.xml 파일의 아래쪽의 plug-ins 섹션에 추가해야 한다.

<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn"/>

컨텍스트 설정 파일의 위치는 "contextConfigLocation" 프라퍼티를 사용하여 임의대로 정할 수 있다.

<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn">
<set-property property="contextConfigLocation"
value="/WEB-INF/action-servlet.xml.xml,
/WEB-INF/applicationContext.xml"/>
</plug-in>

모든 컨텍스트 파일들을 로드하기 위해 이 플러그인을 사용할 수 있는데, 이것은 StrutsTestCase와 같은 테스팅 툴들을 사용할 때 유용할 것이다. StrutsTestCase의 MockStrutsTestCase는 Listeners를 초기화하지 않을 것이기 때문에, 당신의 컨텍스트 파일들을 플러그인에 담는 것이 필요하다. 이 이슈에 대해서는 버그 정리 를 참조하도록 하라.


이 플러그인을 struts-config.xml에 설정한 이후에야 Action을 Spring에 의해 관리되도록 설정할 수 있다. Spring 1.1.3은 이를 위해 두 가지 방법을 제공한다.





  • Struts의 디폴트 RequestProcessor를 Spring의 DelegatingRequestProcessor로 오버라이드한다.



  • <action-mapping>의 타입 속성에 DelegatingActionProxy 클래스를 사용한다.


이 메써드들 모두 당신이 Action들과 action-context.xml 파일에 있는 그것들의 의존성들을 관리할 수 있게 해준다. struts-config.xmlaction-servlet.xml 내의 Action들은 action-mapping의 "path"와 빈의 "name"으로 연결된다. 당신이 struts-config.xml 파일에 다음과 같은 설정을 가진다고 가정하자.

<action path="/users" .../>

그러면, 당신은 Action 빈을 "/users"라는 이름으로 action-servlet.xml 내에 정의해야만 한다.

<bean name="/users" .../>





14.3.1.1. DelegatingRequestProcessor



DelegatingRequestProcessorstruts-config.xml 파일에 설정하기 위해서는, <controller> 요소 내의 "processorClass"를 오버라이드해야 한다. 이 줄들은 <action-mapping> 요소에 뒤따른다.

<controller>
<set-property property="processorClass"
value="org.springframework.web.struts.DelegatingRequestProcessor"/>
</controller>

이 세팅을 추가한 후에, 당신의 Action은 그것이 무슨 타입이건 간에 자동적으로 Spring의 컨텍스트 파일에서 룩업될 것이다. 사실, 당신은 타입을 명시할 필요조차 없다. 다음의 조각코드들은 둘 다 모두 잘 동작할 것이다.

<action path="/user" type="com.whatever.struts.UserAction"/>  
<action path="/user"/>

만약 당신이 Struts의 modules 특징을 사용한다면, 당신의 빈 이름들은 반드시 모듈 접두어를 포함해야만 한다. 예를 들어, 모듈 접두어 "admin"을 가진 <action path="/user"/>로 정의된 action은 <bean name="/admin/user"/>라는 빈 이름을 가져야 한다.


노트: 만약 당신이 Struts 어플리케이션에서 Tiles를 사용한다면, 당신은 DelegatingTilesRequestProcessor 로 <controller>를 설정해야만 한다.






14.3.1.2. DelegatingActionProxy



만약 직접 작성한 RequestProcessor를 가지고 있어서 DelegatingTilesRequestProcessor를 사용할 수 없는 상황이라면, DelegatingActionProxy 를 action-mapping에서 사용할 수 있다.

<action path="/user" type="org.springframework.web.struts.DelegatingActionProxy"
name="userForm" scope="request" validate="false" parameter="method">
<forward name="list" path="/userList.jsp"/>
<forward name="edit" path="/userForm.jsp"/>
</action>

action-servlet.xml에서의 빈 정의는 직접 작성한 RequestProcessor를 쓰건, DelegatingActionProxy를 쓰건 동일하다.


만약 당신이 컨텍스트 파일에 Action을 정의한다면, 그 Action에 대해 Spring 빈 컨테이너의 모든 특징들을 사용할 수 있을 것이다. 각각의 request에 대한 새로운 Action 인스턴스를 초기화하기 위한 옵션으로 의존성 주입을 사용하는 것 등. 후자를 사용하려면 당신의 Action 빈 정의에 singleton="false"를 추가해주어야 한다.

<bean name="/user" singleton="false" autowire="byName"
class="org.example.web.UserAction"/>





14.3.2. ActionSupport 클래스들



앞에서 언급한 것처럼, WebApplicationContextUtils 클래스를 사용해서 ServletContext로부터 WebApplicationContext를 가져올 수 있다. 더 쉬운 방법은 Struts를 위한 Spring의 Action 클래스들을 상속받는 것이다. 예를 들어, Struts의 Action 클래스를 상속하는 것 대신에 Spring의 ActionSupport 클래스를 상속할 수 있다.


ActionSupport 클래스는 getWebApplicationContext()처럼 부가적인 편의 메써드들을 제공해준다. 아래의 예제는 Action에서 어떻게 이것을 사용할 수 있는지를 보여준다.

public class UserAction extends DispatchActionSupport {

public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
if (log.isDebugEnabled()) {
log.debug("entering 'delete' method...");
}

WebApplicationContext ctx = getWebApplicationContext();
UserManager mgr = (UserManager) ctx.getBean("userManager");

// talk to manager for business logic

return mapping.findForward("success");
}
}

Spring은 모든 표준 Struts Action의 하위 클래스들을 포함한다. - Spring 버전은 단지 이름에 Support만을 붙혀놓았을 뿐이다.




추천하는 전략은 당신의 프로젝트에 가장 잘 맞는 접근방법을 사용하는 것이다. 하위클래스 방법은 당신의 코드를 보다 읽기 쉽게 만들어주며 어떻게 의존성들을 해결할 것인지 명확하게 알게 해준다. 반면, ContextLoaderPlugin을 사용하는 것은 컨텍스트 XML 파일에 새로운 의존성을 추가하는 것을 쉽게 해준다. 어느 쪽이던지, Spring은 두 개의 프레임워크들을 통합하기 위해 몇몇 멋진 옵션들을 제공한다.






14.4. Tapestry



Tapestry는 아파치의 자카르타 프로젝트로부터 나온 강력한 컴포넌트 지향 웹 어플리케이션 프레임워크이다.(http://jakarta.apache.org/tapestry) Spring이 그 자체의 강력한 ui 계층을 가지고 있지만, 웹 ui를 위해 Tapestry와의 조합을 사용하여 J2EE 어플리케이션을 개발하는 것은 독특한 몇가지 장점들이 있다. 이 문서는 이 두 개의 프레임워크들을 조합하기 위한 몇 가지 최선의 실습예제들을 상세시 설명할 것이다. 당신이 Tapestry와 Spring 프레임워크 기본 둘 다에 대해 어느정도 친숙하다고 전제하에서 그것들에 대해서 여기서 설명하지는 않을 것이다. Tapestry와 Spring에 대한 일반적인 소개글은 각각의 웹 사이트들에서 접할 수 있다.






14.4.1. 아키텍쳐



Tapestry와 Spring으로 개발된 전형적인 계층적 J2EE 어플리케이션은 최상위 UI 계층은 Tapestry로, 많은 하위 계층들은 하나 혹은 그 이상의 Spring 어플리케이션 컨텍스트에 의해 관리되는 형태로 구성될 것이다.





  • 사용자 인터페이스 계층


    - 사용자 인터페이스와 관련있다.


    - 몇몇 어플리케이션 로직을 포함한다.


    - Tapestry에 의해 제공된다.


    - Tapestry를 경유하여 UI를 제공하는 것은 별개로 하고, 이 계층에서의 코드는 Service 계층의 인터페이스들을 구현한 객체들을 경유하여 작동한다. 이러한 서비스 계층 인터페이스들을 구현한 실질적인 객체들은 Spring 어플리케이션 컨텍스트로부터 가져온다.



  • 서비스 계층


    - 어플리케이션 특화된 '서비스' 코드


    - 도메인 객체들과 작동하며 그 도메인 객체들을 몇몇가지 저장소(database)에 넣고 빼기 위해 Mapper API를 사용한다.


    - 하나 혹은 그 이상의 Spring 컨텍스트에 의해 관리된다.


    - 이 계층에서의 코드는 도메인 모델 내에서 어플리케이션 특화의 형태로 객체들을 조작한다. 그것은 이 계층 내의 다른 코드와 Mapper API를 통해 작동한다. 이 계층의 객체는 그것이 작동할 때 필요한 특정한 매퍼 구현체들을 Spring 컨텍스트를 통해 받는다.


    - 이 계층의 코드는 Spring 컨텍스트에서 관리되기 때문에, 그 자신의 트랜잭션들을 관리하는 것 대신에, Spring 컨텍스트에 의해 트랜잭션화될 것이다.



  • 도메인 모델


    - 이 도메인에 특화된 데이터와 로직을 다루는 도메인 특화된 객체 계층구조이다.


    - 비록 도메인 객체 계층이 어딘가 저장되고 이것에 대한 몇가지 일반적인 특권(예를 들어, 양방향적인 관계들) 가진다는 개념에 기반하여 만들어진 것이지만, 이것은 일반적으로 다른 계층들에 대해 전혀 알 필요가 없다. 그렇기 때문에, 이것은 독자적으로 테스트될 수 있고, 생산과 테스팅을 위해 서로 다른 매핑 구현체들과 함께 사용될 수 있다.


    - 이 객체들은 독립형이거나 Spring 어플리케이션과 결합되어 사용된다. 이것은 고립성, IoC, 다른 전략적 구현체 등등의 컨텍스트의 몇가지 이점들을 가지게 한다.



  • 데이터 소스 계층


    - (데이터 접근 객체 -DAO 라고도 불리는) Mapper API : 도메인 모델을 몇 가지 (일반적으로 DB이지만 파일시스템, 메모리 등도 될 수 있는) 저장소에 저장할 때 사용되는 API


    - Mapper API 구현체들 : 하나 혹은 그 이상의 Mapper API의 특화된 구현체들, 예를 들어, Hibernate 특화 매퍼, JDO 특화 매퍼, JDBC 특화 매퍼 또는 메모리 메퍼 등


    - 매퍼 구현체들은 하나 혹은 그 이상의 Spring 어플리케이션 컨텍스트에 존재한다. 서비스 계층 객체는 컨텍스트를 통해 작동시 필요한 매퍼 객체들을 받는다.



  • 데이터베이스, 파일시스템 혹은 다른 저장소들


    - 도메인 모델 내의 객체들은 하나 혹은 그 이상의 매퍼 구현체들에 의해 하나 이상의 저장소에 저장된다.


    - 저장소는 파일시스템처럼 매우 간단할 수도 있고, DB에서의 스키마처럼 도메인 모델로부터 자신만의 데이터 표현을 가질 수도 있다. 그러나, 다른 계층들에 대해서는 알지 못한다.






14.4.2. 구현체



유일한 실질적인 질문(이 문서에 의해 설명될 필요가 있는)은 어떻게 Tapestry pages가 Spring 어플리케이션 컨텍스트의 인스턴스로 정의된 빈들인 서비스 구현체들에 접근할 수 있느냐이다.






14.4.2.1. 샘플 어플리케이션 컨텍스트



우리가 xml 형태로 다음과 같은 간단한 어플리케이션 컨텍스트 정의를 가지고 있다고 가정하자.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">

<beans>

<!-- ========================= GENERAL DEFINITIONS ========================= -->

<!-- ========================= PERSISTENCE DEFINITIONS ========================= -->

<!-- the DataSource -->
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName"><value>java:DefaultDS</value></property>
<property name="resourceRef"><value>false</value></property>
</bean>

<!-- define a Hibernate Session factory via a Spring LocalSessionFactoryBean -->
<bean id="hibSessionFactory"
class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
<property name="dataSource"><ref bean="dataSource"/></property>
</bean>

<!--
- Defines a transaction manager for usage in business or data access objects.
- No special treatment by the context, just a bean instance available as reference
- for business objects that want to handle transactions, e.g. via TransactionTemplate.
-->
<bean id="transactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager">
</bean>

<bean id="mapper"
class="com.whatever.dataaccess.mapper.hibernate.MapperImpl">
<property name="sessionFactory"><ref bean="hibSessionFactory"/></property>
</bean>

<!-- ========================= BUSINESS DEFINITIONS ========================= -->

<!-- AuthenticationService, including tx interceptor -->
<bean id="authenticationServiceTarget"
class="com.whatever.services.service.user.AuthenticationServiceImpl">
<property name="mapper"><ref bean="mapper"/></property>
</bean>
<bean id="authenticationService"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager"><ref bean="transactionManager"/></property>
<property name="target"><ref bean="authenticationServiceTarget"/></property>
<property name="proxyInterfacesOnly"><value>true</value></property>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>

<!-- UserService, including tx interceptor -->
<bean id="userServiceTarget"
class="com.whatever.services.service.user.UserServiceImpl">
<property name="mapper"><ref bean="mapper"/></property>
</bean>
<bean id="userService"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager"><ref bean="transactionManager"/></property>
<property name="target"><ref bean="userServiceTarget"/></property>
<property name="proxyInterfacesOnly"><value>true</value></property>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>

</beans>

Tapestry 어플리케이션 내에서, 우리는 이 어플리케이션 컨텍스트를 로드할 필요가 있고, Tapestry pages로 하여금 AuthenticationService와 UserService 인터페이스를 각각 구현하고 있는 authenticationService와 userService 빈들을 가지게끔 해야 한다.






14.4.2.2. Tapestry pages에서 빈들을 얻어오기



이 지점에서, Spring의 정적 유틸리티 함수인 WebApplicationContextUtils.getApplicationContext(servletContext)를 호출함으로써 웹 어플리케이션은 어플리케이션 컨텍스트를 사용할 수 있다. 여기에서의 servletContext는 J2EE 명세의 표준 ServletContext를 의미한다. 그렇기 때문에, 페이지가 예를 들어 UserService 인스턴스를 얻기 위한 한가지 간단한 방법은 다음과 같은 코드를 가지는 것이다.

    WebApplicationContext appContext = WebApplicationContextUtils.getApplicationContext(
getRequestCycle().getRequestContext().getServlet().getServletContext());
UserService userService = (UserService) appContext.getBean("userService");
... some code which uses UserService

이러면 된다. 이런 방식은 page 혹은 component를 위한 베이스 클래스의 메써드에 있는 대부분의 기능들을 캡슐화함으로써 난잡함을 최소화할 수 있다. 그러나, 몇 가지 관점에서 이 방식은 Spring이 장려하고, 이 어플리케이션의 다른 계층에서 사용되고 있는 IoC 접근방식에 반대된다. IoC 접근방식 내에서는 이상적으로는 page가 컨텍스트에 이름으로 특정한 빈을 요청하지 않아야 하며, 사실 이상적으로는 page는 컨텍스트에 대해 전혀 몰라야 한다.


운 좋게도, 이것을 가능하게 하는 방식이 있다. 우리는 Tapestry가 이미 page에 프라퍼티를 선언적으로 추가하는 메커니즘을 가지고 있다는 사실에 의존한다. 그리고 이러한 선언적 형태로 page에 대한 모든 프라퍼티들을 관리하는 앞서 언급한 접근방식이 사실상 사용되기 때문에, Tapestry는 그것들의 생명주기를 page와 component 생명주기의 일부로 잘 관리할 수 있다.






14.4.2.3. 어플리케이션 컨텍스트를 Tapestry에 드러내기



먼저 Tapestry page 혹은 Component가 ServletContext를 가지지 않은 채로 ApplicationContext를 사용할 수 있도록 만들 필요가 있다. 이것은 page 혹은 component의 생명주기 내에서 ApplicationContext에 접근할 필요가 생기는 단계에서, ServletContext는 page에서 쉽게 사용하기 힘들기 때문에 우리는 WebApplicationContextUtils.getApplicationContext(servletContext) 를 직접 사용할 수 없다. 하나의 방법은 이것을 드러내주는 Tapestry IEngine의 커스텀 버전을 정의하는 것이다.

package com.whatever.web.xportal;
...
import ...
...
public class MyEngine extends org.apache.tapestry.engine.BaseEngine {

public static final String APPLICATION_CONTEXT_KEY = "appContext";

/**
* @see org.apache.tapestry.engine.AbstractEngine#setupForRequest(org.apache.tapestry.request.RequestContext)
*/
protected void setupForRequest(RequestContext context) {
super.setupForRequest(context);

// insert ApplicationContext in global, if not there
Map global = (Map) getGlobal();
ApplicationContext ac = (ApplicationContext) global.get(APPLICATION_CONTEXT_KEY);
if (ac == null) {
ac = WebApplicationContextUtils.getWebApplicationContext(
context.getServlet().getServletContext()
);
global.put(APPLICATION_CONTEXT_KEY, ac);
}
}
}

이 엔진 클래스는 Spring 어플리케이션 컨텍스트를 Tapestry 어플리케이션의 '글로벌' 객체에 "appContext"라는 속성으로 위치시킬 것이다. 이 특별한 IEngine 인스턴스가 Tapestry 어플리케이션을 위해 사용된되었다는 사실을 Tapestry 어플리케이션 정의 파일에 시작지점으로 등록하는 것을 확실히 해야 한다. 예를 들어보자.

file: xportal.application:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE application PUBLIC
"-//Apache Software Foundation//Tapestry Specification 3.0//EN"
"http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
<application
name="Whatever xPortal"
engine-class="com.whatever.web.xportal.MyEngine">
</application>






14.4.2.4. Component 정의 파일들



이제 ApplicationContext로부터 우리가 필요한 빈들을 가져오기 위해 page 혹은 component 정의 파일(*.page 혹은 *.jwc)에서 우리는 단순히 property-specification 요소들을 추가하고 그것들을 위한 page 혹은 component 프라퍼티들을 생성하면 된다. 예시를 보자.

    <property-specification name="userService"
type="com.whatever.services.service.user.UserService">
global.appContext.getBean("userService")
</property-specification>
<property-specification name="authenticationService"
type="com.whatever.services.service.user.AuthenticationService">
global.appContext.getBean("authenticationService")
</property-specification>

property-specification 내의 OGNL(Original?) 표현은 프라퍼티를 위한 초기값을 컨텍스트로부터 얻어온 빈으로 나타낸다. 전체 page 정의는 아마 다음과 같을 것이다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE page-specification PUBLIC
"-//Apache Software Foundation//Tapestry Specification 3.0//EN"
"http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">

<page-specification class="com.whatever.web.xportal.pages.Login">

<property-specification name="username" type="java.lang.String"/>
<property-specification name="password" type="java.lang.String"/>
<property-specification name="error" type="java.lang.String"/>
<property-specification name="callback" type="org.apache.tapestry.callback.ICallback" persistent="yes"/>
<property-specification name="userService"
type="com.whatever.services.service.user.UserService">
global.appContext.getBean("userService")
</property-specification>
<property-specification name="authenticationService"
type="com.whatever.services.service.user.AuthenticationService">
global.appContext.getBean("authenticationService")
</property-specification>

<bean name="delegate" class="com.whatever.web.xportal.PortalValidationDelegate"/>

<bean name="validator" class="org.apache.tapestry.valid.StringValidator" lifecycle="page">
<set-property name="required" expression="true"/>
<set-property name="clientScriptingEnabled" expression="true"/>
</bean>

<component id="inputUsername" type="ValidField">
<static-binding name="displayName" value="Username"/>
<binding name="value" expression="username"/>
<binding name="validator" expression="beans.validator"/>
</component>

<component id="inputPassword" type="ValidField">
<binding name="value" expression="password"/>
<binding name="validator" expression="beans.validator"/>
<static-binding name="displayName" value="Password"/>
<binding name="hidden" expression="true"/>
</component>

</page-specification>






14.4.2.5. abstract accessors 추가하기



이제 page 혹은 component 자체를 위한 Java 클래스 정의를 위해, 우리가 정의한 프라퍼티들에 접근하기 위한 추상 getter 메써드를 추가해야 한다. page 또는 component가 실제로 Tapestry에 의해 로드될 때, 그것은 정의된 프라퍼티들을 추가하기 위해 클래스파일에 런타임 코드 방식을 실행하고, 새롭게 생성된 필드들에 대한 추상 getter 메써드들을 연결한다. 다음의 예를 보자.

    // our UserService implementation; will come from page definition
public abstract UserService getUserService();
// our AuthenticationService implementation; will come from page definition
public abstract AuthenticationService getAuthenticationService();

For completeness, the entire Java class, for a login page in this example, might look like this:

package com.whatever.web.xportal.pages;

/**
* Allows the user to login, by providing username and password.
* After successfully logging in, a cookie is placed on the client browser
* that provides the default username for future logins (the cookie
* persists for a week).
*/
public abstract class Login extends BasePage implements ErrorProperty, PageRenderListener {

/** the key under which the authenticated user object is stored in the visit as */
public static final String USER_KEY = "user";

/**
* The name of a cookie to store on the user's machine that will identify
* them next time they log in.
**/
private static final String COOKIE_NAME = Login.class.getName() + ".username";
private final static int ONE_WEEK = 7 * 24 * 60 * 60;

// --- attributes

public abstract String getUsername();
public abstract void setUsername(String username);

public abstract String getPassword();
public abstract void setPassword(String password);

public abstract ICallback getCallback();
public abstract void setCallback(ICallback value);

public abstract UserService getUserService();

public abstract AuthenticationService getAuthenticationService();

// --- methods

protected IValidationDelegate getValidationDelegate() {
return (IValidationDelegate) getBeans().getBean("delegate");
}

protected void setErrorField(String componentId, String message) {
IFormComponent field = (IFormComponent) getComponent(componentId);
IValidationDelegate delegate = getValidationDelegate();
delegate.setFormComponent(field);
delegate.record(new ValidatorException(message));
}

/**
* Attempts to login.
*
* <p>If the user name is not known, or the password is invalid, then an error
* message is displayed.
*
**/
public void attemptLogin(IRequestCycle cycle) {

String password = getPassword();

// Do a little extra work to clear out the password.

setPassword(null);
IValidationDelegate delegate = getValidationDelegate();

delegate.setFormComponent((IFormComponent) getComponent("inputPassword"));
delegate.recordFieldInputValue(null);

// An error, from a validation field, may already have occurred.

if (delegate.getHasErrors())
return;

try {
User user = getAuthenticationService().login(getUsername(), getPassword());
loginUser(user, cycle);
}
catch (FailedLoginException ex) {
this.setError("Login failed: " + ex.getMessage());
return;
}
}

/**
* Sets up the {@link User} as the logged in user, creates
* a cookie for their username (for subsequent logins),
* and redirects to the appropriate page, or
* a specified page).
*
**/
public void loginUser(User user, IRequestCycle cycle) {

String username = user.getUsername();

// Get the visit object; this will likely force the
// creation of the visit object and an HttpSession.

Map visit = (Map) getVisit();
visit.put(USER_KEY, user);

// After logging in, go to the MyLibrary page, unless otherwise
// specified.

ICallback callback = getCallback();

if (callback == null)
cycle.activate("Home");
else
callback.performCallback(cycle);

// I've found that failing to set a maximum age and a path means that
// the browser (IE 5.0 anyway) quietly drops the cookie.

IEngine engine = getEngine();
Cookie cookie = new Cookie(COOKIE_NAME, username);
cookie.setPath(engine.getServletPath());
cookie.setMaxAge(ONE_WEEK);

// Record the user's username in a cookie

cycle.getRequestContext().addCookie(cookie);

engine.forgetPage(getPageName());
}

public void pageBeginRender(PageEvent event) {
if (getUsername() == null)
setUsername(getRequestCycle().getRequestContext().getCookieValue(COOKIE_NAME));
}
}






14.4.3. 개요



이 예제에서, 우리는 Spring ApplicationContext에 정의된 서비스 빈들을 선언적 형태로 page에 제공되도록 처리해왔다. page 클래스는 서비스 구현체들이 어디에서 왔는지 알지 못하며, 때문에 예를 들어, 테스트동안 다른 구현체로 살짝 빠져나가기 쉽다. 이러한 IoC는 Spring 프레임워크의 주요한 목적과 이점의 하나이며, 우리는 이 Tapestry 어플리케이션에서의 J2EE 스택을 향한 모든 지점에서 이것을 확장하도록 있도록 처리해왔다.






14.5. WebWork



WebWork 는 간단함을 염두에 두고 설계된 웹 프레임워크이다. 이것은 일반적인 command 프레임워크인 XWork 를 기반으로 개발되었다. XWork 역시 IoC 컨테이너를 가지고 있지만, 이것은 Spring만큼 총체적인 특징을 가지고 있지 못하며, 이 부분에서 그것을 다루지는 않을 것이다. WebWork 컨트롤러들은 Actions로 불리는데, 그것은 Action 인터페이스를 구현해야만 하기 때문일 것이다. ActionSupport 클래스는 이 인터페이스를 구현하는데, WebWork action들을 위한 일반적인 부모 클래스이다.


WebWork는 xwork-optional 프로젝트의 java.net에 위치한 자체 Spring 통합 프로젝트를 유지한다. 현재적으로, WebWork와 Spring을 통합하는데는 세 가지의 선택이 가능하다.





  • SpringObjectFactory: XWork의 디폴트 ObjectFactory 를 오버라이드 해서 XWork는 Spring 빈들을 루트 WebApplicationContext에서 찾을 것이다.



  • ActionAutowiringInterceptor: Action의 의존성들을 그것들이 생성될 때 인터셉터를 사용하여 자동으로 묶는다.



  • SpringExternalReferenceResolver: <action> 요소의 <external-ref> 요소에서 정의된 이름에 기반하여 Spring 빈들을 룩업한다.


이 모든 전략들은 WebWork's Documentation 에서 보다 상세하게 설명되어 있다.