Wicket 이란 무엇인가? 아래는 Wicket 사이트에서 인용한 설명이다.

<div class="note"> Wicket 은 단순함(Simplicity)과 계층화(Separation of Concerns)되어 있고 완전히 새로운 수준으로 개발을 용이하게 해주는 자바 웹 어플리케이션 프레임워크이다. Wicket 페이지는 일반적인 WYSIWYG HTML 툴을 사용하여 작성할 수 있다. 동적인 컨텐츠 처리와 폼 처리는 여러분이 좋아하는 기술을 사용하여 쉽게 영속화될 수 있는 POJO 데이터 빈에 의한 컴포넌트 모델을 사용하여 모두 자바 코드 내에서 이루어진다. </div>

Wicket 은 컴포넌트(웹을 위한 Swing의 JComponent를 생각하라)를 사용하여 프론트엔드의 비쥬얼 디자인(HTML)을 백엔드 어플리케이션 로직(Java)으로부터 분리한다. 또 다른 MVC 프레임워크가 되는 대신에 Wicket은 전통적인 GUI와 같은 이벤트 기반의 프레임워크에 더 가깝다. 각 '컴포넌트'를 HTML 코드로 이루어진 'view' 파트를 갖고 있는 작은 MVC로 볼 수 있다. 잠시 후에 알게되겠지만, JSP처럼 페이지에 로직이 섞이는 것이 아니라 모든 어플리케이션 로직은 자바 클래스 안에 들어간다(진정한 연관의 분리). 자바코드는 대부분의 어떤 HTML 태그에도 사용될 수 있는 특별한 wicket:id 속성을 사용하여 HTML 페이지에 합쳐진다. wicket:id 는 Wicket 에게 그 태그에 컴포넌트를 렌더링할 것을 알려준다. Wicket은 컴포넌트에 id를 설정함으로써 웹페이지에 유니크하게 정의되는 Label, Link, List 등과 같은 컴포넌트와 모델에 의해 표현되는 컨텐츠를 가지고 있다.

자 이제 우리의 예제에 대해 얘기할 시간이다. 우리는 3페이지를 표시하는 어플리케이션을 만들려고 한다. 하나는 블로그 목록을 보여주는 Home Page, 또 하나는 각 블로그의 상세 내용을 보여주는 Blog Detail Page, 그리고 유저가 유효하지 않은 페이지(유효하지 않은 블로그 엔트리)에 접근하려고 할 때 보여질 특별한 Page Not Found 페이지이다.

Wicket 을 사용하기 위해서는 몇가지 요구사항이 있다. 주요한 요구사항의 한가지는 여러분의 웹 어플리케이션은 WebApplication 클래스를 확장한 클래스를 가져야 한다는 것이다. 이것은 Wicket을 다른 프레임워크와 다르게 하는 시작점이다. 내가 본 대부분의 다른 웹 프레임워크는 "XML에 매여있다." 페이지와 상호작용을 정의하기 위해 여러분은 하나 또는 그 이상의 XML 파일을 작성해야한다. Struts에서 여러분은 최소한 struts-config.xml 파일을 작성해야 하고, 만약 Tiles를 사용한다면 또 다른 XML 설정 파일을 필요로 한다. JSF는 faces-config.xml을 작성해야 한다. XML을 사용하는 것이 잘못된 것은 아니지만, 이러한 접근방법은 여러분의 로직을 여러 곳으로 흐트러놓고, 이것은 작업을 하기에 좀 더 곤란하게 만든다. 여러분이 Wicket을 사용한다면 정말로 수정해야할 필요가 있는 XML 파일은 오직 web.xml 하나 밖에 없다. 그리고 이것은 Wicket만의 요구사항이 아니라 서블릿 스펙의 요구사항이다.

그래서, 우리의 예제 어플리케이션을 다룰 서블릿을 정의하기 위해 web.xml 에 다음을 추가한다.

    <servlet>
        <servlet-name>blog</servlet-name>
        <servlet-class>wicket.protocol.http.WicketServlet</servlet-class>
        <init-param>
            <param-name>applicationClassName</param-name>
            <param-value>org.javageek.wicket.BlogApplication</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>blog</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

보는 바와 같이 WicketServlet 서블릿을 정의하고, 초기화 파라미터는 WebApplication 을 확장하는 클래스의 완전한 classpath이다. 그것은 우리의 메인 어플리케이션 클래스인 BlogApplication 이다. 아래는 BlogApplication 클래스의 코드이다.

package org.javageek.wicket;

import wicket.Application;
import wicket.protocol.http.WebApplication;
import wicket.settings.IMarkupSettings;
import domain.Blog;
import domain.BlogService;

public class BlogApplication extends WebApplication {

    private BlogService blogService = new BlogService();

    public BlogApplication() {}
 

    public static final BlogApplication instance() {
        return (BlogApplicationApplication.get();
    }

    public Blog getBlog() {
        return blogService.getBlog();
    }

    public Class getHomePage() {
        return Index.class;
    }

    protected void init() {
        IMarkupSettings markupSettings = getMarkupSettings();
        configure(DEVELOPMENT);
        markupSettings.setStripWicketTags(true);
        markupSettings.setDefaultMarkupEncoding("UTF-8");
        markupSettings.setStripXmlDeclarationFromOutput(false);

        mountBookmarkablePage("/index", Index.class);
        mountBookmarkablePage("/viewBlogEntry", ViewBlogEntry.class);
    }
}

코 드에서 보는 바와 같이 웹 어플리케이션 설정은 자바 코드의 init() 메소드 안에서 설정된다. 내가 사용한 이러한 설정의 대부분은 개발을 위한 것이어서 자세한 의미는 설명하지 않고 넘어가겠다. Wicket 의 Javadoc API를 참고하면 더 자세히 알 수 있다. 우리가 관심을 가져야할 주요한 코드는 굵은 글씨체로 나온 부분이다. 이것은 우리가 홈페이지(우리의 어플리케이션의 메인 페이지)로서 응답할 컴포넌트를 설정하는 부분이고, 우리의 홈페이지인 Index 클래스를 리턴하고 있다.

Wicket 페이지 컴포넌트는 WebPage 클래스를 확장해야한다. 따라서 우리의 모든 페이지는 WebPage 의 서브클래스가 될 것이다. 그러나, 모든 페이지가 일관되게 보이도록 하기 위해 WebPage 를 확장하는 추상 베이스 클래스를 정의하고 어플리케이션의 모든 페이지는 이 클래스를 확장하도록 할 것이다. BasePage 는 페이지에 공통된 컴포넌트들을 추가하는 매우 기본적인 클래스이다.

package org.javageek.wicket;

import domain.Blog;
import wicket.markup.html.WebPage;
import wicket.markup.html.basic.Label;

public abstract class BasePage extends WebPage {

    public BasePage() {
        Blog blog = getBlog();
        add(new Label("blogName", blog.getName()));
        add(new Label("blogDescription", blog.getDescription()));
    }

    public Blog getBlog() {
        return BlogApplication.instance().getBlog();
    }
}

여 기서부터 재미있는 부분이 시작된다. 보는 바와 같이, 난 단지 각각 id 스트링과 content를 가진 두개의 Label 컴포넌트를 추가했다. Wicket은 모든 컴포넌트에 대해 모델을 정의하지만 또한 Label을 위해 내가 사용한 것처럼 편리한 메소드를 제공한다. Wicket은 내가 파라미터로 보낸 스트링을 모델 객체로 감쌀 것이다. 따라서 난 Label과 같이 매우 간단한 정적인 컴포넌트의 경우에 대해 걱정할 필요가 없다. 페이지 클래스가 인스턴스화 된 후에 Label 컴포넌트는 내가 각각의 id를 어디에 사용하건 간에 렌더링하는 동안 HTML 내에서 사용가능하게 될 것이다.

BasePage HTML은 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.sourceforge.net/"
      xml:lang="utf-8"
      lang="utf-8">
  <wicket:head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title wicket:id="pageTitle">[Blog Entry Title]</title>
    <link rel="stylesheet" href="screen.css" type="text/css" />
  </wicket:head>
  <body>
    <div id="container">
      <h1><span wicket:id="blogName">[Blog Name]</span></h1>
      <h2><span wicket:id="blogDescription">[Blog Description]</span></h2>
      <wicket:child/>
    </div>
  </body>
</html>

난 BasePage 클래스를 추상 클래스로 만들었다. 따라서 여러분은 BasePage 를 실제 페이지로 사용할 수 없고 대신 서브클래스를 만들어서 사용해야 한다. body 부분을 좀 더 자세히 보면, <wicket:child/> 태그가 있는데 이것은 BasePage 서브클래스의 HTML 에 의해 대체될 것이다. 우리는 BasePage 를 서브클래스할 것이기 때문에, 이 HTML 은 페이지 템플릿으로서의 역할을 수행한다.

Wicket 은 HTML 을 파싱하고 wicket:id 속성을 가진 어떤 태그라도 컴포넌트의 id에 의거하여 컴포넌트의 모델 컨텐츠로 대체하는 작업을 한다.

우리의 메인 페이지 클래스는 Index 는 BasePage 를 서브클래스싱하고 3개의 가장 최근의 블로그 엔트리를 날짜의 역순으로 보여주는 로직을 처리한다. 각각의 블로그 엔트리에 대해 우리는 제목과 발췌("Read more" 링크와 함께) 또는 전체 본문 그리고 블로그가 게시된 날짜를 보여줄 필요가 있다.

Index 페이지 클래스의 코드는 :

package org.javageek.wicket;

import java.text.DateFormat;
import java.util.List;
import wicket.PageParameters;
import wicket.markup.html.basic.Label;
import wicket.markup.html.link.BookmarkablePageLink;
import wicket.markup.html.list.ListItem;
import wicket.markup.html.list.ListView;
import wicket.model.IModel;
import wicket.model.LoadableDetachableModel;
import wicket.util.string.Strings;
import domain.BlogEntry;

public class Index extends BasePage {

    public Index() {
        IModel entriesModel = new LoadableDetachableModel() {
            @Override
            protected Object load() {
                return getBlog().getBlogEntries();
            }
        };

        add(new ListView("blogEntries", entriesModel) {
            @Override
            protected IModel getListItemModel(IModel model, int index) {
                List entries = (Listmodel.getObject(this);
                BlogEntry current = (BlogEntryentries.get(index);
                return new BlogEntryModel(current);
            }

            @Override
            protected void populateItem(ListItem item) {
                final BlogEntry entry = (BlogEntryitem.getModelObject();
                final String excerpt = entry.getExcerpt();
                final boolean hasExcerpt = !Strings.isEmpty(excerpt);
                item.add(new Label("entryTitle", entry.getTitle()));
                PageParameters params = new PageParameters();
                params.put("id", entry.getId());
                item.add(new BookmarkablePageLink("viewBlogEntry",
                        ViewBlogEntry.class, params) {
                    @Override
                    public boolean isVisible() {
                        return hasExcerpt;
                    }
                });
                if (hasExcerpt) {
                    item.add(new Label("entryBody", entry.getExcerpt())
                            .setEscapeModelStrings(false));
                else {
                    item.add(new Label("entryBody", entry.getBody())
                            .setEscapeModelStrings(false));
                }
                DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG,
                        DateFormat.LONG, getBlog().getLocale());
                item.add(new Label("entryDate", df.format(entry.getDate())));
            }
        });
        add(new Label("pageTitle", getBlog().getName()));
    }

}

그리고 Index 페이지를 위한 전체 HTML 은 다음과 같다.

<wicket:extend>
    <div class="blogEntry" wicket:id="blogEntries">
    <h3 wicket:id="entryTitle">[Blog Entry Title]</h3>
    <div wicket:id="entryBody">[Blog Entry Body]</div>
    <p><a href="#" wicket:id="viewBlogEntry">Read more</a></p>
    <p>Posted on <span wicket:id="entryDate">[Blog Entry Date]</span></p>
    </div>
</wicket:extend>

보 는 바와 같이, 나는 주로 자바 코드를 이용하고 Wicket 에게 컴포넌트를 어디서 어떻게 렌더링하는지 말하는 매우 적은 양의 HTML 코드만으로 웹페이지를 생성했다. 또한 wicket:id 속성은 거의 대부분의 어떤 HTML 태그에도 적용될 수 있다. 이제 나는 Wicket 의 모델이 어떻게 작용하는지에 대해 더 이상 자세히 다루지는 않을 것이다. 그러나 Wicket 은 IModel 과 같은 몇 가지 인터페이스를 이용해서 도메인 객체로부터 Wicket 모델 객체로 모델의 분리를 제공한다고 말할 수 있다.

한 가지 지적하고 싶은 흥미로운 부분은 BookmarkablePageLink(볼드체로 나온 부분)의 isVisible 메소드를 오버라이딩함으로써 "Read more" 링크가 조건적으로 표시된다는 것이다. 그리고 이것이 사람들이 보통 다른 프레임워크에서 익숙한 것 처럼 HTML(또는 jsp) 안에서가 아니라 자바 코드에서 처리된다는 것이다. 지금까지 우리의 메인 페이지를 다루었고, 이제 다른 페이지가 어떻게 이루어지는지 살펴보자.

Blog detail Page#

이 페이지는 한가지만 제외하면 실제로 매우 간단하다. 아래에 코드가 있다.

package org.javageek.wicket;

import java.text.DateFormat;
import wicket.PageParameters;
import wicket.RestartResponseException;
import wicket.markup.html.basic.Label;
import domain.Blog;
import domain.BlogEntry;

public class ViewBlogEntry extends BasePage {

    public ViewBlogEntry(PageParameters parametersthrows Exception {
        super();
        Blog blog = getBlog();
        BlogEntry entry = blog.getBlogEntry(parameters.getLong("id"));
        if (null == entry) {
            throw new RestartResponseException(PageNotFound.class);
        else {
            add(new Label("entryTitle", entry.getTitle()));
            add(new Label("entryBody", entry.getBody())
                    .setEscapeModelStrings(false));
            add(new Label("pageTitle", entry.getTitle() " : "
                    + blog.getName()));
            DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG,
                    DateFormat.LONG, getBlog().getLocale());
            add(new Label("entryDate", df.format(entry.getDate())));
        }
    }

}

그리고 아래는 HTML 코드이다.

<wicket:extend>
    <div class="blogEntry">
    <h3 wicket:id="entryTitle">[Blog Entry Title]</h3>
    <div wicket:id="entryBody">[Blog Entry Body]</div>
    <p>Posted on <span wicket:id="entryDate">[Blog Entry Date]</span></p>
    </div>
</wicket:extend>

이 페이지에서 한 가지 알아두어야 할 점은 GET 또는 POST 요청을 편리하게 Map 객체로 감싼 페이지 파라미터를 인자로 제공하는 WebPage 의 생성자를 오버라이딩 하고 있다는 것이다. 또한 이러한 파라미터들 중 어떠한 것(우리의 경우에는 유효하지 않은 엔트리 id)이라도 null 엔트리를 생성한다면, PageNotFound 페이지 클래스로 리다이렉트 시킨다.

아래는 PageNotFound 페이지 클래스를 위한 코드이고, 기본적으로 이 페이지는 응답 상태 코드를 404로 설정한다.

package org.javageek.wicket;

import wicket.markup.html.basic.Label;
import wicket.protocol.http.WebResponse;

public class PageNotFound extends BasePage {

    public PageNotFound() {
        add(new Label("pageTitle", BlogApplication.instance().getBlog()
                .getName()));
    }

    @Override
    protected void configureResponse() {
        WebResponse wr = getWebRequestCycle().getWebResponse();
        wr.getHttpServletResponse().setStatus(404);
    }

}

이것으로 완성이다. 우리는 방금 Wicket 을 사용하여 웹 어플리케이션을 만들었다.

요약#

Wicket 은 나의 견해로는 개발의 노력을 자바 코드안에서 이루어지게 함으로써 올바른 곳으로 집중시킨다. 그리고 디자인 부분은 그것이 있어야하는 HTML 로 남겨둔다. 너무나 많은 개발자들이 다른 프레임워크를 이용하여 JSTL 과 jsp 의 스크립틀릿에 의존하여 페이지의 로직을 프로그래밍하도록 '강제되어' 왔기 때문에 처음에 여러분은 이러한 패러다임의 변화에 적응하기가 조금 어려울 수도 있다. 하지만 여러분이 일단 이것에 익숙해지면, Wicket 은 정말로 신속한 개발을 제공해줄 것이라고 확신한다.

Add new attachment

Only authorized users are allowed to upload new attachments.
« This page (revision-27) was last changed on 13-Dec-2006 23:15 by 59.187.230.143