이번 포스팅은 인턴 기간 중 맡았던 과제에 대한 리뷰 및 복기 목적 하에 작성합니다.
최근 스프링 부트 3.0으로 업그레이드 되면서 Spring Batch에서도 많은 변화가 있었습니다.
현재 스프링 배치는 5점대입니다.
기존의 많은 블로그 포스팅이나 자료에서는 스프링 2점대 기반의 코드들이었기에 이번 실습을 하면서 어려움이 있었습니다.
혹시 저처럼 해매고 계실 분이 계실까 해 개선한 코드를 공유하고자 합니다.
우선 배치라는 개념이 무엇일까요?
이번에 실습하며 배치라는 개념에 대해 새로 알게되었습니다.
개념에 대한 정의에 설명하기 앞서 예시를 이해해봅시다.
만약 매일 아침 5시에 어제 전체 데이터를 집계 처리해야한다고 가정해봅시다.
저라면, 그냥 Tomcat이랑 Spring MVC 등의 웹 어플리케이션을 활용해 구현할 것 같습니다.
하지만 그렇게 된다면 문제들이 있습니다.
1. 이러한 처리과정의 데이터들은 큰 경우가 많기에 이러한 데이터를 읽고 가공하고 저장한다면
해당 서버는 순식간에 CPU나 I/O 등의 자원을 다 써버려서 다른 요청을 처리하지 못하게 됩니다.
2. 1번 상황처럼 데이터가 너무 크고 많아서 실패가 났다 가정해봅시다.
6만번째 데이터에서 실패 처리가 되었다면, 저는 해당 부분부터 다시 실행하고 싶어집니다.
3. 웹 어플리케이션의 경우 같은 Parameter로 같은 함수를 실행해도 항상 정상 작동이 됩니다.
하지만 지금처럼 데이터를 처리하는 집계 함수를 아침에 A라는 사람이 실행했는데 B라는 사람이 몇 분뒤에 또 실행한다면 필요없는 데이터가 증가해 집계 데이터가 2배로 늘어나 문제가 될 수 있습니다.
데이터 양이 작으면 문제가 안되지만 데이터가 지금처럼 큰 경우에는 문제가 생기게 됩니다.
이러한 문제들을 해결하고자 나온 것이 Batch 입니다.
단발성으로, 대용량의 데이터를 처리하는 어플리케이션을 Batch 어플리케이션이라 합니다.
Batch Application은 해당 조건들을 만족해야합니다. 암기할 필요는 없고 위의 문제들을 해결하는 관점에서 생각해보면
당연한 것들입니다.
- 대용량 데이터를 가져오고, 전달하고, 계산하는 등의 처리에 있어서 문제가 없어야 합니다.
- 심각한 문제 해결을 제외하고는, 사용자가 따로 개입할 필요 없이 자동으로 실행되어야 합니다.
- 잘못된 데이터를 충돌하거나 중단 없이 처리할 수 있어야 합니다.
- 로깅이나 알람 등을 통해서 문제가 생겼을 때 무엇이 잘못되었는지 추적할 수 있어야 합니다.
- 지정한 시간 안에 처리가 되면서 동시에 실행되는 다른 어플리케이션을 방해하지 않아야 합니다.
그렇다면 Spring Batch는 무엇일까요?
Spring Batch는 위에 언급한 Batch를 Spring에서 사용하기 편하게 지원해줍니다.
이제 나머지 설명들은 코드를 같이 보면서 설명해보겠습니다.
@RequiredArgsConstructor
@Configuration
public class BatchJob {
private final ApiService apiService;
@Bean(name="apiJob")
public Job apiJob(JobRepository jobRepository, PlatformTransactionManager platformTransactionManager){
return new JobBuilder("apiJob", jobRepository)
.start(loginStep(jobRepository, platformTransactionManager))
.next(getUserApiStep(jobRepository,platformTransactionManager))
.next(getDeptApiStep(jobRepository,platformTransactionManager))
.build();
}
@Bean
public Step loginStep(JobRepository jobRepository, PlatformTransactionManager platformTransactionManager){
return new StepBuilder("loginStep", jobRepository)
.tasklet(new LoginTasklet(apiService), platformTransactionManager)
.build();
}
@Bean
public Step getUserApiStep(JobRepository jobRepository, PlatformTransactionManager platformTransactionManager){
return new StepBuilder("getUserApiStep", jobRepository)
.tasklet(new getUserApiTasklet(apiService), platformTransactionManager)
.build();
}
@Bean
public Step getDeptApiStep(JobRepository jobRepository, PlatformTransactionManager platformTransactionManager){
return new StepBuilder("getDeptApiStep", jobRepository)
.tasklet(new getDeptApiTasklet(apiService), platformTransactionManager)
.build();
}
}
저기 Service는 제가 그냥 만든 기능이니 무시하셔도 좋고..
여기서 주의 깊게 봐야할 것들은 'Job', 'Step', 'tasklet' 입니다.
Spring Batch에서 Job은 하나의 배치 작업 단위를 의미합니다.
Job 안에는 여러 개의 Step이 존재하고, Step 안에는 Tasklet 또는 {Reader, Processor, Writer } 묶음이 존재합니다.
제가 작성한 기능의 경우 따로 Reader Processor Writer을 구분할 필요성을 느끼지 못해 Tasklet으로 하나로 처리했습니다.
우선 그 전에, Spring Batch를 사용하려면 설정들이 필요합니다.
boot-starter-batch 등의 라이브러리도 의존성에 추가해줘야하고(이건 아마 다들 아실 것 같아서 패스)
제가 처음 해맸던 부분은, 만약 관계형 DB 등을 사용한다면 메타 데이터 테이블을 넣어줘야한다는 겁니다.
메타 데이터는 쉽게, 데이터를 설명하는 데이터라 이해하시면 됩니다.
Spring Batch의 메타 데이터에는
- 이전에 실행한 Job이 어떤 것들이 있는지
- 최근 실패한 Batch Parameter가 어떤 것들이 있고, 성공한 Job은 어떤 것들이 있는지
- 다시 실행하면 어디서부터 다시 실행해야하는지
- 어떤 Job에 어떤 Step들이 있고, Step들 중에서도 성공한 Step이 무엇이고 실패한 Step은 무엇이 있는지
등이 있습니다.
실제 이러한 메타 데이터 테이블들을 DB안에 넣어줘야합니다.
application.properties에 실행할 때마다 자동으로 스키마 테이블을 만드는 기능을 설정해줄 수도 있지만,
수동으로 넣을 수 있는 방법을 설명해보려 합니다.
이러한 테이블들의 스키마는 Spring Batch가 DB들에 맞춰서 각각 존재하기에,
저희는 Spring에서 파일 검색으로 'schema-DBMS이름'으로 찾아서 해당 파일 내용(테이블 생성 sql)을 DB에 넣어주면 됩니다.
intelliJ를 쓰신다면, 코드 화면에서 shift 두번 -> Files에 'schema-DBMS이름'으로 찾으실 수 있습니다.
저는 PostgreSQL을 사용했기에, 'schema-postgresql.sql'파일을 찾아서 DB에 넣어줬습니다.
이 테이블들 중에서 좀 중요하게 봐야할 테이블은 두가지입니다.
- JOB_INSTANCE
- JOB_INSTANCE 테이블은 Job Parameter에 따라 생성되는 테이블입니다.
Job Parameter는 Spring Batch가 실행될 때 외부에서 받을 수 있는 파라미터를 의미합니다.
같은 Batch Job이라도 Job Parameter가 다르면 JOB_INSTANCE 테이블에는 기록이 됩니다.
우리가 Job Parameter를 넣어주려면 코드로는
@Value("#{jobParameters[변수이름]}") 로 하면 됩니다.
그리고 Program Arguments에 변수이름=값 식으로 넣어주면 됩니다.
- JOB_INSTANCE 테이블은 Job Parameter에 따라 생성되는 테이블입니다.
- JOB_EXECUTION
- JOB_EXECUTION과 JOB_INSTANCE는 부모 자식 관계입니다.
JOB_EXECUTION은 자신의 부모인 JOB_INSTANCE가 성공/실패했던 모든 내역을 가지고 있습니다.
그렇기에 동일한 Job Parameter로 2번 실행해도 같은 파라미터로 실행되었다는 에러가 발생하지 않는다.
즉, Spring Batch는 동일한 Job Parameter로 성공한 기록이 있을 때에만 재수행이 안됨을 알 수 있습니다!
- JOB_EXECUTION과 JOB_INSTANCE는 부모 자식 관계입니다.
이제 메타 테이블에 대한 내용은 여기까지로 하고, 다시 Job & Step으로 돌아가봅시다.
Step은 실제 Batch 작업을 수행하는 역할을 합니다.
실제로 Batch 비즈니스 로직을 처리하는 기능은 모두 Step에서 구현되어 있습니다.
Batch 처리 내용들을 담다 보니, Job 내부에서 Step들간의 순서 또는 처리 흐름을 제어할 필요가 있습니다.
다시 코드를 봅시다.
@Bean(name="apiJob")
public Job apiJob(JobRepository jobRepository, PlatformTransactionManager platformTransactionManager){
return new JobBuilder("apiJob", jobRepository)
.start(loginStep(jobRepository, platformTransactionManager))
.next(getUserApiStep(jobRepository,platformTransactionManager))
.next(getDeptApiStep(jobRepository,platformTransactionManager))
.build();
}
@Bean
public Step loginStep(JobRepository jobRepository, PlatformTransactionManager platformTransactionManager){
return new StepBuilder("loginStep", jobRepository)
.tasklet(new LoginTasklet(apiService), platformTransactionManager)
.build();
}
@Bean
public Step getUserApiStep(JobRepository jobRepository, PlatformTransactionManager platformTransactionManager){
return new StepBuilder("getUserApiStep", jobRepository)
.tasklet(new getUserApiTasklet(apiService), platformTransactionManager)
.build();
}
@Bean
public Step getDeptApiStep(JobRepository jobRepository, PlatformTransactionManager platformTransactionManager){
return new StepBuilder("getDeptApiStep", jobRepository)
.tasklet(new getDeptApiTasklet(apiService), platformTransactionManager)
.build();
}
우선 서비스를 작성할 때 저는 무조건 LoginStep을 먼저 실행해야하고, 그 이후에 UserApiStep과 DeptApiStep을 실행시켜야했습니다. 그렇기에 next라는 문법을 활용해 순차적으로 실행할 수 있게 했습니다.
그렇다면 예를 들어, if문처럼 조건 처리를 해줘야하는 상황이면 어떻게 해야할까요?
StepA를 실행해서 성공이면 StepB를 실행하고, 실패하면 StepC를 실행해야 한다고 가정해봅시다.
@Bean
public Job StepNextConditionalJob(JobRepository jobRepository, PlatFormTransactionManager transactionManager){
return new JobBuilder("stepNextconditionalJob")
.start(StepA(jobRepository, transactionManager)
.on("FAILED") // FAILED인 경우
.to(StepC(jobRepository,transactionManager) // StepC로 이동
.on("*") // StepC의 결과와 관계없이
.end() // StepC로 이동하면 Flow 종료
.from(StepA(jobRepository,transactionManager) // StepA로부터
.on("*") // FAILED 외의 모든 경우에
.to(StepB(jobRepository,transactionManager) // StepB로 이동
.on("*") // StepB의 결과와 관계없이
.end() // StepB로 이동하면 Flow 종료
.end() // Job 종료
.build();
}
이렇게 on 과 to 구문을 활용해주면 됩니다.
그럼 이제 제가 어떻게 Tasklet을 구현했는지 설명해보겠습니다.
@RequiredArgsConstructor
@Component
public class getUserApiTasklet implements Tasklet {
private final ApiService apiService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
List<ResponseUserDto> userApi = apiService.getUserApi();
return RepeatStatus.FINISHED;
}
}
Tasklet 인터페이스를 구현해 execute 함수를 오버라이딩 해주면 됩니다.
execute 함수 안에 하고자 하는 기능을 넣어주면 됩니다.
이제 이 배치들을 어떻게 스케쥴링 해서 동작시킬 것인지 고민해봐야합니다.
배치에서 지원하는 @Scheduled 어노테이션도 있지만, 저는 quartz 라는 스케쥴러를 활용했기에 다음 포스팅에서는 quartz로 어떻게 구현했는지 작성해보겠습니다.
글을 작성하며 많은 도움을 받은 포스팅들을 출처에 올립니다.
* 출처 :
https://jojoldu.tistory.com/326?category=902551
https://alwayspr.tistory.com/49
'스프링 정리' 카테고리의 다른 글
테스트코드에서의 @Transactional (0) | 2023.10.30 |
---|---|
Spring Batch - @PersistJobDataAfterExecution (0) | 2023.04.04 |
Spring Security & JWT(Json Web Token) 활용 예제 (0) | 2023.03.28 |
Spring Batch & Quartz 활용해보기 (0) | 2023.03.24 |
Spring Security - authentication /authorization (0) | 2023.03.05 |