이번 글에서는 스프링 배치에서 사용하는 메타 테이블을 생성하지 않고, JPA를 함께 사용하는 방법에 대해 알아보겠습니다.
이 글에서 사용된 예제는 Github에서 확인할 수 있습니다.
@EnableBatchProcessing
@EnableBatchProcessing
어노테이션을 설정하면 다음 빈이 생성됩니다.
JobRepository
: bean namejobRepository
JobLauncher
: bean namejobLauncher
JobRegistry
: bean namejobRegistry
PlatformTransactionManager
: bean nametransactionManager
JobBuilderFactory
: bean namejobBuilders
StepBuilderFactory
: bean namestepBuilders
코드 분석
빈이 생성되고 등록되는 과정은 @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()};
// 생략 ...
}
}
BatchConfigurationSelector
는 SimpleBatchConfiguration
을 반환합니다.
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;
}
// 생략 ...
}
SimpleBatchConfiguration
은 BatchConfigurer
타입의 빈을 조회해서 사용합니다.
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와 함께 사용하기
문제점
위에서 살펴본 SimpleBatchConfiguration
은 AbstractBatchConfiguration
을 상속 받습니다. 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);
}
}
StepBuilderFactory
의 transactionManager
메소드로 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
를 적용해도 매번 stepBuilderFactory
에 transactionManager
를 설정해주는 코드가 중복으로 생성됩니다. 이를 해결하기 위해서
다음과 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);
}
}
이제 StepBuilderFactory
의 transactionManager
메소드를 호출하지 않아도 JPA 관련 쿼리가 정상적으로 수행됩니다.
결론
스프링 배치에서 사용하는 메타 데이터를 저장하기위한 BATCH_...
테이블들을 사용하지 않고, JPA를 사용하는 방법에 대해서 알아보았습니다.
대규모 프로젝트에서는 적용하기 어렵겠지만, 간단한 배치나 토이 프로젝트에 적용하면 편리하게 사용할 수 있습니다.