<div class="note"> 이 글은 안영회님의 블로그인 http://blog.empas.com/ahnyounghoe/에서 "Spring 웹 어플리케이션 만들기" 라는 제목으로 올라온 글을 정리한 것입니다. </div>

Equinox 홈페이지: https://equinox.dev.java.net/

Equinox라는 영어단어의 뜻 : 주야를 똑같이 나누는 구분점

시작 페이지 설정#

먼저 Equinox 가 제공하는 템플릿 성격의 어플리케이션을 파악하고 나면 어떤 산출물이 필요한지 알 수 있고, 예제를 통해 배우는 식으로 따라하다보면 Spring 사용에 더욱 익숙해지리라 생각됩니다.

ahnyounghoe_3910014.png

일단, 첫화면부터 추적을 해나가죠. 출발점이라고 할 수 있는 web.xml 파일을 먼저 살펴보죠. web.xml 전문은 저 아래 첨부합니다. 가장 먼저 보게 되는 페이지는 welcome-file-list 엘리먼트에 의해서 결정되죠. 관습을 깨지 않고 index.jsp로 했군요.

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
 

컨텍스트 리스너가 먼저 실행되겠지만 그건 차후에 관여된 것이 나타날 때 보도록 하고 index.jsp를 먼저 살펴보도록 하겠습니다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" 
    version="2.4">
    <display-name>Equinox</display-name>
    <!-- Define the basename for a resource bundle for I18N -->
    <context-param>
        <param-name>javax.servlet.jsp.jstl.fmt.localizationContext</param-name>
        <param-value>messages</param-value>
    </context-param>
    <filter>
        <filter-name>exportFilter</filter-name>
        <filter-class>org.displaytag.filter.ResponseOverrideFilter</filter-class>
    </filter>
    <filter>
        <filter-name>messageFilter</filter-name>
        <filter-class>org.appfuse.web.MessageFilter</filter-class>
    </filter>
    <filter>
        <filter-name>sitemesh</filter-name>
        <filter-class>com.opensymphony.module.sitemesh.filter.PageFilter</filter-class>
    </filter>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext*.xml</param-value>
    </context-param>
    <filter-mapping>
        <filter-name>messageFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>
    <filter-mapping>
        <filter-name>sitemesh</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>
    <filter-mapping>
        <filter-name>exportFilter</filter-name>
        <url-pattern>*.html</url-pattern>
    </filter-mapping>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>action</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>action</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <error-page>
        <error-code>404</error-code>
        <location>/404.jsp</location>
    </error-page>
    <error-page>
        <error-code>500</error-code>
        <location>/error.jsp</location>
    </error-page>
</web-app>

index.jsp#

가. include 지시문

<%include file="/taglibs.jsp"%>

먼저 include 지시문(directive)이 나옵니다. 공통적으로 사용하는 파일을 매번 타이핑 하지 않기 위한 것이죠. 이름으로 보아선 taglib 지시어를 지정하는 코드를 모아둔 것 같네요.

<%page language="java" errorPage="/error.jsp" %>
<%taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>
<%taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt" %>
<%taglib uri="http://displaytag.sf.net" prefix="display" %>
<%taglib uri="http://www.opensymphony.com/sitemesh/decorator" prefix="decorator"%>
<%taglib uri="http://www.springframework.org/tags/commons-validator" prefix="html" %>
<%taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<c:set var="datePattern"><fmt:message key="date.format"/></c:set>

위의 내용이 taglibs.jsp의 내용입니다. 먼저 page 지시문을 통해서 errorPage를 지정했네요. 오류가 발생하면 error.jsp로 보내라는 것이죠. 예외처리문 출력하는 것입니다. 코드를 설명하는건 생략하죠. 다음에는 예상대로 taglib 지시어가 늘어서 있네요. JSTL 중에서 Core와 Format 을 사용하는 것 같네요. 그리고, 목록보기를 위해선 오픈소스인 DisplayTag를 쓰려고 이를 선언했네요. 뒤어어서 Spring에서 제공하는 두 가지 태그와 함께 화면을 분할하기 위해서 Sitemesh의 태그를 쓰네요. 뒤이어서 JSTL Core의 태그를 썼네요. set 태그는 변수에 값을 할당하는 것입니다. var 속성이 변수 이름을 나타내고 태그안의 텍스트가 값이 됩니다. dataPattern 이라는 변수에 data.format 이라는 키값으로 메시지를 꺼내와서 할당하네요. message 태그는 국제화 지원을 위한 태그 중 하나입니다. 다국어 지원이라는 표현이 더 자연스러울 듯 하네요. 다국어 지원을 위해서 localization context 라는 메커니즘을 사용하게 되는데요. 음... 특정 지역과 언어에 맞는 문자나 통화 기호등을 표현하기 위해서 문맥(context)를 지역에 따라 달리하는 방법이죠. 이를 위해서는 web.xml 에서 javax.servlet.jsp.jstl.fmt.localizationContext 라는 이름의 context parameter를 설정해야 하는데 그 값은 String 타입의 문자열이거나javax.servlet.jsp.jstl.fmt.LocalizationContext 객체 타입이 됩니다.

    <!-- Define the basename for a resource bundle for I18N -->
    <context-param>
        <param-name>javax.servlet.jsp.jstl.fmt.localizationContext</param-name>
        <param-value>messages</param-value>
    </context-param>

web.xml 파일에 위와 같은 설정이 있는데, message 라는 값은 ResourceBundle의 기반(base)이 되는 이름이 되며, 결국 WEB-INFclasses 디렉토리의 message_xx_XX.properties 파일에서 메시지를 읽어오게 되는 것이죠.

date.format=MM/dd/yyyy

messages.properties 파일에 위와 같은 내용이 있네요. 나. 제목 및 본문부

<title><fmt:message key="index.title"/></title>

이 내용은 위와 같이 다국어 메시지를 지원하려는 의도라고 볼 수 있죠.

index.title=Equinox ~ Welcome

이렇게 되어 있네요.

ahnyounghoe_3911300.png

그리고 본문은 주로 html 로 되어 있는 설명문들이라서 설명하지 않을 부분은 생략하고, 전문을 저 아래 첨부하기로 하죠.

<acronym title="Create, Retrieve, Update and Delete">CRUD</acronym>

음.. 이런 태그도 있었네요. ^^;

ahnyounghoe_3911301.png

<a href="?" omclick="readMore(); return false">Click here</a>
Click here 라는 문자에는 onclick 이벤트가 걸려있구요. id가 readmore인 div 태그 안의 버튼에도 onclick 이벤트가 걸려있네요.

<div id="readmore" style="display:none">
    <h3>Introduction to Equinox</h3>
...
        <button class="button" omclick="readMore();">&laquo; Back</button>
    </p>
</div>
onclick 이벤트가 발생하면 호출되는 자바 스크립트 함수의 정의는 맨 아래 있네요.

<script type="text/javascript">
function readMore() {
    var main = document.getElementById("main");
    var more = document.getElementById("readmore");
    if (main.style.display == "") {
        main.style.display = "none";
        more.style.display = "";
    else {
        more.style.display = "none";
        main.style.display = "";
    }
}
</script>

엘리먼트(태그죠)의 id 값이 main인 엘리먼트 객체를 얻고, readmore인 엘리먼트 객체를 각각 얻구요. 그 중에서 main인 엘리먼트(id는 div 태그에만 있어서 결국 div 태그를 말하죠)의 style 속성의 display 값이 아무것도 없으면 none으로 설정하고, readmore 라는 id를 같는 div 태그의 style 속성의 display 값을 아무것도 없는 것으로 설정합니다. 그렇지 않은 경우는 반대로 하구요. 이것은 결국 두 개의 div 태그가 번갈아 보여지는 효과가 나죠. 자바 스크립트 강좌는 아니기 때문에 이건 여기까지 하죠. 저도 자바 스크립트는 약해서.. ^^;

<content tag="underground">
이런 태그도 있었네요.. 별다른 효과는 없는 것으로 보이는데..ㅡㅡ; 아무튼 여기까진 전부 HTML/JS 관련된 것이고, 실제로 자바 웹프로그래밍이나 Spring에 관련된 부분은 바로 이거죠.

<button class="button" omclick="location.href='users.html'">View Demonstration</button>

users.html 이라는 GET 요청을 발생하게 되는 부분이죠.

<%include file="/taglibs.jsp"%>
<title><fmt:message key="index.title"/></title>
<div id="main">
    <h3>Welcome to Equinox!</h3>
    <p>
        <b>Equinox</b> is a lightweight version of <a href="http://raibledesigns.com/appfuse">AppFuse</a>.
        I was inspired to create it while writing <a href="http://springlive.com">Spring Live</a> and
        looking at the <em>struts-blank</em> and <em>webapp-minimal</em>
        applications that ship with Struts and Spring, respectively.
        These "starter" apps were not robust enough for me, and I wanted
        something like AppFuse, only simpler.  Much of the documentation for developing
        with Equinox can be found in the 
  <a href="http://www.sourcebeat.com/docs/Spring%20Live/Rev_4/Spring%20Live_SampleChapter.pdf">
        Spring QuickStart Chapter</a> in Spring Live.  If you have issues downloading this
        PDF, you might try saving it to your hard drive before opening it.
    </p>
    <p>
        The basic Equinox application shows how to do simple
        <acronym title="Create, Retrieve, Update and Delete">CRUD</acronym> on a database table. 
        To see this feature, click on the button below. 
  <a href="?" omclick="readMore(); return false">Click here</a>
        to learn more about Equinox.
    </p>
    <p>
        <button class="button" omclick="location.href='users.html'">View Demonstration</button>
    </p>
</div>
<div id="readmore" style="display:none">
    <h3>Introduction to Equinox</h3>
    <p>
        Equinox is designed to show webapp developers how to start
        a bare-bones webapp using a <a href="http://www.springframework.org">
        Spring</a>-managed middle-tier backend and <a href="http://www.hibernate.org">
        Hibernate</a> for persistence. By default, Equinox uses Spring for
        its MVC framework, but you can change it to
        <a href="http://struts.apache.org">Struts</a>,
        <a href="http://opensymphony.com/webwork">WebWork</a>,
        <a href="http://jakarta.apache.org/tapestry">Tapestry</a>
        or <a href="http://www.myfaces.org">JSF</a>.  Installers are in the "extras" directory.
    </p>
    <p>
        An in-memory <a href="http://hsqldb.sf.net">HSQL</a> database is used by default.
        The database and its tables are created on-the-fly when tests (or the application)
        is run.  The one issue of using this method is that records disappear every
        time the app is started.  The nice side effect is that you never write tests
        that depend on existing data.  If you have issues with records not showing up or
        want to use a different database, 
  see <a href="http://jroller.com/page/raible/20040809#alternate_database_configurations_with_myusers">
        these instructions</a>.  Since there is no container
        configuration required, the application should work with any Servlet 2.4
        servlet engine. 
    </p>
    <p>
        <button class="button" omclick="readMore();">&laquo; Back</button>
    </p>
</div>
<content tag="underground">
<h3>Assumptions</h3>
<ul>
    <li>It's 2004, no one uses Netscape anymore, or at least
        no one does by choice. All HTML will be XHTML compliant,
        without a space: i.e. &lt;br/&gt; not &lt;br /&gt;.</li>
    <li>JSP 2.0 is out, so it will be used to simplify syntax.</li>
    <li>Simplicity is more important than configurability.</li>
</ul>
<h3>Notes</h3>
<ul>
    <li>Equinox ships with project files for both <a href="http://www.eclipse.org">Eclipse</a>
    and <a href="http://www.jetbrains.com/idea/">IDEA</a>. For information on setting up 
    Equinox to run tests and debug Tomcat, see
    <a href="http://confluence.sourcebeat.com/display/SPL/FAQ">the FAQ</a>.</li>
    <li><a href="http://opensymphony.com/sitemesh">SiteMesh</a> is used for page decoration. It was
    <a href="http://raibledesigns.com/page/rd?anchor=sitemesh_passed_the_10_minute">
    so easy to use</a>, I couldn't resist!</li>
</ul>
</content>
<script type="text/javascript">
function readMore() {
    var main = document.getElementById("main");
    var more = document.getElementById("readmore");
    if (main.style.display == "") {
        main.style.display = "none";
        more.style.display = "";
    else {
        more.style.display = "none";
        main.style.display = "";
    }
}
</script>

users.html요청처리#

View Demonstration 버튼을 눌러서 users.html 요청을 발생시키면 어떻게 될까요? 먼저 필터 매핑 정의에 따라서 필터(Servlet Filters)를 거쳐야겠네요.

    <filter-mapping>
        <filter-name>messageFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>
    <filter-mapping>
        <filter-name>sitemesh</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>
    <filter-mapping>
        <filter-name>exportFilter</filter-name>
        <url-pattern>*.html</url-pattern>
    </filter-mapping>

messageFilter와 sitemesh 는 모든 URL(/*)이 거쳐야 하고, users.html 요청이니까 exportFilter 도 거쳐야 하는군요. 역시 같은 web.xml 파일에 정의된 필터 선언을 하나씩 볼까요?

    <filter>
        <filter-name>messageFilter</filter-name>
        <filter-class>org.appfuse.web.MessageFilter</filter-class>
    </filter>

메세지필터는 뭐하는 녀석일까요? 주요 메소드인 doFilter() 만 살펴보죠.

    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain)
    throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequestreq;
        // grab messages from the session and put them into request
        // this is so they're not lost in a redirect
        Object message = request.getSession().getAttribute("message")// (1)
        if (message != null) { // (2)
            request.setAttribute("message", message);  // (2)
            request.getSession().removeAttribute("message")// (2)
        }
        // set the requestURL as a request attribute for templates
        // particularly freemarker, which doesn't allow request.getRequestURL()
        request.setAttribute("requestURL", request.getRequestURL());
        chain.doFilter(req, res);
    }

(1) 먼저 요청을 보낸 세션을 읽어서 message라는 이름의 속성을 얻어옵니다. 처음에는 없으니까 null 이 되겠죠. (2) 만약에 뭔가 (남아) 있다면 이를 제거해버립니다. (3) requestURL 이라는 이름으로 해당 요청의 request url 값을 Request 영역(scope)에서 공유될 수 있는 속성을 설정합니다. 이때, URL은 프로토콜(protocol), 서버 이름(server name), 포트 이름(port number)과 서버 경로(server path) 등을 포함하지만 질의 문자열 매개변수(query string parameters)는 제외됩니다. 이 경우 request url은 http://localhost/myapp/users.html 이 되는 것이죠. 물론 제 경우는 디폴트 HTTP 포트가 80인 경우이니까 8080 등인 경우는 http://localhost:8080/myapp/users.html 이 되겠죠. 아무튼 messageFilter를 거치고 난 요청들은 모두 requestURL 이라는 이름으로 요청의 URL을 읽어올 수 있습니다. 쉽게 생각해서 서버측에서 사용자가 주소창에 무엇을 입력했는지를 식별하기 위한 변수라고 볼 수 있죠. 다음 필터는 sitemesh 인데요.

    <filter>
        <filter-name>sitemesh</filter-name>
        <filter-class>com.opensymphony.module.sitemesh.filter.PageFilter</filter-class>
    </filter>

sitemesh는 jar 형태로 참조하기 때문에 소스를 보시려면 다운로드를 해야 합니다. http://opensymphony.com/sitemesh/download.html 아래 sitemesh의 처리 흐름을 도표화 한 그림을 첨부합니다. 출처는 역시 Opensymphony.com 이구요. 좀 길어져서 쉬었다가 PageFilter 소스를 살펴보죠.

ahnyounghoe_3913497.gif

Sitemesh의 PageFilter 클래스의 소스 코드를 볼까요? doFilter 메소드 위주로 보도록 하죠.

    public void doFilter(ServletRequest rq, ServletResponse rs, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequestrq;
        if (rq.getAttribute(FILTER_APPLIED!= null 
    || factory.isPathExcluded(extractRequestPath(request))) { // (1)
            // ensure that filter is only applied once per request
            chain.doFilter(rq, rs);
        }
        else {
            request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
            // force creation of the session now because Tomcat 4 had problems with
            // creating sessions after the response had been committed
            if (Container.get() == Container.TOMCAT) { // (2)
                request.getSession(true);
            }
            HttpServletResponse response = (HttpServletResponsers;
            // parse data into Page object (or continue as normal if Page not parseable)
            Page page = parsePage(request, response, chain);
            if (page != null) {
                page.setRequest(request);
                Decorator decorator = factory.getDecoratorMapper().getDecorator(request, page);
                if (decorator != null && decorator.getPage() != null) {
                    applyDecorator(page, decorator, request, response);
                    page = null;
                    return;
                }
                // if we got here, an exception occured or the decorator was null,
                // what we don't want is an exception printed to the user, so
                // we write the original page
                writeOriginal(request, response, page);
                page = null;
            }
        }
    }
FILTER_APPLIED 라는 상수는 RequestConstants 인터페이스에 정의된 것이고, PageFilter 는 이를 구현학 있습니다. (1) 구문은 결국 이 필터가 한번만 수행되도록 하는 부분인 것 같네요. Container는 서블릿 컨테이너를 표현하기 위한 클래스인데, 여기 설정된 상수들이 SiteMesh가 지원하는 것들이라고 생각됩니다. 주요 컨테이너는 모두 지원하네요.

    public static final int TOMCAT    = 1;
    public static final int RESIN     = 2;
    public static final int ORION     = 3// Orion or OC4J
    public static final int WEBLOGIC  = 4;
    public static final int HPAS      = 5;
    public static final int JRUN      = 6;
    public static final int WEBSPHERE = 7;

(2) 구문에서는 톰캣에서 수행되는 경우만 새로운 세션을 생성합니다. 하늘색으로 칠한 부분이 SiteMesh가 Decorator 패턴을 활용하여 페이지를 구성하는 것으로 보입니다. 자세한 처리 결과까지 살펴보는 것은 뒤로 미뤄야 할 것 같습니다. Spring을 이해하는 것인데 SiteMesh를 따라 가다가 길을 잃을 우려가 있을 듯 해서..ㅡㅡ; PageFilter 에서는 SiteMesh를 이용하여 페이지를 구성한다 정도로 정리해두죠. 마지막으로 exportFilter 가 남았네요. 보아하니 이 녀석도 DisplayTag라는 라이브러리(3-rd party library)를 이용하는 것이군요. 이번엔 API 정도만 살펴보죠.

    <filter>
        <filter-name>exportFilter</filter-name>
        <filter-class>org.displaytag.filter.ResponseOverrideFilter</filter-class>
    </filter>

이는 DisplayTag 라이브러리의 TableTag를 사용하면서 Export 기능을 구현하고자 할 때 쓰는 것이라고 하네요. DisplayTag 라이브러리 API 문서: http://displaytag.sourceforge.net/apidocs/ 필터 설정을 정리하면 messageFilter 가 요청 URL을 기억해서 공유하게 했구요. sitemesh라는 이름으로 설정한 com.opensymphony.module.sitemesh.filter.PageFilter와 exportFilter 라는 이름으로 설정한 org.displaytag.filter.ResponseOverrideFilter가 각각 SiteMesh를 이용한 페이지 구성 준비와 TableTag의 Export 기능을 준비하게 하는 것이었습니다.

Equinox기반 프로젝트#

ahnyounghoe_3994534.png

어찌 되었건 눈에 보이는 것은 index.jsp 에 기록된 HTML 들인데 첫 화면을 바꾸려면 이것을 바꿔야겠죠. 근데 Ctrl+F 로 찾아봐도 좌측이나 아래쪽의 링크들은 index.jsp에서 볼 수가 없습니다. 아무래도 템플릿으로 화면이 꾸며진 듯 하네요. 이미 SiteMesh를 썼다는 사실은 앞서두 언급했고 뒤져보니까 decorators 디렉토리에 default.jsp 파일이 있는데 이 녀석이 범인 같군요. default.jsp 파일을 body 부분만 div로 분리된 영역을 정리해보면 다음과 같습니다.

container
   - intro
      - pageHeader
      - quickSummary
      - content
   - supportingText
      - underground
      - footer
   - linkList
      - linkList2
         - lresources

논리적으로 크게 삼등분 하고 있고, 이 녀석이 index.jsp 보다 먼저 로딩되는 것만은 확실한 것 같습니다. 다시 이녀석을 분석하고 싶은 마음이 또 생기지만 이를 억누르고 제가 만든(혹은 여러분이 만든) index.jsp가 보여지도록 할 수 있도록 궁리를 해보죠. 직관적으로 두 가지 정도를 먼저 알면 될 것 같습니다. default.jsp가 어떤 설정을 통해서 불리게 되느냐? 그걸 알아야 다른 템플릿을 만들 수 있으니까요. 그리고 다른 하나는 동적으로 다른 jsp를 삽입시키는 방법 이건 아마도 decorator라는 태그가 알고 있을 듯 합니다. default.jsp 에서 decorator 태그가 등장하는 부분만 추려내면 다음과 같습니다.

        <div id="content">
            <%include file="/messages.jsp"%>
            <decorator:body/>
        </div>
    </div>
    <div id="supportingText">
        <div id="underground"><decorator:getProperty property="page.underground"/></div>

먼저 주요 설정 저장소인 WEB-INF 디렉토리를 보면, decorators.xml 파일이 있습니다.

<decorators defaultdir="/decorators">
    <decorator name="default" page="default.jsp">
        <pattern>/*</pattern>
    </decorator>
</decorators>

sitemesh를 잘 모르지만 자바 커뮤니티의 코딩 관습(coding convention)에 따라 해석하면 모든 요청에 대해서 /decorators 디렉토리에 있는 default.jsp를 적용하란 것이라고 생각됩니다. default 라는 이름도 지정했는데 이건 어디서 쓰이는지 모르겠네요. 서블릿처럼 이 녀석도 패턴을 밖으로 정의할 수 있나 봅니다. 이제 풀리지 않은 의문은 decorator:body 와 decorator:getProperty 입니다. 이 의문을 풀 수 있는 단서는 web.xml 의 다음 설정 같은데요.

    <filter>
        <filter-name>sitemesh</filter-name>
        <filter-class>com.opensymphony.module.sitemesh.filter.PageFilter</filter-class>
    </filter>

SiteMesh API를 봐야겠군요. Ultra-API에 없네요. 갱신해야지. 갱신한 것은 북마크에서 Ultra-API 를 클릭하시면 됩니다. API 문서에서 com.opensymphony.module.sitemesh.filter.PageFilter 를 찾아보죠. 설명은 간단하게 아래와 같이 나와 있네요. Main SiteMesh filter for applying Decorators to entire Pages 데코레이터(장식을 해주는 객체죠)를 전체 페이지에 적용하는 SiteMesh의 주요 필터라. 핵심이 되는 녀석이군요. 근데 이녀석만 봐서는 decorator:body 와 decorator:getProperty 를 알기는 어렵네요. 메소드나 필드로 이런 것들이 노출되어 있지는 않네요.

역시 모르니까 헤매고 다녔는데 태그 라이브러리에 있었군요. SiteMesh Tag Reference 매뉴얼을 찾았습니다. 간과했던 taglibs.jsp 가 열쇠였는데...

<%taglib uri="http://www.opensymphony.com/sitemesh/decorator" prefix="decorator"%>

SiteMesh에 기본적으로 포함된 태그 라이브러리는 두 개의 태그를 지원합니다.

Decorator Tags
Used to create decorator pages.
<decorator:head />
<decorator:body />
<decorator:title />
<decorator:getProperty />
<decorator:usePage />

Page Tags
Used to access decorators from content pages.
<page:applyDecorator />
<page:param />
우리가 찾던 의문은 바로 decorator:body 와 decorator:getProperty 입니다. 그 녀석 들 위주로 매뉴얼을 <decorator:body /> Description:

<div class="note" style="margin:10px; background-color:white"> Insert contents of original page's HTML <body> tag. The enclosing tag will not be be written, but its contents will.

Note: the content of the body onload and onunload events (and other body attributes) can be included in the decorator by getting the property body.onload and body.onunload (the named attributes). For example (the decorator): <body omload="<decorator:getProperty name="body.onload" />"> </div>

원래 페이지의 HTML body 태그를 집어넣는다네요. 으흠.. 그럼 default.jsp에서 의 decorator:body 위치에 index.jsp의 <body>가 들어가는군요. 태그는 빠지고 내용만 들어간다고 합니다. 그래서 default.jsp에 보면 <body> 태그가 있네요. body에 이벤트를 걸었거나 특정 속성이 있는 경우 이들을 포함시키기 위해서는 getProperty 태그가 필요한 모양입니다. 음... index.jsp에는 아예 body 태그가 없군요. title 등의 별도의 태그로 감싸지 않는 녀석들은 모두 body의 컨텐츠로 취급하는 것인지 의문이 생기네요. 이러한 의문은 차후에 풀도록 해야겠습니다. 생산성을 위해서... 아무튼 decorator:body 태그를 풀면서 더불어 decorator:getProperty 도 해결하게 되었네요. default.jsp의 해당 코드를 다시 보면

<div id="underground"><decorator:getProperty property="page.underground"/></div>

page.underground 라는 속성 값을 요구했군요. page 태그는 없는데, 아마 index.jsp의 아래 태그를 가리키는 것으로 짐작됩니다.

<content tag="underground">

page는 아마도 SiteMesh가 HTMLPage 객체를 표현하는 내부 객체명인 듯 합니다. 이 부분도 다소 미진하게 이해하게 되네요. ㅡㅡ; default.jsp의 head 부분을 보니까 decorator 태그가 두 개가 더 있네요.

<decorator:title default="Equinox"/>
<decorator:head/>

이 부분은 굳이 설명을 듣지 않아도, 유추해서 의미를 이해할 수 있을 것 같습니다.

음.. default.jsp를 대체할만한 녀석을 하나 만들어야겠습니다. 첫 페이지 용도롤 쓰기 위해서 front.jsp 라고 작명하겠습니다. 그리고 미리 만들어놓은 첫 페이지 내용을 복사하도록 하죠. (혹시 따라해보기 위해서 jsp 파일이 필요하시면 드리겠습니다. 마땅히 올려둘 곳이 없으니 의견달아주시면 메일로 메일이나 메신저 등으로 보내드리죠.) 1. default.jsp에서처럼 태그 라이브러리를 그대로 쓰기 위해서 다음 코드를 추가합니다.

<%include file="/taglibs.jsp"%>

2. 페이지 제목을 아래와 같이 달았었는데 이것도 가변적으로 바꾸겠습니다.

<title>DeveloperInside since 2004, 자바 프로그래머를 넘어서</title>

이 부분을 다음과 같이

<title><decorator:title default="DeveloperInside since 2004, 자바 프로그래머를 넘어서"/></title>

그러면, 틀이 되는 front.jsp만 남고 원래 의도했던 첫 페이지는 없어지니까 일종의 인스턴스격이 첫 페이지를 index.jsp로 만들겠습니다. 기존의 index.jsp는 지워버리기 뭐하니까 index.jsp.bak 으로 파일명을 바꾸겠습니다. 새로 만든 index.jsp에는 다음 내용을 넣겠습니다.

<title><fmt:message key="index.title"/></title>

그리고, index.title 값으로 쓸 문자열도 지정해주죠. WEB-INF/classes/messages.properties 파일을 열면 다음과 같은 부분이 있는데

index.title=Equinox ~ Welcome
이걸 원하는 제목으로 바꾸면 되겠죠. 3. 다음은 link 태그에서 css 위치를 지정할 때 decorators 디렉토리를 중심으로 하면 복잡하니까 웹 어플리케이션 루트(context)를 읽어오기 위해서 JSTL 구문을 넣습니다.

<c:set var="ctx" value="${pageContext.request.contextPath}" scope="request"/>

그리고 아래와 같이 되어 있던 부분을 변수를 EL 변수를 쓰도록 바꿉니다.

<link href="styles/difront.css" rel="stylesheet" type="text/css" />
<link href="styles/dinavigation.css" rel="stylesheet" type="text/css" />

그러면 이렇게 되겠죠.

<link href="${ctx}/styles/difront.css" rel="stylesheet" type="text/css" />
<link href="${ctx}/styles/dinavigation.css" rel="stylesheet" type="text/css" />

이젠 css 파일들을 옮겨 놓아야겠죠. 물론, 미리 만들어둔 것이 있어야죠.

일단 돌려서 눈으로 확인해보고 싶군요. Equinox의 ANT 타겟(<target>s)들의 의미를 정확히 알려하지 않고 이것 저것 시도하다보니 시행착오가 많았습니다. 결국 지금 myapp 로 뭔가 돌아가고 있는 상황에서는 remove -> clean -> compile -> war -> deploywar -> start 위와 같은 순서로 ANT를 돌려야 하더군요. 너무 번거로워서 Run 이라는 이름으로 타겟을 하나 만들어서 이들이 순서대로 돌아가게 했습니다.

 <!-- Run in Tomcat After modification (author: MiKE-->
 <target name="Run" depends="remove, clean, deploywar
        , start">
 </target>

그런데, 이 타겟은 논리적인 결함이 있는 것인지 문제가 좀 있었습니다. 무엇보다 톰캣이 romove 하고 난 컨텍스트를 동적으로 인지하지 못한다는 것이 문제였죠. 결국 톰캣을 내렸다가 올리거나 jsp 만 변경한 경우는 jsp 파일만 복사하거나 해야 합니다. 음... 톰캣을 내렸다 올리는 일을 ANT에 추가해야 할 것 같기도 한데 당장은 돌리는 방법부터 설명드리죠. 1. 일단, 톰캣을 실행중이어야 합니다. 그리고, 예전의 myapp가 실행중인 상태라고 가정하겠습니다. 이를 확인하기 위해서 list 타겟을 실행합니다. myapp가 있으면 remove 타겟을 실행시키세요. 없으면 먼저 start 타겟을 실행하고 다시 list 타겟을 실행해봅니다. start가 안되면 좋고, 되면 다시 remove 타겟을 실행시키고, list를 실행해서 지워진걸 확인합니다. 주의하실 점은 start 되지 않은 상태에서는 remove가 되지 않는다는 점입니다. 그리고 여러 차례 테스트해보니까 remove는 정상적으로 동작하지 않는다고 짐작되네요. 어떤 경우는 remove 후에 톰캣을 내렸다가 올려도 남는 경우도 있네요. 결국 가장 완전한 방법은 CATALINA_HOME 아래의 webapps 디렉토리에서 제거하는 것이죠. 이젠 Run을 약간 바꾸죠. 지우는 것은 따로 하고, 여기서 부터 Run을 하는 것으로 합니다.

 <!-- Run in Tomcat After modification (author: MiKE-->
 <target name="Run" depends="clean, war, install, start" />

이젠 잘 뜨는군요. 문제는 제가 인코딩을 UTF-8로 해서인지 한글이 전부 깨져서 나오네요. 단박에 고쳐질 것 같지는 않은데 밥을 먹고 해야겠네요.

이전 글 에서 예상한 것처럼 인코딩 문제의 해결은 쉽지가 않았다. SiteMesh의 기본 인코딩은 iso-8859-1 다. 다행이 SiteMesh 사이트에는 인코딩을 해결하는 방법을 설명하고 있다. http://www.opensymphony.com/sitemesh/charsets.html 하지만, 알려준대로 해도 해결되지 않았다. 혹시나 해서 SiteMesh 적용을 피해 같은 코드를 테스트하자 브라우저에서 인코딩일 잘 표현되었다. IE와 파이어폭스 모두 그럼... 원인은 SiteMesh 라고 짐작할 수 있다. 일단, 공식 문서에서의 내용이 도움이 되지 못하니까 코드를 뜯어봐야할지도 모른다. ㅡㅡ; 일단, 결과를 확인해보는 기초적인 기법을 통해서 알아낸 것은 sitemesh.xml 에서의 설정에 따라 브라우저가 인코딩을 설정한다는 점이다.

    <page-parsers>
        <parser default="true"
            class="com.opensymphony.module.sitemesh.parser.FastPageParser"/>
        <parser content-type="text/html"
            class="com.opensymphony.module.sitemesh.parser.FastPageParser"/>
        <parser content-type="text/html;charset=ISO-8859-1"
            class="com.opensymphony.module.sitemesh.parser.FastPageParser"/>
    </page-parsers>

즉 위와 같이 되어 있는 경우, 브라우저가 ISO-8859-1 로 페이지를 인식한 반면

    <page-parsers>
        <parser default="true"
            class="com.opensymphony.module.sitemesh.parser.FastPageParser"/>
        <parser content-type="text/html"
            class="com.opensymphony.module.sitemesh.parser.FastPageParser"/>
        <parser content-type="text/html;charset=UTF-8"
            class="com.opensymphony.module.sitemesh.parser.FastPageParser"/>
    </page-parsers>

이렇게 변경하자 UTF-8 로 인식했다. 문제는 그래도 한글이 제대로 출력되지 않는다는 점이다. 브라우저 인식 문제가 아니라, 한글 자체가 잘못 출력되어진다고 할 수 있겠다.

    <page-parsers>
        <parser default="true"
            class="com.opensymphony.module.sitemesh.parser.FastPageParser"/>
        <parser content-type="text/html;charset=UTF-8"
            class="com.opensymphony.module.sitemesh.parser.FastPageParser"/>
    </page-parsers>

이렇게 chsrset이 없는 것에 대한 설정이 지워버리자 IE는 변화가 없고 파이어폭스는 SiteMesh 태그 파싱 자체가 안되었는데 원인은 알 수 없다. ㅡㅡ; 흠... 아무래도 소스 코드를 바꾸는 래퍼를 만드는건 조잡하고 필터.. 그래.. 인코딩 필터를 하나 더 설치해봐야겠다.

Add new attachment

Only authorized users are allowed to upload new attachments.

List of attachments

Kind Attachment Name Size Version Date Modified Author Change note
png
ahnyounghoe_3910014.png 230.8 kB 1 11-Jan-2006 13:33 이동국
png
ahnyounghoe_3911300.png 17.3 kB 1 11-Jan-2006 13:54 이동국
png
ahnyounghoe_3911301.png 20.6 kB 1 11-Jan-2006 13:54 이동국
gif
ahnyounghoe_3913497.gif 38.8 kB 1 11-Jan-2006 13:54 이동국
png
ahnyounghoe_3994534.png 356.7 kB 1 11-Jan-2006 13:54 이동국
« This page (revision-4) was last changed on 06-Apr-2006 09:45 by 이동국  
G’day (anonymous guest) My Prefs

Referenced by
Equinox

JSPWiki v2.8.4