SWF 12장 JSF 통합
12.1. 도입
스프링 Faces는 스프링의 JSF 통합 모듈로 스프링에서 JSF 사용을 간편하게 해준다. JSF UI 컴포넌트 모델을 스프링 MVC와 스프링 웹 플로우 컨틀로러와 함께 사용할 수 있게 해준다.
스프링 Faces는 또한 Ajax와 클리이언트쪽 검증 기능을 제공하는 자그마한 Facelets도 제공한다. 이 컴포넌트 라이브러리는 스프링 자바스크립트를 기반으로 만들었다. 스프링 자바스크립트는 Dojo를 기반 UI 툴킷으로 통합한 자바스크립트 추상화 프레임워크드다.
12.2. 스프링-중심 통합 방법
스프링 Faces는 JSF의 UI 컴포넌트 모델 장정을 스프링의 컨트롤러와 설정 모델 장점과 결합해준다. 아무런 약점없이 JSF의 모든 장점을 활용할 수 있다.
스프링 Faces는 표준 JSF 기능에 다음과 같은 강력한 보완재를 제공한다.
1. managed bean facility
2. scope management
3. event handling
4. navigation rules
5. easy modularization and packaging of views
6. cleaner URLs
7. model-level validation
8. client-side validation and UI enhancement
9. Ajax partial page updates and full navigation
10. progressive enhancement and graceful degradation
이 기능을 사용하면 faces-config.xml에 필요한 설정 분량을 현격하게 줄여줄 것이며 뷰와 컨트롤러 계층을 보다 깔끔하게 분리해주며 애플리케이션의 기능 책임 모듈화를 보다 잘 지원한다. 이 기능들의 사용법은 다음 절에서 살펴보겠다. 이 기능들 대부분은 스프링 웹 플로우의 플로우 정의 언어를 기반으로 한다. 여러분이 플로우 정의하기에 나와있는 기본을 이해하고 있다고 가정한다.
12.3. web.xml 설정하기
스프링 Faces를 사용하는 첫 번째 단계는 요청을 web.xml 파일에 있는 DispatcherServlet으로 라우팅하는 것이다. 이번 예제에서, 우리는 /spring/으로 시작하는 모든 URL을 서블릿으로 맵핑한다. 서블릿을 설정해야 한다. init-param을 사용하여 서블릿에 contextConfigLocation을 넘겨준다. 이 것은 애플리케이션의 스프링 설정 파일 위치다.
<servlet>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/web-application-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<url-pattern>/spring/*</url-pattern>
</servlet-mapping>
JSF를 적절하게 동작시키려면, FacesServlet을 web.xml에 설정해야 한다. 스프링 Faces를 사용할 때는 그것을 사용하여 요청을 라우팅할 필요가 없다.
<!-- Just here so the JSF implementation can initialize, *not* used at runtime -->
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- Just here so the JSF implementation can initialize -->
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.faces</url-pattern>
</servlet-mapping>
스프링 Faces 컴포넌트를 사용할 때에도 스프링 자바스크립트 ResourceServlet을 설정하여 컴포넌트에서 CSS와 자바스크립트 리소스를 제대로 출력할 수 있게 할 필요가 있다. 이 서블릿은 반드시 /resources/*로 맵핑해야 컴포넌트에서 랜더링한 URL에 대해 제대로 동작한다.
<!-- Serves static resource content from .jar files such as spring-faces.jar -->
<servlet>
<servlet-name>Resource Servlet</servlet-name>
<servlet-class>org.springframework.js.resource.ResourceServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<!-- Map all /resources requests to the Resource Servlet for handling -->
<servlet-mapping>
<servlet-name>Resource Servlet</servlet-name>
<url-pattern>/resources/*</url-pattern>
</servlet-mapping>
스프링 Faces 컴포넌트는 JSP 대신에 Facelet을 사용해야 한다. 따라서 이들 컴포넌트를 사용하려면 일반적인 Facelet 설정을 반드시 추가해야 한다.
!-- Use JSF view templates saved as *.xhtml, for use with Facelets -->
<context-param>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.xhtml</param-value>
</context-param>
12.4. JSF 뷰 랜더링 하도록 웹 플로우 설정하기
The next step is to configure Web Flow to render JSF views. To do this, in your Spring Web Flow configuration include the faces namespace and link in the faces flow-builder-services :
다음 단계는 JSF 뷰를 랜더링 하도록 웹 플로우를 설정하는 것이다. 그렇게 하려면 스프링 웹 플로우 설정에 faces 네임스페이스를 추가하고
<?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:webflow="http://www.springframework.org/schema/webflow-config"
xmlns:faces="http://www.springframework.org/schema/faces"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/webflow-config
http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd
http://www.springframework.org/schema/faces
http://www.springframework.org/schema/faces/spring-faces-2.0.xsd">
<!-- Executes flows: the central entry point into the Spring Web Flow system -->
<webflow:flow-executor id="flowExecutor" />
<!-- The registry of executable flow definitions -->
<webflow:flow-registry id="flowRegistry" flow-builder-services="facesFlowBuilderServices" base-path="/WEB-INF">
<webflow:flow-location-pattern value="**/*-flow.xml" />
</webflow:flow-registry>
<!-- Configures the Spring Web Flow JSF integration -->
<faces:flow-builder-services id="facesFlowBuilderServices" />
</beans>
faces:flow-builder-services 태그는 JSF 환경에 적절한 다른 기본 값들도 설정한다. 특히 Unified EL을 기본 EL로 설정한다.
완전히 동작하는 예제는 배포판에서 swf-booking-faces 레퍼런스 애플리케이션을 참고하라.
12.5. faces-config.xml 설정하기
faces-config.xml에 유일하게 설정할 필요가 있는 것은 Facelet 사용에 대한 것이다. 만약 스프링 Faces 컴포넌트를 사용하지 않고 JSP를 사용하고 있다면, 어떠한 스프링 Faces 관련 설정도 faces-config.xml에 추가하지 않아도 된다.
<faces-config>
<application>
<!-- Enables Facelets -->
<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
</application>
</faces-config>
12.6. JSF가 관리하는 빈 기능 교체하기
스프링 Faces는 JSF가 관리하는 빈 기능을 플로우가 관리하는 변수와 스프링이 관리하는 빈으로 완전히 교체할 수 있게 해준다. 그렇게 하여 여러분이 관리하는 객체를 잘 정의되어 있는 초기화 후크와 도메인 객체 실행 후크로 생명 주기를 보다 잘 관리할 수 있다. 게다가, 여러분이 이미 비즈니스 계층에서 스프링을 사용해봤다는 가정한다면, 두 개의 다른 빈 관리 모델을 유지하는 것에 대한 개념적인 오버헤드를 줄일 수 있다.
순수 JSF 개발을 할 떄는 request 스코프로는 복잡한 이벤트-기반 뷰를 다루는 대화형 모델 객체를 저장하기에는 적당하지 않다는 것을 금방 알 수 있을 것이다. 오직 사용할 수 있는 옵션은 session 스코프에 모든 것들을 넣는 것이다. 애플리케이션의 다른 뷰 또는 기능적인 부분을 처리할 때 객체들을 청소해줘야 한다는 추가 작업이 생긴다. 정말로 필요한 것은 request와 session 스코프 중간 쯤 되는 어떤 스코프가 필요하다. 우누 좋게도 웹 플로우는 그러한 확장 기능을 제공한다.
12.6.1. 플로우 변수 사용하기
가장 간단하고 자연스럽게 모델을 선언하고 관리하는 것은 플로우 변수를 사용하는 것이다. 이 변수들을 플로우 시작시에 선언할 수 있다.
<var name="searchCriteria" class="com.mycompany.myapp.hotels.search.SearchCriteria"/>
그런 다음 이 변수를 플로우 중 어떤 JSF 뷰 템플릿에 EL을 통해서 참조한다.
<h:inputText id="searchString" value="#{searchCriteria.searchString}"/>
(좀 더 구체적일 필요가 있다면 그렇게 할 수도 있지만)템플릿에서 변수를 참조할 때 그 스코프로 접두어를 붙이지 않아도 된다는 것에 주목하라. 표준 JSF 빈으로 대응하는 변수에 대해 모든 사용 가능한 스코프에서 찾아볼 것이다. 따라서 EL 표현식을 수정하지 않고도 플로우 정의에서 그것을 참조하는 변수의 스코프를 변경할 수 있다.
또한 뷰 인스턴스 변수를 현재 뷰로 범위를 제한하고 다른 뷰로 이동하면 자동으로 비워버리게 정의할 수 있다. 이렇게 하는 것은 보통 페이지 내에서 다른 뷰로 전이하기 전에 여러 여러 요청에 걸쳐 이벤트를 처리하는 JSF 뷰에서 매우 유용하다.
뷰 인스턴스 변수를 정의할때 var 엘리먼트를 view-state 정의 내부에서 사용할 수 있다.
<view-state id="enterSearchCriteria">
<var name="searchCriteria" class="com.mycompany.myapp.hotels.search.SearchCriteria"/>
</view-state>
12.6.2. 스코프를 가진 스프링 빈 사용하기
비록 자동 연결 플로우 인스터스 변수가 괜찮은 모듈화와 가독성을 제공하지만 가끔 여러분은 AOP 같은 스프링 컨테이너의 기능을 활용하고 싶을 수 있다. 그런 경우 여러분은 스프링 애플리케이션컨텍스트 내부에 있는 빈에 특정 웹 플로우 스코프를 줄 수 있다.
<bean id="searchCriteria" class="com.mycompany.myapp.hotels.search.SearchCriteria" scope="flow"/>
이런 접근 방법의 가장 큰 차이점은 EL 표현식에 의해 접근될 때 비을 초기화 한다는 것이다. EL을 통한 이런 종류의 생성 지연은 JSF가 관리하는 빈이 할당되는 것과 매우 비슷하다.
12.6.3. 모델 조작하기
뷰 랜터링 하기 전에 (데이터베이스에서 영속 엔티티를 로딩하는 것 같은)모델 초기화가 필요한 것은 매우 흔한 일이지만 JSF 자체로는 이러한 초기화에 사용할 편의성 후크(hook)를 제공하지 않는다. 플로우 정의 언어는 액션(Action)을 통해서 이와 관련된 자연스러운 기능을 제공한다. 스프링 Faces는 액션의 결과를 JSF-관련 데이터 구조로 변경해주는 추가적인 편의성을 제공한다. 예제를 보자.
<on-render>
<evaluate expression="bookingService.findBookings(currentUser.name)"
result="viewScope.bookings" result-type="dataModel" />
</on-render>
이렇게 하면 bookingService.findBookings 매서드 결과를 취하고 그것을 표준 JSF DataTable 컴포넌트에서 해당 목록을 사용할 수 있도록 커스텀 JSF DataModel로 감싼다.
<h:dataTable id="bookings" styleClass="summary" value="#{bookings}" var="booking"
rendered="#{bookings.rowCount > 0}">
<h:column>
<f:facet name="header">Name</f:facet>
#{booking.hotel.name}
</h:column>
<h:column>
<f:facet name="header">Confirmation number</f:facet>
#{booking.id}
</h:column>
<h:column>
<f:facet name="header">Action</f:facet>
<h:commandLink id="cancel" value="Cancel" action="cancelBooking" />
</h:column>
</h:dataTable>
커스텀 DataModel은 request 스코프 외의 저장을 위한 직렬화와 EL에서 현재 선택한 줄에 대한 접근 등 몇몇 추가적인 편의성을 제공한다. 예를 들어, DataTable 내의 컴포넌트에 의해 액션 이벤트가 발생한 뷰에서 포스트백 할 때, 여러분은 선택한 줄의 모델 인스턴스를 가질 수 있다.
<transition on="cancelBooking">
<evaluate expression="bookingService.cancelBooking(bookings.selectedRow)" />
</transition>
12.7. 스프링 웹 플로우로 JSF 이벤트 처리하기
스프링 웹 플로우는 낮은 결합도를 유지하면서 JSF 액션 이벤트를 처리할 수 있게 해준다. 자바 코드에서 JSF API에 의존하지 않아도 된다. 이벤트를 커스텀 자바 액션 코드를 전혀 사용하지 않고 플로우 정의 언어를 사용하여 완전하게 처리할 수 있다. 이렇게 하면 (JSF 뷰 템플릿과 SWF 플로우 정의) 이벤트를 연결할 때 만들어지는 구성물들을 전체 애플리케이션을 빌드하고 다시 배포할 필요 없이 즉시 리프래시 되기 때문에 보다 기민한 개발 프로세스가 가능해진다.
12.7.1. JSF In-page 액션 이벤트 처리하기
간단하지만 JSF에서 가장 흔한 경우가 모델을 조작하는 이벤트를 발생시키고 동일한 뷰로 모델의 변경된 상태를 보여주는 것이다. 플로우 정의 언어는 transition 엘리먼트에서 이것을 지원한다.
이 것에 대한 좋은 예제는 페이징 처리를 하는 목록 표다. 거대한 결과 목록 중의 일부만 읽어오고 보여주고 싶다고 가정해보자. 그리고 사용자는 그 결과를 페이징할 수 있다. 목록을 읽어오고 보여주는 초기 view-state 정의는 다음과 같다.
<view-state id="reviewHotels">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)"
result="viewScope.hotels" result-type="dataModel" />
</on-render>
</view-state>
JSF DataTable로 현재 호텔 목록을 보여주도록 할 수 있다. 그런 다음 표 하단에 "More Results" 링크를 제공한다.
<h:commandLink id="nextPageLink" value="More Results" action="next"/>
이 커맨드링크는 action 속성에서 "next" 이벤트를 보낸다. 그럼 여러분은 이벤트를 view-state 정의에 추가하려 처리할 수 있다.
<view-state id="reviewHotels">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)"
result="viewScope.hotels" result-type="dataModel" />
</on-render>
<transition on="next">
<evaluate expression="searchCriteria.nextPage()" />
</transition>
</view-state>
여기서 searchCriteria 인스턴스에서 page 카운트를 증가하여 "next" 이벤트를 처리한다. 그럼 다음 on-render 액션을 변경된 criteria로 호출한다. 그러면 다음 페이지 결과를 DataModel로 로딩해준다. transition 엘리먼트에 to 속성이 없기 때문에 동일한 뷰를 다시 보여준다. 그리고 모델에 변경된 사항을 뷰에 반영해준다.
12.7.2. JSF 액션 이벤트 처리하기
in-page 이벤트 다음 단계는 조작한 모델을 가지고 다른 뷰로 이동하는 이벤트다. 순수 JSF로 이것을 하려면 faces-config.xml에 네비게이션 로직을 추가하고 자바 코드를 JSF가 관리하는 빈에 추가해야 한다.(두 작업 모두 다시 배포해야 한다.) 플로우 정의 언어를 사용하면, in-page 이벤트를 다루던 방식과 매우 비슷하게 한 곳에서 그런 것을 다룰 수 있다.
계속해서 페이징 처리하는 목록을 살펴보자. 보여지는 DataTable의 각각의 row에 row 인스턴스에 대한 자세한 내용 페이지 링크를 가지고 있도록 하고자 한다. 여러분은 테이블에 다음의 commandLink 컴포넌트를 가지고 있는 컬럼을 추가할 수 있다.
<h:commandLink id="viewHotelLink" value="View Hotel" action="select"/>
이것은 "select" 이벤트를 발생시킨다. 그다음 기존의 view-state에 또 다른 transition 엘리먼트를 추가하여 이것을 처리할 수 있다.
<view-state id="reviewHotels">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)"
result="viewScope.hotels" result-type="dataModel" />
</on-render>
<transition on="next">
<evaluate expression="searchCriteria.nextPage()" />
</transition>
<transition on="select" to="reviewHotel">
<set name="flowScope.hotel" value="hotels.selectedRow" />
</transition>
</view-state>
여기서 "select" 이벤트는 DataTable에서 현재 선택한 hotel 인스턴스를 플로우 스코프에 넣어서 처리하고 있다. 그렇게 하면 "reviewHotel" view-state에서 참조할 것이다.
12.7.3. 모델 검증 수행하기
JSF는 변경 사항을 모델에 반영하기 전에 필드-수준 입력 검증 관련 유용한 기능을 제공한다. 하지만 변경 사항을 적용한 뒤에 모델-수준의 좀 더 복잡한 검증을 수행할 필요가 있다면 여러분은 관리하는 빈의 JSF 액션 매서드에 커스텀 코드를 추가해야 한다. 이런 종류의 검증은 모메인 모델 자체 책임이지만 도메인 모델 계층에 원하지 않던 JSF API 의존성을 추사하지 않고서는 에러 메시지를 뷰에 전달하기가 어렵다.
스프링 Faces를 사용하면 일반적이고 낮은-수준의 MessageContext를 여러분의 비즈니스 코드에서 유용하게 사용할 수 있고 그곳에 추가한 모든 메시지는 랜더링 시에 FacesContext에서 사용할 수 있다.
예를 들어, 사용자가 호텔 예약을 완료하기 위해 필요한 상세 정보를 입력하는 뷰가 있다고 가정하자. 여러분은 거기서 입력받은 체크인 체크아웃 날짜가 비즈니스 규칙에 맞는지 확인해야 한다. transition 엘리먼트에서 그러한 모델-수준 검증을 호출할 수 있다.
<view-state id="enterBookingDetails">
<transition on="proceed" to="reviewBooking">
<evaluate expression="booking.validateEnterBookingDetails(messageContext)" />
</transition>
</view-state>
여기서 "proceed" 이벤트는 booking 인스턴스의 모델-수준 검증 매서드를 호출하여 처리한다. 매서드를 호출할 때 MessageContext 인스턴스를 넘겨줘서 메시지를 기록할 수 있게 한다. 그런다음 h:messages 컴포넌트를 사용하여 JSF 메시지를 보여줄 수 있다.
12.7.4. Ajax 이벤트 처리하기
스프링 Faces는 표준 JSF 컴포넌트에 Ajax-기반 일부 뷰 수정 기능을 추가한 몇몇 특별한 UICommand 컴포넌트를 제공한다. 이들 컴포넌트는 사용자가 사용하는 브라우져의 기능이 떨어진다면 전체 페이지를 리프래시 하여 모두 잘 동작할 것이다.
[노트] 노드
스프링 Faces의 코어 JSF 지원이 JSF 1.1 호환가능 하지만, 스프링 Faces Ajax 컴포넌트는 JSF 1.2를 필요로 한다.
Revisiting the earlier example with the paged table, you can change the "More Results" link to use an Ajax request by replacing the standard commandButton with the Spring Faces version (note that the Spring Faces command components use Ajax by default, but they can alternately be forced to use a normal form submit by setting ajaxEnabled="false" on the component):
<sf:commandLink id="nextPageLink" value="More Results" action="next" />
This event is handled just as in the non-Ajax case with the transition element, but now you will add a special render action that specifies which portions of the component tree need to be re-rendered:
<view-state id="reviewHotels">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)"
result="viewScope.hotels" result-type="dataModel" />
</on-render>
<transition on="next">
<evaluate expression="searchCriteria.nextPage()" />
<render fragments="hotels:searchResultsFragment" />
</transition>
</view-state>
The fragments="hotels:searchResultsFragment" is an instruction that will be interpreted at render time, such that only the component with the JSF clientId "hotels:searchResultsFragment" will be rendered and returned to the client. This fragment will then be automatically replaced in the page. The fragments attribute can be a comma-delimited list of ids, with each id representing the root node of a subtree (meaning the root node and all of its children) to be rendered. If the "next" event is fired in a non-Ajax request (i.e., if JavaScript is disabled on the client), the render action will be ignored and the full page will be rendered as normal.
In addition to the Spring Faces commandLink component, there is a corresponding commandButton component with the same functionality. There is also a special ajaxEvent component that will raise a JSF action even in response to any client-side DOM event. See the Spring Faces tag library docs for full details.
An additional built-in feature when using the Spring Faces Ajax components is the ability to have the response rendered inside a rich modal popup widget by setting popup="true" on a view-state .
<view-state id="changeSearchCriteria" view="enterSearchCriteria.xhtml" popup="true">
<on-entry>
<render fragments="hotelSearchFragment" />
</on-entry>
<transition on="search" to="reviewHotels">
<evaluate expression="searchCriteria.resetPage()"/>
</transition>
</view-state>
If the "changeSearchCriteria" view-state is reached as the result of an Ajax-request, the result will be rendered into a rich popup. If JavaScript is unavailable, the request will be processed with a full browser refresh, and the "changeSearchCriteria" view will be rendered as normal.