JPA saveAll() 메서드
JPA의 saveAll() 메서드는 이름만 보면 Bulk Insert를 실행할 것 같지만 내부 구현을 보면 그렇지 않다는 것을 알 수 있다. 반복문을 돌면서 save() 함수를 호출한다. 결론은 JPA의 아이디 생성 전략으로 GenerateType.IDENDITY 사용 시 Batch Insert가 비활성화 된다. JPA는 영속성 컨텍스트와 쓰기 지연이라는 특성을 가진다. 영속성 컨텍스트는 엔티티를 영구 저장하는 환경으로 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상 데이터베이스 같은 역할을 한다. JPA는 영속성 컨텍스트 내부에 엔티티 타입과 기본키 값을 저장해 엔티티를 식별한다. 쓰기 지연(transaction write-behind)은 영속성 컨텍스트에 변경사항이 발생하면 SQL을 버퍼에 모아뒀다가 플러시(flush)하는 시점(트랜잭션이 커밋 시점)에 데이터베이스로 쿼리를 보내 실행한다.
아이디 생성 전략이 IDENTITY인 경우 삽입문을 실행하기 전에는 기본키를 알 수 없기에 즉시 삽입문을 실행해서 기본키 값을 가져와야 한다. 그런데 이는 Hibernate가 채택한 쓰기 지연 플러시 전략을 방해한다. 게다가, Bulk Insert를 실행하는 경우 즉시 데이터베이스에 Bulk Insert를 실행하고 삽입한 모든 데이터를 영속화해 영속성 컨텍스트에서 관리해야 한다. 이는 너무 많은 데이터를 영속화시켜 JVM에 할당된 메모리가 부족해질 수 있다. 이러한 이유로 Hibernate는 아이디 생성 전략이 IDENTITY인 경우 JDBC 수준에서 Batch Insert를 비활성화 한다. 따라서 JPA에서 batch insert를 사용하기 위해서는 GenerateType.SEQUENCE나 GenerateType.TABLE을 사용해야 한다.
JdbcTemplate
GenerateType.IDENTITY 아이디 전략을 사용하는 경우 Batch Insert를 어떻게 실행할 수 있을까? JdbcTemplate를 사용해서 JDBC의 batchUpdate() 메서드를 통해 Batch Insert를 실행할 수 있다. MySQL에서 이 기능을 사용하려면 데이터베이스 URL 환경 변수에 rewriteBatchedStatements를 true로 설정해야 한다.
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 | // 하나의 파일마다 삽입문이 실행된다. file = new File(fileExtension, mimeType, filename, filePath, fileSize); postEntity.getFiles().add(file); // Bulk Insert로 다수의 파일에 단일 삽입문이 실행된다. @Override public void bulkInsert(Long postId, List<File> files) { String sql = "INSERT INTO file(post_id, extension, mime_type, name, path, size) VALUES(?, ?, ?, ?, ?, ?)"; jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { File file = files.get(i); ps.setLong(1, postId); ps.setString(2, file.getExtension()); ps.setString(3, file.getMimeType()); ps.setString(4, file.getName()); ps.setString(5, file.getPath()); ps.setLong(6, file.getSize()); } @Override public int getBatchSize() { return files.size(); } }); } | cs |
4개의 파일(27MB)을 업로드했는데 삽입문을 반복해서 사용하는 경우 5.18초가 걸렸고 Bulk Insert를 사용한 결과 0.742초가 걸렸다. 63개의 파일(398MB)의 경우 19.07초에서 11.17초로 확실히 성능은 향상되었다. 하지만 그래도 뭔가 좀 아쉽다... 더 빠를 수 없을까? 아니면 혹시 다른 곳에 원인이 있을까?
TODO
여러 블로그에서 40배-50배 정도 성능 차이가 난다는 것을 보았다. 입력 파일을 임베디드 타입으로 변환하는 과정은 여전히 for 반복문을 사용한다. 그래서 for 반복문과 변환 과정이 어느 정도 성능에 영향을 준다고 생각해서 여러 방안을 생각했다. 일단 요즘 대부분의 컴퓨터가 다중 프로세서와 다중 코어이다 보니 다중스레드를 사용하는 방법이 떠올랐다. 다른 방법은 Spring Batch를 사용하는 것인데 이 방법을 공부하고 난 다음 구현을 해야겠다.
update: 2024.10.15
댓글 없음:
댓글 쓰기