들어가기
프로젝트 진행 중 20만 건 이상의 데이터 Row를 데이터베이스에 Insert해야 했다. JPA saveAll() 방식을 사용했을 때 저장 속도가 느려서 성능 향상이 필요하다고 느꼈고 JDBC Bulk Insert로 저장 속도를 향상시킬 수 있었다.
JPA saveAll()
아래의 코드는 JPA saveAll() 코드로, while 문 안의 두 번째 라인에서 엔티티를 하나씩 save하도록 구현되어 있다.
@Transactional
public <S extends T> List<S> saveAll(Iterable<S> entities) {
Assert.notNull(entities, "Entities must not be null");
List<S> result = new ArrayList();
Iterator var4 = entities.iterator();
while(var4.hasNext()) {
S entity = (Object)var4.next();
result.add(this.save(entity));
}
return result;
}
따라서, 아래의 쿼리처럼 한 건마다 데이터가 삽입된다.
INSERT INTO member (name, password) VALUES ('name1', 'password1');
INSERT INTO member (name, password) VALUES ('name2', 'password2');
INSERT INTO member (name, password) VALUES ('name3', 'password3');
Bulk Insert
Bulk Insert를 사용하면 여러개의 삽입할 데이터를 모아서 하나의 쿼리가 나가게 된다.
INSERT INTO member (name, password) VALUES ('name1', 'password1'), ('name2', 'password2'), ('name3', 'password3');
스프링에서는 JdbcTemplate에서 batchUpdate() 메소드로 Bulk Insert를 지원한다. 아래와 같이 BulkRepository를 따로 정의해서 구현했다. (아래의 코드에서 Member 엔티티의 PK에 Auto Increment 전략을 사용하지 않고, UUID를 만들어서 지정해주었기에 id 또한 삽입의 대상이다.)
@Repository
@RequiredArgsConstructor
public class MemberBulkRepository {
private final JdbcTemplate jdbcTemplate;
@Transactional
public void batchInsertMembers(List<Member> members) {
String sql =
"INSERT INTO member (id, name, phone_number, email, student_number, department_id, role, created_at, updated_at) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())";
try {
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Member member = members.get(i);
int index = 1;
ps.setString(index, member.getId().toString());
ps.setString(++index, member.getName());
ps.setString(++index, member.getPhoneNumber());
ps.setString(++index, member.getEmail());
ps.setString(++index, member.getStudentNumber());
ps.setLong(++index, member.getDepartment().getId());
ps.setString(++index, "STUDENT");
}
@Override
public int getBatchSize() {
return members.size();
}
});
} catch (Exception e) {
...
}
}
}
JPA SaveAll() vs JDBC Bulk Insert
두 개의 삽입 모두 데이터베이스는 MariaDB를 사용했고, Spring Batch를 활용하여 데이터 삽입 과정을 구성했다. Write 과정에서 데이터 저장하는 방식에만 차이를 두었다.
JPA의 SaveAll()을 사용할 경우 아래와 같이 약 1분 39초가 걸렸다.
Job: [SimpleJob: [name=memberReaderJob]] launched with the following parameters: [{'time':'{value=1742205794030, type=class java.lang.Long, identifying=true}'}]
Executing step: [memberReaderStep]
Step: [memberReaderStep] executed in 1m38s798ms
Job: [SimpleJob: [name=memberReaderJob]] completed with the following parameters: [{'time':'{value=1742205794030, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 1m39s308ms
JDBC Bulk Insert를 사용할 경우 아래와 같이 약 7초가 걸렸다.
Job: [SimpleJob: [name=memberReaderJob]] launched with the following parameters: [{'time':'{value=1742205981743, type=class java.lang.Long, identifying=true}'}]
Executing step: [memberReaderStep]
Step: [memberReaderStep] executed in 6s579ms
Job: [SimpleJob: [name=memberReaderJob]] completed with the following parameters: [{'time':'{value=1742205981743, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 6s920ms
결론
대량의 데이터를 데이터베이스에 삽입할 일이 있다면 JDBC Bulk Insert를 사용하는 것이 약 14배 정도의 성능 향상을 보이기에 JPA를 사용하기 보다 Bulk Insert를 사용하는 것이 훨씬 성능적으로 좋다.