요즘 ORM으로는 하이버네이트, JPA등 많이 사용하고 있으나, 역시 SI 쪽은 mybatis(ibatis)를 많이 사용된다.

문제는 mybatis는 xml로 sql을 관리하고 있는데 보통 조금 바꿀때 마다 서버를 재구동 시켜야 되는 문제가 된다.

시스템이 클 경우 재시작시 오랜 시간 걸리고 session 사용시 또 로그인을 해야 하는듯 개발의 흐름이 끊어지는 문제가 많이 발생한다.

예전에 ibatis를 사용 했을시에도 그런 부분이 많이 불편했었는데, 예전 대우정보시스템의 JCF 프레임워크에서 사용된다고 Refresh 되는 클래스 소스가 한번 공개 된적이 있었다. ( 몇년전인지 기억은 안나지만, 당시 인터넷 검색으로 찾았었다. )

그것이 버전이 문제인지 바로 사용이 안되어서 커스터마이징하고 사용을 잘사용 했었다.

그런데 지금 프로젝트가 mybatis로 진행하기 때문에 예전과 같은 불편함이 또 생기게 되었는데, 이 번에는 그 소스를 mybatis에 맞도로 커스터마이징 하기로 했다.  

일단 사전 조건은 

JDK 1.5 이상, Spring, mybatis, spring-mybatis 라이브러리가 설치되어 있는 환경에서만 된다.


일단 기존 Spring 에서 mybatis 설정을 보겠다.

보통 sqlSessionFactory를 이렇게 설정 한다. 

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:mapperLocations="classpath*:패키지경로/**/mapper.xml" p:configLocation="classpath:/MapperConfig.xml" p:dataSource-ref="dataSource"/>

classpath*:패키지경로/**/mapper.xml  <- 이부분이 재로딩될 xml 파일 경로

이부분에서 굵게 표시한 class 부분만 새로 만든 클래스로 바꾸면 모든게 해결된다.

<bean id="sqlSessionFactory" class="패키지경로.RefreshableSqlSessionFactoryBean" p:mapperLocations="classpath*:패키지경로/**/mapper.xml" p:configLocation="classpath:/MapperConfig.xml" p:dataSource-ref="dataSource" />

RefreshableSqlSessionFactoryBean.java

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;


/**

 * mybatis mapper 자동 감지 후 자동으로 서버 재시작이 필요 없이 반영

 *

 * @author

 *

 */

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();

}

}


만약에 재로딩 되는 시간을 바꾸고 싶으면 

<bean id="sqlSessionFactory" class="패키지경로.RefreshableSqlSessionFactoryBean" p:mapperLocations="classpath*:kr/web/**/mapper.xml" :configLocation="classpath:/MapperConfig.xml" p:dataSource-ref="dataSource" p:interval="1000" />

 p:interval="1000" 이부분을 수치를 정해주면된다. ( 디폴트는 500, 단위 ms )


이제 설정을 서버를 시작해서 위에 로케이션 해당하는 mapper.xml ( sql이 있는 xml , 설정에 따라 다름)에서

sql을 바꿔 보자 그리고 클라이언트에서 바뀐지 확인해보자

큰 문제가 없다면 반영될것이라고 생각된다. 


아 단 운영 시스템에 사용은 보장 못합니다~ 개발시에서만 사용하세요~


이 소스는 예전에 인터넷에서 나돌던 RefreshableSqlMapClientFactoryBean 소스를 커스터마이징한 소스인데 문제가 있다면 연락주시길바랍니다.



원문:http://sbcoba.tistory.com/entry/Spring-mybats-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EC%9E%AC%EC%8B%9C%EC%9E%91-%EC%97%86%EC%9D%B4-%EC%84%9C%EB%B2%84-%EB%B0%98%EC%98%81

Posted by gofly

댓글을 달아 주세요