10/13/2024

JdbcTemplate으로 업로드 파일 Bulk Insert

포스트는 사진과 같은 파일을 등록할 수 있다. 그런데 현재 코드는 2개 이상의 파일을 업로드하면 반복문을 통해서 파일을 하나씩 데이터베이스에 저장한다. 파일이 적은 경우 문제가 없지만 만약에 개수가 많아지면 반복문을 통한 삽입문은 당연히 비효율적일 수 밖에 없다. 파일 6개를 삽입하는데 728ms 정도가 걸린다. Bulk Insert는 여러 개의 데이터를 하나의 삽입문으로 삽입한다. 개별 삽입문은 실행 후 응답을 받은 후에야 다음 삽입문을 실행하기 때문에 지연이 발생하지만 Bulk Insert는 일련의 삽입문을 하나의 트랜잭션으로 묶여 실행하기 때문에 성능이 좋다.


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

댓글 없음:

댓글 쓰기