Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 18장 다른 웹 프레임워크와의 통합

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



18. 다른 웹 프레임워크와의 통합

18.1 소개
이번 장에서는 JSF, Struts, WebWork, Tapestry같은 서드파티 웹 프레임워크와 스프링의 통합에 대해서 다룬다.

스프링 웹 플로우(Spring Web Flow)
스프링 웹 플로우 (SWF, Spring Web Flow)는 웹 어플리케이션 페이지 흐름을 관리하는 최상의 솔루션이다.

SWF는 서블릿 환경과 포틀릿 환경에서 모두에서 스프링 MVC, 스트럿츠, JSF같은 프레임워크와 통합한다. 순사하게 요청 모델과는 반대로 대화식 모델의 이점을 갖는 비즈니스 처리과정을 가진다면 SWF가 해결책이 될 것이다.

SWF는 여러 상황에서 재사용할 수 있는 내장된 모쥴처럼 논리적인 페이지 흐름을 갖을 수 있도록 하고 비즈니스 처리과정을 유도하는 제어된 네비게이션드로 사용자를 도와주는 웹 어플리케이션 모듈을 구성하는데 최적이다.

SWF에 대한 자세한 내용은 Spring Web Flow 웹사이트를 참고해라.

스프링 프레임워크가 제시하는 핵심 가치 중 하나는 선택권을 주는 것이다. 일반적으로 스프링은 특정 아치텍처나 기술, 방법론을 사용하도록 강제하지 않는다. (다른 것들보다 권장하는 것들이 있기는 하지만) 개발자나 개발팀과 가장 관련있는 아키텍처, 기술, 방법론을 선택할 수 있는 자유는 스프링이 스프링의 웹 프레임워크 (Spring MVC)를 제공하는 동시에 다수의 인기있는 서드파티 웹 프레임워크과 통합을 제공하는 것은 웹 분야에서 아주 명백하다. 이는 Struts같은 특정 웹 프레임워크에 대해 이미 가지고 있는 스킬을 계속해서 사용하면서 동시에 데이터 접근, 선언적인 트랜잭션 관리, 유연한 설정과 어플리케이션 구성 부분에서 스프링이 제공하는 장점을 사용할 수 있게 한다.

이번 장에서는 스프링과 선호하는 웹프레임워크와의 통합에 대해서 아주 자세하게 다룰 것이다. 다른 언어에서 자바로 넘어온 개발자들은 자바 분야에서는 엄청나게 많은 웹프레임워크를 사용할 수 있다고 종종 이야기한다. 사실 이러한 프레임워크의 세세한 내용을 하나의 장에서 다 다루기에는 내용이 너무 많다. 그래서 이번 장에서는 자바에서 인기있는 네가지 웹 프레임워크를 골랐고 지원하는 모든 웹프레임워크에 공동되는 스프링 설정부터 시작해서 지원하는 각 웹 프레임워크에 대한 통합 옵션에 대한 자세한 내용을 다룬다.

Note
이번 장에서는 지원하는 웹 프레임워크를 어떻게 사용하는지는 설명하지 않는다. 예를 들어 웹 어플리케이션에서 표현 계층으로 Struts를 사용하고자 한다면 이미 Struts에 익숙하다고 가정한다. 지원하는 웹 프레임워크 자체에 대한 자세한 내용은 이번 장 마지막에 있는 Section 18.7, “추가 자료”를 참고해라.



18.2 공통 설정
지원하는 각 웹 프레임워크의 통합과 관련해서 구체적으로 들어가기 전에 특정 웹 프레임워크에 한정되지 않은 스프링 설정을 먼저 보자.(이번 섹션은 스프링의 웹 프레임워크인 스프링 MVC에도 동일하게 적용할 수 있다.)

(스프링의) 경량 어플리케이션 모델이 지지하는 개념(더 좋은 단어를 찾지 못했다) 중 하나는 계층화된 아키텍처이다. '전통적인' 계층화된 아키텍처에서 웹 계층은 많은 계층 중에 하나라는 것을 기억해라. 웹 계층은 서버측의 진입점 중 하나이면서 비즈니스에 특화된(그러면서 표현기술과는 상관없이) 사용사례를 만족시키기 위해 서비스 계층에 정의된 서비스 객체(퍼사드)에 위임한다. 스프링에서는 이러한 서비스 객체, 비즈니스에 특화된 객체. 데이터접근 객체 등은 웹 계층이나 표현계층 객체(스프링 MVC 컨트롤러같은 표현 객체는 보통 별도의 '표현(presentation) 컨텍스트'에 설정한다)를 담고 있지 않은 별개의 '비즈니스 컨텍스트'에 존재한다. 이번 섹션에서는 어플리케이션의 모든 '비즈니즈 빈'을 담고 있는 스프링 컨테이너 (WebApplicationContext)를 설정하는 방법을 설명한다.

구체적으로 들어가서 모든 어플리케이션은 웹 어플리케이션의 표준 Java EE 서블릿 web.xml에서 ContextLoaderListener를 선언하고 로딩해야하는 스프링 XML 설정파일들을 정의하는 contextConfigLocation <context-param/> 부분(같은 파일에)을 추가해야 한다.

다음의 <listener/> 설정을 봐라.

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

다음의 <context-param/> 설정을 봐라.

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

contextConfigLocation 컨텍스트 파라미터를 지정하지 않았다면 ContextLoaderListener는 로딩할 /WEB-INF/applicationContext.xml 파일을 찾을 것이다. 컨텐스트 파일이 로딩되고 나면 빈 정의에 기반해서 WebApplicationContext 객체를 생성하고 웹 어플리케이션의 ServletContext에 저장한다.

모든 자바 웹 어플리케이션은 서블릿 API를 기반으로 만들어졌다. 그래서 ContextLoaderListener가 생성한 이 '비즈니스 컨텍스트' ApplicationContext에 접근하는 다음 코드를 사용할 수 있다.

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

WebApplicationContextUtils 클래스는 평의상 제공되는 클래스이므로 ServletContext 속성의 이름을 기억할 필요가 없다. WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 키 아래 객체가 존재하지 않는다면 WebApplicationContextUtils의 getWebApplicationContext() 메서드는 null을 반환할 것이다. 어플리케이션에서 NullPointerExceptions를 얻는 위험보다는 getRequiredWebApplicationContext() 메서드를 사용하는 것이 더 낫다. 이 메서드는 ApplicationContext가 누락되었을 때 예외를 던진다.

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

다행히 이번 섹션에서 다루는 대부분의 프레임워크는 더 간단한 빈 검색방법을 가진다. 스프링 컨테이너에서 빈을 쉽게 가져오게 할 뿐만 아니라 컨트롤러에서 의존성 주입을 사용할 수 있게 한다. 더 자세한 각각의 통합 전략은 각 웹프레임워크 부분에서 다룬다.


18.3 자바서버 페이스(JavaServer Faces) 1.1와 1.2
자바서버 페이스(JSF, JavaServer Faces)는 컴포넌트 기반이면서 이벤트 주도인 JCP의 표준 웹 유저인터페이스 프레임워크이다. Java EE 5부터 JSF는 Java EE의 공식으로 포함되었다.

인기있는 JSF 컴포넌트 라이브러리와 JSF 런타임이 필요하다면 Apache MyFaces 프로젝트를 봐라. MyFaces 프로젝트도 풍부한 컨버세이션 범위(conversation scope)를 지원하는 스프링 기반의 JSF 확장인 MyFaces Orchestra같은 공통 JSF 확장을 제공한다.

Note
스프링 웹 플로우 2.0은 JSF 중심적인 사용(이번 섹션에서 설명하듯이)과 스프링 중심적인 사용(스프링 MVC 디스패처내에서 JSF 뷰를 사용해서)을 모두 제공하는 새롭게 만들어진 스프링 페이스(Faces) 모듈로 풍부한 JSF를 지원한다. 자세한 내용은 Spring Web Flow 웹사이트를 확인해라!

스프링의 JSF 통합의 핵심요소는 JSF 1.1 VariableResolver 메카니즘이다. JSF 1.2에서 스프링은 JSF EL 통합의 차세대 버전처럼 ELResolver 메카니즘을 지원한다.


18.3.1 DelegatingVariableResolver (JSF 1.1/1.2)
스프링 미들티어를 JSF 웹 계층과 통합하는 가장 쉬운 방법은 DelegatingVariableResolver 클래스를 사용하는 것이다. 어플리케이션에서 이 변수리졸버(variable resolver)를 설정하려면 faces-context.xml 파일을 수정해야 한다. <faces-config/> 요소를 연 뒤에 요소안에 <application/> 요소와 <variable-resolver/> 요소를 추가해라. 변수 리졸버의 값은 스프링의 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 구현체의 기본 리졸버에 값 검색을 위임하고 그 다음에는 스프링의 '비즈니스 컨텍스트' WebApplicationContext에 위임한다. 이는 JSF과 관리하는 빈에 의존성을 쉽게 주입할 수 있게 한다.

관리하는 빈은 faces-config.xml 파일에 정의한다. #{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>


18.3.2 SpringBeanVariableResolver (JSF 1.1/1.2)
SpringBeanVariableResolver는 DelegatingVariableResolver의 변형이다. SpringBeanVariableResolver는 먼저 스프링의 '비즈니스 컨텍스트' WebApplicationContext에 위임하고 그 다음 사용하는 JSF 구현체의 기본 리졸버에 위임한다. 이는 특히 전문화된 스프링 처리(resolution) 규칙을 가진 요청/세션 범위의 빈을 사용하는 경우 유용하다. 스프링 FactoryBean 구현체가 그 예이다.

설정은 faces-context.xml파일에 SpringBeanVariableResolver를 정의해라.

<faces-config>
  <application>
    <variable-resolver>org.springframework.web.jsf.SpringBeanVariableResolver</variable-resolver>
    ...
  </application>
</faces-config>


18.3.3 SpringBeanFacesELResolver (JSF 1.2+)
SpringBeanFacesELResolver는 JSF 1.2를 따르는 ELResolver 구현체로 JSF 1.2와 JSP 2.1가 사용하는 표준 Unified EL와 통합한다. SpringBeanVariableResolver처럼 SpringBeanFacesELResolver는 먼저 스프링의 '비즈니스 컨텍스트' WebApplicationContext에 위임하고 그 다음에 사용하는 JSF 구현체의 기본 리졸버에 위임한다.

설정은 JSF 1.2 faces-context.xml파일에 SpringBeanFacesELResolver를 정의해라.

<faces-config>
  <application>
    <el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
    ...
  </application>
</faces-config>


18.3.4 FacesContextUtils
faces-config.xml에서 프로퍼티를 빈에 매핑할 때 커스텀 VariableResolver가 잘 동작하지만 때로는 명시적으로 빈을 가져와야 한다. FacesContextUtils 클래스가 이를 쉽게 해준다. FacesContextUtils는 ServletContext파라미터 대신 FacesContext 파리미터를 받는다는 점을 제외하면 WebApplicationContextUtils와 유사하다.

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


18.4 Apache Struts 1.x and 2.x
Struts는 처을 나온 웹 프레임워크중 하나였기 때문에(2001년 6월) 자바 어플리케이션의 사실상 표준(de facto) 웹 프레임워크였다. 지금은 Struts 1이름이 바뀌었다.(Struts 2와 대조적으로) 많은 어플리케이션이 아직도 스트럿츠를 사용한다. Craig McClanahan가 만든 스트럿츠는 아파치 소프트웨어 재단(Apache Software Foundation)에서 호스팅되는 오프소스 프로젝트이다. 그 당시에 스트럿츠는 JSP/서블릿 프로그래밍 패러다임을 엄청나게 간소화했고 자신의 프레임워크를 사용하는 수많은 개발자들을 끌어들였다. 스트럿츠는 프로그래밍 모델을 간소화하고 오픈소스였으며 (그러므로 무료다) 프로젝트가 성장하고 자바 웹 개발자사이에서 인기있게 만드는 커다란 커뮤티니를 가지고 있었다.

Note
다름 섹션에서는 스트럿츠 1 가칭 "스트럿츠 클래식"을 설명한다.

Struts 2는 사실상 완전히 다른 제품으로 WebWork 2.2 (Section 18.5, “웹워크(WebWork) 2.x”에서 설명한다)의 후계자로 지금은 스트럿츠 브랜드로 배포된다. 스트럿츠 2에 내장된 스프링 통합과 관련해서는 스트럿츠 2 스프링 플러그인을 확인해 봐라. 보통 스트럿츠 2는 스프링 통합의 면에서 스트럿츠 1보다는 웹워크 2.2에 더 가깝다.

스트럿츠 1.x 어플리케이션과 스프링을 통합하기 위한 두가지 선택사항이 있다.

  • ContextLoaderPlugin를 사용해서 스프링이 액션(Action)을 빈으로 관리하도록 설정하고 스프링 컨텍스트 파일에 앳션의 의존성을 설정한다.
  • 스프링의 ActionSupport 클래스의 하위클래스를 만들고 getWebApplicationContext() 메서드를 사용해서 명시적으로 스프링이 관리하는 빈을 가져온다.


18.4.1 ContextLoaderPlugin
ContextLoaderPlugin는 스트럿츠 ActionServlet에 대한 스프링 컨텍스트 파일을 로드하는 스트럿츠 1.1+ 플러그인이다. 이 컨텐스트는 루트 WebApplicationContext (ContextLoaderListener가 로드한)를 자신의 부모로 참조한다. 컨텍스트 파일의 기본 이름은 매핑된 서블릿 이름에 -servlet.xml를 추가한 이름이다. ActionServlet이 web.xml에 <servlet-name>action</servlet-name>로 정의되었다면 기본 이름은 /WEB-INF/action-servlet.xml이다.

이 플러그인을 설정하려면 struts-config.xml 파일 하단의 플러그인 부분에 다음 XML을 추가해라.

<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,/WEB-INF/applicationContext.xml"/>
</plug-in>

모든 컨텍스트 파일을 로드하는데 이 플러그인을 사용할 수 있는데 StrutsTestCase같은 테스트도구를 사용하는 경우 유용할 수 있다. StrutsTestCase의 MockStrutsTestCase는 구동할 때 리스너(Listener)를 초기화하지 않으므로 플러그인에 모든 컨텍스트 파일을 두는 것이 우회방법이다. 이 이슈에 관한 버그가 올라왔지만 'Wont Fix'로 닫혔다.

struts-config.xml에 이 플러그인을 설정한 후에는 Action을 스프링이 관리하도록 설정할 수 있다. 스프링 (1.1.3+)는 이를 위한 두가지 방법을 제공한다.

  • 스트럿츠의 기본 RequestProcessor를 스프링의 DelegatingRequestProcessor를 오버라이드한다.
  • <action-mapping>의 type 속성에 DelegatingActionProxy 클래스를 사용해라.
이러한 두 메서드 모두 action-servlet.xml의 Action과 Action의 의존성을 관리할 수 있게 해준다. struts-config.xml과 action-servlet.xml의 Action간의 브릿지는 action-mapping의 "path"와 빈의 "name"으로 만들어진다. struts-config.xml 파일에 다음 코드가 있다면

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

action-servlet.xml에 "/users" 이름으로 Action의 빈을 정의해야 한다.

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


18.4.1.1 DelegatingRequestProcessor
struts-config.xml 파일에 DelegatingRequestProcessor를 설정하려면 <controller>요소에 "processorClass" 프로퍼티를 오버라이드해라. 이러한 라인은 <action-mapping> 요소 다음에 온다.

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

이 설정을 추가한 후에는 타입에 상관없이 스프링 컨텍스트 파일에서 Action을 자동적으로 검색할 것이다. 사실 심지어 타입을 지정할 필요도 없다. 다음 두 코드 모두 동작할 것이다.

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

스트럿츠의 모듈 기능을 사용한다면 빈 이름이 모듈 접두사를 포함하고 있어야 한다. 예를 들어 모듈 접두사 "admin"으로 <action path="/user"/>와 같이 정의한 액션은 <bean name="/admin/user"/>라는 빈 이름을 필요로 한다.

Note
스트럿츠 어플리케이션에서 타일즈(Tiles)를 사용한다면 <controller>를 DelegatingTilesRequestProcessor로 설정해야 한다.



18.4.1.2 DelegatingActionProxy
커스텀 RequestProcessor를 가지고 있고 DelegatingRequestProcessor나 DelegatingTilesRequestProcessor의 접근을 사용할 수 없다면 action-mapping의 타입으로 DelegatingActionProxy를 사용할 수 있다.

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

커스텀 RequestProcessor를 사용하든지 DelegatingActionProxy를 사용하든지 간에 action-servlet.xml의 빈 정의는 동일하다.

컨텍스트 파일에 Action을 정의하면 스프링 빈 컨테이너의 전체 기능을 Action에 사용할 수 있다. 각 요청에 새로운 Action 인스턴스를 인스턴스화하는 선택권뿐만 아니라 의존성 주입까지 사용할 수 있다. Action 인스턴스를 인스턴스화하는 선택권을 활성화하려면 Action의 빈 정의에 scope="prototype"를 추가해라.

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


18.4.2 ActionSupport 클래스
이전에 얘기했듯이 WebApplicationContextUtils 클래스를 사용하는 ServletContext에서 WebApplicationContext를 얻어올 수 있다. 스트럿츠를 위해서 스프링의 Action 클래스를 확장하는 것이 가장 쉬운 방법이다. 예를 들어 스트럿츠의 Action 클래스의 하위클래스를 만드는 대신에 스프링의 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");
    // manager에게 비즈니스로직을 알려준다
    return mapping.findForward("success");
  }
}

스프링은 모든 표준 스트럿츠 액션의 하위클래스를 가진다. 스프링의 하위클래스들은 그저 이름에 Support를 추가했을 뿐이다.

프로젝트에 가장 적합한 접근을 사용하는 것이 추천하는 전략이다. 서브클래스를 만드는 것은 코드의 가독성을 더 좋게 하고 의존성을 어떻게 처리해야 하는지 정확히 알고 있다. 반면에 ContextLoaderPlugin를 사용하는 방법은 컨텍스트 XML 파일에 새로운 의존성을 쉽게 추가할 수 있게 한다. 어떤 방법이든 스프링은 스트럿츠와 통합하는 좋은 선택권을 제공한다.


18.5 웹워크(WebWork) 2.x
다음은 WebWork 홈페이지에서 발췌한 내용이다.

“ WebWork는 자바 웹 어플리케이션 개발 프레임워크이다. 웹워크는 특히 개발생산성과 코드 간소화에 초점을 맞추어서 만들어졌고 폼 컨트롤, UI 테마, 국제화, 자바빈으로의 동적 폼 파라미터 매핑, 풍부한 클라이언트와 서버사이드 유효성 검사등의 재사용 가능한 UI 템플릿을 만드는 것을 강력히 지원한다. ”
웹워크의 아키텍처와 개념은 이해하기 쉽다. 웹워크는 광범위한 태그 라이브러리와 잘 디커플링된 유효성검사도 가진다.

웹워크 기술 스택의 핵심 중 하나는 IoC 컨테이너이다. IoC 컨테이너가 웹워크 액션을 관리하고 비즈니스 객체의 "연결"을 다룬다. 웹워크 2.2 이전의 버전에서는 웹워크 자체 IoC 컨테이너를 사용했다.(그리고 스프링을 함께 사용하는 등의 IoC 컨테이너를 통합할 수 있는 통합 포인트를 제공했다.) 하지만 웹워크 2.2 버전 부터는 웹워크내에서 사용하는 기본 IoC 컨테이너가 스프링이다. 이는 웹워크에서 IoC 설정의 기본, 이디엄(idiom)등에 바로 익숙해 질 수 있다는 의미이므로 스프링 개발자들에게는 분명히 대단한 소식이다.

DRY (Don't Repeat Yourself) 원칙을 따르기 위해 웹워크 팀이 이미 작성해놓았으므로 스프링-웹워크 통합에 대한 문서를 작성하는 것은 어리석은 일이다. 자세한 내용은 WebWork 위키에서 Spring-WebWork 통합 페이지를 참고해라.

Spring-WebWork 통합 코드는 웹워크 개발자들이 직접 작성했다.(그리고 계속해서 유지보수하고 향상시키고 있다.) 그러므로 통합과 관련된 이슈가 있다면 웹워크 사이트와 포럼을 먼저 참고해라. 하지만 Spring 지원 포럼에도 스프링-웹워크 통합과 관련된 질문을 올려도 된다.


18.6 테피스트리(Tapestry) 3.x와 4.x
다음은 Tapestry 홈페이지에서 발췌한 내용이다.

“ 테피스트리는 동적이면서 튼튼하고 확장성이 좋은 자바 웹 어플리케이션을 위한 오픈소스 프레임워크이다. 테피스트리는 표준 자바 서블릿 API위에 만들어졌으므로 어떤 서블릿 컨테이너나 어플리케이션 서버와도 동작한다. ”
스프링은 자체의 강력한 웹 계층을 가지지만 웹 유저 인터페이스에는 테피스트리를 하위 계층에는 스프링 컨테이너를 섞어서 사용하는 엔터프라이즈 자바 어플리케이션을 만들때 여러 장점이 있다. 웹 통합 장의 이번 섹션에서는 두 프레임워크를 같이 사용하는 좋은 사용사례를 설명한다.

테피스트리와 스프링으로 만들어진 일반적인 계층화된 엔터프라이즈 자바 어플리케이션은 테피스트리로 만들어진 상위 유저 인터페이스(UI) 계층과 다수의 하위 계층을 하나 이상의 스프링 컨테이너로 연결해서 구성할 것이다. 테피스트리의 레퍼런스 문서에 좋은 사용사례로 다음 코드가 나와있다. (이 스프링 문서의 작성자가 추가한 부분은 []안에 있다.)

“ 테피스트리의 가장 성공적인 디자인 페턴은 페이지와 컴포넌트랄 아주 간단하게 유지하고 HiveMind(또는 스프링 등등) 서비스로 가능한한 많은 로직을 위임한다. 리스너 메서드는 제대로된 정보를 함께 마샬링하는 것보다 약간 더 이상적으로 수행하고 이를 서비스에 전달한다. ”
핵심 질문은 "어떻게 테피스트리 페이지를 연관된 서비스와 함께 제공하는가?"이고 대답은 이상적으로는 서비스의 테피스트리 페이지에 직접 서비스를 의존성 주입하는 것이다. 테피스트리에서 여러가지 방법으로 이 의존성 주입을 할 수 있다. 이번 섹션에서는 스프링이 할 수 있는 의존성 주입의 의미를 열거할 것이다. 이 스프링-테피스트리 통합과 관련되서 진짜 좋은 점은 테피스트리 자체의 세련되고 유연한 디자인이 스프링이 관리하는 빈의 의존성 주입을 쉽게 하는 것이다. (또 다른 좋은 점은 스프링-테피스트리 통합 코드를 테피스트리를 만든 Howard M. Lewis Ship이 이미 작성했으므로 자연스러운 통합과 관련해서 Howard에게 경의를 표해야 한다.)


18.6.1 스프링이 관리하는 빈 주입하기
다음의 간단한 스프링 컨테이너 정의를 가지고 있다고 가정해 보자. (XML 형식으로)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd">

<beans>
  <!-- 데이터소스 -->
  <jee:jndi-lookup id="dataSource" jndi-name="java:DefaultDS"/>

  <bean id="hibSessionFactory"
      class="org.springframework.orm.hibernate3.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>

  <!-- (트랜잭션이 적용된) 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>

  <!-- (트랜잭션이 적용된) 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>

테피스트리 어플리케이션에서 위의 정의가 스프링 컨테이너로 로드되어야 하고 관계된 테피스트리 페이지에 AuthenticationService와 UserService 인터페이스를 각각 구현한 authenticationService와 userService를 제공해야(주입해야) 한다.

이 관점에서 어플리케이션 컨텍스트는 스프링 정적 유틸리티 함수 WebApplicationContextUtils.getApplicationContext(servletContext)를 (여기서 servletContext는 Java EE 서블릿 스펙의 표준 ServletContext이다.) 호출해서 웹 어플리케이션에서 사용할 수 있다. 에를 들어 UserService의 인스턴스를 얻는 페이지의 간단한 메카니즘을 다음과 같이 작성할 것이다.

WebApplicationContext appContext = WebApplicationContextUtils.getApplicationContext(
    getRequestCycle().getRequestContext().getServlet().getServletContext());
UserService userService = (UserService) appContext.getBean("userService");
// ... UserService를 사용하는 코드

이 메카니즘은 동작한다. 페이지나 컨포넌트에 대한 기반 클래스의 메서드에서 기능의 대부분을 은닉화해서 훨씬 덜 장황하게 만들 수 있다. 하지만 어떤 면에서 이는 IoC 원리에 반한다. 이상적으로 페이지는 이름으로 특정 빈을 컨텍스트에 물어보지 않아야 하고 사실상 페이지는 이상적으로 컨텍스트를 전혀 몰라야 한다.

다행히 이를 가능하게 하는 메카니즘이 있다. 테피스트리가 선언적으로 페이지에 프로퍼티를 추가하는 메카니즘을 이미 가지고 있고 이 선언적인 방법으로 페이지의 모든 프로퍼티를 관리하는 것이 선호하는 접근방법이다. 그러므로 테피스트리는 페이지와 컴포넌트의 생명주기를 적절하게 관리할 수 있다.

Note
다음 섹션은 테피스트리 3.x에 적용된다. 테피스트리 4.x 버전을 사용한다면 Section 18.6.1.4, “테피스트리 페이지에 스프링 빈 주입하기 - 테피스트리 4.x 방식” 부분을 참고해라.



18.6.1.1 테피스트리 페이지에 스프링 빈 의존성 주입하기
우선 ServletContext갖지 않는 테피스트리 페이지나 컴포넌트에서 ApplicationContext를 사용할 수 있게 해야 한다. ApplicationContext에 접근해야할 때 페이지나 컴포넌트의 생명주기에서 ServletContext는 페이지에서 쉽게 사용할 수 없으므로 WebApplicationContextUtils.getApplicationContext(servletContext)를 직접 사용할 수 없다. 이를 노출해주는 테피스트리 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);

    // (없다면) ApplicationContext를 전역에 추가한다
    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);
    }
  }
}

이 엔진(engine) 클래스는 이 테피스트리 앱의 'Global' 객체에 "appContext"라는 속성으로 스프링 어플리케이션 컨텍스트를 둔다. 이 전문화된 IEngine 인스턴스가 테피스트리 어플리케이션 정의 파일의 엔트리로 테피스트리 어플리케이션에서 사용되도록 등록해야 한다. 예를 들면 다음과 같다.

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>


18.6.1.2 컴포넌트 정의 파일
이제 페이지나 컴포넌트 정의 파일(*.page나 *.jwc)에 ApplicationContext 밖에서 필요한 빈을 보관하기 위해서 프로퍼티-명세(property-specification) 요소를 추가하고 이에 대한 페이지나 컴포넌트 프로퍼티를 생성한다. 예를 들면 다음과 같다.

<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 표현식은 컨텍스트에서 가져온 빈으로 프로퍼티의 초기값을 지정한다. 전체 페이지 정의는 다음과 같을 것이다.

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


18.6.1.3 추상 접근자(accessor) 추가하기
이제 페이지나 컴포넌트에 대한 자바 클래스 정의에서 정의한 프로퍼티의 추상 getter 메서드를 추가하는 것(프로퍼티에 접근할 수 있게 하려고)이 해야하는 일의 전부이다.

// UserService 구현제. 이는 페이지 정의에서 가져올 것이다
public abstract UserService getUserService();
// AuthenticationService 구현체. 이는 페이지 정의에서 가져올 것이다
public abstract AuthenticationService getAuthenticationService();

이 예제의 로그인 페이지의 전체 자바 클래스는 다음과 같을 것이다.

package com.whatever.web.xportal.pages;

/**
 *  username과 password를 제공해서 사용자가 로그인할 수 있게 한다.
 *  성공적으로 로그인한 뒤 클라이언트 브라우저에 차후 로그인을 위해서 기본 username을
 *  제공하는 쿠키가 있다.(쿠키는 일주일간 유지된다)
 */
public abstract class Login extends BasePage implements ErrorProperty, PageRenderListener {

  /** 방문했을 때 인증된 사용자 객체의 키를 저장한다. */
  public static final String USER_KEY = "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));
  }

  /**
   *  로그인을 시도한다.
   * <p>
   *  사용자 이름을 모르거나 비밀번호가 틀렸으면 오류 메시지를 보여준다.
   **/
  public void attemptLogin(IRequestCycle cycle) {

    String password = getPassword();

    // 비밀번호를 제거하기 위해 약간의 작업을 한다.
    setPassword(null);
    IValidationDelegate delegate = getValidationDelegate();

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

    // 유효성검사 필드에서 오류가 이미 발생했을 것이다.
    if (delegate.getHasErrors()) {
      return;
    }

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

  /**
   *  {@link User}를 로그인된 유저로 설정하고
   *  username의 쿠키를 생성하고(차후 로그인을 위해서)
   *  적절한 페이지나 지정한 페이지로 리다이렉트한다.
   **/
  public void loginUser(User user, IRequestCycle cycle) {

    String username = user.getUsername();

    // 방문한 객체를 얻는다. 이는 방문한 객체(visit object)와
    // HttpSession의 생성을 강제할 것이다.
    Map visit = (Map) getVisit();
    visit.put(USER_KEY, user);

    // 로그인한 뒤 별도로 지정한 것이 없다면 MyLibrary 페이지로 이동한다
    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);

    // 사용자의 username을 쿠키에 기록한다
    cycle.getRequestContext().addCookie(cookie);
    engine.forgetPage(getPageName());
  }

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


18.6.1.4 테피스트리 페이지에 스프링 빈 주입하기 - 테피스트리 4.x 방식
테피스트리 4.x 버전에서 테피스트리 페이지에 스프링이 관리하는 빈을 의존성 주입하는 것은 훨씬 더 간단하다. 단일 애드온 라이브러리와 약간의 설정이(반드시 필요한 보일러플레이트)이 필요한 전부이다. 그냥 이 라이브러리를 웹 어플리케이션이 필요로 하는 다른 라이브러리(보통 WEB-INF/lib에)와 함께 패키징해서 배포하면 된다.

앞에서 설명한 메서드를 사용해서 스프링 컨테이너를 생성하고 노출해야 한다. 그 다음에는 스프링이 관리하는 빈을 아주 쉽게 테피스트리에 주입할 수 있다. Java 5를 사용한다면 앞의 Login 페이지를 고려해 봐라. 스프링이 관리하는 userService와 authenticationService 객체(글래스 정의의 대부분은 명확함을 위해서 생략했다.)를 의존성 주입하기 위해서 적절한 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();
}

거의 다 됐다. ServletContext에 HiveMind 서비스로 저장된 스프링 컨테이너를 노출하는 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>

  <!-- 스프링 설정을 전체 어플리케이션 초기화과정으로 후킹한다 -->
  <contribution
    configuration-id="tapestry.init.ApplicationInitializers">
    <command id="spring-context"
      object="service:SpringApplicationInitializer" />
  </contribution>
</module>

Java 5를 사용한다면(그러므로 어노테이션에 접근할 수 있다) 이것이 전부이다.

Java 5를 사용하지 않는다면 테피스트리 페이지 클래스에 어노테이션을 붙히지 않는다. 대신 의존성 주입을 선언하는 좋지만 구식인 XML을 사용해라. 예를 들어 Login 페이지(또는 컴포넌트)의 .page나 .jwc 파일 내부에 선언한다.

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

이 예제에서 스프링 컨테이너에 정의된 서비스 빈을 선언적인 방법으로 테피스트리 페이지에 제공할 수 있도록 관리한다. 페이지 클래스는 서비스 구현체가 어디서 오는지 알지 못하고 사실상 다른 구현체로 쉽게 넘어갈 수 있다.(예를 들면 테스트 도중에) 이 제어의 역전이 스프링 프레임워크의 주요 목적과 이득이다. 테피스트리 어플리케이션의 스택 전체에서 이 확장을 관리한다.


18.7 추가 자료
이번 장에서 설명한 여러 웹 프레임워크에 대한 추가 자료를 아래 링크에서 찾을 수 있다.


2013/04/02 01:07 2013/04/02 01:07