JPA saveAll에서 insert이전에 select가 일어난다.

PEPPERMINT100·2022년 2월 9일
1

문제

한 번에 많은 데이터를 데이터베이스에 저장하려고 할 때 일반적으로 데이터 하나당 insert를 날리는 것보다 값들을 묶어서 batch insert하는 경우가 더 성능이 좋다.

여러 데이터를 batch insert하기 위해 List 형태로 데이터를 모아서 saveAll(data)를 실행하였다.

List<Data> dataList = otherService.getData();
repository.saveAll(dataList);
public List<Data> getData() {
	List<Data> result = new ArrayList<>();
	for (int i=0; i<10; i++){
		Data newData = Data.builder().id(id).value(somevalue).build();
		result.add(newData);
	}
	return result;
}

이런식으로 데이터를 10개 만들고 saveAll 을 통해 저장하였다.

당연히 insert 쿼리가 하나로 실행되길 기대했지만 결과는 생각과 너무 달랐다.

select from data where id=?
insert into data (id, value) values (?, ?)
select from data where id=?
insert into data (id, value) values (?, ?)
select from data where id=?
insert into data (id, value) values (?, ?)
select from data where id=?
insert into data (id, value) values (?, ?)
select from data where id=?
insert into data (id, value) values (?, ?)
select from data where id=?
insert into data (id, value) values (?, ?)
select from data where id=?
insert into data (id, value) values (?, ?)
select from data where id=?
insert into data (id, value) values (?, ?)
select from data where id=?
insert into data (id, value) values (?, ?)
select from data where id=?
insert into data (id, value) values (?, ?)
select from data where id=?
insert into data (id, value) values (?, ?)

이런 식으로 insert문도 따로따로 실행되었으며 JPA가 insert 이전에 계속 select쿼리를 생성하는 문제가 있었다. 해당 문제를 해결한 과정을 간단하게 적어보려 한다.

Batch Insert

먼저 따로따로 날아가는 insert문을 묶기 위해서는 몇개의 세팅이 필요하다.

먼저 jpa 설정에

properties.put("hibernate.jdbc.batch_size", 500);
properties.put("hibernate.order_inserts", true);
properties.put("hibernate.order_updates", true);

위와 같은 설정을 추가해주었다. 개인적으로 jpa 관련 설정은 application.yml보다 각 데이터소스마다 설정이 다를 수 있으므로 이곳에 넣는걸 선호하는데, 어떤게 좋은 방법인지는 잘 모르겠다.

application.yml에 넣고 싶다면,

spring:
  jpa:
    properties:
      hibernate:
        order_inserts: true
        order_updates: true
        jdbc:
          batch_size: 500

이런식으로 설정해주면 된다.

그리고 MySQL 기준 jdbcUrl 설정 맨 마지막에 rewriteBatchedStatements=true 을 추가해준다. 기본적으로 false로 설정되어 있다.

이렇게 하면 saveAll 이 정상적으로 한 번에 실행된다. 참고로 로그에는 따로따로 나가는 것으로 표현되므로 MySQL 단에서 로그를 따로 남겨서 확인해야 batch insert가 실행되는 것을 볼 수 있다.

Insert without select

insert 이전에 select가 계속 실행되는 이유는 JPA의 CrudRepository의 save 메소드의 내부 구현을 보면 알 수 있다.

@Transactional
public <S extends T> S save(S entity) {
	if (this.entityInformation.isNew(entity)) {
    	this.em.persist(entity);
        return entity;
    } else {
        return this.em.merge(entity);
    }
 }

확인해보면 entity의 상태가 isNew면 persist하고 다른 경우엔 merge를 한다.

persist의 경우엔 새로운 객체이기 때문에 영속성에 추가하는 것이고 merge의 경우는 새로운 객체인지 아닌지 확인을 하고 새로운 객체면 insert 아니면 update를 한다.

즉 isNew가 false이기 때문에 계속 merge가 실행되었던 것이다.

한 트랜잭션에서 가져온 id는 영속성 관리 대상이 되어 있기 때문에 save에서 isNew 판단이 되지 않아 update 여부를 확인하기 위해 계속 select를 해왔던 것이다.

즉 결과적으로는 트랜잭션 단위를 잘 관리해야 한다.

하지만 다른 해결 방법도 있는데 entity 단에서

@Entity
@Table(name = "some_table")
@AllArgsConstructor @NoArgsConstructor
@Getter @Builder @ToString
public class SomeEntity implements Persistable<String> { // id의 타입을 제네릭에 넣어준다.
	  ...

    @Override
    public boolean isNew() {
        return true;
    }

    @Override
    public String getId() {
        return this.id;
    }
}

아래 처럼 isNew를 강제로 true로 넣어주어도 해결할 수 있다.

profile
기억하기 위해 혹은 잊어버리기 위해 글을 씁니다.

0개의 댓글