ExecutionContext 란?
- Job에서 사용하는 데이터를 보관하는 보관소이다.
- Job처리를 통해서 언제나 참조 가능한 데이터를 보존하고, 추가 및 갱신도 가능하다.
- step처리의 결과 (결과값 등)를 다음 Step에 전달하는 것은 불가능하다.
- JobExecutionContext에 저장해서 어떤 step에서도 참조 가능하게 할 수 있다.
- Step 내 처리에서 데이터를 참조, 추가, 갱신이 자유롭게 가능하다.
- JobExecutonContext와 달리, 다른 step에서 참조가 불가능하다.
- 예를들어, chunk 처리에서 도중에 에러 종료로 끝난 경우 -> 어디까지 데이터를 읽어 들어드렸는지를 stepExecutionContext에 보관한다. -> 실패했던 곳에서부터 처리를 Step 을 재처리 할 수 있다.
코드 예시
@Bean
public Tasklet guLawdCdTasklet() {
return (contribution, chunkContext) -> {
StepExecution stepExecution = chunkContext.getStepContext().getStepExecution();
ExecutionContext executionContext = stepExecution.getJobExecution().getExecutionContext();
List<String> guLawdCdList;
if(!executionContext.containsKey("guLawdCdList")) {
guLawdCdList = lawdRepository.findDistinctGuLawdCd();
executionContext.put("guLawdCdList", guLawdCdList);
executionContext.putInt("itemCount", guLawdCdList.size());
} else {
guLawdCdList = (List<String>)executionContext.get("guLawdCdList");
}
Integer itemCount = executionContext.getInt("itemCount");
if (itemCount == 0){
contribution.setExitStatus(ExitStatus.COMPLETED);
return RepeatStatus.FINISHED;
}
itemCount--;
String guLawdCd = guLawdCdList.get(itemCount);
executionContext.putString("guLawdCd", guLawdCd);
executionContext.putInt("itemCount", itemCount);
contribution.setExitStatus(new ExitStatus("CONTINUABLE"));
return RepeatStatus.FINISHED;
}
}
- ExecutionContext에 저장할 데이터
- guLawdCd: 구 코드 -> 다음 스텝에서 활용할 값
- guLawdCdList: 구 코드 리스트
- itemCount: 남아있는 구 코드의 갯수
- 매 Step 마다 쿼리로 데이터를 불러온다면 성능적으로 문제가 있을 수 있다.
- 쿼리를 1번만 실행해서 그 값을 executionContext에 저장하고, 이후에는 executionContext 에서 가져온다.
- 만약 데이터 양이 너무 많아지면 executionContext 에 저장하는 것이 부담 스러울 수 있으니 상황에 맞게 처리하는것이 좋다.
- DB가 자주 조회되는 DB 인지, 이것저것 따져서봐 캐시에 넣든 executionContext 넣든 차라리 쿼리를 더 날리든 판단해야한다.
리팩토링
@RequiredArgsConstructor
public class GuLawdTasklet implements Tasklet {
private final LawdRepository lawdRepository;
private List<String> guLawdCdList;
private int itemCount;
private static final String KEY_ITEM_COUNT = "itemCount";
private static final String KEY_GU_LAWD_CD_LIST = "guLawdCdList";
private static final String KEY_GU_LAWD_CD = "guLawdCd";
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
ExecutionContext executionContext = getExecutionContext(chunkContext);
initList(executionContext);
initItemCount(executionContext);
if (itemCount == 0) {
contribution.setExitStatus(ExitStatus.COMPLETED);
return RepeatStatus.FINISHED;
}
itemCount--;
executionContext.put(KEY_GU_LAWD_CD, guLawdCdList.get(itemCount));
executionContext.putInt(KEY_ITEM_COUNT, itemCount);
contribution.setExitStatus(new ExitStatus("CONTINUABLE"));
return RepeatStatus.FINISHED;
}
private ExecutionContext getExecutionContext(ChunkContext chunkContext) {
StepExecution stepExecution = chunkContext.getStepContext().getStepExecution();
return stepExecution.getJobExecution().getExecutionContext();
}
private void initList(ExecutionContext executionContext) {
if (executionContext.containsKey(KEY_GU_LAWD_CD_LIST)) {
guLawdCdList = (List<String>) executionContext.get(KEY_GU_LAWD_CD_LIST);
} else {
guLawdCdList = lawdRepository.findDistinctGuLawdCd();
executionContext.put(KEY_GU_LAWD_CD_LIST, guLawdCdList);
executionContext.putInt(KEY_ITEM_COUNT, guLawdCdList.size());
}
}
private void initItemCount(ExecutionContext executionContext) {
if (executionContext.containsKey(KEY_ITEM_COUNT)) {
itemCount = executionContext.getInt(KEY_ITEM_COUNT);
} else {
itemCount = guLawdCdList.size();
}
}
}
- 먼저 문자열을 상수로 빼준다.
- 메소드로 추출해서 의도를 확실히 나태낸다.
- 조건은문 옳지 않는 경우를 먼저 검사하는 것보다, 옳은 경우를 먼저 검사하는 것이 코드 가독성이 좋다.
ExecutionContext 사용시 주의 사항
@JobScope
@Bean
public Step contextPrintStep(Tasklet contextPrintTasklet) {
return stepBuilderFactory.get("contextPrintStep")
.tasklet(contextPrintTasklet)
.build();
}
@StepScope
@Bean
public Tasklet contextPrintTasklet(
@Value("#{jobExecutionContext['guLawdCd']}") String guLawdCd
) {
return (contribution, chunkContext) -> {
System.out.println("[contextPrintStep] guLawdCd = " + guLawdCd);
return RepeatStatus.FINISHED;
};
}
- 위 코드를 아래와 같이 바꿔보았다. 위와 아래는 같은 코드인가?
@JobScope
@Bean
public Step contextPrintStep(@Value("#{jobExecutionContext['guLawdCd']}") String guLawdCd) {
return stepBuilderFactory.get("contextPrintStep")
.tasklet((contribution, chunkContext) -> {
System.out.println("[contextPrintStep] guLawdCd = " + guLawdCd);
return RepeatStatus.FINISHED;
})
.build();
}
- 위의 코드에서는 guLawdCd 는 같은 값만 지정된다. -> @JobScope 이기 때문에 하나의 Job 당 1번만 호출 되기 때문이다.
- 따라서 스탭에 따라 guLawdCd 값이 달라져야 하는 경우엔 아래와 같이 @StepScope 에서 Step 마다 guLawdCd 을 할당해 줘야 한다.
- 다시 정리하면, Job 의 ExecutionContext 와 Step의 ExecutionContext 는 다르다.
- chunkContext.getStepContext().getJobExecutionContext() 는 Job 의 ExecutionContext 이다.
- 변경할 수 없는 값만 넣을 수 있다. 그 이유는 아래와 같이 unmodifiedableMap 이기 때문이다.
- 따라서 get 은 가능하지만 put 을 하게 되면 에러가 발생한다.
- chunkContext.getStepContext().getStepExecution().getJobExection().getExecutionContext() 가 Step의 ExecutionContext 이다.
REFERENCES
- 황지연님의 스프링 배치
- https://hyowong.tistory.com/97
'Spring Batch' 카테고리의 다른 글
대용량 작업 분산처리하기(With Spring Batch) (0) | 2023.06.04 |
---|---|
Scaling and Parallel Processing(With Spring Batch) (0) | 2023.05.20 |
JobParameterValidator (0) | 2022.04.02 |
Job, Step (0) | 2022.04.02 |
Spring Batch 메타 테이블 (0) | 2022.04.02 |