목적

Spring + Mybatis로 구성된 서비스를 운영하던 중 사용자가 몰렸을 때 DB에 부담을 덜어주기 위한 방법을 고민했다.
Mybatis 의 ExcutorType 을 Batch로 설정하면 성능이 향상 되었는데 이유를 파악하기 위해 포스트 남긴다.

결론적으로 Batch 는 statement를 재사용 하며 Create, Insert, Update를 벌크로 모아서 처리하고, 
Select를 만나면 기존에 모아놓았던 쿼리들을 수행하기 때문에 C, I, U 작업은 연달아서 할 때 가장 효율적이었다.
트랜잭션과, select문이 끼어드는 위치를 잘 조정하여 사용하면  DB의 부담을 덜 수 있을것으로 보인다. 

Batch Processing? - wiki

일괄 처리(batch processing)란 컴퓨터 프로그램 흐름에 따라 순차적으로 자료를 처리하는 방식을 뜻한다.
일괄 처리 시스템(batch system)이란 일괄처리(batch processing) 방식이 적용된 시스템으로서, 하나의 작업이 끝나기 전까지는 다른 작업을 할 수 없다.

장점 
  • 많은 사용자 사이에서 컴퓨터 자원을 공유할 수 있다.
  • 작업 프로세스의 시간대를 컴퓨터 리소스가 덜 사용되는 시간대로 이동한다.
  • 분 단위의 사용자 응답 대기와 더불어 컴퓨터 리소스의 유휴 사용을 피한다.
  • 전반적인 이용률을 높임으로써 컴퓨터의 비용을 더 잘 상환하도록 도와 준다.

JDBC에서 Batch Processing 

JDBC 2.0는 batch proccessing 은 다음과 같이 제공한다.
1. Prepare statement.
2. Bind parameters.
3. Add to batch.
4. Repeat steps 2 and 3 until interest has been assigned for each account.
5. Execute.

preparedStatement를 사용한 Batch Processing

PreparedStatement stmt = conn.prepareStatement(
    "UPDATE account SET balance = ? WHERE acct_id = ?");"UPDATE account SET balance = ? WHERE acct_id = ?");
int[] rows;

for(int i=0; i<accts.length; i++) {
    accts[i].calculateInterest( );
    stmt.setDouble(1, accts[i].getBalance( ));
    stmt.setLong(2, accts[i].getID( ));
    stmt.addBatch( );
}
rows = stmt.executeBatch( );
  • addBatch() : create, insert, update, deleted 일괄 처리 결과를 관리 할 필요가 없기 때문에 batch에 statement를 추가하는 일만 한다.
  • excuteBatch()가 쿼리들을 수행하고 정수형 배열을 리턴하는데, 각 배열의 요소는 각 statement로 인해 update된 row의 숫자이다. 

batch모드로 수행할 때 auto-commit을 사용하면 각각의 statement 를 수행 후 commit이 발생한다.
그래서 batch 수행 중 에러가 발생하면 에러 발생 전까지 statement는 DB 에 반영되어있고 BatchUpdateException에서 getUpdateCounts()로 성공한 쿼리의 숫자를 얻어 올 수 있다.
실제 배치 과정에서, 각 statement 마다 commit을 하면 배치의 성능이 떨어지게 되는데 이것을 트랜잭션으로 묶어서 관리하려고 해도 성능에 악영향을 미친다. 가장 좋은 방법은 임의의 수 만큼 statement 를 묶어서, 명시적으로 commit 하는 것 같다. 


Mybatis Batch Mode

Mybatis는 SqlSessionFactory에서 SqlSessionTemplate(SqlSession 구현체)을 생성할 때 ExcutorType을 지정해 줄 수 있다.

ExcutorTypeDescription
SIMPLEexecutor does nothing special 
REUSEexecutor reuses prepared statements 
BATCHexecutor reuses statements and batches updates


ExecutorType.BATCH : 이 실행자는 모든 update(C,I,U,D)구문을 배치처리하고 중간에 select 가 실행될 경우 필요하다면 경계를 표시한다.

여기서 이 경계는 아래 소스코드를 통해 확인 할 수 있다. 


SqlSessionTemplate에서 ExecutorType이 BATCH인 경우에 Create, Insert, Update, Delete 구문은 doUpdate 에서, Select 구문은 doQuery 에서 처리한다.

아래 코드의 59번 줄에서 statement를 재사용하는 로직을 확인 할 수 있고, 61번, 69번 줄에서 statement 재사용 여부에 따라 statementList에 statement를 추가하고 63번, 70번 줄에서 파라미터 추가, 다른 구문일 경우 BatchResult를 추가하는 것을 볼 수 있다.

그리고 doQuery 메소드의 81번줄에서 flushStatements()를 호출 하는데, 이는 그동안 statementList에 쌓아 왔던 statement를  executeBatch() 메소드로 수행한다. 수행 결과는 batchResult에 저장하여 return 하고, 그동안 쌓아왔던 statementList 와 batchResultList 를 초기화한다.

아래 코드처럼 Select문이 실행 될 경우 BATCH 처리를 멈추고 초기화 하기 때문에, BATCH 작업 도중 SELECT 문이 끼어들면 성능이 저하된다. 


org.apache.ibatis.executor.BatchExecutor.class - grepcode

40 public class More ...BatchExecutor extends BaseExecutor {
41
42   public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
43
44   private final List<Statement> statementList = new ArrayList<Statement>();
45   private final List<BatchResult> batchResultList = new ArrayList<BatchResult>();
46   private String currentSql;
47   private MappedStatement currentStatement;
48
49   public More ...BatchExecutor(Configuration configuration, Transaction transaction) {
50     super(configuration, transaction);
51   }
52
53   public int More ...doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
54     final Configuration configuration = ms.getConfiguration();
55     final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
56     final BoundSql boundSql = handler.getBoundSql();
57     final String sql = boundSql.getSql();
58     final Statement stmt;
59     if (sql.equals(currentSql) && ms.equals(currentStatement)) {
60       int last = statementList.size() - 1;
61       stmt = statementList.get(last);
62       BatchResult batchResult = batchResultList.get(last);
63       batchResult.addParameterObject(parameterObject);
64     } else {
65       Connection connection = getConnection(ms.getStatementLog());
66       stmt = handler.prepare(connection);
67       currentSql = sql;
68       currentStatement = ms;
69       statementList.add(stmt);
70       batchResultList.add(new BatchResult(ms, sql, parameterObject));
71     }
72     handler.parameterize(stmt);
73     handler.batch(stmt);
74     return BATCH_UPDATE_RETURN_VALUE;
75   }
76
77   public <E> List<E> More ...doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
78       throws SQLException {
79     Statement stmt = null;
80     try {
81       flushStatements();
82       Configuration configuration = ms.getConfiguration();
83       StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
84       Connection connection = getConnection(ms.getStatementLog());
85       stmt = handler.prepare(connection);
86       handler.parameterize(stmt);
87       return handler.<E>query(stmt, resultHandler);
88     } finally {
89       closeStatement(stmt);
90     }
91   }
92
93   public List<BatchResult> More ...doFlushStatements(boolean isRollback) throws SQLException {
94     try {
95       List<BatchResult> results = new ArrayList<BatchResult>();
96       if (isRollback) {
97         return Collections.emptyList();
98       } else {
99         for (int i = 0, n = statementList.size(); i < n; i++) {
100          Statement stmt = statementList.get(i);
101          BatchResult batchResult = batchResultList.get(i);
102          try {
103            batchResult.setUpdateCounts(stmt.executeBatch());
104            MappedStatement ms = batchResult.getMappedStatement();
105            List<Object> parameterObjects = batchResult.getParameterObjects();
106            KeyGenerator keyGenerator = ms.getKeyGenerator();
107            if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
108              Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
109              jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
110            } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
111              for (Object parameter : parameterObjects) {
112                keyGenerator.processAfter(this, ms, stmt, parameter);
113              }
114            }
115          } catch (BatchUpdateException e) {
116            StringBuffer message = new StringBuffer();
117            message.append(batchResult.getMappedStatement().getId())
118                .append(" (batch index #")
119                .append(i + 1)
120                .append(")")
121                .append(" failed.");
122            if (i > 0) {
123              message.append(" ")
124                  .append(i)
125                  .append(" prior sub executor(s) completed successfully, but will be rolled back.");
126            }
127            throw new BatchExecutorException(message.toString(), e, results, batchResult);
128          }
129          results.add(batchResult);
130        }
131        return results;
132      }
133    } finally {
134      for (Statement stmt : statementList) {
135        closeStatement(stmt);
136      }
137      currentSql = null;
138      statementList.clear();
139      batchResultList.clear();
140    }
141  }
142
143}

 기타, 참조


용어

SqlSessionTemplate - http://www.mybatis.org/spring/ko/sqlsession.html
SqlSessionTemplate은 마이바티스 스프링 연동모듈의 핵심이다.
SqlSessionTemplate은 SqlSession을 구현하고 코드에서 SqlSession를 대체하는 역할을 한다.
SqlSessionTemplate 은 쓰레드에 안전하고 여러개의 DAO나 매퍼에서 공유할수 있다.
getMapper()에 의해 리턴된 매퍼가 가진 메서드를 포함해서 SQL을 처리하는 마이바티스 메서드를 호출할때 SqlSessionTemplate은 SqlSession이 현재의 스프링 트랜잭션에서 사용될수 있도록 보장한다.
추가적으로 SqlSessionTemplate은 필요한 시점에 세션을 닫고, 커밋하거나 롤백하는 것을 포함한 세션의 생명주기를 관리한다.
또한 마이바티스 예외를 스프링의 DataAccessException로 변환하는 작업또한 처리한다.
SqlSessionTemplate은 마이바티스의 디폴트 구현체인 DefaultSqlSession 대신 항상 사용된다.
왜냐하면 템플릿은 스프링 트랜잭션의 일부처럼 사용될 수 있고 여러개 주입된 매퍼 클래스에 의해 사용되도록 쓰레드에 안전하다.
동일한 애플리케이션에서 두개의 클래스간의 전환은 데이터 무결성 이슈를 야기할수 있다.

참조

Batch Processing - https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B4%84_%EC%B2%98%EB%A6%AC

Batch Processing(JDBC) - https://www.safaribooksonline.com/library/view/database-programming-with/1565926161/ch04s02.html

Batch Processing(JDBC) - http://www.tutorialspoint.com/jdbc/jdbc-batch-processing.htm

Mybatis3 - http://www.mybatis.org/mybatis-3/ko/index.html

Grep Code(org.apache.ibatis.executor.BatchExecutor.class) - http://grepcode.com/file/repo1.maven.org/maven2/org.mybatis/mybatis/3.2.7/org/apache/ibatis/executor/BatchExecutor.java#BatchExecutor

+ Recent posts