강의

멘토링

커뮤니티

인프런 커뮤니티 질문&답변

스프링님의 프로필 이미지
스프링

작성한 질문수

죽음의 Spring Batch: 새벽 3시의 처절한 공포는 이제 끝이다.

1장. 작전2: 잡 파라미터와 스프링 배치 Scope (파라미터 없는 배치? 그건 그냥 좀비 프로세스나 마찬가지다 ☠️)

@StepScope 또는 @JobScope와 JobOperator

해결된 질문

작성

·

56

1

킬구형

아래는 step에서 ItemWriter의 jobParameter자리에 null을 넣는 방식으로 처리한거야.

@Scheduled(cron = "0 0 19,22 * * *")
fun runSampleJob() {
    jobOperator.start(sampleJob(),jobParameters)
}

@Bean
    fun sampleJob(): Job =
        JobBuilder("sampleJob",jobRepository)
            .start(sampleStep())
            .build()


    @Bean
    fun sampleStep(): Step =
        StepBuilder("sampleStep", jobRepository)
            .chunk<String, String>(CHUNK_SIZE)
            .transactionManager(transactionManager)
            .reader(sampleReader())
            .writer(sampleWriter(null, null))
            .build()

    @Bean
    @StepScope
    fun sampleReader(): JdbcPagingItemReader<String> =
        JdbcPagingItemReaderBuilder<String>()
            ...
            .build()

    @Bean
    @StepScope
    fun sampleWriter(
        @Value("#{jobParameters['title']}") title: String?,
        @Value("#{jobParameters['content']}") content: String?,
    ): ItemWriter<String> = ItemWriter { chunk ->
        ...doSomeWrite
    }

위 코드를 빈 주입방식으로 변경하는 방법을 모르겠어.
빈 주입 방식으로 변경하면 아래처럼 되잖아?
이때 jobOpterator로 잡을 호출하는 부분까지 파라미터가 올라와버리는데 이걸 어떻게 해야할지 모르겠단 말이야~~!

@Scheduled(cron = "0 0 19,22 * * *")
fun runSampleJob() {
    jobOperator.start(sampleJob(**여기를 어떻게 처리하지?**),jobParameters)
}

@Bean
fun sampleJob(
    sampleStep: Step
): Job =
    JobBuilder("sampleJob",jobRepository)
        .start(sampleStep)
        .build()


@Bean
fun sampleStep(
    sampleReader: ItemReader<String>,
    sampleWriter: ItemWriter<String>
): Step =
    StepBuilder("sampleStep", jobRepository)
        .chunk<String, String>(CHUNK_SIZE)
        .transactionManager(transactionManager)
        .reader(sampleReader)
        .writer(sampleWriter)
        .build()

@Bean
@StepScope
fun sampleReader(): JdbcPagingItemReader<String> =
    JdbcPagingItemReaderBuilder<String>()
...
.build()

@Bean
@StepScope
fun sampleWriter(
    @Value("#{jobParameters['title']}") title: String?,
    @Value("#{jobParameters['content']}") content: String?,
): ItemWriter<String> = ItemWriter { chunk ->
    ...doSomeWrite
}

새해 복 많이 받아 형~

답변 5

2

KILL-9님의 프로필 이미지
KILL-9
지식공유자

당장 생각나는 방법은 두가지정도되는데 간단히 첫번째방법부터 알려주겠다

Job job = jobRegistry.getJob(jobName);
jobOperator.start(job, jobParameters);

하면 ㄱ ㄱ가능하다(배치5를 보고 질문준것으로 이해했다. 배치6이라면 직접 JobRegistry를 빈으로 등록해야한다. 배치5에서는 자동구성되니 주입받아 스면 된다)

배치6 강의를 보고 질문줄것으로 이해했다 💀

형도 새해 복 💀💀💀

1

스프링님의 프로필 이미지
스프링
질문자

파라미터 정상 전달 확인 했고, 잡이 정상 실행된 것도 확인 했어!

KILL-9님의 프로필 이미지
KILL-9
지식공유자

파라미터가 정상적으로 전달되었다면 리더가 읽어야 맞다 혹시 전체로그를 전달해줄수있나 💀💀💀

1

KILL-9님의 프로필 이미지
KILL-9
지식공유자

반갑다 스프링형☠️  방금까지 배치6 나머지 강의 자료를 등록하느라 정신이 없었다. 10분만 휴식을 취하겠다. ☠️ 

0

스프링님의 프로필 이미지
스프링
질문자

@Configuration
class TestConfig(
    private val jobOperator: JobOperator,
    private val dataSource: DataSource,
    private val jobRepository: JobRepository,
    private val transactionManager: PlatformTransactionManager,
) {

    companion object {
        private const val CHUNK_SIZE = 200
    }

    private val logger = KotlinLogging.logger {}

    @Scheduled(cron = "*/30 * * * * *")
    fun runtestJob() {
        val jobParameters = JobParametersBuilder()
            .addString("startTime", LocalDateTime.now().toString())
            .toJobParameters()

        jobOperator.start(testJob(),jobParameters)
    }

    @Bean
    fun testJob(): Job =
        JobBuilder("testJob",jobRepository)
            .start(testStep())
            .build()


    @Bean
    fun testStep(): Step =
        StepBuilder("testStep", jobRepository)
            .chunk<String, String>(CHUNK_SIZE)
            .transactionManager(transactionManager)
            .reader(testReader())
            .writer(testWriter())
            .build()

    @Bean
    @StepScope
    fun testReader(): JdbcPagingItemReader<String> =
        JdbcPagingItemReaderBuilder<String>()
            .name("testReader")
            .dataSource(dataSource)
            .pageSize(CHUNK_SIZE)
            .selectClause("select img_name, pk")
            .fromClause("from card_img")
            .sortKeys(mapOf("pk" to Order.ASCENDING))
            .rowMapper { rs, _ -> rs.getString("img_name") }
            .build()

    @Bean
    @StepScope
    fun testWriter(): ItemWriter<String> = ItemWriter { chunk ->
        logger.info { "${chunk.items.size}개 전달받았습니다." }
    }
}

이 코드를 실행하고 나는 30초마다 x개 전달받았습니다. 라는 로그가 남길 기대한다.

하지만 로그는 아래와 같이 최초 1회만 로그가 남는다.
이유를 모르겠다,,! 하루종일 삽질중이다..

2026-01-04T19:49:30.143+09:00  INFO 98079 --- [ cached-async-1] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=testJob]] launched with the following parameters: [{JobParameter{name='startTime', value=2026-01-04T19:49:30.024757, type=class java.lang.String, identifying=true}}]
2026-01-04T19:49:30.205+09:00  INFO 98079 --- [ cached-async-1] o.s.batch.core.job.SimpleStepHandler     : Executing step: [testStep]
2026-01-04T19:49:30.224+09:00  INFO 98079 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : 15개 전달받았습니다.
2026-01-04T19:49:30.231+09:00  INFO 98079 --- [ cached-async-1] o.s.batch.core.step.AbstractStep         : Step: [testStep] executed in 24ms
2026-01-04T19:49:30.259+09:00  INFO 98079 --- [ cached-async-1] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=testJob]] completed with the following parameters: [{JobParameter{name='startTime', value=2026-01-04T19:49:30.024757, type=class java.lang.String, identifying=true}}] and the following status: [COMPLETED] in 100ms
2026-01-04T19:50:00.050+09:00  INFO 98079 --- [ cached-async-1] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=testJob]] launched with the following parameters: [{JobParameter{name='startTime', value=2026-01-04T19:50:00.006744, type=class java.lang.String, identifying=true}}]
2026-01-04T19:50:00.065+09:00  INFO 98079 --- [ cached-async-1] o.s.batch.core.job.SimpleStepHandler     : Executing step: [testStep]
2026-01-04T19:50:00.069+09:00  INFO 98079 --- [ cached-async-1] o.s.batch.core.step.AbstractStep         : Step: [testStep] executed in 4ms
2026-01-04T19:50:00.089+09:00  INFO 98079 --- [ cached-async-1] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=testJob]] completed with the following parameters: [{JobParameter{name='startTime', value=2026-01-04T19:50:00.006744, type=class java.lang.String, identifying=true}}] and the following status: [COMPLETED] in 31ms
KILL-9님의 프로필 이미지
KILL-9
지식공유자

안되겠군 15분만 대기하라 출격 예정 💀
스프링님의 프로필 이미지
스프링
질문자

항상 고맙고 미안하다..

KILL-9님의 프로필 이미지
KILL-9
지식공유자

형 우선 파악부터해보자
ItemReadListener의 beforeRead()를 구현해서 Step에 설치해보길바란다.

이 녀석도 StepScope로 설정하고 beforeRead()에서 parameter를 로그로남기게해보자 

 

스프링님의 프로필 이미지
스프링
질문자

@Bean
    @StepScope
    fun testReadListener(
        @Value("#{jobParameters['startTime']}") startTime: String?
    ): ItemReadListener<String> = object : ItemReadListener<String> {

        override fun beforeRead() {
            logger.info { "beforeRead() startTime=$startTime" }
        }

        override fun afterRead(item: String) {
             logger.debug { "afterRead item=$item" }
        }

        override fun onReadError(ex: Exception) {
            logger.error(ex) { "onReadError startTime=$startTime" }
        }
    }

위 리스너를 등록했고 아래는 등록 후 실행 로그야 형!

2026-01-04T20:19:18.379+09:00  INFO 231 --- [           main] com.clip.BatchApplicationKt              : Started BatchApplicationKt in 6.585 seconds (process running for 6.988)
2026-01-04T20:19:30.104+09:00  INFO 231 --- [ cached-async-1] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=testJob]] launched with the following parameters: [{JobParameter{name='startTime', value=2026-01-04T20:19:30.019906, type=class java.lang.String, identifying=true}}]
2026-01-04T20:19:30.129+09:00  INFO 231 --- [ cached-async-1] o.s.batch.core.job.SimpleStepHandler     : Executing step: [testStep]
2026-01-04T20:19:30.165+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.167+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.168+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : beforeRead() startTime=2026-01-04T20:19:30.019906
2026-01-04T20:19:30.169+09:00  INFO 231 --- [ cached-async-1] com.clip.batch.fcm.TestConfig            : 15개 전달받았습니다.
2026-01-04T20:19:30.179+09:00  INFO 231 --- [ cached-async-1] o.s.batch.core.step.AbstractStep         : Step: [testStep] executed in 48ms
2026-01-04T20:19:30.201+09:00  INFO 231 --- [ cached-async-1] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=testJob]] completed with the following parameters: [{JobParameter{name='startTime', value=2026-01-04T20:19:30.019906, type=class java.lang.String, identifying=true}}] and the following status: [COMPLETED] in 83ms
2026-01-04T20:20:00.031+09:00  INFO 231 --- [ cached-async-1] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=testJob]] launched with the following parameters: [{JobParameter{name='startTime', value=2026-01-04T20:20:00.007752, type=class java.lang.String, identifying=true}}]
2026-01-04T20:20:00.059+09:00  INFO 231 --- [ cached-async-1] o.s.batch.core.job.SimpleStepHandler     : Executing step: [testStep]
2026-01-04T20:20:00.064+09:00  INFO 231 --- [ cached-async-1] o.s.batch.core.step.AbstractStep         : Step: [testStep] executed in 4ms
2026-01-04T20:20:00.088+09:00  INFO 231 --- [ cached-async-1] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=testJob]] completed with the following parameters: [{JobParameter{name='startTime', value=2026-01-04T20:20:00.007752, type=class java.lang.String, identifying=true}}] and the following status: [COMPLETED] in 44ms
KILL-9님의 프로필 이미지
KILL-9
지식공유자

아 형 설마 배치 6.0.0??

6.0.1로 변경후 ㄱ ㄱ 한번 부탁한다 
스프링님의 프로필 이미지
스프링
질문자

??? 6.0.0인데 이거 뭐 하자있는 거였슴꽈,,,?

스프링님의 프로필 이미지
스프링
질문자

크아아악 킬구형,, 내 하루가,,;;;

6.0.0에서 6.0.1로 숫자 하나 바꾸고 해결됐다.
뭔 차이인지 궁금하군,,
민망하니,,,, 일주일 뒤쯤에나 다시 찾아오도록 하겠다.
촤하하핫 뻘쭘하군;;;

진짜 고맙다
형밖에 없다 😘

KILL-9님의 프로필 이미지
KILL-9
지식공유자

미안하다 배치5인줄알았군

배치6 4장 작전4에 소개할 ChunkOrientedStep 내부의 버그때문이다

https://github.com/86dh/spring-batch/commit/69665d83d8556d9c23a965ee553972a277221d83

이 커밋으로해결되었다.
굿굿

추가로,
사용하는 버전은 배치6이지만 코드 스멜이 배치5 느낌이나서 안내하자면
배치5버전을 생각해서 작업할땐 
chunk(int chunkSize) 

이 녀석 대신에

chunk(int chunkSize, PlatformTransactionManager transactionManager)
이녀석을 사용하도록
스프링님의 프로필 이미지
스프링
질문자

이런건 어떻게 알아내고 공부하는거야 형;;
암튼 형 사랑하는 내맘 알아줘라!

KILL-9님의 프로필 이미지
KILL-9
지식공유자

너도 강의를 만들면 이렇게 된다 관심있다면 한 번 해보는 것을 추천한다

굿굿

0

스프링님의 프로필 이미지
스프링
질문자

@Configuration
class SampleScheduler(
    private val jobOperator: JobOperator,
    private val sampleSchedulerContentService: SampleSchedulerContentService,
    private val dataSource: DataSource,
    private val jobRepository: JobRepository,
    private val transactionManager: PlatformTransactionManager,
    private val jobRegistry: JobRegistry,
) {

    companion object {
        private const val CHUNK_SIZE = 200
    }

    private val logger = KotlinLogging.logger {}

    @Scheduled(cron = "0 0 19,22 * * *")
    fun runSampleJob() {

        val hour = LocalDateTime.now().hour
    
        val content = when (hour) {
            19 -> sampleSchedulerContentService.findFirstSchedulerContent()
            22 -> sampleSchedulerContentService.findSecondSchedulerContent()

            else -> throw IllegalArgumentException("Invalid hour for FCM scheduling")
        }
        val jobParameters = JobParametersBuilder()
            .addString("title", content.title)
            .addString("content", content.content)
            .addString("startTime", LocalDateTime.now().toString())
            .toJobParameters()
        val job = jobRegistry.getJob("sampleJob")
            ?: throw IllegalStateException("Job not found: sampleJob")

        jobOperator.start(job, jobParameters)
    }

    @Bean
    fun sampleJob(sampleStep: Step): Job =
        JobBuilder("sampleJob",jobRepository)
            .start(sampleStep)
            .build()


    @Bean
    fun sampleStep(
        sampleReader: JdbcPagingItemReader<String>,
        sampleWriter: ItemWriter<String>,
        jdbcTemplate: JdbcTemplate
    ): Step =
        StepBuilder("sampleStep", jobRepository)
            .chunk<String, String>(CHUNK_SIZE)
            .transactionManager(transactionManager)
            .listener(object : StepExecutionListener {
                override fun beforeStep(se: StepExecution) {
                    val cnt = jdbcTemplate.queryForObject(
                        "select count(*) from member where is_allow_notify = true and firebase_token is not null",
                        Long::class.java
                    )
                    logger.info { "PRE-CHECK count=$cnt" }
                }
                override fun afterStep(se: StepExecution): ExitStatus {
                    logger.info { "readCount=${se.readCount}, writeCount=${se.writeCount}, commitCount=${se.commitCount}" }
                    return se.exitStatus
                }
            })
            .reader(fcmReader)
            .writer(fcmWriter)
            .build()

    @Bean
    @StepScope
    fun sampleReader(): JdbcPagingItemReader<String> {

        val reader = JdbcPagingItemReaderBuilder<String>()
            .name("sampleReader")
            .dataSource(dataSource)
            .pageSize(CHUNK_SIZE)
            .selectClause("select firebase_token, pk")
            .fromClause("from member")
            .whereClause("is_allow_notify = true and firebase_token is not null")
            .sortKeys(mapOf("pk" to Order.ASCENDING))
            .rowMapper { rs, _ -> rs.getString("firebase_token") }
            .build()
        logger.info { "sampleReader bean created: " + System.identityHashCode(reader) }
        return reader
    }


    @Bean
    @StepScope
    fun sampleWriter(
        @Value("#{jobParameters['title']}") title: String?,
        @Value("#{jobParameters['content']}") content: String?,
    ): ItemWriter<String> {
        val itemWriter = ItemWriter<String> { chunk ->

            logger.info { "${chunk.items.size}개의 fcmToken을 대상으로 multicastFcm 비동기처리 하였습니다." }
        }
        logger.info { "sampleWriter bean created: " + System.identityHashCode(itemWriter) }
        return itemWriter
    }
}

형 위 코드처럼 돌리면 처음 1회는 리더에서 쿼리 조건에 해당하는 만큼 레코드를 읽어오고 writer까지 잘 돌거든? 근데 2번째 실행부터 reader에서 0개를 읽어오네? 그러니까 당연히 writer도 동작 안하고,,

reader 빈도 스탭마다 새로 잘 생성되는 걸 확인했는데 왜 실행하면 0개 읽고 끝내버릴까,,!?
리스너에서 대상 래코드가 잘 존재하는 것도 확인했어!

KILL-9님의 프로필 이미지
KILL-9
지식공유자

음
1) 파라미터가 정상적으로 전달되었는지
2) 잡이 정상 실행된게맞는지를

우선 확인해주길 바란다
스프링님의 프로필 이미지
스프링

작성한 질문수

질문하기