Spring Batch 메타 데이터 테이블 사용하지 않기

5 min read

이번 글에서는 스프링 배치에서 사용하는 메타 테이블을 생성하지 않고, JPA를 함께 사용하는 방법에 대해 알아보겠습니다.

이 글에서 사용된 예제는 Github에서 확인할 수 있습니다.

@EnableBatchProcessing

@EnableBatchProcessing 어노테이션을 설정하면 다음 빈이 생성됩니다.

  • JobRepository: bean name jobRepository
  • JobLauncher: bean name jobLauncher
  • JobRegistry: bean name jobRegistry
  • PlatformTransactionManager: bean name transactionManager
  • JobBuilderFactory: bean name jobBuilders
  • StepBuilderFactory: bean name stepBuilders

코드 분석

빈이 생성되고 등록되는 과정은 @EnableBatchProcessing 코드를 살펴보면 알 수 있습니다.


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(BatchConfigurationSelector.class)
public @interface EnableBatchProcessing {

  boolean modular() default false;

}

@EnableBatchProcessing 어노테이션은 BatchConfigurationSelector를 Import 합니다.

public class BatchConfigurationSelector implements ImportSelector {

  @Override
  public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    // 생략 ...
    imports = new String[]{SimpleBatchConfiguration.class.getName()};
    // 생략 ...
  }

}

BatchConfigurationSelectorSimpleBatchConfiguration 을 반환합니다.

public class SimpleBatchConfiguration extends AbstractBatchConfiguration {

  // 생략 ...

  @Override
  @Bean
  public JobRepository jobRepository() throws Exception {
    return createLazyProxy(jobRepository, JobRepository.class);
  }

  @Override
  @Bean
  public JobLauncher jobLauncher() throws Exception {
    return createLazyProxy(jobLauncher, JobLauncher.class);
  }

  @Override
  @Bean
  public JobRegistry jobRegistry() throws Exception {
    return createLazyProxy(jobRegistry, JobRegistry.class);
  }

  @Override
  @Bean
  public JobExplorer jobExplorer() {
    return createLazyProxy(jobExplorer, JobExplorer.class);
  }

  @Override
  @Bean
  public PlatformTransactionManager transactionManager() throws Exception {
    return createLazyProxy(transactionManager, PlatformTransactionManager.class);
  }

  // 생략 ...

  protected void initialize() throws Exception {
    if (initialized) {
      return;
    }
    // BatchConfigurer 빈 조회
    BatchConfigurer configurer = getConfigurer(context.getBeansOfType(BatchConfigurer.class).values());
    jobRepository.set(configurer.getJobRepository());
    jobLauncher.set(configurer.getJobLauncher());
    transactionManager.set(configurer.getTransactionManager());
    jobRegistry.set(new MapJobRegistry());
    jobExplorer.set(configurer.getJobExplorer());
    initialized = true;
  }

  // 생략 ...
}

SimpleBatchConfigurationBatchConfigurer 타입의 빈을 조회해서 사용합니다.

jobRepository, jobLauncher, transactionManager , jobRegistry, jobExplorer 인스턴스가 생성되고 @Bean 어노테이션에 의해 빈으로 등록됩니다.

public class DefaultBatchConfigurer implements BatchConfigurer {
  // 생략 ...
  @PostConstruct
  public void initialize() {
    try {
      // dataSource 값이 null 일 경우
      if (dataSource == null) {
        logger.warn("No datasource was provided...using a Map based JobRepository");

        if (getTransactionManager() == null) {
          logger.warn("No transaction manager was provided, using a ResourcelessTransactionManager");
          // TransactionManager 생성
          this.transactionManager = new ResourcelessTransactionManager();
        }

        MapJobRepositoryFactoryBean jobRepositoryFactory = new MapJobRepositoryFactoryBean(getTransactionManager());
        jobRepositoryFactory.afterPropertiesSet();
        // JobRepository 생성
        this.jobRepository = jobRepositoryFactory.getObject();

        MapJobExplorerFactoryBean jobExplorerFactory = new MapJobExplorerFactoryBean(jobRepositoryFactory);
        jobExplorerFactory.afterPropertiesSet();
        // JobExplorer 생성
        this.jobExplorer = jobExplorerFactory.getObject();
      } else {
        this.jobRepository = createJobRepository();
        this.jobExplorer = createJobExplorer();
      }

      // JobLauncher 생성
      this.jobLauncher = createJobLauncher();
    } catch (Exception e) {
      throw new BatchConfigurationException(e);
    }
  }
}

생성되는 인스턴스는 DefaultBatchConfigurer에 정의되어 있습니다.

Customize

코드 분석에서 알아본 내용을 토대로 실제 데이터베이스에 배치용 테이블을 생성하지 않고, 메모리 기반으로 작동하려면 다음과 같이 설정하면 됩니다.

  • DefaultBatchConfigurer를 상속 받은 CustomBatchConfigurer 클래스를 정의합니다.
  • @EnableBatchProcessing 어노테이션을 지정합니다.
  • setDatasource 메소드를 오버라이드해서, 아무런 동작도 하지 않게 정의합니다.

@Configuration
@EnableBatchProcessing
public class CustomBatchConfigurer extends DefaultBatchConfigurer {

  @Override
  public void setDataSource(DataSource dataSource) {
    // Do nothing
  }

}

JPA와 함께 사용하기

문제점

위에서 살펴본 SimpleBatchConfigurationAbstractBatchConfiguration을 상속 받습니다. AbstractionBatchConfiguration에는 다음과 같은 코드가 있습니다.

public abstract class AbstractBatchConfiguration implements ImportAware, InitializingBean {

  @Bean
  public JobBuilderFactory jobBuilders() throws Exception {
    return this.jobBuilderFactory;
  }

  @Bean
  public StepBuilderFactory stepBuilders() throws Exception {
    return this.stepBuilderFactory;
  }

  @Bean
  public abstract PlatformTransactionManager transactionManager() throws Exception;

  // 생략 ...

  @Override
  public void afterPropertiesSet() throws Exception {
    this.jobBuilderFactory = new JobBuilderFactory(jobRepository());
    // StepBuilderFactory 생성
    this.stepBuilderFactory = new StepBuilderFactory(jobRepository(), transactionManager());
  }

  // 생략 ...

}

위 설정에 의해서 JobBuilderFactory, StepBuilderFactory 빈이 생성됩니다. 이 때 transactionManager() 추상 메소드에 의해 생성된 PlatformTransactionManager를 생성자로 전달해서 사용하게 됩니다.

이로 인해서 Step 내부에서는 ResourcelessTransactionManager의 인스턴스가 사용되고, JPA Entity를 저장하는 동작을 실행해도, 쿼리가 수행되지 않습니다.

해결 방법 1

JpaTransactionManager를 빈으로 등록합니다.


@Configuration
public class JpaConfig {

  @Bean
  public PlatformTransactionManager jpaTransactionManager(EntityManagerFactory emf) {
    return new JpaTransactionManager(emf);
  }

}

StepBuilderFactorytransactionManager 메소드로 jpaTransactionManager를 지정합니다.


@Component
@RequiredArgsConstructor
public class ProductJobConfig {

  private final JobBuilderFactory jobFactory;
  private final StepBuilderFactory stepFactory;
  private final PlatformTransactionManager jpaTransactionManager;   // jpaTransactionManager 주입

  @Bean
  public Job productJob(Step step) {
    return jobFactory.get("product-job")
        .start(step)
        .build();
  }

  @Bean
  public Step productStep(ItemReader<Product> reader,
                          ItemProcessor<Product, Product> processor,
                          ItemWriter<Product> writer) {
    return stepFactory.get("product-step")
        .<Product, Product>chunk(10)
        .reader(reader)
        .processor(processor)
        .writer(writer)
        .transactionManager(jpaTransactionManager)  // jpaTransactionManager 적용
        .build();
  }

  // 생략 ...

}

해결 방법 2

해결 방법 1을 적용한 상태에서 @Transactional 어노테이션을 사용할 경우 PlatformTransactionManager 주입 과정에서 다음과 같은 오류가 발생합니다.

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 1 of method productStep in com.raegon.example.batch.job.ProductJobConfig required a single bean, but 2 were found:
- transactionManager: defined by method 'transactionManager' in class path resource [org/springframework/batch/core/configuration/annotation/SimpleBatchConfiguration.class]
- jpaTransactionManager: defined by method 'jpaTransactionManager' in class path resource [com/raegon/example/batch/config/CustomBatchConfigurer.class]

이를 해결하기 위해서 JpaTransactionManager@Primary 빈으로 등록합니다.


@Configuration
@EnableBatchProcessing
public class CustomBatchConfigurer extends DefaultBatchConfigurer {

  @Override
  public void setDataSource(DataSource dataSource) {
    // Do nothing
  }

  @Bean
  @Primary  // SimpleBatchConfiguration 에서 생성된 빈 대체
  public PlatformTransactionManager jpaTransactionManager(EntityManagerFactory emf) {
    return new JpaTransactionManager(emf);
  }

}

해결 방법 3

해결 방법 2를 적용해도 매번 stepBuilderFactorytransactionManager를 설정해주는 코드가 중복으로 생성됩니다. 이를 해결하기 위해서 다음과 StepBuilderFactory를 Primary 빈으로 등록합니다.


@Configuration
@EnableBatchProcessing
public class CustomBatchConfigurer extends DefaultBatchConfigurer {

  @Override
  public void setDataSource(DataSource dataSource) {
    // Do nothing
  }

  @Bean
  @Primary  // SimpleBatchConfiguration 에서 생성된 빈 대체
  public PlatformTransactionManager jpaTransactionManager(EntityManagerFactory emf) {
    return new JpaTransactionManager(emf);
  }

  @Bean
  @Primary  // AbstractBatchConfiguration 에서 생성된 빈 대체
  public StepBuilderFactory stepBuilderFactory(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
    return new StepBuilderFactory(jobRepository, transactionManager);
  }

}

이제 StepBuilderFactorytransactionManager 메소드를 호출하지 않아도 JPA 관련 쿼리가 정상적으로 수행됩니다.

결론

스프링 배치에서 사용하는 메타 데이터를 저장하기위한 BATCH_... 테이블들을 사용하지 않고, JPA를 사용하는 방법에 대해서 알아보았습니다. 대규모 프로젝트에서는 적용하기 어렵겠지만, 간단한 배치나 토이 프로젝트에 적용하면 편리하게 사용할 수 있습니다.

참고

© 2023 Raegon Kim