
Spring Batch에서 데이터를 데이터베이스에 저장하는 방법은 크게 JDBC 기반 Writer와 JPA 기반 Writer로 나눌 수 있다. JDBC Writer는 SQL을 직접 실행하여 데이터를 저장한다. 동작 방식이 단순하고 데이터베이스와 직접 통신하기 때문에 대량 데이터를 처리하는 환경에서도 비교적 예측 가능한 성능을 보인다. 반면 JPA 기반 Writer는 엔티티를 통해 데이터를 저장한다. SQL을 직접 실행하는 대신 영속성 컨텍스트(Persistence Context) 를 통해 엔티티 상태를 관리하고 변경 사항을 데이터베이스에 반영한다.
이 방식은 도메인 모델을 그대로 사용할 수 있다는 장점이 있다. 하지만 배치 환경에서는 몇 가지 추가로 고려해야 할 점이 존재한다. 특히 대량 데이터를 처리하는 경우 영속성 컨텍스트에 많은 엔티티가 쌓이면서 메모리 사용량이 증가할 수 있다. 또한 SQL이 즉시 실행되는 것이 아니라 flush 시점에 실행된다는 점도 성능에 영향을 줄 수 있다.Spring Batch는 이러한 환경에서 사용할 수 있는 JPA 기반 Writer로 JpaItemWriter를 제공한다.
JpaItemWriter는 JPA의 EntityManager를 사용하여 엔티티를 저장하는 Writer이다. Reader와 Processor를 통해 전달된 엔티티를 영속성 컨텍스트에 등록하고 트랜잭션이 커밋되는 시점에 데이터베이스에 반영한다.
Writer의 주요 구성 요소는 다음과 같다.
| 구성 요소 | 설명 |
|---|---|
| EntityManagerFactory | Writer 실행 시 EntityManager를 생성한다 |
| EntityManager | 엔티티 저장과 영속성 컨텍스트 관리를 담당한다 |
| merge | 엔티티 상태를 영속성 컨텍스트에 반영한다 |
| flush | 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다 |
| clear | 영속성 컨텍스트를 초기화한다 |
JpaItemWriter는 전달된 엔티티를 순회하면서 EntityManager.merge()를 호출한다. merge는 엔티티 상태를 영속성 컨텍스트에 반영하고, 트랜잭션 커밋 시점에 실제 SQL이 실행된다. 이 과정에서 엔티티는 영속성 컨텍스트에 계속 누적될 수 있다. 따라서 대량 데이터를 처리하는 배치 환경에서는 영속성 컨텍스트 관리가 중요하다.
Spring Batch는 기본적으로 Chunk 단위로 트랜잭션을 처리한다.
예를 들어 Step이 다음과 같이 구성되어 있다고 가정해 보자.
stepBuilderFactory.get("orderStep")
.<Order, Order>chunk(1000)
.reader(reader)
.processor(processor)
.writer(writer)
.build();
이 경우 처리 흐름은 다음과 같이 진행된다.
read → process → write → commit
Reader가 1000개의 데이터를 읽고 Processor가 이를 처리한 뒤 Writer가 저장 작업을 수행한다. 이후 트랜잭션이 커밋된다. 즉 1000개의 데이터가 하나의 트랜잭션 단위가 된다. JpaItemWriter는 write 단계에서 엔티티를 EntityManager에 등록한다. 하지만 SQL은 즉시 실행되지 않는다. 실제 SQL은 flush 시점이나 트랜잭션 커밋 시점에 실행된다.
다음은 JpaItemWriter 설정 예제이다.
@Bean
public JpaItemWriter<Order> orderWriter(EntityManagerFactory emf) {
JpaItemWriter<Order> writer = new JpaItemWriter<>();
writer.setEntityManagerFactory(emf);
return writer;
}
Processor에서 반환된 Order 엔티티는 Writer로 전달되고 JpaItemWriter는 이를 EntityManager에 등록한다.
트랜잭션이 커밋되는 시점에는 다음과 같은 SQL이 실행된다.
INSERT INTO orders (id, status, user_id)
VALUES (?, ?, ?)
JPA는 엔티티 변경 내용을 즉시 데이터베이스에 반영하지 않는다. 변경된 엔티티는 먼저 영속성 컨텍스트에 저장되고 flush 시점에 SQL이 실행된다. 배치 환경에서 대량 데이터를 처리하면 영속성 컨텍스트에 많은 엔티티가 누적될 수 있다. 이 상태가 유지되면 JVM 메모리 사용량이 증가하고 GC 부담이 커질 수 있다.
flush()는 영속성 컨텍스트에 저장된 변경 내용을 데이터베이스에 반영한다. 이 과정에서 INSERT, UPDATE, DELETE SQL이 실행된다.
clear()는 영속성 컨텍스트를 초기화한다. 관리 중이던 엔티티가 detach 상태로 변경되며 이후에는 영속성 컨텍스트의 관리 대상에서 제외된다.
일반적인 동작 흐름은 다음과 같다.
엔티티 저장 → flush → SQL 실행 → clear → 영속성 컨텍스트 초기화
배치 작업에서는 일정 단위로 flush와 clear를 수행하여 영속성 컨텍스트에 엔티티가 계속 누적되는 상황을 방지할 수 있다.
JPA 기반 배치 처리에서는 Hibernate의 Batch 옵션을 통해 성능을 개선할 수 있다.
| 옵션 | 설명 |
|---|---|
| hibernate.jdbc.batch_size | JDBC batch 실행 시 한 번에 묶어서 실행할 SQL 개수를 지정한다 |
| hibernate.order_inserts | INSERT SQL을 정렬하여 같은 테이블의 INSERT가 연속적으로 실행되도록 한다 |
| hibernate.order_updates | UPDATE SQL을 정렬하여 같은 테이블의 UPDATE가 묶여 실행되도록 한다 |
hibernate.jdbc.batch_size는 Hibernate가 여러 개의 SQL을 모아서 실행하도록 만드는 설정이다. 예를 들어 batch size를 100으로 설정하면 Hibernate는 100개의 INSERT나 UPDATE SQL을 모아서 JDBC batch로 실행한다.
hibernate.order_inserts와 hibernate.order_updates는 SQL 실행 순서를 정렬하는 옵션이다. 동일한 테이블의 SQL이 연속적으로 실행되도록 정렬하여 JDBC batch 효율을 높이는 역할을 한다.