주요글: 도커 시작하기
반응형
1부에서 설계한 메일 발송 커스텀 태그를 구현해본다.

sendMail 태그의 구현

실제로 구현을 하는 과정에서 필자는 지난 1부에서 설계한 커스텀 태그를 다음과 같이 약간 변경하였다.

  <javacan:sendMail smtpServer="stmp server" charset="character encoding">     <javacan:from>era13@hanmail.net(최범균)(</javacan:from>     <javacan:to>a@a.com(나);b@b.com(너)</javacan:to>     <javacan:subject>메일의 주제</javacan:subject>     <javacan:content html="true|false">메일의 내용</javacan:content>  </javacan:sendMail>

지난 1부에서 javacan:body 커스텀 태그는 javacan:content 태그로 변경되었으며 javacan:cc 커스텀 태그는 제거하였다. 그 외에는 모두 지난 1부에서와 같은 의미를 지닌다.

주의 - 이 글에서는 커스텀 태그에 대한 자세한 설명은 하지 않을 것이므로 JSP 기초 서적을 통해서 커스텀 태그를 미리 공부하기 바란다.

sendMail 태그의 구현

sendMail 태그의 주된 역할은 메일을 발송하는 데 필요한 정보를 수집하여 메일 메시지를 생성한 후 지정된 SMTP 서버를 통해서 메일을 발송하는 것이다. sendMail 태그는 중첩되어 있는 from, to, subject, content 태그로부터 메일 메시지를 생성하는 데 보내는사람, 받는 사람(들), 주제, 내용과 같은 항목을 얻게 되고, smtpServer 속성을 사용하여 메일을 발송할 SMTP 서버를 지정하게 된다. charset 속성은 메일을 구성하고 있는 데이터의 캐릭터셋을 지정하기 위해서 사용되며, from, to, subject, content 커스텀 태그는 charset 속성에서 지정한 캐릭터셋에 알맞은 데이터를 입력받아야 한다.

sendMail 태그의 몸체를 처리한 후 sendMail의 끝태그(</javacan:sendMail>)는 몸체에 중첩되어 있는 from, to, subject, content 태그를 통해서 입력받은 메일 항목으로부터 javax.mail.internet.MimeMessage 객체를 생성한 후 메일을 전송한다. 이는 커스텀 태그의 끝태그를 만났을 때 호출되는 doEndTag() 메소드를 통해서 처리된다.

다음 코드는 sendMail 커스텀 태그를 구현한 클래스인 javacan.talib.mail.SendMailTag의 소스 코드이다.

  package javacan.talib.mail;
  
  import javax.servlet.jsp.*;
  import javax.servlet.jsp.tagext.*;
  import javax.mail.*;
  import javax.mail.internet.*;
  
  import javacan.util.mail.*;
  
  import java.util.Properties;
  
  /**
   * 메일을 전송해주는 SendMailTag
   * 
   *  <javacan:sendMail smtpServer="localhost" charset="euc-kr">
   *     <javacan:from>fromEmail</javacan:from>
   *     <javacan:to>email1 ; email2 ; email3</javacan:to>
   *     <javacan:subject>주제</javacan:subject>
   *     <javacan:content html="true">
   *     내용
   *     </javacan:content>
   *  </javacan:sendMail>
   *
   * @author 최범균, era13@hanmail.net
   */
  public class SendMailTag extends TagSupport {
     /**
      * 기본 SMTP 서버 주소: localhost 이다.
      */
     public static final String DEFAULT_SMTP_SERVER = "localhost";
     /**
      * 메일의 기본 캐릭터셋. euc-kr 이다.
      */
     public static final String DEFAULT_CHARSET = "euc-kr";
     
     /**
      * SMTP 서버
      */
     private String smtpServer = null; 
     /**
      * 메일의 캐릭터셋. 메일의 주제를 지정할 때 사용된다.
      */
     private String charset    = null; 
     /**
      * 메일의 주제
      */
     private String subject    = null;
     /**
      * 메일의 내용. String 이나 Multipart가 올 수 있다.
      */
     private Object content    = null;
     /**
      * 메일의 컨텐츠 타입
      */
     private String contentType = null;
     /**
      * 보내는 사람의 주소
      */
     private String from = null;
     private InternetAddress fromAddress = null;
     /**
      * 받는 사람 주소
      */
     private String to = null;
     private InternetAddress[] toAddresses = null;
     
     public void setSmtpServer(String smtpServer) {
        this.smtpServer = smtpServer;
     }
     
     public void setCharset(String charset) {
        this.charset = charset;
     }
     
     public String getCharset() {
        return charset;
     }
     
     public void setSubject(String subject) {
        this.subject = subject;
     }
     
     public void setFrom(String from) {
        this.from = from;
     }
     
     public void setTo(String to) {
        this.to = to;
     }
     
     /**
      * 메일의 내용을 지정해준다. contentType은 메일의
      * MIME을 지정해준다. contentType을 null로 지정할 경우
      * MIME 타입은 text/plain 이 되며 이 때, charset의 값은
      * setCharset() 메소드를 사용하여 지정해준 값으로 결정된다.
      * content가 Multipart일 경우 contentType은 사용되지 않는다.
      *
      * @param content 메일의 내용
      * @param contentType 메일의 콘텐츠 타입
      */
     public void setContent(Object content, String contentType) {
        this.content = content;
        this.contentType = contentType;
     }
     
     public int doStartTag() throws JspException {
        checkDefaultValue();
        return EVAL_BODY_INCLUDE;  // 몸체 내용 포함
     }
     
     public int doEndTag() throws JspException {
        try {
           checkMailParameter();
           sendMail();
        } catch(MessagingException ex) {
           throw new JspException(ex.getMessage());
        } finally {
           clearParameter();
        }
        return EVAL_PAGE;
     }
     /**
      * 메일을 발송한다.
      */
     private void sendMail() throws MessagingException {
        Session mailSession = null;
        Properties prop = new Properties();
        prop.setProperty("mail.smtp.host", smtpServer);
        
        mailSession = Session.getDefaultInstance(prop, null);
  
        MimeMessage message = new MimeMessage(mailSession);
        
        message.setFrom(fromAddress);
        message.setRecipients(Message.RecipientType.TO, toAddresses);
        message.setSubject(subject, charset);
        if (content instanceof String) {
           message.setContent((String)content, contentType);
        } else if (content instanceof MimeMultipart) {
           message.setContent((MimeMultipart)content);
        }
        Transport.send(message);
     }
     /**
      * smtpServer와 charset이 지정되어 있지 않을 경우 기본값으로
      * 지정해준다.
      */
     protected void checkDefaultValue() {
        if (smtpServer == null) smtpServer = DEFAULT_SMTP_SERVER;
        if (charset == null) charset = DEFAULT_CHARSET;
     }
     
     /**
      * 메일을 전송할 때 필요한 요소가 올바르게 지정되었는 지 확인한다.
      */
     protected void checkMailParameter() throws JspException {
        if (subject == null || subject.equals("") )
           throw new JspException("Invalid subject");
        if (content == null )
           throw new JspException("Invalid mail content");
        if (!(content instanceof Multipart) && ! (content instanceof String) )
           throw new JspException("Content must be Multipart or String");
        
        if (from == null || from.trim().equals(""))
           throw new JspException("from is null");
        
        if (to == null || to.trim().equals(""))
           throw new JspException("to is null");
        
        // 이메일 주소 구함
        try {
           EmailParser emailParser = new EmailParser('(', ')', ';', charset);
           emailParser.setParsingString(from);
           fromAddress = emailParser.nextInternetAddress();
           emailParser.setParsingString(to);
           toAddresses = emailParser.parseInternetAddresses();
        } catch(Exception ex) {
           throw new JspException(ex.getMessage());
        }
        
        // 컨텐츠 타입 지정
        if (content instanceof String) {
           if (contentType == null) {
              contentType = "text/plain; charset="+charset;
           } else {
              if (contentType.indexOf("charset") == -1) {
                 // charset이 없을 경우
                 contentType = contentType+"; charset="+charset;
              }
           }
        }
     }
     
     protected void clearParameter() {
        smtpServer = null;
        charset = null;
        from = null;
        fromAddress = null;
        to = null;
        toAddresses = null;
        subject = null;
        content = null;
        contentType = null;
     }
  }

SendMailTag 클래스가 좀 복잡하게 느껴질 지 모르니 천천히 살펴보도록 하자. 먼저 SendMailTag 클래스는 stmpServer 속성과 charset 속성의 값을 입력받을 수 있도록 setSmtpServer() 메소드와 setCharset() 메소드를 정의하고 있다. SendMailTag 클래스는 이 두 메소드 뿐만 아니라 다음과 같은 set 메소드를 정의하고 있다.

  • public void setSubject(String subject)
  • public void setFrom(String from)
  • public void setTo(String to)
  • public void setContent(Object content, String contentType)
이 메소드들은 sendMail 태그에 중첩되어 있는 from, to, subject, content 태그를 구현한 클래스에서 호출하는 메소드로서 각각 주제, 보낸사람, 받는사람, 내용을 지정할 때 사용된다. 이 메소드를 통해서 전달된 값들은 각각 SendMailTag 클래스의 from, to, subject, content 필드에 저장되며 sendMail() 메소드에서 MimeMessage를 생성할 때 사용된다.

이 중 특이할 만한 점은 setContent() 메소드의 파라미터가 두개라는 점이다. 이 중, 첫번재 파라미터의 타입은 Object 인데, 이는 어떤 종류의 값이든지 메일의 내용으로 사용할 수 있도록 하기 위함이다. contentType 메소드는 첫번째 파라미터의 MIME 타입을 지정해준다. 만약 첫번째 파라미터인 content의 타입이 Multipart나 MimeMultipart일 경우에는 두번째 파라미터가 사용되지 않으며, 첫번째 파라미터의 타입이 String인 경우에만 두번째 파라미터가 사용된다. 이는 checkMailParameter() 메소드와 sendMail() 메소드를 통해서 확인할 수 있다.

메일 발송은 sendMail의 끝태그를 만날 때 처리되므로, doEndTag() 메소드를 살펴보면 메일 발송이 어떤 순서로 이루어지는 지 알 수 있다.

  public int doEndTag() throws JspException {
     try {
        checkMailParameter();
        sendMail();
     } catch(MessagingException ex) {
        throw new JspException(ex.getMessage());
     } finally {
        clearParameter();     }
     return EVAL_PAGE;
  }

doEndTag() 메소드는 먼저 checkMailParameter() 메소드를 호출하여 메일 메시지를 생성하는 데 필요한 모든 항목들이 올바르게 지정되었는지 확인을 하며, checkMailParameter() 메소드는 항목중 하나라도 올바르게 지정되어 있지 않을 경우 JspException을 발생한다. checkMailParameter() 메소드를 무사히 통과하면 sendMail() 메소드를 호출하여 메일을 발송한다. 메일을 발송하고 나면 clearParameter() 메소드를 호출하여 메일을 발송할 때 사용된 모든 필드의 값을 초기상태로 되돌려 놓는다.

checkMailParameter() 메소드를 보면 EmailParser 라는 클래스가 존재하는데, 이 클래스는 a@a.com(이름);b@b.com(이름1) 의 형태를 지닌 이메일 주소 목록을 IneternetAddress 클래스의 형태로 추출해내는 유틸리티 클래스이다. 이 클래스의 소스 코드는 관련 링크의 소스 코드 부분을 참고하기 바란다.

sendMail() 메소드 차제는 크게 복잡하지 않으므로 추가적인 설명은 하지 않겠다.

from, to, subject, content 커스텀 태그의 구현

from, to, subject, content 커스텀 태그의 구현은 거의 동일하다. 이 네 태그는 모두 몸체의 내용을 클라이언트에 출력하지 않고 대신 몸체 내용을 데이터로서 사용한다. 또한, 이 네개의 커스텀 태그는 sendMail 커스텀 태그에 중첩되어 있다는 점이 같다. 그래서 필자는 이 네개의 커스텀 태그에 앞서서 두 개의 베이스 클래스인 ProcessBodyTagSupport 클래스와 MailBodyReaderTag 클래스를 작성하였다.

먼저 ProcessBodyTagSupport 클래스를 살펴보자. 이 클래스는 몸체 내용을 알맞게 처리해야 하는 클래스들이 상속받아서 사용할 수 있는 추상 클래스로서 다음과 같다.

  package javacan.talib;
  
  import javax.servlet.jsp.*;
  import javax.servlet.jsp.tagext.*;
  
  /**
   * 태그의 몸체 내용을 알맞게 처리하는 커스텀 태그 클래스들의 
   * 기본 상위 클래스
   *
   * @author 최범균
   */
  public abstract class ProcessBodyTagSupport extends BodyTagSupport {
     
     public int doAfterBody() {
        processBody(getBodyContent().getString());
        return SKIP_BODY;
     }
     
     protected abstract void processBody(String body);
  }

위 코드를 보면 doAfterBody() 메소드는 몸체의 내용을 processBody() 메소드에 전달해준후 SKIP_BODY를 리턴하고 있다. 실제 몸체 내용의 처리는 processBody() 메소드를 통해서 호출되는 것이다. 예를 들어, 커스텀 태그의 몸체 내용을 로그 메시지로 기록하는 logging 이라는 커스텀 태그가 있다고 해 보자. 이 경우 logging 커스텀 태그를 구현한 클래스는 다음과 같이 ProcessBodyTagSupport 태그를 상속받아 processBody() 메소드를 알맞게 구현하기만 하면 된다.

  public class LoggingTag extends ProcessBodyTagSupport {
     
     protected void processBody(String logMessage) {
        Logger.log(logMessage);
     }
  }

MailBodyReaderTag 클래스는 sendMail 커스텀 태그에 중첩되어 있는 from, to, subject, content 커스텀 태그를 구현한 클래스가 상속받을 클래스로서, MailBodyReaderTag 클래스는 부모 태그를 나타내는 클래스가 SendMailTag 인지를 판단하는 역할을 한다. MailBodyReaderTag 클래스는 다음과 같다.

  package javacan.talib.mail;
  
  import javax.servlet.jsp.*;
  import javax.servlet.jsp.tagext.*;
  import javacan.talib.ProcessBodyTagSupport;
  
  /**
   * SendMailTag가 나타내는 커스텀 태그에 중첩되는 커스텀 태그를 구현할
   * 클래스들이 상속받아야 하는 클래스
   */
  public abstract class MailBodyReaderTag extends ProcessBodyTagSupport {
     
     public int doStartTag() throws JspException {
        checkParent();
        return super.doStartTag();
     }
     
     protected void checkParent() throws JspException {
        Tag parent = getParent();
        if (parent == null || !(parent instanceof SendMailTag) )
           throw new JspException("Invalid parent tag");
     }
  }

MailBodyReaderTag는 ProcessBodyTagSupport 클래스를 상속받고 있으며, doStartTag()에서 checkParent() 메소드를 호출하여 부모 태그가 SendMailTag 클래스인지를 검사한다. 따라서 MailBodyReaderTag를 상속받는 클래스는 단순히 processBody() 메소드를 알맞게 구현해주기만 하면 된다.

from, to, subject, content 커스텀 태그를 구현한 FromTag, ToTag, SubjectTag, ContentTag 클래스는 모두 MailBodyReaderTag 클래스를 상속받고 있으며, 대부분 매우 간단하게 구현된다. 먼저 FromTag 부터 살펴보자.

  package javacan.talib.mail;
  
  public class FromTag extends MailBodyReaderTag {
  
     protected void processBody(String from) {
        SendMailTag parent = (SendMailTag)getParent();
        parent.setFrom(from);
     }
  }

FromTag는 단순히 processBody() 메소드만을 구현하고 있는 것을 알 수 있다. FromTag 클래스는 getParent() 메소드를 통해서 SendMailTag 객체를 구한 후, setFrom() 메소드를 호출하여 보내는사람의 이메일 주소를 지정해준다. ToTag와 SubjectTag 역시 FromTag와 거의 유사하며, 차이점이 있다면 SendMailTag 클래스의 setFrom() 메소드가 아닌 setTo() 메소드와 setSubject() 메소드를 호출한다는 점 뿐이다.

ContentTag 클래스 역시 FromTag 클래스와 비슷하나 html 이라는 속성을 갖고 있으며, 다른 것들에 비해서 약간(아주 약간이다!) 복잡하다. 먼저 ContentTag 클래스의 소스 코드를 살펴보자.

  package javacan.talib.mail;
  
  public class ContentTag extends MailBodyReaderTag {
     
     private boolean isHtml = false;
     
     public void setHtml(String html) {
        if (html.toLowerCase().equals("true")) {
           isHtml = true;
        } else {
           isHtml = false;
        }
     }
     
     protected void processBody(String content) {
        SendMailTag parent = (SendMailTag)getParent();
        parent.setContent(content, isHtml ? "text/html" : "text/plain" );
     }
  }

ContentTag 클래스는 html 속성을 알맞게 처리하기 위한 setHtml() 메소드를 정의하고 있으며, setHtml() 메소드는 파라미터로 전달받은 값이 "true"일 경우 isHtml 필드의 값을 true로 지정한다. processBody() 메소드는 FromTag 클래스의 processBody() 메소드와 마찬가지로 SendMailTag 객체를 구한 후 알맞은 메소드를 호출하는데, 이때 isHtml의 값이 true일 경우 SendMailTag.setContent() 메소드의 두번째 파라미터의 값을 "text/html"로 지정해준다. setContent() 메소드의 두번째 파라미터는 MIME 타입을 나타낸다는 것은 앞의 SendMailTag 클래스이 소스 코드를 통해서 알 수 있다.

메일 발송 커스텀 태그의 배치

커스텀 태그를 나타내는 클래스를 구현했다고 해서 모든 게 끝난 것이 아니다. 실제로 웹 어플리케이션에서 커스텀 태그를 사용할 수 있도록 배치를 해야 구현이 끝났다고 할 수 있다. 커스텀 태그를 배치하기 위해서는 먼저 커스텀 태그 라이브러리를 설명한 TLD(Tag Library Descriptor) 파일을 작성해야 한다. 여기서 사용된 TLD 파일은 다음과 같다.

  <?xml version="1.0" encoding="ISO-8859-1" ?>
  <!DOCTYPE taglib
     PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN"
     "http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd">
  
  <taglib>
     <tlibversion>1.0</tlibversion>
     <jspversion>1.1</jspversion>
     <shortname>javacan</shortname>
     <uri>http://www.javacan.com/mail/mail-taglib</uri>
     <info>
        메일 전송을 위한 커스텀 태그 라이브러리
     </info>
     
     <tag>
        <name>sendMail</name>
        <tagclass>javacan.talib.mail.SendMailTag</tagclass>
        <bodycontent>JSP</bodycontent>
        <info>
           메일을 전송을 위한 커스텀 태그의 최상위 태그.
        </info>
        
        <attribute>
           <name>smtpServer</name>
           <required>false</required>
           <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
           <name>charset</name>
           <required>false</required>
           <rtexprvalue>false</rtexprvalue>
        </attribute>
     </tag>
  
     <tag>
        <name>from</name>
        <tagclass>javacan.talib.mail.FromTag</tagclass>
        <bodycontent>JSP</bodycontent>
        <info>
           메일을 보내는 사람의 주소를 지정한다.
        </info>
     </tag>
  
     <tag>
        <name>to</name>
        <tagclass>javacan.talib.mail.ToTag</tagclass>
        <bodycontent>JSP</bodycontent>
        <info>
           메일을 받는 사람의 주소를 지정한다.
        </info>
     </tag>
  
     <tag>
        <name>subject</name>
        <tagclass>javacan.talib.mail.SubjectTag</tagclass>
        <bodycontent>JSP</bodycontent>
        <info>
           주제를 지정한다.
        </info>
     </tag>
  
     <tag>
        <name>content</name>
        <tagclass>javacan.talib.mail.ContentTag</tagclass>
        <bodycontent>JSP</bodycontent>
        <info>
           내용을 지정한다.
        </info>
        
        <attribute>
           <name>html</name>
           <required>false</required>
           <rtexprvalue>true</rtexprvalue>
        </attribute>
     </tag>
  
  </taglib>

TLD 파일은 JSP 1.1 규약에 따라서 작성하였다.

TLD 파일을 작성했다면 이제 JSP 페이지에서 커스텀 태그를 사용할 수 있도록 web.xml 파일에 <taglib> 태그를 추가해주어야 한다. 예를 들면 다음과 같이 web.xml 파일을 수정해주면 된다.

  <?xml version="1.0" encoding="ISO-8859-1"?>
  
  <!DOCTYPE web-app
      PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
      "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
  
  <web-app>
     <taglib>
        <taglib-uri>
           http://www.javacan.com/mail/mail-taglib
        </taglib-uri>
        <taglib-location>
           /WEB-INF/tlds/mail-talib.tld
        </taglib-location>
     </taglib>
  </web-app>

이제 sendMail, from, to, subject, content 커스텀 태그를 사용할 모든 준비가 끝났다. 남은 것은 JSP 페이지에서 지금까지 작성한 커스텀 태그를 사용하는 것 뿐이다. 이 글에서는 올바르게 동작하는 지 확인해보기 위해 메일 내용을 입력받는 sendmailform.html 파일과 폼에서 입력받은 내용을 메일로 전송해주는 sendmail.jsp를 작성하였다. 먼저 sendmailform.html 파일은 다음과 같다.

  <html>
  <body>
  <form action="sendmail.jsp" method="post">
  보내는사람이메일: <input type="text" name="from" size="40">
  <br>
  받는사람이메일: <input type="text" name="to" size="40">
  <br>
  주제: <input type="text" name="subject" size="50">
  <br>
  <textarea name="content" cols="50" rows="10"></textarea>
  <br>
  <select name="html" size="1">
     <option value="true">true</option>
     <option value="false">false</option>
  </select>
  <br>
  <input type="submit">
  </form>
  </body>
  </html>

위 HTML 문서는 다음 그림과 같은 폼을 출력해준다.


실제로 커스터 태그를 사용하여 메일을 발송해주는 sendmail.jsp는 다음과 같다.

  <%@ taglib uri="http://www.javacan.com/mail/mail-taglib"
             prefix="javacan" %>
  <%@ page contentType="text/html; charset=euc-kr" %>
  <html>
  <body>
  <javacan:sendMail smtpServer="mail.tpage.com" charset="EUC_KR">
     <javacan:from><%= request.getParameter("from") %></javacan:from>
     <javacan:to><%= request.getParameter("to") %></javacan:to>
     <javacan:subject><%= request.getParameter("subject") %></javacan:subject>
     <javacan:content html="<%= request.getParameter("html") %>">
        <%= request.getParameter("content") %>
     </javacan:content>
  </javacan:sendMail>  메일을 전송했습니다.
  </body>
  </html>

참고적으로 필자는 레신2.0에서 테스트를 해보았다. 다른 JSP 엔진을 사용할 경우에는 한글 처리에 있어서 인코딩 변환을 필요할지도 모른다는 점을 알아두기 바란다.

결론

1부에서는 메일 전송을 위한 커스텀 태그를 설계화는 과정에 대해서 살펴보았고 이번 2부에서는 메일을 전송하는 데 사용되는 커스텀 태그를 실제로 구현해보았다. 지금까지 살펴본 내용을 바탕으로 여러분은 커스텀 태그가 얼마나 많은 양의 스크립트 코드를 대체할 수 있으며, 또한 덜 복잡한 JSP를 작성할 수 있도록 해 주는 지 알게 되었을 것이다.

물론, 커스텀 태그를 개발하는 시간을 무시할 수 없겠지만 커스텀 태그를 객체지향적인 개념하에 개발할 경우 커스텀 태그의 재사용성은 극대화되며 그에 따라 차후 프로젝트에서 중복 개발에 따르는 시간 낭비를 최소화시킬 수 있으며 따라서 재사용성이 높은 커스텀 태그를 개발하는 것은 그만큼 가치가 있다.

관련링크:

+ Recent posts