SWF 5장 액션 실행하기

5.1. 소개

이번 장에서는 action-state 엘리먼트를 사용하여 플로우 내에서 특정 시저에 액션 실행을 제어하는 방법을 살펴본다. 또한 decision-state 엘리먼트를 사용하여 플로우 방향을 결정하는 방법도 살펴본다. 마지막으로 플로우 내에서 가능한 다양한 지점에서 액션을 호출하는 예제를 다룰 것이다.

5.2. 액션 스테이트 정의하기

액션을 호출하고 싶을 때 action-state 엘리먼트를 사용하면 액션의 결과에 기반하여 다른 상태로 전이한다.

<action-state id=”moreAnswersNeeded”>
    <evaluate expression=”interview.moreAnswersNeeded()” />
    <transition on=”yes” to=”answerQuestions” />
    <transition on=”no” to=”finish” />
</action-state>

위의 action-state를 사용하여 인터뷰를 완성하는데 더 많은 질문이 필요한지 확인하는 인터뷰 플로우는 다음과 같다.

<flow xmlns=”http://www.springframework.org/schema/webflow”
      xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
      xsi:schemaLocation=”http://www.springframework.org/schema/webflow
                          http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd”>

    <on-start>
        <evaluate expression=”interviewFactory.createInterview()” result=”flowScope.interview” />
    </on-start>

    <view-state id=”answerQuestions” model=”questionSet”>
        <on-entry>
            <evaluate expression=”interview.getNextQuestionSet()” result=”viewScope.questionSet” />
        </on-entry>
        <transition on=”submitAnswers” to=”moreAnswersNeeded”>
            <evaluate expression=”interview.recordAnswers(questionSet)” />
        </transition>
    </view-state>
   
    <action-state id=”moreAnswersNeeded”>
        <evaluate expression=”interview.moreAnswersNeeded()” />
        <transition on=”yes” to=”answerQuestions” />
        <transition on=”no” to=”finish” />
    </action-state>

    <end-state id=”finish” />
   
</flow>

5.3. 의사결정 상태 정의하기

decision-state 엘리먼트를 action-state 대신 사용하여 편리한 if/else 문법을 사용하여 경로 결정을 할 수 있다. 아래 예제는 위에서 봤던 moreAnswersNeeded 상태를 action-state 대신 의사결정 상태로 구현한 것이다.

<decision-state id=”moreAnswersNeeded”>
    <if test=”interview.moreAnswersNeeded()” then=”answerQuestions” else=”finish” />
</decision-state>
           
5.4. 액션 결과 이벤트 맵핑

액션은 보통 일반 자바 객체의 매서드를 호출한다. action-state와 decision-state에서 호출한 매서드는 뷰 상태를 전이할 때 사용할 값을 반환한다. 전이는 이벤트에 의해 발생하기 때문에 매서드의 반환값은 반드시 먼저 Event 객체로 맵핑되어야 한다. 다음 표는 흔한 반환값을 어떻게 Event 객체로 맵핑하는지 보여준다.

표 5.1. 액션 매서드 반환 값을 이벤트 id로 맵핑하기

|| 매서드 반환 값 || 맵핑한 이벤트 식별자 표현식 ||
| java.lang.String | 문자열 값 |
| java.lang.Boolean | (true 면) yes, (false 면) no |
| java.lang.Enum | Enum 이름 |
| 그밖에 다른 타입 | success |

아래 예제의 액션 스테이트에서 매서드가 boolean 값을 반환한다는 것을 알 수 있다.

<action-state id=”moreAnswersNeeded”>
    <evaluate expression=”interview.moreAnswersNeeded()” />
    <transition on=”yes” to=”answerQuestions” />
    <transition on=”no” to=”finish” />
</action-state>
       
5.5. 액션 구현

액션 코드를 POJO 로직으로 작성하는 것이 가장 일반적이지만, 다른 액션 구현 방법도 있다. 가끔은 액션 코드에서 플로우 컨텍스트에 접속할 필요가 있을 것이다. 이 때 POJO를 호출하고 거기에 flowRequestContext를 EL 변수로 넘겨줄 수 있다. 또는, Action 인터페이스를 구현하거나 MultiAction 기본 클래스를 상속받을 수도 있다. 이러한 대안들이 보다 강한 타입 안전성을 제공하여 여러분의 액션 코드와 스프링 웹 플로우 API 사이를 자연스럽게 연결해준다. 아래에서 이들 각각에 대한 예제를 살펴보자.

5.5.1. POJO 액션 호출하기

<evaluate expression=”pojoAction.method(flowRequestContext)” />   
           

public class PojoAction {
    public String method(RequestContext context) {
        …
    }
}
           
5.5.2. 커스텀 Action 구현체 호출하기

<evaluate expression=”customAction” />   
           

public class CustomAction implements Action {
    public Event execute(RequestContext context) {
        …
    }
}
           
5.5.3. MultiAction 구현체 호출하기

<evaluate expression=”multiAction.actionMethod1″ />
   
           

public class CustomMultiAction extends MultiAction {
    public Event actionMethod1(RequestContext context) {
        …
    }

    public Event actionMethod2(RequestContext context) {
        …
    }

    …
}
           
5.6. 액션 예외

액션은 보통 복잡한 비즈니스 로직을 캡슐화한 서비스를 호출한다. 이러한 서비스는 액션 코드가 처리해야 할 비즈니스 에외를 던질 수도 있다.

5.6.1. 비즈니스 예외를 POJO 액션에서 처리하기

다름 예제는 비즈니스 예외를 잡는 액션을 호출하고, 에러 메시지를 컨텍스트에 추가하고, 결과 이벤트 식별자를 반환한다. 결과는 호출한 플로우가 반응할 수 있는 플로우 이벤트로 간주한다.

<evaluate expression=”bookingAction.makeBooking(booking, flowRequestContext)” />   
           

public class BookingAction {
   public String makeBooking(Booking booking, RequestContext context) {
       try {
           BookingConfirmation confirmation = bookingService.make(booking);
           context.getFlowScope().put(“confirmation”, confirmation);
           return “success”;
       } catch (RoomNotAvailableException e) {
           context.addMessage(new MessageBuilder().error().
               .defaultText(“No room is available at this hotel”).build());
           return “error”;
       }
   }
}
           
5.6.2. 비즈니스 예외를 MultiAction으로 처리하기

다음 예제는 기능적으로 위에 것과 같지만 POJO 액션이 아니라 MultiAction으로 구현했다. MultiAction은 ${methodName}(RequestContext) 형태의 액션 매서드를 필요로 하며 강한 타입 안전성을 제공한다. 반면 POJO 액샨은 더 많은 자유를 제공한다.

<evaluate expression=”bookingAction.makeBooking” />   
           

public class BookingAction extends MultiAction {
   public Event makeBooking(RequestContext context) {
       try {
           Booking booking = (Booking) context.getFlowScope().get(“booking”);
           BookingConfirmation confirmation = bookingService.make(booking);
           context.getFlowScope().put(“confirmation”, confirmation);
           return success();
       } catch (RoomNotAvailableException e) {
           context.getMessageContext().addMessage(new MessageBuilder().error().
               .defaultText(“No room is available at this hotel”).build());
           return error();
       }
   }
}
           
5.7. 기타 액션 호출 예제

5.7.1. on-start

다음 예제는 서비스의 매서드를 호출하여 새로운 Booking 객체를 만드는 액션을 보여준다.

<flow xmlns=”http://www.springframework.org/schema/webflow”
      xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
      xsi:schemaLocation=”http://www.springframework.org/schema/webflow
                          http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd”>

    <input name=”hotelId” />

    <on-start>
        <evaluate expression=”bookingService.createBooking(hotelId, currentUser.name)”
                  result=”flowScope.booking” />
    </on-start>

</flow>
   

5.7.2. on-entry

다음 예제는 상태 진입 액션으로 특별한 fragments 변수를 정의하여 view-state가 뷰의 일부만 랜더링 하도록 하고 있다.

<view-state id=”changeSearchCriteria” view=”enterSearchCriteria.xhtml” popup=”true”>
    <on-entry>
        <render fragments=”hotelSearchForm” />
    </on-entry>
</view-state>
           

5.7.3. on-exit

다음 예제는 상태 종료(exit) 액션으로 편집중이던 레코드에 대한 롹을 해제하고 있다.

<view-state id=”editOrder”>
    <on-entry>
        <evaluate expression=”orderService.selectForUpdate(orderId, currentUser)”
                  result=”viewScope.order” />
    </on-entry>
    <transition on=”save” to=”finish”>
        <evaluate expression=”orderService.update(order, currentUser)” />
    </transition>
    <on-exit>
        <evaluate expression=”orderService.releaseLock(order, currentUser)” />
    </on-exit>
</view-state>

5.7.4. on-end

다음 예제는 위와 같은 객체 롹킹을 플로우 시작과 종료(end) 액션으로 하는 것을 보여준다.

<flow xmlns=”http://www.springframework.org/schema/webflow”
      xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
      xsi:schemaLocation=”http://www.springframework.org/schema/webflow
                          http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd”>

    <input name=”orderId” />

    <on-start>
        <evaluate expression=”orderService.selectForUpdate(orderId, currentUser)”
                  result=”flowScope.order” />
    </on-start>

    <view-state id=”editOrder”>
        <transition on=”save” to=”finish”>
            <evaluate expression=”orderService.update(order, currentUser)” />
        </transition>
    </view-state>

    <on-end>
        <evaluate expression=”orderService.releaseLock(order, currentUser)” />
    </on-end>
   
</flow>

5.7.5. on-render

다음 예제는 랜더 액션으로 뷰를 랜더링 하기 전에 보여줄 호텔 목록을 로딩한다.

<view-state id=”reviewHotels”>
    <on-render>
        <evaluate expression=”bookingService.findHotels(searchCriteria)”
                  result=”viewScope.hotels” result-type=”dataModel” />
    </on-render>
    <transition on=”select” to=”reviewHotel”>
        <set name=”flowScope.hotel” value=”hotels.selectedRow” />
    </transition>
</view-state>
           
5.7.6. on-transition

다음 예제는 하위플로우 결과 이벤트 속성을 컬렉션에 추가하는 트랜지션 액션을 보여준다.

<subflow-state id=”addGuest” subflow=”createGuest”>
    <transition on=”guestCreated” to=”reviewBooking”>
        <evaluate expression=”booking.guestList.add(currentEvent.attributes.newGuest)” /> 
    </transition>
</subfow-state>
           
5.7.7. Names actions

다음 예제는 하나의 action-state 내에서 액션 체인을 호출하는 방법을 보여준다. 각각의 액션 이름은 액션 결과 이벤트에 대한 식별자가 된다.

<action-state id=”doTwoThings”>
    <evaluate expression=”service.thingOne()”>
        <attribute name=”name” value=”thingOne” />
    </evaluate>
    <evaluate expression=”service.thingTwo()”>
        <attribute name=”name” value=”thingTwo” />
    </evaluate>
    <transition on=”thingTwo.success” to=”showResults” />
</action-state>
       
이 예제에서, 플로우는 thingTwo가 무사히 완료되면 showResults로 전이할 것이다.

SWF 4장 뷰 랜더링

4. 뷰 랜더링

4.1. 소개

이번 장에서는 view-state 엘리먼트를 사용하여 플로우에서 뷰를 랜더링하는 방법을 살펴본다.

4.2. 뷰 상태 정의하기

view-state 엘리먼트를 사용하여 뷰를 랜더링하고 사용자 이벤트를 대기하는 플로우 스탭을 정의한다.

<view-state id=”enterBookingDetails”>
    <transition on=”submit” to=”reviewBooking” />
</view-state>
       
규약에 따라, view-state는 자신의 id를 flow가 위치한 디렉터리에서 뷰 템플릿으로 맵핑한다. 예를 들어, 만약에 flow 자체가 /WEB-INF/hotels/booking에 있다면 위의 state는 /WEB-INF/hotels/booking/enterBookingDetails.xhtml을 랜더링할 것이다.

아래는 예제 디렉터리 구조로 뷰와 메시지 번들 같은 기타 리소스가 그들의 플로우 정의와 같이 있는 것을 보여준다.

사용자 삽입 이미지
4.3. 뷰 식별자 작성하기

view 속성을 사용하여 랜더링 할 뷰를 명시적으로 기입한다.

4.3.1. 플로우 상대 뷰 id

뷰 id는 플로우가 있는 작업 디렉터리에 위치한 뷰 리소스에 대한 상대 경로가 될 수 있다.

<view-state id=”enterBookingDetails” view=”bookingDetails.xhtml”>
           
4.3.2. 절대 뷰 id

뷰 id는 웹 애플리케이션 루트 디렉터리에 있는 뷰의 절대 경로가 될 수도 있다.

<view-state id=”enterBookingDetails” view=”/WEB-INF/hotels/booking/bookingDetails.xhtml”>
           
4.3.3. 논리 뷰 id

스프링 MVC의 뷰 프레임워크 같은 몇몇 뷰 프레임워크에서 뷰 id는 프레임워크가 판단할 떄 사용할 논리적인 식별자가 될 수도 있다.

<view-state id=”enterBookingDetails” view=”bookingDetails”>
           
스프링 MVC 연동 뿐에서 어떻게 MVC ViewResolver 기반 시설과 연동하는지 더 많은 정보를 살펴보도록 하자.

4.4. 뷰 스코프

view-state는 들어올 때마다 새로운 viewScope을 할당한다. 이 스코프는 Ajax 요청처럼 같은 뷰에서 여러 요청에 걸쳐 객체를 조작할 때 유용하다. view-state는 해당 state를 나갈 때 viewScope을 소멸한다.

4.4.1. 뷰 변수 할당하기

var 태그를 사용하여 뷰 변수를 선언한다. 플로우 변수처럼, 뷰 상태가 다시 시작하면  모든 @Autowired 레퍼런스를 자동으로 복원한다.

<var name=”searchCriteria” class=”com.mycompany.myapp.hotels.SearchCriteria” />
           
4.4.2. viewScope 변수 할당하기

on-render 태그를 사용하여 뷰 랜더링하기 전에 액션 결과에 변수를 할당한다.

<on-render>
    <evaluate expression=”bookingService.findHotels(searchCriteria)” result=”viewScope.hotels” />
</on-render>
           

4.4.3. 뷰 스코프에 있는 객체 조작하기

뷰 스코프에 있는 객체는 보통 동일한 뷰에서 여러 요청에 의해 조작된다. 다음 예제 페이지는 검색 결과 목록을 보여준다. 목록은 매번 랜더링 하기 전에 뷰 스코프에서 수정된다. 비동기 이벤트 핸들러가 현재 데이터 페이지를 수정하고 검색 결과 부분에 대한 요청을 다시 랜더링 한다.

<view-state id=”searchResults”>
    <on-render>
        <evaluate expression=”bookingService.findHotels(searchCriteria)”
                  result=”viewScope.hotels” />
    </on-render>
    <transition on=”next”>
        <evaluate expression=”searchCriteria.nextPage()” />
        <render fragments=”searchResultsFragment” />           
    </transition>
    <transition on=”previous”>
        <evaluate expression=”searchCriteria.previousPage()” />
        <render fragments=”searchResultsFragment” />         
    </transition>
</view-state>
           
4.5. render 액션 실행하기

on-render 엘리먼트를 사용하여 뷰 랜더링을 하기전에 한 개 이상의 액션을 실행한다. 뷰의 일부만 다시 랜더링하는 것을 포함하여 랜더 액션은 이후의 리프래시와 마찬가지로 초기 랜더링 단계에 실행된다.

<on-render>
    <evaluate expression=”bookingService.findHotels(searchCriteria)” result=”viewScope.hotels” />
</on-render>
       
4.6. 모델로 바인딩하기

model 속성을 사용하여 뷰를 바인딩할 모델 객체를 선언한다. 이 속성은 보통 폼 같은 데이터 컨트롤을 랜더링하는 뷰랑 같이 사용한다. 폼 데이터 바인딩과 검증 작업을 모델 객체에 대한 메타데이터로 주도할 수 있다.

다음 예는 booking 모델을 다루는 enterBookingDetails State다.

<view-state id=”enterBookingDetails” model=”booking”>

모델은 flowScope 또는 viewScope 같이 접근 가능한 모든 스코프에 있는 객체다. model을 정의하면 뷰 이벤트가 발생할 때 다음 작업이 이뤄진다.

  1. 뷰에서 모델로 바인딩. 뷰 포스트백(postback)을 할 때 사용자 입력 값을 모델 객체 속성으로 바인딩한다.
  2. 모델 검증. 바인딩을 한 다음, 만약 모델 객체가 검증을 필요로 하면, 검증 로직을 호출한다.

뷰 상태 전이를 할 수 있는 플로우 이벤트가 발생할 때, 모델 바인딩은 반드시 성공적으로 완료되어야 한다. 만약 바인딩이 실패하면, 뷰는 다시 랜더링하여 사용자가 다시 편집할 수 있게 한다.

4.7. 타입 변환 수행하기

뷰 포스트백을 할 때 모델 바인딩이 발생하면, 바인딩 시스템은 필요하다면 입력 값을 타겟 모델 속성 타입으로 변환을 시도할 것이다. 숫자들과 기본타입, 이넘, 그리고 Date에 대한 기본 컨버터(Converter)는 자동으로 적용된다. 사용자는 또한 자신들이 직접 정의한 타입에 대한 컨버터를 등록할 수 있고 기본 컨버터를 재정의 할 수도 있다.

4.7.1. 컨버터 구현하기

컨버터를 구현하려면, org.springframework.binding.convert.converters.TwoWayConverter 인터페이스를 구현하라. 입력값으로 문자열을 받아서 사용자가 정의한 객체로 변경하는 컨버터를 만들 때 이 인터페이스 구현을 편리하게 해주는 기본 클래스 StringToObject를 제공한다. 간단하게 이 클래슬르 상속하고 두 개의 매서드를 재정의하면 된다.

protected abstract Object toObject(String string, Class targetClass) throws Exception;

protected abstract String toString(Object object) throws Exception;           
          
toObject(String, Class)는 입력 문자열을 객체 타입으로 바꿔야 하고 toString(Object)는 그 반대 일을 해야 한다.

다음 예제는 문자열을 MonetaryAmount로 변환하는 컨버터다.

public class StringToMonetaryAmount extends StringToObject {

  public StringToMonetaryAmount() {
      super(MonetaryAmount.class);
  }

  @Override
  protected Object toObject(String string, Class targetClass) {
      return MonetaryAmount.valueOf(string);
  }

  @Override
  protected String toString(Object object) {
      MonetaryAmount amount = (MonetaryAmount) object;
      return amount.toString();
  }
}           
  
더 많은 컨버터 구현 예제를 보고 싶다면 org.springframework.binding.convert.converters 패키지에 미리 만들어둔 컨버터들을 참고하라.

4.7.2. 컨버터 등록하기

여러분이 작성한 컨버터를 등록하거나 기본 컨버터를 변경하고 싶다면, org.springframework.binding.convert.service.DefaultConversionService를 상속하고 addDefaultConverters() 매서드를 재정의 하라. addConverter(Converter) 매서드를 사용하여 String과 MonetaryAmount 같은 두 타입을 변환할 때 사용할 주요 컨버터(Primary Converter)를 등록하라. 부가적으로 addConverter(String, Converter)를 사용하여 같은 타입에 대한 여러 대체 컨버터(Alternate Converter)를 등록할 수 있다. 예를 들어, java.util.Date를 여러 형태의 문자열로 변환할 수 있겠다.

대체 컨버터는 유일한 converterId로 인덱싱을 하는데 모델 바인딩을 설정할 때 참조할 수 있다. converterId를 지정하지 않으면 주요 컨버터를 사용한다.

ConversionService는 웹 플로우가 런타임에 어떤 타입을 다른 타입으로 변환 해주는 변환 실행기를 룩업할 때 사용하는 객체다. 애플리케이션 당 하나의 ConversionService가 있다. System Setup  절을 참조하여 애플리케이션 전반에 걸쳐 커스텀 컨버터를 등록하는 ConversionService 구현체를 설정하는 방법을 살펴보라. 또한 자세한 내용은 Convert API를 참고하라.

4.8. 바인딩 방지하기

bind 속성을 사용하여 특정 뷰 이벤트에 대한 검증과 모델 바인딩을 방지할 수 있다. 다음 예는 cancel 이벤트가 발생했을 때 바인딩을 방지한다.

<view-state id=”enterBookingDetails” model=”booking”>
    <transition on=”proceed” to=”reviewBooking”>
    <transition on=”cancel” to=”bookingCancelled” bind=”false” />
</view-state>

4.9. 명시적으로 바인딩 설정하기

binder 엘리먼트를 사용하여 뷰에서 사용할 수 있는 모델 바인딩 집합을 설정할 수 있다. 이것은 특히 스프링 MVC 환경에서 뷰 마다 “허용하는 입력값” 집합을 제한할 때 유용하다.

<view-state id=”enterBookingDetails” model=”booking”>
    <binder>
        <binding property=”creditCard” />
        <binding property=”creditCardName” />
        <binding property=”creditCardExpiryMonth” />
        <binding property=”creditCardExpiryYear” />
    </binder>
    <transition on=”proceed” to=”reviewBooking” />
    <transition on=”cancel” to=”cancel” bind=”false” />
</view-state>

 만약에 binder 엘리먼트를 설정하지 않으면 모델의 모든 public 속성이 뷰에서 바인딩하는 대상이 된다. binder 엘리먼트를 사용하여 오직 명시적으로 표현한 속성만 바인딩 한다.

각각의 바인딩은 컨버터를 적용하여 모델 속성 값을 사용자에게 익숙한 형태로 변경할 수 있을 것이다. 만약 컨버터를 명시하지 않으면 모델 속성 타입에 따라 기본 컨버터를 사용하게 된다.

<view-state id=”enterBookingDetails” model=”booking”>
    <binder>
        <binding property=”checkinDate” converter=”shortDate” />
        <binding property=”checkoutDate” converter=”shortDate” />   
        <binding property=”creditCard” />
        <binding property=”creditCardName” />
        <binding property=”creditCardExpiryMonth” />
        <binding property=”creditCardExpiryYear” />
    </binder>
    <transition on=”proceed” to=”reviewBooking” />
    <transition on=”cancel” to=”cancel” bind=”false” />
</view-state>

위의 예제에서 shortDate 컨버터를 chekingDate와 checkoutDate 속성에 적용하고 있다. 커스텀 컨버터를 애플리케이션의 ConventionService로 등록할 수도 있다.

각각의 바인딩은 또한 필수 여부를 확인하여 사용자가 입력한 값이 비어있다면 검증 에러를 발생시키도록 할 수 있다.

<view-state id=”enterBookingDetails” model=”booking”>
    <binder>
        <binding property=”checkinDate” converter=”shortDate” required=”true” />
        <binding property=”checkoutDate” converter=”shortDate” required=”true” />
        <binding property=”creditCard” required=”true” />
        <binding property=”creditCardName” required=”true” />
        <binding property=”creditCardExpiryMonth” required=”true” />
        <binding property=”creditCardExpiryYear” required=”true” />
    </binder>
    <transition on=”proceed” to=”reviewBooking”>
    <transition on=”cancel” to=”bookingCancelled” bind=”false” />
</view-state>

위의 예제에서 모든 바인딩을 필수로 설정했다. 만약 바인딩 할 입력값이 하나라도 비었다면 에러를 만들고 뷰를 에러와 함께 다시 보여줄 것이다.

4.10. 모델 검증하기

모델 검증은 도델 객체에 기반하여 설정한 제약사항으로 주도한다. 웹 플로우는 그러한 제약 사항을 프로그래밍으로 제공한다.

4.10.1. 프로그래밍적인 검증

모델 검증을 프로그래밍적으로 수행하는 두 가지 방법이 있다. 하나는 검증 로직을 모델 객체 내부에 구현하는 것이다. 두 번째는 별도의 Validator를 구현하는 것이다. 두 가지 방법 모두 ValidationContext를 사용하여 에러 메시지를 기록하고 현재 사용자 정보에 접근할 수 있다.

4.10.1.1. 모델 검증 매서드 구현하기

모델 객체 내부에 검증 로직을 정의하는 것은 그 상태를 검증하는 가장 간단한 방법이다. 한 번 이런 로직을 웹 플로우 규약에 맞게 만들어 두면 웹 플로우가 자동으로 그 로직을 view-state 포스트백 라이프사이클 동안에 호출할 것이다. 웹 플로구 규약은 뷰에서 수정하는 모델 속성 서브셋을 쉽게 검증할 수 있게 해준다. 이렇게 하려면 간단하게 validate${state} 형태의 이름을 가진 public 매서드를 만들면 된다. ${state}는 검증을 수행할 view-state의 id다. 예를 보자.

public class Booking {
    private Date checkinDate;
    private Date checkoutDate;
    …
       
    public void validateEnterBookingDetails(ValidationContext context) {
        MessageContext messages = context.getMessages();
        if (checkinDate.before(today())) {
            messages.addMessage(new MessageBuilder().error().source(“checkinDate”).
                defaultText(“Check in date must be a future date”).build());
        } else if (!checkinDate.before(checkoutDate)) {
            messages.addMessage(new MessageBuilder().error().source(“checkoutDate”).
                defaultText(“Check out date must be later than check in date”).build());
        }
    }
}

위 예제에서, Booking 모델을 편집하는 enterBookingDetails view-state로 전이가 발생하면 웹 플로우는 해당 전이에 검증을 방지하지 않았다면 validateEnterBookingDetails(ValidationContext) 매서드를 자동으로 호출한다. 그런 view-state 예제는 아래와 같다.

<view-state id=”enterBookingDetails” model=”booking”>
    <transition on=”proceed” to=”reviewBooking”>
</view-state>
       
모든 검증 메소드를 정의했다. 일반적으로 플로우는 모델을 여러 뷰에서 편집한다. 이 경우에 검증을 수행할 필요가 있는 각각의 view-state에 대한 검증 매서드를 정의했을 것이다.

4.10.1.2. Validator 구현하기

두 번째 방법은 모델 객체를 검증할 Validator라고 하는 별도의 객체를 정의하는 것이다. 그렇게 하려면 우선 ${model}Validator 패턴의 이름을 가진 클래스를 만든다. ${model}은 booking 같은 모델 표현식 형태를 띈다. 그런 다음 validate${state} 형태의 이름을 가진 매서드를 정의한다. ${state}는 enteringBookingDetails같은 view-state id에 해당한다. 클래스는 스프링 빈으로 배포될 수 있어야 한다. 여러 검증 매서드를 정의할 수 있다, 예제를 보자.

@Component
public class BookingValidator {
    public void validateEnterBookingDetails(Booking booking, ValidationContext context) {
        MessageContext messages = context.getMessages();
        if (booking.getCheckinDate().before(today())) {
            messages.addMessage(new MessageBuilder().error().source(“checkinDate”).
                defaultText(“Check in date must be a future date”).build());
        } else if (!booking.getCheckinDate().before(booking.getCheckoutDate())) {
            messages.addMessage(new MessageBuilder().error().source(“checkoutDate”).
                defaultText(“Check out date must be later than check in date”).build());
        }
    }
}
               
위 예제에서 Booking 모델을 편집하는 enterBookingDetilas view-state로 전이가 발생하면 웹 플로우는 validateEnterBookingDetails(Booking, ValidationContext) 매서드를 자동으로 호출할 것이다. 단 이때 해당 전이에 대한 검증을 방지한 상태가 아니어야 한다.

또한 Validator는 스프링 MVC의 Errors 객체로 받을 수 있다. 이 객체는 기존의 스프링 검증기를 호출할 때 필수다.

검증기는 스프링 빈으로 등록되어야 하고 ${model}Validator 라는 이름 규약을 지켜야 한다 그럼 자동으로 감지되고 호출된다. 위 예제에서 스프링 2.5 클래스패스-스캔을 하게 되면 @Component를 감지하고 자동으로 bookingValidator라는 이름의 빈으로 등록이 된다. 그러면 bokking 모델 검증이 필요할 때 이 bookingValidator 인스턴스가 호출될 것이다.

4.10.2. ValidationContext

ValidationContext는 MessageContext를 가져와서 검증을 하는 동안 메시지를 기록한다. 또한 userEvent와 현재 사용자의 Principal 같이 현재 사용자에 대한 정보를 공개한다. 이러한 정보는 검증 로직을 접속한 사용자 또는 UI의 어떤 버튼 또는 링크에 기반하여 커스터마이징 할 수 있도록 해준다. 자세한 정보는 ValidationContext API를 참고하라.

4.11. 검증 방지하기

validate 속성을 사용하여 특정 뷰 이벤트에 대한 모델 검증을 방지할 수 있다.

<view-state id=”chooseAmenities” model=”booking”>
    <transition on=”proceed” to=”reviewBooking”>
    <transition on=”back” to=”enterBookingDetails” validate=”false” />
</view-state>
       
위 예제는 back 이벤트가 발생할 경우 바인딩은 하지만 검증은 하지 않는다.

4.12. 뷰 전이 실행하기

하나 또는 그 이상의 transition 엘리먼트를 정의하여 뷰에서 발생하는 사용자 이벤트를 다룰 수 있다. 트랜지션은 사용자한테 다른 뷰를 보여주거나 단순하게 액션을 실행하거나 현재 뷰를 다시 보여주는 일을 한다. 또한 트랜지션은 애이작스(Ajax)를 다룰 때 프레그먼츠(“fragments”)라고 하는 뷰의 일부를 다시 보여주길 요청할 수도 있다. 마지막으로 글로벌(“global”) 트랜지션이라고 하는 여러 뷰에 걸쳐서 공유하는 것을 정의할 수도 있다.

뷰 트랜지션을 정의하는 방법을 다음 절에서 살펴보도록 하자.

4.12.1. 트랜지션 액션

view-state 트랜지션을 수행하기 전에 하나 또는 그 이상의 액션을 실행할 수 있다. 이러한 액션은 아마 현재 view-state를 종료하지 못하게 에러 결과를 반환할 수도 있다. 만약 에러 결과가 있다면, 뷰는 다시 보여질 것이며 사용자에게 적절한 메시지를 보여줄 것이다.

만약 트랜지션 액션이 일반 자바 매서드를 호출하면, 호출된 매서드는 false를 반환하여 트랜지션 실행을 방지할 수 있다. 이런 기술은 서비스-계층 매서드에서 발생한 예외를 다룰 때 사용할 수 있겠다. 아래 예제는 서비스를 호출하는 액션을 호출하고 예외 상황을 다룬다.

<transition on=”submit” to=”bookingConfirmed”>
    <evaluate expression=”bookingAction.makeBooking(booking, messageContext)” />
</transition>
           

public class BookingAction {
   public boolean makeBooking(Booking booking, MessageContext context) {
       try {
           bookingService.make(booking);
           return true;
       } catch (RoomNotAvailableException e) {
           context.addMessage(builder.error().
               .defaultText(“No room is available at this hotel”).build());
           return false;
       }
   }
}
           
4.12.2. Global 트랜지션

플로우의 global-transitions 엘리먼트를 사용하여 여러 뷰에 걸쳐 적용할 트랜지션을 생성한다, Global-transition은 종종 레이아웃을 구성하는 메뉴 링크 같은 것을 다룰 때 사용한다.

<global-transitions>
    <transition on=”login” to=”login”>
    <transition on=”logout” to=”logout”>
</global-transitions>
           
4.12.3. 이벤트 핸들러

view-state에서 타겟이 없는 트랜지션을 정의할 수도 있다. 이러한 트랜지션을 “이벤트 핸들러”라고 한다.

<transition on=”event”>
    <!– Handle event –>
</transition>
       
이런 이벤트 핸들러는 플로우의 상태를 변경하지 않는다. 단지 그들의 액션을 실행하고 현재 뷰 또는 하나 이상의 프레그먼트를 다시 보여준다.

4.12.4. 프레그먼트 다시 보여주기

트랜지션에서 render 엘리먼트를 사용하여 이벤트를 처리한 다음 현재 뷰에서 일부분만 다시 보여주도록 요청할 수 있다.

<transition on=”next”>
    <evaluate expression=”searchCriteria.nextPage()” />
    <render fragments=”searchResultsFragment” />           
</transition>
           
fragments 속성은 다시 보여주길 원하는 뷰 엘리먼트(들)의 id(여러개)가 될 것이다. 여러 엘리먼트를 다시 보여주도록 설정하려면 콤마로 구분하여 적으면 된다.

이렇게 일부를 다시 보여주는 기법은화면의 특정 부위를 업데이트하는 Ajax에 의한 이벤로 사용된다.

4.13. 메시지 다루기

스프링 웹 플로우의 MessageContext는 플로우 실행 도중에 메시지를 기록하기 위한 API다. 일반 텍스트 메시지는 물론이고 스프링 MessageSource를 사용하는 국제화 메시지도 추가할 수 있다. 메시지는 뷰에서 보여지며 자동으로 플로우 실행 리다이렉트에도 살아남을 것이다. 세 종류의 메시지를 제공한다. info, warning, error다. 또한, 편리한 MessageBuilder를 제공하여 메시지 작성을 돕고 있다.

4.13.1. 일반 텍스트 메시지 추가하기

MessageContext context = …
MessageBuilder builder = new MessageBuilder();
context.addMessage(builder.error().source(“checkinDate”)
    .defaultText(“Check in date must be a future date”).build());
context.addMessage(builder.warn().source(“smoking”)
    .defaultText(“Smoking is bad for your health”).build());
context.addMessage(builder.info()
    .defaultText(“We have processed your reservation – thank you and enjoy your stay”).build());

4.13.2. 국체화 메시지 추가하기

MessageContext context = …
MessageBuilder builder = new MessageBuilder();
context.addMessage(builder.error().source(“checkinDate”).code(“checkinDate.notFuture”).build());
context.addMessage(builder.warn().source(“smoking”).code(“notHealthy”)
    .resolvableArg(“smoking”).build());           
context.addMessage(builder.info().code(“reservationConfirmation”).build());
           
4.13.3. 메시지 번들 사용하기

국제화 메시지는 스프링 MessageSource로 접근하는 메시지 번들에 정의한다. 플로우-관련 메시지 번들을 만들려면, 간단하게 message.properties 파일을 플로우 디렉터리에 만든다. messages.properties 파일을 만들고 지원하고자 하하는 지역 Locale에 해당하는 .properties 파일을 각각 추가한다.

#messages.properties
checkinDate=Check in date must be a future date
notHealthy={0} is bad for your health
reservationConfirmation=We have processed your reservation – thank you and enjoy your stay
           
뷰 또는 플로우에서 여러분은 resourceBundle EL 변수를 사용하여 메시지 번들에 접근할 수도 있다.

<h:outputText value=”#{resourceBundle.reservationConfirmation}” />
           
4.13.4. 시스템이 생성한 메시지 이해하기

웹 플로우 자체가 사용자에게 보여줄 메시지를 생성하는 몇몇 부분이 있다. 이런 일이 발생하는 중요한 지점 중 하나는 뷰에서 모델로 데이터 바인딩을 할 때다. 변환 에러 같은 바인딩 에러가 발생하면 웹 플로우는 자동으로 에러를 여러분 리소스번들에서 가져온 메시지로 맵핑한다. 보여줄 메시지를 가져오려고 웹 플로우는 바인딩 에러 코드와 타겟 속성 이름을 담고 있는 리소스 키를 시도한다.

아래 예제는 Booking 객체의 checkinDate 속성에 바인딩을 생각해보고 있다. 사용자가 알파벳 문자열을 입력했다고 하자. 이런 경우 타입 변환 에러가 발생할 것이다. 웹 플로우는 “typeMismatch’ 에러 코드를 여러분 리소스 번들 중에 있는 메시지를 찾을 때 다음 키를 사용할 것이다.

booking.checkinDate.typeMismatch

키의 앞 부분은 모델 클래스의 짧은 이름이다. 두 번째 부분은 속성의 이름이다, 세 번째 부분은 에러 코드다. 모델 속성에 바인딩이 실패하면 사용자에게 보여줄 유일한 메시지를 이런식으로 찾는다. 아마 다음과 같은 메시지일 수 있다.

booking.checkinDate.typeMismatch=The check in date must be in the format yyyy-mm-dd.

만약 저 형식에 해당하는 키를 찾지 못하면, 보다 일반적인 키로 시도해본다. 이 키는 간단한 에러 코드 형태다. 속성의 플드 이름은 메시지 인자로 제공된다.

typeMismatch=The {0} field is of the wrong type.
           
4.14. 팝업 보여주기

popup 속성을 사용하여 모델 팝업 창에서 뷰를 랜더링 한다.

<view-state id=”changeSearchCriteria” view=”enterSearchCriteria.xhtml” popup=”true”>
       
스프링 자바스크립트랑 같이 웹 플로우를 사용하면, 팝업을 보여줄 때 클라이언트 쪽 코드를 사용할 필요가 없다. 웹 플로우는 팝업에서 뷰로 리다이렉트를 요청하는 사용자에게 응답을 보낼 것이고 그 결과 클라이언트는 요청은 잘 처리될 것이다.

4.15. 뷰 백트랙킹(backtracking)

기본적으로, 뷰 스테이트를 종료하고 새로운 뷰 스테이트로 전이하면, 여러분은 브라우저의 뒤로 가기 버튼을 사용하여 이전 상태로 이동할 수 있다. 이러한 뷰 스테이트 히스토리 정책은 history 속성을 사용하여 트랜지션 당 설정할 수 있다.

4.15.1. 히스토리 방지하기

history 속성을 discard로 설정하여 뷰 백트랙킹을 방지할 수 있다.

<transition on=”cancel” to=”bookingCancelled” history=”discard”>

4.15.2. 히스토리 금지하기

history 속성을 invalidate로 설정하여 뷰 백트랙킹을 방지하고 이전의 뷰로 전이하는 것도 방지한다.

<transition on=”confirm” to=”bookingConfirmed” history=”invalidate”>
           

EJ2E Item 19. 인터페이스는 오직 타입을 정의할 때만 사용하라

참조: Effective Java 2nd Edition

어떤 클래스가 인터페이스를 구현할 때 인터페이스는 해당 클래스의 인스턴스를 참조할 수 있는 타입을 제공한다. 즉 해당 클래스 인스턴스를 가지고 고객이 무엇을 할 수 있는지 알려주는 것이다. 이 경우 이외에 다른 의도로 인터페이스를 사용하는 것은 부적절하다.

상수 인터페이스(constant interface) 안티 패턴
– 매서드 없이 상수를 표현한 static final 필드만 있다.
– 그 상수를 사용하는 클래스는 해당 인터페이스를 구현하여 상수 이름을 클래스 이름으로 구분해야 할 필요를 없앤다.(A.C B.C 대신에 A와 B가 구현하는 인터페이스 Z로 C를 올려서 Z.C 형태로 사용하게 하는 건가보네요.)
– 클래스가 내부에서 어떤 상수를 사용하는지는 구체적인 내용에 해당하는데 클래스가 상수 인터페이스를 구현하면 구체적인 내용을 밖으로 노출하게 된다.
– 차후에 해당 상수가 필요없어지더라도 바이너리 호환성을 위해 해당 인터페이스를 그대로 구현하고 있어야 한다.
– 자바 라이브러리 중에 java.io.ObjectStreamConstants는 예외다.
– 상수를 다룰 땐 해당 상수가 속하는 클래스나 인터페이스를 잘 고려해야 하며 열거형이 아닌지 보고 열거형일 경우에는 enum type 사용을 고려하라.

상수를 자주 사용할 때는 static import를 사용할 수도 있겠다.

인터페이스로는 타입을 정의해야지 상수를 표현하지 않아야 한다.

난 몇 살?

lk250000000000.mp3
나이가 세 종류나 되며 각각의 계산 식도 있네요. 검색해본 글에 따르면 제 나이는..

기본정보
– 태어난 날: 1982년 7월 15일
– 오늘: 2009년 1월 26일

1. 만 나이(western age) = 2009 – 1982 – 1 = 26
2. 연 나이(year age) = 2009 -1982 = 27
3. 한국 나이(korean age) = 2009 – 1982 + 1 = 28

26~28 중에 아무 숫자나 부르면 되는군요. 만으로 26인데 한국 나이로는 28살이라~ 캬.. 느낌이 전혀 다르네요.