Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- sha1
- 암호학
- IPTV
- AES
- Runnable
- 한글조사
- Freemaker
- 이클립스 플러그인 개발
- RSA
- 한글조사처리
- xlet
- DAMO
- ACAP
- 자바
- JCE
- Callable
- Executor
- StringUtils
- ORM
- mac
- date
- Postman
- Log4J
- 자바 암호화
- PKCS
- String
- Instrumentation
- Executors
- PKCS#8
- Java
Archives
- Today
- Total
오늘은 어디로 갈까...
스케줄러를 이용한 메일 발송 본문
봄을 재촉하는 비가 내리고 있다. 이런날을 커다란 창문이 있는 곳에 앉아, 하염없이 내리는 비를 바라보는게 즐거움일텐데, 어쩔 수 없이 모니터만 뚫어지게 바라보고 있다.
봄하면 생각나는게 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
: 반복 시간을 지정할수 있다.
+ org.springframework.scheduling.quartz.CronTriggerBean
: 다양한 시간간격을 지정할수 있다. (매일, 매주, 심시어 요일별로 지정할 수 있다)
표현식 -> http://quartz.sourceforge.net/javadoc/org/quartz/CronTrigger.html
- 실행시킬 작업(Job)을 정의하는 클래스
+ org.springframework.scheduling.quartz.JobDetailBean
: org.springframework.scheduling.quartz.QuartzJobBean을 상속받아 executeInternal(JobExecutionContext) 메소드를 구현하면 된다.
+ org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean
: 지정한 Bean의 대상 메소드를 실행시켜준다.
- 스케줄러를 지정해 주면, spring이 초기화되면서 자동으로 작동을 한다. triggers란 이름의 속성에 작동시킬 Trigger들을 넣어주면 된다.
3. DAO 수정하기
- 기존 SimpleMailDao랑, SimpleMailTemplateDao는 과감히 날려버리고 iBatis를 이용해서 새로이 만들겠다.
- MailDao의 getMailList()메소드와 MailTemplateDao의 getMailTemplateList()가 변경되었으니 유의바란다.
- 여기 샘플은 insert, update, delete, select 모두 구현되어 있는데, 코드 생성기로 구현한거라 그런것일뿐, 실제적으로는 아주 조금만(?) 필요하다.
- 지금에 와서야 고백(?)을 하지만 오라클의 자릿수를 지정하지 않은 Number형은 자바의 Long형의 범위를 벗어나서 BigDecimal이 맞는것이지만, 귀찮은 관계로 Long형을 이용했다. ^^;
4. SqlMap 만들기
- iBatis에서 사용할 sql을 만들어보자.
- 실제적으로 사용할 부분은 update 부분과 getList 부분이다. 상태코드에 맞는 데이터를 가져오고, 생태코드를 갱신하면 되는것이다. iBatis와 별로 안친하신분들을 JDBC를 직접 이용해서 구현하시면 되겠다.
- 원래 의도는 select for update를 이용해서 동일한 데몬이 동시에 여러개 실행될때 데이터를 어떻게 가지고 오는지도 설명해보려고 했으나, 오라클 종속(?)적이고, postman의 성격과는 별로 안 맞는거 같아서 넘어가도록 한다.
- getList만 사용한다. USE_FLAG가 "Y"인 놈만 가져오게 사용할것이다.
5. Service 수정하기
- DAO가 수정된 관계로 Manager 클래스들도 변경을 하도록 하겠다.
- DAO에서 데이터를 조회할때, 파라메터를 넘겨주는 부분이 추가되었고, update 하는 부분이 변경되었다.
- DAO에서 데이터를 조회할때, 파라메터를 넘겨주는 부분이 추가되었다.
6. 작업 클래스 만들기
- 기존에서는 PostMan 클래스에서 주기마다 반복하면서 작업을 처리했는데, 스케줄러를 도입했으므로, 단순히 작업만 하는 클래스를 만들어보겠다.
- 간단한 PostMan 클래스에서 주기 처리 부분이 빠졌다고 보면 되겠다.
- PostMan 클래스는 단순히 스프링 프레임워크의 ApplicationContext 를 생성하는 역할만을 한다.
7. XMl 만들하기
- 자, 이제 공포의 XML을 만들어보자.
- data-source-context.xml (데이터베이스 연결 관리, 트랜잭션관리등을 선언해놓았다.)
- ibatis-config.xml (iBatis 설정 정보이다.)
- post-man-context.xml
- post-man-scheduler.xml (quartz를 이용한 스케줄 설정 파일이다.)
- MethodInvokingJobDetailFactoryBean 를 이용해서 POJO인 PostManJob의 run()메소드를 실행시킨다.
- CronTriggerBean을 이용해서, 30초다(사실은 0초, 30초인 시점이지만 ^^;)마다 메일을 조회해서 발송한다.
- postman.properties (설정 정보)
8. 넋두리
- Spring, iBatis를 전혀 모르면, 이해하기 어려우실테지만, 뭐 그냥 Quartz란 스케줄러가 있다는 정도로 만족 하시면 되겠다.
- PostMan을 상당히 많이 우려먹고 있는데, 봄이니까~~~ 이해바란다. ^^; (사실 몇번 더 우려 먹어도 될거 같다.)
봄하면 생각나는게 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을 상당히 많이 우려먹고 있는데, 봄이니까~~~ 이해바란다. ^^; (사실 몇번 더 우려 먹어도 될거 같다.)
commons-collections-3.2.1.jar