JPA Batch Insert를 사용하는 방법(feat : MySQL, JDBC)

개발하는 구황작물·2022년 10월 14일
2

메인 프로젝트

목록 보기
9/10

이 POST를 쓰게 된 계기

메인 프로젝트 성능 개선 작업 도중 게시글 사진 업로드 시 bulk insert가 필요해 save대신 List로 한번에 saveAll을 사용했다.

하지만 의도와 다르게 한번에 insert되지 않고 insert쿼리가 여러번 날아갔다.

해결 과정

1. Batch-size 설정

application.yml에 다음과 같이 설정을 해주었다.


  jpa:
    hibernate:
      ddl-auto: create  # (1) ??? ?? ??
    show-sql: true      # (2) SQL ?? ??
    properties:
      hibernate:
        jdbc:
        format_sql: true  # (3) SQL pretty print
        batch_size: 100

여기서 JDBC batching 에 대해 알아보면 여러개의 SQL statement 을 하나의 구문(Single PreparedStatement)으로 처리한다는 것을 의미한다.

여기선 batch-size를 100으로 설정했으므로 최대 100개의 statement를 한번에 처리할 수 있다

하지만 예상과 다르게 여전히 insert 쿼리문이 여러번 나왔다.

그렇게 오랫동안 구글링한 결과.....

GenerationType.IDENTITY를 적용하면 insert가 배치로 실행되지 않는다는 것을 알게 되었다

이 말을 듣고 난 후 GeneratedType를 SEQUENCE로 바꾸려했으나 MySQL은 SEQUENCE 전략을 지원하지 않는다는 사실도 알게되었다.

2. GeneratedType를 Table전략 사용

결국 Identity와 Sequence 전략 대신 Table전략을 적용해보았다.

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@TableGenerator(
        name = "GENERATOR_NAME", //식별자 생성기 이름
        table = "sequence_table", //키 생성 테이블명
        pkColumnName = "sequence_name", //시퀀스 컬럼명
        valueColumnName = "next_val", //시퀀스 값 컬럼명
        initialValue = 1, //초기 값
        allocationSize = 20 시퀀스 한번 호출 할 때마다 증가하는 수
)
public class Posts {
    ...
    @Id
    @GeneratedValue(
            strategy = GenerationType.TABLE
            , generator = "GENERATOR_NAME"
    )
    private Long id;
    
    }

여기서 중요한 것은 allocationSize를 조절해야 한다는 것이다.

JPA는 시퀀스에 접근하는 횟수를 줄이기 위해 allocationSize를 사용한다.
allocationSize로 설정한 값 만큼 한번에 시퀀스 값을 증가시키고 그만큼 메모리에 시퀀스 값을 할당한다.

데이터베이스에 직접 접근해서 데이터를 등록할 때 시퀀스 값이 한번에 많이 증가하므로 allocateSize를 조절하면 최적화할 때 도움이 된다고 한다.

그리고 application.yml 또한 수정을 해주었다.


spring:
  datasource:
    hikari:
      driver-class-name: com.mysql.cj.jdbc.Driver
      # 기존 jdbc-url에서 MySQL 쿼리 Log를 확인할 수 있는 profileSQL, profileSQL, maxQuerySizeToLog 옵션을 추가했음.
      jdbc-url: jdbc:mysql://localhost:3306/{DB이름}?rewriteBatchedStatements=true&profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=200
      username: username
      password: password
      
      ...
      
  jpa:
    hibernate:
      ddl-auto: create  # (1) ??? ?? ??
    properties:
      hibernate:
#        format_sql: true  # (3) SQL pretty print
        show-sql: true
        jdbc:
          batch_size: 100
          batch_versioned_data: true
        order_inserts: true # hibernate 내부에 추가한 insert 작업을 먼저 정렬 후 순서대로 insert 작업 실행
        order_updates: true
    database-platform: org.hibernate.dialect.MySQL8Dialect

rewriteBatchedStatemant = true 설정시 Mysql에서도 insert 쿼리가 합쳐진다고 해서 추가하였다.

덕분에 insert가 한번에 실행되었으나
update 쿼리문이 insert 실행 전에 나타나는 것을 확인할 수 있었다.

TABLE 전략은 값을 조회하면서 SELECT 쿼리를 사용하고, 다음 값으로 증가시키기 위해 UPDATE 쿼리를 사용하기 때문이었다.

결국 Bulk Insert만을 위해 IDENTITY 전략 대신 TABLE 전략을 쓰는 것은 비효율적이라는 결론을 내었다.

3. 돌고 돌아 다시 IDENTITY 전략(?)

결국 다시 IDENTITY 전략으로 돌아오게 되었으나...

이를 해결할 방법이 있다고 한다

  1. Hibernate 전용 어노테이션 사용
  2. JDBC 이용

필자는 1번 방식으로 진행하기로 하였다

Batch 채번은 Hibernate 전용 어노테이션을 사용해야 했다.

덕분에 여러 데이터를 한번에 insert 할 수 있게 되었다.

references
https://techblog.woowahan.com/2695/

https://github.com/HomoEfficio/dev-tips/blob/master/JPA-GenerationType-%EB%B3%84-INSERT-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90.md

https://homoefficio.github.io/2020/01/25/Spring-Data%EC%97%90%EC%84%9C-Batch-Insert-%EC%B5%9C%EC%A0%81%ED%99%94/

https://kapentaz.github.io/jpa/JPA-Batch-Insert-with-MySQL/#


+)

jpa를 사용하기 위해 ID전략을 바꾸는 것은 예기치 못한 에러를 불러올 수 있다는 사실을 듣게 되었다.

결국 2번 방법으로 수정하게 되었다...

@Repository
public class PostsImgJDBCRepository extends JDBCRepository<PostsImg> {


    public PostsImgJDBCRepository(JdbcTemplate jdbcTemplate) {
        super(jdbcTemplate);
    }

    @Override
    public void batchInsert(List<PostsImg> lists) {
        String sql = "INSERT INTO posts_img " +
                "(file_name, img_url, posts_id) VALUES (?, ?, ?)";

        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                PostsImg postsImg = lists.get(i);
                ps.setString(1, postsImg.getFileName());
                ps.setString(2, postsImg.getImgUrl());
                ps.setLong(3, postsImg.getPosts().getId());

            }

            @Override
            public int getBatchSize() {
                return 5;
            }
        });
    }
}
profile
어쩌다보니 개발하게 된 구황작물

0개의 댓글