들어가며
저번 게시글 대용량
데이터 등록
에서는 JPA(Java Persistence API)
배치 인서트(Batch Insert)
를 사용해서 대용량의 데이터
를 등록했습니다.
하지만 프로젝트
가 진행되면서 추가할 데이터
의 수가 점점 더 많아져 홍수
가 되었고 시간을 단축할 필요성이 생겼습니다.
그래서 방법을 찾아보니 JDBC(Java Database Connectivity)
를 이용한 배치 인서트
방식이 속도가 빠르다는 것을 알았습니다.
그런데 막상 기술을 채택하려니 항상 인터넷 자료만 보고 기술을 바로 적용했던 기억이 떠올라 이번 기회에 직접 테스트 해보기로 결정했습니다.
즉, 이번 글의 주제는 JPA를 이용한 배치 인서트와 JDBC를 이용한 배치 인서트를 비교해보고 성능을 확인 후 실제 프로젝트에 적용입니다.
배치 인서트
일반적인 인서트
는 단일 레코드를 등록하기 때문에 매 건 마다 트랜잭션(Transaction)
과 검증을 수행하면서 엄청난 시간 소요와 네트워크
비용을 발생시킵니다.
그래서 단일 등록으로 대량의 데이터
를 등록하면 사용자가 아래 밈처럼 기다리다 지치거나 서비스 사용을 아예 꺼릴 수 있습니다.
이러한 문제를 방지하기 위한 방법 중 하나가 데이터베이스
에 대량의 데이터
를 한 번에 삽입하는 배치 인서트
입니다.
배치 인서트
는 대규모 데이터
를 효율적으로 처리하는 방법이며 여러 개의 SQL
문장을 하나의 작업으로 묶어서 데이터베이스
에 전송해 여러 레코드를 삽입하는 방식입니다.
따라서 일반적인 인서트
보다 훨씬 더 빠른 성능을 제공합니다.
특징지어 소개하면,
성능 향상
: 대량의데이터
를 한 번에 삽입하면데이터베이스
에 대한 연산을 줄일 수 있어 성능 향상 효과를 가져옵니다.
데이터베이스
I/O
및로깅 오버헤드
가 감소합니다.트랜잭션 관리
: 여러 행을 하나의트랜잭션
에서 삽입하기 때문에 모든 행이 삽이되거나 모든 행이롤백
됩니다.
이 때문에배치 인서트
전에 해당데이터
에 대한무결성
입증 작업을 해주는 것이 중요합니다.작업 간소화
:데이터베이스
에 대한 쿼리를 수행하는 횟수를 줄일 수 있어 코드 작성과 관리에 용이합니다스크립트 성능 향상
:데이터 마이그레이션 스크립트
또는 초기데이터
적재 작업에 매우 유용합니다.
저번 게시글에서도 설명했지만, 하나의 트랜잭션
에서 대량의 데이터
를 등록하기 때문에 해당 데이터
들에 대한 무결성
이 입증되지 않았을 때를 생각해야 합니다.
예로 배치 인서트
중 문제가 발생한다면 해당 트랜잭션
의 배치 인서트
가 모두 롤백
되어 데이터
가 등록되지 않는 결과가 발생할 수 있습니다.
그러므로 특정한 경우 또는 무결성
이 입증된 경우에만 사용해야 합니다.
배치 인서트의 종류
1. JPA 배치
JPA
를 이용한 배치
는 엔티티매니저(EntityManager)
의 persist()
메소드를 사용하여 엔티티
객체를 영속화
시키고 사용자가 원하는
배치 사이즈(Batch Size)
에 도달했을 때 flush()
를 활용해 데이터베이스
에 반영하고 clear()
를 통해 영속성 컨텍스트
를 초기화하는 과정을 반복합니다.
코드로 보면 이렇습니다. 엔티티 영속화
과정을 보기 쉽게 JPA
구현체 중 하이버네이트(Hibernate)
를 활용했습니다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("nextree");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin(); // 트랜잭션 시작
for (int i = 0; i < 100; i++) {
Nextree nextree = new Nextree(i);
em.persist(nextree); // 엔티티 객체를 영속화합니다.
if (i % 50 == 0) { // 50개 단위로 메모리를 비웁니다.
em.flush(); // 영속화된 엔티티를 데이터베이스에 반영합니다.
em.clear(); // 영속성 컨텍스트를 초기화합니다.
}
}
em.getTransaction().commit();
em.close(); // 엔티티 매니저 종료
여기서 핵심은 persist()
를 호출할 때마다 INSERT SQL
을 실행하는 것이 아니라 쌓아두고 flush()
를 통해 해당 트랜잭션
을 커밋
하는 시점에 SQL
을 동시 실행하는 것입니다.
유의할 점은 트랜잭션 커밋
전까지 영속성 컨텍스트
에 캐시를 유지하고 엔티티
를 계속 쌓기 때문에 배치 사이즈
를 지나치게 높게 설정하면 메모리 사용량이 크게 늘어나는 문제가 발생할 수 있습니다.
그러므로 등록할 데이터
의 특성에 맞는 적절한 배치 사이즈
결정이 중요합니다.
2. JDBC 배치
JDBC
를 이용한 배치는 여러 개의 SQL
문을 한 번에 실행하여 대량의 데이터
를 효율적으로 처리하는 점에서 JPA
와 동일합니다.
단지, 데이터베이스
와 직접 상호작용하기 때문에 엔티티 영속화
과정이 없는 점과 쿼리
를 직접 작성하고 해당 쿼리
를 addBatch()
로 실행문에 더하고 executeBatch()
메소드를 활용해 배치
처리를 수행하는 점이 다릅니다.
try (Connection conn = DriverManager.getConnection(url, username, password);
Statement stmt = conn.createStatement()) {
conn.setAutoCommit(false); // 자동 커밋 비활성화
for (int i = 0; i < 100; i++) {
String sql = "INSERT INTO nextree (column1, column2) VALUES ('value1', 'value2')";
stmt.addBatch(sql); // 배치에 SQL 문 추가
}
int[] result = stmt.executeBatch(); // 배치 실행 결과를 배열로 반환
conn.commit(); // 변경 사항 커밋
} catch (SQLException e) {
e.printStackTrace();
}
JPA VS JDBC 장단점 비교
위에서 배치 인서트에 JPA 배치 인서트
와 JDBC 배치 인서트
가 있다는 것을 파악했습니다. 그러면 배치 인서트
이전에 두 기술은 어떤 장단점이 있는지 파악할 필요성을 인식했습니다.
JPA의 장단점
JPA 장점
JAVA
의객체지향적
방식을 그대로 사용해데이터
를 다룰 수 있어 개발자가SQL
을 직접 다루지 않습니다.- 위의 장점으로 인해
쿼리
문법에 대한 지식이 크게 필요 없어 생산성이 높고 유지보수 및 확장이 용이합니다. DBMS
에 독립적이므로 다양한데이터베이스
에 대한 호환성이 좋습니다
JPA 단점
- 후술할
JDBC
보다 성능이 느립니다. - 복잡한 쿼리를 작성하기 어렵고 쿼리 튜닝도 힘들어 최적화에 제한이 있을 수 있습니다.
JDBC의 장단점
JDBC의 장점
- 성능 최적화와 쿼리 튜닝에 유리하고 복잡한
SQL
쿼리를 작성하는 데 자유롭습니다. 데이터베이스
와 직접적으로 연결되어 빠른 속도를 자랑합니다.엔티티 영속화
과정이 존재하지 않습니다.
JDBC 단점
SQL
쿼리를 직접 작성하여 해서 쿼리 문법에 대한 이해가 필요합니다.- 가장 큰 단점으로
DBMS
마다쿼리
문법이 달라 호환성 문제가 발생할 수 있습니다. 즉,DBMS
에 따라쿼리
를 다시 작성해야 합니다. - 위의 단점으로 인해 코드가 복잡해지고 유지보수 및 확장이 어려워지는 문제가 발생합니다.
즉,JPA
는DBMS
를 편식하지 않아 범용성이 뛰어나고 확장성이 높지만,JDBC
에 비해 성능이 떨어지고SQL
튜닝이 쉽지 않습니다.
따라서 게시글의 근본 목적인 빠른 성능을 위해서는JDBC 배치 인서트
를 선택해야 하는 쪽으로 기울었습니다.
하지만 아직 성능 비교가 남았기 때문에 비교 후에 선택하겠습니다.
JPA 배치 인서트 VS JDBC 배치 인서트 성능 비교 후 선택 (등록 데이터 수 : 150,000개 / 배치 사이즈 : 2,000개)
JPA | JDBC | |
---|---|---|
1회차 | 450000ms (7분 30.0초) | 254000ms (4분 14.0초) |
2회차 | 499000ms (8분 19.0초) | 250000ms (4분 10.0초) |
3회차 | 493000ms (8분 13.0초) | 237000ms (3분 57.0초) |
4회차 | 428000ms (7분 8.0초) | 254000ms (4분 14.0초) |
5회차 | 493000ms (8분 13.0초) | 264000ms (4분 24.0초) |
평균 | 472600ms (7분 52.6초) | 251800ms (4분 11.8초) |
결과를 살펴보면 JDBC
를 이용한 배치 인서트
가 JPA
의 배치 인서트
에 비해 평균적으로 약 46.77% 빠르다는 유의미한 결과가 나왔습니다.
JDBC
성능이 압도적으로 좋았기 때문에 기존의 JPA 배치 인서트
에서 JDBC 배치 인서트
로 전환을 선택했습니다.
JDBC와 JDBC 템플릿
JDBC
를 이용한 배치 인서트
로 전환 결정 후 JDBC
외에도 스프링 프레임워크
에서 제공하는 JDBC 템플릿
이라는 기술도 있다는 것을 확인했습니다.
그래서 JDBC 배치 인서트
사용 전에 JDBC 템플릿
이 뭔지 알아보고 두 기술의 장단점을 비교한 후 어떤 기술을 사용할지 정하기로 했습니다.
JDBC
JDBC
는 자바
언어를 사용하여 데이터베이스
와 상호 작용하기 위한 자바
표준 API 입니다.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/nextree";
String username = "nextree";
String password = "nextree";
try (Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM nextree")) {
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("actor"));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
JDBC 장점
세밀한 제어
: 기존JDBC
를 사용하면SQL
쿼리, 연결,트랜잭션
관리 등을 직접 제어가 가능합니다.범용성
: 거의 모든 종류의데이터베이스
와 호환됩니다. 다양한데이터베이스
를 사용할 경우 선택해야 합니다.
JDBC 단점
코드 복잡성
:Connection
,Statement
,ResultSet
등을 직접 관리하고 예외 처리도 해줘야 합니다.반복적인 코드 작성
: 매번데이터베이스
연결과 자원 해제를 위한 코드를 작성해야만 합니다.
JDBC 템플릿
JDBC 템플릿
은 스프링 프레임워크
에서 제공하는 기술로, JDBC
를 보다 쉽게 사용하고 관리할 수 있도록 돕는 기술입니다.
사용방법은 어렵지 않습니다.
왜냐하면 기본적으로 스프링 부트(Spring boot)
의 application.yml
또는 application.properties
파일에서 데이터베이스
연결 정보를 읽고 DataSource
빈을 자동으로 구성하는데 이 빈이 JDBC 템플릿
을 생성하는데 사용됩니다.
즉, 스프링 부트
에서 데이터베이스
연결 설정만 완료하면 JDBC 템플릿
을 언제든 아래 코드처럼 사용이 가능합니다.
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
public class JdbcTemplateExample {
private final JdbcTemplate jdbcTemplate;
public static void main(String[] args) {
jdbcTemplate.query(
"SELECT * FROM nextree",
(rs, rowNum) -> rs.getString("actor")
).forEach(System.out::println);
}
}
JDBC 템플릿 장점
간결한 코드
: SQL 쿼리 실행, 결과 처리, 예외 처리 등에 대한 반복적 코드 작성을 없앱니다.자원 관리
:Connection
및PreparedStatement
와 같은 리소스 생성과 해제를 자동으로 관리합니다.일관된 예외 처리
: 여러 예외가 발생하는 것이 아닌스프링
의DataAccessException
계열로 예외 처리가 가능합니다.
JDBC 템플릿 단점
세밀한 제어 불가
:템플릿
은 틀이기 때문에직접적인 접근
을 통한 세부적인 리소스의 컨트롤이 어렵고추상적인 접근
만 가능합니다.
JDBC 템플릿 선택!
현재 프로젝트가 MySql
하나만 사용하고 있기 때문에 굳이 JDBC
를 사용해서 반복적인 코드 작성을 할 필요가 없어 JDBC 템플릿
을 선택했습니다.
JDBC 템플릿
을 통해 JDBC
코드를 추상화
하고 중복을 제거하며 개발 생산성
을 향상시킬 수 있습니다.
JDBC 템플릿 배치 인서트 방법
JDBC 템플릿
은 배치 작업을 위한 전용 인터페이스 BatchPreparedStatementSetter
가 존재합니다.
이 인터페이스를 활용하면 아래와 같은 장점이 있습니다.
public class NextreeBatchSetter implements BatchPreparedStatementSetter {
//
private List<NextreeJpo> nextreeJpos;
public NextreeBatchSetter(List<NextreeJpo> nextreeJpos){
//
this.nextreeJpos = nextreeJpos;
}
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
NextreeJpo nextreeJpo = nextreeJpos.get(i);
ps.setLong(1, nextreeJpo.getEntityVersion());
ps.setString(2, nextreeJpo.getModifiedBy());
ps.setLong(3, nextreeJpo.getModifiedOn());
ps.setString(4, nextreeJpo.getRegisteredBy());
ps.setLong(5, nextreeJpo.getRegisteredOn());
ps.setString(6, nextreeJpo.getActorId());
ps.setString(7, nextreeJpo.getPavilionId());
ps.setString(8, nextreeJpo.getStageId());
ps.setString(9, nextreeJpo.getDatabaseId());
ps.setString(10, nextreeJpo.getDatabaseName());
ps.setString(11, nextreeJpo.getDescription());
ps.setString(12, nextreeJpo.getName());
ps.setString(13, nextreeJpo.getTableMetricJson());
ps.setString(14, nextreeJpo.getId());
}
@Override
public int getBatchSize() {
return nextreeJpos.size();
}
}
일괄 처리
:BatchPreparedStatementSetter
를 사용하면 여러개의데이터
레코드를 한 번에 효율적으로 일괄 처리할 수 있습니다.코드 간소화
: 개발자는데이터
준비 및배치 처리 논리
의 간소화가 가능합니다. 이로 인해SQL문
과매개변수
설정에 집중할 수 있으며JDBC 템플릿
사용의 효과를 높입니다.에러 처리 및 롤백
:배치
작업 중 하나라도 실패하면롤백
이 가능하며,에러
처리 및롤백
관리가 용이합니다.
이는데이터베이스
작업의 안전성을 높입니다.
위와 같은 이유들로 JDBC 템플릿 배치 인서트
에 BatchPreparedStatementSetter
를 사용했습니다.
@Override
@Transactional
public void insertNextreeBatch(List<NextreeJpo> nextreeJpos) {
//
String sql = "insert into nextree (entity_version, modified_by, modified_on, registered_by, registered_on, actor_id," +
" pavilion_id, stage_id, database_id, database_name, description, name, table_metric_json, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
NextreeBatchSetter setter = new NextreeBatchSetter(nextreeJpos);
jdbcTemplate.batchUpdate(sql, setter);
}
따라서 위와 같이 일괄적으로 대량의 데이터
를 BatchPreparedStatementSetter
를 통해 준비하고 바로 배치 작업
을 수행했습니다.
batchUpdate()
메소드는 JDBC 템플릿
에서 여러개의 SQL
문을 일괄 처리하고 데이터베이스
에 대량의 작업을 수행하기 위한 메소드입니다.
마무리
프로젝트에 참여하면서 구글
또는 ChatGPT
에 이런 상황에는 어떤 기술이 좋을까를 검색 후 바로 적용하는 경우가 많았습니다.
필연적으로 타인이 결론지은 선택이기 때문에 직접 코드를 짜면서도 기술에 대한 신뢰도 없고 기술의 작동 원리도 깊게 파악하지 못했었습니다.
하지만 이번 블로그 작성을 기회로 프로젝트에서 많이 사용되고 성능이 중요한 필수 기능에 사용할 기술을 직접 테스트해보고 눈으로 본 후 선택했습니다.
이러한 시도를 통해 기술의 동작 원리, 강점 및 약점을 이해하고 기술에 대한 통찰력을 높일 수 있었습니다.
또 언젠가 문제가 발생했을 때 직접 비교하고 선택한 기술 스택이기 때문에 유지보수 및 문제 해결도 자신감이 생겼습니다.
제 경험이 조금이라도 도움이 되었으면 하며 이만 줄이겠습니다. 감사합니다 😊
Eric