저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

'템플릿'에 해당되는 글 2건

  1. 2002.08.19 Struts의 템플릿 기능과 JSP만을 이용한 MVC 패턴 구현
  2. 2001.04.02 스트링 템플릿
Struts의 템플릿에 대해서 살펴보고, JSP만을 이용하여 모델 2 구조를 구현해본다.

Struts의 템플릿 기능

Struts는 Jakarta 프로젝트 중의 하나로서 모델-뷰-컨틀롤러(MVC) 패턴에 기반하여 웹 어플리케이션을 개발할 수 있는 프레임워크를 제공해주는 API 이다. 현재 1.0.2 버전이 안정화된 버전이며, http://jakarta.apache.org/struts/index.html 에서 Struts에 대한 다양한 구할 수 있다. 앞서 말했듯이 Struts는 MVC 패턴, 즉 모델 2 구조를 구현하기에 알맞은 프레임워크를 제공하고 있다. 예를 들어, Struts는 기능과 모델 사이의 연결이나 모델과 뷰 사이의 연결 등을 하나의 컨트롤러를(그리고 하나의 설정파일을) 통해서 중앙집중식으로 처리할 수 있다는 장점을 제공하고 있다. 게다가 메시지 자원(resource)의 지역화(localization)을 지원한다는 장점도 있다. 하지만, Structs API를 사용하여 모델2 구조를 구현하려면 구현해야 할 클래스들이 적잖이 많다. 대형 프로젝트에서는 각 기능에 따라 모델 클래스를 분류할 경우 웹 어플리케이션을 관리하는 데 큰 도움이 되는 것이 사실이지만, 1-3개월 정도의 기간이 소비되는 소형 프로젝트에서는 불필요하다 생각될 정도로 처리해야 할 것들이 많아지는 것도 사실이다. 그래서 필자가 생각해낸 것이 Struts의 템플릿 기능을 사용하여 모델 2 구조를 구현해보는 것이었다.

Struts의 템플릿 기능은 말 그대로 JSP 페이지를 템플릿으로 처리할 수 있도록 해 주는 기능이다. 예를 들어 대부분의 웹 사이트는 다음 그림과 같이 하나의 일관된 웹 페이지 디자인을 전체 페이지에 걸쳐서 공통으로 사용하며, 보통 이러한 일정한 형태의 디자인이 적게는 2-3개 많으면 10개 정도 사용되는 것이 일반적이다.


위 그림은 IBM의 한 페이지를 살펴본 것인데, 그림을 보면 사이트가 헤더-서브메뉴-내용-풋터로 구성되어 있으며, 이 중 서브메뉴와 내용 부분은 페이지에 따라 변경되고 헤더와 풋터는 변경되지 않는다는 것을 알 수 있다. Struts의 템플릿 기능은 이와 같이 동일한 구조를 갖는 페이지들이 수십 또는 수백개 이상 존재할 때 유용하게 사용할 수 있다. 실제로 얼마나 Struts의 템플릿 기능이 유용하게 사용될 수 있는 지 살펴보기 위해 간단하게 Struts의 템플릿 기능을 사용하여 헤더-내용-푸터의 구조를 갖는 페이지를 작성해보도록 하자.

Struts를 사용하려면?

Struts를 사용하려면 다음과 같이 하면 된다.
  1. http://jakarta.apache.org/struts/index.html에서 Struts 1.0.2 버전을 다운로드 받는다.
  2. 다운로드 받은 파일의 압축을 푼 다음의 파일들을 알맞게 복사한다.
    • /lib/struts.jar 파일을 WEB-INF/lib/ 디렉토리에 복사하거나 [톰캣]/lib와 같이 JSP 콘테이너가 클래스패스로 인식하는 디렉토리에 복사한다.
    • /lib/struts-template.tld 파일을 WEB-INF/tlds/ 디렉토리에 복사한다.

  3. web.xml 파일에 다음의 내용을 추가한다.
    <taglib>
        <taglib-uri>/WEB-INF/tlds/struts-template.tld</taglib-uri>
        <taglib-location>/WEB-INF/tlds/struts-template.tld</taglib-location>
    </taglib>


Strut의 템플릿을 사용할 때는 [템플릿을 사용하는 JSP 페이지], [템플릿 JSP 페이지], [각 부분을 담당하는 JSP 페이지] 이렇게 3가지 구성요소가 필요하다. 예제에서 사용할 [템플릿 JSP 페이지]인 template.jsp는 다음과 같다.

    <%--
        filename: template.jsp
        템플릿 파일.
    --%>
    <%@ page contentType="text/html; charset=euc-kr" %>
    <%@ taglib uri="/WEB-INF/tlds/struts-template.tld" prefix="template" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    
    <body>
    
    <table border="0" cellspacing="0" cellpadding="0" 
           width="100%" bgcolor="#3333AA">
        <tr>
            <td width="100%" rowspan="2">
            <font color="#FFFFFF">
            <template:get name='header'/>
            </font>
            </td>
        </tr>
    </table>
    
    <table border="0" cellspacing="0" width="100%">
        <tr>
            <td>
            <template:get name='content'/>
            </td>
        </tr>
    </table>
    
    <table border="0" cellpadding="0" cellspacing="0"
           width="100%" bgcolor="#000000">
        <tr>
            <td width="100%" align="center" class="copyright">
            <font color="#FFFFFF" size="-1">
            <template:get name='footer'/>
            </font>
            </td>
        </tr>
    </table>
    
    </body>
    
    </html>

template.jsp는 일반적인 JSP 페이지와 큰 차이점이 없다. 차이점이 있다면 커스텀 태그인 <template:get>을 사용한다는 점이다. 위 코드를 보면 세 개의 <template:get> 커스텀 태그가 사용된 것을 알 수 있는데, 바로 이 <template:get> 커스텀 태그가 템플릿 기능의 핵심적인 기능을 제공한다. 각각의 <template:get> 태그를 보면 name 속성의 값을 'header', 'content', 'footer'로 지정한 것을 알 수 있는데, <template:get> 태그는 바로 이 name 속성의 값에 해당하는 페이지를 그 위치에 삽입한다. 지금의 설명만으로는 뭔가 부족할 것이다. 실제로 위 template.jsp를 템플릿으로 사용하는 JSP 페이지를 살펴보자. 다음은 template.jsp를 템플릿으로 사용하는 index.jsp의 소스 코드이다.

    <%--
        page: index.jsp
        template.jsp를 템플릿으로 사용하는 JSP 페이지
    --%>
    <%@ taglib uri="/WEB-INF/tlds/struts-template.tld" prefix="template" %>
    <template:insert template="/template.jsp">
        <template:put name="header" content="/header.jsp"/>
        <template:put name="content" content="/mainContent.jsp"/>
        <template:put name="footer" content="/footer.jsp"/>
    </template:insert>

위 코드에서 눈여겨 볼 부분은 <template:insert> 태그와 <template:put> 태그이다. 먼저 <template:insert> 태그는 template 속성을 통해서 어떤 JSP 파일을 템플릿으로 사용할 지 지정한다. 위 코드에서는 앞에서 작성한 template.jsp를 템플릿으로 사용한다고 지정하였다. 그리고 <template:insert> 태그에 중첩된 <template:put> 태그는 사용할 템플릿 파일에 어던 파일들을 삽입할 지를 지정한다. <template:put> 태그는 name 속성과 content 속성을 갖고 있는데, name 속성의 값은 템플릿의 파일의 <template:get>의 name 속성에서 사용되는 값과 동일하며, content 속성은 name 속성의 값이 일치하는 <template:get< 태그의 위치에 삽입될 페이지를 지정한다. index.jsp와 template.jsp의 관계를 정리하면 다음과 같다.

  • index.jsp는 template.jsp를 템플릿으로 사용한다. (index.jsp의 <template:insert> 태그)
  • template.jsp의 <template:get name='header'/>에는 header.jsp가 삽입된다. (index.jsp의 <template:put name="header" content="/header.jsp"/>)
  • 마찬가지로 'content'와 'footer'에 해당하는 <template:get> 태그에는 각각 mainContent.jsp와 footer.jsp가 삽입된다.
실제로 웹브라우저에서 index.jsp를 실행해보면 다음과 같은 결과가 출력된다.


만약, index.jsp가 아닌 다른 페이지에서 같은 모양의 JSP 페이지를 만들어 내고 싶다면 다음과 같이 하면 될 것이다. 다음 코드의 경우는 index.jsp와 똑같은 디자인을 사용하고 똑같은 헤더와 풋터를 사용하고 내용 부분만 index.jsp와 다른 JSP 페이지를 사용하게 된다.

    <%@ taglib uri="/WEB-INF/tlds/struts-template.tld" prefix="template" %>
    <template:insert template="/template.jsp">
        <template:put name="header" content="/header.jsp"/>
        <template:put name="content" content="/contentList.jsp"/>
        <template:put name="footer" content="/footer.jsp"/>
    </template:insert>

템플릿에 대한 보다 자세한 사용방법은 http://jakarta.apache.org/struts/doc-1.0.2/api/org/apache/struts/taglib/template/package-summary.html에서 얻을 수 있으니 참고하기 바란다.

JSP만을 이용한 모델 2 구조와 Struts의 템플릿 기능 적용

사실 앞에서 살펴본 내용만으로도 Struts의 템플릿의 기본적인 기능을 사용하는 데는 큰 어려움이 없다. 사실, Struts의 템플릿 기능은 이 글의 핵심 내용이 아니다. 이 글의 핵심 내용은 Struts의 템플릿 기능을 사용하여 JSP 만으로 MVC 패턴을 구현하는 것이다. 물론, 이 글을 읽고 있는 다수의 개발자들은 '서블릿+JSP+모델객체'로 구성된 모델 2 구조를 사용하면 될거라고 생각할 것이다. 뭐, 틀린 말은 아니고 일반적으로 '서블릿+JSP+모델객체'의 모델 2 구조가 정석으로 받아들여지고 있다. 하지만, MVC 패턴에서 컨트롤러의 역할을 하는 서블릿을 수정해야 되는 경우 컴파일을 다시 해야 하고, 운영 환경이었을 경우 클래스를 교체해야 하는 시기 및 서블릿 엔진의 재가동 여부 등을 모두 고려해야 한다. 특히, 소규모 프로젝트에서 서블릿을 만들고 컴파일하고 때때로 서블릿 엔진을 재가동하는 등의 작업은 개발자에게 있어서 매우 귀찮은 작업이 된다.

이러한 귀찮은 작업을 없앨 수 있는 방법이 바로 템플릿 기능을 사용하여 MVC 패턴을 구현해보는 것이다. 생각해보면 JSP 만으로 MVC 패턴을 구현하지 못하리란 법도 없다. 왜냐면, JSP가 곧 서블릿이기 때문이다. (JSP에 대한 서적을 제대로 읽은 독자라면, JSP 페이지가 서블릿으로 컴파일된 후 실행된다는 것을 알고 있을 것이다.) 따라서, 컨트롤러 서블릿을 만들듯, JSP를 컨트롤러 JSP로 만들면 매우 간단하게 MVC 패턴을 구현할 수 있게 된다. 만약 if-else 구문에 기반한 컨트롤러를 만든다면, JSP 페이지는 다음과 같은 형태를 취하게 될 것이다.

  <%
      String cmd = request.getParameter("cmd");
      String viewPage = null;
      if (cmd.equals(Command.ADD_PROCESS)) {
          someProcessingAdd(request, response);
          viewPage = ADD_PROCESS_PAGE;
      } else if (cmd.equals(Command.ADD_FORM)) {
          someProcessingAddForm(request, response);
          viewPage = ADD_FORM_PAGE;
      } else if (cmd.equals(Command.LIST)) {
          someProcessingList(request, response);
          viewPage = LIST_PAGE;
      }
  %>
  <jsp:forward page = "<%= viewPage %> />
  ...
  

위 코드는 MVC 패턴을 구현하는 방법 중 가장 단순한 형태이다. 물론, someProcessingXX() 종류의 메소드를 커맨드 패턴을 적용하여 객체단위로 분리시킬 수 있을 것이고, 팩토리 패턴을 적용하면 위 JSP 코드는 보다 간결해질 것이다. 하지만, 어떤 패턴을 적용하든지 간에 위와 같은 형태의 코드가 MVC 패턴을 구현하는 가장 기본적인 형태가 된다. (이에 대한 내용은 자바캔 필자가 올린 'JSP Model 2 Architecture 2부, 커맨드 패턴의 적용'을 참고하기 바란다.)

JSP 만으로 MVC 패턴을 구현하는 것이 서블릿을 사용하여 MVC 패턴을 구현하는 것과 별다른 차이점이 없기 때문에 JSP 만으로 MVC 패턴을 구현하다 해서 별다른 어려움이 있는 것은 아니다. 소규모 프로젝트의 경우는 컴파일 과정을 줄이면서 동시에 객체지향적 방법을 적용할 수 있기 때문에 서블릿-JSP로 구성된 모델 2 구조보다 JSP-JSP로 구성된 MVC 패턴 구조가 현실적으로 더 좋은 구조라고도 볼 수 있다.

지금쯤이면 글을 읽는 여러분중에 몇몇은 JSP-JSP로 모델2 구조를 구현하는 것과 Struts의 템플릿 기능 사이에 어떤 관계가 있을까 하고 의심의 눈초리를 보내고 있을지도 모르겠다. 물론 둘 사이에 특별한 관계가 있는 것은 아니다. 하지만, JSP-JSP로 구성된 MVC 패턴 구조에 Struts의 템플릿 기능을 더하면 개발자는 많은 편의를 얻게 된다. 다음의 코드를 살펴보자.

    <%@ taglib uri="/WEB-INF/tlds/struts-template.tld" prefix="template" %>
    <%
        String cmd = request.getParameter("cmd");
        String viewPage = null;
        if (cmd.equals(Command.ADD_PROCESS)) {
            someProcessingAdd(request, response);
            viewPage = ADD_PROCESS_PAGE;
        } else if (cmd.equals(Command.ADD_FORM)) {
            someProcessingAddForm(request, response);
            viewPage = ADD_FORM_PAGE;
        } else if (cmd.equals(Command.LIST)) {
            someProcessingList(request, response);
            viewPage = LIST_PAGE;
        }
    %>
    <%-- jsp:forward 대신에 template:insert를 사용한다 --%>
    <template:insert template="/template.jsp">
        <template:put name="header" content="/header.jsp"/>
        <template:put name="content" content="<%= viewPage %>"/>
        <template:put name="footer" content="/footer.jsp"/>
    </template:insert>
    ...
    

어떤가? 간단하게 컨트롤러 개발의 귀찮음과 웹페이지 레이아웃의 유지가 한꺼번에 해결되는 것을 알 수 있다. 만약 컨트롤러를 서블릿으로 한다면 다음과 같은 불편함이 발생할 수 있다.

  • 템플릿을 사용하는 JSP 페이지로 포워딩을 해야 한다.
  • 템플릿을 사용하지 않는 경우, 별도의 레이아웃 관리 기법을 개발해야 한다.
  • 컨트롤의 코드가 바뀌면 다시 소스 코드를 컴파일 해야하고, 때에 따라서 어플리케이션 서버를 재가동해야 하는 경우도 발생한다.
이 글에서 제시한 방법을 사용한다면, 위와 같은 불편함이 줄어들 뿐만 아니라 하나의 템플릿을 사용하면서도, 적은 노력을 기울여서 일정한 레이아웃을 유지하면서 동시에 다양한 화면 구성을 만들어낼 수 있다는 장점이 있다. 또한, 템플릿 파일 자체가 JSP 페이지이기 때문에 해당 템플릿을 사용하여 출력되는 모든 웹페이지에 대해서 동일한 처리를 할 수 있다. 예를 들어, 로그인 여부를 파악한다던가, 각 페이지에 대한 사용자 추적 로그를 작성한다던가, 통합적으로 에러 로그를 처리하는 등의 작업을 하나의 템플릿 파일(또는 경우에 따라서 4-5개 정도의 템플릿 파일에서)을 통해서 일괄적으로 처리할 수 있다.

결론

필자가 이 글을 통해서 말하고자 하는 건, 이 글에서 제시한 방법이 실제 개발과정에서 생각하는 것 이상으로 편리하다는 점이다. 그렇다고 해서 관리가 전혀 안 되는 것도 아니다. 웹 화면을 구성하는 각 요소들이 각각의 JSP로 분리되기 때문에 관리하기가 오히려 더 수월해지며, 컨트롤 역할을 하는 JSP 역시 하나의 객체로서 처리되기 때문에(즉, 서블릿으로 변환되기 때문에) 객체 단위의 관리가 가능하다.

실제로 필자는 이 글에서 소개한 방법을 사용하여 물류 관련 경매 사이트를 개발해보았다. DB 테이블과 1대1로 매핑되는 자바빈 객체와 DB 테이블에 대해서 CRUD(Create-Read-Update-Delete) 작업을 처리해주는 래퍼 객체를 기반으로 하였으며, JSP-JSP로 구성된 MVC 구조와 Struts의 템플릿 기능을 혼합해서 사용했었는데 서블릿 기반의 모델 2 구조를 사용하는 것보다 편리하고 빠르게 개발을 할 수 있었다. JSP를 컨트롤러 객체로 작성했을 경우 한가지 문제점이 발생하는 데 그건 바로 Javadoc을 사용하여 컨트롤러 JSP에 대한 문서화를 할 수 없다는 것이다. 서블릿으로 작성할 경우 다른 자바 클래스처럼 Javadoc을 사용하여 컨트롤러에 대한 문서를 간단하게 생성할 수 있지만, JSP의 경우는 J2SDK에서 제공하는 Javadoc으로는 처리할 수 없는 것이 사실이다. 하지만, JSP 페이지에 대해서 별도의 문서화 방법을 강구할 수 있기 때문에, 이것도 문제가 되지 않는다.

여러분에게 필자의 방법을 강요할 순 없지만, 대규모 프로젝트가 아니라면(설사 대규모 프로젝트라 할지라도) 이 글에서 소개한 JSP를 기반으로 MVC 구조를 구현해볼 것을 권한다. 아마 기대했던 것 이상의 개발 효과를 얻을 수 있을 것이다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

스트링 템플릿

Java 2001.04.02 12:38
이메일의 내용이나 제목, 또는 JSP 페이지에서 사용되는 문서를 템플릿으로 처리할 수 있는 클래스를 작성해본다.

템플릿과 템플릿의 필요성

엔터테이먼트 사이트, 인터넷 뱅킹을 지원해주는 금융 사이트 그리고 인터넷 매거진 형태로 운영되는 사이트 등 요즘 많은 웹 사이트가 회원제로 운영되고 있다. 이런 회원제로 운영되는 웹 사이트는 회원에게 정보를 알리는 방법으로서 뉴스레터를 사용하고 있다. (자바캔 역시 회원에게 금주에 새로 추가된 기사를 알려주기 위해 뉴스레터를 사용하고 있다.) 뉴스레터의 특징은 고정된 형식을 갖고 있고 특정 부분의 내용만 변경된다는 사실이다. 예를 들어, 다음과 같은 내용을 갖는 뉴스레터가 있다고 해 보자.

최범균 회원님
안녕하세요. XXXXX(주)입니다.

아직 떠나기 싫은 추위가 가끔씩 기승을 부리기는 하지만 여기 저기서 새싹이 돋는
걸 보니 그래도 봄은 봄입니다. 봄에 싱그러운 새싹이 나듯이 새롭게 출간된 도서를
알려드리겠습니다.  

....

위와 같은 내용에서 눈여겨 볼 점은 "최범균" 부분만 회원마다 다를 뿐 나머지 내용은 모든 회원에게 보내지는 뉴스레터가 모두 같다는 점이다. 이처럼 전체적인 내용은 완전히 동일하고 일부만 변경될 필요가 있는 경우 사용할 수 있는 것이 바로 템플릿이다.

템플릿

템플릿이라는 용어는 흔히 사용되는 용어로서 C++ 프로그래밍을 해 본 개발자라면 매우 익숙할 것이다. C++에서의 템플릿은 자료 타입이 변경되는 것이라면, 이 글에서 설명하고자 하는 템플릿은 문서 내용 중 일부가 변경되는 것이라 할 수 있다. 회원제 서비스에서 많이 사용되는 뉴스레터는 문서로 볼 수 있으며, 또한 뉴스레터는 템플릿을 사용하기에 가장 알맞은 문서 중의 하나이다.

이 글에서 사용할 템플릿은 다음과 같은 형태를 지닌다.

<%$ memberName %> 회원님
안녕하세요. XXXXX(주)입니다.

저희 XXXXX(주)에서 새로이 다음과 같은 신간이 추가되었습니다.

<%$ content %>

앞으로 저희 XXXXX(주) 책에 많은 관심 부탁드립니다.

위에서 <%$ memberName %>과 <%$ content %>가 템플릿에서 변경되는 부분이고, 나머지는 템플릿에서 변경되지 않는 부분이다. 이 글에서는 변경되는 부분을 템플릿 인자(template argument; 이 용어는 필자가 정한 것이다)라고 표현할 것이다. 템플릿 인자는 다음과 같은 형식을 갖고 있다.

<%$ 인자명 %>

"인자명"은 각각의 템플릿 인자를 구분하기 위해 사용된다. 앞에서 <%$ memberName %>과 <%$ content %>의 두 템플릿 인자가 있는데, 이 두 템플릿 인자에는 들어가게 될 내용이 서로 다르다. 따라서, 각각의 템플릿 인자는 "인자명"을 통해서 구분되며, StringTemplate 클래스는 "인자명"을 사용하여 각각의 템플릿 인자에 알맞은 내용을 삽입하게 된다.

StringTemplate 클래스의 사용

이 글에서는 StringTemplate 클래스를 어떻게 구현하는지에 앞서 StringTemplate 클래스를 어떻게 사용할지에 대해서 먼저 알아보도록 하자. StringTemplate 클래스가 제공하는 메소드는 다음과 같다.

  • public void setArgument(String key, String value)
    각각의 템플릿의 인자에 들어갈 값을 지정한다. key는 인자명을 나타내며, value는 템플릿 인자 대신 들어갈 값을 나타낸다.
  • public String parse()
    템플릿에 있는 템플릿 인자를 알맞게 변환한 결과를 String 객체로 리턴해준다.
StringTemplate 클래스는 템플릿 문서를 파일로부터 읽어온다. 이를 위해 StringTemplate 클래스의 생성자는 템플릿 문서로 사용할 파일을 입력받는다. 다음은 StringTemplate 클래스 생성자를 보여주고 있다.

public StringTemplate(File file, String encoding) throws IOException {
   // 지정한 파일을 Template으로 사용한다.
   if (!file.isFile() || !file.canRead() ) {
      throw new IOException("Invalid File instance.");
   }
   this.templateFile = file;
   size = (int)file.length();
   buffer = new StringBuffer((int)(size * 1.1));
   args = new HashMap();
   
   this.encoding = encoding;
}

StringTemplate 클래스의 생성자를 보면 파라미터로 encoding을 넘겨주는데, 이 값은 파일을 읽어올 때 사용할 인코딩을 나타낸다. 예를 들어, "ISO-8859-1" 인코딩을 사용하여 템플릿 문서 파일을 읽어오고 싶을 경우 다음과 같이 하면 된다.

File file = new File("c:/template/mail.template");
StringTemplate template = new StringTemplate(file, "iso-8859-1");

일단 StringTemplate 객체를 생성하면, setArgument() 메소드를 사용하여 템플릿 인자에 들어갈 값을 지정한 후 parse() 메소드를 사용하여 템플릿에 템플릿 인자를 적용한 결과를 구하면 된다. 예를 들어, 템플릿 문서가 다음과 같다고 해 보자.

<%$ memberName %> (<%$ memberId %>)님께서 회원 가입시에 입력하신
정보는 다음과 같습니다.

주소: <%$ address %>전화번호: <%$ tel %>이메일: <%$ email %><%$ memberName %> 회원님의 입력한 정보가 맞지 않다면,<%$ adminEmail %>로 연락주시기 바랍니다.

이 템플릿 문서가 "c:/template/register.tempate" 파일이라고 할 경우, 다음과 같이 StringTemplate 클래스를 사용하여 각 템플릿 인자에 들어갈 내용을 지정할 수가 있다.

File file = new File("c:/template/register.template");
StringTemplate template = new StringTemplate(file, "iso-8859-1");
template.setArgument("memberName", "최범균");
template.setArgument("memberId", "madvirus");
template.setArgument("address", "서울시 서대문구 북아현동");
template.setArgument("tel", "011-xxx-yyyy");
template.setArgument("email", "era13@hanmail.net");
template.setArgument("adminEmail", "admin@hostname.com");

String result = template.parse();

여기서 parse() 메소드를 생성자에서 입력받은 템플릿 문서와 setArgument() 메소드를 통해서 입력받은 템플릿 인자를 사용하여 최종 결과를 생성해낸다. 위에서 template.parse() 의 결과를 저장하게 되는 result는 다음과 같을 것이다.

최범균 (era13)님께서 회원 가입시에 입력하신
정보는 다음과 같습니다.

주소: 서울시 서대문구 북아현동전화번호: 011-xxx-yyyy이메일: era13@hanmail.net최범균 회원님의 입력한 정보가 맞지 않다면,admin@hostname.com로 연락주시기 바랍니다.

여기서 굵게 표시한 부분은 템플릿 인자가 적용된 부분이다.

StringTemplate 클래스의 구현

이제 StringTemplate 클래스의 핵심 메소드인 setArgument() 메소드와 parse() 메소드를 구현해보자. 먼저 setArgument()를 구현해보자. setArgument() 메소드는 파라미터로 전달받은 템플릿 인자의 이름과 값을 알맞게 매핑시키면 된다. 이처럼 이름과 값을 매핑시킬 때 사용할 수 있는 것으로 Hashtable과 HashMap이 있다. 여기서는 HashMap을 사용하였다. 다음은 setArgument() 메소드의 정의이다.

public void setArgument(String key, String value) {
   args.put(key, value);
}

위 코드에서 args는 HashMap을 나타낸다. 매우 간단하게 setArgument() 메소드가 구현된다는 것을 알 수 있다. 이제 parse() 메소드를 살펴보자. parse() 메소드의 구현이 어렵지는 않지만, setArgument() 메소드에 비해 상대적으로 매우 복잡한 편이다. 다음은 parse() 메소드 내에서 이루어지는 작업을 순서대로 정리한 것이다.

  1. 템플릿 파일을 읽어온다.
  2. 읽어온 템플릿 문서에서 <%$ 를 찾는다.
  3. <%$ 가 존재할 경우.
    1. <%$ 이전까지의 내용을 분석 결과에 저장한다.
    2. %>가 있는 지 검색한다. 존재할 경우 템플릿 인자명을 구한 후 인자명에 해당하는 값을 HashMap으로부터 읽어와 분석 결과에 저장한다. 그런 후 2부터 과정을 반복한다.
    3. %>가 존재하지 않을 경우 <%$ 부터의 내용을 그대로 분석 결과에 저장한다.
  4. <%$가 존재하지 않을 경우 내용을 그대로 출력한다.
실제 parse() 메소드는 다음과 같다.

public String parse() throws IOException {
   // templateFile을 연다.
   if (!aleadyRead) {
      
      if (encoding != null) {
         // 지정한 인코딩이 존재한다면,
         byte[] tempTemplate = new byte[size];
         BufferedInputStream is = null;
         try {
            FileInputStream fis = new FileInputStream(templateFile);
            is = new BufferedInputStream(fis);
            is.read(tempTemplate);
         } finally {
            if (is != null) try { is.close(); } catch(IOException ex1){}
         }
         String tempString = new String(tempTemplate, encoding);
         template = tempString.toCharArray();
      } else { // 지정된 인코딩이 없다면,
         template = new char[size];
         
         BufferedReader br = null;
         try {
            FileReader fr = new FileReader(templateFile);
            br = new BufferedReader(fr);
            br.read(template);
         } finally {
            if (br != null) try { br.close(); } catch(IOException ex1) {}
         }
      }
      aleadyRead = true;
   }
   
   // 버퍼에 있는 내용을 삭제한다.
   int len = buffer.length();
   if (len > 0) {
      buffer.delete(0, len-1);
   }
   
   // 문자열의 처음부터 끝까지 훑는다.
   int end = size - 5; // 시작태그를 찾을 때 사용되는 마지막 인덱스
   int end1 = size - 2; // 끝태그를 찾을 때 사용되는 마지막 인덱스
   int lastProcessed = -1;
   
   outter:
   for (int i = 0 ; i < end ; i++) {
      if (template[i] == '<' && template[i+1] == '%' && template[i+2] == '$') { // 시작 태그라면,
         // 시작 태그 이전의 내용을 저장한다.
         if (lastProcessed == -1) {
            if (i > 0) buffer.append(template, 0, i); // i 이전까지의 내용을 저장한다.
         } else {
            buffer.append(template, lastProcessed + 1, i - lastProcessed - 1);
         }
         
         // 끝태그를 찾는다.
         inner:
         for (int i1 = i+4 ; i1 < end1 ; i1++) {
            if (template[i1] == '%' && template[i1+1] == '>') { // 끝태그라면,
               // 시작 태그와 끝 태그 사이에 있는 이름을 구한다.
               String key = new String(template, i+3, i1 - (i+3)).trim(); // 버퍼에 알맞게 내용 출력
               String value = (String)args.get(key);
               if (value != null) buffer.append(value);
               
               lastProcessed = i1+1;
               i = lastProcessed;
               continue outter;
            }
         }
         
         // 끝태그가 없다면, 시작태그부터 모든 내용을 그대로 출력한다.
         buffer.append(template, i, size-i);
         lastProcessed = size - 1;
         i = lastProcessed;
      }
   }
   
   if (lastProcessed == -1) { // 태그가 없었다면,
      buffer.append(template);
   } else if (lastProcessed < size - 1) {
      // 끝태그 이후로 남아 있는 것이있다면,
      buffer.append(template, lastProcessed + 1, size - (lastProcessed + 1) );
   }
   
   // 파싱 결과를 저장한다.
   lastResult = buffer.toString();
   
   return lastResult;
}

위 코드에서 template 변수는 char 형 배열로서, 템플릿 문서의 내용을 저장하고 있다. 템플릿 문서의 내용을 String 객체가 아닌 char 형 배열로 저장하고 있는 이유는 처리 속도를 더 빠르게 하기 위해서이다. (이에 대한 내용은 O'reilly의 'Java Performance Tuning'을 참고하기 바란다. StringTemplate 클래스의 완전한 소스 코드는 관련 링크를 참조하기 바란다.

StringTemplate 클래스의 응용

StringTemplate 클래스를 응용할 수 있는 분야는 앞에서 말했듯이 메일링 리스트이다. 예를 들어, 회원 중 최근 30일 내에 로그인 한 사용자에게만 메일을 전송하고 싶다고 해 보자. 이 경우 다음과 같은 형태로 StringTemplate 클래스를 사용할 수 있을 것이다.

MimeMessage message = new MimeMessage();
// 메일과 관련된 내용 지정
StringTemplate template = new StringTemplate(templateFile, "KSC5601");
template.setContent("content", content);
PreparedStatement pstmt = conn.prepareStatement("select name, email from Member where login >= ?");
Calenader cal = Calendar.getInstance();
cal.add(Calendar.DATE, -30);
pstmt.setTimestamp(1, new Timestamp(cal.getTime().getTime());
rs = pstmt.executeQuery();
while (rs.next()) {
   template.setArument("memberName", rs.getString("name"));   
   message.setRecipient(rs.getString("email"), rs.getString("email") );
   message.setContent(template.parse(), "text/plain");
   message.send();
}
rs.close();
pstmt.close();

위 코드는 Member 테이블이 회원 정보를 갖고 있고 name 컬럼과 email 컬럼이 각각 회원의 이름과 이메일을 나타낸다고 가정한 상태에서 작성한 것이다. 위 코드를 보면 setArgument() 메소드를 통해서 회원마다 달라져야 하는 템플릿 인자의 값을 지정하고 바로 이메일을 전송하는 것을 알 수 있다.

결론

이 글에서는 템플릿 문서를 사용할 수 있도록 해 주는 StringTemplate 클래스의 사용법과 구현이 어떻게 되어 있는지에 대해서 살펴보았다. 같은 내용을 갖는 메일을 대량으로 발송하거나 회원에게 메일을 발송할 때 이 클래스를 유용하게 사용할 수 있을 것이다.

Posted by 최범균 madvirus

댓글을 달아 주세요