본문 바로가기

개발일기장

Bulk INSERT로 그룹 시작 기능 성능을 최적화 해보겠습니다.

안녕하세요. 예비 개발자를 위한 스터디 중개 서비스 Xuni를 개발하고 있는데요. 오늘은 스터디 시작 기능을 최적화한 내용을 설명해보려고 합니다.

 

 

최적화에 앞서 스터디 시작 기능에 대해 가볍게 설명해보려고 합니다.

 

 

기본적으로 Xuni에서 스터디를 진행하기 위해서는 책 혹은 공식 문서를 가지고 스터디를 진행합니다.

 책, 공식 문서의 공통점은 목차가 존재합니다.

 

스터디를 시작하면 아래와 같이 그룹원마다 체크리스트가 생깁니다.

 

(참고로, 해당 테이블은 컬렉션 테이블이기 때문에 엔티티에 생명주기를 의존합니다.)

 

 

이정도면 그룹 시작 기능에 대해 어느 정도 이해가 되셨을거라고 생각합니다:)

 

목차

1. JPA의 쓰기 지연과 변경 감지

2. 벌크성 쿼리를 통한 개선

3. 성능 개선 요약 및 결정


JPA의 변경 감지와 쓰기 지연

Xuni 프로젝트에서는 데이터 접근 기술로 JPA를 주력으로 사용하고 있습니다. JPA의 변경 감지와 쓰기 지연을 이해하고 있다면 그룹 시작 기능이 성능이 나오지 않는 이유를 알 수 있을 것입니다.

 

변경 감지란 쉽게, 엔티티의 변경된 부분을 감지해서 이를 데이터베이스에 반영하는 것입니다. 더 자세한 설명은 아래 코드를 통해 알아보도록 하겠습니다.

 

설명을 위해 도메인 레이어에 있는 로직을 꺼내왔습니다. 실제 프로덕트 코드에는 3 ~ 8행 로직이 start 메서드 내부에 존재합니다.

 

초록색 블럭 부분을 통해 엔티티를 조회하면 영속성 컨텍스트(1차 캐시)에 엔티티와 스냅샷이 보관됩니다. 스냅샷이란 조회한 시점의 엔티티를 복사해두는 일종의 기록입니다.

 

이후, 아래 로직을 통해 엔티티가 변경되고 group.start() 로직을 마치면 트랜잭션 커밋을 호출하려고 하는데요. 그 때  엔티티 매니저는 아래와 같은 행위를 통해 변경 감지를 합니다.

 

1. 커밋 호출 전, 엔티티 매니저가 플러시를 호출한다.

2. 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.

3. 변경된 엔티티가 있으면 (INSERT, UPDATE, DELETE) 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.

4. 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.

5. 데이터베이스 트랜잭션을 커밋한다.

 

근데 문제는 요놈이 쓰기 지연 저장소에 모와서 한 번에 보내기는 하는데 INSERT 단일 건으로 보냅니다. 이제 문제를 해결해보도록 하겠습니다.


벌크성 쿼리를 통한 개선

문제를 해결할 수 있는 방법은 다양하겠지만 제가 알고 있는 방법은 2가지 입니다.

1. hibernate.jdbc.batch_size 설정

2. jdbcTemplate batchUpdate

 

두 방법 모두 아래와 같이 단일 INSERT에서 벌크성 INSERT를 날리는 방식으로 변경하는 것입니다.

이렇게 말이죠.

 

1. hibernate.jdbc.batch_size

이 방법은 제약이 존재합니다. 기본 키 생성 전략이 IDENTITY라면 사용할 수 없습니다. 왜냐하면 애플리케이션 레벨에서 IDENTITY를 사용하면 기본키 생성 전략을 데이터베이스에 위임하기 때문에 기본키를 알 수 있는 방법이 없습니다.

 

저 또한 엔티티에서는 IDENTITY 전략을 사용하고 있지만, 제가 날리는 벌크성 쿼리는 엔티티 안에 있는 컬렉션 테이블을 INSERT 하는 것이기 때문에 사용할 수 있습니다.

 

spring.datasource.url에 rewriteBatchedStatements=true 을 추가합니다.

spring.datasource.url={설정}&rewriteBatchedStatements=true

그리고 아래 batch_size도 설정해줍시다. 저는 위 사례에 최적화하기 위해 140으로 설정하였습니다.

spring.jpa.properties.hibernate.jdbc.batch_size = 140

 

많은 글에서 아래 옵션도 설정해야 한다고 하지만 저는 스프링부트 3.0.5를 사용하고 있습니다. 스프링 부트 3.0.5에서는 hibernate.core 6.1.7을 주입해주는데요. 아래 옵션을 설정하지 않아도 동작하네요. 

spring.jpa.properties.hibernate.order_inserts = true

소스 코드에는 변화를 주지 않아도 됩니다.

성능 개선 (적용 전 933ms 적용 후 358ms)

Bulk INSERT 적용 전


적용 후 

 

 

 

2. jdbcTemplate batchUpdate

많은 엔티티를 INSERT해야 하는 상황에서 기본키 생성 전략이 IDENTITY라면 JDBC를 활용해야 합니다. 저는 그중에서도 jdbcTemplate을 사용해서 문제를 해결해보았습니다.

 

 

이 방법을 사용할 때는 영속성 컨텍스트의 변경 감지 기능을 주의해야 합니다.

 

만약 위와 같이 비영속으로 만들지 않는다면 INSERT가 두 번 일어날 수 있습니다. 아래와 같이 말이죠. (원래는 140개 레코드만 만들어져야 합니다.)

 

위에서 설명한 변경 감지를 생각해보면 당연한 결과입니다. 그렇기 때문에 변경 감지가 일어나지 않도록 영속성을 비영속 상태(detach() 메서드)로 만들어줘야 합니다. 변경 감지는 영속성 상태에서만 일어납니다. 

 

 jdbcTemplate batchUpdate 를 이용했을 때도 성능이 개선되는 것을 확인할 수 있습니다.


성능 개선 요약 및 결정

성능 적용 전, 933ms 적용 후 358ms (100% 이상 개선)

결정. hibernate.jdbc.batch_size 옵션 선택

 

1번과 같은 간단한 선택지가 있기 때문에 더 많은 내용을 이해해야하고 의존을 추가해야되는 jdbcTemplate은 꼭 필요한 경우가 아니라면 사용하지 않기로 했습니다.