-
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 할시 출력할 쿼리 최대 길이 설정
'Project > TravelFeelDog' 카테고리의 다른 글
Firebase 에서 OAuth2 , JWT 로 전환기(2) (0) 2023.09.22 SpringBoot 3.1.0 으로 마이그레이션 (0) 2023.09.07 Service Layer 에 읽기 쓰기 분리기 (0) 2023.08.10 Firebase 에서 OAuth2 , JWT 로 전환기(1) (0) 2023.08.08 yml 환경 변수 설정 (0) 2023.07.18