ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SpringBoot MySql bulkInsert 도입기
    Project/TravelFeelDog 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 할시 출력할 쿼리 최대 길이 설정
Designed by Tistory.