[JAVA] Spring Batch와 JPA 연동하기 — 대량 데이터 처리에서의 효율적인 영속성 관리

Spring Batch는 대용량 데이터를 안정적으로 처리할 수 있는 강력한 프레임워크입니다. 하지만 많은 개발자들이 실무에서 가장 많이 겪는 문제 중 하나가 바로 JPA와의 연동입니다. 단순히 Entity를 저장하면 될 것 같지만, 대량 데이터를 다루는 배치 환경에서는 영속성 컨텍스트 관리, 메모리 누수, 트랜잭션 범위 등의 세밀한 고려가 필요합니다.

 

 

Spring Batch와 JPA 연동의 필요성

Spring Batch는 기본적으로 JDBC 기반의 ItemReaderItemWriter를 제공합니다. 하지만, 프로젝트에서 이미 Spring Data JPA를 사용 중이라면, 배치 처리에서도 동일한 엔티티 매핑 로직과 JPA 리포지토리를 그대로 재사용하는 것이 효율적입니다.

예를 들어 “회원 포인트 정산” 작업을 배치로 수행한다고 가정하면, JPA를 통해 엔티티를 조회하고, 가공 후 다시 DB에 반영하는 흐름을 유지할 수 있습니다.

 

JPA 기반 Spring Batch 구조

JPA와 함께 사용하는 Spring Batch의 구성은 다음과 같습니다:

  • JpaPagingItemReader — 페이징 기반으로 대량 데이터를 JPA로 조회
  • ItemProcessor — 조회된 엔티티 가공
  • JpaItemWriter — 가공된 엔티티를 JPA 영속성 컨텍스트를 통해 저장

이 구조를 통해 Spring Data JPA의 이점을 살리면서도, 배치 프레임워크의 안정성과 트랜잭션 제어 기능을 그대로 활용할 수 있습니다.

 

의존성 및 설정

1. 의존성 추가 (Gradle)

implementation 'org.springframework.boot:spring-boot-starter-batch'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'

2. JPA 설정

Spring Batch는 기본적으로 JobRepository를 위해 별도의 데이터소스를 사용합니다. 하지만 단일 DB를 사용한다면 JPA의 데이터소스를 그대로 공유할 수 있습니다.

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/batchdb
    username: batch_user
    password: pass1234
  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        jdbc:
          batch_size: 1000
        order_inserts: true
        order_updates: true

hibernate.jdbc.batch_size는 JPA의 batch insert/update 성능을 향상시키는 핵심 옵션입니다.

 

JpaPagingItemReader 예시

JpaPagingItemReader는 페이지 단위로 데이터를 조회하므로 대량 데이터 처리에 적합합니다.

@Configuration
@RequiredArgsConstructor
public class MemberPointBatchConfig {

    private final EntityManagerFactory emf;
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job memberPointJob() {
        return jobBuilderFactory.get("memberPointJob")
                .start(memberPointStep())
                .build();
    }

    @Bean
    public Step memberPointStep() {
        return stepBuilderFactory.get("memberPointStep")
                .<Member, Member>chunk(1000)
                .reader(memberReader())
                .processor(memberProcessor())
                .writer(memberWriter())
                .build();
    }

    @Bean
    public JpaPagingItemReader<Member> memberReader() {
        return new JpaPagingItemReaderBuilder<Member>()
                .name("memberReader")
                .entityManagerFactory(emf)
                .queryString("SELECT m FROM Member m WHERE m.active = true")
                .pageSize(1000)
                .build();
    }

    @Bean
    public ItemProcessor<Member, Member> memberProcessor() {
        return member -> {
            member.addPoints(10);
            return member;
        };
    }

    @Bean
    public JpaItemWriter<Member> memberWriter() {
        JpaItemWriter<Member> writer = new JpaItemWriter<>();
        writer.setEntityManagerFactory(emf);
        return writer;
    }
}

이 구성에서는 1,000건씩 읽고 처리 후 커밋하는 방식으로 안정적인 트랜잭션 처리가 이루어집니다.

 

성능 및 안정성 고려사항

  • 1️⃣ 영속성 컨텍스트 초기화: JpaItemWriter는 chunk마다 flush/clear를 수행하므로 메모리 누수를 방지함
  • 2️⃣ Batch Size 설정: hibernate.jdbc.batch_size를 DB 성능에 맞게 조정 (1000~5000 추천)
  • 3️⃣ Lazy Loading 주의: Reader에서는 Fetch Join을 활용하거나 필요한 데이터만 로드
  • 4️⃣ Transaction 범위: Step 단위 트랜잭션이므로, DB 락을 유발할 수 있는 장기 작업은 피해야 함

 

실무에서 자주 하는 실수

  • ItemWriter에서 save() 호출: JpaItemWriter를 사용하지 않고 Repository.save()를 직접 호출하면 flush 타이밍이 어긋나고 성능 저하
  • Reader에서 모든 데이터 로드: Paging 미사용 시 OutOfMemoryError 발생 가능
  • 엔티티 변경 감지 미사용: 영속성 컨텍스트를 벗어난 Detached Entity 수정은 반영되지 않음

 

Batch와 JPA 병행 시 전략

  • 대량 데이터 삽입: JDBC 기반 JdbcBatchItemWriter 사용 고려
  • 비즈니스 로직이 복잡한 경우: JPA 사용 (변경감지, 연관관계 활용)
  • 단순 ETL 용도: JPA보다는 MyBatis나 FlatFileReader/Writer로 전환

즉, JPA는 “복잡한 비즈니스 로직 처리용”, JDBC는 “순수 데이터 대량처리용”으로 구분해 사용하는 것이 좋습니다.

 


 

Spring Batch와 JPA는 함께 사용할 때 개발 생산성과 유지보수성을 크게 높일 수 있습니다. 다만, 대량 데이터 처리에서는 JPA의 영속성 관리와 트랜잭션 동작 원리를 명확히 이해해야 합니다. Chunk 단위 커밋, 영속성 초기화, Batch Size 조정을 잘 설계하면 안정적이면서도 빠른 배치 시스템을 구현할 수 있습니다.