글 작성자: beaniejoy

개인프로젝트로 Spring Boot 3버전의 Spring Batch를 사용해보다가 최근에 실무에서 같은 3버전대의 스프링 신규 프로젝트에도 배치모듈을 적용할 일이 있어 신규 모듈을 만들게 되었습니다.

개인프로젝트와 실무에서 Spring Boot 3 버전에서의 Spring Batch를 적용한 것들을 블로그에 정리하려고 합니다. 이번 게시글에서는 Spring Boot3 & Spring Batch 5버전에서 multi datasource를 어떻게 설정하는지에 대해 작성해보고자 합니다.

 

1. Spring Boot 3 & Spring Batch 5 적용시 알아야할 내용

spring boot 3.1.11-SNAPSHOT 버전에서의 spring batch 버전

Spring Boot 3버전대에서는 Spring Batch가 5버전을 기본으로 사용하게 되었습니다. 이에 따라 기존에 사용하던 Spring Batch 방식에서 꽤나 많은 부분이 변경이 되었는데요. 그 중에 중요한 몇 개만 언급하려고 합니다.

 

1-1. @EnableBatchProcessing은 이제 사용하지 않아요~

Sprinb Boot 2.x 버전에서 spring batch를 사용했다면 @EnableBatchProcessing 어노테이션은 필수로 붙여서 사용했습니다. 하지만 Spring Batch 5버전으로 올라오면서 해당 어노테이션 없이도 자동으로 batch 관련 설정이 되도록 바뀌었습니다.
https://docs.spring.io/spring-batch/reference/whatsnew.html

 

What’s New in Spring Batch 5.1 :: Spring Batch

Embracing JDK 21 LTS is one of the main themes for Spring Batch 5.1, especially the support of virtual threads from Project Loom. In this release, virtual threads can be used in all areas of the framework, like running a concurrent step with virtual thread

docs.spring.io

그러면 custom한 설정을 통해 multi datasource 환경에서 어떤 datasource와 transactionManager를 batch 기본으로 설정해줄지에 대해서 고민이 될 수 있는데요. 그 때 @EnableBatchProcessing 어노테이션의 관련 옵션들을 사용하라고 가이드해줍니다.
(Spring Boot 3 버전대에서 이 설정도 사용하기 힘들어졌는데요 조금 있다가 언급하도록 하겠습니다.)

@Configuration
@EnableBatchProcessing(
    dataSourceRef = "batchDataSource", 
    transactionManagerRef = "batchTransactionManager"
)
class BatchConfig {
	//...
}

위와 같이 해당 어노테이션에서 제공해주는 properties 중에 dataSourceRef, transactionManagerRef를 사용하라고 가이드해주고 있습니다. 

그런데 Spring Boot에서 batch 관련 자동으로 설정해주는 BatchAutoConfiguration을 보면 @EnableBatchProcessing 어노테이션 관련 설정이 있는데요. 이부분을 잘 봐야 합니다.

// BatchAutoConfiguration.java
@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class)

Spring Boot autoconfigure에 있는 Batch 관련 자동 설정 클래스내용입니다. ConditionalMissingBean 어노테이션이 추가됐는데요. @EnableBatchProcessing 혹은 DefaultBatchConfiguration을 사용해서 스프링 배치를 설정하게 되면 Spring Boot에서 batch 관련 자동설정을 해주지 않게 됩니다.

즉 @EnableBatchProcessing 어노테이션을 설정하면 스프링 부트는 BatchAutoConfiguration에 있는 Bean들을 자동으로 등록해주지 않습니다. 스프링 배치 실행을 위한 관련 Bean들(특히 Batch ApplicationRunner)이 등록되지 못했기 때문에 스프링 부트를 통해 배치를 실행해도 해당 배치는 실행되지 않게 됩니다.

@EnableBatchProcessing을 BatchConfig에 설정하고 배치 실행하면 위와 같이 스프링 배치는 실행되지 않습니다.

이와 관련해서 정보를 찾아보니 JobLauncherApplicationRunner을 따로 Bean으로 설정하면 된다는 글을 보긴 했는데요.
multi datasource로 구성된 환경에서 JobLauncherApplicationRunner 따로 등록하는 방식으로 해봐도 제대로 동작하지 않았습니다.

아마 제가 잘 못 알고 있거나 잘 못 설정했을 가능성이 크긴 한데요. 무엇보다 BatchAutoConfiguration에 있는 Bean들을 따로 등록하고 하는 것들이 복잡하기도 하고 스프링 배치가 의도한 대로 동작하지 않을 있겠다는 생각을 하게 되어 저는 Spring Batch 5 가이드에서 언급한 대로 @EnableBatchProcessing 어노테이션을 사용하지 않았습니다.

 

 

1-2. spring batch meta table은 사용하는 것으로,,,

spring batch를 사용하면 당연히 meta table을 사용하는 거지 무조건 사용해야된다? 무슨 얘기인가 싶은 생각을 하셨을 수도 있습니다. 그런데 실무에서 spring batch 사용할 때 meta table을 아예 사용하지 않는 프로젝트들이 많이 있습니다.

관리의 이슈와 spring batch meta table에서 데이터가 꼬이는 일로 배치 오류가 발생하지 않도록 본연의 배치 잡 실행에 집중하고자 meta table을 아예 사용하지 않은 것으로 생각했는데요.

Spring Batch 5버전 이전에서는 meta table이 생성되지도, 사용하지도 않도록 하는 설정이 가능했습니다.

@Configuration
@EnableBatchProcessing
class BatchConfig : DefaultBatchConfigurer() {
	
    override fun getTransactionManager(): PlatformTransactionManager {
        return ResourcelessTransactionManager()
    }

    override fun setDataSource(dataSource: DataSource) {
    }
    
    @Bean
    fun mapJobRepositoryFactory(
    	txManager: ResourcelessTransactionManager?
    ): MapJobRepositoryFactoryBean {
        val factory = MapJobRepositoryFactoryBean(txManager)
        factory.afterPropertiesSet()
        return factory
    }
    
    //...
}

DefaultBatchConfigurer를 상속받아 transactionManager를 ResourcelessTransactionManager로 설정하고 DataSource setter에는 아무것도 설정을 하지 않습니다. 그리고 MapJobRepositoryFactoryBean을 제공했는데요. 

MapJobRepositoryFactoryBean

A FactoryBean that automates the creation of a SimpleJobRepository using non-persistent in-memory DAO implementations. 
This repository is only really intended for use in testing and rapid prototyping.
...

영속화되지 않는 인메모리 구현체라고 소개하고 있습니다. 이걸 통해 spring batch에 DataSource를 설정하지 않을 수 있었고 MapJobRepositoryFactoryBean를 통해 생성된 JobRepository는 batch job이 실행되어도 따로 spring batch metatable에 관련 정보들을 영속화하지 않게 됩니다.

하지만 Spring Batch 5버전부터는 이러한 것들이 deprecated 되면서 사실상 spring batch metatable은 필수 사항이 되었습니다.

Deprecated as of v4.3 in favor or using the JobRepositoryFactoryBean with an in-memory database.
Scheduled for removal in v5.0.

https://stackoverflow.com/questions/76245029/how-to-disable-spring-batch-meta-data-in-spring-5-0-1

 

How to Disable Spring Batch Meta Data in spring 5.0.1

I googled a lot and trying to follow this doc https://docs.spring.io/spring-batch/docs/5.0.1/reference/html/whatsnew.html#whatsNew But not able to find a way to disable spring batch meta data. Even I

stackoverflow.com

stackoverflow에도 이와 관련한 내용이 있었는데요. in-memory Map-based는 deprecated 되었고 JDBC 구현체를 사용해야 한다고 나와있는데요. 결국 spring batch meta table을 사용할 수 밖에 없다는 내용 같습니다.

그럼에도 meta table을 사용하고 싶지 않다면 h2 database 같은 in memory db datasource를 따로 설정해서 spring batch 기본 datasource로 설정해서 사용하라는 글도 봤습니다. 방법이 될 수 있으나 제 개인적으로는 이렇게 사용하고 싶지 않다는 생각을 했습니다. (배치 실행때마다 in-memory h2 database를 띄워야 하고 관련 테이블 DDL 수행 후 배치를 실행해야 하는게 좀 걸렸습니다.)

저는 그래서 meta table를 담는 batch용 database와 서비스 용도의 database를 따로 구성해서 2개의 datasource를 가지고 설정하게 되었습니다.

 

2. 프로젝트 설정

배치 테스트용 프로젝트는 Spring Boot 3.2.2 버전, kotlin 1.9.22 버전, java 17 베이스로 구성했습니다.
(build.gradle.kts 설정 내용은 repository에서 확인하시면 될 것 같습니다.)

 

2-1. datasource 관련 설정

2개의 DB를 사용할 것이기에 datasource 설정은 2개로 구분지어야 합니다.

# application-local.yml
spring:
  datasource:
    service:
      pool-name: service-db
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/cafe?characterEncoding=utf8&serverTimezone=Asia/Seoul
      username: root
      password: beaniejoy
    batch:
      pool-name: batch-db
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3307/cafe?characterEncoding=utf8&serverTimezone=Asia/Seoul
      username: root
      password: beaniejoy

service용 db와 batch용 db에 대한 connection 정보를 설정하고 Config 파일에 Bean으로 등록해줍니다.

@Configuration(proxyBeanMethods = false)
class DataSourceConfig {
    @Primary
    @Bean(BATCH_DATASOURCE)
    @ConfigurationProperties(prefix = "spring.datasource.batch")
    fun batchDataSource(): DataSource {
        return DataSourceBuilder
            .create()
            .type(HikariDataSource::class.java)
            .build()
    }

    @Bean(SERVICE_DATASOURCE)
    @ConfigurationProperties(prefix = "spring.datasource.service")
    fun serviceDataSource(): DataSource {
        return DataSourceBuilder
            .create()
            .type(HikariDataSource::class.java)
            .build()
    }
}

그리고 중요한 것이 batch용 datasource bean에 @Primary를 붙여야 합니다. 안그러면 DataSource 타입으로 두 개의 빈이 등록되기 때문에 중복등록으로 실행과정에서 오류가 발생할 것입니다.

 

2-2. TransactionManager 설정

서비스용 datasource는 JpaTransactionManager에 설정해야하고 batch용 datasource는 JdbcTransactionManager에 설정했습니다.

// BatchConfig
@Primary
@Bean(BATCH_TRANSACTION_MANAGER)
fun batchTransactionManager(
    @Qualifier(BATCH_DATASOURCE) batchDataSource: DataSource
): PlatformTransactionManager {
    return JdbcTransactionManager(batchDataSource)
}

// JpaConfig
// LocalContainerEntityManagerFactoryBean 따로 Bean 등록 필수
@Bean(SERVICE_TRANSACTION_MANAGER)
fun serviceTransactionManager(
    @Qualifier(SERVICE_ENTITY_MANAGER) serviceEntityManager: LocalContainerEntityManagerFactoryBean
): PlatformTransactionManager {
    return JpaTransactionManager().apply {
        this.entityManagerFactory = serviceEntityManager.`object`
    }
}

TransactionManager도 마찬가지로 batch에서 사용하는 Bean에 @Primary를 붙여햐 합니다.

 

2-3. Batch Job 등록 및 실행

datasource, transactionManager 관련 설정이 다 됐고 스프링 배치를 한 번 실행해보겠습니다.

# application.yml
spring:
  profiles:
    active: local

  batch:
    job:
      enabled: true
      name: ${job.name:NONE}
    jdbc:
      initialize-schema: always

yml 파일에 spring batch 관련 기본 설정을 먼저 해줍니다.
spring.batch.jdbc.initialize-schema는 spring batch meta table들을 실행할 때 자동으로 등록되도록 할 것인지에 대해 설정해주는 옵션입니다. 운영 환경에서는 되도록 never로 하는 것이 좋을 것 같습니다. (테스트를 위해 always로 하겠습니다.)

@Configuration
class TestJobConfig(
    private val jobRepository: JobRepository,
    private val transactionManager: PlatformTransactionManager
) {
    companion object : KLogging()

    @Bean
    fun testJob(
        simpleStep1: Step,
        simpleStep2: Step
    ): Job {
        return JobBuilder("testJob", jobRepository)
            .incrementer(RunIdIncrementer())
            .start(simpleStep1)
            .next(simpleStep2)
            .build()
    }

    @Bean
    fun simpleStep1(): Step {
        val testTasklet = Tasklet { _, _ ->
            logger.info { ">>>>>> this is step1" }
            RepeatStatus.FINISHED
        }

        return StepBuilder("simpleStep1", jobRepository)
            .tasklet(testTasklet, transactionManager)
            .build()
    }

    @Bean
    fun simpleStep2(): Step {
        val testTasklet = Tasklet { _, _ ->
            logger.info { ">>>>>> this is step2" }
            RepeatStatus.FINISHED
        }

        return StepBuilder("simpleStep2", jobRepository)
            .tasklet(testTasklet, transactionManager)
            .build()
    }
}

 

위와 같이 테스트용 배치 잡하나를 등록하고 program arguments에 --job.name=testJob 와 함께 애플리케이션 실행하면 됩니다.

(저는 테스트한다고 배치 실행한 이력이 있어서 run.id가 6으로 나오고 있습니다.)

의도한 대로 batch용 DB에 spring batch meta table들이 생성되어 있고, 서비스용 DB에는 JPA Entity 내용이 그대로 테이블로 등록된 것을 볼 수 있습니다.

(위의 코드에 대한 Github Repository 링크 드립니다. 참고하세요.)

 

back-overall-repository/spring-boot-v3-batch-demo at main · beaniejoy/back-overall-repository

🧪 Back-end Study & Test Repository, which manages and tests various frameworks, libraries and modules, that consists of directories named by each topic. - beaniejoy/back-overall-repository

github.com