여러 개의 DML(INSERT/UPDATE) 일괄적으로 처리해야 할 일이 생겼다.
이전에는 처리량이 많지 않아 자바 코드 레벨에서 forEach로 DAO를 지속적으로 호출했었다.
위 방법은 퍼포먼스와 리소스를 고려하자면 비효율적이란 생각을 갖고 있었다.
이번에 일괄적으로 데이터를 업데이트 해야 할 일이 생겼고, 스프링과 MyBatis를 이용하여 효율적으로 일괄 처리하기 위해 겪은 경험을 공유하고자한다.
jdbc:mysql://localhost:3306/test?allowMultiQueries=true
allowMultiQueries 옵션을 true로 설정하게 되면 Mapper에서 아래와 같이 DML 자체를 forEach로 수행 할 수 있다.
<foreach collection="list" item="student" separator=";">
INSERT INTO TB_USER (name, id, password)
VALUES (#{student.name}, #{student.id}, #{student.password})
</foreach>
결과 ↓
INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');
DAO를 직접 반복해서 호출 하는 방법보다 더 나은 방법이라고 생각한다.
하지만 나는 실무에서 이 방법을 채택하지 않았고 그 이유를 서술해보겠다.
스프링 MyBatis
의 연동 모듈인 SqlSessionTemplate
는 인자로 ExecutorType
를 포함하는 생성자를 갖고 있다.
이 인자는 예로 스프링 설정 XML를 아래와 같이 설정하여 SqlSession
을 만들 수 있다.
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
<constructor-arg index="1" value="BATCH" />
</bean>
주의할 점
1. 모든 명령이 배치 처리되기 때문에 원하는 실행 방법이 기본 설정과 다른 경우에만 사용해야한다.
2. BATCH가 아닌ExecutorType
의 트랜잭션과 혼용될 수 없고 만약 섞이게 된다면 Exception이 발생하기 때문에 해당 메서드를 별도의 트랜잭션으로 구분해야한다.
이미 부모 트랜잭션이 존재하는 상태에서 해당 배치 DAO를 호출할 경우에는 아래와 같이 트랜잭션을 구분해줘야 Exception
이 발생하지 않는다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void modifyBatch(List<Menu> users) {
batchUpdate("updateUser", users);
}
Propagation.REQURIES_NEW
는 트랜잭션 전파 레벨 중 하나이다.
해당 속성으로 부모 트랜잭션이 있다면 잠시 대기시키고, 자식 트랜잭션을 별도로 실행한다.
해당 트랜잭션에서 예외 발생 시, 롤백이 부모 트랜잭션에게 전파가 되지 않는다.
즉 별도의 트랜잭션으로 구분된다.
스프링 설정 XML에서 기본 session과 배치 처리를 위한 session을 개별 생성한다.
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<bean id="sqlSessionTemplateBatch" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
<constructor-arg index="1" value="BATCH" />
</bean>
public class BaseDao {
앞서 설정한 세션을 주입받는다.
private final SqlSessionTemplate sqlSessionTemplate;
@Qualifier("sqlSessionBatch")
private final SqlSessionTemplate sqlSessionTemplateBatch;
protected update(String sqlId, Object param) {
sqlSessionTemplate.update(sqlId, param);
}
protected void batchUpdate(String sqlId, List<?> paramList) {
for (Object param : paramList) {
sqlSessionTemplateBatch.update(sqlId, param);
}
}
}
단일 쿼리의 경우 update
일괄 처리의 경우 batchUpdate
메서드를 사용한다.
이 때, Mapper의 경우 단일 쿼리 다시 말하자면 배치 처리를 원하는 쿼리에 특수한 처리 없이 단일 쿼리를 재사용하면 된다.
최초 설정과 기본 DAO 메서드를 생성해두고, 기존의 sql을 재사용하면 되기 때문에
1번 방법에 비해 코드 중복을 줄이고, 확장성에 이점이 있다고 생각하여 해당 방법을 채택하였다.
해당 방법으로 기존 DAO를 반복하여 호출하는 방식보다 7~8배 처리 속도 향상을 확인하였다.