티스토리 뷰
MyBatis 를 사용하여 개발하는 경우 쿼리를 수정했을때 WAS를 재기동해야 변경된 내용이 반영된다. 클래스가 몇개 없는 작은 프로젝트에서는 이렇게 재기동을 해서 확인을 하는것이 큰 부담이 없지만 재기동되는데 시간이 오래 걸리는 프로젝트인 경우는 이것은 큰 부담이 된다. 이를 해결해주기 위해 어느 능력자분이 서버의 재기동 없이 Query Reload를 해주는 로직을 개발해주셨다. RefreshableSqlSessionFactoryBean 라는 아주 유명한 로직이다. 언제 누가 만든건지 모르지만 일단 그분께 감사를 드린다. 2011년도에도 이걸 사용했던것 같은데 벌써 10년이 지났다. 시간 참 빠르다.
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 java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
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;
public class RefreshableSqlSessionFactoryBean extends SqlSessionFactoryBean implements DisposableBean {
private static final Logger LOG = LoggerFactory.getLogger(RefreshableSqlSessionFactoryBean.class);
private SqlSessionFactory proxy;
private int interval = 1000;
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 {
w.lock();
try {
super.afterPropertiesSet();
} finally {
w.unlock();
}
LOG.info("sqlMapClient refreshed.");
}
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 {
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) {
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();
}
}
일단은 이 유명한 RefreshableSqlSessionFactoryBean 클래스를 적당한 곳에 생성을 해 놓는다.
그리고 SqlSessionFactory를 설정하는 곳에서 다음과 같이 설정을 해준다.
****-context.xml (spring application context 설정부)
<!--<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">-->
<bean id="sqlSessionFactory" class="com.oingdaddy.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" />
</bean>
1 line에 있는 기존의 org.mybatis.spring.SqlSessionFactoryBean 대신 RefreshableSqlSessionFactoryBean을 끼워준다. 그리고 RefreshableSqlSessionFactoryBean 에서 설정 가능한 interval이라는 속성도 넣어준다. Refresh를 해주는 주기이다. (ms) 즉 1초다마 계속 Refresh를 해주겠다는거다. 이정도면 고치고 저장을 하면 거의 실시간으로 변경된 내용이 반영이 된다고 보면 된다.
이렇게 설정까지 만들었다면 WAS가 기동된 상태에서 특정 쿼리를 수정해보자. 그리고 바로 결과가 잘 반영이 되는지 확인을 해보자. RefreshableSqlSessionFactoryBean 관련된 로그가 나오며 update 되었다는 문구가 나오면 성공이다.
운영에서 사용해서도 검증이 됐다고는 하지만 모험을 하지는 않으려고 한다. 개발시에만 개발생산성 향상을 위해 사용할 계획이다.
요즘 개발환경인 Springboot 기반의 프로젝트에 적용하기 위해 Java Config로 이 부분을 설정하는 것도 조만간 시도를 해 볼 계획 (아래글에서 Springboot 기반의 프로젝트에서 Java Config로 설정하는것을 확인하도록 하자. )이다.
끝!
'Framework > Spring' 카테고리의 다른 글
Springboot Profile 설정방법 및 가져오기 (1) | 2021.09.23 |
---|---|
Springboot + MyBatis에서 Query 변경시 서버 재시작 없이 Reload 하기 (w. RefreshableSqlSessionFactoryBean) (8) | 2021.09.23 |
Spring Boot Transaction Timeout 설정 및 기본값 (0) | 2021.09.02 |
Springboot + Ehcache 초간단 설정 및 사용방법 (0) | 2021.08.26 |
Springboot Multi Datasource 설정 및 사용법 (0) | 2021.08.04 |