개발놀이터

멀티스레드 환경에서의 스프링 배치 : Multi-threaded Step 본문

Spring/Spring Batch

멀티스레드 환경에서의 스프링 배치 : Multi-threaded Step

마늘냄새폴폴 2022. 10. 22. 03:18

본 포스팅은 인프런의 정수원님의 스프링 배치 강의를 듣고 정리한 포스팅입니다. 더 자세한 내용은 강의를 참고해주세요. 

 

Multi-threaded Step

기본 개념

  • Step내에서 멀티 스레드로 Chunk 기반 처리가 이루어지는 구조
  • TaskExecutorRepeatTemplate이 반복자로 사용되며 설정한 개수 만큼의 스레드를 생성하여 수행한다. 

이를 그림으로 알아보면 다음과 같습니다. 

 

기존에는 TaskletStep이 RepeatCallback을 수행하고 이 때 ChunkOrientedTasklet이 수행됩니다. 

 

하지만 Multi-threaded Step 에서는 TaskExecutorRepeatTemplate이 Runnable을 실행하고 그 안에서 RepeatCallback이 수행됩니다. 

 

이 부분이 원래 싱글 스레드로 동작할 때와 다른 점입니다. 

 

또한 멀티 스레드 환경에서 ChunkOrientedTasklet을 수행하는 경우 주의해야 하는 점이 ItemReader가 Thread-safe한지를 체크해야 합니다. 만약 Thread-safe 하지 않다면 데이터 처리에 문제가 생길 수도 있습니다. 

 

 

Multi-threaded Step 에서 실행 순서를 보면

  1. Job이 TaskletStep을 실행합니다.
  2. TaskletStep이 TaskExecutorRepeatTemplate을 실행합니다.
  3. TaskExecutorRepeatTemplate이 TaskExecutor를 이용해 스레드를 생성합니다.
  4. 각각의 스레드는 새로 생성되어 Runnable에 접근합니다. 동시에 ChunkOrientedTasklet을 실행합니다.
  5. 각 스레드는 ItemReader, ItemProcessor, ItemWriter에 접근해 데이터를 가공후 처리합니다. 

이 때 ItemReader, ItemProcessor, ItemWriter는 서로 데이터가 공유되지 않고 Chunk를 스레드마다 새로 생성하기 때문에 Thread-safe 합니다. 

 

하지만 각각의 스레드가 ItemReader를 공유하기 때문에 ItemReader가 DB에서 데이터를 가져올 때 동시성 문제가 발생할 수 있기 때문에 ItemReader의 Thread-safe는 따로 신경써야 합니다. 

 

 

사용 방법

Multi-threaded Step 을 사용하는 방법은 아주 간단합니다. 앞선 시간 포스팅 했던 AsyncItemProcessor 만큼 간단하고 쉽죠

 

우선 Job을 만들어줍니다. 이 때 주의해야 하는 점은 앞서 말했듯이 Thread-safe한 ItemReader를 만들어야 합니다. ItemReader 중에선 PagingItemReader가 Thread-safe합니다. CursorItemReader는 안됩니다. 

 

저는 JpaPagingItemReader를 사용했고 ItemProcessor는 직접 구현했습니다. ItemWriter는 JpaItemWriter를 사용했습니다. 

 

Step을 build 할 때 taskExecutor() API를 이용해 taskExecutor를 만들어주면 됩니다. 

 

@Bean
public Step jpaStep() {
    return stepBuilderFactory.get("jpaStep")
            .<Member, Member>chunk(chunk)
            .reader(jpaReader())
            .processor(jpaProcessor())
            .writer(jpaWriter())
            .taskExecutor(taskExecutor())
            .build();
}

taskExecutor는 디폴트가 SyncTaskExecutor입니다. 따라서 디폴트 생성자로 만들어주면 사용하는 의미가 없습니다. 따라서 개발자가 따로 만들어준 TaskExecutor를 사용해야 합니다. 

 

TaskExecutor는 이렇게 생겼습니다. 

 

@Bean
public TaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(4);
    taskExecutor.setMaxPoolSize(8);
    taskExecutor.setThreadNamePrefix("async-thread");

    return taskExecutor;
}

 

TaskExecutor는 우선 CoreThread를 몇개로 생성할 것인지 선언해 주어야 합니다. 우선 저는 네개로 설정했구요. MaxPoolSize는 Thread가 지연됐을 경우 스레드를 추가로 생성해주는 API입니다. 최대 8개까지 만드는 것으로 설정했습니다. 

 

그리고 스레드 이름을 정해주고 Bean으로 만들어주면 끝입니다. 이 상태로 데이터 1000건에 대해 얼마나 걸리는지 테스트를 해보도록 하겠습니다. 

 

기존 싱글 스레드로 1000건의 데이터를 테스트 해봤을 때는 

 

32.661초가 걸렸습니다. 

 

이렇게 오래 걸린 이유는 아무래도 JpaItemWriter가 단건으로 데이터를 업데이트 하기 때문에 그럴 것입니다. 더 나은 성능을 원하신다면 JdbcBatchItemWriter를 사용하시는 것을 추천드립니다. 둘의 차이에 대해 비교 분석하고 테스트 해본 포스팅은 아래에 있습니다. 

 

https://coding-review.tistory.com/203

 

Chunk 지향 처리 : ItemWriter (심화)

본 포스팅은 인프런의 정수원님의 스프링 배치 강의를 듣고 정리한 포스팅입니다. 더 자세한 내용은 강의를 참고해주세요. ItemReader와 마찬가지로 ItemWriter 또한 Flat File이나 XML, json과 같이 데이

coding-review.tistory.com

 

같은 멀티 스레드 방식인 AsyncItemProcessor, AsyncItemWriter에서는 ItemReader, ItemWriter에서 생기는 성능 문제를 해결해주지 못했습니다. 

 

이 문제에 대해 자세히 포스팅 되어있는 아래의 링크를 확인해주시면 감사합니다.

 

https://coding-review.tistory.com/206

 

멀티 스레드 환경에서의 스프링 배치 : AsyncItemProcessor/AsyncItemWriter

본 포스팅은 인프런의 정수원님의 스프링 배치 강의를 듣고 정리한 포스팅입니다. 더 자세한 내용은 강의를 참고해주세요. AsyncItemProcessor/AsyncItemWriter 기본 개념 Step 안에서 ItemProcessor 가 비동기

coding-review.tistory.com

 

아무래도 AsyncItemProcessor, AsyncItemWriter는 Process 과정에서만 멀티 스레드로 운영되기 때문에 그런 것 같았습니다. 따라서 위의 포스팅에서 결론적으로 Process 과정이 오래 걸리는 작업 (Ex. 이메일 보내기) 에서 큰 효과를 볼 것으로 기대했습니다. 

 

하지만 이번 Multi-threaded Step은 하나의 Chunk 과정을 스레드로 각각 나눠 작업하는 것이기 때문에 성능에 문제가 있는 부분을 어느정도 커버할 수 있을 것이라고 추측했습니다.

 

 

실제로 테스트를 돌려보니 네개의 스레드로 돌렸기 때문에 정확히 네배 성능이 좋아진 것을 확인할 수 있었습니다. 

 

 

정리

이렇게 Multi-threaded Step에 대해서 학습해보고 테스트까지 해봤습니다. 

 

AsyncItemProcessor, AsyncItemWriter 와 Multi-threaded Step 은 각각 장단점이 확실히 있는 것 같습니다. 

 

현업에서는 어떻게 사용하는지 모르겠지만 제가 프로젝트에서 실전으로 사용해야 한다면 Process가 오래 걸리는 작업에는 AsyncItemProcessor를, 일반적인 상황에서는 Multi-threaded Step을 사용할 것 같습니다. 

 

스레드를 몇개까지 늘릴 수 있는지는 테스트 해보지 못했지만 CPU의 스레드 개수만큼 늘어나는 것이 최대일 것으로 추측됩니다. 

 

Multi-threaded Step은 앞선 튜닝에서와 다르게 드라마틱한 성능향상이 이뤄지진 않았습니다. 따라서 대중적으로 사용될 수는 있지만 정말 압도적인 성능 향상을 원한다면 ItemReader에는 QuerydslNoOffsetPagingItemReader를 사용하고, ItemProcessor에는 AsyncItemProcessor를 사용하고, ItemWriter에는 jdbcBatchItemWriter를 사용하면 될 것 같습니다. 

 

하지만 보통의 대용량 배치 작업은 트래픽이 많지 않은 새벽에 돌아간다는 점으로 미루어 봤을 때 굳이 그렇게까지 성능에 집착해야 되나 싶긴 합니다. 

 

아무튼 여기까지 Multi-threaded Step 이었습니다. 다음 포스팅에서는 다른 멀티스레드 환경에서의 배치를 알아보도록 하겠습니다. 긴 글 읽어주셔서 감사합니다.