Spring JDBC๋ฅผ ์ฌ์ฉํ์ฌ Batch Insert ์ํํ๊ธฐ
์๋ก
ํ์์ JPA๋ฅผ ํ์ฉํ๋ฉด์ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ ๋, ๋ง์ ๊ฐ๋ฐ์๋ค์ด repository.save() ํจ์๋ฅผ ์ด์ฉํ์ฌ ๋จ๊ฑด ๋จ์๋ก ์ ์ฅํ๋ ๋ก์ง์ ๊ตฌํํฉ๋๋ค. ๊ทธ๋ฌ๋ ์๋ฐฑ, ์์ฒ ๊ฐ ์ด์์ ๋๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํด์ผ ํ๋ ๊ฒฝ์ฐ์๋ ํด๋น ๋ฐฉ๋ฒ์ ์ฌ์ฉํด๋ ๋ ๊น์? ์ด๋ ์ฑ๋ฅ๊ณผ ํด๋ผ์ด์ธํธ์ ๋๊ธฐ ์๊ฐ์ ๊ฒฐ์ ํ๋ ์ค์ํ ์์์ ๋๋ค.
๋ฐ๋ผ์ ์ด๋ฒ ๊ธ์์๋ Spring ํ๊ฒฝ์์ ๋ค๋์ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ์ฝ์ ํ๋ ๋ฐฉ๋ฒ์ธ Batch Insert์ ๋ํด ์์๋ณด๋ ค ํฉ๋๋ค.
ํ ์คํธ๋ Apple Silicon (M1), Java 11, Spring Boot, JPA, JUnit5, Docker MySQL ํ๊ฒฝ์์ ์งํํ์์ผ๋ฉฐ, ์์ค์ฝ๋๋ ๊นํ๋ธ์์ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
๋ชฉ์ฐจ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- Batch Insert๋?
- Identity ์ ๋ต์ผ๋ก๋ Batch Insert๊ฐ ๋ถ๊ฐ๋ฅํ ์ด์
- JdbcTemplate๋ฅผ ์ฌ์ฉํ์ฌ Batch Insert ์ ์ฉํ๊ธฐ
- ์ฑ๋ฅ ๋น๊ต
Batch Insert๋?
Batch Insert๋ ๋ง์ ์์ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์ฝ์ ํ๋ ๋ฐฉ๋ฒ์ ๋๋ค. ์๋์ ์ผ๋ฐ์ ์ธ Insert SQL๊ณผ ๋น๊ตํด ๋ณด๋ฉด ์ดํดํ๊ธฐ ์ฝ์ต๋๋ค.
INSERT INTO table (col1, col2) VALUES (val1, val11);
INSERT INTO table (col1, col2) VALUES (val2, val22);
INSERT INTO table (col1, col2) VALUES (val3, val33);
์ ์ฟผ๋ฆฌ๋ ๊ฐ๋ณ Insert์ ๋๋ค.
INSERT INTO table (col1, col2) VALUES
(val1, val11),
(val2, val22),
(val3, val33);
์ ์ฟผ๋ฆฌ๋ Batch Insert์ ๋๋ค.
๋ณดํต ์ฟผ๋ฆฌ๋ฅผ ์คํํ๊ณ ์๋ต์ ๋ฐ์ ํ์์ผ ๋ค์ ์ฟผ๋ฆฌ๋ฅผ ์ ๋ฌํ๊ธฐ ๋๋ฌธ์ ๊ฐ๋ณ Insert์ ๊ฒฝ์ฐ ์ง์ฐ ์๊ฐ์ด ๋์ด๋์ง๋ง, ํ๋์ ํธ๋์ญ์ ์ผ๋ก ๋ฌถ์ด๋ Batch Insert๋ ํ๋์ ์ฟผ๋ฆฌ๋ฌธ์ผ๋ก ์ฌ๋ฌ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ์ด ๋ฐ์ด๋ฉ๋๋ค.
Identity ์ ๋ต์ผ๋ก๋ Batch Insert๊ฐ ๋ถ๊ฐ๋ฅํ ์ด์
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Example {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
JPA์ MySQL์ ํจ๊ป ์ฌ์ฉํ ๋, ์์ ๊ฐ์ด IDENTITY ์ ๋ต์ ์ฌ์ฉํ์ฌ auto_increment๋ฅผ ํตํด PK ๊ฐ์ ์๋์ผ๋ก ์ฆ๊ฐ์ํค๋ ๋ฐฉ์์ ์ผ๋ฐ์ ์ผ๋ก ์ฌ์ฉํ๋ฉฐ. ์ด๋ ID๋ @GeneratedValue(strategy = GenerationType.IDENTITY)๋ก ์ค์ ํ๊ณ ์๋์ ๊ฐ์ด save() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ ์ฅํ ์ ์์ต๋๋ค.
Product product = new Product(title, price);
productRepository.save(product);
์ด ๋ฐฉ์์ Spring Data JPA์์ ์ ๊ณตํ๋ JpaRepository.save(T) ๋ฉ์๋์ ๋ด๋ถ ๋์ ๋ฐฉ์์ผ๋ก, ID๊ฐ์ ๋ช ์ํ์ง ์์๋ ์๋์ผ๋ก ์ ์ฅ๋ฉ๋๋ค.
๊ทธ๋ฌ๋ ์ด ๋ฐฉ์์ ์ฌ์ฉํ๋ฉด Hibernate๋ JDBC ์์ค์์ Batch Insert๋ฅผ ๋นํ์ฑํํฉ๋๋ค(์ฐธ๊ณ ). ์ด์ ๋ ์๋ก ํ ๋นํ Key ๊ฐ์ ๋ฏธ๋ฆฌ ์ ์ ์๋ IDENTITY ์ ๋ต์ ์ฌ์ฉํ ๊ฒฝ์ฐ, Hibernate๊ฐ ์ฑํํ flush ๋ฐฉ์์ธ 'Transactional Write Behind'์ ์ถฉ๋์ด ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ ๋๋ค. ๋ฐ๋ผ์ IDENTITY ์ ๋ต์ ์ฌ์ฉํ๋ฉด Batch Insert๋ ๋์ํ์ง ์์ต๋๋ค.
์ด๋ฅผ ๊ตฌ์ฒด์ ์ธ ์๋ก ์ค๋ช ํ๋ฉด, OneToMany์ Entity๋ฅผ insertํ ๊ฒฝ์ฐ Hibernate๋ ์๋ ๊ณผ์ ์ ์งํํ๋ฉฐ ์ด ๊ณผ์ ์ ์ฟผ๋ฆฌ๋ฅผ ๋ชจ์์ ์คํํฉ๋๋ค.
- ๋ถ๋ชจ Entity๋ฅผ insertํ๊ณ ์์ฑ๋ Id๋ฅผ ๋ฐํ
- ์์ Entity์์๋ ์ด์ ์ ์์ฑ๋ ๋ถ๋ชจ Id๋ฅผ FK ๊ฐ์ผ๋ก ์ฑ์์ insert
ํ์ง๋ง Batch Insert์ ๊ฐ์ ๋๋ ๋ฑ๋ก์ ๊ฒฝ์ฐ, ์ด ๋ฐฉ์์ ์ฌ์ฉํ ์ ์๋๋ฐ ๋ถ๋ชจ Entity๋ฅผ ํ ๋ฒ์ ๋๋์ผ๋ก ๋ฑ๋กํ๊ฒ ๋๋ฉด ์ด๋ ์์ Entity๊ฐ ์ด๋ ๋ถ๋ชจ Entity์ ๋งคํ๋์ด์ผ ํ๋์ง ์ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ IDENTITY ์ ๋ต์ ์ฌ์ฉํ๋ฉด Batch Insert๋ ๋์ํ์ง ์์ต๋๋ค.
๋ฌผ๋ก Auto Increment๊ฐ ์๋ ๊ฒฝ์ฐ์ ์๋์ ๊ฐ์ ์ต์ ์ ํตํด values ์ฌ์ด์ฆ๋ฅผ ์กฐ์ ํ์ฌ Batch Insert๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
spring.jpa.properties.hibernate.jdbc.batch_size=๊ฐ์
JdbcTemplate๋ฅผ ์ฌ์ฉํ์ฌ Batch Insert ์ ์ฉํ๊ธฐ
ํ ์ด๋ธ ์ ๋ต์ ๋ณ๊ฒฝํ๋ ๋ฐฉ๋ฒ์ด ์์ง๋ง, ์ด๋ ํ ์ด๋ธ ๋ณ๊ฒฝ์ด ํ์ํ๊ณ ์ด๋ฏธ ์งํ ์ค์ธ ํ๋ก์ ํธ์ ์ ์ฉํ๊ธฐ ์ด๋ ต์ต๋๋ค. ๊ทธ๋์ ๋์์ผ๋ก JdbcTemplate๋ฅผ ์ฌ์ฉํ์ฌ Batch Insert๋ฅผ ์ ์ฉํ๋ ๋ฐฉ์์ ์ค๋ช ํ๊ฒ ์ต๋๋ค.
JdbcTemplate์๋ Batch๋ฅผ ์ง์ํ๋ batchUpdate() ๋ฉ์๋๊ฐ ์์ต๋๋ค. ๋จผ์ MySQL์์ Bulk Insert๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด, DB-URL์ 'rewriteBatchedStatements=true' ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
spring:
datasource:
url: jdbc:mysql://localhost:3306/batch_test?&rewriteBatchedStatements=true
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
'rewriteBatchedStatements'๋ฅผ true๋ก ์ค์ ํ์ง ์์ผ๋ฉด Insert ์ฟผ๋ฆฌ๊ฐ ์ฌ์ ํ ๋จ๊ฑด์ผ๋ก ์ํ๋ฉ๋๋ค.
Batch Insert๊ฐ ์ ๋๋ก ์งํ๋๋์ง ํ์ธํ๋ ค๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ถ๊ฐ ์ต์ ์ ์ค์ ํ ์ ์์ต๋๋ค.
spring:
datasource:
url: jdbc:mysql://localhost:3306/db๋ช
?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์ผ๋ก ์ง์ ์ ์ฟผ๋ฆฌ์ ๊ธ์ ์ ํ์ด ๋ฌด์ ํ์ผ๋ก ์ค์ ๋ฉ๋๋ค.
๋ค์์ Entity์ Batch Insert๋ฅผ ์ ์ํ Repository ์ฝ๋์ ๋๋ค. Batch Insert ๊ด๋ จ ์ฝ๋๋ ๊ณต์๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ฌ ์์ฑํ์์ต๋๋ค.
์ฝ๋
@Entity(name = "product")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Long price;
public Product(String title, Long price) {
this.title = title;
this.price = price;
}
}
@Repository
@RequiredArgsConstructor
public class ProductBulkRepository {
private final JdbcTemplate jdbcTemplate;
@Transactional
public void saveAll(List<Product> products) {
String sql = "INSERT INTO product (title, price) " +
"VALUES (?, ?)";
jdbcTemplate.batchUpdate(sql,
products,
products.size(),
(PreparedStatement ps, Product product) -> {
ps.setString(1, product.getTitle());
ps.setLong(2, product.getPrice());
});
}
}
batchUpdate ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ๋ ์์๋๋ก "sql, batchArgs, batchSize, sql ?์ ๋ค์ด๊ฐ ๊ฐ"์ ๋๋ค.
๋๋ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ ์๋ ์์ต๋๋ค.
@Repository
@RequiredArgsConstructor
public class ProductBulkRepository {
private final JdbcTemplate jdbcTemplate;
@Transactional
public void saveAll(List<Product> products) {
String sql = "INSERT INTO product (title, price) " +
"VALUES (?, ?)";
jdbcTemplate.batchUpdate(sql,
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Product product = products.get(i);
ps.setString(1, product.getTitle());
ps.setLong(2, product.getPrice());
}
@Override
public int getBatchSize() {
return products.size();
}
});
}
}
์ฑ๋ฅ๋น๊ต
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Transactional
@Rollback(value = false)
class ProductTest {
private static final int COUNT = 10_000;
@Autowired
private ProductRepository productRepository;
@Autowired
private ProductBulkRepository productBulkRepository;
@BeforeAll
void init() {
Product product = new Product("์ด๊ธฐ", 10_000L);
productRepository.save(product);
}
@Test
@DisplayName("normal insert")
void ์ผ๋ฐ_insert() {
long startTime = System.currentTimeMillis();
for (long i = 2; i <= COUNT; i++) {
String title = "์ด๋ฆ: " + i;
long price = i + 1L;
Product product = new Product(title, price);
productRepository.save(product);
}
long endTime = System.currentTimeMillis();
System.out.println("---------------------------------");
System.out.printf("์ํ์๊ฐ: %d\n", endTime - startTime);
System.out.println("---------------------------------");
}
@Test
@DisplayName("bulk insert")
void ๋ฒํฌ_insert() {
long startTime = System.currentTimeMillis();
List<Product> products = new ArrayList<>();
for (long i = 0; i < COUNT; i++) {
String title = "์ด๋ฆ: " + i;
long price = i + 1L;
Product product = new Product(title, price);
products.add(product);
}
productBulkRepository.saveAll(products);
long endTime = System.currentTimeMillis();
System.out.println("---------------------------------");
System.out.printf("์ํ์๊ฐ: %d\n", endTime - startTime);
System.out.println("---------------------------------");
}
}
๊ฐ๋จํ๊ฒ ์์ ๊ฐ์ด ํ ์คํธ์ฝ๋๋ฅผ ์์ฑํ์ฌ ์ํ์๊ฐ์ ๋น๊ตํ์ต๋๋ค.
๋จ์ํ 10,000๊ฑด์ ์ฝ์ ํ๋๋ฐ ์์ ๊ฐ์ ์ํ์๋์ ์ฐจ์ด๊ฐ ๋์์ต๋๋ค. ๋จ์ํ ๋ฐ์ดํฐ์๋ ์์ฒญ๋ ์ฐจ์ด๋ฅผ ๋ณด์ฌ์ฃผ๋๋ฐ ๋ฐ์ดํฐ๊ฐ ๋ ๋ง๊ณ , ๋ณต์กํ๋ค๋ฉด Batch Insert๋ฅผ ํ์๋ก ์ฌ์ฉํ์ฌ์ผ ํ๋ค๊ณ ์๊ฐํฉ๋๋ค.