2019.03.24 18:35

요즘 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 sbcoba
*/
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*:패키지경로/**/mapper.xml" :configLocation="classpath:/MapperConfig.xml" p:dataSource-ref="dataSource" p:interval="1000" />

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


 

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

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

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


 

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


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



https://sbcoba.tistory.com/16

Posted by 물색없는세상
2019.03.23 23:04

보통 전자정부같은 Spring기반의 프로젝트를 할때 에러가 나던가 혹은 서버에서 어떤 액션이 일어나면 이클립스 콘솔창에


많은 정보들이 후두두둑 하고 올라간다.


내가 날린 쿼리가 무엇인지, 내가 요청한 URL은 무엇있지 버그나 오류가 있을땐 어떤 오류인지 온갖 정보들이 나타나는데


가끔 쿼리를 실행해도 어떤 쿼리를 실행했는지 나타나지 않는 경우가 있다. 단지 오류가 났을때만 콘솔창에 로그를 뿌려주는 경우


이럴때 버그가 있더라도 시스템에서 오류가 나지 않는다면  어떤 쿼리를 실행시켰는지 어떤 url을 요청했는지 알기 힘들게 된다.



이런경우의 대부분은 로깅 프로퍼티의 설정 레벨이 warn 정도로 되어 있는 경우 인것 같다.


정상적인 쿼리도 콘솔창에 보이게 할수 있도록 설정하는 방법을 알아보자.



log4j를 쓰던 log4j2를 쓰던 logback를 쓰던 설정 xml파일의 이름엔 [log]가 들어간다


프로젝트의 구성,설계에 따라 해당 xml파일은 리소스에 들어가는 경우도 있고 web-inf에 들어가는 경우도 있다


파일을 열어 보면


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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration 
    xmlns:log4j="http://jakarta.apache.org/log4j/" 
    debug="false">
    
    <appender name="console" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %5p [%c] %m%n" />
        </layout>   
    </appender>
    
    <logger name="egovframework" additivity="false">
        <level value="DEBUG"/>
        <appender-ref ref="console"/>
    </logger>
 
    <!-- log SQL with timing information, post execution -->
    <logger name="jdbc.sqltiming" additivity="false">
        <level value="WARN" />
        <appender-ref ref="console" />
    </logger>
 
    <logger name="jdbc.audit" additivity="false">
        <level value="WARN" />
        <appender-ref ref="console" />
    </logger>
    
    <logger name="jdbc.resultset" additivity="false">
        <level value="WARN" />
        <appender-ref ref="console" />
    </logger>
    
    <logger name="org.springframework" additivity="false">
        <level value="INFO" />
        <appender-ref ref="console" />
    </logger>
    
    <logger name="java.sql" additivity="false">
        <level value="WARN"/
        <appender-ref ref="console"/
    </logger
    
    <root>
        <level value="INFO" />
        <appender-ref ref="console" />
    </root>
                       
</log4j:configuration>
 
cs


이런식으로 되어 있을 것이다. 이부분은 log4j나 logback나 크게 다른 부분은 없다.


단지 로거 안에 레벨이 들어가냐 안들어가냐 어펜드가 포함되냐 안되냐 그리고 최상단에의 정보들같은  이런 약간의 차이만 있을 뿐이다.



보면 <level value="warn"/> 이라고 되어있는데


여기서 밸류값에 들어가는 종류는


DEBUG, INFO, WARN, ERROR, FATAL, ALL, OFF 가 있다.



ALL은 모든 로깅

OFF는 로깅 해제


DEBUG = 디버깅

INFO = 강조정보

WARN = 경고

ERROR = 오류

FATAL = 심각한 오류


라는 의미 이며


WARN은 경고수준 이하 레벨의 정보는 로깅하지 않는 다는 뜻이다

그러니 정상적으로 돌아가는 SQL쿼리문은 콘솔창에 나타나지 않으니


레벨의 밸류를 전부 DEBUG 로 바꿔주자


로거 name="" << 이부분의 네임은 어떤 부분에서 로그를 띄워줄지 적는 부분이다.


뭔지 모르겠고 sql만 띄우고 싶다면 jdbc.sqltiming  이부분의 레벨만 DEBUG 로 바꿔보도록 하자


설정에 따라  


1
2
3
4
5
6
7
<logger name="org.springframework" level="DEBUG " additivity="false">
     <appender-ref ref="console" />
</logger>
  
<logger name="org.mybatis" level="DEBUG "  additivity="false">
    <appender-ref ref="console" />
</logger>
cs


이런식의 스프링프레임워크나 마이바티스에 관한 로그를 띄울 수도 있다.



아무튼 원하는 부분의 레벨을 DEBUG로 설정한 후 구동시키면 정상적으로 실행되는 정보들도 출력이 된다.


additivity 이부분은 

http://seosh81.info/?p=404  << 이분의 블로그에서 잘 설명이 되어 있으니 궁금하면 들어가서 보자


간단히 설명하자면 로그이벤트를 부모에게 전달하도록 하느냐 마느냐 정도의 설정부분인것 같은데


어차피 나는 야매개발자라서 정확하고 자세하겐 모른다.



 <appender name="console" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %5p [%c] %m%n" />
        </layout>   
    </appender>


이부분에서 ConversionPattern이부분의 밸류는 콘솔에 표현되는 표현식을 지정하는 부분이다.


표현식은 아래와 같다.


%m: 로그 내용 출력
%p: debug, info, warn, error, fatal 등의 priority 출력
%r: 어플이 시작 후 이벤트가 발생하는 시점까지의 경과시간 밀리세컨드로 출력
%c: package 출력
%c{n}: n(숫자) 만큼의 package를 가장 하단 부터 역으로 출력
        예) %c{2} 일때 a.b.c 는 b.c 로 출력된다.
%n: 개행문자 출력. 플렛폼에 따라 \r\n 또는 \n 출력.
%d: 이벤트 발생 날짜 출력 ( 프로그램의 실행속도를 느리게 한다.)
        예) %d{HH:mm:ss} 또는 %d{dd MMMM yyyy HH:mm:ss}
%C: 호출자의 클래스명 출력
        예) %C{2} 일때 a.b.c.TestClass 는 c.TestClass 로 출력된다.
%M: method 이름.
%F: 프로그램 파일명.
%l: caller의 정보
%L: caller의 라인수
%x: thread와 관련된 NDC(nested diagnostic context) 
%X: thread와 관련된 MDC(mapped diagnostic context) 
%%: % 표시를 출력  
%t: 쓰레드 이름


로그띄울줄 몰라서 들어온 사람이라면 어차피 저 표현식을 쓸일은 없을테니 그냥 이런게 있다는것만 알아두고 넘어가자.



 

https://yamea-guide.tistory.com/108

Posted by 물색없는세상
2015.03.04 13:15
이번에 다이나믹 쿼리를 이용하여 간단히 데이터 배치작업을 하려고 만든 프로그램에서 에러가 발생했다.
평소엔 SQL-MAP 파일에 SQL Injection이 발생할 수 있는 코드는 사용하지 않았기 때문에 이런 문제가 발생하지 않았는데 배치작업을 위해 여러 테이블에서 두루 사용하기 위한 쿼리문을 등록하려다 보니 자연히 SQL Injection이 발생될 수 있는(보안상 안좋은) 쿼리를 등록해서 사용하게 되었다.

문제의 쿼리는 아래와 같았다.
<select id="getList" parameterClass="map" resultClass="java.util.HashMap">
SELECT
<iterate property="fieldName" prepend=",">
$fieldName[]$
</iterate>
FROM
$tableName$
WHERE
$condition$
</select>
여러 테이블을 조회하기 때문에 파라미터로 입력되는 모든 값들이 변경될 수 있다.
호출은 아래와 같이 했다.
List fieldList = new ArrayList();
fieldList.add("foo");
fieldList.add("bar");
Map map = new HashMap();
map.put("tabeName", "Hello");
map.put("condition", "idx > 0");
map.put("fieldName", fieldList);
List<Map> result = sqlMapClient.queryForList("map-id.getList", map);
위의 코드가 한번만 호출이 된다면 아무런 문제가 없이 해결이 된다.
하지만 아래와 같이 fieldList와 tableName이 변경하여 다시 조회하면 에러가 발생한다.

List fieldList = new ArrayList();
fieldList.add("foo2");
fieldList.add("bar2");
Map map = new HashMap();
map.put("tabeName", "Hello2");
map.put("condition", "idx > 0");
map.put("fieldName", fieldList);
List<Map> result = sqlMapClient.queryForList("map-id.getList", map);
이는 iBatis에서 SQL-ID가 한번 실행되면 쿼리의 Meta Data를 캐쉬하고 있기 때문으로 다음번에 호출되었을 때 이미 캐쉬에 저장된 foo, bar 필드를 쿼리결과에서 찾는데 필드가 없기 때문(foo2, bar2가 존재할 뿐)에 에러를 리턴하게 된다. MetaData의 캐쉬를 하지 않으려면 쿼리를 다음과 같이 변경해야 한다.

<select id="getList" parameterClass="map" resultClass="java.util.HashMap" remapResults="true">
SELECT
<iterate property="fieldName" prepend=",">
$fieldName[]$
</iterate>
FROM
$tableName$
WHERE
$condition$
< /select>

http://kamkami.tistory.com/102

Posted by 물색없는세상
2012.10.17 14:35

SqlMaps를 작성할때, 종종 SQL의 일부가 중복되곤한다. 예를 들어, FROM절이나 제약조건등이 그렇다. iBATIS는 이러한 중복되는 SQL문의 일부를 재사용하기 위한 강력한 태그를 제공한다. iBATIS사용시 대개 다음처럼 작성할것이다.

<select id="selectItemCount" resultClass="int">
  SELECT COUNT(*) AS total
  FROM items
  WHERE parentid = 6
</select>

<select id="selectItems" resultClass="Item">
  SELECT id, name
  FROM items
  WHERE parentid = 6
</select>

여기서는 FROM절 이하가 모두 중복이다. 이 중복을 제거하기 위해서, <sql> 과 <include> 태그를 사용한다. <sql> 태그는 재사용하기 위한 조각을 포함한다. <include> 태그는 이러한 조각을 포함한다.

<sql id="selectItem_fragment">
  FROM items
  WHERE parentid = 6
</sql>
<select id="selectItemCount" resultClass="int">
  SELECT COUNT(*) AS total
  <include refid="selectItem_fragment"/>
</select>
<select id="selectItems" resultClass="Item">
  SELECT id, name
  <include refid="selectItem_fragment"/>
</select>

<include> 태그는 명명공간을 인식한다. 그래서 당신은 다른 map에 위치한 조각들도 참조할수 있다(어쨌든 iBATIS가 SqlMaps를 로드하는 방식에 따라, 포함된 조각은 statement를 포함하기 전에 로드될것이다). 조각은 파라미터처럼 사용될수 있기 때문에 쿼리-수행시 포함되고 처리된다.

<sql id="selectItem_fragment">
  FROM items
  WHERE parentid = #value#
</sql>
<select id="selectItemCount" parameterClass="int" resultClass="int">
  SELECT COUNT(*) AS total
  <include refid="selectItem_fragment"/>
</select>
<select id="selectItems" parameterClass="int" resultClass="Item">
  SELECT id, name
  <include refid="selectItem_fragment"/>
</select>

http://harurun.blog.me/120055300736

Posted by 물색없는세상