Java

[펌]JSF(JavaServer Faces) 활용하기 [2/3]

_침묵_ 2005. 10. 6. 20:24
  제   목 :JSF(JavaServer Faces) 활용하기 [2/3]
사용자 삽입 이미지
  작성자 :theclub (theclub@jspgeek.com)작성일 : 2003-11-06 01:45:35    조회 :541

지난 호 「JSF(JavaServer Faces) 활용하기 [1/3]」에서는 「JavaServer Faces」(이하JSF) 의 개요를 설명한 후,「Java Web Services Developer Pack」(이하JWSDP) 에 포함되어 있는 샘플 어플리케이션을 동작 시켜 보았다. 이번에는 필자가 작성한 JSF 어플리케이션을 소개하면서 더 깊게 이해하는 시간이 되었으면 좋겠다.

설명을 시작하기 전에 전회의 글을 읽어도 JSF의 메리트를 잘 모른다고 하는 분들을 위해서 다시 한번 JSF의 메리트에 대해서 아래와 같이 정리해 본다.

●스테이트 정보 보관 유지
폼에 입력된 값은 JSF의 context에 보관 유지된다. 그렇기 때문에 validator에 의해서 값이 적당하지 않다고 판단되었을 경우 다시 폼을 표시할 때 이전에 입력한 값들을 폼에 표시할 수 있다.

●Web 어플리케이션을 구축하는데 필요한 컴퍼넌트 준비

입력 값의 체크(validator)
입력한 값이 적당(필수 필드의 체크 , 문자 입력 패턴의 체크 등)한지 판단한다. 적당하지 않은 경우 메세지를 표시한다.

형 변환(converter)
입력된 값을 지시한 Java의 형으로 변환한다. 이런 기능덕분에 용이하게 데이터 모델과 연계할 수 있다.

이벤트의 제어(이벤트 handler)

컴퍼넌트마다 이벤트를 정의할 수가 있다.

데이터 모델 매핑
개발 최종 단계인 시스템 데이터(데이타베이스, JavaBeans 등)와 JSF의 UI component를 매핑 할 수가 있다.

JSP 태그
JSF의 UI component를 사용하기 위한 태그가 준비되어 있어 용이하게 JSF를 사용해서 Web 페이지를 만들 수 있다.

UI component 추가

JSF에 준비되어 있는 컴퍼넌트 이외에 독자적인 UI component를 작성할 수가 있어 자유로운 UI를 작성을 할 수 있다. 그리고 이 컴퍼넌트를 재사용할 수 있다.

지금까지 JSP와 Servlet, JavaBeans를 이용해서 Web 어플리케이션을 개발하면서 필요하다고 모두가 느끼고 있었던 것이지만, 귀찮았던 처리를 없애주는 것이 JSF라고 말할 수 있겠다. 그리고 JSF가 Web 어플리케이션의 유저 인터페이스를 구축하기 위한 표준 체제가 되면 개발 모델의 통일화를 꾀할 수도 있어 개발 효율이나 서포트 효율의 향상도 기대할 수 있겠다.

그러면 JSF 어플리케이션의 구축으로 이야기를 옮긴다.

JSF 어플리케이션의 구축

필자가 작성한 어플리케이션은 JWSDP에 포함되어 있는 「Car Demo Sample」를 간략화한 것이다. 어플리케이션의 원시 코드와 WAR패키지는이쪽에서다운로드하기 바란다.

어플리케이션의 내용은 아래와 같습니다.

 

(1)카탈로그로에서 차를 선택
(2)차의 패키지(Custom,Deluxe), 옵션(엔진 , 카 오디오 ,GPS, 시큐러티 시스템) 선택 , 변경 및 비용의 계산
(3)쇼핑 내용 확인
(4)신용 카드 번호, 카드의 사용 기한을 입력해서 쇼핑 완료
(5)감사 메세지 표시

사용자 삽입 이미지
Java Server Faces Lifecycle (JSR-127)

그리고 어플리케이션의 페이지 구성과 클래스의 관계는 아래 사진과 같다.

사용자 삽입 이미지
도1 어플리케이션의 페이지 구성과 클래스의 관계

다음에 JSF 어플리케이션의 디렉토리 구조, 파일 구성(이미지 파일 등은 생략)에 대해서 설명한다. JSF 어플리케이션의 디렉토리 구조는 기존의 Web 어플리케이션과 같다. 어플리케이션의 루트 디렉토리에는 JSP,HTML등의 파일을 배치하고, WEB-INF 디렉토리에 faces-config.xml, web.xml 등의 어플리케이션 정의 파일, WEB-INF/classes 디렉토리에 작성한 클래스 파일을 저장하고 WEB-INF/lib 디렉토리에는 어플리케이션에 필요한 라이브러리를 배치한다.

JSF 어플리케이션을 실행하기 위해서 필요한 라이브러리는 아래 그림을 참고하기 바란다 (다만, 이번 어플리케이션은 JWSDP에 번들되어 있는 Tomcat에 deploy하기 때문에 아래 그림의 모든 Jar 파일을 WEB-INF/lib에 준비하지 않아도 동작한다 ).

사용자 삽입 이미지
도2 디렉토리 구성

아래는 필자가 JSF 어플리케이션을 작성한 순서이다.

(1)모델, 비지니스 논리
·모델, 비지니스 논리 클래스의 개발
·configuration file의 설정
(2)

custom converter, custom validator, 탭 handler 클래스의 개발
configuration file의 설정

(3)UI component, JSF 태그 라이브러리를 사용해서 JSP 작성
(4)페이지 네비게이션의 정의, 기타

그러면 작성한 순서에 따라 어플리케이션 처리 내용을 설명해 나가겠다.


모델과 비지니스 논리

모델 , 비지니스 논리 클래스의 개발

우선 데이터를 보관 유지하는 모델부터 설명한다. 데이터를 보관 유지하기 위한 클래스는 크게 2가지가 있는데, 하나는 선택한 차와 차의 옵션에 관한 정보를 보관 유지하는CurrentOptionServer이고 또 다른 하나는 신용 카드 번호 등의 정보를 보관 유지하는CustomerBean이다. CurrentOptionServer는 초기 페이지(Storefront.jsp)에서 선택한 차종과 옵션 선택 화면(more.jsp) 에서 선택한 패키지 등 옵션에 관한 정보를 보관 유지하기 위한 클래스이다. 이 클래스내에서는 JSF의 API도 사용하고 있으나 이 API 에 대해서는 잠시 후에 설명할 것이다.

사용자 삽입 이미지
초기 페이지(Storefront.jsp)

사용자 삽입 이미지
옵션 선택 화면(more.jsp)

다음은 유저의 액션에 대응한 비지니스 논리를 실행하는 2개의 클래스를 설명하겠다. 지난 호에서 설명한 것처럼 JSF는 이벤트 listener 모델을 사용하고 있어, Action 이벤트와 value-changed 이벤트 등 2개의 이벤트를 지원하고 있다. Action 이벤트는 UICommand와 버튼이나 링크를 유저가 클릭했을 경우에 발생하는 이벤트로써, javax.faces.event.ActionListener에 의해 이벤트를 처리한다. CarActionListener는 이 ActionListener를 implement한 클래스이다.

그리고 value-changed 이벤트는 UIInput이나 서브 클래스의 컴퍼넌트의 값이 변화했을 경우에 발생하며, javax.faces.event.ValueChangedListener에 의해 이벤트를 처리한다.

ActionListener의 개발 클래스에서는 getPhaseId()와 processAction(actionEvent) 등 2개의 메소드를 정의한다. getPhaseId 메소드는 라이프 사이클의 어느 단계에서 이 이벤트가 처리될지 식별자를 지정한다. Listener는 이 식별자로 정의된 처리가 완료되면 처리를 실행한다. 여기에서는 「PhaseId.APPLY_REQUEST_VALUES」을 지정하고 있어 Apply Request 페이지가 완료한 후 에 이벤트가 처리되는 것을 지정할 수 있다.

리스트1 getPhaseId메소드
public PhaseId getPhaseId() {
    return PhaseId.APPLY_REQUEST_VALUES;
}

그리고 processAction에는 발생한 이벤트를 처리한다. CarActionListener는 Storefront.jsp에서 차의 선택, more.jsp에서 패키지의 선택, 비용의 재계산 등에 대한 이벤트를 처리한다. processAction은 인수로 javax.faces.event.ActionEvent를 받고, JSF 태그내에서 지정된 commandName을 받아 이 커멘드에 대응한 처리를 호출한다. 예를 들면 commandName이 「deluxe」인 경우(more.jsp 페이지에서 패키지로 「Deluxe」을 선택한 경우)는 processAction에서 processDeluxe를 호출해서 이 메소드내에서 처리한다.

리스트2 processAction메소드
public void processAction(ActionEvent event) {
    String actionCommand = event.getActionCommand();
    processActionCommand(actionCommand);
    ResourceBundle rb = ResourceBundle.getBundle("carshop/Resources", FacesContext.getCurrentInstance().getLocale());

    if (actionCommand.equals("custom")) {
        processCustom(event, rb);
    } else if (actionCommand.equals("deluxe")) {
        processDeluxe(event, rb);
-- 생략 --
}

사용자 삽입 이미지
패키지에서 Deluxe를 선택했는데

다음에는 processDeluxe 메소드의 처리에 대해 설명하겠다. processDeluxe 처음에 ActionEvent로부터 이 액션을 생성한 UI component를 꺼내고 있어, 이 UI component를 통해 컴퍼넌트 트리내에서 UI component를 취득할 수 있다.

예를 들어 component.findComponent("securitySystem")을 실행했을 경우 more.jsp에 차의 옵션 「SecuritySystem」을 선택하기 위한 체크 박스로 사용되고 있는 컴퍼넌트를 취득할 수 있다.

private void processDeluxe(ActionEvent event, ResourceBundle rb) {
        UIComponent component = event.getComponent();
-- 생략 --
component.findComponent("securitySystem");

다음은 FacesContext.getCurrentInstance()로 javax.faces.context.FacesContext을 취득하고 있다. FacesContext는 1개의 Request와 거기에 대응하는 Response에 관한 상태를 보관 유지하고 있기때문에, 검증시 에러가 발생한다면, 메세지 등을 유저에게 표시ㅏ기 위해서 FacesContext에 저장한다. 여기서 취득한 FacesContext는 이 후 CurrentOptionServer의 값을 꺼내거나 갱신을 하는데 사용한다.

FacesContext context = FacesContext.getCurrentInstance();

다음은 Web 어플리케이션을 나타내는 javax.faces.application.Application을 얻는다. Application 인스턴스는 javax.faces.FactoryFinder의 getFactory 메소드에 의해서 javax.faces.application.ApplicationFactory를 얻고, 이ApplicationFactory의 getApplication()를 호출해서 javax.faces.application.Application을 얻는다. 그리고 Application 클래스의 메소드 getValueBinding에 인수로 CurrentOptionServer.engineOption을 지정하면, CurrentOptionServer의 engineOption property 값을 나타내는 javax.faces.el.ValueBinding 오브젝트를 얻을 수 있다. ValueBinding 오브젝트를 취득한 후에는 FacesContext를 getValue 메소드에 의한 모델 property 값을 취득하거나 setValue로 값을 갱신할 수 있다.

JSF에서는 이와 같이 FacesContext, Application를 취득 후, ValueBinding 오브젝트를 Application로부터 취득해, 모델의 property 값을 가져오거나 갱신한다.

ApplicationFactory factory = (ApplicationFactory)FactoryFinder.getFactory(
"javax.faces.application.ApplicationFactory");
Application application = factory.getApplication();
ValueBinding eOptionBinding = application.getValueBinding("CurrentOptionServer.engineOption");
eOptionBinding.setValue(context, engineOption);

그리고 이 메소드내에서는 상기 이외에 각종 UI component의 취득이나 특정 UI component의 property 설정 등을 한다. 하지만 세세한 설명에 대해서는 생략한다. 실제로 어플리케이션을 동작시켜 보면서 내용을 확인해 주기 바란다.

리스트3 processDeluex메소드 전체
    

private void processDeluxe(ActionEvent event, ResourceBundle rb) {
        UIComponent component = event.getComponent();
        UIComponent foundComponent = null;
        UIOutput uiOutput = null;   
        String value = null;
        boolean packageChange = false;
        int i = 0;


        FacesContext context = FacesContext.getCurrentInstance();

        String[] engines = {"V4", "V6", "V8"};
        ArrayList engineOption = new ArrayList(engines.length);
        for (i=0; i<engines.length; i++) {
            engineOption.add(new SelectItem(engines[i], engines[i], engines[i]));
        }

        ApplicationFactory factory = (ApplicationFactory)FactoryFinder.getFactory(
"javax.faces.application.ApplicationFactory");
        Application application = factory.getApplication();
        ValueBinding eOptionBinding = 
application.getValueBinding("CurrentOptionServer.engineOption");
        eOptionBinding.setValue(context, engineOption);
        
        foundComponent = component.findComponent("currentEngine");
        uiOutput = (UIOutput)foundComponent;
        if (!((application.getValueBinding("CurrentOptionServer.currentPackage")
).getValue(context)).equals("deluxe")) {
            value = engines[0];
            packageChange = true;
        } else {
            value = (String)uiOutput.getValue();
        }

        uiOutput.setValue(value);


        ValueBinding currentEOBinding = application.getValueBinding("CurrentOptionServer.currentEngineOption");
        currentEOBinding.setValue(context, value);
        
        ValueBinding currentPBinging = application.getValueBinding("CurrentOptionServer.currentPackage");
        currentPBinging.setValue(context, "deluxe");


        foundComponent = component.findComponent("securitySystem");
        foundComponent.setAttribute("disabled", "true");
        ((UIOutput)foundComponent).setValue(Boolean.TRUE);
        foundComponent.setAttribute("selectbooleanClass", "package-selected");


        ValueBinding securityBinding = application.getValueBinding("CurrentOptionServer.securitySystem");
        securityBinding.setValue(context, Boolean.TRUE);
     -- 생략 --
        foundComponent = component.findComponent("custom");
        foundComponent.setAttribute("commandClass", "package-unselected");

        foundComponent = component.findComponent("deluxe");
        foundComponent.setAttribute("commandClass", "package-selected");
    }
-- 생략 --
}


다음은 PackageValueChanged클래스에 대해서 설명한다. ActionListener의 경우와 마찬가지로 ValueChangedListener도getPhaseId를 개발해 처리되어야 할 페이즈를 지정한다. 그리고 ValueChangedListener ,processValueChanged(valueChangedEvent)메소드에 대해 발생한 ValueChangedEvent에 대한 처리를 개발한다.

processValueChanged 처음에 ValueChangedEvent를 발생시킨 컴퍼넌트의 ID를 취득한 다음에 CurrentOptionServer로부터 현재의 차의 가격을 꺼내고 있다. 그리고 컴퍼넌트의 변경에 가격을 계산한 결과를 CurrentOptionServer에 갱신한다. 그리고 이 클래스내에서도 JSF의 API를 사용하고 있지만 CarActionListener에서 설명한 것과 같은 처리를 실시하고 있으므로 설명을 생략한다.

리스트4 PackageValueChanged.java
public class PackageValueChanged implements ValueChangedListener {
    -- 생략 --
    public PhaseId getPhaseId() {
        return PhaseId.PROCESS_VALIDATIONS;
    }
    
    public void processValueChanged(ValueChangedEvent vEvent) {
        try {
            String componentId = vEvent.getComponent().getComponentId();
            FacesContext context = FacesContext.getCurrentInstance();
            String currentPrice;
            int cPrice = 0;

            ApplicationFactory factory = 
(ApplicationFactory)FactoryFinder.getFactory(
"javax.faces.application.ApplicationFactory");
            Application application = factory.getApplication();

            ValueBinding binding = application.getValueBinding("CurrentOptionServer.carCurrentPrice");
            currentPrice = (String)binding.getValue(context);
            cPrice = Integer.parseInt(currentPrice);
            if ((componentId.equals("currentEngine")) || (componentId.equals("currentAudio"))) {
                cPrice = cPrice - (this.getPriceFor((String)vEvent.getOldValue()));
                cPrice = cPrice + (this.getPriceFor((String)vEvent.getNewValue()));
            } else {
                Boolean optionSet = (Boolean)vEvent.getNewValue();
                cPrice = calculatePrice(componentId, optionSet, cPrice); 
            }
            
            currentPrice = Integer.toString(cPrice);
            ValueBinding resultBinding = application.getValueBinding("CurrentOptionServer.carCurrentPrice");
            resultBinding.setValue(context, currentPrice);
        } catch (NumberFormatException ignored) {
        }
    }
    
    public int calculatePrice(String optionKey, Boolean optionSet, int cPrice) {
        if (optionSet.equals(Boolean.TRUE)) {
            cPrice = cPrice + (this.getPriceFor(optionKey));
        } else {
            cPrice = cPrice - (this.getPriceFor(optionKey));
        }
        return cPrice;
    }
    
    public int getPriceFor(String option) {
     -- 생략 --
        if (option.equals("V4")) {
            return (100);
        }
        else if (option.equals("V6")) {
            return (300);
        }
    }
}

configuration file의 설정

다음으로는 여기까지 소개한 클래스를 JSF 어플리케이션으로 이용하기 위해서 필요한 정의에 대해 설명한다.

JSF에서는 빈을 어플리케이션 설정 파일인 「faces-config.xml」에 등록해서 인스턴스화하는 것이 가능하다. JSF 개발은 어플리케이션 기동시에 faces-config.xml의 정의를 읽어들여 빈을 초기화하고 지정된 scope내에 보존한다. 정확히 말하면 JSP안에서 useBean 액션을 사용해서 인스턴스화하는 것과 동일하게 configuration 파일을 사용해 실시할 수 있는 것이다.

어플리케이션에 대해 반드시 필요한 빈에 관해서는 종래와 같이 페이지에서 인스턴스화하는 것이 아니라, configuration 파일에서 인스턴스화할 수 있기 때문에, 의도하지 않은 페이지 액세스에도 대응할 수 있다.

<managed-bean>
    <managed-bean-name>CurrentOptionServer</managed-bean-name>
    <managed-bean-class>carshop.CurrentOptionServer</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
    <managed-property>
        <property-name>carImage</property-name>
        <value>current.gif</value>
    </managed-property>
</managed-bean>

 

custom converter, custom validator, tab handler

custom converter, custom validator, tab handler 클래스 개발

다음으로는 custom converter, custom validator, validator용 태그 handler를 설명한다.

손님 정보 입력 화면(Customer.jsp)에 있어 입력된 신용 카드 번호를 변환해 주는 converter는 CreditCardConverter이다.

사용자 삽입 이미지
손님 정보 입력 화면(Customer.jsp)

독자적인 converter를 작성하는 경우는 javax.faces.convert.Converter 인터페이스를 implements한 클래스를 작성해 2개의 메소드를 작성한다. 첫번째 매소드는 request parameter로 받은 String등에 대응하는 모델 오브젝트로 변환하는 getAsObject 메소드로 Apply Request Values 페이지로 불려 간다. 그리고 두번째가 모델 데이터를 프레젠테이션의 String로 변환하는 getAsString 메소드로 Render Response 페이즈에 대해 불려 간다.

리스트5 CreditCardConverter.java

public class CreditCardConverter implements Converter {
    
    public Object getAsObject(
FacesContext context, UIComponent component, String newValue) 
throws ConverterException {

        String convertedValue = null;
        if ( newValue == null ) {
            return newValue;
        }
        convertedValue = newValue.trim();
        if ( ((convertedValue.indexOf("-")) != -1) || 
((convertedValue.indexOf(" ")) != -1)) {
            char[] input = convertedValue.toCharArray();


            StringBuffer buffer = new StringBuffer(50);
            for ( int i = 0; i < input.length; ++i ) {
                if ( input[i] == '-' || input[i] == ' '  ) {
                    continue;
                } else {
                    buffer.append(input[i]);
                }
            }
            convertedValue = buffer.toString();
        }           
        return convertedValue;
    }
     
    public String getAsString(
FacesContext context, UIComponent component, Object value)
 throws ConverterException {
    -- 생략 -- 
// 모델 데이터를 , 프레젠테이션으로서의String로 변환하는 논리를 개발
    }
}

입력된 신용 카드 번호의 포맷이 올바른가를 검증하는 것이 FormatValidator 클래스이다. 독자적인 Validator을 개발할 때는 javax.faces.validator.Validator 인터페이스를 implements해서 constructor, 아크셋사메솟드, 그리고 실제로 값을 검증하는 validate 메소드를 개발한다.

validate 메소드에서는 처음에 유저가 입력한 신용 카드 번호를 (((UIOutput)component).getValue()).toString()로 취득해 이 후 소개하는 FormatValidatorTag JSP custom 태그의 formatPatterns로 지정한 포맷과 일치하는지를 검증한다. 그리고 검증의 결과는 validator가 등록되어 있는 UI component에 setValid 메소드로 설정한다.

그런데, 앞에서 소개한 guessNumber 어플리케이션에서는 룰에 반하는 범위외의 수치를 입력했을 때에 에러가 표시되었다. 이번 어플리케이션에서는 이 메세지의 생성하는 부분의 처리에 대해서도 개발한다.

우선 메세지 생성을 위해서 getMessageResources 메소드로 javax.faces.context.MessageResources의 인스턴스를 취득한다. MessageResources는 message 템플릿의 콜렉션으로 로컬라이즈 된 javax.faces.application.Message 인스턴스를 구축하기 위해서 사용하며, 여기에서는 「carShopResources」이라고 하는 ID로 취득하고 있다. MessageResources의getMessage() 메소드를 호출하는 것으로 Message 인스턴스를 취득, FacesContext에 등록하면 에러 메세지가 유저에게 돌아간다.

덧붙여 여기에서는 실제로 메세지 자체를 기술하고 있지만, 메세지는 다음에 설명하는 faces-config.xml에 정의한다.

리스트6 FormatValidator.java
public class FormatValidator implements Validator {
    
    public static final String FORMAT_INVALID_MESSAGE_ID  = "carshop.Format_Invalid";
    private ArrayList formatPatternsList = null;
    private String formatPatterns = null;
    
    public FormatValidator(String formatPatterns) {
        super();
        this.formatPatterns = formatPatterns;
        parseFormatPatterns();
    }
    public String getFormatPatterns() {
        return (this.formatPatterns);
    }
    public void setFormatPatterns(String formatPatterns) {
        this.formatPatterns = formatPatterns;
        parseFormatPatterns();
    }
    public void parseFormatPatterns() {
    -- 생략 --
        StringTokenizer st =  new StringTokenizer(formatPatterns, "|");
        while (st.hasMoreTokens()) {
            String token = st.nextToken();
            formatPatternsList.add(token);
        }
    }
    
    public void validate(FacesContext context, UIComponent component) {
     -- 생략 --
        String value = (((UIOutput)component).getValue()).toString();
        Iterator patternIt = formatPatternsList.iterator();
        while (patternIt.hasNext()) {
            valid = isFormatValid(((String)patternIt.next()), value);
            if (valid) {
                break;
            }
        }
        if (valid) {
            component.setValid(true);
        } else {
            component.setValid(false);
            Message errMsg = getMessageResources().getMessage(context, FORMAT_INVALID_MESSAGE_ID, 
(new Object[] {formatPatterns})); // 메세지를 나타내는Message의 취득
            context.addMessage(component, errMsg); // FacesContext에의 메세지의 등록
        }
    }
    
    protected boolean isFormatValid(String pattern, String value) {
     -- 생략 --
        value = value.trim();

        char[] input = value.toCharArray();
        char[] fmtpattern = pattern.toCharArray();
        for ( int i = 0; i < fmtpattern.length; ++i ) {
            if (!(Character.isDigit(input[i]))) {
                valid = false;
            }
        }
        return valid;
    }    
    
    public synchronized MessageResources getMessageResources() {
        MessageResources carResources = null;
        ApplicationFactory aFactory = (ApplicationFactory)FactoryFinder.getFactory(
FactoryFinder.APPLICATION_FACTORY);
        Application application = aFactory.getApplication();
carResources = application.getMessageResources("carShopResources");
        return (carResources);
    }

FormatValidatorTag는 FormatValidator를 JSP에서 이용하기 위한 태그 handler 클래스이다. Validator의 태그 handler를 개발할 때는 javax.faces.webapp.ValidatorTag 클래스를 상속한다.

FormatValidatorTag는 우선 constructor 내에서 super.setID("FormatValidator")를 호출해서, FormatValidator라고 하는 ID로 이Validator를 등록한다. 이 ID는 faces-config.xml에 Validator를 등록할 때 ID(<validator-id>)와 일치해야 한다. 다음에 createValidator 메소드로 FormatValidar를 인스턴스화해 JSP 태그내에서 지정한 포맷을 formatPatterns property로 설정하고 있다. 실제로 값의 검증은 방금전 작성한 FormatValidator에서 실시한다.

리스트7 FormatValidatorTag.java
public class FormatValidatorTag extends ValidatorTag {
    
    protected String formatPatterns = null;    
    
    public FormatValidatorTag() {
        super();
        super.setId("FormatValidator");        
    }
    
    public String getFormatPatterns() {
        return formatPatterns;
    }

    public void setFormatPatterns(String fmtPatterns){
       formatPatterns = fmtPatterns;
    }
    
    protected Validator createValidator() throws JspException {
        
        FormatValidator result = null;
        result = (FormatValidator) super.createValidator();        
        result.setFormatPatterns(formatPatterns);
        
        return result;
    }
}

사용자 삽입 이미지
구입 화면(buy.jsp)


UI component, JSF 태그 라이브러리를 사용해서 JSP 작성

온라인 쇼핑몰의 첫 페이지인 Storefront.jsp부터 작성한다. 덧붙여 이번은 HTML 관련의 태그에 관해서는 기본적으로 설명을 생략 하고 이번에 개발한 ActionListener,Validator 등 을 이용하는 태그를 중심으로 설명한다.

어플리케이션 전반적으로 uri="http://java.sun.com/jstl/fmt"를 지정한 태그 라이브러리를 사용하고 있지만, 이 태그 라이브러리는 JTSL(JSP Standard Tag Library) 의 국제화용 태그다. JSF에도 어플리케이션의 국제화를 위한 독자적인 기능도 있지만, 기존의 기술도 적극적으로 이용하고 있다. fmt:setBundle에 의해 자원·번들명을 설정해 각각의 HTML 태그내에서 자원·번들내의 메세지를 이용하고 있다.

예를 들면 <h:command_button..>라고 쓰면서 key="moreButtun" bundle="carshopBundle"이라고 지정하면, 설정한 자원의 moreButton 키의 값을 꺼내, 버튼의 라벨로 사용하게 된다.

Storefront.jsp에서는 버튼으로서 표시되고 있는 UICommand 컴퍼넌트를 사용하고 있어 이 커멘드 컴퍼넌트에 <f:action_listener type="carshop.CarActionListener"/> 태그를 지정함으로써 ActionListener 를 컴퍼넌트에 등록하고 있다. 이 정의에 의해 이 버튼이 클릭되었을 때 이벤트가 CarActionListener에서 처리되게 된다.

리스트9 Storefront.jsp

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

  <fmt:setBundle basename="carshop.Resources"
 scope="session" var="carshopBundle"/>

  <f:use_faces>
  <h:form formName="carStoreForm">

  <TABLE BORDER="0" CELLPADDING="0"
 CELLSPACING="6" WIDTH="660" BGCOLOR="white">
      <TR>
          <TD WIDTH="50%" VALIGN="TOP">
              <TABLE BORDER="0" ALIGN="left">
                  <TR>
                      <TD><h:graphic_image  graphicClass=
"alignLeft" url="/150x126_Roadster.jpg" />
                  </TR>
              </TABLE>
              <P><BR>
              <B><FONT SIZE="4" COLOR="#330066"
 FACE="Arial, Helvetica">
                  <h:output_text key="car1Title"
 bundle="carshopBundle" />
              </FONT></B>
              <FONT FACE="Arial, Helvetica"><BR><BR>
                  <h:output_text  key="car1Desc"
 bundle="carshopBundle"  />
              <BR></FONT>


              <h:command_button label="More"
 key="moreButton"
 action="success" bundle="carshopBundle"
 commandName="more1">
                  <f:action_listener type=
"carshop.CarActionListener"/>
              </h:command_button>
          </TD>
     -- 생략 --


사용자 삽입 이미지
초기 페이지(Storefront.jsp)

다음으로 패키지의 변경(Custom,Deluxe), 옵션의 선택 (엔진, Audio,SecuritySystem, GPS), 금액을 계산하는 more.jsp이다. more.jsp에서 Engine 옵션을 선택하기 위한 리스트 박스, Audio의 옵션을 선택하는 라디오 버튼을 표시하고 있는 컴퍼넌트에 대해서 <f:valuechanged_listener type="carshop.PackageValueChanged" />태그로 value-changed 이벤트를 등록하고 있다.

리스트10 more.jsp
-- 생략 --
<TR>
  <TD WIDTH="100%" BGCOLOR="white"><B>
    <FONT SIZE="3" COLOR="#330066" FACE="Arial, Helvetica">
      <h:output_text key="Package" bundle="carshopBundle" />
    </FONT></B>
    <BR>
    <%-- 패키지를 선택하기 위한 버튼 및ActionListener을 등록 --%>
    <h:command_button id="custom"
 commandName= "custom" 
commandClass="package-selected" key="Custom" bundle="carshopBundle">
      <f:action_listener type="carshop.CarActionListener" />
    </h:command_button>
    <h:command_button id="deluxe" commandName=
"deluxe" key="Deluxe" bundle="carshopBundle">
     <f:action_listener type="carshop.CarActionListener" />
    </h:command_button>
    <BR><BR>
-- 생략 --
        <TD><P><B>
          <FONT COLOR="#93B629" FACE="Arial, Helvetica">
            <h:output_text key="Engine" bundle="carshopBundle" />
          </FONT></B></P>
          <BLOCKQUOTE><P>
          <FONT FACE="Arial, Helvetica">
<%-- 엔진 옵션 선택용 셀렉트 메뉴 ,
ValueChangedListener의 등록--%>
            <h:selectone_menu id="currentEngine" 
valueRef="CurrentOptionServer.currentEngineOption">
              <f:valuechanged_listener type=
"carshop.PackageValueChanged" />
              <h:selectitems valueRef=

"CurrentOptionServer.engineOption"/>
            </h:selectone_menu>  
          </FONT></P>
          </BLOCKQUOTE>
        </TD>
-- 생략 --

사용자 삽입 이미지
옵션 선택 화면(more.jsp)

Customer.jsp는 유저의 정보를 입력하기 위한 페이지로 여기에서는 독자적으로 개발한 converter, validator 및 validator 태그를 사용하고 있다. <cd:format_validator…>태그로 스텝3에서 개발한 FormatValidatorTag를 사용한 태그가 된다. 태그내의formatPatterns에 신용 카드 번호의 올바른 포맷이 지정되어 있다. 여기서 지정한 값이 FormatValidatorTag의 property로 설정되어 한층 더 FormatValidator에 값을 전달한다.

그리고 다음의 <h:output_errors for="ccno"/>이 Validattion가 실패했을 경우에 메세지를 표시하는 부분이 된다.

리스트11 Customer.jsp
-- 생략 --
<td valign="top"> <font face="Arial, Helvetica">
  <%-- custom validator --%>
  <h:input_text id="ccno" size="16" converter="creditcard" >
    <cd:format_validator formatPatterns=
      "9999999999999999|9999 9999 9999 9999|
         9999-9999-9999-9999"/>
  </h:input_text>
  <h:output_errors for="ccno"/></font>
</td>
-- 생략 --

사용자 삽입 이미지
유저 정보를 입력하는 화면(customer.jsp)

마지막으로 thanks.jsp는 구입하고 난후에 메세지를 표시하기 위한 페이지이다.

사용자 삽입 이미지
thanks.jsp에 의한 화면

 

페이지 네비게이션의 정의

마지막으로 JSF 네비게이션·룰 및 그 다른 JSF 어플리케이션의 동작에 필요한 정의를 설정 파일에 추가한다.

앞에서도 설명한 것처럼 faces-config.xml 파일에 네비게이션·룰로 정의한다.

<navigation-rule>
  <from-tree-id>/Storefront.jsp</from-tree-id>
  <navigation-case>
    <from-outcome>success</from-outcome>
    <to-tree-id>/more.jsp</to-tree-id>
  </navigation-case>
</navigation-rule>
<navigation-rule>
  <from-tree-id>/more.jsp</from-tree-id>
  <navigation-case>
    <from-outcome>success</from-outcome>
    <to-tree-id>/buy.jsp</to-tree-id>
  </navigation-case>
</navigation-rule>
<navigation-rule>
  <from-tree-id>/buy.jsp</from-tree-id>
  <navigation-case>
    <from-outcome>success</from-outcome>
    <to-tree-id>/Customer.jsp</to-tree-id>
  </navigation-case>
</navigation-rule>
<navigation-rule>
  <from-tree-id>/Customer.jsp</from-tree-id>
  <navigation-case>
    <from-outcome>success</from-outcome>
    <to-tree-id>/thanks.jsp</to-tree-id>
  </navigation-case>
</navigation-rule>

마지막으로 JSF 어플리케이션을 동작시키기 위해서 필요한 정의를 web.xml에 추가한다. 아래는 <servlet>과 <servlet-mapping>정의는 JSF 어플리케이션을 사용하기 위해서는 필수사항이다.

<description>Car Demo Sample Application</description>
<display-name>Car Demo Sample Application</display-name>

<servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/faces/*</url-pattern>
</servlet-mapping>

그리고, 어플리케이션에서 필요로하는 resource file에 대해서는 여기에서 설명하지만, 사용하는 언어마다의 resource bundle 파일은 별도로 준비해야 한다. 이렇게 해서 어플리케이션을 완성한다.

어플리케이션의 패키징은 앞에서 인스톨한 JWSDP의 샘플로 제공되고 있는 build.xml 등을 참고해서 an로 작성하기 바란다. 그리고, 이미 패키지한 것은이쪽(carShop.war)으로부터 다운로드받을 수 있다.

통계

연재를 통해 JSF의 개요 및 어플리케이션의 구축 방법에 대해 설명해 왔다. JSF는 지금까지 Web 어플리케이션을 구축할 때 JSP 및 JavaBeans로 구현되고 있던 프레젠테이션 논리를 보다 간단하게 구축하기 위한 테크놀러지이다. 게다가 JCP에서 스팩이 진행되고 있으므로, 향후 Web 어플리케이션을 구축할 때 표준 테크놀러지가 될 것으로 기대되고 있어 많은 기업이 주목하고 있다.

썬마이크로시스템, IBM, 오라클 ,BEA, 볼랜드 등 J2EE를 리드하는 각사는 JSF를 툴에 도입함으로써, 더욱 간단하게 유저 인터페이스를 구축하는 솔루션을 준비하고 있는 것 같다.

썬마이크로시스템에서는 금년의 「JavaOne 2003」에 「Project Rave」를 발표했다. Project Rave(이)란 , 「Project」라는 이름이 붙어 있어 조금 혼란을 일으킬 수 있지만, Web 어플리케이션을 보다 간단하게 구축하는 「개발툴」이다. 이 툴에서는 Web 인터페이스 전체를 캠퍼스로 파악해 그 캔버스에 UI component(텍스트 박스나 실렉트 박스 등)을 드래그&드롭(그래피컬하게)으로 붙일 수가 있고, 그 붙인 컴퍼넌트에 대해서 converter나 validator 등의 기능을 부가할 수가 있다. 게다가 본 연재에서 소개한 다양한 source coding도 이 툴을 사용하면 간단하게 구현할 수 있다.