Project/TravelFeelDog

SpringBoot MySql bulkInsert 도입기

sung.hyun.1204 2023. 8. 30. 22:58

배치란 일련의 작업을 한번에 수행하는 작업을 뜻한다고 한다.

 

1만건의 데이터를 생성한 후 한방 쿼리를 사용하여 데이터베이스에 저장을 하는 테스트 코드이다.

 

@IntegrationTest
public class FeedBulkInsertTest {
    @Autowired
    private FeedRepository feedRepository;
    @Test
    public void bulkInsert() {
        var easyRandom = FeedFixtureFactory.get(
                4L,
                LocalDate.of(1998, 12, 1),
                LocalDate.of(2024, 2, 1)
        );

        var stopWatch = new StopWatch();
        stopWatch.start();

        int _1만 = 10000;
        var posts = IntStream.range(0, _1만*1)
                .parallel()
                .mapToObj(i -> easyRandom.nextObject(Feed.class))
                .toList();

        stopWatch.stop();
        System.out.println("객체 생성 시간 : " + stopWatch.getTotalTimeSeconds());

        var queryStopWatch = new StopWatch();
        queryStopWatch.start();

        feedRepository.bulkInsert(posts);

        queryStopWatch.stop();
        System.out.println("DB 인서트 시간 : " + queryStopWatch.getTotalTimeSeconds());
    }
}

 

 

feedRepository 의 bulkInsert 는 다음과 같다. 

Jpa 의 지원을 받지 못하는 상황이라 jdbc 템플릿을 사용하여 작성했다.

 

JPA 를 이용한 구현시 auto_increment 전략을 이용시 bulk insert 가 불가하다라고 한다.

MySql 의 경우 SEQUENCE 채번 방식을 지원하지 않으며 ,table 의 방식을 사용하기에는 부담스럽다.

 

public void bulkInsert(List<Feed> feeds) {
        String sql = String.format(
                "INSERT INTO %s (member_id, feed_title, feed_body, created_time, updated_time) " +
                        "VALUES (:memberId, :title, :body, :createdTime, :updatedTime)",
                "feed");

        SqlParameterSource[] params = feeds.stream()
                //.map(BeanPropertySqlParameterSource::new) feed 에 member property 가 없어 사용 불가.
                .map(feed -> {
                    MapSqlParameterSource param = new MapSqlParameterSource();
                    param.addValue("memberId", feed.getMember().getId());
                    param.addValue("title", feed.getTitle());
                    param.addValue("body", feed.getBody());
                    param.addValue("createdTime", feed.getCreatedDateTime());
                    param.addValue("updatedTime", feed.getUpdatedDateTime());
                    return param;
                })
                .toArray(SqlParameterSource[]::new);

        namedParameterJdbcTemplate.batchUpdate(sql, params);
    }

 

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Feed extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "feed_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
    ....

 

id 가 identity 로 되어 있으며 

[QUERY] INSERT INTO feed (member_id, feed_title, feed_body, created_time, updated_time) 
VALUES (null, 'WVbIISkzGBveHIOQimeKUT', 'ziagVMoORXttkVGTbcNdRXYnktgCcWMWjLwBnWYoTuTVlat', '2018-11-20 02:14:01.268203', '2017-12-26 08:46:27.07464')

다음과 같이 쿼리에 memberId 가 null 로 날라가며 db 에는

 

다음과 같이 값들이 생긴다.

 

 

1만건의 데이터의 경우 맥북 m1 , 16 ram 에서는 이상없이 테스트가 통과가 되며 . 

 

AWS , RDS 프리티어 t3 인스턴스의 경우도 이상 없이 잘된다.

 

spring:
  datasource:
    url: jdbc:mysql://${Db_URL}:${Db_PORT}/${Db_Name}?rewriteBatchedStatements=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=999999
    username: ${Db_User_Name}
    password: ${Db_User_PassWord}
    driver-class-name: com.mysql.cj.jdbc.Driver

 

 

[ rewriteBatchedStatements=true ] 

 

공식 문서

https://dev.mysql.com/doc/connector-j/8.1/en/connector-j-connp-props-performance-extensions.html

 

위의 설정을 true 로 설정해줘야 insert 문이 개별로 날라가지 않고 한번에 처리가 된다.

 

rewriteBatchedStatements = false 

rewriteBatchedStatements = true

 

 

 DB 에 넣을 데이터가 1000 건인 경우의 테스트는 다음과 같다 .  

 

- DB 인서트 시간 : 3.076869042 

 

- DB 인서트 시간 : 0.325032834

 

로컬 환경에서의 차이가 벌써 이정도 이니, 실 환경에서는 필수 옵션이라 생각이 든다. 

 

그외 :

  • profileSQL : Trace queries and their execution/fetch times to the configured
  • logger : Driver에서 쿼리 출력시 사용할 Logger를 설정
  • maxQuerySizeToLog : Trace 할시 출력할 쿼리 최대 길이 설정