스프링 배치의 모든 잡은 실패하거나 중지될 때 다시 실행할 수 있었다. 스프링 배치는 기본적으로 이렇게 동작하므로 우리는 다시 실행하면 안되는 잡이 있을시 재시작을 방지해야한다.
preventRestart()
preventRestart() 메서드를 호출하여 잡이 실패하거나 어떤 이유로 중지된 경우에도 다시 실행할 수 없다.
/**
* 잡 실행
* @return
*/@Bean
public Job transactionJob() {
return this.jobBuilderFactory.get("transactionJob")
.preventRestart() /* 잡은 기본적으로 재시작이 가능하도록 구성되어있다. 잡의 재시작 방지 */.start(...)
.next(...)
.build();
}
잡의 재시작 횟수 제한
startLimit(n) : 재시작 횟수를 n번으로 제한한다.
@Bean
public Step importTransactionFileStep() {
return this.stepBuilderFactory.get("importTransactionFileStep")
.startLimit(2) /* 잡의 재시작 횟수 제한 */
.<Transaction, Transaction>chunk(100)
.reader(...)
.writer(...)
.listener(...) /* 스텝 빌드하기 전 실행할 리스너 등록 */.build();
}
완료된 스텝 재실행하기
allowStartIfComplete(true)
스텝이 잘 완료됐더라도, 다시 실행할 수 있어야 할때 사용한다.주의할 점은, 잡의 ExitStatus 가 COMPLETE 라면 모든 스텝에allowStartIfComplete(true) 를 적용하더라도 이와 관계없이 잡 인스턴스는 다시 실행할 수 없다.
잡이 재실행될때 무조건 실행되어야할 스텝이 존재할 경우 설정한다.
@Bean
public Step importTransactionFileStep() {
return this.stepBuilderFactory.get("importTransactionFileStep")
.<Transaction, Transaction>chunk(100)
.reader(...)
.writer(...)
.allowStartIfComplete(true) /* 잡이 재시작될시, 스텝이 다시 실행될 수 있도록 재시작 허용 */.listener(...) /* 스텝 빌드하기 전 실행할 리스너 등록 */.build();
}
일반적으로 Spring Batch는 단일 쓰레드에서 실행됩니다. 즉, 모든 것이 순차적으로 실행되는 것을 의미하는데요. Spring Batch에서는 이를 병렬로 실행할 수 있는 방법을 여러가지 지원합니다. 이번 시간에는 그 중 하나인 멀티스레드로 Step을 실행하는 방법에 대해서 알아보겠습니다.
Spring Batch의 멀티쓰레드 Step은 Spring의TaskExecutor를 이용하여각 쓰레드가 Chunk 단위로 실행되게하는 방식입니다.
여기서 어떤TaskExecutor를 선택하냐에 따라 모든 Chunk 단위별로 쓰레드가 계속 새로 생성될 수도 있으며 (SimpleAsyncTaskExecutor) 혹은 쓰레드풀 내에서 지정된 갯수의 쓰레드만을 재사용하면서 실행 될 수도 있습니다. (ThreadPoolTaskExecutor)
Spring Batch에서 멀티쓰레드 환경을 구성하기 위해서 가장 먼저 해야할 일은 사용하고자 하는Reader와 Writer가 멀티쓰레드를 지원하는지확인하는 것 입니다.
JpaPagingItemReader의 Javadoc
각 Reader와 Writer의 Javadoc에 항상 저thread-safe문구가 있는지 확인해보셔야 합니다. 만약 없는 경우엔 thread-safe가 지원되는 Reader 와 Writer를 선택해주셔야하며, 꼭 그 Reader를 써야한다면SynchronizedItemStreamReader등을 이용해thread-safe로 변환해서 사용해볼 수 있습니다.
그리고 또 하나 주의할 것은 멀티 쓰레드로 각 Chunk들이 개별로 진행되다보니 Spring Batch의 큰 장점중 하나인 실패 지점에서 재시작하는 것은 불가능 합니다. 이유는 간단합니다. 단일 쓰레드로 순차적으로 실행할때는 10번째 Chunk가 실패한다면 9번째까지의 Chunk가 성공했음이 보장되지만, 멀티쓰레드의 경우 1~10개의 Chunk가 동시에 실행되다보니 10번째 Chunk가 실패했다고 해서 1~9개까지의 Chunk가 다 성공된 상태임이 보장되지 않습니다.
그래서 일반적으로는 ItemReader의 saveState 옵션을 false 로 설정하고 사용합니다.
이건 예제 코드에서 설정을 보여드리겠습니다.
자 그럼 실제로 하나씩 코드를 작성하면서 실습해보겠습니다.
2. PagingItemReader 예제
가장 먼저 알아볼 것은 PagingItemReader를 사용할때 입니다. 이때는 걱정할 게 없습니다. PagingItemReader는Thread Safe하기 때문입니다.
멀티 쓰레드로 실행할 배치가 필요하시다면 웬만하면 PagingItemReader로 사용하길 추천드립니다.
1) 부모 Entity를 insert 하고 생성된 Id 반환 2) 자식 Entity에선 1) 에서 생성된 부모 Id를 FK 값으로 채워서 insert
위 과정를 진행하는 쿼리를 모아서 실행하는게 Hibernate의 방식인데, 이때 Batch Insert과 같은 대량 등록의 경우엔 이 방식을 사용할 수가 없습니다. (부모 Entity를 한번에 대량 등록하게 되면, 어느 자식 Entity가 어느 부모 Entity에 매핑되어야하는지 알 수 없겠죠?)
그럼 ID 생성 전략을 Auto Increment가 아닌 Table (Sequence)를 선택하면 되지 않을까 생각하게 되는데요. 아래 글에서 자세하게 설명하고 있지만,성능상 이슈와Dead Lock에 대한 이슈로 Auto Increment를 강력하게 추천합니다.
@Testpublicvoidnon_auto_increment_test_jdbc()throws Exception {
//given
JdbcBatchItemWriter<Person2> writer = new JdbcBatchItemWriterBuilder<Person2>()
.dataSource(dataSource)
.sql("insert into person(id, name) values (:id, :name)")
.beanMapped()
.build();
writer.afterPropertiesSet();
List<Person2> items = new ArrayList<>();
for (long i = 0; i < TEST_COUNT; i++) {
items.add(new Person2(i, "foo" + i));
}
// when
writer.write(items);
}
1만건을 요청하는 위 코드를 직접 MySQL에 요청을 해보면?
0.586초라는 JpaItemWriter에 비해 압도적인 성능을 보여줍니다.
3-2. Auto Increment 성능
그럼 Auto Increment일 경우엔 어떻게 될까요?
@Testpublicvoidauto_increment_test_jdbc()throws Exception {
//given
JdbcBatchItemWriter<Person> writer = new JdbcBatchItemWriterBuilder<Person>()
.dataSource(dataSource)
.sql("insert into person(name) values (:name)")
.beanMapped()
.build();
writer.afterPropertiesSet();
List<Person> items = new ArrayList<>();
for (long i = 0; i < TEST_COUNT; i++) {
items.add(new Person( "foo" + i));
}
// when
writer.write(items);
}
동일하게 1만건을 요청할 경우에도 마찬가지로0.561초라는 결과를 보여줍니다.
순수하게 단일 테이블의 등록면에 있어서는 Jdbc Batch Insert의 성능이 비교가 안될 정도로 좋다는 것을 알 수 있습니다.
다만 무조건 많은 양의 row를 한번에 요청하는게 빠른 방법은 아닙니다. 한번에 몇개의 insert value를 만들지 MySQL의max_allowed_packet,Buffer Size,bulk_insert_buffer_size등 여러 옵션들에 따라 상이하니 적절한 성능 테스트를 통해 값을 찾아야 합니다.
3. 최종 비교
최종적으로 Spring Batch ItemWriter들의 성능을 비교하면 다음과 같습니다.
ItemWriter ModeNon Auto Increment (10,000 row)Auto Increment (10,000 row)
ItemWriter Mode
Non Auto Increment
Auto Increament
Jpa.Merge
2m 16s
1m 1s
Jpa.Persist
1m 9s
1m 2s
Jdbc Batch Insert
0.586s
0.586s
순수하게단일 테이블에 대량으로 등록할 경우 Jdbc의 Batch Insert 방식이 압도적인 성능을 보여줍니다. 다만, 무조건 Jdbc Batch Insert 방식을 사용하기엔 아래와 같은 단점들이 있습니다.
OneToMany, ManyToMany와 같이 복잡한 Entity 관계가 insert가 필요할 경우 직접 구현해야할 부분이 너무나 많이 존재
컴파일체크, 타입힌트, 자동완성등 유지보수가 어려운 개발 환경
그래서 다음과 같이혼합 방식을 선택하기도 합니다.
이를 테면 OneToMany의 관계가 등록이 필요할 경우
부모 Entity는 JpaItemWriter를 이용하여 ChunkSize별로 저장하여 PK값과 Entity를 확보
PK가 확보된 부모 Entity를 통해 자식 Entity들을 생성 (부모 ID값을 갖고 생성)
Spring Batch에서의 Chunk란 데이터 덩어리로 작업 할 때각 커밋 사이에 처리되는 row 수를 얘기합니다. 즉, Chunk 지향 처리란 한 번에 하나씩 데이터를 읽어 Chunk라는 덩어리를 만든 뒤, Chunk 단위로 트랜잭션을 다루는 것을 의미합니다. 여기서 트랜잭션이라는게 중요한데요. Chunk 단위로 트랜잭션을 수행하기 때문에실패할 경우엔 해당 Chunk 만큼만 롤백이 되고, 이전에 커밋된 트랜잭션 범위까지는 반영이 된다는 것입니다. Chunk 지향 처리가 결국 Chunk 단위로 데이터를 처리한다는 의미이기 때문에 그림으로 표현하면 아래와 같습니다.
Chunk 지향 프로세싱은 1000개의 데이터에 대해 배치 로직을 실행한다고 가정하면, Chunk 단위로 나누지 않았을 경우에는 한개만 실패해도 성공한 999개의 데이터가 롤백된다. Chunk 단위를 10으로 한다면, 작업 중에 다른 Chunk는 영향을 받지 않는다.
Reader에서 데이터 하나를 읽어 온다.(item 단위)
읽어온 데이터를 Processor에서 가공한다.(item 단위)\
가공된 데이터들을 별도의 공간에 모은 뒤 Chunk 단위만큼 쌓이게 되면 Writer에 전달하고 Writer는 일괄 저장한다. (Chunk = items)
여기선 Reader, Processor에서는 1건씩 다뤄지고, Writer에서는 Chunk 단위로 처리된다는 것을 기억하면 된다.
Chunk-oriented processing의 장점
위에서 알아본청크지향 프로세싱을 사용하지 않는다 하더라도 개발자가 충분히 비슷한 로직으로 구현을 할 수도 있습니다. 하지만청크지향 프로세싱은 단순히 청크단위의트랜잭션만 제공해주는것은 아닙니다.
Spring batch 청크지향 프로세싱의 가장 큰 장점이라고 하면, 내결함성 (Falut tolernat)를 위한 다양한 기능들을 제공하고 있다는 것 입니다.
멀티 스레드 환경에서 chunk 지향 불가
각 Reader와 Writer의 Javadoc에 항상 저thread-safe문구가 있는지 확인해보셔야 합니다. 만약 없는 경우엔 thread-safe가 지원되는 Reader 와 Writer를 선택해주셔야하며, 꼭 그 Reader를 써야한다면SynchronizedItemStreamReader등을 이용해thread-safe로 변환해서 사용해볼 수 있습니다.
그리고 또 하나 주의할 것은 멀티 쓰레드로 각 Chunk들이 개별로 진행되다보니 Spring Batch의 큰 장점중 하나인 실패 지점에서 재시작하는 것은 불가능 합니다. 이유는 간단합니다. 단일 쓰레드로 순차적으로 실행할때는 10번째 Chunk가 실패한다면9번째까지의 Chunk가 성공했음이 보장되지만, 멀티쓰레드의 경우 1~10개의 Chunk가 동시에 실행되다보니 10번째 Chunk가 실패했다고 해서1~9개까지의Chunk가 다 성공된 상태임이 보장되지 않습니다. 그래서 일반적으로는 ItemReader의saveState옵션을false로 설정하고 사용합니다.
웹 프레임워크에는 MVC 패턴이라는 것이 있고 특정 MVC 프레임워크를 이해하고 나면 다른 MVC 프레임워크를 사용하더라도 서로 다른 문법적 차이만 이해하면 된다. 그러나 배치 프레임워크는 종류가 많지 않다. 그래서 배치 프레임워크는 분야가 조금 생소할 수 있다. job(잡)이나 Step(스텝)이 무엇인지? ItemReader와 ItemWriter가 어떻게 연관돼 있는지 Tasklet이 무엇인지? 2장에서는 위의 질문에 대한 답을 할 것이다.
잡과 스텝
배치 잡
ex) 은행업무 Step1 : 다른 시스템에서 수신한 거래 정보 파일을 읽어와 데이터베이스에 저장한다. Step2 : 모든 입금 정보를 계좌에 반영한다. Step3 : 모든 출금정보를 계좌에 반영한다.
각 스텝은 잡을 구성하는 독립된 작업의 단위라는 것을 알 수 있다.
스탭
테스크릿(tasklet) 기반 스텝
청크(chunk) 기반 스텝
테스크릿(tasklet) 기반 스텝
스텝이 중지될 때까지 execute 메서드가 반속해서(execute 메서드를 호출할 때마다 독립적인 트랜잭션이 얻어짐) 수행 된다.
초기화
저장 프로시저 실행
알람 전송
청크(chunk) 기반 스텝
약간 스텝의 구조가 복잡하며, 아이템 기반의 처리가 사용된다.
ItemReader(필수)
ItemProcessor(선택)
ItemWriter(필수)
ItemReader와 ItemWrier만으로 구성해 스텝을 진행 할 수도 있다.(이러한 스텝은 데이터 마이그레이션 잡에 일반적으로 사용된다.)
이 에너테이션은 배치 인프라스트럭처를 위한 대부분의 스프링 빈 정의를 제공하므로 다음과 같은 컴포넌트를 직접 포함시킬 필요는 없다
컴포넌트
설명
JobRepository
실행 중인 잡의 상태를 기록하는 데 사용됨
JobLauncher
잡을 구동하는 데 사용됨
JobExplorer
JobRepository를 사용해 읽기 전용 작업을 수행하는 데 사용됨
JobRegistry
특정한 런처 구현체를 사용할 때 잡을 찾는 용도로 사용됨
PlatformTransactionManager
잡 진행 과정에서 트랜잭션을 다루는 데 사용됨
JobBuilderFactory
잡을 생성하는 빌더
StepBuilderFactory
스텝을 생성하는 빌더
@SpringBootApplication 에너테이션의 역할
@componenetScan과 @EnableAutoConfiguration을 결합한 메타 에너테이션이다. 이 에너텡션은 데이터 소스뿐만 아니라 스프링 부트 기반의 적절한 자동 구성을 만들어 준다.
public Step step()
이 잡은 단일 스텝으로 구성되므로 간단하게 스텝이름만 지정한다. 스텝은 스프링 빈으로 구성됐으며, 이 간단한 예제에서는 두가지 요인인 이름 및 테스크릿만 필요하다. System.out.println("Hello, World")을 호출한 다음에 RepeatStatus.FINISHED를 반환한다.
고객의 마음을 바꿔 주문을 취소할 때, 아직 배송하기 전이라면 훨씬 더 저렴한 비용으로 취소할 수 있다. 고객에게 몇 시간을 더 주면서 배치로 배송을 처러하면 소매업자는 많은 돈을 절약할 수 있다.(주문/거래/배송->정산)
왜 자바로 배치를 처리하는 가?
배치 처리 개발에 자바 및 오픈소를 사용해야 하는 이유 6가지
유지 보수성
유연성
확장성
개발 리소스
지원
비용
스프링 배치 프레임워크 구조
스프링 배치는 레이어(Layer) 구조로 조립된 세 개의 티어(Tier)로 이뤄져 있다.
어플리케이션 레이어 [ 코어 / 인프라스트럭처 레이어 ]
어플리케이션 레이어
가장 바깥쪽에 위치하며, 배치 처리 구축에 사용되는 '모든 사용자 코드' 및 '구성'이 포함된다. '업무 로직 서비스' 등은 물론 '잡 구조'와 관련된 구성도 포함한다.
가장 최상위에 있는 것이 아니라 코어/인프라스트럭처를 감싸고 있음에 주목하자. (그 이유는 개발자가 개발하는 대부분의 코드가 '코어 레이어'와 함께 동작하는 어플리케이션레어 레이어 이지만 때로는 커스텀 리더(Reader)나 커스텀 라이터(Writer)와 같이 인프라스트럭처의 일부를 만들기도 하기 때문이다.)
이해가 안가지만 대충 어플리케이션 레이어는 코어 레이어랑 많이 노는데 가끔 인프라스트럭처랑도 논다. 이런 느낌
무중단 처리 또는 상시 데이터 처리
스프링 프레임 워크를 사용하면 큐(queue)에서 메시지를 읽은 뒤, 청크 단위로 배치 처리를 수행하는 과정을 끝없이 반복할 수 있다. 따라서 이러한 솔루션을 처음부터 개발하는 복잡한 상황을 이해할 필요 없이(Queue가 알아서해주기 때문에) 대량 데이터 처리량을 늘릴수 있다.
Chunk(청크) 처리란? Spring Batch에서의 Chunk란 데이터를 덩어리로 작업 할 때 각 커밋 사이에 처리되는 row의 수 이다. 즉, Chunk 지향 처리란 한 번에 하나씩 데이터를 읽어 Chunk라는 덩어리르 만든 뒤, Chunk 단위로 트랜잭션을 다루는 것을 의미한다.
- Reader에서 데이터를 하나 읽어옴 - 읽어온 데이터를 Processor에서 가공 - 가공된 데이터를 별도의 공간에 모은 뒤, Chunk 단위만큼 쌓이게 되면 Writer에 전달하고 Writer는 일괄 저장 즉, Reader와 Processor에서는 1건씩 다뤄지고 Writer에서는 Chunk 단위로 처리된다.
스프링으로 잡 정의하기
잡은 중단이나 상호작용 없이 처음부터 끝까지 실행되는 처리이다.
잡은 여러 개의 스텝이 모여 이뤄질 수 있다.
스텝이 실패했을 때 반복 실행할 수도 있고 못할 수도 있다.
잡의 플로우(flow)는 조건부일 수 있다. (예를들어 수익을 계산하는 스텝이 $1,000,000 이상의 수익을 반환할 때만 보너스 계산 스텝을 실행하는 경우)
첫 번째 Bean은 AccountTasklet이다. 커스텀 컴포넌트이다. 스텝이 동작하는 동안에 비즈니스 로직을 수행한다. 스프링배치는 AccountTasklet이 완료될 때까지 단일 메서드(execute 메서드)를 반복해서 호출하는데, 이때 각각은 새 트렌잭션으로 호출된다.
두 번째 Bean은 스프링 배치 잡이다. 이 Bean 정의 내에서는 팩토리가 제공하는 빌더를 사용해, 조금 전 정의했던 AccountTasklet을 감싸는 스텝하나를 생성한다. 그런 다음에 잡 빌더를 사용해 스텝을 감싸는 잡을 생성한다. 스프링 부트(Spring Boot는 애플리케이션 기동 시에 이 잡을 찾아내 자동으로 실행시킨다.