티스토리 뷰

이전에 Spring + MyBatis에서 Query 변경시 서버 재시작 없이 Reload 하기에 대한 글을 작성했었다. 

 

Spring + Mybatis 에서 Query 변경시 서버 재시작 없이 Reload 하기

MyBatis 를 사용하여 개발하는 경우 쿼리를 수정했을때 WAS를 재기동해야 변경된 내용이 반영된다. 클래스가 몇개 없는 작은 프로젝트에서는 이렇게 재기동을 해서 확인을 하는것이 큰 부담이 없

oingdaddy.tistory.com

하지만 요즘 대세인 Springboot 환경에서 적용하기 위해서 Java Config로 이를 전환하였다. Xml Config에서 Java Config로 전환을 하는데 RefreshableSqlSessionFactoryBean을 그대로 사용하니 잘 동작하지 않았다.

그래서 RefreshableSqlSessionFactoryBean의 proxy를 초기화 시켜주는 부분을 조금 변경하였다. 

 

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

    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() throws Exception {
        if(this.proxy == null) {
            setRefreshable();
        }
        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();
    }
}

여기서 139 linegetObject() 해오는 부분에서 proxy에 값을 넣을수 있도록 변경을 해주고 67 line의 afterPropertiesSet에서는 setRefreshable()을 호출하는 부분을 삭제했다. 

 

그리고 이것을 설정하는 Java Config는 다음과 같다. 

@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
        
	SqlSessionFactoryBean factoryBean = new RefreshableSqlSessionFactoryBean();
	factoryBean.setDataSource(dataSource);
	factoryBean.setVfs(SpringBootVFS.class);
	factoryBean.setConfigLocation(applicationContext.getResource("classpath:mybatis/mybatis-config.xml"));
	ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
	Resource[] resource = resolver.getResources("classpath*:mybatis/sql/**/*.xml");
	factoryBean.setMapperLocations(resource);
	((RefreshableSqlSessionFactoryBean) factoryBean).setInterval(1000);

	return factoryBean.getObject();
}

이렇게 적당한 Config 클래스에 추가를 해주자. 물론 ConfigLocaction이라던지 MapperLocataion은 프로젝트에 맞게 변경을 해줘야 한다. Xml Config와 마찬가지로 interval 을 추가해서 갱신주기도 설정해 주도록 하자 (1000 = 1초)

 

이렇게 만든 sqlSessionFactory를 SqlSessionTemplate이라던지 CommonDao 같은 곳에 넣어서 사용하도록 하자. 

 

설정이 다 되었으면 query를 변경한 후 저장을 누르면 

이런식으로 쿼리가 변경된것을 감지하고 refresh 해주는것을 확인할 수 있다. 

 

끝!

댓글
최근에 올라온 글
최근에 달린 댓글
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31