주요글: 도커 시작하기
반응형
javax.activation.DataSource를 이용하여 파일 첨부 기능을 구현해본다.

메일의 파일첨부와 javax.activation.DataSource

2부에서는 웹을 기반으로 한 메일 전송에 대해서 알아보았다. 하지만, 지난번에 살펴본 내용에는 파일을 첨부하는 기능이 포함되어 있지 않았다. 이번 3부에서는 2부에서 예고한 것 처럼 파일 첨부 기능을 추가해보도록 하자.

파일 첨부 기능을 구현하기 위해서는 JAF(JavaBeans Activation Framework) API를 이용해야 한다. JAF API는 다양한 종류의 데이터 타입을 표현할 수 있도록 javax.activation.DataSource 인터페이스를 제공하고 있다. DataSource 인터페이는 다음과 같은 메소드를 선언하고 있다.

메소드 설명
String getContentType() 데이터의 MIME 타입을 리턴한다.
InputStream getInputStream() 데이터를 읽어올 수 있는 InputStream을 리턴한다.
String getName() 이 객체의 이름을 리턴한다.
OutputStream getOutputStream() 데이터를 쓸 수 있는 OutputStream을 리턴한다. 만약 그럴 수 없다면 예외를 발생한다.

DataSource 인터페이스를 implements 하여 알맞게 데이터 소스를 저장할 수 있는 클래스를 구현하면, javax.activation.DataHandler 클래스를 사용하여 DataSource 로부터 데이터를 읽어올 수 있게 된다. 예를 들어, 다음은 이번 예제에서 첨부 파일 데이터를 나타낼 때 사용되는 데이터 소스 클래스는 ByteArrayDataSource 이며, 이는 다음과 같다.

package javacan.web.mail;

import javax.activation.*;
import java.io.*;

class ByteArrayDataSource implements DataSource {
   byte[] bytes;
   String contentType;
   String name;

   ByteArrayDataSource(byte[] bytes, String contentType, String name) {
      this.bytes = bytes;
      if(contentType == null)
         this.contentType = "application/octet-stream";
      else
         this.contentType = contentType;
      this.name = name;
   }

   public String getContentType() {
      return contentType;
   }

   public InputStream getInputStream() {
      // 가장 마지막의 CR/LF를 없앤다.
      return new ByteArrayInputStream(bytes,0,bytes.length - 2);
   }
   
   public String getName() {
      return name;
   }
   
   public OutputStream getOutputStream() throws IOException {
      throw new FileNotFoundException();
   }
}

위 코드를 보면 ByteArrayDataSource 클래스는 DataSource 인터페이스를 implements 하고 있으며, DataSource 인터페이스에 선언되어 있는 모든 메소드를 알맞게 구현하고 있다. ByteArrayDataSource 클래스에서 눈여겨 볼 부분은 byte 배열을 데이터로 입력받는다는 점이다. 여기서 byte 배열은 첨부되는 파일을 나타낸다. 그리고 contentType 필드의 값이 "application/octet-stream" 인 것을 알 수 있는 데, 이는 첨부되는 파일의 MIME 타입을 지정하는 것이다.

위 코드에서 또하나 눈여겨 볼 부분은 getInputStream() 메소드이다. ByteArrayDataSource 클래스의 getInputStream() 메소드는 생성자에서 파라미터로 전달받은 byte 배열로부터 데이터를 읽어오는 것을 알 수 있다. 이때, 가장 마지막의 2 글자는 사용하지 않는데, 이는 웹 브라우저가 웹 서버에 "multipart/form-data" 인코딩 타입을 사용하여 데이터를 전송할 때에는 각 필드의 마지막에 "\r\n"을 추가하기 때문이다. (multipart/form-data 인코딩 타입으로 전송되는 데이터에 대한 내용을 알고자 하는 회원들은 시중에 나와 있는 서적에서 파일 업로드 부분을 보기 바란다.)

만약 로컬 파일시스템에 있는 파일로부터 데이터를 읽어오길 원한다면 javax.activation.FileDataSource 클래스를 사용하면 된다. 하지만, 이 글에서는 웹 브라우저를 통해서 입력받은 바이트 데이터를 이용해야 하기 때문에 FileDataSource 클래스를 사용하지 않고 앞에서 작성한 ByteArrayDataSource 클래스를 사용한다. FileDataSource 클래스를 사용하여 파일 첨부 기능을 구현하는 것에 대해서는 자바 서비스넷-http://www.javaservice.net-에 있는 글을 참고하기 바란다.

MimeBodyPart의 데이터로 DataSource 사용하기

MimeBodyPart는 DataSource를 데이터로 나타낼 수 있는 방법을 제공하고 있다. 이는 MimeBodyPart 클래스의 setDataHandler() 메소드를 통해서 이루어진다. setDataHandler() 메소드는 DataHandler 객체를 파라미터로 전달받으며, DataHalder 클래스는 DataSource로부터 데이터를 읽어올 수 있도록 해 주는 핸들러 역할을 한다. 예를 들어, 앞에서 작성한 ByteArrayDataSource 객체에 저장된 데이터를 내용으로 사용하는 MimeBodyPart 객체를 작성하고 싶다면 다음과 같이 하면 된다.

MimeBodyPart bodyPart = new MimeBodyPart();
DataSource ds = new ByteArrayDataSource(
    byteArray,
    contentType, filename);
bodyPart.setDataHandler( new DataHandler(ds));
bodyPart.setDisposition("attachment; filename=\"" + filename + "\"");
bodyPart.setFileName(filename);

MimeMultipart multipart = new MimeMultipart();
multipart.addBodyPart(bodyPart);

이 코드는 첨부할 파일을 MimeMultipart에 추가하는 것으로서 DataHandler와 DataSource를 사용하여 BodyPart의 내용을 지정하는 방법을 보여주고 있다. 위 코드는 다음과 같은 순서로 DataSource 데이터를 MimeBodyPart의 내용으로 지정하고 있다.

  1. DataSource 생성
  2. 생성된 DataSource를 사용하여 DataHandler 생성
  3. MimeBodyPart.setDataHandler() 메소드를 사용하여 DataSource로부터 데이터를 읽어오도록 지정.
그 외 MimeBodyPart.setDisposition() 메소드를 사용하여 현재 BodyPart가 첨부된 파일이라는 것을 지정하고 있으며, setFileName() 메소드를 사용하여 첨부된 파일의 이름을 지정하고 있다.

웹 기반의 메일 전송 2

이제 실제로 파일 첨부가 가능한 메일을 전송해주는 서블릿을 작성해보자. 먼저 서블릿에 메일 데이터를 전송해주는 폼부터 작성할 것이다. 폼은 mailform.jsp를 통해서 보여지며, mailform.jsp는 다음과 같다.

<html>
<head><title>메일 전송 폼</title>
</head>
<body>

<form action="http://localhost:8080/servlet/javacan.web.mail.WebSendMail"
      method="post"      enctype="multipart/form-data">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
   <td>보내는사람</td>
   <td><input type="text" name="from" size="20"></td>
</tr>
<tr>
   <td>반는사람</td>
   <td><input type="text" name="to" size="20"></td>
</tr>
<tr>
   <td>참조</td>
   <td><input type="text" name="cc" size="20"></td>
</tr>
<tr>
   <td>제목</td>
   <td><input type="text" name="subject" size="40"></td>
</tr>
<tr>
   <td>내용</td>
   <td><textarea name="body" rows="10" cols="40"></textarea></td>
</tr>
<tr>
   <td>첨부파일</td>
   <td><input type="file" name="attachment"></td>
</tr>
<tr>
   <td colspan="2"><input type="submit" value="전송"></td>
</tr>
</table>
</form>

</body>
</html>

mailform.jsp를 보면 <form> 태그의 enctype 속성의 값이 "multipart/form-data" 인 것을 알 수 있는데, 이렇게 함으로써 파일 데이터를 전송할 수 있게 된다. mailform.jsp를 실행해보면 다음 그림과 같은결과가 출력된다.


위 그림에서 [전송] 버튼을 누르면 실제로 데이터를 javacan.web.mail.WebSendMail 서블릿에 전송하며, WebSendMail 서블릿은 입력받은 데이터를 이용하여 메일을 전송한다. POSTE 방식을 사용하여 데이터를 전송하기 때문에 WebSendMail 서블릿의 doPost() 메소드가 실행된다. WebSendMail 서블릿의 doPost() 메소드는 다음과 같다.

public void doPost(HttpServletRequest request,
                   HttpServletResponse response)
                   throws IOException, ServletException {
    if(request.getContentType().startsWith("multipart/form-data")) {
        try {
            HashMap data = getMailData(request, response);
            sendMail(data);
            
            ServletContext sc = getServletContext();
            RequestDispatcher rd = sc.getRequestDispatcher("/sendedMail.jsp");
            rd.forward(request, response);
        } catch(MessagingException ex) {
            throw new ServletException(ex);
        }
    } else {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
    }
}

doPost() 메소드에서는 먼저 사용자가 전송한 데이터 타입이 "multipart/form-data" 인지를 검사한다. 만약 그렇다면, getMailData() 메소드를 사용하여 사용자가 폼을 통해서 입력한 메일 데이터를 읽어오고 이어서 sendMail() 메소드를 사용하여 메일을 전송한다. getMailData()는 "multipart/form-data" 타입으로 전송된 데이터로부터 사용자가 입력한 데이터를 추출해서 HashMap에 저장한다. HashMap에 저장할 때 사용되는 키 값은 폼의 name 속성의 값과 같다. 다음은 getMailData() 메소드이다.

private HashMap getMailData(HttpServletRequest request,
                            HttpServletResponse response)
        throws IOException, ServletException, MessagingException {
   String boundary = request.getHeader("Content-Type");
   int pos = boundary.indexOf('=');
   boundary = boundary.substring(pos + 1);
   boundary = "--" + boundary;
   ServletInputStream in = request.getInputStream();
   byte[] bytes = new byte[512];
   int state = 0;
   ByteArrayOutputStream buffer = new ByteArrayOutputStream();
   String name = null, value = null, 
          filename = null, contentType = null;
   HashMap mailData = new HashMap();
   
   int i = in.readLine(bytes,0,512);
   while(-1 != i) {
      String st = new String(bytes, 0, i);
      if(st.startsWith(boundary)) {
         state = 0;
         if(null != name) {
            if(value != null)
               // -2 to remove CR/LF
               mailData.put(name, value.substring(0, value.length() - 2));
            else if(buffer.size() > 2) {
               MimeBodyPart bodyPart = new MimeBodyPart();
               DataSource ds = new ByteArrayDataSource(
                  buffer.toByteArray(),
                  contentType, filename);
               bodyPart.setDataHandler(new DataHandler(ds));
               bodyPart.setDisposition(
                    "attachment; filename=\"" + filename + "\"");
               bodyPart.setFileName(filename);
               mailData.put(name, bodyPart);
            }
            name = null;
            value = null;
            filename = null;
            contentType = null;
            buffer = new ByteArrayOutputStream();
         }
      } else if(st.startsWith("Content-Disposition: form-data") && state == 0) {
         StringTokenizer tokenizer = new StringTokenizer(st,";=\"");
         while(tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            if(token.startsWith(" name")) {
               name = tokenizer.nextToken();
               state = 2;
            } else if(token.startsWith(" filename")) {
               filename = tokenizer.nextToken();
               StringTokenizer ftokenizer = new StringTokenizer(filename,"\\/:");
               filename = ftokenizer.nextToken();
               while(ftokenizer.hasMoreTokens())
                  filename = ftokenizer.nextToken();
               state = 1;
               break;
            }
         }
      } else if(st.startsWith("Content-Type") && state == 1) {
         pos = st.indexOf(":");
         // + 2 to remove the space
         // - 2 to remove CR/LF
         contentType = st.substring(pos + 2,st.length() - 2);
      } else if(st.equals("\r\n") && state == 1)
         state = 3;
      else if(st.equals("\r\n") && state == 2)
         state = 4;
      else if(state == 4)
         value = value == null ? st : value + st;
      else if(state == 3)
         buffer.write(bytes,0,i);
      i = in.readLine(bytes,0,512);
   }
   return mailData;
}

getMailData() 메소드는 사용자가 폼에 입력한 값을 HashMap인 mailData에 저장한다. 굵게 표시한 부분이 사용자가 전송한 파일 데이터를 읽어와 MimeBodyPart 객체에 저장하는 부분이다.

getMailData() 메소드를 사용하여 메일 데이터를 HashMap에 저장하면 이제 sendMail() 메소드가 실행된다. sendMail() 메소드는 실제로 MimeMessage 클래스를 이용하여 인터넷 이메일을 작성한 후 메일을 전송해주는 역할을 한다. sendMail() 메소드는 다음과 같다.

private void sendMail(HashMap mailData) throws MessagingException {
   System.setProperty("mail.smtp.host", "smpthost");
   Message msg = new MimeMessage(
                     Session.getDefaultInstance(
                     System.getProperties(),null));
   msg.setFrom(new InternetAddress((String)mailData.get("from")));
   InternetAddress[] tos = InternetAddress.parse((String)mailData.get("to"));
   msg.setRecipients(Message.RecipientType.TO,tos);
   if(mailData.get("cc") != null) {
      InternetAddress[] ccs = InternetAddress.parse((String)mailData.get("cc"));
      msg.setRecipients(Message.RecipientType.CC,ccs);
   }
   msg.setSubject((String)mailData.get("subject"));
   msg.setSentDate(new Date());
   if(null == mailData.get("attachment"))
      msg.setText((String)mailData.get("body"));
   else {
      BodyPart body = new MimeBodyPart();
      BodyPart attachment = (BodyPart)mailData.get("attachment");
      body.setText((String)mailData.get("body"));
      MimeMultipart multipart = new MimeMultipart();
      multipart.addBodyPart(body);
      multipart.addBodyPart(attachment);
      msg.setContent(multipart);
   }
   Transport.send(msg);
}

sendMail() 메소드는 2부에서 살펴본 내용과 크게 다르지 않다. 다른 점이 있다면 첨부 파일을 처리할 수 있다는 점이다. 위 코드에서 굵게 표시한 부분이 파일을 첨부하는 부분이다. MimeMultipart 클래스는 다중의 BodyPart를 가질 수 있으며, sendMail() 메소드에서는 getMailData()에서 읽어온 첨부 파일을 저장하고 있는 BodyPart를 addBodyPart() 메소드를 사용하여 MimeMultipart에 추가하고 있다. 위 코드에서 주의할 점은 System.setProperty("mail.smtp.host", "smtphost") 부분에서 "smtphost"의 값을 SMTP 서버에 알맞게 변경해주어야 한다는 점이다.

이제 mailform.jsp를 통해서 화면에 출력된 폼을 사용하여 첨부할 파일을 선택한 후 [전송] 버튼을 클릭해보자. 그러면 파일이 첨부된 이메일이 전송될 것이다.

결론

이번 글에서는 파일 첨부 기능을 구현해보았다. JAF API의 DataSource 인터페이스는 다양한 데이터를 표현할 수 있는 방법을 제공하고 있으며, 이를 이용하여 매우 손쉽게 파일 첨부 기능을 구현할 수 있었다.

지금까지 3부에 걸쳐서 Java Mail API의 기본적인 내용에서부터 응용까지의 내용을 살펴보았다. 이제 여러분은 어렵지 않게 웹 기반의 메일 전송 프로그램을 작성할 수 있을 것이며, 또한 여러분이 구현한 웹 사이트에 메일 전송 기능을 쉽게 추가할 수 있을 것이다.

관련링크:

+ Recent posts