주요글: 도커 시작하기
반응형
이메일의 내용이나 제목, 또는 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 클래스의 사용법과 구현이 어떻게 되어 있는지에 대해서 살펴보았다. 같은 내용을 갖는 메일을 대량으로 발송하거나 회원에게 메일을 발송할 때 이 클래스를 유용하게 사용할 수 있을 것이다.

+ Recent posts