이전에 프로젝트를 진행하면서 매일 00시에 테이블을 일괄적으로 갱신해는 작업이 있었다. 경험이 부족했던 그땐 spring batch라는 기능의 존재만 알고 결국에는 스케쥴러로 구현했는데 결국 돌고돌아 spring batch를 맞이하게 되었다.
이 둘의 차이는 동작이 일어나는 행위를 하는 주체가 Spring batch이고 이를 유발하는 트리거가 스케줄러이다. 이전에는 작업할 때는 Spring batch를 사용하지 않고 스케줄러로 메서드를 실행하는 것으로 구현하였는데 이번에 공부하면서 보니 Spring batch의 장점과 활용도를 알게되었다.
Spring batch는 대용량의 작업을 처리하는데 사용된다. 이를 서비스를 제공하고 있는 서버에서 실행시키게 되면 해당 서버의 리소스를 엄청나게 잡아먹어 성능 저하 혹은 다운되는 악영향을 미칠수있다. 따라서 보통 배치 서버를 별도로 두고 배치 서버에서는 배치작업만 수행하도록 하게 작성한다.
이해를 위해서 예를 들어보자
시나리오1.
배치 작업으로 100억개의 데이터를 다뤄야 하는 일이 있다.
배치 작업이 99억개 까지 진행되다가 99억 1번째 데이터에서 오류가 발생해서 배치 작업이 실패로 돌아갔다.
실패한 작업을 재시작해야 하는데 처음부터 작업을 한다면 정말 비효율의 끝이다.
우리는 이런 상황에서 정확하게 실패가 발생한 99억 1번째 부터 다시 배치 작업을 수행하고 싶다.
일반적인 메서드에서 이를 트레킹해서 추적하는것은 매우 힘들다. 하지만 Spring Batch에서는 해당 기능을 제공해준다.
그렇기 때문에 배치 작업을 하는데 Spring Batch를 사용하는 것이다.
인터넷에 흔히 보이는 배치 조건인 대용량 데이터, 자동화, 견고성, 신뢰성, 성능 등의 조건이 있는데 나는 위의 조건으로 이해하고 적용하였다.
배치 작업에서 가장 큰 단위의 작업 수행 범위이다. 실질적인 작업이 실행되는 Step들을 담고 있는 컨테이너이다. 한개의 Job은 한개 이상의 Step을 가질수있다. 또한 Job안에는 작업의 이름, Step의 정의 및 순서, 작업의 재시작 가능 여부 등을 포함하고 있다. Job은 인터페이스인 기술 명세서이다. 이를 구현하는 구현체로는 가장 기본적인 SimpleJob이 있다.
Job을 참조하거나 식별하기 위한 데이터로 Spring Batch가 수행될 때 변수로 주어진다. Job Parameters를 기준으로 Job을 식별 할 수 있다.
Job Parameters를 받아서 실행한 각각의 Job을 구별하는 인스턴스이다. 쉽게 말해 Job이란 클래스에서 Job Parameters를 기준으로 Job Instance 인스턴스를 생성한것과 동일하다. 여기서 각각의 Job Instance는 고유하다. 따라서 같은 Job에 동일한 Job Parameters를 넣을수는 없다.(이렇게 생성된 인스턴스들을 구분 할 수 없기 때문에) 물론 기본 설정 값을 조작한다면 가능하긴하다.
Job을 실행시켜 생성된 Job Instance의 상태(성공, 실패, 진행중)를 저장한 메타 데이터이다.
Step은 Job의 독립적이고 순차적인 단계를 모듈화 시킨 객체이다. Step을 통해 내가 수행하고 싶은 작업을 정의하면 된다.
Job Execution과 동일하게 수행된 Step의 상태를 저장한 메타 데이터이다.
Spring batch에서 제어하는 테이블로 Key/Value 형태의 Collection으로 Job이나 Step의 정보를 저장한다. 이 부분에서 각 작업의 상태 정보를 저장하기 때문에 첨 부터 다시 시작할 필요없이 실패했던 부분 부터 작업을 시작 할 수 있다.
Spring batch는 일을 수행할 묶음인 Job아래 실질적인 작업이 수행되는 Step이 존재하고 Step은 값을 읽어오는 Reader, 읽어온 값을 조작하는 Processor, 조작된 값을 저장하는 Writer로 나뉜다.
Spring batch는 대용량의 데이터를 처리하는데 유리하다. 예를들어 만개의 작업을 하는데 이를 일괄 처리하면 실패 했을 때 처음부터 다시 실행해야한다. 하지만 만개를 천개씩 나눠서 작업한다면 실패가 발생한 그 부분 부터 실행하면 되기 때문에 실패 대처시 불필요한 작업을 줄일 수 있다.
이렇게 부분으로 나눠서 처리하는 양을 설정하는게 Chunk이다. Reader에서 데이터를 읽어오면 Processor에서 데이터를 처리하고 처리된 결과를 Writer를 통해서 출력해준다.
Reader에서 100개를 읽어오면 Processor에서 100개의 데이터를 개별 처리하고 처리된 데이터를 모았다가 더 이상 처리할 데이터가 없을 때 Writer로 List로 보내서 일괄 처리하는 형식이다.
Chunk는 배치의 트렌잭션 단위라고 생각하면 편하다. Chunk는 한번에 하나씩 데이터를 읽어서 원하는 만큼의 작업을 하나의 Chunk로 묶고 이렇게 데이터 묶음 단위인 Chunk를 단위로 커밋하고 트랜잭션을 수행한다. 트랜젹선의 특징 답게 Chunk에 묶여있는 데이터 중 하나라도 실패하면 전부 실패하고 롤백한다.
Step에서 다룰 데이터를 읽어오는 역할을 한다. 데이터를 읽는데 File,Xml,Json등의 파일을 읽을 수 있고 필요하다면 직접 커스텀하여 데이터를 읽을 수 있다.
@Bean
@StepScope
public JpaPagingItemReader<(가져올 Entity name)> jpaPagingItemReader() {
log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 데이터 읽기");
return new JpaPagingItemReaderBuilder<Pay>()
.name("(원하는 빌더 이름)")
.entityManagerFactory(entityManagerFactory)
.pageSize((한번에 가져올 양!))
.queryString("SELECT p FROM Pay p ")
.build();
}
JpaPagingItemReaderBuilder를 통해서 Jpa로 ItemReader를 생성한다. 여기서 중요한 점은 직접 Builder에 entityManagerFactory를 생성해야한다.
Reader에서 읽어온 데이터를 처리하는 역할을한다.
@Bean
@StepScope
public ItemProcessor<(Reader에서 받아오는 타입), (Writer에 보낼 타입)> processor() {
log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>. 데이터 변환");
return Pay::getAmount;
}
Processor에서 처리한 데이터를 출력하는 역할을 수행합니다.
@Bean
@StepScope
public ItemWriter<Long> writer(@Value("#{jobParameters[requestDate]}") String requestData) {
log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 데이터 전환 시작");
return items -> {
long sum = 0L;
for(Long item : items){
sum +=item;
}
Calculate calculate = new Calculate(sum,requestData);
calculateRepository.save(calculate);
};
}
gitHub : https://github.com/DongHyunKIM-Hi/springbatchtest