인프런 커뮤니티 질문&답변
@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
지식공유자
당장 생각나는 방법은 두가지정도되는데 간단히 첫번째방법부터 알려주겠다
Job job = jobRegistry.getJob(jobName);
jobOperator.start(job, jobParameters);
하면 ㄱ ㄱ가능하다(배치5를 보고 질문준것으로 이해했다. 배치6이라면 직접 JobRegistry를 빈으로 등록해야한다. 배치5에서는 자동구성되니 주입받아 스면 된다)
배치6 강의를 보고 질문줄것으로 이해했다 💀
형도 새해 복 💀💀💀1
KILL-9
지식공유자
1
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 31msKILL-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스프링
질문자
크아아악 킬구형,, 내 하루가,,;;;
6.0.0에서 6.0.1로 숫자 하나 바꾸고 해결됐다.
뭔 차이인지 궁금하군,,
민망하니,,,, 일주일 뒤쯤에나 다시 찾아오도록 하겠다.
촤하하핫 뻘쭘하군;;;
진짜 고맙다
형밖에 없다 😘
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)
이녀석을 사용하도록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개 읽고 끝내버릴까,,!?
리스너에서 대상 래코드가 잘 존재하는 것도 확인했어!




