[Spring WebFlow 가이드] 4. Expression Language(EL)
4.1. Introduction
웹 플로우는 EL을 데이터 모델과 액션을 일으키는데 사용합니다. 이번 챕터에서는 flow definition에서 사용하는 EL 문법과 설정법, 특정 EL 변수들과 친숙해져도록 하겠습니다.
EL은 플로우에서 보통 다음과 같을 때 많이 쓰입니다.
- 클라이언트 데이터에 접근 할 때. request 파라미터를 참조하거나 플로우 인풋을 선언하는 경우 등이 해당.
- 웹플로우의 RequestContext에 접근 할 때. flowScope나 currentEvent 등이 RequestContext에 해당.
- 액션을 통해 스프링 관리 객체의 메소드를 호출 할 때.
- expression을 해석할 때. 상태 변환 조건(state transition criteria), 서브플로우 id, 뷰 name 등의 expression이 해당.
EL은 또한 폼 파라미터를 모델 오브젝트로 묶을 때나, 반대로 포맷 폼 필드를 모델 오브젝트의 프로퍼티로부터 보여줄 때 사용되곤 합니다. (즉, 폼 값을 모델 오브젝트에 넣을 때나, 모델 오브젝트의 값으로 폼을 그릴 때 사용) 그러나 웹 플로우가 표준 JSF lifecycle 사용할 때는 적용되지 않습니다.
4.1.1. Expression types
알고 있어야 하는 중요한 개념은 웹 플로우 상에 크게 2가지 타입의 expression이 존재 한다는 것입니다.
( standard expression과 template expression )
- Standard Expressions
첫번째 expression 타입은 standard expression입니다. (가장 많이 쓰이는 expression이기도 하다.)
이 expression은 EL을 통해서 직접 evaluate됩니다. 즉, #{}과 같은 delimiter로 감싸줄 필요가 없습니다. 다음은 그 예시입니다.
<evaluate expression="searchCriteria.nextPage()" />
위의 expression은 식이 evaluate될 때 searchCriteria의 nextPage 메소드가 실행되는 것을 나타냅니다. 만약 이걸 #{}같은 delimiter로 감싸버린다면 IllegalArgumentException 같은 에러를 얻을 것입니다.
- Template expressions
두번째 expression 타입은 template expression입니다.
template expression은 문장과 하나 이상의 standard expression과의 혼용을 허용합니다. 각각의 standard expression은 #{}과 같은 delemiter로 감싸져 사용됩니다. 다음이 그 예문입니다.
<view-state id="error" view="error-#{externalContext.locale}.xhtml" />
위의 expression이 template expression입니다. 위와 같이 표현된 문장은 error-와 .xhtml이 양쪽에 위치하게 되고 중간에 externalContext.locale의 값이 계산되서 넣어집니다.
4.2. EL Implementations
4.2.1. Spring EL
웹 플로우 2.1버전부터는 Spring Expression Language(Spring EL)을 사용합니다. Spring EL은 스프링 제품 전체를 아우를 수 있도록 제공된 expression language입니다. Spring EL은 org.springframework.expression이라는 jar 파일로 분리되서 배포됩니다. 어플리케이션에서 이를 사용하려면 org.jboss.el이나 org.ognl과 같은 의존성은 지우고, 대신 org.springframework.expression을 첨부해야 합니다.
4.2.2 Unified EL
웹 플로우 2.0에서 Unified EL은 jboss-el로 구현된 기본 expression language 였습니다. Unifed EL의 사용은 비록 웹 컨테이너가 제공한다고 하더라도, el-api의 의존이 필요했습니다. Spring EL이 기본적이고 추천되는 expression language입니다만 당신이 원한다면 Unifeid EL로도 변환해서 사용 할 수 있습니다. 그러기 위해서는 WebFlowELExpressionParser를 flow-builder-services에 플러그인 해야하는데, 다음과 같은 설정이 필요합니다.
<webflow:flow-builder-services expression-parser="expressionParser"/>
<bean id="expressionParser" class="org.springframework.webflow.expression.el.WebFlowELExpressionParser">
<constructor-arg>
<bean class="org.jboss.el.ExpressionFactoryImpl" />
</constructor-arg>
</bean>
만약 커스텀 컨버터를 등록하고 싶다면 다음과 같이 conversion service를 사용해야합니다.
<webflow:flow-builder-services expression-parser="expressionParser" conversion-service="conversionService"/>
<bean id="expressionParser" class="org.springframework.webflow.expression.el.WebFlowELExpressionParser">
<constructor-arg>
<bean class="org.jboss.el.ExpressionFactoryImpl" />
</constructor-arg>
<property name="conversionService" ref="conversionService"/>
</bean>
<bean id="conversionService" class="somepackage.ApplicationConversionService"/>
4.2.3. OGNL
OGNL은 웹 플로우 2.4버전에서 deprecated되었다고 합니다. 생략하겠습니다.
4.3. EL portablility
보통 당신은 Spring EL, Unifed EL, OGNL과 같은 문법을 사용할 것입니다.
그러나 그 중 Spring EL은 특별한 장점이 있습니다. 예를 들어 Spring EL은 스프링 3의 타입 컨버전과 연동이 쉽고, 이로서 당신은 스프링을 더 제대로 활용 할 수 있습니다. 특히 제네릭 타입의 자동 탐색과 어노테이션 포맷의 사용은 현재 Spring EL을 통해서만 지원됩니다.
만약 Unifed EL 혹은 OGNL에서 Spring EL로 전환하려 한다면, 다음과 같은 몇가지 변화를 알아야 합니다.
- Expression의 delimeter가 ${}에서 #{}로 바뀌었다.
- #{currentEvent =='submit'}과 같은 expression은 #{currentEvent.id == 'submit'}으로 바뀌어야 한다.
- #{currentUser. name}과 같은 프로퍼티를 해석하면 NullPointerException이 나타날 수 있다. 때문에 #{currentUser != null ? currentUser.name : null}과 같이 표현해 줘야 한다. 더 나은 대안은 #{currentUser?.name}과 같이 safe navigation operator를 사용하는 것이다.
4.4. Special EL variables
플로우 상에서 참조하게 될 몇가지 내포된 변수가 존재합니다. 이 변수들을 이번 섹션에서 다룰 것입니다.
다음과 같은 룰을 명심하세요. 데이터 스코프(FlowScope, viewScope, requestScope와 같은) 변수들은 어느 한 스코프에 대해서 새로 변수를 할당 할 때에만 쓰입니다.
예를 들어 bookingService.findHotels(searchCriteria)를 콜해서 그 결과를 hotels라는 변수에 할당 할 때, 당신은 prefix로 그 변수의 스코프를 지정해주어야 합니다. 그래야만 웹 플로우가 변수를 어느 스코프에 저장할지 알 수 있습니다.
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow" ... >
<var name="searchCriteria" class="org.springframework.webflow.samples.booking.SearchCriteria" />
<view-state id="reviewHotels">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" />
</on-render>
</view-state>
</flow>
그러나 이미 있는 변수라면 아래처럼 스코프를 붙이지 않아도 참조할 수 있습니다.
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow" ... >
<var name="searchCriteria" class="org.springframework.webflow.samples.booking.SearchCriteria" />
<view-state id="reviewHotels">
<transition on="sort">
<set name="searchCriteria.sortBy" value="requestParameters.sortBy" />
</transition>
</view-state>
</flow>
다음 내용들은 당신이 참조 할 수 있는 스코프 변수들입니다.
4.4.1. flowScope
플로우가 시작될 때부터 끝날 때까지 함께하는 스코프입니다. 기본적으로 모든 오브젝트는 저장될 때 직렬화가 필요합니다.
<evaluate expression="searchService.findHotel(hotelId)" result="flowScope.hotel" />
4.4.2. viewScope
<view-state>와 함께하는 스코프입니다. 진입할 때 생성되고, 빠져나올 때 해지됩니다. 이 스코프는 오직 <view-state> 사이에서만 참조가 가능하며, 기본적으로 모든 오브젝트는 저장될 때 직렬화가 필요합니다.
<on-render>
<evaluate expression="searchService.findHotels(searchCriteria)" result="viewScope.hotels"
result-type="dataModel" />
</on-render>
4.4.3. requestScope
플로우가 요청 될 때 생성되고 플로우가 값을 리턴할 때 해지되는 스코프입니다.
<set name="requestScope.hotelId" value="requestParameters.id" type="long" />
4.4.4. flashScope
flowScope처럼 플로우가 시작 될 때 생성되고, 플로우가 끝날 때 해지됩니다. 다만 다른 점은 모든 뷰가 렌더링 된다면 이 스코프는 clear 됩니다. 기본적으로 모든 오브젝트는 저장될 때 직렬화가 필요합니다.
<set name="flashScope.statusMessage" value="'Booking confirmed'" />
4.4.5. conversationScope
최상위 플로우가 시작 될 때 생성되며, 마찬가지로 최상위 플로우가 끝날 때 해지됩니다. conversationScope는 최상위 플로우가 실행되는 동안 모든 하위 플로우에서 공유됩니다. 기본적으로 이 스코프의 오브젝트는 HTTP session으로 저장되며, 직렬화가 필요합니다.
4.4.6. requestParameters
클라이언트의 request parameter에 접근할 때 쓰입니다.
<set name="requestScope.hotelId" value="requestParameters.id" type="long" />
4.4.7. currentEvent
현재 Event 속성에 접근 할 때 사용합니다.
<evaluate expression="booking.guests.add(currentEvent.attributes.guest)" />
4.4.8. currentUser
authenticated Principal 에 접근 할 때 사용합니다.
<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)" result="flowScope.booking" />
4.4.9. messageContext
플로우를 검색하거나 생성할 때 메시지를 관리하는 context에 접근 할 때 사용합니다.
<evaluate expression="bookingValidator.validate(booking, messageContext)" />
4.4.10. resourceBundle
메시지 리소스에 접근 할 때 사용합니다.
<set name="flashScope.successMessage" value="resourceBundle.successMessage" />
4.4.11. flowRequestContext
RequestContext API에 접근 할 때 사용합니다. RequestContext API는 현재 플로우 리퀘스트를 나타내는데 자세한 내용은 API Javadocs를 살펴보세요.
4.4.12. flowExecutionContext
FlowExecutionContext API에 접근 할 때 사용합니다. FlowExecutionContext API는 현재 플로우의 스테이트를 나타내는데 자세한 내용은 API Javadocs를 살펴보세요.
4.4.13. flowExecutionUrl
context-relative URI에 접근 할 때 사용합니다. 이는 현재 플로우의 뷰 스테이트의 실행을 위해 사용됩니다.
4.4.14. externalContext
user session 속성을 갖고 있는 클라이언트 환경에 접근 할 때 사용합니다. ExternalContext API에 대해 알고 싶으면 Javadocs를 살펴보세요.
<evaluate expression="searchService.suggestHotels(externalContext.sessionMap.userProfile)" result="viewScope.hotels" />
4.5. Scope searching algorithm
위의 내용에서 우리는 변수를 할당 할 때 다음과 같이 각 스코프를 참조해주어야만 합니다.
<set name="requestScope.hotelId" value="requestParameters.id" type="long" />
하지만 변수를 참조 할 때는 다음과 같이 스코프를 굳이 참조 안해주어도 됩니다.
<evaluate expression="entityManager.persist(booking)" />
위의 경우처럼 만약 스코프가 참조되지 않았다면, 스코프 서칭 알고리즘이 사용됩니다. 이 알고리즘은 request, flash, view, flow, conversation 스코프에서 변수 값을 찾아냅니다. 만약 어디에도 값이 존재하지 않는다면 EvaluationException을 던집니다.