TIL

잡 파라미터와 스프링 배치 Scope

프로퍼티와 JobParameters 차이

입력값 동적 변경

메타데이터

JobParameters 전달하기

JobParameters 기본 표기법

parameterName=parameterValue,parameterType,identificationFlag

다양한 타입의 Job 파라미터

기본 데이터 타입 파라미터 전달

    @Bean
    @StepScope
    fun terminatorTasklet(
        @Value("#{jobParameters['terminatorId']}") terminatorId: String,
        @Value("#{jobParameters['targetCount']}") targetCount: Int,
    ): Tasklet =
        Tasklet { _: StepContribution, _: ChunkContext ->
            // ...
            RepeatStatus.FINISHED
        }

열거형 파라미터 전달

    @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 }

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,
)
    @Bean
    fun terminatorParamTasklet(params: PojoParameters): Tasklet =
        Tasklet { _: StepContribution, _: ChunkContext ->
            // ...
            RepeatStatus.FINISHED
        }

기본 파라미터 표기법의 한계

JobParameter의 JSON 기반 표기법

    @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 표기법 사용을 위한 준비

커맨드 라인 파라미터가 실제 Job으로 전달되는 과정

프로그래밍 방식으로 JobParameters 생성/전달

val jobParameters = JobParametersBuilder()
    .addString("inputFilePath", "/data/input/users.csv")
    .toJobParameters()

jobLauncher.run(dataProcessingJob, jobParameters)

JobParameters 코드로 직접 접근

@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
    }
}

public class StepExecution extends Entity {
	private final JobExecution jobExecution;
	// ...
	public JobParameters getJobParameters() {
		if (jobExecution == null) {
			return new JobParameters();
		}
		return jobExecution.getJobParameters();
	}
}

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)")
        }
    }
}
    @Bean
    fun systemDestructionJob(
        jobRepository: JobRepository,
        systemDestructionStep: Step,
        validator: JobParametersValidatorEx
    ): Job {
        return JobBuilder("systemDestructionJob", jobRepository)
            .validator(validator)
            .start(systemDestructionStep)
            .build()
    }
.validator(
    DefaultJobParametersValidator(
        arrayOf("destructionPower"),
        arrayOf("system.destruction.level"),
    )
)

Jop과 Step의 Scope

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와 StepScope 사용 시 주의 사항

@Scope(value = "job", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JobScope {}
org.springframework.beans.factory.support.ScopeNotActiveException: 
Error creating bean with name 'scopedTarget.systemDestructionStep': 
Scope 'step' is not active for the current thread

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()
    }