Skip to content

Batch Insert 최적화

Robin Choi edited this page Apr 5, 2022 · 9 revisions

1. 도입 이유

프로젝트 내의 작품을 업로드하거나 게시물을 업로드 할 때, 내용에 같이 딸려가야 하는 데이터가 있었다.
작품페이지의 경우 이미지가 개수 제한 없이 업로드 되며, 게시글의 경우 이미지와 태그 리스트가 같이 올라가게 된다.
이 때, 리스트로 올라가는 이미지나 태그의 개수만큼 저장을 하기 위해서, 기존에 스프링 데이터 jpa 에서 가지고 있는 save를 사용하였다.

save를 사용하게 되면 한 번에 한 데이터만 db에 저장할 수 있으므로 리스트의 길이만큼 인서트 쿼리가 발생하고,
이는 게시글 작성 시에 이미지 개수 이상의 쿼리가 기본적으로 나갈 수 밖에 없다는 걸 의미했다.
따라서 쿼리의 개수를 줄이고, 이를 효율적으로 진행하기 위해서 벌크로 데이터를 집어넣는 방법을 찾게 되었다.

기존코드

@Transactional
    public int createPost(Long accountId, PostRequestDto.PostCreate dto, List<MultipartFile> imgFile) {
        Account account = accountRepository.findById(accountId).orElseThrow(() -> new ApiRequestException(ErrorCode.NO_USER_ERROR));
        if (account.getPostCreateCount() >= 5) {
            throw new ApiRequestException(ErrorCode.DAILY_WRITE_UP_BURN_ERROR);
        }
        Post post = Post.of(account, dto);
        Post savedPost = postRepository.save(post);
        if(imgFile!=null){
            imgFile.forEach((file) -> {
                String img_url = fileProcessService.uploadImage(file);
                PostImage postImage = PostImage.builder().post(post).postImg(img_url).build();
                postImageRepository.save(postImage);
            });
        }
        setPostTag(dto.getHashTag(), savedPost);
        account.upPostCountCreate();
        return 5 - account.getPostCreateCount();
    }

2. 문제 상황

프로그램의 각 테이블의 pk 자동증가 전략을 테이블에 종속적이게 identity로 설정하였으나,
Hibernate 자체에서 Identity 식별자 생성 방식의 경우 JDBC 수준에서 batch insert를 비활성화 시켜서 사용할 수가 없었다.
하이버네이트는 Transactional Write Behind라는 방식을 사용하고 있고, 이는 영속성 컨텍스트에서 트랜잭션 커밋이 될 때까지
데이터를 내부 쿼리 저장소에 모아뒀다가 한번에 실행시킨다. 하지만 Identity 전략의 경우, insert가 실행되기 전까지는 Id에 대한 할당값을 알 수 없기 때문에, 배치 인서트 데이터 중 어떤 것이 먼저인지, 알 수 없어서 Batch Insert를 진행할 수 없다.

3. 해결 방안

Option 1) Sequence나 table 방식을 사용하면서 채번 부하를 낮추는 방법
Option 2) Spring Data Jpa를 벗어나 Spring Data JDBC를 사용한 batch insert

위와 같은 옵션 2가지가 제시되었다.

4. 의견 조율

옵션 1의 경우는 일단 db를 MySql을 사용하기 때문에 채번을 Batch 처리 방법을 알아보았으나, Hibernate 전용 어노테이션이 아래와 같이 복잡한 구조로 되어있고, 야믈파일로 배치 크기 설정을 외부에서 일괄로 지정할 수 없었다. 따라서 각 어노테이션 별로 batch Insert 지정하다보면 자칫 사이즈가 달라질 수 있기 때문에 관리가 어렵다는 점으로 옵션 2 방법으로 해결을 진행하기로 했다.

@Id
    @GenericGenerator(
            name = "SequenceGenerator",
            strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
            parameters = {
                    @Parameter(name = "sequence_name", value = "hibernate_sequence"),
                    @Parameter(name = "optimizer", value = "pooled"),
                    @Parameter(name = "initial_value", value = "1"),
                    @Parameter(name = "increment_size", value = "500")
            }
    )
    @GeneratedValue(
            strategy = GenerationType.SEQUENCE,
            generator = "SequenceGenerator"
    )
    private Long id;
    private String name;
    private String description;
}

5. 의견 결정

옵션 2번을 사용하기 위해 태그와 이미지를 일괄로 업데이트 하는 BatchInsertRepository를 별도로 만들어서 해당 로직을 서비스단에서 분리한 후, **JdbcTemplate.batchUpdate()**를 사용하여 아래와 같은 배치 인서트 메소드를 별도로 만들었다.

jdbcTemplate.batchUpdate()를 이용한 batch Insert

private void postImageBatchInsert(List<PostImage> bowl) {
        jdbcTemplate.batchUpdate("insert into post_image(`post_img`, `post_id`) VALUES (?, ?)",
                new BatchPreparedStatementSetter() {
                    // TODO 직접 insert 하기 때문에 재설정 필요
                    @Override
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        ps.setString(1, bowl.get(i).getPostImg());
                        ps.setString(2, String.valueOf(bowl.get(i).getPost().getId()));
                    }
                    @Override
                    public int getBatchSize() {
                        return bowl.size();
                    }
                });
        bowl.clear();

서비스단의 간단해진 코드

@Transactional
    public int createPost(Long accountId, PostRequestDto.PostCreate dto, List<MultipartFile> imgFile) {
        Account account = accountRepository.findById(accountId).orElseThrow(() -> new ErrorCustomException(ErrorCode.NO_USER_ERROR));
        if (account.getPostCreateCount() >= 1000) {
            throw new ErrorCustomException(ErrorCode.DAILY_POST_WRITE_UP_BURN_ERROR);
        }
        Post post = Post.of(account, dto);
        Post savedPost = postRepository.save(post);
        setPostImage(imgFile, post);
        setPostTag(dto.getHash_tag(), savedPost);
        account.upPostCountCreate();
        return 5 - account.getPostCreateCount();
    }

실제로 이미지를 삽입하여, 기존 코드와의 성능차이를 비교해봤다. 첫번째는 프로젝트를 런했을 때이고, 시간 차를 두고 한번 더 비교해 봤는데,
두 케이스 모두 batch Insert를 사용했을 때 거의 2배 이상의 시간 단축이 이루어진 유의미한 결과가 나왔다.

1) for문과 save를 사용한 다중인서트 (런 직후) 그냥 다중 인서트 속도

2) batch insert를 사용 (런 직후) 배치 인서트 속도

3) for문과 save를 사용한 다중인서트 다중 인서트 속도 2

4) batch insert를 사용 배치인서트 2

6. 레퍼런스

Hibernate doc 문서

Spring Data Batch Insert 최적화

Why does Hibernate disable INSERT batching when using an IDENTITY identifier generator