Programming/SpringBoot

[SpringBoot] batchUpdate()를 활용한 bulkInsert

owls 2024. 7. 30. 16:49
728x90

saveAll()

jpa에서 제공하는 saveAll() 메소드를 사용하면 100건 등록시 insert가 100번 수행됩니다.

100명의 사용자가 각각  saveAll()메소드를 사용한다면 100*100 = 10000 번의 insert가 수행되는 것입니다. 

요청 사용자가 많을 수록 데이터베이스 성능 저하의 원인이 되고 결과적으로 서버 응답 시간이 길어지게 됩니다.

 

 

spring data jpa구현체인 SimpleJpaRepository를 확인해보면 saveAll() 메소드 내부에서 for문을 통해 save() 메소드를 호출합니다.

@Override
	@Transactional
	public <S extends T> List<S> saveAll(Iterable<S> entities) {

		Assert.notNull(entities, "Entities must not be null");

		List<S> result = new ArrayList<>();

		for (S entity : entities) {
			result.add(save(entity));
		}

		return result;
	}

https://github.com/spring-projects/spring-data-jpa/blob/main/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java

 

JPA Batch insert 의 성능 이슈

여러 건의 삽입을 할 때 여러 개의 쿼리가 나가는 것을 단건 삽입, 

하나의 쿼리로 나가는게 Bulk 삽입입니다.

 

쿼리를 던지고 응답을 받은 뒤 다음 쿼리를 전달하기 때문에 Insert의 경우에는 지연이 많이 발생하지만

하나의 트랜잭션으로 묶이는 Batch Insert는 하나의 쿼리문으로 수행하기 때문에 성능이 훨씬 좋습니다.

 

save(), saveAll() 은 모두 단건 삽입에 해당합니다.

 

데이터베이스마다 적용되는 기본키 생성 전략이 다릅니다.

MySQL의 auto_increment옵션으로 기본키를 연속적으로 생성해주는 옵션을 사용했습니다. 

MySQL은 jpa에서 기본키 생성 전략으로 GenerationType.IDENTITY를 가져갑니다.

 

springboot에서 jpa 오픈소스로 Hibernate를 사용하고 있습니다.

Hibernate는 batch insert를 제공하지만 기본키 생성 전략중 IDENTITY전략에서는 Hibernate가 batch insert를 비활성화 하기 때문에 사용이 불가능합니다.

 

IDENTITY : 기본키 생성 전략

비활성화를 하는 이유는 영속성 컨텍스트 내부에 엔티티를 식별할 때 엔티티ID(PK)와 타입으로 식별하지만, IDENTITY전략의 경우 DB에 INSERT한 후  ID(PK)확인이 가능하기 때문입니다. ( @Id 로 지정된 필드가 비어있기에 영속화가 불가능하다)

즉, IDENTITY방식 때문에 Batch Insert를 JPA에서 사용할 수 없습니다. 이유는 DB에 Insert가 되어야 Id값을 알 수 있다는 JPA의 쓰기지연 특성 때문입니다.

 

그렇기 때문에 jpa에서 bulk insert할 때 N번 째 데이터의 기본키(식별자)를 채번하기 위해 N-1번까지 insert가 되어있는 상태에서 데이터를 insert하고 생성된 기본키를 가져오게 되어서 실질적으로 N번의 insert쿼리가 발생합니다.

 

jpa의 saveAll()메소드를 사용한다면 성능 개선이 필요합니다.

기본키 생성 전략 변경, JDBC batchUpdate 사용 등 다양한 방법이 있습니다.

 

기본키 생성 전략을 IDENTITY대신 채번하지 않는 SEQUENCE 또는 TABLE방식을 사용하는 방법이 있다. 하지만 DB스키마를 변경(테이블의 구조를 변경)해야 합니다. 

 

그렇기에 JDBC batchUpdate를 사용하여 성능 개선하는 방법을 사용하겠습니다.

 

JDBC batchUpdate()

application.properties 설정에서 bulk Insert를 사용하도록 설정을 추가합니다.

rewriteBatchedStatements=true

spring.datasource.jdbc-url=jdbc:mysql://localhost:3306/database?serverTimezone=UTC&rewriteBatchedStatements=true

 

JDBC구현 클래스

    public void saveAll(List<ImageDto> imageDtoList, Long postId){
        String sql = "INSERT INTO images (post_id, path, original_name, size) VALUES (?,?,?,?)";

        BatchPreparedStatementSetter batchPreparedStatementSetter = new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ImageDto imageDto = imageDtoList.get(i);
                ps.setLong(1, postId);
                ps.setString(2, imageDto.getFilePath());
                ps.setString(3, imageDto.getOriginalName());
                ps.setLong(4, imageDto.getFileSize());
            }

            @Override
            public int getBatchSize() {
                return imageDtoList.size();
            }
        };
        jdbcTemplate.batchUpdate(sql, batchPreparedStatementSetter);
    }

 

 

결과

jpa saveall() 메소드 실행 시간 측정

9개 이미지 정보 저장

--------코드 실행 시간(s) : 0.011720099


jpa saveall() 메소드 실행 시간 측정

1개 이미지 정보 저장


 

jdbc batchupdate 코드 실행 시간 측정 ← jdbc에 batch설정 전

9개 이미지 정보 저장


jdbc batchupdate 코드 실행 시간 측정 ← jdbc batch설정 후

9개 이미지 정보 저장

--------코드 실행 시간(s) : 0.0050107


 

jpa saveAll() 대비 jdbc batchUpdate사용 시 약 2.3배 빨라졌습니다.

 

 

 

참고

 

https://github.com/spring-projects/spring-data-jpa/blob/main/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java

 

 

 

728x90