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

15.1. 소개

이 장은 Spring과 Struts, JSF, Tapestry, 그리고 WebWork와 같은 다른 웹 프레임워크와의 통합을 상세히 다룬다.

Spring프레임워크의 핵심적인 가치를 가지는 유혹(proposition)중 하나는 각각에 대해 선택이 가능하도록 한다는 것이다. 대개 Spring은 특정 구조, 기술 또는 방법론을 사용하거나 구매하도록 강요하지는 않는다(비록 다른 누군가에게 명백하게 추천하더라도). 대부분의 개발자와 개발팀에게 관련된 구조, 기술 또는 방법론을 끄집어내고 선택하기 위한 자유는 같은 시점에 다른 많은 웹 프레임워크와의 통합을 제공하는 Spring이 자체적인 웹 프레임워크(SpringMVC)을 제공하는 웹 영역에서 대부분 명백하다. 이것은 같은 시점에 데이터 접근, 선언적인 트랜잭션 관리 그리고 유연한 설정및 애플리케이션 조합과 같은 다른 영역에서 Spring에 의해 달성되는 이득을 즐길수 있는 반면에 Struts와 같은 유명한 웹 프레임워크를 습득하는 모든 스킬이나 일부에 영향을 지소적으로 끼친다.

이 장의 나머지는 선호하는 웹 프레임워크와 Spring을 통합하는 충실하고 상세한 내용에 집중할것이다. 다른 개발언어에서 Java로 전향하는 개발자들에 의해 종종 남겨지는 글의 내용은 Java로 사용가능한 웹 프레임워크의 풍부함이다. Java영역에는 정말 많은 웹프레임워크가 있다. 사실 하나의 장에서 외형적인 상세함을 너무 많이 다룬다. 이 장은 웹 프레임워크를 모두 지원하는 공통적인 Spring설정을 시작으로 각각의 웹 프레임워크의 개별적인 통합 옵션을 다루면서 Java에서 좀더 유명한 4개의 웹 프레임워크를 다룬다.

이 장에서 지원되는 웹 프레임워크를 사용하는 방법을 설명하고자 하지는 않는다. 예를 들면, 웹 애플리케이션의 표현 레이어를 위해 Struts를 사용하길 원한다면, 이미 우리는 당신이 Struts에 친숙하다고 가정한다. 만약 지원되는 각각의 프레임워크에 대한 상세정보를 얻기를 원한다면, 이 장의 마지막의 Section 15.7, “추가적인 자원” 부분을 보라.

15.2. 공통 설정

지원되는 각각의 웹 프레임워크에 특정한 통합으로 나누기 전에, 하나의 웹 프레임워크에 종속적이지않은 Spring설정을 보자(이 부분은 Spring자체의 웹 프레임워크인 SpringMVC에도 적용가능하다.).

(Spring의) 가벼운 애플리케이션 모델에 의해 채택되는 개념중 하나는 레이어화된(layered) 구조이다. '고전적(classic)' 레이어 구조에서 생각해보자. 웹 레이어는 많은 레이어중 하나는 아니다. 이것은 서버측 애플리케이션에 대한 항목지점(entry point)중 하나로 제공한다. 그리고 이것은 비지니스 종속적인(그리고 표현-기술에 관용적인) 유즈케이스를 만족하기 위한 서비스 레이어에 정의된 서비스 객체(facades)에 위임한다. Spring에서, 서비스 객체, 다른 비지니스-종속 객체, 데이터 접근 객체 등등. 다른 '비지니스 컨텍스트'에 존재하는 웹이나 표현레이어 객체(다른 '표현 컨텍스트(presentation context)'에 대개 설정되는 Spring MVC컨트롤러와 같은 표현 객체)를 포함하지 않는다. 이 부분은 하나의 애플리케이션내 모든 '비지니스 bean'을 포함하는 Spring컨테이너(WebApplicationContext)를 설정하는 방법을 설명한다.

할 필요가 있는 모든것은 ContextLoaderListener 를 표준 J2EE서블릿 web.xml 파일에 선언하고, 어떤 Spring XML설정파일을 로드할것인지 정의하는 contextConfigLocation <context-param>를 사용하기만 하면 된다.

아래 <listener> 설정을 보라.

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

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

Find below the <context-param/> configuration:

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

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

모든 자바 웹 프레임워크들은 Servlet API에 기반하여 만들어졌기 때문에, ContextLoaderListener에 의해 생성된 '비지니스 컨텍스트(business context)' ApplicationContext를 얻기 위해 다음의 코드를 사용할 수 있다.

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

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

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

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

15.3. JavaServer Faces

JavaServer Faces(JSF)는 점점 유명해지는 컴포넌트 기반, 이벤트-주도(driven) 웹 프레임워크이다. Spring의 JSF통합내 핵심 클래스는 DelegatingVariableResolver 클래스이다.

15.3.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>
</faces-config>

DelegatingVariableResolver는 처음엔 값을 룩업하는 것을 기반하는 JSF 구현의 디폴트 처리자에 위임한다. 그리고나서 Spring의 '비지니스 컨텍스트(business context)' WebApplicationContext에 위임한다. 이것은 당신이 JSF에 의해 관리되는 bean들에 의존성을 쉽게 주입할 수 있도록 해준다.

관리되는 bean들은 faces-config.xml 파일에 정의된다. 아래는 Spring '비지니스 컨텍스트(business context)' 로부터 가져온 bean인 #{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>

15.3.2. FacesContextUtils

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

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

DelegatingVariableResolver는 JSF와 Spring 통합을 위해 추천되는 전략이다. 만약 좀더 견고한 통합 기능을 찾는다면, JSF-Spring 프로젝트를 찾길 원할지도 모른다.

15.4. Struts

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

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

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

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

15.4.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>type 속성에 DelegatingActionProxy 클래스를 사용한다.

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

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

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

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

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

[Note]Note

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

15.4.1.2. DelegatingActionProxy

만약 사용자정의 RequestProcessor를 가지고 있고 DelegatingRequestProcessorDelegatingTilesRequestProcessor를 사용할 수 없다면, 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에서의 bean 정의는 사용자정의 RequestProcessorDelegatingActionProxy를 사용하는 것에 상관없이 동일하다.

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

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

15.4.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은 두 개의 프레임워크들을 통합하기 위해 몇몇 멋진 옵션들을 제공한다.

15.5. Tapestry

Tapestry 홈페이지로 부터...

Tapestry는 Java로 동적이고 견고하며 높은 확장성을 가진 웹 애플리케이션을 만들기 위한 오픈소스 프레임워크이다. Tapestry 는 표준 Java Servlet API를 보완하고 빌드한다. 그래서 서블릿 컨테이너나 애플리케이션 서버에서 작동한다.

Spring이 자체의 강력한 웹 레이어를 가지는 반면, 웹 유저 인터페이스를 위한 Tapestry와 낮은 레이어를 위한 Spring컨테이너의 조합을 사용하여 J2EE애플리케이션을 빌드하기 위한 유일한 장점을 많이 있다. 웹 통합부분은 이러한 두개의 프레임워크를 조합하기 위한 몇몇 가장 좋은 상황을 설명하는 것을 시도한다.

Tapestry와 Spring으로 빌드된 전형적으로 계층화된 J2EE애플리케이션은 하나 이상의 Spring컨테이너에 의해 묶인 Tapestry로 빌드된 가장 상위의 유저 인터페이스(UI) 레이어, 많은 수의 하위 레이어를 구성할것이다. Tapestry 자체의 참조문서 는 가장 좋은 상황에 대한 다음의 충고를 포함한다(내가 작성한 텍스트는 []내 포함된다.) .

Tapestry에서 가장 성공한 디자인 패턴은 페이지를 유지하고 컴포넌트를 매우 간단한 상태로 유지한다. 그리고 가능한한 HiveMind [또는 Spring, 이나 어떤것들] 서비스로 많은 로직을 위임한다. 리스너 메소드는 정확한 정보와 함께 직렬화되는것보다 조금 작동하고 서비스로 이것을 전달한다.

핵심 질문은 협력 서비스를 가진 Tapestry페이지를 어떻게 제공하는가 ? 대답은 이러한 서비스를 Tapestry페이지로 직접 의존성 삽입하는 것을원하는 것이다. Tapestry에서, 더양한 도구에 의해 의존성 삽입에 영향을 줄수 있다. 이 부분은 Spring에 의해 발생할수 있는 의존성삽입을 설명할것이다. Spring-Tapestry 통합에서 실제로 멋진 것은 Tapestry의 멋지고 유연한 디자인 자체가 Spring관리 bean의 의존성 삽입을 달성한다는것이다.(다른 좋은 것은 Tapestry생성자인 Howard M. Lewis Ship에 의해 Spring-Tapestry통합 코드가 작성되고 유지가 지속된다는 것이다. 그래서 유연한 통합이 된다.)

15.5.1. Injecting Spring-managed beans

Assume we have the following simple Spring container definition (in the ubiquitous XML format):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" 
        "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
 
<beans>
    <!-- the DataSource -->
    <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName" value="java:DefaultDS"/>
    </bean>

    <bean id="hibSessionFactory" 
          class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="transactionManager" 
          class="org.springframework.transaction.jta.JtaTransactionManager"/>

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

    <!-- (transactional) AuthenticationService -->
    <bean id="authenticationService" 
          class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager" ref="transactionManager"/>
        <property name="target">
            <bean class="com.whatever.services.service.user.AuthenticationServiceImpl">
                <property name="mapper" ref="mapper"/>
            </bean>
        </property>
        <property name="proxyInterfacesOnly" value="true"/>
        <property name="transactionAttributes">
            <value>
                *=PROPAGATION_REQUIRED
            </value>
        </property>
    </bean>  
 
    <!-- (transactional) UserService -->
    <bean id="userService" 
          class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager" ref="transactionManager"/>
        <property name="target">
             <bean class="com.whatever.services.service.user.UserServiceImpl">
                 <property name="mapper" ref="mapper"/>
             </bean>
        </property>
        <property name="proxyInterfacesOnly" value="true"/>
        <property name="transactionAttributes">
            <value>
                *=PROPAGATION_REQUIRED
            </value>
        </property>
    </bean>  
 
 </beans>

Tapestry 애플리케이션 내부에서, 위 bean정의는 Spring컨테이너에 로드될 필요가 있고 관련 Tapestry페이지는 각각 AuthenticationService 인터페이스와 UserService 인터페이스를 구현하는 authenticationService bean과 userService bean과 함께 제공(삽입)될 필요가 있다.

이 지점에서, 애플리케이션 컨텍스트는 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

이 기법은 언급한것처럼 작동한다. 이것은 페이지나 컴포넌트를 위한 기본(base) 클래스내 메소드내 대부분의 기능을 캡슐화하여 다서 덜 장황하다. 어쨌든, 이 애플리케이션의 다른 레이어에서 사용되는 몇가지 관점에서, Spring이 지원하는 IoC접근법에 대해 반대로 작동한다. 이론적으로 당신은 이름(name)에 의해 종속 bean을 위한 컨텍스트를 요청하지 않기 위한 페이지를 좋아할것이다. 그리고 사실, 페이지는 컨텍스트에 대해 알지 않는다.

운좋게, 여기에 이것을 허용하는 기법이 있다. Tapestry는 프라퍼티를 페이지에 명시적으로 추가하기 위한 기법을 이미 가지고 있다. 그리고 이것은 선언적인 방식으로 페이지의 모든 프라퍼티를 관리하기 위한 접근법을 선호한다. 그래서 Tapestry는 아마도 페이지와 컴포넌트 생명주기의 일부처럼 자체의 생명주기를 관리할수 있다.

[Note]Note

다음 부분은 Tapestry 4.0이전 버전에 사용가능하다. 만약 Tapestry 4.0+을 사용한다면, Section 15.5.1.4, “Tapestry 페이지에 Spring Beans 의존성 삽입하기 - Tapestry 4.0이상의 버전 스타일”를 보라.

15.5.1.1. Tapestry페이지에 대한 Spring Bean 의존성 삽입

먼저 우리는 ServletContext를 가지지 않고 Tapestry페이지나 컴포넌트에 사용가능한 ApplicationContext를 만들필요가 있다. 이것 때문에, 우리가 ApplicationContext에 접근할때 페이지/컴포넌트의 생명주기에서, ServletContext를 위해 페이지에 쉽게 사용가능하지 않을것이다. 그래서 우리는 직접 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);
        }
    }
}

이 엔진 클래스는 Tapestry애플리케이션의 '전역(Global)' 객체내 'appContext' 라고 불리는 속성처럼 Spring 애플리케이션 컨텍스트를 둔다. 특별한 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>

15.5.1.2. 컴포넌트 정의 파일

우리의 페이지나 컴포넌트 정의 파일(*.page 나 *.jwc)에서, 우리는 ApplicationContext를 필요로 하고 페이지나 컴포넌트 프라퍼티를 생성하는 bean을 가로채기 위해 프라퍼티 성격의 요소를 간단히 추가한다. 예를 들면:

    <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>

프라퍼티 특성 내부의 OGNL표현은 컨텍스트로부터 얻어지는 bean처럼 프라퍼티를 위한 초기값을 명시한다. 전체 페이지 정의는 다음과 같을것이다.

<?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>

15.5.1.3. 추상 접근자(accessor) 추가하기

지금 페이지나 컴포넌트 자체를 위한 Java클래스 정의내에서, 우리가 해야할 필요가 있는 모든것은 우리가 정의(언급된 프라퍼티에 접근하기 위해)한 프라퍼티를 위한 추상 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();

완성을 위해, 이 예제내 로그인 페이지를 위한 전체 Java클래스는 다음과 같을 것이다.

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 the cookie that identifies a user **/
    private static final String COOKIE_NAME = Login.class.getName() + ".username";  
    private final static int ONE_WEEK = 7 * 24 * 60 * 60;
 
    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();
 
    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);
        }

        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));
        }
    }
}

15.5.1.4. Tapestry 페이지에 Spring Beans 의존성 삽입하기 - Tapestry 4.0이상의 버전 스타일

Tapestry 4.0이상의 버전에서 Tapestry 페이지에 Spring관리 bean의 의존성 삽입의 영향은 좀더 단순하다. 해야 할 필요가 있는 것은 하나의 애드온 라이브러리, 와 몇몇 설정, 다른 웹 프레임워크에 의해 요구되는 다른 라이브러리와 함께(대개 WEB-INF/lib에서) 간단히 패키징하고 배치하는 것이다.

그리고 나서 이전에 이미 언급된 메소드를 사용하여 Spring컨테이너를 생성하고 나타낼 필요가 있다. Tapestry에 Spring 관리 bean을 매우 간단히 삽입할수 있다. 만약 Java 5를 사용한다면, 위 Login 페이지를 보라. 우리는 Spring관리 userServiceauthenticationService객체의 의존성 삽입을 위한 적절한 getter메소드를 주석처리할 필요가 있다.

package com.whatever.web.xportal.pages;

public abstract class Login extends BasePage implements ErrorProperty, PageRenderListener {
    
    @InjectObject("spring:userService")
    public abstract UserService getUserService();
    
    @InjectObject("spring:authenticationService")
    public abstract AuthenticationService getAuthenticationService();

}

지금 시점에서는 많은 작업을 수행했다. 남은 것은 HiveMind서비스로 ServletContext내 저장된 Spring 컨테이너를 나타내는 HiveMind설정이다.

<?xml version="1.0"?>
<module id="com.javaforge.tapestry.spring" version="0.1.1">

    <service-point id="SpringApplicationInitializer"
        interface="org.apache.tapestry.services.ApplicationInitializer"
        visibility="private">
        <invoke-factory>
            <construct class="com.javaforge.tapestry.spring.SpringApplicationInitializer">
                <set-object property="beanFactoryHolder"
                    value="service:hivemind.lib.DefaultSpringBeanFactoryHolder" />
            </construct>
        </invoke-factory>
    </service-point>

    <!-- Hook the Spring setup into the overall application initialization. -->
    <contribution
        configuration-id="tapestry.init.ApplicationInitializers">
        <command id="spring-context"
            object="service:SpringApplicationInitializer" />
    </contribution>

</module>

만약 Java 5를 사용한다면(게다가 주석(annotations)에 접근한다면), 이것은 다 된셈이다.

만약 Java 5를 사용하지 않는다면, 주석을 가진 Tapestry페이지 클래스를 주석처리하지 않는다. 대신 의존성 삽입을 명시하기 위해 좋은 예전 형태의 XML을 사용한다. Login 페이지(나 컴포넌트)를 위한 .page.jwc 파일내부는 ..

<inject property="userService" object="spring:userService"/>
<inject property="authenticationService" object="spring:authenticationService"/>

이 예제에서, 우리는 선언적인 형태로 Tapestry페이지를 위해 제공되는 Spring컨터이너내 정의된 서비스 bean을 허용하기 위해 관리된다. 페이지 클래스는 서비스 구현물이 어디서 발생하는지 알지 않는다. 사실, 예를 들어, 테스팅하는 동안 다른 구현물을 빠뜨리기 쉽다. 이 IoC는 Spring프레임워크의 목표와 이득중 하나이다. 그리고 우리는 이 Tapestry애플리케이션내 J2EE스택을 모두 확장하기 위해 관리된다.

15.6. WebWork

WebWork 홈페이지로 부터...

WebWork는 Java 웹 애플리케이션 개발 프레임워크이다. 이것은 폼 제어, UI테마, 국제화, JavaBean에 대한 동적 폼 파라미터 맵핑, 견고한 클라이언트및 서버측 유효성 체크 그리고 다른 많은 것들과 같은 재사용가능한 UI템플릿을 빌드하기 위한 견고한 지원을 제공하여 개발자 생산성과 코드 단순화를 가진채 명백하게 빌드되었다.

WebWork는 매우 깔끔하고, 정밀한 웹 프레임워크이다(저자의 의견을 따르면). 이 구조와 key개념은 이해하기 매우 쉬울뿐 아니라 풍부한 태그 라이브러리, 제대로 디-커플링된 유효성 체크(validation)를 가지고 있다. 그리고 다음번에 생산되기 쉽도록 되어 있다.

WebWork의 기술 스택내 핵심 가능자(enabler)중 하나는 Webwork Action을 관리하고 비지니스 객체의 "wiring"을 다루는 IoC 컨테이너이다. WebWork버전 2.2이전에는, WebWork가 자체적으로 적절한 IoC컨테이너를 사용했다(그리고 제공된 통합 지점은 Spring처럼 IoC컨테이너와 통합할수 있다). 어쨌든, WebWork 2.2처럼, WebWork내 사용되는 디폴트 IoC컨테이너는 Spring이다. Spring개발자라면 이러한 사항은 굉장히 멋진 소식이다. 왜냐하면 이것은 IoC설정의 기본에 이미 친숙하다는 것을 의미한다.

지금 DRY(Dont Repeat Yourself - 당신 스스로 되풀이 하지말라) 법칙을 따른다면, WebWork팀이 이미 작성해둔 Spring-WebWork통합을 다시 작성하는 것을 행하지 말라. WebWork 위키에서 Spring-WebWork 통합 페이지를 보라.

Spring-WebWork통합 코드는 WebWork개발자들에 의해 개발되었다. WebWork사이트에서 참조하라. 관련글은 Spring 지원 포럼에서 볼수 있다.

15.7. 추가적인 자원

아래는 이 장에서 언급한 다양한 웹 프레임워크에 대한 추가적인 자원에 대한 링크이다.