본문 바로가기
웹개발/기타

Spring + MyBatis 쿼리 수정 시 재 시작 없이 반영하기

by heavenLake 2022. 7. 26.
반응형

 

출처 : https://shxrecord.tistory.com/196

 

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

 

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

 

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

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

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

 

RefreshableSqlSessionFactoryBean.java

RefreshableSqlSessionFactoryBean.java
0.01MB

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 파일을 수정할 때마다 아래와같은 로그를 확인할 수 있을 것이다.

 

 

 

출처: https://shxrecord.tistory.com/196 [첫 발:티스토리]

 

반응형

댓글