오늘은 어디로 갈까...

스케줄러를 이용한 메일 발송 본문

낙서

스케줄러를 이용한 메일 발송

剛宇 2009. 4. 16. 12:49
 봄을 재촉하는 비가 내리고 있다. 이런날을 커다란 창문이 있는 곳에 앉아, 하염없이 내리는 비를 바라보는게 즐거움일텐데, 어쩔 수 없이 모니터만 뚫어지게 바라보고 있다.
 봄하면 생각나는게 spring~ 그렇다. PostMan을 spring화(?) 하자. 스프링을 사용하는김에 Persistant layer도 제대로 구현해보도록 하겠다. 여기서는 iBatis를 사용하겠다. (Spring Batch를 사용할까도 생각해봤지만, 구조를 설명하는게 좌절인거 같아서 간단한(?) spring만 사용하겠다.)

1. 관련 라이브러리
 - 갑자기 라이브러리가 많이 필요해졌다. 대충 정리하면 아래와 같은데, 알아서 구해보시길. ^^;
 - spring 2.5.6 (http://www.springsource.org/download)
 - iBatis 2.3.4 (http://ibatis.apache.org/javadownloads.cgi)
 - commons-dbcp 1.2.2 (http://commons.apache.org/dbcp/downloads.html)
 - commons-pool 1.4 (http://commons.apache.org/pool/downloads.html)
 - commons-collections(http://commons.apache.org/downloads/download_collections.cgi)
 - JDBC Driver (http://www.oracle.com/technology/software/tech/java/sqlj_jdbc/index.html)



2. Quartz Scheduler(http://www.opensymphony.com/quartz/
 - 원래 의도는 Timer를 사용해서 간단한 스케줄러 기능을 구현하려고 했지만, 멋(?)을 위해 Quartz Scheduler를 사용하도록 하겠다.
 - Quartz는 다양한 시간간격에 의한 수행과 사용자 정의에 의한 수행을 모두 구현할 수 있다.
 - spring에서는 quartz 연동 클래스를 제공해 준다. 
 - 발동(?) 클래스
   + org.springframework.scheduling.quartz.SimpleTriggerBean
    : 반복 시간을 지정할수 있다.
	<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
		<property name="jobDetail" ref="jobDetail" />
		<property name="startDelay" value="30000" />
		<property name="repeatInterval" value="30000" />
	</bean>

   + org.springframework.scheduling.quartz.CronTriggerBean
     : 다양한 시간간격을 지정할수 있다. (매일, 매주, 심시어 요일별로 지정할 수 있다)
       표현식 -> http://quartz.sourceforge.net/javadoc/org/quartz/CronTrigger.html
	<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
		<property name="jobDetail" ref="jobDetail" />
		<property name="cronExpression" value="0/30 * * * * ?" />
	</bean>

  - 실행시킬 작업(Job)을 정의하는 클래스
    + org.springframework.scheduling.quartz.JobDetailBean
      : org.springframework.scheduling.quartz.QuartzJobBean을 상속받아 executeInternal(JobExecutionContext) 메소드를 구현하면 된다.
	<bean name="jobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">
		<property name="jobClass" value="kr.kangwoo.postman.XxxJob" />
		<property name="jobDataAsMap">
			<map>
				<entry key="timeout" value="30000" />
			</map>
		</property>
	</bean>

    + org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean
      : 지정한 Bean의 대상 메소드를 실행시켜준다.
	<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
		<property name="targetObject" ref="postManJob" />
		<property name="targetMethod" value="run" />
		<property name="concurrent" value="false" /> <!-- job이 동시에 여러개 실행될지 여부를 지정하는것이다. -->
	</bean>
  
   - 스케줄러를 지정해 주면, spring이 초기화되면서 자동으로 작동을 한다. triggers란 이름의 속성에 작동시킬 Trigger들을 넣어주면 된다.
	<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
		<property name="triggers">
			<list>
				<ref local="simpleTrigger"/>
				<ref local="cronTrigger"/>
			</list>
		</property>
	</bean>


3. DAO 수정하기
 - 기존 SimpleMailDao랑, SimpleMailTemplateDao는 과감히 날려버리고 iBatis를 이용해서 새로이 만들겠다.
 - MailDao의 getMailList()메소드와 MailTemplateDao의 getMailTemplateList()가 변경되었으니 유의바란다.
 - 여기 샘플은 insert, update, delete, select 모두 구현되어 있는데, 코드 생성기로 구현한거라 그런것일뿐, 실제적으로는 아주 조금만(?) 필요하다.
 - 지금에 와서야 고백(?)을 하지만 오라클의 자릿수를 지정하지 않은 Number형은 자바의 Long형의 범위를 벗어나서 BigDecimal이 맞는것이지만, 귀찮은 관계로 Long형을 이용했다. ^^;
package kr.kangwoo.postman.repository;

import java.util.List;
import java.util.Map;

import kr.kangwoo.postman.domain.Mail;


public interface MailDao {

    void insert(Mail mail);

    int update(Mail mail);

    int updateSelective(Mail mail);

    int delete(Long mailNo);

    Mail getMail(Long mailNo);

    List getList(Map parameterMap);

    int count(Map parameterMap);

}

package kr.kangwoo.postman.repository;

import java.util.List;
import java.util.Map;

import kr.kangwoo.postman.domain.MailTemplate;


public interface MailTemplateDao {

    void insert(MailTemplate mailTemplate);

    int update(MailTemplate mailTemplate);

    int updateSelective(MailTemplate mailTemplate);

    int delete(String templateId);

    MailTemplate getMailTemplate(String templateId);

    List getList(Map parameterMap);

    int count(Map parameterMap);

}

package kr.kangwoo.postman.repository.ibatis;

import java.util.List;
import java.util.Map;

import kr.kangwoo.postman.domain.Mail;
import kr.kangwoo.postman.repository.MailDao;

import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;


public class MailDaoSqlMap extends SqlMapClientDaoSupport implements MailDao {

    public void insert(Mail mail) {
        getSqlMapClientTemplate().insert("kr.kangwoo.postman.repository.MailDao.insert", mail);
    }

    public int update(Mail mail) {
        int rows = getSqlMapClientTemplate().update("kr.kangwoo.postman.repository.MailDao.update", mail);
        return rows;
    }

    public int updateSelective(Mail mail) {
        int rows = getSqlMapClientTemplate().update("kr.kangwoo.postman.repository.MailDao.updateSelective", mail);
        return rows;
    }

    public int delete(Long mailNo) {
        Mail key = new Mail();
        key.setMailNo(mailNo);
        int rows = getSqlMapClientTemplate().delete("kr.kangwoo.postman.repository.MailDao.delete", key);
        return rows;
    }

    public Mail getMail(Long mailNo) {
        Mail key = new Mail();
        key.setMailNo(mailNo);
        Mail record = (Mail) getSqlMapClientTemplate().queryForObject("kr.kangwoo.postman.repository.MailDao.getMail", key);
        return record;
    }

    @SuppressWarnings("unchecked")
    public List<Mail> getList(Map<String, Object> parameterMap) {
        List<Mail> records = getSqlMapClientTemplate().queryForList("kr.kangwoo.postman.repository.MailDao.getList", parameterMap);
        return records;
    }

    public int count(Map<String, Object> parameterMap) {
        Integer count = (Integer)  getSqlMapClientTemplate().queryForObject("kr.kangwoo.postman.repository.MailDao.count", parameterMap);
        return count;
    }

}

package kr.kangwoo.postman.repository.ibatis;

import java.util.List;
import java.util.Map;

import kr.kangwoo.postman.domain.MailTemplate;
import kr.kangwoo.postman.repository.MailTemplateDao;

import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;


public class MailTemplateDaoSqlMap extends SqlMapClientDaoSupport implements MailTemplateDao {

    public void insert(MailTemplate mailTemplate) {
        getSqlMapClientTemplate().insert("kr.kangwoo.postman.repository.MailTemplateDao.insert", mailTemplate);
    }

    public int update(MailTemplate mailTemplate) {
        int rows = getSqlMapClientTemplate().update("kr.kangwoo.postman.repository.MailTemplateDao.update", mailTemplate);
        return rows;
    }

    public int updateSelective(MailTemplate mailTemplate) {
        int rows = getSqlMapClientTemplate().update("kr.kangwoo.postman.repository.MailTemplateDao.updateSelective", mailTemplate);
        return rows;
    }

    public int delete(String templateId) {
        MailTemplate key = new MailTemplate();
        key.setTemplateId(templateId);
        int rows = getSqlMapClientTemplate().delete("kr.kangwoo.postman.repository.MailTemplateDao.delete", key);
        return rows;
    }

    public MailTemplate getMailTemplate(String templateId) {
        MailTemplate key = new MailTemplate();
        key.setTemplateId(templateId);
        MailTemplate record = (MailTemplate) getSqlMapClientTemplate().queryForObject("kr.kangwoo.postman.repository.MailTemplateDao.getMailTemplate", key);
        return record;
    }

    @SuppressWarnings("unchecked")
    public List<MailTemplate> getList(Map<String, Object> parameterMap) {
        List<MailTemplate> records = getSqlMapClientTemplate().queryForList("kr.kangwoo.postman.repository.MailTemplateDao.getList", parameterMap);
        return records;
    }

    public int count(Map<String, Object> parameterMap) {
        Integer count = (Integer)  getSqlMapClientTemplate().queryForObject("kr.kangwoo.postman.repository.MailTemplateDao.count", parameterMap);
        return count;
    }

}




4. SqlMap  만들기
- iBatis에서 사용할 sql을 만들어보자.
- 실제적으로 사용할 부분은 update 부분과 getList 부분이다. 상태코드에 맞는 데이터를 가져오고, 생태코드를 갱신하면 되는것이다. iBatis와 별로 안친하신분들을 JDBC를 직접 이용해서 구현하시면 되겠다.
 - 원래 의도는 select for update를 이용해서 동일한 데몬이 동시에 여러개 실행될때 데이터를 어떻게 가지고 오는지도 설명해보려고 했으나, 오라클 종속(?)적이고, postman의 성격과는 별로 안 맞는거 같아서 넘어가도록 한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd" >
<sqlMap namespace="kr.kangwoo.postman.repository.MailDao" >
    
    <resultMap id="Mail" class="kr.kangwoo.postman.domain.Mail" >
        <result column="MAIL_NO" property="mailNo" jdbcType="NUMBER" />
        <result column="TEMPLATE_ID" property="templateId" jdbcType="CHAR" />
        <result column="STATUS_CODE" property="statusCode" jdbcType="CHAR" />
        <result column="TO_ADDRESS" property="toAddress" jdbcType="VARCHAR2" />
        <result column="TO_NAME" property="toName" jdbcType="VARCHAR2" />
        <result column="FROM_ADDRESS" property="fromAddress" jdbcType="VARCHAR2" />
        <result column="FROM_NAME" property="fromName" jdbcType="VARCHAR2" />
        <result column="SUBJECT_DATA" property="subjectData" jdbcType="VARCHAR2" />
        <result column="COTENT_DATA" property="cotentData" jdbcType="VARCHAR2" />
        <result column="SENT_BY" property="sentBy" jdbcType="VARCHAR2" />
        <result column="SENT_DATE" property="sentDate" jdbcType="DATETIME" />
        <result column="CREATED_BY" property="createdBy" jdbcType="VARCHAR2" />
        <result column="CREATION_DATE" property="creationDate" jdbcType="DATETIME" />
        <result column="UPDATED_BY" property="updatedBy" jdbcType="VARCHAR2" />
        <result column="UPDATED_DATE" property="updatedDate" jdbcType="DATETIME" />
    </resultMap>
    
    <insert id="insert" parameterClass="kr.kangwoo.postman.domain.Mail" >
        insert into MAIL (MAIL_NO, TEMPLATE_ID, STATUS_CODE, TO_ADDRESS, TO_NAME, FROM_ADDRESS, FROM_NAME,
        SUBJECT_DATA, COTENT_DATA, SENT_BY, SENT_DATE, CREATED_BY, CREATION_DATE, UPDATED_BY, UPDATED_DATE)
        values (#mailNo:NUMBER#, #templateId:CHAR#, #statusCode:CHAR#, #toAddress:VARCHAR2#,
        #toName:VARCHAR2#, #fromAddress:VARCHAR2#, #fromName:VARCHAR2#, #subjectData:VARCHAR2#,
        #cotentData:VARCHAR2#, #sentBy:VARCHAR2#, #sentDate:DATETIME#, #createdBy:VARCHAR2#,
        #creationDate:DATETIME#, #updatedBy:VARCHAR2#, #updatedDate:DATETIME#)
    </insert>
    
    <update id="update" parameterClass="kr.kangwoo.postman.domain.Mail" >
        update MAIL
        set TEMPLATE_ID = #templateId:CHAR#, 
            STATUS_CODE = #statusCode:CHAR#, 
            TO_ADDRESS = #toAddress:VARCHAR2#, 
            TO_NAME = #toName:VARCHAR2#, 
            FROM_ADDRESS = #fromAddress:VARCHAR2#, 
            FROM_NAME = #fromName:VARCHAR2#, 
            SUBJECT_DATA = #subjectData:VARCHAR2#, 
            COTENT_DATA = #cotentData:VARCHAR2#, 
            SENT_BY = #sentBy:VARCHAR2#, 
            SENT_DATE = #sentDate:DATETIME#, 
            CREATED_BY = #createdBy:VARCHAR2#, 
            CREATION_DATE = #creationDate:DATETIME#, 
            UPDATED_BY = #updatedBy:VARCHAR2#, 
            UPDATED_DATE = #updatedDate:DATETIME#
        where MAIL_NO = #mailNo:NUMBER#
    </update>
    
    <update id="updateSelective" parameterClass="kr.kangwoo.postman.domain.Mail" >
        update MAIL
        <dynamic prepend="set" >
            <isNotNull prepend="," property="templateId" >
                    TEMPLATE_ID = #templateId:CHAR#
            </isNotNull>
            <isNotNull prepend="," property="statusCode" >
                    STATUS_CODE = #statusCode:CHAR#
            </isNotNull>
            <isNotNull prepend="," property="toAddress" >
                    TO_ADDRESS = #toAddress:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="toName" >
                    TO_NAME = #toName:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="fromAddress" >
                    FROM_ADDRESS = #fromAddress:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="fromName" >
                    FROM_NAME = #fromName:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="subjectData" >
                    SUBJECT_DATA = #subjectData:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="cotentData" >
                    COTENT_DATA = #cotentData:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="sentBy" >
                    SENT_BY = #sentBy:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="sentDate" >
                    SENT_DATE = #sentDate:DATETIME#
            </isNotNull>
            <isNotNull prepend="," property="createdBy" >
                    CREATED_BY = #createdBy:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="creationDate" >
                    CREATION_DATE = #creationDate:DATETIME#
            </isNotNull>
            <isNotNull prepend="," property="updatedBy" >
                    UPDATED_BY = #updatedBy:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="updatedDate" >
                    UPDATED_DATE = #updatedDate:DATETIME#
            </isNotNull>
        </dynamic>
        where MAIL_NO = #mailNo:NUMBER#
    </update>
    
    <delete id="delete" parameterClass="kr.kangwoo.postman.domain.Mail" >
        delete from MAIL
        where MAIL_NO = #mailNo:NUMBER#
    </delete>
    
    <select id="getMail" parameterClass="kr.kangwoo.postman.domain.Mail" resultMap="Mail" >
        select MAIL_NO, TEMPLATE_ID, STATUS_CODE, TO_ADDRESS, TO_NAME, FROM_ADDRESS, FROM_NAME,
        SUBJECT_DATA, COTENT_DATA, SENT_BY, SENT_DATE, CREATED_BY, CREATION_DATE, UPDATED_BY, UPDATED_DATE
        from MAIL
        where MAIL_NO = #mailNo:NUMBER#
    </select>
    
    <select id="getList" parameterClass="java.util.Map" resultMap="Mail" >
        select MAIL_NO, TEMPLATE_ID, STATUS_CODE, TO_ADDRESS, TO_NAME, FROM_ADDRESS, FROM_NAME,
        SUBJECT_DATA, COTENT_DATA, SENT_BY, SENT_DATE, CREATED_BY, CREATION_DATE, UPDATED_BY, UPDATED_DATE
        from MAIL
        <dynamic prepend="where" >
            <isNotNull prepend="and" property="statusCode" >
                    STATUS_CODE = #statusCode:CHAR#
            </isNotNull>
            <isNotNull prepend="and" property="listSize" >
                    ROWNUM <= #listSize:NUMBER#
            </isNotNull>
        </dynamic>
        <dynamic prepend="order by" >
            <isNotNull property="orderBy" >
                    $orderBy$
            </isNotNull>
        </dynamic>
    </select>
     
    <select id="count" parameterClass="java.util.Map" resultClass="java.lang.Integer" >
        select count(*) cnt from MAIL
    </select>
</sqlMap>

- getList만 사용한다. USE_FLAG가 "Y"인 놈만 가져오게 사용할것이다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd" >
<sqlMap namespace="kr.kangwoo.postman.repository.MailTemplateDao" >
    
    <resultMap id="MailTemplate" class="kr.kangwoo.postman.domain.MailTemplate" >
        <result column="TEMPLATE_ID" property="templateId" jdbcType="CHAR" />
        <result column="TEMPLATE_DIR" property="templateDir" jdbcType="VARCHAR2" />
        <result column="TEMPLATE_FILENAME" property="templateFilename" jdbcType="VARCHAR2" />
        <result column="MAIL_SUBJECT" property="mailSubject" jdbcType="VARCHAR2" />
        <result column="DATA_MODEL_TYPE" property="dataModelType" jdbcType="CHAR" />
        <result column="USE_FLAG" property="useFlag" jdbcType="CHAR" />
        <result column="CREATED_BY" property="createdBy" jdbcType="VARCHAR2" />
        <result column="CREATION_DATE" property="creationDate" jdbcType="DATETIME" />
        <result column="UPDATED_BY" property="updatedBy" jdbcType="VARCHAR2" />
        <result column="UPDATED_DATE" property="updatedDate" jdbcType="DATETIME" />
    </resultMap>
    
    <insert id="insert" parameterClass="kr.kangwoo.postman.domain.MailTemplate" >
        insert into MAIL_TEMPLATE (TEMPLATE_ID, TEMPLATE_DIR, TEMPLATE_FILENAME, MAIL_SUBJECT,
        DATA_MODEL_TYPE, USE_FLAG, CREATED_BY, CREATION_DATE, UPDATED_BY, UPDATED_DATE)
        values (#templateId:CHAR#, #templateDir:VARCHAR2#, #templateFilename:VARCHAR2#,
        #mailSubject:VARCHAR2#, #dataModelType:CHAR#, #useFlag:CHAR#, #createdBy:VARCHAR2#,
        #creationDate:DATETIME#, #updatedBy:VARCHAR2#, #updatedDate:DATETIME#)
    </insert>
    
    <update id="update" parameterClass="kr.kangwoo.postman.domain.MailTemplate" >
        update MAIL_TEMPLATE
        set TEMPLATE_DIR = #templateDir:VARCHAR2#, 
            TEMPLATE_FILENAME = #templateFilename:VARCHAR2#, 
            MAIL_SUBJECT = #mailSubject:VARCHAR2#, 
            DATA_MODEL_TYPE = #dataModelType:CHAR#, 
            USE_FLAG = #useFlag:CHAR#, 
            CREATED_BY = #createdBy:VARCHAR2#, 
            CREATION_DATE = #creationDate:DATETIME#, 
            UPDATED_BY = #updatedBy:VARCHAR2#, 
            UPDATED_DATE = #updatedDate:DATETIME#
        where TEMPLATE_ID = #templateId:CHAR#
    </update>
    
    <update id="updateSelective" parameterClass="kr.kangwoo.postman.domain.MailTemplate" >
        update MAIL_TEMPLATE
        <dynamic prepend="set" >
            <isNotNull prepend="," property="templateDir" >
                    TEMPLATE_DIR = #templateDir:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="templateFilename" >
                    TEMPLATE_FILENAME = #templateFilename:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="mailSubject" >
                    MAIL_SUBJECT = #mailSubject:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="dataModelType" >
                    DATA_MODEL_TYPE = #dataModelType:CHAR#
            </isNotNull>
            <isNotNull prepend="," property="useFlag" >
                    USE_FLAG = #useFlag:CHAR#
            </isNotNull>
            <isNotNull prepend="," property="createdBy" >
                    CREATED_BY = #createdBy:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="creationDate" >
                    CREATION_DATE = #creationDate:DATETIME#
            </isNotNull>
            <isNotNull prepend="," property="updatedBy" >
                    UPDATED_BY = #updatedBy:VARCHAR2#
            </isNotNull>
            <isNotNull prepend="," property="updatedDate" >
                    UPDATED_DATE = #updatedDate:DATETIME#
            </isNotNull>
        </dynamic>
        where TEMPLATE_ID = #templateId:CHAR#
    </update>
    
    <delete id="delete" parameterClass="kr.kangwoo.postman.domain.MailTemplate" >
        delete from MAIL_TEMPLATE
        where TEMPLATE_ID = #templateId:CHAR#
    </delete>
    
    <select id="getMailTemplate" parameterClass="kr.kangwoo.postman.domain.MailTemplate" resultMap="MailTemplate" >
        select TEMPLATE_ID, TEMPLATE_DIR, TEMPLATE_FILENAME, MAIL_SUBJECT, DATA_MODEL_TYPE, USE_FLAG,
        CREATED_BY, CREATION_DATE, UPDATED_BY, UPDATED_DATE
        from MAIL_TEMPLATE
        where TEMPLATE_ID = #templateId:CHAR#
    </select>
    
    <select id="getList" parameterClass="java.util.Map" resultMap="MailTemplate" >
        select TEMPLATE_ID, TEMPLATE_DIR, TEMPLATE_FILENAME, MAIL_SUBJECT, DATA_MODEL_TYPE, USE_FLAG,
        CREATED_BY, CREATION_DATE, UPDATED_BY, UPDATED_DATE
        from MAIL_TEMPLATE
        <dynamic prepend="where" >
            <isNotNull prepend="," property="useFlag" >
                    USE_FLAG = #useFlag:CHAR#
            </isNotNull>
        </dynamic>
    </select>
    
    <select id="count" parameterClass="java.util.Map" resultClass="java.lang.Integer" >
        select count(*) cnt from MAIL_TEMPLATE
    </select>
</sqlMap>


5. Service 수정하기
- DAO가 수정된 관계로 Manager 클래스들도 변경을 하도록 하겠다.
- DAO에서 데이터를 조회할때, 파라메터를 넘겨주는 부분이 추가되었고, update 하는 부분이 변경되었다.
package kr.kangwoo.postman.service;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import kr.kangwoo.postman.core.MailStatusCode;
import kr.kangwoo.postman.domain.Mail;
import kr.kangwoo.postman.repository.MailDao;

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

public class SimpleMailManager implements MailManager {

	private Logger logger = LoggerFactory.getLogger(getClass());
	
	private MailDao mailDao;

	public void setMailDao(MailDao mailDao) {
		this.mailDao = mailDao;
	}

	public List<Mail> getSendList(String daemonName) {
		Map<String, Object> parameterMap = new HashMap<String, Object>();
		parameterMap.put("statusCode", MailStatusCode.CREATED);
		parameterMap.put("listSize", 2);
		parameterMap.put("orderBy", "MAIL_NO");
		List<Mail> list = mailDao.getList(parameterMap);
		if (list != null) {
			// 상태값 변경
			Date updatedDate = new Date();
			for (Mail mail : list) {
				mail.setStatusCode(MailStatusCode.ACCEPTED);
				mail.setSentBy(daemonName);
				mail.setUpdatedBy(daemonName);
				mail.setUpdatedDate(updatedDate);
				
				Mail uMail = new Mail();
				uMail.setMailNo(mail.getMailNo());
				uMail.setStatusCode(mail.getStatusCode());
				uMail.setSentBy(mail.getSentBy());
				uMail.setUpdatedBy(mail.getUpdatedBy());
				uMail.setUpdatedDate(mail.getUpdatedDate());
				
				mailDao.updateSelective(uMail);
				
				uMail = null;
			}
		}
		return list;
	}

	public int updateMail(Mail mail) {
		return mailDao.updateSelective(mail);
	}

}

- DAO에서 데이터를 조회할때, 파라메터를 넘겨주는 부분이 추가되었다.
package kr.kangwoo.postman.service;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import kr.kangwoo.postman.core.MailException;
import kr.kangwoo.postman.core.MailStatusCode;
import kr.kangwoo.postman.core.MessageException;
import kr.kangwoo.postman.domain.Mail;
import kr.kangwoo.postman.domain.MailTemplate;
import kr.kangwoo.postman.repository.MailTemplateDao;
import kr.kangwoo.util.MessageUtils;
import kr.kangwoo.util.StrTokenizer;
import kr.kangwoo.util.StringUtils;

import org.xml.sax.InputSource;

import freemarker.cache.FileTemplateLoader;
import freemarker.cache.MultiTemplateLoader;
import freemarker.cache.TemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

public class FreemarkerMailTemplateManager implements MailTemplateManager {

	private MailTemplateDao mailTemplateDao;
	
	private Map<String, MailTemplate> mailTemplateMap;
	private Configuration configuration;
	
	public FreemarkerMailTemplateManager() {
	}
	
	public void setMailTemplateDao(MailTemplateDao mailTemplateDao) {
		this.mailTemplateDao = mailTemplateDao;
	}
	
	public List<MailTemplate> getMailTemplateList() {
		Map<String, Object> paramMap = new HashMap<String, Object>(2);
		paramMap.put("useFlag", "Y");
		return mailTemplateDao.getList(paramMap);
	}

	public void reload() {
		try {
			mailTemplateMap = new HashMap<String, MailTemplate>();
			configuration = new Configuration();
			
			List<MailTemplate> templateList = getMailTemplateList();
			if (templateList != null) {
				List<TemplateLoader> loaderList = new ArrayList<TemplateLoader>();
				Set<String> templateDirs = new HashSet<String>();
				String dir = null;
				for (MailTemplate mailTemplate : templateList) {
					dir = mailTemplate.getTemplateDir();
					if (StringUtils.isNotBlank(dir)) {
						mailTemplateMap.put(mailTemplate.getTemplateId(), mailTemplate);
						if (!templateDirs.contains(dir)) {
							// 중복 디렉토리 제거
							loaderList.add(new FileTemplateLoader(new File(dir)));
							templateDirs.add(dir);						
						}
					} else {
						throw new MessageException(MessageUtils.format("{0} 템플릿의 경로가 지정되어 있지 않습니다.", mailTemplate.getTemplateId()));
					}
				}
				TemplateLoader[] loaders = new TemplateLoader[loaderList.size()];
				MultiTemplateLoader multiTemplateLoader = new MultiTemplateLoader(loaderList.toArray(loaders));
				configuration.setTemplateLoader(multiTemplateLoader);
			}
		} catch (MessageException e) {
			throw e;
		} catch (Exception e) {
			throw new MessageException("메일 템플릿 정보를 가져오는 중 문제가 발생했습니다.", e);
		}
	}
	
	public String getSubject(Mail mail) throws MailException {
		// TODO 제목도 파라메터를 가져올 수 있게 수정할것. 
		MailTemplate mailTemplate = mailTemplateMap.get(mail.getTemplateId());
		if (mailTemplate == null) {
			throw new MailException(MailStatusCode.TEMPLATE_INFO_NOT_FOUND, MessageUtils.format("메일템플릿 정보가 존재하지 않습니다. (TEMPLATE_ID={0})", mail.getTemplateId()));
		}
		return mailTemplate.getMailSubject();
	}
	
	public String getContent(Mail mail) throws MailException {
		MailTemplate mailTemplate = mailTemplateMap.get(mail.getTemplateId());
		if (mailTemplate == null) {
			throw new MailException(MailStatusCode.TEMPLATE_INFO_NOT_FOUND, MessageUtils.format("메일템플릿 정보가 존재하지 않습니다. (TEMPLATE_ID={0})", mail.getTemplateId()));
		}
		Template template;
		try {
			template = configuration.getTemplate(mailTemplate.getTemplateFilename());
		} catch (FileNotFoundException e) {
			throw new MailException(MailStatusCode.TEMPLATE_FILE_NOT_FOUND, MessageUtils.format("템플릿 파일을 가져올 수 없습니다.(TEMPLATE_ID={0}, TEMPLATE_DIR={1}, TEMPLATE_FILE={2})", mailTemplate.getTemplateId(), mailTemplate.getTemplateDir(), mailTemplate.getTemplateFilename()));
		} catch (IOException e) {
			throw new MailException(MailStatusCode.TEMPLATE_ERROR, MessageUtils.format("템플릿을 가져오는 중 에러가 발생했습니다.(TEMPLATE_ID={0}, TEMPLATE_DIR={1}, TEMPLATE_FILE={2})", mailTemplate.getTemplateId(), mailTemplate.getTemplateDir(), mailTemplate.getTemplateFilename()));
		}
		
		Map<String, Object> root = new HashMap<String, Object>();
		String dataModelType = mailTemplate.getDataModelType();
		if ("X".equals(dataModelType)) {
			// XML
			InputSource input = new InputSource(new StringReader(mail.getCotentData()));
			try {
				root.put("doc", freemarker.ext.dom.NodeModel.parse(input));
			} catch (Exception e) {
				throw new MailException(MailStatusCode.DATA_MODEL_ERROR, MessageUtils.format("데이터 모델을 분석하는중 에러가 발생했습니다. (MAIL_NO={0}, TEMPLATE_ID={1})", mail.getMailNo(), mail.getTemplateId()), e);
			}
		} else if ("J".equals(dataModelType)) {
			// TODO JSON 구현
		} else if ("P".equals(dataModelType)) {
			// Parameter
			StrTokenizer tokenizer = new StrTokenizer(mail.getCotentData(), "&");
			String token = null;
			while (tokenizer.hasMoreTokens()) {
				token = tokenizer.nextToken();
				root.put(StringUtils.substringBefore(token, "="), StringUtils.substringAfter(token, "="));
			}
		} else {
			throw new MailException(MailStatusCode.UNKOWN_DATA_MODEL_TYPE, MessageUtils.format("{0}은 사용할 수 없는 데이터 모델 타입(DATA_MODE_TYPE)입니다. ", dataModelType));
		}
		Writer out = new StringWriter();
		try {
			template.process(root, out);	
		} catch (IOException e) {
			throw new MailException(MailStatusCode.BAD_CONTENT, MessageUtils.format("메일 본문 내용을 작성하는 중 에러가 발생했습니다. (MAIL_NO={0}, TEMPLATE_ID={1})", mail.getMailNo(), mail.getTemplateId()), e);
		} catch (TemplateException e) {
			throw new MailException(MailStatusCode.BAD_CONTENT, MessageUtils.format("메일 본문 내용을 작성하는 중 에러가 발생했습니다. (MAIL_NO={0}, TEMPLATE_ID={1})", mail.getMailNo(), mail.getTemplateId()), e);
		}
		return out.toString();
	}

}


6. 작업 클래스 만들기
- 기존에서는 PostMan 클래스에서 주기마다 반복하면서 작업을 처리했는데, 스케줄러를 도입했으므로, 단순히 작업만 하는 클래스를 만들어보겠다.
- 간단한 PostMan 클래스에서 주기 처리 부분이 빠졌다고 보면 되겠다.

package kr.kangwoo.postman.core;

import java.util.Date;
import java.util.List;

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.StringUtils;

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

public class PostManJob {

	private Logger logger = LoggerFactory.getLogger(PostManJob.class);
	
	private String daemonName = getClass().getName();
	
	private MailManager mailManager;
	private MailTemplateManager mailTemplateManager;
	private MailSendManager mailSendManager;
	
	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 run() {
		try {
			logger.info("메일 템플릿 정보 적재 시작");
			mailTemplateManager.reload();
			logger.info("메일 템플릿 정보 적재 완료");
			
			List<Mail> sendList = mailManager.getSendList(daemonName);
			if (logger.isDebugEnabled()) {
				logger.debug("{}개의 메일을 가져왔습니다.", sendList != null ? sendList.size() : 0);	
			}
			
			if (sendList != null) {
				String subject = null;
				String content = null;
				for (Mail mail : sendList) {
					if (StringUtils.equals(mail.getStatusCode(), MailStatusCode.ACCEPTED)) {
						try {
							subject = mailTemplateManager.getSubject(mail);
							content = mailTemplateManager.getContent(mail);
							mail.setStatusCode(MailStatusCode.SEND_READY);
							
							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);
						} finally {
							mail.setUpdatedBy(daemonName);
							mail.setUpdatedDate(new Date());
							mailManager.updateMail(mail);
						}
					} else {
						logger.warn("메일 상태 코드가 잘못되었습니다. (STATUS_CODE={})", mail.getStatusCode());
					}
				}
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}
	}
}

- PostMan 클래스는 단순히 스프링 프레임워크의 ApplicationContext 를 생성하는 역할만을 한다.
package kr.kangwoo.postman.core;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class PostMan {

	private Logger logger = LoggerFactory.getLogger(PostMan.class);
	
	public static void main(String[] args) {
		ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[] {"post-man-scheduler.xml"});
	}

}


7. XMl 만들하기
- 자, 이제 공포의 XML을 만들어보자.
- data-source-context.xml (데이터베이스 연결 관리, 트랜잭션관리등을 선언해놓았다.)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
	<bean id="placeholderProperties"
		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="location" value="classpath:postman.properties" />
	</bean>
	
	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
		<property name="driverClassName" value="${batch.jdbc.driver}" />
		<property name="url" value="${batch.jdbc.url}" />
		<property name="username" value="${batch.jdbc.user}" />
		<property name="password" value="${batch.jdbc.password}" />
	</bean>
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>
	<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:ibatis-config.xml" />
	</bean>
	<bean id="baseTransactionProxy"
		class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
		lazy-init="true">
		<property name="transactionManager">
			<ref local="transactionManager" />
		</property>
		<property name="transactionAttributes">
			<props>
				<prop key="add*">PROPAGATION_REQUIRED</prop>
				<prop key="insert*">PROPAGATION_REQUIRED</prop>
				<prop key="update*">PROPAGATION_REQUIRED</prop>
				<prop key="delete*">PROPAGATION_REQUIRED</prop>
				<prop key="save*">PROPAGATION_REQUIRED</prop>
				<prop key="get*">PROPAGATION_SUPPORTS</prop>
				<prop key="getList*">PROPAGATION_SUPPORTS</prop>
				<prop key="count*">PROPAGATION_SUPPORTS</prop>
				<prop key="*">PROPAGATION_SUPPORTS</prop>
			</props>
		</property>
	</bean>
</beans>

- ibatis-config.xml (iBatis 설정 정보이다.)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
    <settings
        useStatementNamespaces="true"/>
   <sqlMap resource="kr/kangwoo/postman/repository/ibatis/MailDaoSqlMap.xml" />
   <sqlMap resource="kr/kangwoo/postman/repository/ibatis/MailTemplateDaoSqlMap.xml" />
</sqlMapConfig>

- post-man-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>

 	<!-- B:Data Access Object -->
    <bean id="mailDao" class="kr.kangwoo.postman.repository.ibatis.MailDaoSqlMap">
        <property name="sqlMapClient">
            <ref bean="sqlMapClient"/>
        </property>
    </bean>
    <bean id="mailTemplateDao" class="kr.kangwoo.postman.repository.ibatis.MailTemplateDaoSqlMap">
        <property name="sqlMapClient">
            <ref bean="sqlMapClient"/>
        </property>
    </bean>
     <!-- E:Data Access Object -->
    
    <!-- B:Service -->
    <bean id="mailManager" parent="baseTransactionProxy">
        <property name="proxyInterfaces" value="kr.kangwoo.postman.service.MailManager" />
        <property name="target">
            <bean class="kr.kangwoo.postman.service.SimpleMailManager">
                <property name="mailDao">
                    <ref bean="mailDao"/>
                </property>
	        </bean>
        </property>
    </bean>
    
    <bean id="mailSendManager" parent="baseTransactionProxy">
        <property name="proxyInterfaces" value="kr.kangwoo.postman.service.MailSendManager" />
        <property name="target">
            <bean class="kr.kangwoo.postman.service.SMTPMailSendManager">
                <property name="host" value="${smtp.host}" />
                <property name="port" value="${smtp.port}" />
                <property name="starttlsEnable" value="${smtp.starttlsEnable}" />
                <property name="userName" value="${smtp.userName}" />
                <property name="password" value="${smtp.password}" />
	        </bean>
        </property>
    </bean>
    
    <bean id="mailTemplateManager" parent="baseTransactionProxy">
        <property name="proxyInterfaces" value="kr.kangwoo.postman.service.MailTemplateManager" />
        <property name="target">
            <bean class="kr.kangwoo.postman.service.FreemarkerMailTemplateManager">
                <property name="mailTemplateDao">
                    <ref bean="mailTemplateDao"/>
                </property>
	        </bean>
        </property>
    </bean>
    <!-- E:Service -->
    
</beans>

- post-man-scheduler.xml (quartz를 이용한 스케줄 설정 파일이다.)
- MethodInvokingJobDetailFactoryBean 를 이용해서 POJO인 PostManJob의 run()메소드를 실행시킨다.
- CronTriggerBean을 이용해서, 30초다(사실은 0초, 30초인 시점이지만 ^^;)마다 메일을 조회해서 발송한다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
	<import resource="data-source-context.xml" />
	<import resource="post-man-context.xml" />
	
	<bean id="postManJob"
		class="kr.kangwoo.postman.core.PostManJob">
		<property name="mailManager" ref="mailManager" />
		<property name="mailTemplateManager" ref="mailTemplateManager" />
		<property name="mailSendManager" ref="mailSendManager" />
	</bean>
	
	<bean id="jobDetail"
		class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
		<property name="targetObject" ref="postManJob" />
		<property name="targetMethod" value="run" />
		<property name="concurrent" value="false" />
	</bean>
	
	<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
		<property name="triggers">
			<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
				<property name="jobDetail" ref="jobDetail" />
				<property name="cronExpression" value="0/30 * * * * ?" />
			</bean>
		</property>
	</bean>
</beans>

- postman.properties (설정 정보)
#Database
batch.jdbc.driver=oracle.jdbc.driver.OracleDriver
batch.jdbc.url=jdbc:oracle:thin:@xxx.kangwoo.kr:1521:XE
batch.jdbc.user=USER
batch.jdbc.password=PWD

#SMTP
smtp.host=smtp.gmail.com
smtp.port=587
smtp.starttlsEnable=true
smtp.userName=xxx@mail.com
smtp.password=pwd



8. 넋두리
- Spring, iBatis를 전혀 모르면, 이해하기 어려우실테지만, 뭐 그냥 Quartz란 스케줄러가 있다는 정도로 만족 하시면 되겠다.
- PostMan을 상당히 많이 우려먹고 있는데, 봄이니까~~~ 이해바란다.  ^^;  (사실 몇번 더 우려 먹어도 될거 같다.)