잡 파라미터와 스프링 배치 Scope
JobParameters
는 배치 작업에 전달되는 입력 값이다.
- 어떤 조건에서, 어떤 데이터를 다룰 것인가
- ex) 매일 실행되는 배치의 경우 날짜나 실행 경로 등 매번 바뀐다.
- 즉
JobParameters
는 배치 잡을 동적이고 유연하게 만들어 준다.
프로퍼티와 JobParameters 차이
- -D 옵션 으로 실행 시 파라미터를 주입할 수도 있지만
JobParameters
와 그 목적이 다르다.
입력값 동적 변경
- -D 옵션 프로퍼티는 앱 시작 시 주입되는 정적인 값이다.
JobParameters
로는 웹 요청이 들어올 때마다 비동기 배치로 매번 다른 파라미터를 동적으로 전달할 수 있다.
메타데이터
- 스프링 배치에선 JobParameters의 모든 값을 메타데이터 저장소에 기록한다.
- 이를 통해 다음을 제공한다.
Job
인스턴스 식별 및 재시작 처리
Job
실행 이력 추적
JobParameters 전달하기
- 실제 운영 환경에선 커맨드 라인을 통해 파라미터를 전달하는 방식이 핵심이다.
- 젠킨스와 같은 CI/CD 도구 등 대부분 스케줄러 도그는 커맨드라인 실행을 기본으로 지원
- ex)
./gradlew bootRun --args='--spring.batch.job.name=dataProcessingJob inputFilePath=/data/input/users.csv,java.lang.String’
—spring.batch.job.name
은 Job
의 이름을 지정, 그 뒤의 key, value들이 실제 JobParameters
다.
JobParameters 기본 표기법
parameterName=parameterValue,parameterType,identificationFlag
parameterName
: Job에서 파라미터를 찾을 때 사용하는 key 값
parameterValue
: 파라미터의 실제 값
parameterType
: 파라미터의 타입으로 명시하지 않으면 String
으로 가정한다.
java.lang.String
와 같은 fully qualified name 사용
- 스프링 배치의
DefaultJobParametersConverter
컴포넌트를 통해 적절한 타입으로 변환된다.
indentificationFlag
: 스프링 배치에게 해당 파라미터가 JobInstance
식별에 사용되는지 여부를 전달
다양한 타입의 Job 파라미터
기본 데이터 타입 파라미터 전달
@Bean
@StepScope
fun terminatorTasklet(
@Value("#{jobParameters['terminatorId']}") terminatorId: String,
@Value("#{jobParameters['targetCount']}") targetCount: Int,
): Tasklet =
Tasklet { _: StepContribution, _: ChunkContext ->
// ...
RepeatStatus.FINISHED
}
@Value
애노테이션과 #{jobParameters['parameterName']}
표현식으로 JobParameters
를 주입 받을 수 있다.
- 그리고 아래 명령어로 위 태스크에 파라미터를 전달하며 실행시킬 수 있다.
./gradlew bootRun --args='--spring.batch.job.name=processTerminatorJob terminatorId=KILL-9,java.lang.String targetCount=5,java.lang.Integer’
@StepScope
- @Value
를 통해 잡 파라미터를 주입 받기 위해 필요
열거형 파라미터 전달
- 열거형(
Enum
)을 JobParameters
로 사용할 수 있다.
@Bean
@StepScope
fun terminatorTaskletEnum(
@Value("#{jobParameters['questDifficulty']}") questDifficulty: QuestDifficulty,
): Tasklet =
Tasklet { contribution: StepContribution, chunkContext: ChunkContext ->
// ...
val rewardMultiplier =
when (questDifficulty) {
QuestDifficulty.EASY -> 1
QuestDifficulty.NORMAL -> 2
QuestDifficulty.HARD -> 3
QuestDifficulty.EXTREME -> 5
}
// ...
RepeatStatus.FINISHED
}
enum class QuestDifficulty { EASY, NORMAL, HARD, EXTREME }
Enum
파라미터 전달을 위한 커맨드 라인은 아래와 같다.
./gradlew bootRun --args='--spring.batch.job.name=processTerminatorJob questDifficulty=HARD,com.system.batch.JobParametersConfig$QuestDifficulty’
Enum
클래스가 JobParametersConfig
클래스의 내부 클래스로 선언되어 있기에 JobParametersConfig$QuestDifficulty
로 명시해야 한다.
POJO를 활용한 Job 파라미터 주입
- 대규모 배치 작업에선 여러 파라미터를 효율적으로 관리해야 할 때 별도 클래스로 파라미터를 관리할 수 있다.
@Component
@StepScope
class PojoParameters(
@Value("#{jobParameters[missionName]}")
private val missionName: String,
@Value("#{jobParameters[securityLevel]}")
private val securityLevel: Int,
@Value("#{jobParameters[operationCommander]}")
private val operationCommander: String,
)
- 마찬가지로
@Value
어노테이션으로 파라미터를 주입받을 수 있고 다음을 지원한다.
- 필드 직접 부입
- 생성자 파라미터 주입
- 세터 메서드 주입
@Bean
fun terminatorParamTasklet(params: PojoParameters): Tasklet =
Tasklet { _: StepContribution, _: ChunkContext ->
// ...
RepeatStatus.FINISHED
}
- Pojo 파라미터를 전달하는 방법은 기존과 똑같다.
./gradlew bootRun --args='--spring.batch.job.name=processTerminatorJob missionName=안산_데이터센터_침투,java.lang.String operationCommander=KILL-9 securityLevel=3,java.lang.Integer,false’
기본 파라미터 표기법의 한계
- 파라미터 값에 아래처럼 쉼표(,)가 포함된다면?
infiltrationTargets=판교_서버실,안산_데이터센터,java.lang.String
- Spring Batch는 파라미터 타입을 ‘안산_데이터센터’로 오해버리는 한계가 있다.
- 이를 보완하기 위해 Spring Batch 5부터 JSON 기반 표기법을 새로 제공한다.
JobParameter의 JSON 기반 표기법
- Json 표기법은 다음과 같다.
infiltrationTargets='{"value": "판교_서버실,안산_데이터센터", "type": "java.lang.String"}’
- 표기법 구성 요소(
value
, type
, identifying
)들은 기본 표기법과 동일한 의미를 가진다.
@Bean
@StepScope
fun terminatorTaskletJson(
@Value("#{jobParameters['infiltrationTargets']}") infiltrationTargets: String,
): Tasklet =
Tasklet { _: StepContribution, _: ChunkContext ->
val targets: Array<String?> =
infiltrationTargets.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
// ...
log.info { "첫 번째 타겟: ${targets[0]} 침투 시작" }
log.info { "마지막 타겟: ${targets[1]} 에서 집결" }
// ...
RepeatStatus.FINISHED
}
Json 표기법 사용을 위한 준비
- 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-json’
JsonJobParametersConverter
빈 등록
@Bean *fun* jobParametersConverter(): *JobParametersConverter* = JsonJobParametersConverter()
- 그리고 아래 명령어를 통해 job을 실행할 수 있다.
./gradlew bootRun --args="--spring.batch.job.name=terminatorJob infiltrationTargets='{\"value\":\"판교서버실,안산데이터센터\",\"type\":\"java.lang.String\"}'”
커맨드 라인 파라미터가 실제 Job으로 전달되는 과정
JobLauncherApplicationRunner
- Spring Boot3에서 Spring Batch를 실행하면 동작하는
ApplicationRunner
의 한 종류
- 커맨드라인으로 전달된 잡 파라미터를 해석하고 실제
Job
을 실행하는 역할을 맡는다.
JobLauncherApplicationRunner
처리 과정
- 처리 목록 준비 -
ApplicationContext
에 등록된 모든 Job
타입 빈이 JobLauncherApplicationRunner
에 자동 주입
- 유효성 검증 (주로
--spring.batch.job.name
검증)
- 만약
Job
이 여러 개인데 이름을 지정하지 않은 경우 검증 실패 (Job
이 하나면 허용)
- 커맨드라인에서 전달한
Job
이름을 찾을 수 없으면 검증 실패
- 명령어 해석 - 커맨드라인으로 전달된 값들을 파싱
Job
실행 - Job
이름에 따라 Job
을 찾고 파라미터를 전달해 실행한다. (JobLauncher
사용)
this.jobLauncher.run(job, parameters)
프로그래밍 방식으로 JobParameters 생성/전달
- 실무에선 커맨드라인 외에 다양한 방식으로 배치를 실행할 때가 있다.
- REST API를 통해 배치 작업을 트리거
- 메시지 큐에서 메시지가 도착했을 때
- 특정 비즈니스 이벤트가 발생했을 때
@Scheduled
태스크에서 동적 파라미터와 함께 실행할 때
- 위와 같은 상황에선 프로그래밍 방색으로
JobParameters
를 전달해야 하는데 JobParametersBuilder
컴포넌트가 사용된다.
val jobParameters = JobParametersBuilder()
.addString("inputFilePath", "/data/input/users.csv")
.toJobParameters()
jobLauncher.run(dataProcessingJob, jobParameters)
JobParameters 코드로 직접 접근
JobParameters
에 접근하려면 JobExecution
을 통해야 한다.
JobExecution
- Job
의 실행 정보, 실행 상태, 파라미터, 실행 결과 등을 쥐고 있다.
@Component
class ProgrammingAccessJobParameterTasklet : Tasklet {
override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus {
val jobParameters: JobParameters =
chunkContext.stepContext.stepExecution.jobParameters
val targetSystem = jobParameters.getString("system.target")
val destructionLevel: Long = jobParameters.getLong("system.destruction.level")!!
// ...
return RepeatStatus.FINISHED
}
}
- 위 코드를 보면
StepExecution
을 통해 JobParameters
를 가져온다.
StepExecution
- Step
의 실행 정보를 담고 있으며 내부적으로 부모 Job
의 JobExecution
을 참조한다.
public class StepExecution extends Entity {
private final JobExecution jobExecution;
// ...
public JobParameters getJobParameters() {
if (jobExecution == null) {
return new JobParameters();
}
return jobExecution.getJobParameters();
}
}
JobParametersValidator
- JobParametersValidator를 사용하면 잘못된 파라미터가 들어오는 순간 즉시 차단할 수 있다.
public interface JobParametersValidator {
void validate(@Nullable JobParameters parameters) throws JobParametersInvalidException;
}
@Component
class JobParametersValidatorEx : JobParametersValidator {
override fun validate(parameters: JobParameters?) {
if (parameters == null) throw JobParametersInvalidException("파라미터가 NULL입니다")
val destructionPower: Long =
parameters.getLong("destructionPower")
?: throw JobParametersInvalidException("destructionPower 파라미터는 필수값입니다")
if (destructionPower > 9) {
throw JobParametersInvalidException("파괴력 수준이 허용치를 초과했습니다: $destructionPower(최대 허용치: 9)")
}
}
}
- 위처럼 만든 validator는 JobBuilder에 추가할 수 있다.
@Bean
fun systemDestructionJob(
jobRepository: JobRepository,
systemDestructionStep: Step,
validator: JobParametersValidatorEx
): Job {
return JobBuilder("systemDestructionJob", jobRepository)
.validator(validator)
.start(systemDestructionStep)
.build()
}
- 단순히 파라미터 존재 여부만 확인하면 될 땐 별도 구현 없이
DefaultJobParametersValidator
를 사용하면 된다.
- 첫 번째 파라미터는 필수, 두 번째 파라미터는 선택적 파라미터로 취급된다.
- 필수 파라미터가 없으면 검증에 실패한다.
.validator(
DefaultJobParametersValidator(
arrayOf("destructionPower"),
arrayOf("system.destruction.level"),
)
)
Jop과 Step의 Scope
- Spring Batch는 Spring의 싱글톤과는 다른 스코프를 제공한다.
JobScope
, StepScope
- 스프링 실행 시점엔 프록시로 존재하지만 그 후 접근을 시도하면 실제 빈이 생성된다.
- 아래는 각 스코프 빈들의 생명 주기이다.
graph TD
A[Application Start] --> B[Proxy for JobScope & StepScope Beans]
B --> C[JobExecution start]
B --> D[StepExecution start]
C --> D
C --> E[JobScope Bean Initialized]
E --> F[JobParameters Injected]
F --> G[JobExecution End]
G --> H[JobScope Bean Destroyed]
D --> I[StepScope Bean Initialized]
I --> J[JobParameters Injected]
J --> K[StepExecution End]
K --> L[StepScope Bean Destroyed]
JobScope
와 StepScoe
를 지연 로딩하는 이점
- 런타임에 결정되는
JobParameters
를 실행 시점에 주입 받을 수 있다.
- 여러
Job
이 동시에 실행되도 각각 독립적인 빈이기에 동시성에 안전하다.
- 실행이 끝나면 빈도 함께 사라지므로 불필요한 메모리를 점유하지 않는다.
JobScope와 StepScope 사용 시 주의 사항
- 프록시 대상 타입이 클래스라면 상속 가능한 클래스여야 한다.
- CGLIB 기반 프록시로 동작하기에 상속 가능해야 한다.
@Scope(value = "job", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JobScope {}
Step
빈에는 @StepScope
를 사용하면 안 된다.
@StepScope
를 Step
빈에 달면 빈 생성 활성화 시점이 맞지 않아 오류가 발생한다.
Job
시작 → @StepScope
Step
빈 접근 시도 → Step
활성화 순으로 진행
- 때문에 2번째 단계에서
Step
이 아직 없어 에러가 발생
- 마찬가지로
@JobScope
에도 Step
빈에 선언해선 안된다.
org.springframework.beans.factory.support.ScopeNotActiveException:
Error creating bean with name 'scopedTarget.systemDestructionStep':
Scope 'step' is not active for the current thread
- 단순한 배치 작업에선 문제가 없어도 다음 상황에서 문제가 발생할 수 있다.
JobOperator
를 통한 Step
실행 제어 시
- Spring Integration(Remote partitioning)을 활용한 배치 확장 기능 사용 시
- 즉,
Step
, Job
빈은 싱글톤으로 등록해야 하고, 실제 실행 시점에 StepScope
혹은 JobScope
객체가 필요한 경우에만 해당 어노테이션을 적용해야 한다.
Tasklet
, ItemReader
, ItemProcessor
, ItemWriter
등 “실제 실행 시점마다 값이 달라져야 할 객체”에만 사용
ExecutionContext
- 실행 상태 등을 관리할 때
JobExecution
과 StepExecution
을 사용하지만 이것만으론 부족할 때가 있다.
- 비즈니스 로직 중 발생하는 커스텀 데이터를 관리할 때
ExecutionContext
라는 데이터 컨테이너를 사용한다.
- 집계 중간 결과물 같은 데이터 저장 가능
- 배치가 중단 후 중단 지점부터 재시작할 때
ExecutionContext
가 복원되므로 처리를 이어나갈 수 있다.
@Bean
@JobScope
fun systemDestructionTasklet(
@Value("#{jobExecutionContext['previousSystemState']}") prevState: String
): Tasklet? {
// JobExecution의 ExecutionContext에서 이전 시스템 상태를 주입받는다
TODO()
}
@Bean
@StepScope
fun infiltrationTasklet(
@Value("#{stepExecutionContext['targetSystemStatus']}") targetStatus: String
): Tasklet? {
// StepExecution의 ExecutionContext에서 타겟 시스템 상태를 주입받는다
TODO()
}
- 각
Job
과 Step
의 ExecutionContext
에선 각각 다른 스코프를 가진다.
Job
의 ExecutionContext
는 Job
에 속한 모든 컴포넌트에서 #{jobExecutionContext['key']}
로 접근 가능
Step
의 ExecutionContext
는 다른 Step에선 접근할 수 없고 해당 Step
의 데이터만 #{stepExecutionContext['key']}
로 접근 가능
- 다른
Step
의 데이터를 참조하고 싶다면 Job
ExecutionContext
를 활용하면 된다.
- 각
Step
간의 데이터 독립성을 완벽하게 보장하기 위해 이러한 엄격한 접근 제어를 사용한다.