12. Common Batch Patterns
일부 배치 잡은 스프링 배치의 기성 컴포넌트만으로 조립될 수 있다. 예를 들어, 아이템리더(ItemReader)
및 아이템라이터(ItemWriter)
구현은 광범위한 상황를 처리할 수 있다. 그러나 대부분의 경우 커스텀 코드를 작성해야 한다. 애플리케이션 개발자를 위한 주요 API는 태스크릿(Tasklet)
, 아이템리더(ItemReader)
, 아이템라이터(ItemWriter)
및 다양한 리스너 인터페이스이다. 대부분의 간단한 배치 잡은 스프링 배치 아이템리더(ItemReader)
의 순수 입력 기능을 사용할 수 있지만, 개발자가 아이템라이터(ItemWriter)
또는 아이템프로세서(ItemProcessor)
를 직접 구현해야 하는 처리 및 쓰기에 커스텀 문제가 있는 경우가 종종 있다.
이 장에서는, 커스텀 비즈니스 로직의 일반적인 패턴에 대한 몇 가지 예시를 제공한다. 이 예제에는 주로 리스너 인터페이스가 포함되어 있다. 아이템리더(ItemReader)
또는 아이템라이터(ItemWriter)
는 필요하다면 리스너 인터페이스로 구현할 수 있다는 점에 유의해야 한다.
12.1. Logging Item Processing and Failures
일반적인 사례는 특정 스텝에서 아이템별 오류를 특수하게 처리해야 하는 경우, 특수 채널에 로그인하거나 데이터베이스에 레코드를 삽입하는 경우 등이다. 스텝 팩토리 빈에서 생성된 청크 지향 스텝를 통해 사용자는 읽기 오류에 대한 간단한 아이템리드리스너(ItemReadListener)
및 쓰기 오류에 대한 아이템라이트리스너(ItemWriteListener)
를 사용하여 이 사례를 구현할 수 있다. 다음 코드 조각은 읽기 및 쓰기 실패를 모두 기록하는 리스너를 보여준다:
public class ItemFailureLoggerListener extends ItemListenerSupport {
private static Log logger = LogFactory.getLog("item.error");
public void onReadError(Exception ex) {
logger.error("Encountered error on read", e);
}
public void onWriteError(Exception ex, List<? extends Object> items) {
logger.error("Encountered error on write", ex);
}
}
위 리스너 구현 후, 스텝에 등록해야 한다.
다음 예시는 XML에서 스텝에 리스너를 등록하는 방법을 보여준다:
XML 구성
<step id="simpleStep">
...
<listeners>
<listener>
<bean class="org.example...ItemFailureLoggerListener"/>
</listener>
</listeners>
</step>
다음 예시는 자바에서 스텝에 리스너를 등록하는 방법을 보여준다:
자바 구성
@Bean
public Step simpleStep(JobRepository jobRepository) {
return new StepBuilder("simpleStep", jobRepository)
...
.listener(new ItemFailureLoggerListener())
.build();
}
리스너가
onError()
메서드에서 잡을 수행하는 경우, 롤백될 트랜잭션 내부에 있어야 한다.onError()
메서드 내에서 데이터베이스와 같은 트랜잭션 리소스를 사용해야 하는 경우 해당 메서드에 선언적으로 트랜잭션을 추가하고(자세한 내용은 스프링 코어 레퍼런스 가이드 참고) 해당 프로파게이션(propagation) 애트리뷰트에REQUIRES_NEW
값을 제공하는 것을 고려해보자.
12.2. Stopping a Job Manually for Business Reasons
스프링 배치는 잡오퍼레이터(JobOperator)
인터페이스를 통해 stop()
메소드를 제공하지만 실제로는 개발자가 아닌 운영자가 사용하기 위한 것이다. 때로는 비즈니스 로직 내에서 잡 실행을 중지하는 것이 더 편리하거나 더 합리적일 때도 있다.
가장 간단한 방법은 런타임익셉션(RuntimeException)
(무한히 재시도되거나 스킵되지 않는 예외)을 발생시키는 것이다. 다음 예제와 같이 커스텀 예외 타입을 사용할 수 있다:
public class PoisonPillItemProcessor<T> implements ItemProcessor<T, T> {
@Override
public T process(T item) throws Exception {
if (isPoisonPill(item)) {
throw new PoisonPillException("Poison pill detected: " + item);
}
return item;
}
}
스텝 실행을 중지하는 또 다른 간단한 방법은 다음 예제와 같이 아이템리더(ItemReader)
에서 null
을 반환하는 것이다:
public class EarlyCompletionItemReader implements ItemReader<T> {
private ItemReader<T> delegate;
public void setDelegate(ItemReader<T> delegate) { ... }
public T read() throws Exception {
T item = delegate.read();
if (isEndItem(item)) {
return null; // 여기서 스텝이 끝난다.
}
return item;
}
}
위의 예제는 실제로 처리할 아이템이 null
일 때 전체 배치에 알리는 컴플리션폴리시(CompletionPolicy)
전략의 기본 구현체가 있다는 사실에 의존한다. 심플스텝팩토리빈(SimpleStepFactoryBean)
을 통해 보다 정교한 컴플리션 폴리시을 구현하고 스텝
에 주입할 수 있다.
다음 예제는 XML에서 스텝의 컴플리션 폴리시를 삽입하는 방법을 보여준다:
XML 구성
<step id="simpleStep">
<tasklet>
<chunk reader="reader" writer="writer" commit-interval="10" chunk-completion-policy="completionPolicy"/>
</tasklet>
</step>
<bean id="completionPolicy" class="org.example...SpecialCompletionPolicy"/>
다음 예제는 자바에서 스텝의 컴플리션 폴리시를 삽입하는 방법을 보여준다:
자바 구성
@Bean
public Step simpleStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("simpleStep", jobRepository)
.<String, String>chunk(new SpecialCompletionPolicy(),transactionManager)
.reader(reader())
.writer(writer())
.build();
}
다른 방법은 아이템 처리 사이에 프레임워크의 스텝 구현에 의해 확인되는 스텝익스큐션(StepExecution)
에 플래그를 설정하는 것이다. 이 방법을 구현하려면 현재 스텝익스큐션(StepExecution)
에 접근해야 하며 이는 스텝리스너(StepListener)
를 구현하고 이를 스텝에 등록하여야 한다. 다음 예에서 플래그를 설정하는 리스너를 보여준다. 다음 예에서는 플래그를 설정하는 리스너를 보여준다:
public class CustomItemWriter extends ItemListenerSupport implements StepListener {
private StepExecution stepExecution;
public void beforeStep(StepExecution stepExecution) {
this.stepExecution = stepExecution;
}
public void afterRead(Object item) {
if (isPoisonPill(item)) {
stepExecution.setTerminateOnly();
}
}
}
기본 동작은 플래그가 설정되면, 스텝에서 잡인터럽트익셉션(JobInterruptedException)
을 발생시키는 것이다. 이 동작은 스텝인터럽션폴리시(StepInterruptionPolicy)
를 통해 제어할 수 있다. 그러나, 발생할 수 있는 상황은 예외를 던지거나 던지지 않는 것이므로, 이는 잡의 비정상적인 종료로 처리된다.
12.3. Adding a Footer Record
플랫 파일에 쓸 때, 모든 처리가 완료된 후, 파일 끝에 “바닥글” 레코드를 추가해야 하는 경우가 많다. 이는 스프링 배치에서 제공하는 플랫파일푸터콜백(FlatFileFooterCallback)
인터페이스를 사용하여 달성할 수 있다. 플랫파일푸터콜백(FlatFileFooterCallback)
(및 이에 상응하는 플랫파일헤더콜백(FlatFileHeaderCallback)
)은 플랫파일아이템라이터(FlatFileItemWriter)
의 옵셔널 프로퍼티이며 아이템 라이터(item writer)에 의해 추가될 수 있다.
다음 예제에서는 XML에서 플랫파일헤더콜백(FlatFileHeaderCallback)
및 플랫파일푸터콜백(FlatFileFooterCallback)
을 사용하는 방법을 보여준다:
XML 구성
<bean id="itemWriter" class="org.spr...FlatFileItemWriter">
<property name="resource" ref="outputResource" />
<property name="lineAggregator" ref="lineAggregator"/>
<property name="headerCallback" ref="headerCallback" />
<property name="footerCallback" ref="footerCallback" />
</bean>
다음 예제에서는 XML에서 플랫파일헤더콜백(FlatFileHeaderCallback)
및 플랫파일푸터콜백(FlatFileFooterCallback)
을 사용하는 방법을 보여준다:
자바 구성
@Bean
public FlatFileItemWriter<String> itemWriter(Resource outputResource) {
return new FlatFileItemWriterBuilder<String>()
.name("itemWriter")
.resource(outputResource)
.lineAggregator(lineAggregator())
.headerCallback(headerCallback())
.footerCallback(footerCallback())
.build();
}
바닥글 콜백 인터페이스에는 다음 인터페이스에 표시된 대로 바닥글(footer)을 작성해야 할 때 호출되는 메서드 하나만 있다:
public interface FlatFileFooterCallback {
void writeFooter(Writer writer) throws IOException;
}
12.3.1. Writing a Summary Footer
바닥글(footer) 레코드와 관련된 일반적인 요구 사항은 출력 프로세스 중에 정보를 집계하고, 이 정보를 파일 끝에 추가하는 것이다. 이 바닥글은 파일 요약 역할을 하거나 체크섬(checksum)을 제공하는 경우가 많다. 예를 들어, 배치 잡이 거래 레코드를 플랫 파일에 기록하고 모든 거래의 총액이 바닥글에 배치되어야 하는 요구사항이 있는 경우 다음 아이템라이터(ItemWriter)
구현체을 사용할 수 있다:
public class TradeItemWriter implements ItemWriter<Trade>, FlatFileFooterCallback {
private ItemWriter<Trade> delegate;
private BigDecimal totalAmount = BigDecimal.ZERO;
public void write(Chunk<? extends Trade> items) throws Exception {
BigDecimal chunkTotal = BigDecimal.ZERO;
for (Trade trade : items) {
chunkTotal = chunkTotal.add(trade.getAmount());
}
delegate.write(items);
// 모든 아이템을 성공적으로 작성한 이후
totalAmount = totalAmount.add(chunkTotal);
}
public void writeFooter(Writer writer) throws IOException {
writer.write("처리된 전체 금액: " + totalAmount);
}
public void setDelegate(ItemWriter delegate) {...}
}
이 트레이드아이템라이터(TradeItemWriter)
는 작성된 각 무역 품목의 금액에 따라 증가하는 totalAmount
값을 저장한다. 마지막 거래가 처리된 후 프레임워크는 writeFooter
를 호출하여 totalAmount
를 파일에 넣는다. write
메소드는 청크의 총 거래 금액을 저장하는 임시 변수인 청크토탈(ChunkTotal)
을 사용한다. 이는 write
메소드에서 스킵이 발생하는 경우 totalAmount
가 변경되지 않은 상태로 유지되도록 하기 위해서이다. 예외가 발생하지 않는다는 것이 확실하면, write
메소드의 끝에서만 totalAmount
를 업데이트한다.
writeFooter
메소드를 호출하려면 플랫파잇푸터콜백(FlatFileFooterCallback)
구현체인 트레이드아이템라이터(TradeItemWriter)
를 푸터콜백(FooterCallback)
으로 플랫파일아이템라이터(FlatFileItemWriter)
에 연결해야 한다.
다음 예제는 XML에서 트레이드아이템라이터(TradeItemWriter)
를 연결하는 방법을 보여준다:
XML 구성
<bean id="tradeItemWriter" class="..TradeItemWriter">
<property name="delegate" ref="flatFileItemWriter" />
</bean>
<bean id="flatFileItemWriter" class="org.spr...FlatFileItemWriter">
<property name="resource" ref="outputResource" />
<property name="lineAggregator" ref="lineAggregator"/>
<property name="footerCallback" ref="tradeItemWriter" />
</bean>
다음 예제는 자바에서 트레이드아이템라이터(TradeItemWriter)
를 연결하는 방법을 보여준다:
자바 구성
@Bean
public TradeItemWriter tradeItemWriter() {
TradeItemWriter itemWriter = new TradeItemWriter();
itemWriter.setDelegate(flatFileItemWriter(null));
return itemWriter;
}
@Bean
public FlatFileItemWriter<String> flatFileItemWriter(Resource outputResource) {
return new FlatFileItemWriterBuilder<String>()
.name("itemWriter")
.resource(outputResource)
.lineAggregator(lineAggregator())
.footerCallback(tradeItemWriter())
.build();
}
지금까지 트레이드아이템라이터(TradeItemWriter)
가 작성된 방식은 스텝를 재시작할 수 없는 경우에만 올바르게 작동한다. 이는 클래스가 totalAmount
를 저장하므로 상태 저장형이고 totalAmount
가 데이터베이스에 저장되지 않기 때문이다. 따라서 재시작 시 복구할 수 없다. 이 클래스를 재시작할 수 있게 만들려면, 다음 예제와 같이 open
및 update
메서드와 함께 아이템스트림(ItemStream)
인터페이스를 구현해야 한다:
public void open(ExecutionContext executionContext) {
if (executionContext.containsKey("total.amount") {
totalAmount = (BigDecimal) executionContext.get("total.amount");
}
}
public void update(ExecutionContext executionContext) {
executionContext.put("total.amount", totalAmount);
}
update
메소드는 해당 객체가 데이터베이스에 유지되기 직전에 최신 버전의 totalAmount
를 익스큐션컨텍스트(ExecutionContext)
에 저장한다. open
메소드는 익스큐션컨텍스트(ExecutionContext)
에서 기존 totalAmount
를 검색하고 이를 시작점으로 사용하여 트레이드아이템라이터(TradeItemWriter)
가 이전 스텝 실행 시 중단된 부분부터 재시작할 수 있도록 한다.
12.4. Driving Query Based ItemReaders
리더와 라이터 장에서는 페이징을 이용한 데이터베이스 입력에 대해 논의했다. DB2와 같은 많은 데이터베이스 공급업체(vendor)는 읽고 있는 테이블을 온라인 애플리케이션의 다른 부분에서도 사용해야 하는 경우 문제를 일으킬 수 있는 극도로 비관적인 잠금 전략(pessimistic locking strategies)을 가지고 있다. 또한 매우 큰 데이터에 대해 커서를 열면 특정 공급업체의 데이터베이스에는 문제가 발생할 수 있다. 따라서 많은 프로젝트에서는 데이터 읽기에 ‘드라이빙 쿼리(Driving Query)’ 접근 방식을 선호한다. 이 접근 방식은 다음 이미지에 나와 있는 것처럼 반환해야 하는 전체 객체가 아닌 키를 반복하는 방식으로 작동한다:
이미지 26. 드라이빙 쿼리 잡(Driving Query Job)
위 이미지에 표시된 예는 커서 기반 예제에서 사용된 것과 동일하게 ‘FOO’ 테이블을 사용한다. 그러나, SQL 문(statement)에서는 전체 로우을 선택하지 않고 ID만 선택했다. 따라서 read
에서 FOO
객체가 반환되는 대신 인티저(Integer)
가 반환된다. 그런 다음 이 숫자를 사용하여 다음 이미지에 표시된 대로 완전한 Foo
객체의 ‘상세정보’를 쿼리할 수 있다:
이미지 27. 드라이빙 쿼리 예시
아이템프로세서(ItemProcessor)
를 사용하여 쿼리해서 얻은 키를 전체 Foo 객체로 변환해야 한다. 기존 DAO를 사용하여 키를 기반으로 전체 객체를 쿼리할 수 있다.
12.5. Multi-Line Records
일반적으로 각 레코드가 한줄로 제한되는 플랫 파일의 경우도 있지만, 파일에 여러 포맷으로 여러 줄에 걸쳐 있는 레코드도 있을 수 있다. 다음 파일에서 발췌한 내용은 이러한 배열의 예를 보여준다:
HEA;0013100345;2007-02-15
NCU;Smith;Peter;;T;20014539;F
BAD;;Oak Street 31/A;;Small Town;00235;IL;US
FOT;2;2;267.34
‘HEA’로 시작하는 줄과 ‘FOT’으로 시작하는 줄 사이의 모든 아이템은 하나의 레코드가 된다. 이 상황을 올바르게 처리하려면 다음과 같은 몇 가지 사항을 고려해야 한다:
- 한 번에 하나의 레코드를 읽는 대신
아이템리더(ItemReader)
는 여러 줄 레코드를 그룹으로 읽어야아이템라이터(ItemWriter)
에 그대로 전달될 수 있다. - 각 줄의 타입을 다르게 토큰화해야 할 수도 있다.
단일 레코드는 여러 줄에 걸쳐 있고 줄 수를 알 수 없기 때문에 아이템리더(ItemReader)
는 항상 전체 레코드를 읽도록해야 한다. 이를 수행하려면, 커스텀 아이템리더(ItemReader)
를 플랫파일아이템리더(FlatFileItemReader)
의 래퍼(wrapper)로 구현해야 한다.
다음 예제는 XML에서 커스텀 아이템리더(ItemReader)
를 구현하는 방법을 보여준다:
XML 구성
<bean id="itemReader" class="org.spr...MultiLineTradeItemReader">
<property name="delegate">
<bean class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="resource" value="data/iosample/input/multiLine.txt" />
<property name="lineMapper">
<bean class="org.spr...DefaultLineMapper">
<property name="lineTokenizer" ref="orderFileTokenizer"/>
<property name="fieldSetMapper" ref="orderFieldSetMapper"/>
</bean>
</property>
</bean>
</property>
</bean>
다음 예제는 자바에서 커스텀 아이템리더(ItemReader)
를 구현하는 방법을 보여준다:
자바 구성
@Bean
public MultiLineTradeItemReader itemReader() {
MultiLineTradeItemReader itemReader = new MultiLineTradeItemReader();
itemReader.setDelegate(flatFileItemReader());
return itemReader;
}
@Bean
public FlatFileItemReader flatFileItemReader() {
FlatFileItemReader<Trade> reader = new FlatFileItemReaderBuilder<>()
.name("flatFileItemReader")
.resource(new ClassPathResource("data/iosample/input/multiLine.txt"))
.lineTokenizer(orderFileTokenizer())
.fieldSetMapper(orderFieldSetMapper())
.build();
return reader;
}
고정 길이 입력에 특히 중요한 각 줄이 적절하게 토큰화되었는지 확인하기 위해 패턴매칭컴포직라인토크나이저(PatternMatchingCompositeLineTokenizer)
를 델리게이트(delegate)로써 플랫파일아이템리더(FlatFileItemReader)
에서 사용할 수 있다. 자세한 내용은 Readers and Writers 장의 플랫파일아이템리더(FlatFileItemReader)를 참고하자. 그 다음 델리게이트 리더(delegate reader)는 패스쓰로우필드셋매퍼(PassThroughFieldSetMapper)
를 사용하여 각 줄에 대한 필드셋(FieldSet)
을 래핑 아이템리더(wrapping ItemReader)
로 다시 전달한다.
다음 예제는 XML에서 각 줄이 적절하게 토큰화되었는지 확인하는 방법을 보여준다:
XML 내용
<bean id="orderFileTokenizer" class="org.spr...PatternMatchingCompositeLineTokenizer">
<property name="tokenizers">
<map>
<entry key="HEA*" value-ref="headerRecordTokenizer" />
<entry key="FOT*" value-ref="footerRecordTokenizer" />
<entry key="NCU*" value-ref="customerLineTokenizer" />
<entry key="BAD*" value-ref="billingAddressLineTokenizer" />
</map>
</property>
</bean>
다음 예제는 자바에서 각 줄이 적절하게 토큰화되었는지 확인하는 방법을 보여준다:
자바 내용
@Bean
public PatternMatchingCompositeLineTokenizer orderFileTokenizer() {
PatternMatchingCompositeLineTokenizer tokenizer = new PatternMatchingCompositeLineTokenizer();
Map<String, LineTokenizer> tokenizers = new HashMap<>(4);
tokenizers.put("HEA*", headerRecordTokenizer());
tokenizers.put("FOT*", footerRecordTokenizer());
tokenizers.put("NCU*", customerLineTokenizer());
tokenizers.put("BAD*", billingAddressLineTokenizer());
tokenizer.setTokenizers(tokenizers);
return tokenizer;
}
이 래퍼(wrapper)는 끝에 도달할 때까지(델리게이트(delegate)의 read()
를 계속 호출하기 위해) 레코드의 끝을 인식해야 한다. 읽은 각 줄에 대해 래퍼는 반환할 아이템을 구성해야 한다. 바닥글(footer)에 도달하면 다음 예와 같이 아이템이 아이템프로세서(ItemProcessor)
및 아이템라이터(ItemWriter)
로 반환되어 전달될 수 있다:
private FlatFileItemReader<FieldSet> delegate;
public Trade read() throws Exception {
Trade t = null;
for (FieldSet line = null; (line = this.delegate.read()) != null;) {
String prefix = line.readString(0);
if (prefix.equals("HEA")) {
t = new Trade(); // 레코트는 틀림없이 헤더(header)로 시작해야함.
} else if (prefix.equals("NCU")) {
Assert.notNull(t, "No header was found.");
t.setLast(line.readString(1));
t.setFirst(line.readString(2));
...
} else if (prefix.equals("BAD")) {
Assert.notNull(t, "No header was found.");
t.setCity(line.readString(4));
t.setState(line.readString(6));
...
} else if (prefix.equals("FOT")) {
return t; // 기록은 바닥글(footer)로 끝나야 한다.
}
}
Assert.isNull(t, "No 'END' was found.");
return null;
}
12.6. Executing System Commands
많은 배치 잡은 배치 잡 내부에서 외부 명령을 호출해야 한다. 이러한 프로세스는 스케줄러에 의해 시작될 수 있지만, 실행에 대한 공통 메타데이터를 저장하고있는 이점은 상실된다. 또한 멀티 스텝 잡도 여러 잡으로 분할해야 한다. 일반적으로 많이 필요하기 때문에, 스프링 배치는 시스템 명령 호출을 위한 태스크릿(Tasklet)
구현체를 제공한다.
다음 예제는 XML에서 외부 명령을 호출하는 방법을 보여준다: XML 구성
<bean class="org.springframework.batch.core.step.tasklet.SystemCommandTasklet">
<property name="command" value="echo hello" />
<!-- 명령이 완료되는 까지 5초 후 제한시간 -->
<property name="timeout" value="5000" />
</bean>
다음 예제는 자바에서 외부 명령을 호출하는 방법을 보여준다: 자바 구성
@Bean
public SystemCommandTasklet tasklet() {
SystemCommandTasklet tasklet = new SystemCommandTasklet();
tasklet.setCommand("echo hello");
tasklet.setTimeout(5000);
return tasklet;
}
12.7. Handling Step Completion When No Input is Found
많은 배치 프로세스에서 처리할 데이터베이스나 파일에 로우가 없는 경우 예외가 아닐 수 있다. 스텝은 단순히 작업을 찾지 못한 것으로 간주되며, 읽은 아이템이 0개로 완료된다. 스프링 배치에서 기본적으로 제공되는 모든 아이템리더(ItemReader)
구현체는 이 접근 방식을 기본으로 한다. 입력이 있는 경우에도 아무것도 기록되지 않으면 혼란이 발생(일반적으로 파일명이 잘못됐거나 유사한 문제가 있는 경우 발생)할 수 있다. 이러한 이유로, 메타데이터 자체를 검사하여 프레임워크에서 처리된 작업량이 얼마나 되는지 확인해야 한다. 그러나 입력을 찾지 못한 것이 예외적인 것으로 간주된다면 어떻게 될까? 이 경우 처리된 아이템이 없는지 메타데이터를 프로그래밍 방식으로 확인하여 오류를 일으키는 것이 최선의 해결 방법이다. 이는 일반적인 사례이기 때문에 스프링 배치는 노워크파운드스텝익셉션리스너(NoWorkFoundStepExecutionListener)
클래스를 정의하며 표시된 대로 정확히 리스너에게 제공한다.
public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {
public ExitStatus afterStep(StepExecution stepExecution) {
if (stepExecution.getReadCount() == 0) {
return ExitStatus.FAILED;
}
return null;
}
}
스텝익스큐션리스너(StepExecutionListener)
는 ‘afterStep’ 단계 동안 스텝익스큐션(StepExecution)
의 readCount
프로퍼티을 검사하여 읽은 아이템이 없는지 확인한다. 이 경우 스텝이 실패해야 함을 나타내는 종료 코드 FAILED
가 반환된다. 그렇지 않으면 null이
반환되며 이는 스텝
상태에 영향을 주지 않는다.
12.8. Passing Data to Future Steps
한 스텝에서 다른 스텝으로 정보를 전달하는 것이 유용한 경우가 많다. 이는 익스큐션컨텍스트(ExecutionContext)
를 통해 수행될 수 있다. 문제는 두 개의 익스큐션컨텍스트(ExecutionContext)
가 있다는 것이다: 하나는 스텝 레벨에 있고 다른 하나는 잡 레벨에 있다. 스텝 익스큐션컨텍스트(ExecutionContext)
는 스텝 동안만 유지되는 반면, 잡 익스큐션컨텍스트(ExecutionContext)
는 전체 잡 동안 유지된다. 반면에, 스텝 익스큐션컨텍스트(ExecutionContext)
는 스텝이 청크를 커밋할 때마다 업데이트되는 반면, 잡 익스큐션컨텍스트(ExecutionContext)
는 각 스텝이 끝날 때만 업데이트된다.
이러한 분리된 결과 데이터는 스텝이 실행되는 동안 스텝 익스큐션컨텍스트(ExecutionContext)
에 저장되어야 한다. 이렇게 하면 스텝이 실행되는 동안 데이터가 올바르게 저장됐다는 것을 보증할 수 있다. 데이터가 잡 익스큐션컨텍스트(ExecutionContext)
에 저장되면 스텝 실행 중 메타데이터가 저장되지 않는다. 스텝이 실패하면 해당 데이터가 손실된다.
public class SavingItemWriter implements ItemWriter<Object> {
private StepExecution stepExecution;
public void write(Chunk<? extends Object> items) throws Exception {
// ...
ExecutionContext stepContext = this.stepExecution.getExecutionContext();
stepContext.put("someKey", someObject);
}
@BeforeStep
public void saveStepExecution(StepExecution stepExecution) {
this.stepExecution = stepExecution;
}
}
데이터를 향후 스텝에서 사용할 수 있도록 하려면 스텝이 완료된 후, 잡 익스큐션컨텍스트(ExecutionContext)
로 “승격(promoted)”되어야 한다. 스프링 배치는 이러한 목적으로 익스큐션컨텍스트프로모션리스너(ExecutionContextPromotionListener)
를 제공한다. 리스너는 승격되어야 하는 익스큐션컨텍스트(ExecutionContext)
의 데이터와 관련된 키로 구성되어야 한다. 선택적으로 승격이 발생해야 하는 종료 코드 패턴 목록으로 구성할 수도 있다(기본값은 COMPLETED
임). 모든 리스너와 마찬가지로 스텝
에 등록해야 한다.
다음 예제는 XML에서 잡 익스큐션컨텍스트(ExecutionContext)
로 스텝를 프로모션(promotion)하는 방법을 보여준다:
XML 구성
<job id="job1">
<step id="step1">
<tasklet>
<chunk reader="reader" writer="savingWriter" commit-interval="10"/>
</tasklet>
<listeners>
<listener ref="promotionListener"/>
</listeners>
</step>
<step id="step2">
...
</step>
</job>
<beans:bean id="promotionListener" class="org.spr....ExecutionContextPromotionListener">
<beans:property name="keys">
<list>
<value>someKey</value>
</list>
</beans:property>
</beans:bean>
다음 예제는 자바에서 잡 익스큐션컨텍스트(ExecutionContext)로 스텝을 승격(promotion)하는 방법을 보여준다:
자바 구성
@Bean
public Job job1(JobRepository jobRepository) {
return new JobBuilder("job1", jobRepository)
.start(step1())
.next(step1())
.build();
}
@Bean
public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("step1", jobRepository)
.<String, String>chunk(10, transactionManager)
.reader(reader())
.writer(savingWriter())
.listener(promotionListener())
.build();
}
@Bean
public ExecutionContextPromotionListener promotionListener() {
ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();
listener.setKeys(new String[] { "someKey" });
return listener;
}
마지막으로, 다음 예제와 같이 저장된 값을 잡 익스큐션컨텍스트(ExecutionContext)
에서 검색해야 한다:
public class RetrievingItemWriter implements ItemWriter<Object> {
private Object someObject;
public void write(Chunk<? extends Object> items) throws Exception {
// ...
}
@BeforeStep
public void retrieveInterstepData(StepExecution stepExecution) {
JobExecution jobExecution = stepExecution.getJobExecution();
ExecutionContext jobContext = jobExecution.getExecutionContext();
this.someObject = jobContext.get("someKey");
}
}