👉 Batch Insert란 ?
Spring Data JPA의 saveAll() 메서드를 통하여 엔티티를 저장한다면 아래와 같이
데이터의 갯수만큼 Insert 쿼리가 나가게됩니다. 이렇게 된다면 저장해야될 데이터가 많아질 수록 시간이 오래걸리고 성능저하의 문제가 발생한다.
INSERT INTO users(created_at, modified_at, email, nickname, password, user_role) values('2024-11-20 20:43:05.944329', '2024-11-20 20:43:05.944329', 'dl805327@gmail.com', '권형심', '$2a$04$LbX5GfGOwEMKSBqN0QT6L.D5deA/Ys2Okyt1sc6E51uXcVh.rd0Tm', 'ROLE_USER')
INSERT INTO users(created_at, modified_at, email, nickname, password, user_role) values('2024-11-20 20:43:05.944329', '2024-11-20 20:43:05.944329', 'dl805327@gmail.com', '어묵', '$2a$04$LbX5GfGOwEMKSBqN0QT6L.D5deA/Ys2Okyt1sc6E51uXcVh.rd0Tm', 'ROLE_USER')
INSERT INTO users(created_at, modified_at, email, nickname, password, user_role) values('2024-11-20 20:43:05.944329', '2024-11-20 20:43:05.944329', 'dl805327@gmail.com', '피자', '$2a$04$LbX5GfGOwEMKSBqN0QT6L.D5deA/Ys2Okyt1sc6E51uXcVh.rd0Tm', 'ROLE_USER')
Batch Insert의 경우는 하나의 쿼리문으로 여러 데이터를 처리하기 때문에 성능이 뛰어나다.
INSERT INTO users(created_at, modified_at, email, nickname, password, user_role)
values('2024-11-20 20:43:05.944329', '2024-11-20 20:43:05.944329', 'dl805327@gmail.com', '권형심', '$2a$04$LbX5GfGOwEMKSBqN0QT6L.D5deA/Ys2Okyt1sc6E51uXcVh.rd0Tm', 'ROLE_USER'),
('2024-11-20 20:43:05.944329', '2024-11-20 20:43:05.944329', 'dl805327@gmail.com', '이이', '$2a$04$LbX5GfGOwEMKSBqN0QT6L.D5deA/Ys2Okyt1sc6E51uXcVh.rd0Tm', 'ROLE_USER'),
('2024-11-20 20:43:05.944329', '2024-11-20 20:43:05.944329', 'dl805327@gmail.com', '아아', '$2a$04$LbX5GfGOwEMKSBqN0QT6L.D5deA/Ys2Okyt1sc6E51uXcVh.rd0Tm', 'ROLE_USER')
😲 Batch Insert가 동작하지 않는 경우 !!
JPA와 MySQL을 함께 사용할 때 엔티티 @ID의 전략을 IDENTITY 전략을 사용할 시 auto_increment를 통해 PK 값을 자동으로 올려줘 편하지만 이 방식을 사용하면 Hibernate는 JDBC 수준에서 Batch Insert를 비활성화한다. 이유는 새로 할당할 Key 값을 미리 알수 없는 IDENTITY 전략을 사용할 경우, Hibernate가 채택한 flush 방식인 쓰기지연과 충돌이 발생하기 때문이다. 그렇기 때문에 IDENTITY 전략을 사용한다면 Batch Insert가 동작하지 않는다.
IDENTITY 전략을 사용하지 않는경우 아래의 옵션을 통해 batch_size를 조절하여 Batch Insert를 사용할 수 있다.
spring.jpa.properties.hibernate.jdbc.batch_size=개수
😁 JdbcTemplate를 사용하여 Batch Insert 사용하기
위의 제약 때문에 테이블 전략을 변경하는 방법도 있지만, JdbcTemplate를 사용한다면 IDENTITY 전략을 사용하면서 Batch Insert를 사용할 수 있다.
🦴 설정하기
DB-URL에 'rewriteBatchedStatements=true' 를 추가해주면된다.
spring.application.name=expert
spring.datasource.url=jdbc:mysql://localhost:3306/plus?rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=1234
추가로 옵션을 추가해줄 수도 있다.
spring.datasource.url=jdbc:mysql://localhost:3306/plus?rewriteBatchedStatements=true&profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=999999
- postfileSQL = true : Driver에 전송하는 쿼리를 출력한다.
- logger = Slf4JLogger : Driver에서 쿼리 출력 시 사용할 로거를 설정한다.
- MySQL 드라이버 : 기본값은 System.err로 출력하도록 설정되어 있기 때문에 필수로 지정해줘야한다.
- MariaDB 드라이버 : Slf4j를 이용하여 로그를 출력하기 때문에 설정할 필요가 없다.
- maxQuerySizeToLog = 999999 : 출력할 쿼리 길이
- MySQL 드라이버 : 기본값이 0으로 지정되어 있어 값을 설정하지 않을 경우 쿼리가 출력되지 않는다.
- MariaDB 드라이버 : 기본값어 1024로 지정되어 있다. MySQL과는 달리 0으로 지정 시 쿼리의 글자 제한이 무제한으로 설정된다.
🥁 코드
UserEntity
@Getter
@Entity
@NoArgsConstructor
@Table(name = "users")
public class User extends Timestamped {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String email;
private String password;
@Column(name = "nickname")
private String nickName;
@Enumerated(EnumType.STRING)
private UserRole userRole;
public User(String email, String password, String nickName, UserRole userRole) {
this.email = email;
this.password = password;
this.nickName = nickName;
this.userRole = userRole;
}
}
UserJdbcRepository
@Repository
@RequiredArgsConstructor
public class UserJdbcRepository {
private final JdbcTemplate jdbcTemplate;
public void batchInsert(List<User> users){
String sql = "INSERT INTO users"
+"(created_at, modified_at, email, nickname, password, user_role)"
+ " VALUES (?, ?, ?, ?, ?, ?)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(
PreparedStatement ps,
int i) throws SQLException {
User user = users.get(i);
ps.setTimestamp(1, Timestamp.valueOf(LocalDateTime.now()));
ps.setTimestamp(2, Timestamp.valueOf(LocalDateTime.now()));
ps.setString(3, user.getEmail());
ps.setString(4, user.getNickName());
ps.setString(5, user.getPassword());
ps.setString(6, user.getUserRole().toString());
}
@Override
public int getBatchSize() {
return users.size();
}
});
}
}
Test 코드
@Test
@DisplayName("Batch Insert 사용")
public void testInsert() {
long startTime = System.currentTimeMillis();
Random random = new Random();
String randomName;
int batchSize = 100000;
users = new ArrayList<>();
for(int i = 1; i <= 1000000; i++){
randomName = first[random.nextInt(first.length-1)] + name[random.nextInt(name.length-1)] + name[random.nextInt(name.length-1)];
User user = new User("dl"+i+"@gmail.com", password, randomName, UserRole.ROLE_USER);
users.add(user);
if(users.size() == batchSize) {
//List의 사이즈가 10_0000개가 될 시에 batchInsert를 시키고
//List를 비운다.
userJdbcRepository.batchInsert(users);
users.clear();
}
}
long endTime = System.currentTimeMillis();
log.info("BatchInsert 사용한 insert 걸리는 시간 :" + (endTime - startTime)/1000);
}
💻 성능비교
jdbcTemplate batch insert 미사용한 데이터 100만건 생성
jdbcTemplate batch insert 사용한 데이터 100만건 생성
⚙️ 트러블슈팅
Batch Insert를 이용하여 insert를 할 때 처음에는 100만개를 한번에 넣고 싶어서 100만개를 한번에 넣으려 했지만 아래와 같은 에러가 나왔다.
원인
Java heap 메모리 용량 부족
시도해본 방법들
1. 인텔리제이에서 heap영역 최대 크기 늘려주기
Help > Edit Custom VM Options > -Xmx4096m 으로 변경
2. List의 사이즈가 10만이 될 때마다 Beatch Insert를 실행 후 List를 비워준다.
해결
처음에 1번방법으로 해봤지만 그래도 똑같이 Java heap space 에러가 떠서
2번 방법으로 하였다.
느낀점
이번에 처음으로 대용량 데이터를 추가해봤는데 처음에 Batch Insert를 사용하지 않고 JPA SaveAll()을 사용하여 insert를 했을 때 거의 10분이라는 시간이 걸려서 굉장히 놀라웠다.. 그래서 이걸 줄일 방법이 있을까 싶은 마음에 검색을 해봤는데 Batch Insert라는 것을 알게 되어서 사용까지 해봤는데 9분걸리던게 40초 밖에 안걸린다는 점이 가장 놀라웠다.
'TIL' 카테고리의 다른 글
내일배움캠프 Kotlin & Spring 3기 수료 후기 (2) | 2025.01.09 |
---|---|
CI/CD (0) | 2024.11.21 |
Docker - 자주 사용하는 명령어 (0) | 2024.11.19 |
Docker란 ? (0) | 2024.11.18 |
JPA - QueryDSL을 이용한 동적쿼리 (0) | 2024.11.15 |