스프링 부트 배치는 대용량 데이터를 처리하는 기술로만 알고 있어서, 이번 기회에 한번 개념만 살펴보았습니다.
스프링 부트 배치를 왜 사용하는지 장점부터 살펴보았습니다.
일반적으로 스프링 부트 배치의 절차는 읽기 -> 처리 -> 쓰기
를 따릅니다.
- 읽기(read): 데이터 저장소(일반적으로 데이터 베이스)에서 특정 데이터 레코드를 읽습니다.
- 처리(processing): 원하는 방식으로 데이터를 가공/처리합니다.
- 쓰기(write): 수정된 데이터를 다시 저장소(데이터베이스)에 저장합니다. 혹은 외부 API를 통해 내보내기도 합니다.
Job과 Step은 1:M, Step과 ItemReader, ItemProcessor, ItemWriter는 1:1의 관계를 가집니다. 즉, Job이라는 하나의 큰 일감(Job)에 여러 단계(Step)을 두고, 각 단계를 배치의 기본 흐름대로 구현합니다.
스프링 부트 배치 용어들에 대해서 간단하게 개념정리를 해봤습니다.
Job은 배치 처리 과정을 하나의 단위로 만들어 표현한 객체입니다. 전체 배치 처리에 있어 항상 최상단 계층에 있습니다. 스프링 배치에서 Job 객체는 여러 Step 인스턴스를 포함하는 컨테이너 입니다.
@Slf4j
@RequiredArgsConstructor
@Configuration
public class SimpleJobConfiguration {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job simpleJob() {
// simpleJob이라는 이름을 가진 Job을 생성할 수 있는 JobBuilder 인스턴스 반환
return jobBuilderFactory.get("simpleJob")
// simpleJobBuilder 인스턴스 반환
.start(simpleStep1())
// simpleJob이라는 이름을 가진 Job 인스턴스 반환
.build();
}
}
보통 배치 Job 객체를 만드는 빌더는 여러 개 있습니다. 여러 빌더를 통합 처리하는 공장인 JobBuiderFactory 객체로 원하는 Job을 손쉽게 만들 수 있습니다. JobBuilderFactory의 get() 메서드를 호출하여 JobBuilder를 생성하고 이를 이용합니다.
JobBuildr의 메서드들을 까보면 모든 반환 타입이 빌더입니다. 아무래도 비즈니스 환경에 따라서 Job 생성 방법이 모두 다르기 때문에 별도의 구체적인 빌더를 구현하고 이를 통해 Job 생성이 이루어지게 하려는 의도가 아닌가 싶습니다.
JobInstance는 배치에서 Job이 실행될 때 하나의 Job 실행 단위입니다. 만약 하루에 한 번씩 배치의 Job이 실행된다면 어제와 오늘 실행한 각각의 Job을 JobInstance라고 부를 수 있습니다.
만약 JobInstance가 Job 실행이 실패하면 JobInstance가 끝난 것이 아닙니다. 이 경우 JobExecution이 실패 정보를 가지고 있고, 성공하면 JobInstance는 끝난 것으로 간주합니다. 그리고 성공한 JobExecution을 가져서 총 두개를 가지게 됩니다. 여기서 JobInstance와 JobExecution은 부모와 자식관계로 생각하면 됩니다.
JobParameters는 Job이 실행될 때 필요한 파라미터들을 Map 타입으로 저장하는 객체입니다.
JobParameters는 JobInstance를 구분하는 기준이 되기도 합니다. 예를 들어서 Job 하나를 생성할 때 시작 시간 등의 정보를 파리미터로 해서 하나의 JobInstance를 생성합니다. 즉 JobInstance와 JobParameters는 1:1 관계입니다. 파라미터 타입으로는 String, Long, Date, Double
를 사용할 수 있습니다.
Job에 JobExecution이라는 Job 실행 정보가 있따면 Step에는 StepExecution이라는 Step 실행 정보를 담는 객체가 있습니다. 각각의 Step이 실행될 때마다 StepExecution이 생성됩니다.
JobRepository는 배치 처리 정보를 담고 있는 메커니즘입니다. 어떤 Job이 실행되었으며 몇 번 실행되었고 언제 끝났는지 등 배치 처리에 대한 메타 데이터를 저장합니다.
예를 들어 Job 하나가 실행되면 JobRepository에서는 배치 실행에 관련된 정보를 담고 있는 도메인인 JobExecution을 생성합니다.
JobRepository는 Step의 실행 정보를 담고 있는 StepExecution도 저장소에 저장하며 전체 메타데이터를 저장/관리하는 역할을 수행합니다.
JobLauncher는 Job, JobParameters와 함께 배치를 실행하는 인터페이스입니다. 인터페이스의 메서드는 run() 메서드 하나입니다.
public interface JobLauncher {
public JobExecution run(Job job, JobParameters jobParameters) throw ...
}
run() 메서드는 매개변수로 Job과 JobParameters를 받아 JobExecution을 반환합니다. 만약 매개변수가 이전과 동일하면서 이전에 JobExecution이 중단된 적이 있다면 동일한 JobExecution을 반환합니다.
ItemReader는 Step의 대상이 되는 배치 데이터를 읽어오는 인터페이스입니다. FILE, XML, DB 등 여러 타입의 데이터를 읽어올 수 있습니다.
public interface ItemReader<T> {
T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException;
}
ItemReader에서 read() 메서드의 반환 타입을 제네릭으로 구현했기 때문에 직접 타입을 지정할 수 있습니다.
ItemProcessor는 ItemReader로 읽어온 배치 데이터를 변환하는 역할을 수행합니다.
ItemProcessor가 존재하는 이유는 비즈니스 로직을 분리하기 위해서입니다. ItemWriter는 정말 저장만 수행하고, ItemProcessor 로직 처리만 수행해 역할을 명확하게 분리하여 유지보수성을 용이하게 합니다. 또 다른 이유는 읽어온 배치 데이터와 쓰여질 데이터 타입이 다를 경우 대응하기 위해서입니다. 명확한 인풋과 아웃풋을 ItemProcessor로 구현해놓는다면 더 직관적인 코드가 됩니다.
public interface ItemProcessor<I, O> {
O process(I item) throws Exception;
}
ItemWriter는 배치 데이터를 저장합니다. 일반적으로 DB나 파일에 저장합니다.
public interface ItemWriter<T> {
public write<(List<? extends T> items) throws Exception;
}
ItemWriter와 ItemReader와 비슷한 방식으로 구현하면 됩니다. 제네릭으로 원하는 타입을 받습니다. write() 메서드는 List 자료구조를 사용해 지정한 타입이 리스트를 매개변수로 받습니다. 리스트 데이터 수는 설정한 청크 단위로 불러옵니다. write() 메서드의 반환 값은 따로 없고 매개 변수로 받은 데이터를 저장하는 로직을 구현하면 됩니다.