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이다. 이건 큰 문제 없이.. 문제가 있더라도 잘 해결할 수 있었으면 좋겠다.
'프로젝트 > 대용량 이관' 카테고리의 다른 글
[대용량 이관] DB 성능 튜닝 (InnoDB Buffer Pool) (0) | 2025.05.22 |
---|---|
[대용량 이관] bookRatingStep 개선 (0) | 2025.05.13 |
[대용량 이관] ItemWriter 선택 (JPA vs JDBC vs MyBatis) (1) | 2025.05.07 |
[대용량 이관] 데이터 찾기 (Kaggle) (0) | 2025.05.07 |