모르지 않다는 것은 아는것과 다르다.

Spring Batch

ExecutionContext

채마스 2022. 4. 2. 11:14

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

'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