<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>빛을 담고 세상 넓히기 &#187; iBatis</title>
	<atom:link href="http://fantazic.com/archives/tag/ibatis/feed" rel="self" type="application/rss+xml" />
	<link>http://fantazic.com</link>
	<description>마음의 빛으로 넓은 세상을 비추고 싶다.</description>
	<lastBuildDate>Sun, 27 Nov 2011 23:52:11 +0000</lastBuildDate>
	
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>iBatis에서 batch 기능 활용하기</title>
		<link>http://fantazic.com/archives/228</link>
		<comments>http://fantazic.com/archives/228#comments</comments>
		<pubDate>Tue, 07 Apr 2009 00:19:40 +0000</pubDate>
		<dc:creator>따지크</dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[batch]]></category>
		<category><![CDATA[iBatis]]></category>

		<guid isPermaLink="false">http://fantazic.com/?p=228</guid>
		<description><![CDATA[
Tweet

iBatis는 아래와 같은 방법으로 batch 처리가 가능하다.  iBatis 내부 코드를 확인해 본 바로는 PreparedStatement.addBatch()를 사용하고 있고, 동일한 쿼리가 반복해서 들어올 때 하나의 batch로 처리해준다.

try {
  SqlMapClient.startTransaction();
  SqlMapClient.startBatch();
  while (...) {
    SqlMapClient.insert(query, params);
  }
  SqlMapClient.executeBatch();
  SqlMapClient.commitTransaction();
} catch (Exception e) {
  log.error(e, e);
} finally {
  [...]]]></description>
			<content:encoded><![CDATA[<div style="display:block;margin-left: 10px; margin-bottom: 10px;">
<a href="http://twitter.com/share" class="twitter-share-button" data-url="http://fantazic.com/archives/228" data-text="iBatis에서 batch 기능 활용하기" data-count="horizontal" >Tweet</a><script type="text/javascript" src="http://platform.twitter.com/widgets.js"></script>
</div>
<p>iBatis는 아래와 같은 방법으로 batch 처리가 가능하다.  iBatis 내부 코드를 확인해 본 바로는 PreparedStatement.addBatch()를 사용하고 있고, 동일한 쿼리가 반복해서 들어올 때 하나의 batch로 처리해준다.</p>
<pre>
try {
  SqlMapClient.startTransaction();
  SqlMapClient.startBatch();
  while (...) {
    SqlMapClient.insert(query, params);
  }
  SqlMapClient.executeBatch();
  SqlMapClient.commitTransaction();
} catch (Exception e) {
  log.error(e, e);
} finally {
  SqlMapClient.endTransaction();
}
</pre>
<p>이 기능을 활용해서 BatchManager를 만들어서 사용하고 있는데, 사용자의 로그인 시간을 기록하거나 게시물의 조회수를 늘리는 등 <strong>빈번하게 동일한 update가 발생하는 서비스에 사용하면 효과</strong>가 있다.</p>
<p>사용법은 기존의 서비스 코드 수정을 최소화하는 방법으로 고안했다. SqlMapClient.insert(query, params)를 BatchManager.insert(query, params)로 수정하면 된다.</p>
<p><strong>관련 코드)</strong><br />
BatchManager.java</p>
<pre>
public class BatchManager {
  private static BatchWorker worker = BatchWorker.getInstance();
  static {
    Runtime.getRuntime().addShutdownHook(new Thread() {
      public void run() {
        try {
          worker.flushAll();
          worker.stop();
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    });
  }

  public static void insert(String query, Object params) {
    worker.put(new BatchQuery(INSERT, query, params));
  }

  public static void update(String query, Object params) {
    worker.put(new BatchQuery(UPDATE, query, params));
  }
}
</pre>
<p>BatchWorker.java</p>
<pre>
public class BatchWorker {
  public static final int HEARTBEAT = 1000;
  public static int MAX_WAIT = 30000;
  public static final int SIZE_OF_ONE_BATCH = 200;
  public static final int MAX_SIZE = 100;
  private long lastTime;
  private final Timer timer;
  private Vector<BatchQuery> queue;
  private static BatchWorker singletonWorker;

  private BatchWorker() {
    lastTime = System.currentTimeMillis();
    timer = new Timer(true);
    queue = new Vector<BatchQuery>();
    startWorker();
  }

  public static synchronized BatchWorker getInstance() {
    if (singletonWorker == null)
      singletonWorker = new BatchWorker();
    return singletonWorker;
  }

  private void startWorker() {
    timer.scheduleAtFixedRate(new TimerTask() {
      @Override
      public void run() {
        try {
          doBatch();
        } catch (Exception e) {
          // ignore
          e.printStackTrace();
        }
      }
    }, 0, HEARTBEAT);
  }

  private void doBatch() {
    if (System.currentTimeMillis() - lastTime > MAX_WAIT
      || queue.size() >= MAX_SIZE) {
      executeQuery();
      lastTime = System.currentTimeMillis();
    }
  }

  private synchronized void executeQuery() {
    if (queue.size() == 0)
      return;
    try {
      int cnt = 0;
      SqlMapClient.startTransaction();
      SqlMapClient.startBatch();
      while (cnt++ < SIZE_OF_ONE_BATCH) {
        if (queue.size() == 0)
          break;
        BatchQuery batchQuery = queue.remove(0);
        switch (batchQuery.type) {
          case INSERT:
            SqlMapClient.insert(batchQuery.query, batchQuery.params);
            break;
          case UPDATE:
            SqlMapClient.update(batchQuery.query, batchQuery.params);
            break;
          default:
            break;
        }
      }
      SqlMapClient.executeBatch();
      SqlMapClient.commitTransaction();
    } catch (Exception e) {
      log.error(e, e);
    } finally {
      SqlMapClient.endTransaction();
    }
  }

  public int size() {
    return queue.size();
  }

  public void put(BatchQuery query) {
    queue.add(query);
  }

  public void flushAll() {
    while (queue.size() > 0)
      executeQuery();
  }

  public void stop() {
    timer.cancel();
  }
}
</pre>
<p>BatchQuery.java</p>
<pre>
public class BatchQuery {
  public enum QueryType {
    INSERT, UPDATE
  }

  public String query;
  public Object params;
  public QueryType type;

  public BatchQuery(QueryType type, String query, Object params) {
    this.type = type;
    this.query = query;
    this.params = params;
  }
}
</pre>
<p><strong>참고)</strong></p>
<ul>
<li>Oracle10g 환경에서는 batch로 처리할 경우 쿼리 수행은 빨라지나 batch 처리마다 쿼리 파싱이 발생해서 CPU 비용은 증가하는 경우도 보였다.</li>
<li>예전에 찾아본 바로는 한번에 만건 이상도 batch 처리가 가능하다고 한다. 환경에 따라 가장 효율적인 batch 크기를 결정해야 한다.</li>
<li>batch 처리할 경우 수행속도가 빨라지는 장점이 있고 transaction lock이 적게 잡혀 DB 부담을 줄여주는 효과도 있다.</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://fantazic.com/archives/228/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>iBatis 환경에서 DB 유닛 테스트를 위한 팁</title>
		<link>http://fantazic.com/archives/222</link>
		<comments>http://fantazic.com/archives/222#comments</comments>
		<pubDate>Tue, 24 Mar 2009 11:21:48 +0000</pubDate>
		<dc:creator>따지크</dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[iBatis]]></category>
		<category><![CDATA[TestCase]]></category>

		<guid isPermaLink="false">http://fantazic.com/?p=222</guid>
		<description><![CDATA[
Tweet

최근에는 새로운 기능을 개발할 때 test case를 먼저 작성하는 경우가 많다. 특히 DB와 연동되는 기능을 개발할 경우 다양한 종류의 실수로 작업이 지연될 수 있기 때문에 unit test를 꼭 먼저 작성하고 개발을 시작한다.
하지만 일반적인 개발환경에서 테스트 데이터가 DB에 직접 쌓이게 되면 데이터가 꼬이는 경우가 발생할 수 있어서 이를 해결할 방법이 필요하다. 예를 들어 게시판에 글을 쓰고, [...]]]></description>
			<content:encoded><![CDATA[<div style="display:block;margin-left: 10px; margin-bottom: 10px;">
<a href="http://twitter.com/share" class="twitter-share-button" data-url="http://fantazic.com/archives/222" data-text="iBatis 환경에서 DB 유닛 테스트를 위한 팁" data-count="horizontal" >Tweet</a><script type="text/javascript" src="http://platform.twitter.com/widgets.js"></script>
</div>
<p>최근에는 새로운 기능을 개발할 때 test case를 먼저 작성하는 경우가 많다. 특히 DB와 연동되는 기능을 개발할 경우 다양한 종류의 실수로 작업이 지연될 수 있기 때문에 unit test를 꼭 먼저 작성하고 개발을 시작한다.</p>
<p>하지만 일반적인 개발환경에서 테스트 데이터가 DB에 직접 쌓이게 되면 데이터가 꼬이는 경우가 발생할 수 있어서 이를 해결할 방법이 필요하다. 예를 들어 게시판에 글을 쓰고, 수정하고, 지우는 기능을 테스트하면서 오류로 글이 지워지지 않고 남는 경우가 있을 수 있고, 새로운 데이터를 입력하는 테스트인 경우 unique key 제한에 걸려 매번 테스트 데이터를 수정해야 하는 불편함이 따를 수 있다.</p>
<p>이 문제를 피해가기 위해 여러가지 방법을 활용할 수 있는데 Spring에서는 <a href="http://whiteship.tistory.com/170">AbstractTransactionalSpringContextTests</a>와 같은 방식으로 이를 해결하고 있다. iBatis 환경에서는 다음과 같이 JUnit4의 기능을 활용해서 DB unit test를 편하게 할 수 있다.</p>
<pre>
public class TransactionalTestCase {

  @BeforeClass
  public static void create() {
    SqlMapClient.startTransaction();
  }

  @AfterClass
  public static void destroy() {
    SqlMapClient.endTransaction();
  }

  protected ResultSet executeQuery(String sql) throws SQLException {
    Connection conn = SqlMapClient.getConnection();
    Statement st = conn.createStatement();
    return st.executeQuery(sql);
  }

  protected int executeUpdate(String sql) throws SQLException {
    Connection conn = SqlMapClient.getConnection();
    Statement st = conn.createStatement();
    return st.executeUpdate(sql);
  }

  protected int countTableRows(String tableName) throws SQLException {
    int count = 0;
    ResultSet rs = executeQuery("select count(*) from " + tableName);
    while (rs.next()) {
      count = rs.getInt(1);
    }
    return count;
  }

  protected void deleteTable(String tableName) throws SQLException {
    executeUpdate("delete from " + tableName);
  }
}
</pre>
<p>기본적으로 클래스의 시작과 끝에 Transaction을 걸어주고 있고, 데이터 초기화 기능 및 간단한 테이블 조회 기능을 추가했다. 각자의 환경에 따라 세부적인 코드는 변할 수 있겠지만 이 코드를 바탕으로 쉽게 DB unit test를 할 수 있는 기회가 될 수 있기를 바란다.</p>
]]></content:encoded>
			<wfw:commentRss>http://fantazic.com/archives/222/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

