오늘은 어디로 갈까...

메일 발송 최적화 본문

낙서

메일 발송 최적화

剛宇 2009. 4. 17. 12:10
 최적화라는 거창한 단어를 사용하긴 했지만, 사실 별 볼일 없다. 최적화라는것은 운영 환경, 데이터의 모델등 여러 요소를 복합적으로 고려해서 해야하는것이므로, 사실상 정답은 없다. 단지 최선의 방법만이 존재할뿐...
 현재 본인의 상황에서 PostMan의 병목현상은 메일 전송 부분에 있다. gmail을 이용해서 보내고 있는데, 메일 1개 전송하는데 보통 3-4초가 소요된다. 아무리 유희를 위해서 만들었다지만, 너무 심하지 않는가... 그래서 약간 수정을 가해서 빠르게 전송하는것처럼(?) 만들어보자. 사실, 구조 자체를 바꿔버리고 싶은 욕망이 꿈틀되지만, 이번만은 참도록 하겠다. ^^;
  만약 전용(?) 메일서버를 사용하다면, 이것보다는 빠를거 같지만, 가난한 개발자 & 게으른 개발자인 본인에게는 머나먼 얘기이므로, 없는셈치겠다.
 자, 그러면 어떻게 빠르게 전송할 수 있을까?
 사실은 빠르게 전송할 수 있는 방법이 없다. gmail한테 요청할 수도 없는 노릇이니말이다. 그래서 여기서는 빠르게 전송하기보다는 한번에 많이 전송해서, 빠르게 전송하는것처럼 보이는 방법을 사용하였다. 즉, 메일 전송 부분을 다중 쓰레드로 구현해서 파~파~팍 쏘게 만들었다.
 본인 환경에서는 메일 10개를 쓰레드 10개로 전송하는데 10초 걸렸다. 즉 메일 1개당 1초 정도 소요된것처럼 보이는것이다. 

 메일 전송을 쓰레드로 하기 위해서 MailSendWorker 클래스를 만들고, 기존 PostManJob을 약간 수정한 PostManFastJob 클래스를 만들자. (개발하는데 가장 어려운것은 작명이 아닐까 생각된다. ㅠㅠ)

- 기존 PostManJob 클래스에 메일전송하는 부분을 따로 분리하여 만든것이다. 결과값(?)을 받기 위해 Callable 인터페이스를 사용했다.
package kr.kangwoo.postman.core;

import java.util.Date;
import java.util.concurrent.Callable;

import kr.kangwoo.postman.domain.Mail;
import kr.kangwoo.postman.service.MailSendManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MailSendWorker implements Callable<Mail> {

	private Logger logger = LoggerFactory.getLogger(getClass());
	
	private MailSendManager mailSendManager;
	
	private Mail mail;
	private String subject;
	private String content;

	public MailSendWorker(MailSendManager mailSendManager, Mail mail, String subject, String content) {
		this.mailSendManager = mailSendManager;

		this.mail = mail;

		this.subject = subject;
		this.content = content;
	}

	public Mail call() throws Exception {
		try {
			mailSendManager.send(mail.getToAddress(), mail.getToName(), mail.getFromAddress(), mail.getFromName(), subject, content);
			mail.setStatusCode(MailStatusCode.SEND_OK);
			mail.setSentDate(new Date());
		} catch (MailException e) {
			mail.setStatusCode(e.getStatusCode());
			logger.warn("메일 발송 중 에러가 발생했습니다.", e);
		} catch (MessageException e) {
			mail.setStatusCode(MailStatusCode.UNKOWN_ERROR);
			logger.warn("메일 발송 중 에러가 발생했습니다.", e);
		} catch (Exception e) {
			mail.setStatusCode(MailStatusCode.UNKOWN_ERROR);
			logger.warn("메일 발송 중 에러가 발생했습니다.", e);
		}
		return mail;
	}
}

- JDK 1.5 이상에서 제공해는 Executors.newFixedThreadPool()을 이용해서 쓰레드풀을 만든다음, 처리할 작업을 실행하였다. (http://blog.kangwoo.kr/58)

package kr.kangwoo.postman.core;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import kr.kangwoo.postman.domain.Mail;
import kr.kangwoo.postman.service.MailManager;
import kr.kangwoo.postman.service.MailSendManager;
import kr.kangwoo.postman.service.MailTemplateManager;
import kr.kangwoo.util.StopWatch;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PostManFastJob {

	private Logger logger = LoggerFactory.getLogger(PostManFastJob.class);
	
	private String daemonName = getClass().getName();
	
	private MailManager mailManager;
	private MailTemplateManager mailTemplateManager;
	private MailSendManager mailSendManager;
	
	private int threadPoolSize = 10;
	private int listSize = 100;	// 데이터 베이스에서 한번에 가져올 목록 크기
	private int futureMaxSize = 1000; //ThreadPool에 적재할 작업들의 최대크기
	
	public void setDaemonName(String daemonName) {
		this.daemonName = daemonName;
	}

	public void setMailManager(MailManager mailManager) {
		this.mailManager = mailManager;
	}

	public void setMailTemplateManager(MailTemplateManager mailTemplateManager) {
		this.mailTemplateManager = mailTemplateManager;
	}

	public void setMailSendManager(MailSendManager mailSendManager) {
		this.mailSendManager = mailSendManager;
	}
	
	public void setThreadPoolSize(int threadPoolSize) {
		this.threadPoolSize = threadPoolSize;
	}

	public void setListSize(int listSize) {
		this.listSize = listSize;
	}

	public void setFutureMaxSize(int futureMaxSize) {
		this.futureMaxSize = futureMaxSize;
	}

	public void run() {
		
		StopWatch stopWath = new StopWatch();
		ExecutorService executorService = null;
		try {
			logger.info("메일 템플릿 정보 적재 시작");
			mailTemplateManager.reload();
			logger.info("메일 템플릿 정보 적재 완료");
			
			if (logger.isDebugEnabled()) {
				logger.debug("메일 템플릿 적재 소요 시간 -> " + stopWath.getElapsedTimeString());	
			}
			
			List<Mail> sendList = null;
			List<Future<Mail>> futureList = new ArrayList<Future<Mail>>();
			while (!Thread.currentThread().isInterrupted() 
					&& ((sendList = mailManager.getSendList(daemonName, listSize)) != null) 
					&& sendList.size() > 0) {
				if (logger.isDebugEnabled()) {
					logger.debug("{}개의 메일을 가져왔습니다.({})", sendList != null ? sendList.size() : 0, stopWath.getElapsedTimeString());	
				}
				
				if (executorService == null) {
					executorService = Executors.newFixedThreadPool(threadPoolSize);
				}

				String subject = null;
				String content = null;
				for (Mail mail : sendList) {
					subject = mailTemplateManager.getSubject(mail);
					content = mailTemplateManager.getContent(mail);
					mail.setStatusCode(MailStatusCode.SEND_READY);
					
					futureList.add(executorService.submit(new MailSendWorker(mailSendManager, mail, subject, content)));
				}
				
				// 완료된 작업 정리하기.
				int futureListSize = futureList.size();
				List<Future<Mail>> doneList = new ArrayList<Future<Mail>>();
				Date updatedDate = new Date();
				for (Future<Mail> future : futureList) {
					if (future.isDone()) {
						doneList.add(future);
						
						try {
							Mail mail = future.get();
							mail.setUpdatedBy(daemonName);
							mail.setUpdatedDate(updatedDate);
							mailManager.updateMail(mail);
						} catch (Exception e) {
							logger.warn("메일 발송 중 에러가 발생했습니다.", e);
						}
					}
				}
				if (doneList.size() > 0) {
					for (Future<Mail> future : doneList) {
						futureList.remove(future);
					}
					if (logger.isDebugEnabled()) {
						logger.debug("FutureList ({}->{})", futureListSize, futureList.size());	
					}
					futureListSize = futureList.size();
				}
				
				if (futureListSize > futureMaxSize) {
					if (logger.isDebugEnabled()) {
						logger.debug("FutureList가 최대크기를 초과하여 정리합니다.({}/{})", futureList.size(), futureMaxSize);	
					}
					for (Future<Mail> future : futureList) {
						try {
							Mail mail = future.get();
							mail.setUpdatedBy(daemonName);
							mail.setUpdatedDate(updatedDate);
							mailManager.updateMail(mail);
						} catch (Exception e) {
							logger.warn("메일 발송 중 에러가 발생했습니다.", e);
						}
					}
					futureList.clear();
					if (logger.isDebugEnabled()) {
						logger.debug("FutureList ({}->{})", futureListSize, futureList.size());	
					}
				}
			}
			
			// 작업 완료될때까지 대기
			Date updatedDate = new Date();
			for (Future<Mail> future : futureList) {
				try {
					Mail mail = future.get();
					mail.setUpdatedBy(daemonName);
					mail.setUpdatedDate(updatedDate);
					mailManager.updateMail(mail);
				} catch (Exception e) {
					logger.warn("메일 발송 중 에러가 발생했습니다.", e);
				}
			}
			if (logger.isDebugEnabled()) {
				logger.debug("메일 발송 소요 시간 -> " + stopWath.getElapsedTimeString());	
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		} finally {
			if (executorService != null) {
				executorService.shutdown();
			}
		}
	}
}

- 새로만든 Job을 사용하게 post-man-scheduler.xml을 수정해주자
	<bean id="postManJob"
		class="kr.kangwoo.postman.core.PostManFastJob">
		<property name="mailManager" ref="mailManager" />
		<property name="mailTemplateManager" ref="mailTemplateManager" />
		<property name="mailSendManager" ref="mailSendManager" />
	</bean>




* 넋두리
 - 최적화란 단어를 사용했음에도 불구하고, 코드 최적화는 전혀 안되어 있지만, 중요한 것은 병목이 생기는 지점을 찾아내서 해결하는것이기에 넘어가자.
 - 현재 소스에는 중지(?) 시키기 위한 코드가 구현되어 있지 않는데, 한번 해보시길 ^^;
 - 사실을 고백하자면 아래처럼, 연결을 맺은 다음 메일을 전송해주면 훨씬 빠르다.. ^^;;;;;;
   Transport transport;
		transport = getTransport(getSession());
		transport.connect(getHost(), getPort(), getUsername(), getPassword());
		for (MimeMessage mimeMessage : mimeMessages) {
			if (mimeMessage.getSentDate() == null) {
				mimeMessage.setSentDate(new Date());
			}

			String messageId = mimeMessage.getMessageID();
			mimeMessage.saveChanges();
			if (messageId != null) {
				mimeMessage.setHeader("Message-ID", messageId);
			}
			transport.sendMessage(mimeMessage, mimeMessage.getAllRecipients());
		}
		transport.close();