SpringBoot MySql bulkInsert 도입기
배치란 일련의 작업을 한번에 수행하는 작업을 뜻한다고 한다.
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 할시 출력할 쿼리 최대 길이 설정