Framework/Spring

[Spring Framework]MyBatis 서버 재실행없이 XML 적용하기(RefreshableSqlSessionFactoryBean)

  • -
반응형

Spring + MyBatis 환경에서 개발하다보면 xml 파일을 수정할 때마다 서버를 재실행해주어야하는 불편함을 느끼셨을 겁니다. 아마도 가장 큰 불편함은 특정 페이지를 개발하다가 서버를 재실행하면 세션이 끊겨 다시 그 페이지로 가는 과정(+또는 행동)을 거쳐야하는 게 아닐까 싶습니다.

 

본 포스팅에서는 xml 파일 수정시 서버의 재실행없이 반영되게 하는 방법을 정리해보았습니다.

 

※ 테스트 환경
  • jdk 1.8
  • Eclipse 2019-06   
  • Spring Framework 4.3
  • MyBatis 3.4.6
  • MariaDB

설정을 하기 앞서 Class 파일이 필요하다.

아래 소스를 원하는 패키지 경로에 추가한다.

 

RefreshableSqlSessionFactoryBean.java

package com.planm.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.ibatis.session.SqlSessionFactory; 
import org.mybatis.spring.SqlSessionFactoryBean; 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 Logger logger = LoggerFactory.getLogger(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;
	} 
	
	public void refresh() throws Exception {
		if(logger.isInfoEnabled()) {			
			logger.info("> Refresh SqlMapper");
			logger.info("======================================================================================");
		}
		
		w.lock();
		
		try {
			super.afterPropertiesSet();
		} finally { 
			w.unlock(); 
		} 
	} 
	
	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) { 
						logger.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());	// 전체경로 
							modifiedResources.add(resource.getFilename());		// 파일명							
							
							retVal = true;
						} 
					} else { 
						map.put(resource, new Long(modified));
					} 
				} catch (IOException e) { 
					logger.error("caught exception", e); 
				}
				
				if(retVal) {
					if(logger.isInfoEnabled()) {						
						logger.info("======================================================================================");
						logger.info("> Update File name : " + 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();
	}
}

 

현재 테스트 중인 프로젝트 구조는 아래 이미지와 같이 단순하다.

/WEB-INF/spring/root-context.xml 에서 JDBC와 DB 커넥션 설정을 함께 했다.

 

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"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
	xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd
		http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
		http://www.springframework.org/schema/beans https://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/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
	
	<!-- Root Context: defines shared resources visible to all other web components -->
	<!-- 
	<property name="driverClassName" value="org.mariadb.jdbc.Driver" />		
	<property name="url" value="jdbc:mariadb://127.0.0.1:3306/shxdb" /> 
	-->
	<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">		
		<property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy" />		
		<property name="url" value="jdbc:log4jdbc:mysql://127.0.0.1:3306/shxdb" />		
		<property name="username" value="root" />
		<property name="password" value="1234" />	
	</bean>
		
	<!-- <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> -->
	<bean id="sqlSessionFactory" class="com.planm.util.RefreshableSqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:/mybatis-config.xml" />
		<property name="mapperLocations" value="classpath:/mapper/**/*.xml" /> 
	</bean>	
	
	<!-- MyBatis - Spring 연동 모듈 -->
	<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" destroy-method="clearCache">
        <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory" />
    </bean>
</beans>

 

설정 파일을 보면 기존에 SqlSessionFacotyrBean 객체를 추가된 RefreshableSqlSessionFactoryBean으로 변경해주었다.

<!-- <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> -->
<bean id="sqlSessionFactory" class="com.planm.util.RefreshableSqlSessionFactoryBean">
	<property name="dataSource" ref="dataSource" />
	<property name="configLocation" value="classpath:/mybatis-config.xml" />
	<property name="mapperLocations" value="classpath:/mapper/**/*.xml" /> 
	<property name="interval" value="1000" />	<!-- Mapper 파일 리로딩 간격(ms단위) -->
</bean>	

 

적용이 잘되었다면 Mapper 파일을 수정할 때마다 아래와같은 로그를 확인할 수 있을 것이다.

 

참고)

 

Spring + mybatis 환경에서 xml 파일 변경시 서버 재시작 없이 반영 방법

요즘 ORM으로는 하이버네이트, JPA등 많이 사용하고 있으나, 역시 SI 쪽은 mybatis(ibatis)를 많이 사용된다. 문제는 mybatis는 xml로 sql을 관리하고 있는데 보통 조금 바꿀때 마다 서버를 재구동 시켜야

sbcoba.tistory.com

 

반응형
Contents

포스팅 주소를 복사했습니다.

이 글이 도움이 되었다면 공감 부탁드립니다.