[Spring Batch] JobParameter와 Scope

DaeHoon·2023년 12월 11일

JobParameter와 Scope

  • Spring Batch의 경우 외부 혹은 내부에서 파라미터를 받아 여러 Batch 컴포넌트에서 사용할 수 있게 지원하고 있는데, 이 파라미터를 JobParameter라고 한다.
@Value("#{jobParameters[파라미터명]}")
  • 위와 같이 SpEL로 선언해서 사용하면 된다.
  • jobParameters 외에도 jobExecutionContext, stepExecutionContext 등도 SpEL로 사용할 수 있다.
  • @JobScope에선 stepExecutionContext는 사용할 수 없고, jobParametersjobExecutionContext만 사용할 수 있다.
  • JobParameters는 @Value를 통해서 등록이 가능하다.
  • JobParameters는 Scope Bean을 생성할때만 사용 가능하다. 즉, @StepScope, @JobScope Bean을 생성할때만 JobParameters가 생성된다.

코드 예시

@Configuration
@EnableBatchProcessing
class MemberWithdrawalConfiguration(
  private val jobBuilderFactory: JobBuilderFactory,
  private val memberWithdrawalJobListener: MemberWithdrawalJobListener,
  private val skipListener: MemberWithdrawalSkipListener,
  private val stepBuilderFactory: StepBuilderFactory,
  private val transactionManager: JpaTransactionManager,
  private val memberWithdrawalJobParameter: MemberWithdrawalJobParameter,
  private val memberWithdrawalItemReader: ItemReader<DormantMemberDetails>,
  private val memberWithdrawalCompositeItemWriter: CompositeItemWriter<DormantMemberDetails>

): Log {
	@Bean
	fun memberWithdrawalJob(): Job {
		return jobBuilderFactory.get("memberWithdrawalJob")
			.start(memberWithdrawalStep(null))
			.listener(memberWithdrawalJobListener)
			.build()
	}
	@Bean
	@JobScope
	fun memberWithdrawalStep(@Value("#{jobParameters[CHUNK]}") chunk: Long?): Step{	
    	return stepBuilderFactory["memberWithdrawalStep"]
			.chunk<DormantMemberDetails, DormantMemberDetails>( memberWithdrawalJobParameter.chunk ?: 100)
			.reader(memberWithdrawalItemReader)
			.processor(memberWithdrawalProcessor())
			.writer(memberWithdrawalCompositeItemWriter)
			.faultTolerant()
			.listener(skipListener)
			.skip(Exception::class.java)
			.skipLimit(100)
			.transactionManager(transactionManager)
			.build()
	}

	@Bean
	fun memberWithdrawalProcessor() = MemberWithdrawalProcessor()

}
  • @JobScope는 Step 선언문에서 사용 가능하고, @StepScope는 Tasklet이나 ItemReader, ItemWriter, ItemProcessor에서 사용할 수 있음.
  • JobParameter 타입으로 사용할 수 있는 것은 Double, Long, Date, String이 있으면 LocalDate, LocalDateTime, Instant는 지원하지 않아 String으로 받은 다음에 다시 타입변환을 해서 사용해야 한다.
  • 예제 코드를 보시면 호출하는 쪽에서 null 를 할당하고 있는데, 이는 어플리케이션 실행시에 JobParameter의 할당을 하지 않기 때문에 가능하다.
  • @Bean@StepScope를 함께 쓰는 것은 @Scope (value = "step", proxyMode = TARGET_CLASS)로 표시하는 것과 같다.

@StepScope & @JobScope

  • Spring Bean의 기본 Scope는 singleton이나, pring Batch 컴포넌트 (Tasklet, ItemReader, ItemWriter, ItemProcessor 등)에 @StepScope를 사용하게 되면 Step 실행 시점에 해당 컴포넌트를 Bean으로 생성한다.
@Configuration
class MemberWithdrawalReaderConfiguration(
  private val memberWithdrawalJobParameter: MemberWithdrawalJobParameter,
  @Qualifier("memberSlaveDataSource")	private val memberSlaveDataSource: DataSource
): Log {

  @Bean
  @StepScope
  fun memberWithdrawalItemReader(): ItemReader<DormantMemberDetails> {
    val status = "dormant"
    val date = ZonedDateTime.now().minusYears(5)
    logger.info(date.toString())
    return JdbcPagingItemReaderBuilder<DormantMemberDetails>()
      .name("itemReader")
      .pageSize(memberWithdrawalJobParameter.chunk ?: 100)
      .fetchSize(memberWithdrawalJobParameter.chunk ?: 100)
      .dataSource(memberSlaveDataSource)
      .queryProvider(queryProvider())
      .rowMapper{ rs, _ -> mappingResultSet(rs)}
      .parameterValues(
        mapOf(
          "status" to status,
          "dormantDate" to date
        )
      )
      .build()
  }
}
  • Bean의 생성 시점을 지정된 Scope가 실행되는 시점으로 지연시킨다.
  • Bean의 생성시점을 어플리케이션 실행 시점이 아닌, Step 혹은 Job의 실행시점으로 지연시키면서 얻는 장점은 크게 2가지가 있다.

장점

JobParameter의 Late Binding이 가능

  • JobParameter를 StepContext 또는 JobExecutionContext 레벨에서 할당시킬 수 있다.
  • Application이 실행되는 시점이 아니더라도 Controller나 Service와 같은 비지니스 로직 처리 단계에서 JobParameter를 할당시킬 수 있다.

동일한 컴포넌트를 병렬 혹은 동시에 사용할 때 유용하다

  • Step 안에 Tasklet이 있고, 이 Tasklet은 멤버 변수와 이 멤버 변수를 변경하는 로직이 있다고 가정해보자
  • 이 경우 @StepScope 없이 Step을 병렬로 실행시키게 되면 서로 다른 Step에서 하나의 Tasklet을 두고 마구잡이로 상태를 변경하려고 할 것이다.
  • 하지만 @StepScope가 있다면 각각의 Step에서 별도의 Tasklet을 생성하고 관리하기 때문에 서로의 상태를 침범할 일이 없다.

아니 그냥 시스템 변수 쓰면 되지 않나요? 왜 잡파라미터를 써야됨?

  • 시스템 변수를 사용할 경우 Spring Batch의 Job Parameter 관련 기능을 못쓰게 된다.
    • 예를 들어, Spring Batch는 같은 JobParameter로 같은 Job을 두 번 실행하지 않는다. 하지만 시스템 변수를 사용하게 될 경우 이 기능이 전혀 작동하지 않는다.
    • 또한 Spring Batch에서 자동으로 관리해주는 Parameter 관련 메타 테이블이 전혀 관리되지 않는다.
  • Late Binding을 할 수 없다.
    • 예를 들어 웹 서버가 있고, 이 웹서버에서 Batch를 수행한다고 가정해보자 외부에서 넘겨주는 파라미터에 따라 Batch가 다르게 작동해야한다면, 이를 시스템 변수로 풀어내는 것은 어려우나 JobParameter를 이용한다면 아주 쉽게 해결할 수 있다. (물론 웹 서버에서 Spring Batch를 관리하는 건 권장하지 않는 방법)

JobParameter를 주입 받아서 사용하기

기존 방식

@Configuration
@EnableBatchProcessing
class MemberWithdrawalConfiguration(
  private val jobBuilderFactory: JobBuilderFactory,
  private val memberWithdrawalJobListener: MemberWithdrawalJobListener,
  private val skipListener: MemberWithdrawalSkipListener,
  private val stepBuilderFactory: StepBuilderFactory,
  private val transactionManager: JpaTransactionManager,
  private val memberWithdrawalJobParameter: MemberWithdrawalJobParameter,
  private val memberWithdrawalItemReader: ItemReader<DormantMemberDetails>,
  private val memberWithdrawalCompositeItemWriter: CompositeItemWriter<DormantMemberDetails>

): Log {
	@Bean
	fun memberWithdrawalJob(): Job {
		return jobBuilderFactory.get("memberWithdrawalJob")
			.start(memberWithdrawalStep(null))
			.listener(memberWithdrawalJobListener)
			.build()
	}
	@Bean
	@JobScope
	fun memberWithdrawalStep(@Value("#{jobParameters[CHUNK]}") chunk: Long?): Step{	
    	return stepBuilderFactory["memberWithdrawalStep"]
			.chunk<DormantMemberDetails, DormantMemberDetails>( memberWithdrawalJobParameter.chunk ?: 100)
			.reader(memberWithdrawalItemReader)
			.processor(memberWithdrawalProcessor())
			.writer(memberWithdrawalCompositeItemWriter)
			.faultTolerant()
			.listener(skipListener)
			.skip(Exception::class.java)
			.skipLimit(100)
			.transactionManager(transactionManager)
			.build()
	}

	@Bean
	fun memberWithdrawalProcessor() = MemberWithdrawalProcessor()

}
  • 예시 코드를 보면 잡파라미터를 Step의 인자로 넘겨주고 있는데, 여기서는 몇가지의 문제점이 존재한다.
    • start(memberWithdrawalStep(null)): memberWithdrawalStep을 호출하기 위해 null이라는 임시 값을 강제로 넣어줘야 한다.
    • 만약에 잡 파라미터로 시간을 가져와야 하는 경우 (예를 들어 @Value("#{jobParameters[TIME]}") time: String?) 이런 식으로 가져와야 하는 경우 이 타입을 Step 함수에서 형변환을 해줘야한다. 형 변환된 파라미터는 Reader/Processor/Writer 코드에서 재사용을 할 수 없고, 잡 파라미터의 확장성이 떨어진다.
      • 예를 들어, 2020.03으로 파라미터가 넘어오면 Reader에서 2020.03.01 과 2020.03.31로 2개의 값으로 분리되서 필요하다면 Reader에서 파라미터를 받아 본인이 원하는 형태로 분리하는 로직도 함께 갖고 있어야만 한다.
      • 즉, Reader 가 해야할 일은 기간별 조회 기능 + 월로 넘어온 값을 시작일/종료일로 분리하기라는 2가지 기능을 같이 해야만 하는 경우가 생기게 된다. -> SRP 위반

JobParameter를 주입받아서 사용하기

  • 위의 문제를 해결하기 위해서는 JobParameter에 관한 모든 기능을 담당할 JobParameter 클래스를 작성하면 된다.
open class MemberWithdrawalJobParameter {
  @Value("#{jobParameters[CHUNK]}")
  open val chunk: Int? = null
  open var referenceDate: Instant? = null

  @Value("#{jobParameters[REFERENCE_DATE]}")
  fun setReferenceDate(referenceDate: String?) {
    this.referenceDate = Instant.parse(referenceDate)
  }
}
  • MemberWithdrawalJobParameter라는 잡파라미터 클래스를 만들고 위와 같이 코드를 작성했다.
  • LocalDate, LocalDateTime, Instant 등 자동 형변환이 되지 않는 경우에는 Setter를 정의한 다음 @Value를 사용하여 문자열로 받은 후, 형 변환을 진행한다.
@Configuration
@EnableBatchProcessing
class MemberWithdrawalConfiguration(
  private val jobBuilderFactory: JobBuilderFactory,
  private val memberWithdrawalJobListener: MemberWithdrawalJobListener,
  private val skipListener: MemberWithdrawalSkipListener,
  private val stepBuilderFactory: StepBuilderFactory,
  private val transactionManager: JpaTransactionManager,
  private val memberWithdrawalJobParameter: MemberWithdrawalJobParameter,
  private val memberWithdrawalItemReader: ItemReader<DormantMemberDetails>,
  private val memberWithdrawalCompositeItemWriter: CompositeItemWriter<DormantMemberDetails>
): Log {
	@Bean
	@JobScope
	fun memberWithdrawalJobParameter(): MemberWithdrawalJobParameter {
		return MemberWithdrawalJobParameter()
	}

	@Bean
	fun memberWithdrawalJob(): Job {
		return jobBuilderFactory.get("memberWithdrawalJob")
			.start(memberWithdrawalStep())
			.listener(memberWithdrawalJobListener)
			.build()
	}
	@Bean
	@JobScope
	fun memberWithdrawalStep(): Step {
		return stepBuilderFactory["memberWithdrawalStep"]
			.chunk<DormantMemberDetails, DormantMemberDetails>( memberWithdrawalJobParameter.chunk ?: 100)
			.reader(memberWithdrawalItemReader)
			.processor(memberWithdrawalProcessor())
			.writer(memberWithdrawalCompositeItemWriter)
			.faultTolerant()
			.listener(skipListener)
			.skip(Exception::class.java)
			.skipLimit(100)
			.transactionManager(transactionManager)
			.build()
	}

	@Bean
	fun memberWithdrawalProcessor() = MemberWithdrawalProcessor()

}
  • 이렇게 만들어진 클래스는 Job 클래스에서 아래와 같이 @JobScope를 가진 Bean으로 등록하여 사용하면 된다.

잡 파라미터 클래스에 Open 키워드를 붙여야 하는 이유

  • 해당 키워드를 붙이지 않을 경우 아래와 같은 에러가 발생한다.
Caused by: org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'memberWithdrawalJobParameter' defined in BeanDefinition defined in class path resource 
[kr/co/mustit/batch/job/MemberWithdrawalConfiguration.class]: Initialization of bean failed; nested exception is 
org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class kr.co.mustit.batch.dto.MemberWithdrawalJobParameter: Common causes 
of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Cannot subclass final class 
kr.co.mustit.batch.dto.MemberWithdrawalJobParameter

Reference

5. Spring Batch 가이드 - Spring Batch Scope & Job Parameter
JobParameter 활용 방법 (feat. LocalDate 파라미터 사용하기)

profile
평범한 백엔드 개발자

0개의 댓글