티스토리 뷰

개발일지

batch insert 와 JPA 의 한계

땅속 디그다 2022. 11. 4. 15:01

파이썬을 통해 검사결과 양식을 받아서 spring 서버에서 처리하는 도중 검사 결과 1건당 15~20 여개의 insert query 가 발생했습니다

당연하게도 검사 파일은 수십만건이므로 insert query 또한 수백만 건이 발생하게 됩니다.

따라서 insert 에 대해 단건 insert 가 수백만 개가 예상되어 상당한 성능 저하가 예상이 되었고 이를 batch insert 로 해결해 보겠습니다.

 

batch insert 로 처리를 계획한 이후 당연하게도 jpa 의 영속성 컨텍스트와 관련하여 키생성 전략과 맞물려 이슈가 발생하였고 이를 해결하는 과정을 포스팅 해보겠습니다.

 

알아볼 내용은 다음과 같습니다.

1. 영속성 컨텍스트의 Entity 저장 전략

2. 각 키생성 전략의 Id 초기화 방식

3. spring Data Jpa 의 문제점

 

먼저 JPA의 영속성 컨텍스트 부터 살펴 보겠습니다.

영속성 컨텍스트는 Entity의 @Id 를 통해서 엔티티 객체를 식별하고 동등성을 비교하게 됩니다. 즉, 엔티티 객체가 영속성 컨텍스트에 올라가기 위해서는 id 값을 반드시 필수로 가지고 있어야 합니다.

 

당연하게도 @Id + @GenerateValue 를 사용하게 되면 이러한 문제는 거의 직면하지 않습니다. 만약 @GenerateValue가 없으면 어떨까요? JpaSystemException 이 발생하게 됩니다.

Member 는 간단하게 id, name 만을 column 으로 가지는 엔티티입니다.

id 값을 넣어주게 되면 잘 저장이 되게 됩니다.

하지만 여기서 Spring data jpa 의 한계가 들어나게 되는데, Spring data jpa 의 경우 Entity의 id 값이 채워져 있다면 merge 를 하게 됩니다. 즉 다음과 같은 형식입니다.

 

if ( 엔티티의 id 값이 채워져 있다면 ){
  entityManager.merge()
} else {
   entityManager.persist()
}

 

spring Data Jpa 는 id 값이 채워져 있다면 엔티티의 상태가 영속성 컨텍스트에 한번 들어갔다 나온 detach 상태로 파악을 하고 merge 를 하게 되는 것이고 이 merge 과정 속에서 단순하게 insert 쿼리 한번을 기대했지만 db에 select 후 없는 것을 확인한 후 쓰기 지연 저장소를 통해 insert query 가 나가는 것을 확인 할 수 있습니다.

 

Hibernate: select member0_.id as id1_1_0_, member0_.name as name2_1_0_ from member member0_ where member0_.id=?

Hibernate: insert into member (name, id) values (?, ?)

 

그렇다면 EntityManager를 직접 사용하는 경우는 어떨까요?

 

하나의 insert 만이 발생하는 것을 확인 할 수 있습니다.

즉 결론적으로는 @GenerateValue 를 사용하지 않고 DB 에 insert 를 할 경우 jpa 를 고집할 이유는 없어 보입니다.


이를 @GenerateValue 들로 전환해 각각 키생성이 어떤식으로 이루어 지는지 살펴보겠습니다.

처음으로 시퀀스 전략을 사용해보겠습니다.

 

시퀀스 전략의 경우에 데이터베이스 자체적으로 시퀀스 전략을 지원해야지 사용할 수 있습니다. h2의 경우 아무설정을 하지 않는 다면

다음을 볼 수 있게 됩니다.

 

시퀀스 전략을 사용했을 때의 쿼리를 살펴보도록 하겠습니다

시퀀스 전략은 기본적으로 JPA 에서 Hibernate Sequence next value 를 가져와 id 를 부여하게 됩니다 hibernate sequence 가져 오게 될 때 기본값이 1인 next value 를 수정해서 사용할 수 있습니다.

 

쿼리를 살펴 보겠습니다.

1. call next value for hibernate_sequence

2. Hibernate: insert into member (name, id) values (?, ?)


다음 으로 테이블 전략을 사용해 보겠습니다. 시퀀스 전략을 지원하지 않는 데이터베이스가 있을 때 시퀀스 전략을 따라하기 위해 테이블로 흉내내는 전략이라고 볼 수 있습니다.

 

하지만 시퀀스 전략의 경우 시퀀스 값을 가져오면 자동으로 update 되는 시스템과는 달리 테이블 전략의 경우 select 를 통해 id 값을 가져온후 update 를 통해 테이블의 값을 업데이트를 하여 시퀀스 전략보다 성능이 떨어집니다.

select tbl.next_val from hibernate_sequences tbl where tbl.sequence_name=? for update
update hibernate_sequences set next_val=? where next_val=? and sequence_name=?

추가적으로 동시성을 보장을 위해 시퀀스 테이블에 대한 value 에 락을 걸게 됩니다.

 

따라서 아무리 봐도 시퀀스 전략을 흉내내는 전략인 테이블 전략을 사용하는 것을 지양해야 할 듯 합니다.


마지막 전략은 Identity 전략으로 데이터베이스에 들어가는 시점에 data row 의 id 값을 알 수 있는 방식입니다.

그런데 여기서 문제가 발생합니다. 영속성 컨텍스트의 경우 엔티티를 persist 상태로 두기위해서는 id 값이 반드시 필수 입니다.

Identity 전략의 경우에 당연하게도 insert query 시에 id 값을 할당 받기 때문에 identity 전략으로 insert 에 대한 쓰기 지연을 사용하지 못합니다.

 

쓰기 지연을 사용하지 못하기 때문에 다시말해 batch insert 를 사용하지 못하게 됩니다.

 

2개의 엔티티를 저장해보고 시퀀스 전략과 비교를 해보겠습니다.

 

2개의 member Entity 를 저장해 보겠습니다

 

처음으로 identity 전략입니다. 앞서 말씀드렸다싶이 쓰기 지연이 적용되지 않게 되므로 insert query가 바로 나가는 것을 확인 할 수 있습니다.

이제 시퀀스 전략을 보겠습니다.

insert 쿼리 없이 단순하게 hibernate_sequence value 만 업데이트 하는 것을 볼 수 있습니다.


각 키생성 전략에 대해 설명을 드렸고 다음으로 현재 프로젝트에서 나오는 문제점을 파악하고 해결하는 과정을 서술해 보겠습니다.

 

먼저 batch insert 관련입니다.

batch insert 의 경우 물론 JPA 에서 쓰기 지연 저장소를 통해 지원을 하게 됩니다. 추가 사항은 설정파일에

spring.jpa.properties.hibernate.jdbc.batch_size 옵션에 숫자를 부여하면 되겠습니다.

 

가령, 그 값이 10 이라면 10개의 쿼리를 한방쿼리로 만들어 준다고 생각하면 편리하겠습니다.

 

시퀀스 전략, 테이블 전략 모두 batch insert 를 지원하고 identity 전략은 batch insert 를 사용하지 못합니다.

현재 프로젝트의 DataBase 환경은 MySQL 로 시퀀스 전략을 지원하지 않습니다.

 

따라서 문제점에 대한 방법은 다음 3개로 압축됩니다.

1. 테이블 전략을 사용해 batch insert 를 작성하기

2. identity 전략 그냥 사용하기

3. jdbc 직접 사용하여 batch insert 작성 (물론 완전 직접 사용 아니고 spring jdbc 를 사용합니다.)

 

각각의 환경에서의 속도를 h2 에서 테스팅을 해보고 마무리를 해보겠습니다.

 

먼저 batch insert 를 적용하지 않았을 때의 시퀀스 전략 소요시간과 적용했을 때의 소요시간의 차이를 살펴 보겠습니다.

 

Member Entity 객체를 10만건을 넣어보는 비교를 진행하였습니다.

 

 

Member Entity 객체를 사용하여 insert 쿼리를 진행 시켜 보겠습니다.

여러번 실험결과 대략 13초 ~ 16초정도의 시간이 소요됩니다.

 

다음으로는 batch insert 를 통해 쿼리를 진행 시켜 보겠습니다. batch size 는 1000으로 진행을 했습니다.

대략 8~10초 정도의 시간이 소요됩니다. 추가적으로 기본값인 allocate size를 1로 두었기 때문에 이를 높인다면 더 좋은 성능을 끌어낼 수 있을것으로 보입니다.

 

대략 40퍼센트의 성능 차이가 발생하는 것을 확인해 볼 수 있습니다.

 

그렇다면 Table 전략을 사용하면 어떻게 반응할까요?

테이블 전략의 경우 별도의 시퀀스를 흉내내는 테이블을 기반으로 id 값을 select , update 하게 됩니다.

마찬가지로 현재 allocate size 를 1로 두고 진행합니다.

 

별도의 시퀀스 테이블을 관리하여 select update 쿼리로 인해 오히려 성능이 떨어지는 현상을 발견 할 수 있습니다.

물론 마찬가지로 allocate size 를 크게 가져갈 경우 더 성능을 좋게 만들 수 있습니다.

 

하지만 시퀀스 전략과는 다르게 테이블 전략의 경우 개발자가 직접 시퀀스 테이블에 대해 관리를 하고 유지보수를 해야할 뿐 아니라 락을 사용하여 시퀀스 값을 관리하게 되므로 좋은 방법은 아닙니다.

 

마지막으로 Identity 전략(DB 에서 id 를 지정) + spring jdbc 를 사용하여 가장 빠른 결과를 얻어내 보겠습니다.

 

위에서와 마찬가지로 batch size 는 1000으로 진행하였습니다.

해당 내용의 경우 시퀀스값을 가져오거나 시퀀스 테이블에 대한 select + update 가 발생하지 않고 오직 batch insert 쿼리만 작성 되기 때문에 상당히 빠른 결과를 도출해 낼 수 있게 됩니다.


결론적으로 프로젝트에서 여러개의 insert 가 나오는 부분을 해결하는 과정은 다음과 같아집니다.

1. 검사 결과 하나당 10~20개의 insert 쿼리 발생 -> batch insert 를 통해 쿼리 최적화

 

2. mysql 에서는 시퀀스 전략을 채택할 수 없다.

-> 대안 1. 테이블 전략

--> 개발자가 직접 테이블 전략에 대한 설정 관리 유지보수를 해야하고 allocate size 설정을 잘못하면 select - update 가 자주 나가게 되어 오히려 개별 insert 보다 성능이 떨어질 수 있다.

 

-> 대안 2. identity 전략(database 의 auto increasement) + jdbcTemplate 사용 

-->  데이터 베이스의 auto increasement 전략 사용시에는 JPA를 사용하여 batch insert 를 사용하지 못한다. 따라서 jdbcTemplate를 사용해야 한다.

 

저의 경우 대안 2를 선택하여 insert 쿼리를 최적화를 진행했고 성능에 대해 70퍼센트 정도의 시간소요를 줄일 수 있게 되었습니다.

댓글
05-09 09:32
Total
Today
Yesterday
링크