JPA Batch Insert/Update를 적용하여 성능 향상하기
# Back-End/Spring

JPA Batch Insert/Update를 적용하여 성능 향상하기

 

주제

  • 하이버네이트, JDBC에 Batch Insert/Update 설정을 추가해본다
  • Batch로 처리했을 때의 성능 개선을 경험한다.

 


1. Batch Insert/Update에 대해 알아보기

1.1  Batch Insert/Update 란?

INSERT INTO USER (ID, USER_NAME, AGE)
VALUES (1, '춘식이', 19);

INSERT INTO USER (ID, USER_NAME, AGE)
VALUES (2, '라이언', 20);

INSERT INTO USER (ID, USER_NAME, AGE)
VALUES (3, '어피치', 21);

...
...

 

대량의 데이터를 조작하려면 여러 번 Save 혹은 Update를 수행해야 한다.

저장할 데이터가 10,000건일 경우 데이터 1개를 저장하는 쿼리(로직)를 10,000번 수행하는 것 보다,

10,000개의 데이터를 한 번에 저장하는 것이 오버헤드가 덜 발생한다.

 

 

위와 같은 단 건의 Insert문은 아래와 같이 대체할 수 있다.

INSERT INTO USER (ID, USER_NAME, AGE)
VALUES
    (1, '춘식이', 19), 
    (2, '라이언', 20),
    (3, '어피치', 21)
    
    ...
    ...
    (10000, '브라운', 20);

 

단 건의 Insert문이 여러 개의 데이터를 조작한다.

 

 

 

2. JPA에 적용해보기

2.1  Hibernate와 JDBC를 이용한 Batch 설정

2.1.1 하이버네이트와 JDBC 란?

사진 출처 : suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/

 

1) Hibernate

하이버네이트는 JPA(Java Persistence API)의 대표적인 구현체 이다.

우리가 사용하는 JPA라는 ORM 기술을 사용하기 위해 만들어진 프레임워크라고 생각하면 될 것 같다.

Hibernate의 설정에 따라 생성한 엔티티가 DB의 테이블로 생성되기도 하고, 어플리케이션단에서 작성한 로직들이 SQL로 변환되어 수행되기도 한다.

 

 

 

2) JDBC

JDBC란 Java DataBase Connectivity 의 약어로 Java와 DB연결을 위한 표준 API

 

스프링 부트에 의해 자동화되어 있는 수많은 설정들 중에 Database와 연결하기 위해 사용되는 것이 JDBC 이다.

JDBC는 우리가 만든 Java Application이 DB와 통신할 수 있게 해준다.

 

JDBC를 통해 DB와 통신하는 과정을 간단하게 나타내보면 

  1. JDBC 드라이버 불러오기
  2. Connection 생성하기
  3. Connection을 통해 DB와 통신하기

와 같다.

 

하지만 Spring Boot에서는 일반 적으로 Connection Pool이라는 미리 만들어 놓은 JDBC Connection들을 보관하는 곳이 있으며, DB와 연결할 때 이 Connection Pool에서 빌려온 Connection을 사용한 후 다시 반환한다.

(이때 사용하는건 HikariCP 커넥션 풀 라이브러리)

 

 

2.1.2 적용 방법

1) 순수 JPA를 이용한 프로그래밍 방식

@Transactional(rollbackFor = Exception.class)
    public void bulkInsertForIdEntity2(int totalCount, int batchSize) {
        log.info("[bulkInsertForIdEntity2] bulk insert. total count is {}.", totalCount);
        simpleIdRepository.deleteAll();

        List<SimpleIdEntity> testData = createIdEntityList(totalCount);

        int i = 0;
        for (SimpleIdEntity entity : testData) {
            em.persist(entity);

            if(i % batchSize == 0) {
                em.flush();
                em.clear();
            }

            i++;
        }

        em.flush();
        em.clear();
    }

 

EntityManager에 엔티티를 영속시키다가 Batch Size만큼 쌓이게 되면 영속성을 Flush, Clear하여 캐시된 엔티티를 영속성 컨텍스트에서 제거한다.

이로써 메모리 부족 문제를 방지한다.

 

 

 

2) Hibernate와 JDBC 속성을 이용한 방식

MySql에서 사용 가능한 속성인 rewriteBatchedStatements 속성을 Hibernate에 적용한다.

 

rewriteBatchedStatements 속성은 MySQL에서 사용 가능한 속성으로, JDBC에서 Batch 작업 수행 시 insert, update, delete 쿼리를 rewrite하여 성능을 개선하는 기능이다.

 

 

 

Hibernate에서 rewriteBatchedStatements 속성을 사용하려면, JDBC URL에 다음과 같은 파라미터를 추가한다.

jdbc:mysql://localhost:3306/mydb?rewriteBatchedStatements=true

 

 

다음으로 위에서 For문을 사용하여 Batch Size에 도달할 때마다 Flush 해주는 것과 같이 Hibernate에서 설정할 수 있다.

# properties
spring.jpa.properties.hibernate.jdbc.batch_size=50

# yaml
spring:
  jpa:
    database: mysql
    properties:
      hibernate:
        jdbc.batch_size: 100
        order_inserts: true
        order_updates: true

 

jdbc.batch_size : 한 번에 작업을 수행할 데이터의 수

order_inserts: 작업을 수행하는 동안 다른 Insert문이 끼어들지 못하도록 수행될 쿼리 순서를 정렬

order_updates: 작업을 수행하는 동안 다른 Update문이 끼어들지 못하도록 수행될 쿼리 순서를 정렬

 

 

2.1.3 로그 확인하기

1) 인텔리 제이 로그

Console 창에서 

logging:
  level:
    org.hibernate: DEBUG
    org.springframework.orm.jpa: DEBUG
    org.springframework.transaction: DEBUG

hibernate에 대한 로그 설정을 DEBUG로 주고, 

  • Reusing batch statement
  • Executing batch size

상단의 두 문구가 출력되는지 확인한다.

https://stackoverflow.com/questions/30352212/hibernate-batch-insert

 

 

 

2) MySql 로그 확인

# DB 설정
show variables like 'general_log%'; 
set global general_log = 'ON';

# DB 접속 후 로그 확인
tail -f ...

 

 

 

2.2  주의사항

2.2.1 기본 키 생성 방식

기본 키의 생성방식이 Auto Increment일 경우 지원하지 않는다.

 

 

2.2.2 Database 벤더에 따른 지원 여부

참고로, rewriteBatchedStatements 속성은 MySQL에서만 사용 가능한 속성이므로, 다른 데이터베이스를 사용할 때는 해당 속성이 지원되지 않을 수 있습니다. 따라서 Hibernate에서 Batch 작업을 수행할 때는 데이터베이스 종류에 따라 최적화된 방법을 사용하는 것이 좋습니다.

 

 

2.2.3 적절한 Batch Size

Batch 작업을 수행할 때 적절한 Batch size는 다양한 요소에 따라 달라질 수 있습니다. 일반적으로는 다음과 같은 요소들이 영향을 미치게 됩니다.

1. 데이터베이스 종류
2. 사용하는 JDBC 드라이버
3. 시스템 리소스 (메모리, CPU, 디스크 I/O 등)
4. 네트워크 대역폭

따라서, 적절한 Batch size를 정하기 위해서는 위 요소들을 고려하여 성능 테스트를 수행하는 것이 가장 좋은 방법입니다. 하지만, 일반적으로는 20 ~ 50 정도의 Batch size가 적절하다고 알려져 있습니다.

Batch size를 크게 설정하면, 메모리를 많이 사용하게 되어 OutOfMemoryError가 발생할 가능성이 높아집니다. 또한, 너무 큰 Batch size는 데이터베이스 서버에 부하를 줄 수 있으며, 이로 인해 전체적인 성능이 저하될 수 있습니다.

Batch size를 작게 설정하면, 여러 번의 JDBC 호출이 발생하게 되므로 데이터베이스 서버에 부하를 줄 수 있습니다. 또한, JDBC 호출 횟수가 많아질수록 네트워크 대역폭을 많이 사용하게 되므로, 데이터베이스 서버와 애플리케이션 서버 간의 네트워크 대역폭이 좁은 경우에는 성능이 저하될 수 있습니다.

따라서, 적절한 Batch size를 선택하기 위해서는 각종 시스템 리소스와 데이터베이스 환경 등을 고려하여 여러 가지 Batch size를 실험적으로 결정하는 것이 좋습니다.

ChatGPT의 답변

 

 

2.2.4 왜 batch_size 속성은 default가 false인가요?

Batch 작업을 수행했을 때 메모리 문제가 발생할 수 있어 False인 것으로 보임

https://stackoverflow.com/questions/27755461/why-is-hibernate-batching-order-inserts-order-updates-disabled-by-default

 

 

 

3. 다른 기술들이 Batch 작업을 처리하는 방법

Mybatis, QueryDSL, JOOQ, JPA는 모두 Java에서 SQL 쿼리를 작성하고 실행하는 라이브러리이며, 각각의 라이브러리는 Batch 작업을 수행하는 방법이 조금씩 다릅니다. 아래는 각 라이브러리에서 Batch 작업을 수행하는 방법을 비교한 내용입니다.

1) Mybatis: Mybatis에서는 SqlSessionTemplate을 사용하여 Batch 작업을 수행합니다. SqlSessionTemplate은 JDBC의 Batch 기능을 지원하며, SqlSessionTemplate의 batch 메서드를 사용하여 Batch 작업을 수행할 수 있습니다.

2) QueryDSL: QueryDSL에서는 Batch 쿼리를 작성하는 데 사용되는 JPQLBatchQuery를 사용하여 Batch 작업을 수행할 수 있습니다. JPQLBatchQuery는 내부적으로 JDBC Batch 처리 기능을 사용하며, 이를 통해 매우 빠른 성능을 제공합니다.

3) JOOQ: JOOQ에서는 Batch 작업을 수행하기 위해 BatchQuery 객체를 사용합니다. BatchQuery 객체는 JOOQ에서 제공하는 DSL을 사용하여 생성되며, 이를 통해 Batch 쿼리를 생성할 수 있습니다.

4) JPA: JPA에서는 EntityManager의 flush 메서드를 사용하여 Batch 작업을 수행합니다. flush 메서드는 영속성 컨텍스트에 쌓인 변경사항을 데이터베이스에 반영하는 역할을 합니다. 따라서, Batch 작업을 수행하기 위해서는 EntityManager에게 영속성 컨텍스트의 변경사항을 반영하도록 명시적으로 호출해야 합니다.

위의 방법들 중에서 가장 빠른 성능을 보장하는 것은 JOOQ이며, JOOQ는 내부적으로 JDBC의 Batch 처리 기능을 사용하기 때문에 최적화된 성능을 제공합니다. 그러나, JOOQ는 SQL 쿼리를 생성하는 데에 많은 코드를 작성해야 하므로, 개발자들에게는 학습 곡선이 있을 수 있습니다.

나머지 라이브러리들은 모두 ORM(Object-Relational Mapping) 라이브러리로, SQL 쿼리를 자동으로 생성해주는 등의 편의성을 제공합니다. 그러나, ORM 라이브러리들은 내부적으로 많은 코드를 실행하기 때문에 성능이 JOOQ보다는 느릴 수 있습니다. 또한, ORM 라이브러리들은 SQL 쿼리를 자동으로 생성하므로, 개발자들은 SQL 쿼리를 작성할 필요가 없으나, ORM 라이브러리의 내부 동작을 이해하고 있어야 하는 경우가 있습니다.

 

 

# 참고자료

 

728x90