지난번에 spring quartz로 사용하지 않고 @scheduled 어노테이션만 사용하여 스케쥴링 프로세스를 구성하였더니 cron식을 동적으로 사용할 수 없었다.

그래서 이번엔 quartz를 사용하여 동적 스케쥴링을 구성해보았다.

 

구글링을 해보면 거의 대부분 spring 설정 xml파일에 quartz bean들을 추가하는 방식으로 설명이 되있고 회사에서 담담하고 있는 프로젝트 또한 그런 형태로 되어있다.

 

필자는 xml파일에 설정하는 것이 아닌 java단에서 스케쥴링 프로세스가 바로 실행될 수 있도록 구성해 보았다.(xml에 끄적이는 편을 선호하지 않음)

 

1. 필요 라이브러리

 

 - springframework-version : 3.3.1

 - java-version : open jdk 1.8

 - quartz  : 1.8.6(spring quartz를 사용하기 위한 라이브러리)

 - cglib : @configuration 사용 시 cglib exception발생 시 사용

<dependency> 	
	<groupId>org.quartz-scheduler</groupId> 	
    <artifactId>quartz</artifactId> 	
    <version>1.8.6</version> 
</dependency> 
<dependency>         
    <groupId>cglib</groupId> 	
    <artifactId>cglib</artifactId> 	
    <version>3.2.10</version> 
</dependency>

 

 

2. 소스 코드는 다음과 같다.

 

<SchedulExecuter.java>

package com.spring.test.util;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.scheduling.quartz.CronTriggerBean;
import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
import org.springframework.stereotype.Component;
import com.spring.test.dao.NoticeDao;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class ScheldulExecuter {        
    
    //tomcat 구동 시 메소드가 자동 실행 되도록 하는 어노테이션 	
    @PostConstruct 	
    public void schedulerSet() throws Exception {
        //jobDetail 설정 		
        MethodInvokingJobDetailFactoryBean jobDetailBean = new MethodInvokingJobDetailFactoryBean(); 		
        jobDetailBean.setTargetObject(new ScheldulExecuter()); 		jobDetailBean.setTargetMethod("scheduledStart"); 		
        jobDetailBean.setGroup("test1"); 		
        jobDetailBean.setName("scheduledStart"); 	
        jobDetailBean.afterPropertiesSet(); 	
        
        //cronTriger 설정 		
        CronTriggerBean cronTrigger = new CronTriggerBean(); 		
        cronTrigger.setJobDetail((JobDetail)jobDetailBean.getObject()); 	
        cronTrigger.setCronExpression("0/5 * * * * ?"); 		
        cronTrigger.setName("scheduledStart"); 		
        Scheduler sch = new StdSchedulerFactory().getScheduler(); 	
        sch.scheduleJob((JobDetail)jobDetailBean.getObject(),cronTrigger); 		
        sch.start(); 	
    }  	
    
    public void scheduledStart() throws Exception{ 		
        logger.info("스케쥴 수행!!"); 	
    } 
}

소스는 간단하다. 

 - jobDetail

  TargetObject :  실행할 프로세스가 정의된 class 객체를 넣어준다.

  TargetMethod : 실행할 프로세스가 정의된 method 명을 넣어준다.

  Group : 스케쥴의 그룹 명칭을 정의한다.(보통 비슷한 스케쥴끼리 묶어서 정의함)

  Name : 스케쥴의 이름을 정의한다.(보통 실행되는 프로세스 명칭을 정의함, 본인은 TargetMethod와 동일하게 넣음)

  afterPropertiesSet : doc에는 bean 설정이 끝난 후 수행한다는 의미처럼 적혀있는 것 같다. 해당 설정을 안하면 scheduleJob에 jobDetailBean.getObject() 값에 null로 들어가 exception이 발생한다.

 

 - cronTrigger

   jobDetail : jobDetail 객체를 넣어준다.

   cronExpression : cron식을 넣어준다.

   name : 트리거의 이름을 정의한다.(jobDetail의 name과 동일하게 넣음)

 

jobDetail과 cronTrigger가 모두 정의 되면 schedulejob에 넣어준 후 start()메소드를 호출하면 된다.

 

<tomcat 구동 시 스케쥴이 실행됨을 확인, 본인은 데이터베이스 연결된 것 까지 확인하였다.>

 

이후에 SchedulExecuter에 dao단을 주입한 후 schedulerSet()메소드 안에서 쿼리를 수행하여 가져온 값을 핸들링 하여 setCronExpression값에 사용하면 되겠다.

'spring' 카테고리의 다른 글

spring 스케쥴링 설정  (0) 2019.02.12
view에서 특정 함수 반복 실행 방지  (0) 2019.02.12
spring mybatis 게시판  (1) 2019.02.12
spring mybatis 셋팅  (0) 2019.02.11

spring quartz로 스케쥴링 프로세스를 구현해보려고 했으나 xml에 끄적이는 것을 별로 선호하지 않는 편이라

간단하게 구현할 수 있는 @scheduled 어노테이션을 사용하여 구현해 보았다. 


@scheduled 어노테이션은 spring 3.x 이상에서 부터 지원하는 어노테이션으로 servlet-context.xml의 beans에 task 등록을 한 후

<task:annoctation-driven/>을 끄적여 주면 된다.


<serlvet-context.xml 소스 코드>

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:task="http://www.springframework.org/schema/task"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.1.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>
	
	<context:component-scan base-package="com.spring.test" />
	<task:annotation-driven/>
	
	
</beans:beans>


이제 비즈니스 로직에 @scheduled 어노테이션을 사용해 보자.

필자는 간단하게 테스트만 해보기 위해서 기존 spring 게시판 프로젝트NoticeDao.java에 추가하였다.


@Scheduled(fixedDelay=10000)

public void scheldulerlog() {

logger.info("스케쥴 로그 ok!!");

}

위와 같이 메소드 위에 어노테이션을 추가해주면 끝이다. log찍는 부분에 스케쥴링할 메소드를 추가하여 사용해주면 되겠다.

@scheduled 어노테이션에 셋팅할 수있는 값들은 cron, fixedDelay, fixedRate가 있다.

cron은 cron식을 사용하여 설정이 가능하다. [예시 : @scheduled(cron="0 0 0 * * * ?")]

fixedDealy는 이전에 실행된 Task의 종료시간으로 부터 정의된 시간만큼 지난 후 Task를 실행한다.(밀리세컨드 단위)

fixedRate는 이전에 실행된 Task의 시작시간으로 부터 정의된 시간만큼 지난 후 Task를 실행한다.(밀리세컨드 단위)


필자는 https://javafactory.tistory.com/1386 사이트를 참조하였다. 크론식 구성하는 방법은 구글링하면 아주 잘나온다.


<NoticeDao.java 소스코드>

package com.spring.test.dao;

import java.util.List;

import javax.annotation.Resource;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import com.spring.test.vo.NoticeVo;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service(value = "noticeDao")
public class NoticeDao{

	@Resource(name = "noticeMapper")
	private NoticeMapper noticeMapper;
	
	@Scheduled(fixedDelay=10000)
	public void scheldulerlog() {
		logger.info("스케쥴 로그 ok!!");
	}
	
	public int getNoticeListCount() {
		return noticeMapper.noticeListCount();
	}
	
	public List<NoticeVo> getNoticeList(NoticeVo notice){
		return noticeMapper.noticeList(notice);
	}
	
	public NoticeVo getNoticeOne(String notice_id) {
		return noticeMapper.noticeOne(notice_id);
	}
	
	public int NoticeInsert(NoticeVo notice) {
		return noticeMapper.noticeInsert(notice);
	}
	
	public int NoticeUpdate(NoticeVo notice) {
		return noticeMapper.noticeUpdate(notice);
	}
	
	public int NoticeDelete(String notice_id) {
		return noticeMapper.noticeDelete(notice_id);
	}
	
}

위와 같이 소스를 구성한 후 tomcat을 구동하면

이렇게 10초마다 로그가 찍히는 것을 확인할 수 있다.

'spring' 카테고리의 다른 글

spring quartz 스케쥴링 java config  (3) 2019.02.15
view에서 특정 함수 반복 실행 방지  (0) 2019.02.12
spring mybatis 게시판  (1) 2019.02.12
spring mybatis 셋팅  (0) 2019.02.11

특정 사이트에서 어떤 프로세스를 수행하는 버튼을 한번만 눌렀는데 프로세스가 두번 연속으로 수행된 적이 있었다.

실제 tomcat의 localhost_access_log를 확인해보았는데 컨트롤러가 같은 시간에 두번 연속으로 요청이 된 것을 확인할 수 있었다.

이러한 현상을 방지하기 위해 특정 시간동안 프로세스가 한번만 수행할 수 있도록 프론트 단을 수정하였다.


<예제 샘플>

var timer; 
$("#bt").click(function () {
        	 if(confirm("실행하시겠습니까?")){
        		 if(timer){
        			 clearTimeout(timer);
        		 }
                 timer = setTimeout(function(){
	                 //수행될 프로세스를 입력하는 곳
                 },200); //2초 동안 재 수행되는 것을 방지
             }
         });

위와 같이 수행될 프로세스를 넣어주면 2초동안 연속으로 수행되는 것을 막아줄 수 있다.(시간은 상황에 맞게 변경하면 되겠다.)

'spring' 카테고리의 다른 글

spring quartz 스케쥴링 java config  (3) 2019.02.15
spring 스케쥴링 설정  (0) 2019.02.12
spring mybatis 게시판  (1) 2019.02.12
spring mybatis 셋팅  (0) 2019.02.11

spring mybatis 셋팅이 끝났다면 이제 게시판을 만들 차례이다.

공지사항 게시판을 예로 들어 먼저 공지사항 리스트 페이지를 만들어 보자.


가장 먼저 게시판을 위한 controller, dao, mapper, vo를 생성하자.


<프로젝트 패키지 구성>

필자는 위와 같이 패키지와 클래스를 생성하였다.


Controller 소스를 구성하기 전에 기존에 pom.xml에 등록했던 lombok 라이브러리의 기능을 사용하기 위해 lombok.config라는 파일을 생성하자.

경로 : 프로젝트 하위(pom.xml과 동일한 경로)


<lombok.config 소스 코드>

lombok.log.fieldName=logger

이 설정은 @Slf4j 어노테이션 사용 시 static logger를 생성하지 않고 바로 log를 찍을 수 있게 해준다.(아래의 소스들을 보면 확인이 가능하다.)


<게시판 table 생성>

CREATE TABLE `push_notice` (
  `notice_id` int(11) NOT NULL AUTO_INCREMENT,
  `notice_title` varchar(32) NOT NULL,
  `notice_coments` varchar(4000) NOT NULL,
  `mod_date` timestamp NULL DEFAULT NULL,
  `use_yn` varchar(2) NOT NULL DEFAULT 'Y',
  PRIMARY KEY (`notice_id`)
)

필자는 위와같이 테이블을 생성하였다.


1. Controller 단 구성


<NoticeController.java 소스 코드>

package com.spring.test.controller;

import java.util.List;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.spring.test.dao.NoticeDao;
import com.spring.test.vo.NoticeVo;
import com.spring.test.vo.PagingVo;

import lombok.extern.slf4j.Slf4j;

@Controller
@Slf4j
public class NoticeController {

	@Resource(name = "noticeDao")
	private NoticeDao noticeDao;
	
	@RequestMapping(value = "/noticeList")
	public String noticeList(@ModelAttribute("Notice") NoticeVo notice,
							@RequestParam(defaultValue="1") int curPage,
							Model model
								) {
		logger.info("noticeList page");
		int listCnt = noticeDao.getNoticeListCount();
		PagingVo paging = new PagingVo(listCnt, curPage);
		notice.setStartIndex(paging.getStartIndex());
		notice.setCntPerPage(paging.getPageSize());
			
		List<NoticeVo> noticeList=noticeDao.getNoticeList(notice);
		model.addAttribute("noticeList", noticeList);
		model.addAttribute("listCnt", listCnt);
		model.addAttribute("paging", paging);
		return "notice/noticeList";
	}
	
	@RequestMapping(value = "/noticeRegi")
	public String noticeRegi() {
		logger.info("notice Regi page");
			return "notice/noticeDetail";
	}
	
	@RequestMapping(value = "/noticeDetail/{notice_id}")
	public String noticeEdit(@PathVariable String notice_id, Model model) {
		logger.info("notice detail page [notice_id = {}]",notice_id);
		model.addAttribute("notice",noticeDao.getNoticeOne(notice_id));
		return "notice/noticeDetail";
	}
	
	@ResponseBody
	@RequestMapping(value = "/noticeInsert", method=RequestMethod.POST)
	public int noticeInsert(NoticeVo notice) {
		logger.info("notice insert"); 
		return noticeDao.NoticeInsert(notice);
	}
	
	@ResponseBody
	@RequestMapping(value = "/noticeUpdate", method=RequestMethod.POST)
	public int noticeUpdate(NoticeVo notice) {
		logger.info("notice update {} ", notice.getNotice_id()); 
		return noticeDao.NoticeUpdate(notice);
	}
	
	@ResponseBody
	@RequestMapping(value = "/noticeDelete", method=RequestMethod.POST)
	public int noticeDelete(NoticeVo notice) {
		logger.info("notice delete {} ", notice.getNotice_id()); 
		return noticeDao.NoticeDelete(notice.getNotice_id());
	}	
}

컨트롤러 구성은 위에서 부터 게시판 리스트 페이지, 등록페이지, 수정페이지, 게시판 정보 insert, 게시판 정보 update, 게시판 정보 delete 로 구성하였다.

하나의 메소드에서 여러개의 기능을 하도록 소스를 구성할 수 있으나, 보기 쉽게 할 수 있도록 메소드를 나뉘었다.

또한 noticeList()에서 페이징 처리를 할 수 있도록 소스를 구성하였다.


-> 여기서 @ResponseBody 어노테이션을 사용한 메소드는 view의 ajax로 부터의 요청에 대해 view 즉 jsp의 경로를 돌려주는게 아닌 문자열만 리턴하도록 처리된다.

-> Spring에서 제공하는 @Autowired 어노테이션 대신 java에서 제공하는 @Resource 어노테이션을 사용하여 가용성을 높였다.


2. model 단 구성


<PagingVo.java 소스코드>

package com.spring.test.vo;

import lombok.Data;

@Data
public class PagingVo {
	/** 한 페이지당 게시글 수 **/
    private int pageSize = 10;
    
    /** 한 블럭(range)당 페이지 수 **/
    private int rangeSize = 10;
    
    /** 현재 페이지 **/
    private int curPage = 1;
    
    /** 현재 블럭(range) **/
    private int curRange = 1;
    
    /** 총 게시글 수 **/
    private int listCnt;
    
    /** 총 페이지 수 **/
    private int pageCnt;
    
    /** 총 블럭(range) 수 **/
    private int rangeCnt;
    
    /** 시작 페이지 **/
    private int startPage = 1;
    
    /** 끝 페이지 **/
    private int endPage = 1;
    
    /** 시작 index **/
    private int startIndex = 0;
    
    /** 이전 페이지 **/
    private int prevPage;
    
    /** 다음 페이지 **/
    private int nextPage;
    
    public PagingVo(int listCnt, int curPage){
        
        /**
         * 페이징 처리 순서
         * 1. 총 페이지수
         * 2. 총 블럭(range)수
         * 3. range setting
         */
        
        // 총 게시물 수와 현재 페이지를 Controller로 부터 받아온다.
        /** 현재페이지 **/
        setCurPage(curPage);
        /** 총 게시물 수 **/
        setListCnt(listCnt);
        
        /** 1. 총 페이지 수 **/
        setPageCnt(listCnt);
        /** 2. 총 블럭(range)수 **/
        setRangeCnt(pageCnt);
        /** 3. 블럭(range) setting **/
        rangeSetting(curPage);
        
        /** DB 질의를 위한 startIndex 설정 **/
        setStartIndex(curPage);
    }
 
    public void setPageCnt(int listCnt) {
        this.pageCnt = (int) Math.ceil(listCnt*1.0/pageSize);
    }
    public void setRangeCnt(int pageCnt) {
        this.rangeCnt = (int) Math.ceil(pageCnt*1.0/rangeSize);
    }
    public void rangeSetting(int curPage){
        
        setCurRange(curPage);        
        this.startPage = (curRange - 1) * rangeSize + 1;
        this.endPage = startPage + rangeSize - 1;
        
        if(endPage > pageCnt){
            this.endPage = pageCnt;
        }
        
        this.prevPage = curPage - 1;
        this.nextPage = curPage + 1;
    }
    public void setCurRange(int curPage) {
        this.curRange = (int)((curPage-1)/rangeSize) + 1;
    }
    public void setStartIndex(int curPage) {
        this.startIndex = (curPage-1) * pageSize;
    }
}


<NoticeVo.java 소스코드>

package com.spring.test.vo;

import lombok.Data;

@Data
public class NoticeVo {
	private String notice_id;
	private String notice_title;
	private String notice_coments;
	private String use_yn;
	private String mod_date;
	private int startIndex;
	private int cntPerPage;
}

->lombok의 @Data 어노테이션을 사용하여 setter, getter 생성을 생략할 수 있다.


<NoticeMapper.java 소스코드>

package com.spring.test.dao;

import java.util.List;

import org.springframework.stereotype.Repository;

import com.spring.test.vo.NoticeVo;

@Repository(value = "noticeMapper")
public interface NoticeMapper {
	public int noticeListCount();
	public List<NoticeVo> noticeList(NoticeVo notice);
	public NoticeVo noticeOne(String notice_id);
	public int noticeInsert(NoticeVo notice);
	public int noticeUpdate(NoticeVo notice);
	public int noticeDelete(String notice_id);
}


<NoticeDao.java 소스코드>

package com.spring.test.dao;

import java.util.List;

import javax.annotation.Resource;
import org.springframework.stereotype.Service;

import com.spring.test.vo.NoticeVo;

@Service(value = "noticeDao")
public class NoticeDao{

	@Resource(name = "noticeMapper")
	private NoticeMapper noticeMapper;
	
	public int getNoticeListCount() {
		return noticeMapper.noticeListCount();
	}
	
	public List<NoticeVo> getNoticeList(NoticeVo notice){
		return noticeMapper.noticeList(notice);
	}
	
	public NoticeVo getNoticeOne(String notice_id) {
		return noticeMapper.noticeOne(notice_id);
	}
	
	public int NoticeInsert(NoticeVo notice) {
		return noticeMapper.noticeInsert(notice);
	}
	
	public int NoticeUpdate(NoticeVo notice) {
		return noticeMapper.noticeUpdate(notice);
	}
	
	public int NoticeDelete(String notice_id) {
		return noticeMapper.noticeDelete(notice_id);
	}
}

-> NoticeDao.java에서 현재 소스에는 없지만 추후 트랜잭션을 처리하기에 용이할 것 같다.


<noticeMapper.xml 소스 코드>

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.spring.test.dao.NoticeMapper">
 	<resultMap id="NoticeVo" type="com.spring.test.vo.NoticeVo">
		<result property="notice_id" column="notice_id"/>
		<result property="notice_title" column="notice_title"/>
		<result property="notice_coments" column="notice_coments"/>
		<result property="mod_date" column="mod_date"/>
		<result property="use_yn" column="use_yn"/>
	</resultMap>
	
	<select id="noticeListCount" resultType="Integer">
		select count(*)
		from push_notice;
	</select>
	
	<select id="noticeList" resultMap = "NoticeVo">
		select	notice_id,
				notice_title,
				notice_coments,
				use_yn,
				DATE_FORMAT(mod_date, '%Y-%m-%d') mod_date
		from push_notice
		order by notice_id
		limit #{startIndex}, #{cntPerPage}
		
	</select>
	<select id="noticeOne" parameterType="String" resultMap = "NoticeVo">
		select	notice_id,
				notice_title,
				notice_coments
		from push_notice
		where notice_id = #{notice_id}
	</select>
	<insert id="noticeInsert" parameterType="com.spring.test.vo.NoticeVo">
		insert into push_notice
		(
			notice_title,
			notice_coments,
			mod_date
		)
		values
		(
			#{notice_title},
			#{notice_coments},
			NOW()
		)
	</insert>
	<update id="noticeUpdate" parameterType="com.spring.test.vo.NoticeVo">
		update push_notice set
			notice_title = #{notice_title},
			notice_coments = #{notice_coments},
			mod_date = NOW()
		where notice_id = #{notice_id}
	</update>
	<delete id="noticeDelete" parameterType="String">
		delete from push_notice
		where notice_id = #{notice_id}
	</delete>
 </mapper>

비즈니스 로직을 다양한 방법으로 구성할 수 있으나 필자는 위와 같이 구성하는 것이 추후 유지보수 하거나 트랜잭션을 구성하는데 용이하다고 판단되었다.


3. View 단 구성

View단은 noticeList.jsp와 noticeDetail.jsp로 구성하였으며, jquery는 3.3.1버전을 사용하였다.


<noticeList.jsp 소스코드>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <link rel="stylesheet" type="text/css" href="/test/resources/css/noticeList.css"> <script type="text/javascript" src="/test/resources/js/jquery-3.3.1.min.js"></script> <script> $(document).ready(function(){ $("#notice_regi").on("click",function(){ location.href="/test/noticeRegi" }); }); function fn_paging(curPage){ location.href="/test/noticeList?curPage="+curPage; } function notice_push(notice_id){ alert(notice_id); } </script> </head> <body> <div class="title">공지사항</div> <div class="contents"> <input type="button" id="notice_regi" value="등록"> <div class="divTable greenTable"> <div class="divTableHeading"> <div class="divTableRow"> <div class="divTableHead"><input type="checkbox"></div> <div class="divTableHead">NO</div> <div class="divTableHead">제목</div> <div class="divTableHead">등록일자</div> <div class="divTableHead"></div> </div> </div> <c:forEach var="v" items="${noticeList}" varStatus="status"> <div class="divTableBody"> <div class="divTableRow"> <div class="divTableCell"><input type="checkbox"></div> <div class="divTableCell">${status.index+1+(paging.curPage-1)*10}</div> <div class="divTableCell"> <a href="/test/noticeDetail/${v.notice_id}">${v.notice_title}</a> </div> <div class="divTableCell">${v.mod_date}</div> <div class="divTableCell"><input type="button" onclick="notice_push(${v.notice_id})" value="전송"></div> </div> </div> </c:forEach> </div> <div class="greenTable outerTableFooter"> <div class="tableFootStyle"> <div class="links"> <a href="#" onClick="fn_paging(1)">[처음]</a> <c:if test="${paging.curPage ne 1}"> <a href="#" onClick="fn_paging(${paging.prevPage })">[이전]</a> </c:if> <c:forEach var="pageNum" begin="${paging.startPage }" end="${paging.endPage }"> <c:choose> <c:when test="${pageNum eq paging.curPage}"> <span style="font-weight: bold;"> <a href="#" onClick="fn_paging(${pageNum })" style="font-weight: bold; color:red;"> ${pageNum } </a> </span> </c:when> <c:otherwise> <a href="#" onClick="fn_paging(${pageNum })">${pageNum }</a> </c:otherwise> </c:choose> </c:forEach> <c:if test="${paging.curPage ne paging.pageCnt && paging.pageCnt > 0}"> <a href="#" onClick="fn_paging(${paging.nextPage })">[다음]</a> </c:if> <c:if test="${paging.curRange ne paging.rangeCnt && paging.rangeCnt > 0}"> <a href="#" onClick="fn_paging(${paging.pageCnt })">[끝]</a> </c:if> </div> </div> </div> <div> 총 게시글 수 : ${paging.listCnt } / 총 페이지 수 : ${paging.pageCnt } / 현재 페이지 : ${paging.curPage } / 현재 블럭 : ${paging.curRange } / 총 블럭 수 : ${paging.rangeCnt } </div> </div> </body> </html>

<noticeDetail.jsp 소스코드>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <link rel="stylesheet" type="text/css" href="/test/resources/css/noticeDetail.css"> <script type="text/javascript" src="/test/resources/js/jquery-3.3.1.min.js"></script> <script> $(document).ready(function(){ //공지사항 신규 등록 $("#notice_regist").on("click",function(){ var formData = $("#notice_form").serialize(); $.ajax({ type : "post", url : "/test/noticeInsert", data : formData, success : function(data){ if(data==1) alert("등록 완료"); else alert('등록 실패'); }, error : function(error){ alert("등록 실패"); console.log("notice insert fail : "+error); } }); }); //공지사항 수정 $("#notice_edit").on("click",function(){ var formData = $("#notice_form").serialize(); $.ajax({ type : "post", url : "/test/noticeUpdate", data : formData, success : function(data){ if(data==1) alert("수정 완료"); else alert('수정 실패'); }, error : function(error){ alert("수정 실패"); console.log("notice update fail : "+error); } }); }); //공지사항 삭제 $("#notice_delete").on("click",function(){ var formData = $("#notice_form").serialize(); alert(formData); $.ajax({ type : "post", url : "/test/noticeDelete", data : formData, success : function(data){ if(data==1){ alert("삭제 완료"); location.href="/push/noticeList"; }else alert('삭제 실패'); }, error : function(error){ alert("삭제 실패"); console.log("notice delete fail : "+error); } }); }); $("#notice_backPage").on("click",function(){ location.href="/test/noticeList"; }); }) </script> </head> <body> <div class="big_title">공지사항 수정/삭제/추가</div> <div class="big_contents"> <form id="notice_form"> <input type="hidden" id="notice_id" name="notice_id" value="${notice.notice_id }"> <div class="title"> <div> 제목 </div> <div> <input type="text" id="notice_title" name="notice_title" value="${notice.notice_title}"> </div> </div> <div class="contents"> <div> 내용 </div> <div> <textarea id="notice_coments" name="notice_coments">${notice.notice_coments}</textarea> </div> </div> </form> <div class="footer"> <c:if test="${null eq notice }"> <input type="button" id="notice_regist" value="등록"> </c:if> <c:if test="${null ne notice }"> <input type="button" id="notice_edit" value="수정"> </c:if> <input type="button" id="notice_backPage" value="뒤로"> <input type="button" id="notice_delete" value="삭제"> </div> </div> </body> </html>

noticeDetail.jsp에서 ajax를 사용하여 등록, 수정, 삭제 기능을 처리하였다.

-> ajax에서 serialize()메소드를 통해 POST로 넘길 form data들을 객체 변수로 생성하여 controller에서 NoticeVo 변수로 받을 수 있다.


<noticeList.css 소스코드>

.title{
	position : relative;
	text-align : center;
	left : 50%;
	width : 200px;
	margin-left : -100px;
}
.contents{
	position : relative;
	top : 30px;
	left : 50%;
	width : 1000px;
	margin-left : -500px;
}		
div.greenTable {
  font-family: Georgia, serif;
  border: 0px solid #949494;
  background-color: #FFFFFF;
  width: 100%;
  border-collapse: collapse;
}
.divTable.greenTable .divTableCell, .divTable.greenTable .divTableHead {
  border: 1px solid #000000;
  padding: 3px 2px;
}
.divTable.greenTable .divTableBody .divTableCell {
  font-size: 13px;
}
.divTable.greenTable .divTableHeading .divTableHead {
  font-size: 15px;
  font-weight: bold;
  color: #000000;
  text-align: center;
}
.greenTable .tableFootStyle {
  font-size: 13px;
  font-weight: bold;
}
.greenTable .tableFootStyle {
  font-size: 13px;
}
.greenTable .tableFootStyle .links {
	 text-align: right;
}
.greenTable .tableFootStyle .links a{
  display: inline-block;
  color: #101010;
  padding: 2px 8px;
  border-radius: 5px;
}
.greenTable.outerTableFooter {
  border-top: none;
}
.greenTable.outerTableFooter .tableFootStyle {
  padding: 3px 5px; 
}
.divTableRow>div:first-child{                                        
	width : 5px;
	text-align : center;
}
.divTableRow>div:nth-child(2){
	width : 10px;
	text-align : center;
}
.divTableRow>div:nth-child(3){
	width : 700px;
}
.divTableRow>div:nth-child(4){
	width : 100px;
	text-align : center;
}
.divTableRow>div:nth-child(5){
	width : 10px;
	text-align : center;
}


/* DivTable.com */
.divTable{ display: table; }
.divTableRow { display: table-row; }
.divTableHeading { display: table-header-group;}
.divTableCell, .divTableHead { display: table-cell;}
.divTableHeading { display: table-header-group;}
.divTableFoot { display: table-footer-group;}
.divTableBody { display: table-row-group;}

.divTableBody a,.tableFootStyle a{
	text-decoration:none;
	color : black;
}


<noticeDetail.css 소스코드>

body{
	margin : 0px;
	padding : 0px;
}
.big_title{
	position : relative;
	text-align : center;
	left : 50%;
	width : 200px;
	margin-left : -100px;
}
.big_contents{
	position : relative;
	top : 30px;
	left : 50%;
	width : 800px;
	margin-left : -400px;
}
.title{
	position : absolute;
	border-top : 1px solid black;
	width : 100%;
}
.contents{
	position : absolute;
	top : 30px;
	width : 100%;
	border-top : 1px solid black;
	border-bottom : 1px solid black;
}
.footer{
	position : absolute;
	width : 140px;
	top : 560px;
	left: 50%;
	margin-left : -70px;
}
.title>div, .contents>div{
	display : inline-block;
	padding : 3px 0 3px 0;
	width : 100%;
}
.title>div:first-child, .contents>div:first-child{
	width : 100px;
	text-align : center;
}
.title>div:nth-child(2), .contents>div:nth-child(2){
	width : 600px;
}
.title input[type=text] {
	width : 602px;
}
.contents textarea {
	width : 600px;
	height : 500px;
}
.contents>div:first-child{
	position : relative;
	bottom : 250px;
}

페이징 처리 로직은 https://gangnam-americano.tistory.com/18 을 참고하였다.(참고 사이트에서는 일부 소스가 누락되어있음)


4. 모두 적용이 되었다면 tomcat에 올려 실행시켜 보자.(tomcat 7을 사용하였다.)


<공지사항 리스트 페이지>




<공지사항 상세 페이지>





<공지사항 등록 페이지>




'spring' 카테고리의 다른 글

spring quartz 스케쥴링 java config  (3) 2019.02.15
spring 스케쥴링 설정  (0) 2019.02.12
view에서 특정 함수 반복 실행 방지  (0) 2019.02.12
spring mybatis 셋팅  (0) 2019.02.11

개발 환경 : STS(3.9.5), spring(5.1.2), mybatis, openjdk1.8, mariaDB 등


sts에서 spring legacy project중에 spring mvc project를 생성한다.


pom.xml 사용 라이브러리 : 

- jstl : jsp에서 jstl 태그를 사용하기 위함

- mariadb-java-clien : mariaDB와 connnection을 맺기 위함

- mybatis-spring  : Spring에서 연동을 지원하는 mybatis

- mybatis              

- spring-jdbc : Spring에서 지원하는 JDBC

- commons-dbcp : 커넥션풀을 담당하는 Apache Commons DBCP

- commons-lang3 : 문자열 라이브러리를 apache commons lang으로 통일하기 위함

- log4jdbc-remix : mybatis 로그를 출력하기 위함

- lombok : @data, @slf4 어노테이션 등을 사용하기 위함

- springloaded : java 소스 수정 시 tomcat을 자동으로 reload해주기 위함(tomcat restart를 안해도 된다)


<pom.xml 소스>

		<dependency>
		    <groupId>jstl</groupId>
		    <artifactId>jstl</artifactId>
		    <version>1.2</version>
		</dependency>
		<dependency>
		    <groupId>org.mariadb.jdbc</groupId>
		    <artifactId>mariadb-java-client</artifactId>
		    <version>1.1.7</version>
		</dependency>
		<dependency>
		    <groupId>org.mybatis</groupId>
		    <artifactId>mybatis-spring</artifactId>
		    <version>1.3.2</version>
		</dependency>
		<dependency>
		    <groupId>org.mybatis</groupId>
		    <artifactId>mybatis</artifactId>
		    <version>3.4.6</version>
		</dependency>
		<dependency>
		    <groupId>org.springframework</groupId>
		    <artifactId>spring-jdbc</artifactId>
		    <version>5.1.4.RELEASE</version>
		</dependency>
		<dependency>
		    <groupId>commons-dbcp</groupId>
		    <artifactId>commons-dbcp</artifactId>
		    <version>1.4</version>
		</dependency><dependency>
		    <groupId>org.apache.commons</groupId>
		    <artifactId>commons-lang3</artifactId>
		    <version>3.0</version>
		</dependency>
		<dependency>
		    <groupId>org.lazyluke</groupId>
		    <artifactId>log4jdbc-remix</artifactId>
		    <version>0.2.7</version>
		</dependency>
		<dependency>
		    <groupId>org.projectlombok</groupId>
		    <artifactId>lombok</artifactId>
		    <version>1.18.4</version>
		    <scope>provided</scope>
		</dependency>
		<dependency>
		    <groupId>org.springframework</groupId>
		    <artifactId>springloaded</artifactId>
		    <version>1.2.8.RELEASE</version>
		</dependency>


위와 같이 셋팅 한 후에 mybatis 설정을 위해 config파일들을 추가해야 한다.

추가 경로(이클립스 경로) : src/main/webapp/WEB-INF/spring/

추가 경로 하위에 mybatis-config-base.xml와 mybatis-context.xml을 추가한다.


<mybatis-context.xml 소스 코드>

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd"> <bean id="jdbcProp" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location" value="classpath:META-INF/jdbc.properties" /> </bean> <bean id="dataSourceSpied" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> <bean id="dataSource" class="net.sf.log4jdbc.Log4jdbcProxyDataSource"> <constructor-arg ref="dataSourceSpied" /> <property name="logFormatter"> <bean class="net.sf.log4jdbc.tools.Log4JdbcCustomFormatter"> <property name="loggingType" value="MULTI_LINE" /> <property name="sqlPrefix" value="SQL : "/> </bean> </property> </bean> <bean id="sqlSessionFactory" class="com.spring.test.util.RefreshableSqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="typeAliasesPackage" value="com.spring.test"/> <property name="configLocation" value="WEB-INF/spring/mybatis-config-base.xml" /> <property value="classpath:com/spring/**/dao/mapper/*Mapper.xml" name="mapperLocations" /> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.spring.test" /> <property name="annotationClass" value="org.springframework.stereotype.Repository"/> </bean> </beans>

기본적인 mybatis셋팅에서 dataSource를 셋팅하는 부분이 살짝 바뀌었다. 이는 mybatis 로그를 출력하기 위한 셋팅이다.

또한 sqlSessionFactory 부분에서 RefreshableSqlSessionFactoryBean.java를 참조하여 셋팅하였는데 mapper 수정 시 tomcat restart를 하지 않고 바로 적용되도록 하기 위함이다.


<RefreshableSqlSessionFactoryBean.java 소스코드>

package com.spring.test.util; import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.beans.factory.DisposableBean; import org.springframework.core.io.Resource; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class RefreshableSqlSessionFactoryBean extends SqlSessionFactoryBean implements DisposableBean { private static final Log log = LogFactory.getLog(RefreshableSqlSessionFactoryBean.class); private SqlSessionFactory proxy; private int interval = 500; private Timer timer; private TimerTask task; private Resource[] mapperLocations; /** * 파일 감시 쓰레드가 실행중인지 여부. */ private boolean running = false; private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock r = rwl.readLock(); private final Lock w = rwl.writeLock(); public void setMapperLocations(Resource[] mapperLocations) { super.setMapperLocations(mapperLocations); this.mapperLocations = mapperLocations; } public void setInterval(int interval) { this.interval = interval; } /** * @throws Exception */ public void refresh() throws Exception { if (log.isInfoEnabled()) { log.info("refreshing sqlMapClient."); } w.lock(); try { super.afterPropertiesSet(); } finally { w.unlock(); } } /** * 싱글톤 멤버로 SqlMapClient 원본 대신 프록시로 설정하도록 오버라이드. */ public void afterPropertiesSet() throws Exception { super.afterPropertiesSet(); setRefreshable(); } private void setRefreshable() { proxy = (SqlSessionFactory) Proxy.newProxyInstance( SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSessionFactory.class}, new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // log.debug("method.getName() : " + method.getName()); return method.invoke(getParentObject(), args); } }); task = new TimerTask() { private Map<Resource, Long> map = new HashMap<Resource, Long>(); public void run() { if (isModified()) { try { refresh(); } catch (Exception e) { log.error("caught exception", e); } } } private boolean isModified() { boolean retVal = false; if (mapperLocations != null) { for (int i = 0; i < mapperLocations.length; i++) { Resource mappingLocation = mapperLocations[i]; retVal |= findModifiedResource(mappingLocation); } } return retVal; } private boolean findModifiedResource(Resource resource) { boolean retVal = false; List<String> modifiedResources = new ArrayList<String>(); try { long modified = resource.lastModified(); if (map.containsKey(resource)) { long lastModified = ((Long) map.get(resource)) .longValue(); if (lastModified != modified) { map.put(resource, new Long(modified)); modifiedResources.add(resource.getDescription()); retVal = true; } } else { map.put(resource, new Long(modified)); } } catch (IOException e) { log.error("caught exception", e); } if (retVal) { if (log.isInfoEnabled()) { log.info("modified files : " + modifiedResources); } } return retVal; } }; timer = new Timer(true); resetInterval(); } private Object getParentObject() throws Exception { r.lock(); try { return super.getObject(); } finally { r.unlock(); } } public SqlSessionFactory getObject() { return this.proxy; } public Class<? extends SqlSessionFactory> getObjectType() { return (this.proxy != null ? this.proxy.getClass() : SqlSessionFactory.class); } public boolean isSingleton() { return true; } public void setCheckInterval(int ms) { interval = ms; if (timer != null) { resetInterval(); } } private void resetInterval() { if (running) { timer.cancel(); running = false; } if (interval > 0) { timer.schedule(task, 0, interval); running = true; } } public void destroy() throws Exception { timer.cancel(); } }


<mybatis-config-base.xml 소스코드>

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="false"/> <setting name="multipleResultSetsEnabled" value="true"/> <setting name="useColumnLabel" value="true"/> <setting name="useGeneratedKeys" value="false"/> <setting name="defaultExecutorType" value="SIMPLE"/> <setting name="defaultStatementTimeout" value="25000"/> </settings> <typeHandlers> <!-- java.sql.Timestamp 를 java.util.Date 형으로 반환 --> <typeHandler javaType="java.sql.Timestamp" handler="org.apache.ibatis.type.DateTypeHandler"/> <typeHandler javaType="java.sql.Time" handler="org.apache.ibatis.type.DateTypeHandler"/> <typeHandler javaType="java.sql.Date" handler="org.apache.ibatis.type.DateTypeHandler"/> </typeHandlers> </configuration>

위의 소스는 mybatis 설정 정보 파일로 필요한 옵션을 추가해서 사용하면 되겠다.(mybaits.org 또는 구글링)에서 참조


위의 파일을 추가한 후 설정을 읽어오기 위해 root-context.xml파일을 수정한다.

<root-context.xml 소스 코드>

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- Root Context: defines shared resources visible to all other web components --> <import resource="mybatis-context.xml" /> </beans>


root-context.xml의 소스를 수정한 후 mybatis-context.xml에서 설정한 dataSource 정보를 읽어오기 위해 jdbc.properties 파일을 추가한다.

추가 경로 : src/main/resource/META-INF/

<jdbc.properties 소스 코드>

# Maria DB jdbc.driverClassName=org.mariadb.jdbc.Driver jdbc.url=jdbc:mariadb://localhost:3306/javaboja?useUnicode=true&characterEncoding=utf-8&interactiveClient=true&autoReconnect=true&autoReconnectForPools=true&zeroDateTimeBehavior=convertToNull jdbc.username = javaboja jdbc.password = javaboja

마지막으로 mybatis 쿼리 로그를 출력하기 위해 log4j.xml을 수정하자.

<log4j.xml 소스 코드>

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">

	<!-- Appenders -->
	<appender name="console" class="org.apache.log4j.ConsoleAppender">
		<param name="Target" value="System.out" />
		<layout class="org.apache.log4j.PatternLayout">
			<param name="ConversionPattern" value="%-5p: %c - %m%n" />
		</layout>
	</appender>
	
	<!-- Application Loggers -->
	<logger name="com.spring.test">
		<level value="info" />
	</logger>
	 <!-- Query Loggers -->
    <logger name="jdbc.sqlonly" additivity="false">
        <level value="INFO" />
        <appender-ref ref="console-infolog" />
    </logger>
    <logger name="jdbc.resultsettable" additivity="false">
        <level value="INFO" />
        <appender-ref ref="console" />
    </logger>
	<!-- 3rdparty Loggers -->
	<logger name="org.springframework.core">
		<level value="info" />
	</logger>
	
	<logger name="org.springframework.beans">
		<level value="info" />
	</logger>
	
	<logger name="org.springframework.context">
		<level value="info" />
	</logger>

	<logger name="org.springframework.web">
		<level value="info" />
	</logger>

	<!-- Root Logger -->
	<root>
		<priority value="warn" />
		<appender-ref ref="console" />
	</root>
	
</log4j:configuration>

위 소스 코드에서 각 패키지를 입력하는 부분은 자기 자신의 프로젝트 패키지 명에 맞게 수정해줘야 한다.

여기까지 진행했다면 셋팅은 끝났다. 이제 게시판을 만들어 보자.

'spring' 카테고리의 다른 글

spring quartz 스케쥴링 java config  (3) 2019.02.15
spring 스케쥴링 설정  (0) 2019.02.12
view에서 특정 함수 반복 실행 방지  (0) 2019.02.12
spring mybatis 게시판  (1) 2019.02.12

+ Recent posts