Tiny Star

프로젝트/대용량 이관

[대용량 이관] Book Job/Step 구성

흰둥아 2025. 5. 8. 14:43

20만건 정도의 book 데이터를 이관하는 것부터 시작했다. 구성보다는 에러를 해결한 내용이 위주이다...ㅎ

 


 

Book Job, Step 구성

@RequiredArgsConstructor
@Configuration
public class BatchConfiguration {
    private final Step stepBook;

    @Bean
    public Job bookJob(JobRepository jobRepository) {
        return new JobBuilder("bookJob", jobRepository)
                .start(stepBook)
                .build();
    }
}
@Configuration
public class BookStep {
    @Bean(name = "stepBook")
    public Step bookStep() {
        return new StepBuilder("stepBook", jobRepository)
                .<Map<String, Object>, BookDto>chunk(1000, transactionManager)
                .reader(bookReader())
                .processor(bookProcessor())
                .writer(bookWriter())
                .build();
    }
    ...

Spring Batch 5부터는 'JobBuilderFactory'를 주입하는게 아닌 'JobBuilder'를 생성해서 빈으로 만들기만하면 된다. Step도 마찬가지.

 

문제 1

Could not autowire. Qualified bean must be of 'Step' type.

Step 클래스명을 BookStep으로 하고 StepBuilder에도 BookStep으로 하게 되면, 'BookStep'이라는 빈이 충돌된다. 클래스의 빈이름을 바꿔주던가 스텝의 이름을 바꿔줘야한다.

나는 bookStep -> stepBook 으로 변경해서 사용했다.

 

 

문제 2

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. 2025-05-08T12:45:58.112+09:00 ERROR 42096 --- [batch] [ main] o.s.boot.SpringApplication : Application run failed org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [SELECT JOB_INSTANCE_ID, JOB_NAME FROM BATCH_JOB_INSTANCE WHERE JOB_NAME = ? and JOB_KEY = ?]

설정하고 실행하면 위와 같은 에러를 마주한다. Spring Batch 메타데이터 테이블(BATCH_JOB_INSTANCE 등)이 존재하지 않아서 실행에 실패한 상태다.

DDL을 참고하여 직접 생성(링크)하던지, yml 에 아래 조건을 임시로 넣고 자동 생성하도록 할 수 있다.

spring:
  batch:
    jdbc:
      initialize-schema: always

나는 한번 실행 후 주석처리 했다.

 

 

문제 3

2025-05-08T12:51:36.521+09:00 INFO 11352 --- [batch] [ main] .s.b.a.l.ConditionEvaluationReportLogger : Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. 2025-05-08T12:51:36.534+09:00 ERROR 11352 --- [batch] [ main] o.s.boot.SpringApplication : Application run failed java.lang.IllegalStateException: Failed to execute ApplicationRunner at org.springframework.boot.SpringApplication.lambda$callRunner$6(SpringApplication.java:796) ~[spring-boot-3.4.5.jar:3.4.5] at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:66) ~[spring-core-6.2.6.jar:6.2.6] at org.springframework.util.function.ThrowingConsumer$1.accept(ThrowingConsumer.java:86) ~[spring-core-6.2.6.jar:6.2.6] at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:797) ~[spring-boot-3.4.5.jar:3.4.5] at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:785) ~[spring-boot-3.4.5.jar:3.4.5] at org.springframework.boot.SpringApplication.lambda$callRunners$3(SpringApplication.java:773) ~[spring-boot-3.4.5.jar:3.4.5] at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183) ~[na:na] at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:357) ~[na:na] at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510) ~[na:na] at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na] at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150) ~[na:na] at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173) ~[na:na] at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na] at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) ~[na:na] at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:773) ~[spring-boot-3.4.5.jar:3.4.5] at org.springframework.boot.SpringApplication.run(SpringApplication.java:325) ~[spring-boot-3.4.5.jar:3.4.5] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1362) ~[spring-boot-3.4.5.jar:3.4.5] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1351) ~[spring-boot-3.4.5.jar:3.4.5] at io.github.haeun.batch.BatchApplication.main(BatchApplication.java:13) ~[main/:na] Caused by: org.springframework.batch.core.repository.JobExecutionAlreadyRunningException: A job execution for this job is already running: JobInstance: id=1, version=0, Job=[bookJob] at org.springframework.batch.core.repository.support.SimpleJobRepository.createJobExecution(SimpleJobRepository.java:145) ~[spring-batch-core-5.2.2.jar:5.2.2]

그리고 에러로 인해 자동으로 종료되면 상관없지만, 중간에 강제종료를 하면 이번엔 이런 에러가 뜬다.

이미 bookJob이라는 Job이 같은 파라미터로 실행된 상태이기 때문에, 다시 실행할 수 없다는 의미다.

 

아까 생성한 테이블(BATCH_JOB_EXECUTION, STEP_JOB_EXECUTION )을 확인해보면 아직 STARTED 상태인 걸 확인할 수 있다. 이걸 강제로 FAILED나 COMPLETED 처리하면 다시 재실행 가능하다. 또한 vm option에 `--run.id=20240508131400` 이런식으로 추가해도 되지만, 그때마다 업데이트하긴 너무 귀찮다!

 

@Configuration
@Slf4j
public class SequenceJobLauncher {

    public static void run(ConfigurableApplicationContext ctx, String[] args) {
        try {
            JobLauncher jobLauncher = (JobLauncher) ctx.getBean("jobLauncher");
            JobParameters jobParameters = new JobParametersBuilder()
                    .addLong("run.id", System.currentTimeMillis())
                    .toJobParameters();

            String[] jobNames = System.getProperty("spring.batch.job.names").split(",");

            for (String jobName : jobNames) {
                jobLauncher.run((Job) ctx.getBean(jobName), jobParameters);
            }
        } catch (Exception e) {
            log.error("error", e);
        }
    }
}
@SpringBootApplication
public class BatchApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = SpringApplication.run(BatchApplication.class, args);
        SequenceJobLauncher.run(ctx, args);
        System.exit(SpringApplication.exit(ctx));
    }
}

JobLauncher를 생성해서, jobParameters에 run.id 를 현재 시간으로 넣어주는 조건을 추가한다.

그리고 아직은 필요하지 않지만, job을 여러 개 실행시키고 싶을 때 콤마를 기준으로 실행될 수 있도록 추가했다. 하나만 실행할거라면 바로 property 를 넣으면 된다.

application에는 해당 런처를 실행하도록 한다.

 

 

문제 4

Cannot invoke "String.split(String)" because the return value of "java.lang.System.getProperty(String)" is null

원래라면 program arguments 에 job 옵션을 줬었으나, 실행 방법이 변경되면서 VM Option에 해당 조건을 줘야한다.

 

 

 

 

이제 Step을 전체적으로 구성한 후 실행하면 csv 에 있는 내용들이 인서트되는 걸 확인할 수 있다.

 

데이터 20만개 인서트하는데 13~15초 정도 걸렸다.

 

* StepExecuteTime 확인 쿼리

SELECT STEP_EXECUTION_ID,
       STEP_NAME,
       START_TIME,
       END_TIME,
       TIMESTAMPDIFF(SECOND, START_TIME, END_TIME) AS duration_seconds
FROM BATCH_STEP_EXECUTION
WHERE EXIT_CODE = 'COMPLETED'
ORDER BY START_TIME DESC;

 

항상 프로젝트를 하면서 생각하는거지만, 실무에서는 기본적으로 환경이 구성되어 있기 때문에 이렇게 잦은 에러들을 마주할 일이 잘 없다. (자잘한 에러까지 따지면 훨씬 많다..!) 

단순히 Spring Batch Job과 Step 을 생성해서 실행하는 거지만 혼자 처음부터 구성하려니 여기까지 실행시키기까지도 꽤 걸렸다. 이제 다음은 대용량인 bookRating이다. 이건 큰 문제 없이.. 문제가 있더라도 잘 해결할 수 있었으면 좋겠다.

 

 

[전체 Git Code 확인]

top