티스토리 뷰

대용량 데이터 삽입

mysql 8.4.2 + Springboot를 이용해서 대용량 데이터 삽입을 한 내용에 대해 포스팅하려고 합니다.

약 10만 명의 유저 mock 데이터를 생산하여 데이터베이스에 넣는 시간을 비교해 보겠습니다.

 

대략적인 개요는 아래와 같습니다.

  • 10만 명의 mock 유저 데이터를 만든다.
  • jpa saveAll()을 사용해서 데이터베이스에 insert를 한다. (id 생성 전략 -> Table )
  • jpa batch insert를 설정하고 insert 한다.
  • jdbc batch insert를 설정하고 insert 한다
  • 멀티스레드를 활용하여 jdbc batch insert를 한다.

설정 환경은 다음과 같습니다.

  • mysql 8.42
  • Springboot 2.7.5
  • mac m1
  • RAM 16gb 
  • intellj

1. 10만 명의 mock 데이터는 어떻게 만듦??

사실 대용량 데이터를 만드는 것부터가 문제인 경우가 있습니다. 

저는 mock 데이터이기 때문에 테스트에서 사용했던 Fixture Mokey라는 라이브러리를 사용했습니다.

테스트할 경우 객체를 생성해 주는 라이브러리입니다.

자세한 내용은 아래의 사이트를 참고해 주세요!

https://naver.github.io/fixture-monkey/v1-0-0/

 

Fixture Monkey

 

naver.github.io

Fixture Monkey에서는 테스트 코드를 제외하고는 사용을 권장하지 않습니다.

 

아래 코드는 10만 개의 mock 데이터를 만들어 List로 반환하여 줍니다.

	private List<User> users;

	@BeforeAll
	void setUpEach(){
		var value = new AtomicInteger(0);
		users = FixtureMonkeyFactory.get().giveMeBuilder(User.class)
			.setNull("id")
			.setLazy("email", () -> "test" + value.addAndGet(1) + "@gmail.com")
			.setLazy("displayName", () -> "displayName" + value.addAndGet(1))
			.setLazy("phone", () -> "010-0000-000" + value.addAndGet(1))
			.set("password", "password")
			.set("address", new Address("서울시 부평구 송도동", "101 번지"))
			.setLazy("realName", () -> "홍길동" + value.addAndGet(1))
			.set("roles", UserRoles.USER)
			.set("userStatus", UserStatus.USER_ACTIVE)
			.setNull("subscriptionCartId")
			.setNull("normalCartId")
			.setNull("oauthId")
			.setNull("reviewIds")
			.sampleList(100000);
	}

 

2. Mysql의 Identity 생성 전략의 문제 -> Table 생성 전략

Jpa에서 Mysql의 Identity 생성 전략을 사용할 경우 Batch insert를 지원하지 않습니다.

Identity 전략은 insert 된 후 id를 바로 영속성 콘텍스트에 저장해야 하기 때문에 개별적으로 insert가 수행되어야 합니다.

그런데 batch insert는 insert문을 한 번에 보내므로 id를 영속성 콘텍스트에 바로 저장하기 힘듭니다. 즉, 미리 id 값이 지정돼야 합니다. 

 

그래서 저는 Table 전략을 선택해 보았습니다. (Mysql은 시퀀스 전략이 없습니다!!)

create table id_sequences(
    sequences_name varchar(255) not null,
    next_val bigint,
    primary key(sequences_name)
)

 

@Slf4j
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@TableGenerator(
	name = "user_id_generator",
	table = "id_sequences",
	pkColumnValue = "user_id"
)
public class User extends BaseEntity {

	@Id
	@GeneratedValue(
		strategy = GenerationType.TABLE,
		generator = "user_id_generator"
	)
	private Long id;
    
    ...

 }

 

3. Jpa saveAll() 사용

우선 batch 설정을 하지 않고 삽입을 진행해 보았습니다.

Jpa의 saveAll()은 for문을 사용하여 하나씩 데이터를 insert 합니다.

spring:
  jpa:
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect
        hbm2ddl:
          auto: create
        show_sql: true
@Commit
@Test
@DisplayName("Table 전략 saveAll()")
void test1() throws Exception {
  userRepository.saveAll(users);
}

 

유저 mock 데이터 생성 시간 포함

table 전략은 키 생성용 전용 테이블에서 id를 가지고 와 적용하는 전략이기 때문에 select 쿼리와 update쿼리가 나갑니다.

id를 설정해 주기 위해서 select 쿼리와 update 쿼리가 나간다.

만약 identity 전략을 사용했다면 훨씬 적은 시간이 소요될 것 같습니다.

 

4. Jpa Batch insert 적용

이제 batch insert를 적용해 보겠습니다.

Mysql 8.0 이상부터 rewriteBatchedStatements=true는 생략 가능합니다.

yml 설정입니다.

spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 100 //batch 크기 설정
        order_inserts: true  //insert batch
        order_updates: true  //update batch
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect
        hbm2ddl:
          auto: create
        show_sql: true
        
     datasource:
       username: root
       password: 1234
       driver-class-name: com.mysql.cj.jdbc.Driver
       url: jdbc:mysql://localhost:3306/pillivery?serverTimezone=Asia/Seoul&jdbcCompliantTruncation=false

jpa batch insert 적용

실행 결과 약 23%가 증가했습니다.

성능 향상은 있지만 눈에 띌 정도는 아닌 거 같습니다. 역시나 키를 가져오고 업데이트하는 쿼리가 나가는 것이 영향이 있는 것 같습니다.

 

5. jdbc batch insert 적용

Spring Data JDBC를 이용해서 Batch insert를 적용해 보았습니다.

이때, JDBC를 사용하기 때문에 id 생성 전략을 identity로 바꾸었습니다. 

JDBC는 영속성 콘텍스트를 사용하지 않으므로 identity 전략을 사용해도 무관합니다!

 

밑의 코드는 JDBC 코드로 batch를 적용한 코드입니다.

package com.team33.moduleadmin.repository;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.team33.modulecore.core.user.domain.entity.User;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Repository
@RequiredArgsConstructor
public class UserBatchDao{

	private final JdbcTemplate jdbcTemplate;

	@Value("${spring.jpa.properties.hibernate.jdbc.batch_size}")
	private int batchSize;

	public void saveAll(List<User> users) {
		log.info("batch size : {}", batchSize);
		log.info("users size : {}", users.size());
		int batchCount = 0;
		List<User> subUsers = new ArrayList<>(104);
		for (int i = 0; i < users.size(); i++) {
			subUsers.add(users.get(i));
			if ((i + 1) % batchSize == 0) {
				batchCount = batchInsert(subUsers, batchCount);
			}
		}
		if(!subUsers.isEmpty()) {
			batchCount = batchInsert(subUsers, batchCount);
		}
		log.info("batchCount : {}", batchCount);
	}

	private int batchInsert(List<User> subUsers, int batchCount) {
		jdbcTemplate.batchUpdate(
			"insert into users ("
				+ "email, displayName, phone, password, city, detailAddress, realName, roles, userStatus, subscriptionCartId, normalCartId, oauthId) "
				+ "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
			new BatchPreparedStatementSetter() {
				@Override
				public void setValues(PreparedStatement ps, int i) throws SQLException {
					ps.setString(1, subUsers.get(i).getEmail());
					ps.setString(2, subUsers.get(i).getDisplayName());
					ps.setString(3, subUsers.get(i).getPhone());
					ps.setString(4, subUsers.get(i).getPassword());
					ps.setString(5, subUsers.get(i).getAddress().getCity());
					ps.setString(6, subUsers.get(i).getAddress().getDetailAddress());
					ps.setString(7, subUsers.get(i).getRealName());
					ps.setString(8, String.valueOf(subUsers.get(i).getRoles()));
					ps.setString(9, String.valueOf(subUsers.get(i).getUserStatus()));
					ps.setString(10, String.valueOf(i));
					ps.setString(11, String.valueOf(i + 1));
					ps.setString(12, null);
				}

				@Override
				public int getBatchSize() {
					return subUsers.size();
				}
			}
		);
		subUsers.clear();
		batchCount++;
		return batchCount;
	}
}

JDBC batch 사용

 

Jpa batch insert를 사용했을 경우보다 약 91퍼센트 향상이 됐습니다.

6. 멀티스레드 JDBC Batch insert 사용

이번에는 멀티스레드를 이용해서 Batch insert를 시도해 보겠습니다.

 

아래 코드에서 Callable 객체를 사용해서 멀티스레드로 Batch insert를 시도해 보았습니다.

데이터소스의 커넥션을 풀을 모두 사용하였습니다. 

package com.team33.moduleadmin.service;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.team33.moduleadmin.repository.UserBatchDao;
import com.team33.modulecore.core.user.domain.entity.User;
import com.zaxxer.hikari.HikariDataSource;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UserBatchService {

	private final DataSource jdbcDataSource;
	private final UserBatchDao userBatchDao;

	@Value("${spring.jpa.properties.hibernate.jdbc.batch_size}")
	private int batchSize;

	public void saveAll(List<User> users) {
		HikariDataSource hikariDataSource = (HikariDataSource) jdbcDataSource;
		ExecutorService executorService = Executors.newFixedThreadPool( hikariDataSource.getMaximumPoolSize());
		List<List<User>> userSubList = getUserSubList(users);

		List<Callable<Void>> collect = userSubList.stream().map(subList -> (Callable<Void>)() -> {
			userBatchDao.saveAll(subList);
			return null;
		}).collect(Collectors.toList());

		try {
			executorService.invokeAll(collect);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}

	private List<List<User>> getUserSubList(List<User> users) {
		List<List<User>> listOfSubList = new ArrayList<>(batchSize + 4);
		for (int i = 0; i < users.size(); i += batchSize) {
			if (i + batchSize <= users.size()) {
				listOfSubList.add(users.subList(i, i + batchSize));
			} else {
				listOfSubList.add(users.subList(i, users.size()));
			}
		}
		return listOfSubList;
	}
}

 

멀티스레드 사용 jdbc batch insert

 

Jpa batch insert를 사용했을 경우보다 약 95% 향상이 됐습니다.

그리고 싱글 스레드를 사용한 JDBC Batch insert 보다 50% 향상됐습니다.

 


정리

Mysql에서 Batch insert를 할 경우에는 JDBC를 사용하는 것이 훨씬 빠릅니다.

만약 시퀀스 전략을 사용할 수 있는 데이터베이스라면 Jpa만을 사용해서 훨씬 좋은 성능 향상을 할 수 있을 것이라고 예상됩니다.

또한, Batch size를 얼마로 하느냐도 성능에 영향을 줄 것으로 예상됩니다.


reference

- https://homoefficio.github.io/2020/01/25/Spring-Data%EC%97%90%EC%84%9C-Batch-Insert-%EC%B5%9C%EC%A0%81%ED%99%94/

 

Spring Data에서 Batch Insert 최적화

Spring Data에서 Batch Insert 최적화Spring Data JPA가 안겨주는 편리함 뒤에는 가끔 성능 손실이 숨어있다. 이번에 알아볼 Batch Insert도 그런 예 중 하나다. 성능 손실 문제가 발생하는 이유와 2가지 해결

homoefficio.github.io

- https://medium.com/@wahyaumau/boost-jpa-bulk-insert-performance-by-90-3a6232d9068d

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함