Spring/Spring Batch

[Spring Batch] 3장 : 배치의 도메인 언어(The Domain Language of Batch) 2 (Step, StepExecution, ExecutionContext, Job Repository / Launcher, ItemReader / Writer)

공대키메라 2023. 11. 21. 22:38

지난 시간에 배치의 도메인 언어중 Job, JobInstance, JobParameter, JobExecution에 대해 알아보았다.

(지난 내용을 보고 싶다면 여기 클릭!)

 

저번에 내용이 너무 길었던 관계로 끊은 부분에서 이어서 Step부터 다시 읽을 것이다.

 


 

1. Step

Step은 한 배치 잡(batch job)의 독립적이고 연속적인  단계를 캡슐화한 하나의 도매인 객체이다.

그러므로 모든 job은 하나 혹은 더 많은 Step들로 구성되있다. Step은 실제 배치 처리를 정의하고 제어하는에 필요한 모든 정보를 포함한다. 이것은 어쩔수없이 모호한 묘사인데(?) 아무 Step의 내용들이 Job을 사용하는 개발자의 재량에 달렸기 때문이다. Step은 개발자의 요구사항에 따라 간단하거나 복잡할 수도 있다. 간단한 Step은 파일에서 데이터베이스로 데이터를 로드할수도 있다. 더 복잡한 Step은 처리 부분에서 적용된 복잡한 비즈니스 규칙을 가질수도 있다.

Job과 함께 Step도 유일한 JobExecution과 상관관계가 있는 개인적인 StepExecution을 가진다. 다음 그림처럼 말이다.

 

Figure 4. Job Hierarchy With Steps

 

 

여기서 Job와 Step이 관계가 있다는데 무슨 관계가 있는 것일까?

그것은 Step의 StepExecution중 하나로도 실패하면 JobExecution 도 실패한다는 것이다.
Step의 StepExecution이 모두 정상적으로 완료 되어야만 JobExecution이 정상적으로 완료된다.
또한, 하나의 Job에 여러 개의 Step으로 구성했을 궁여 각 StepExecution은 하나의 JobExecution을 부모로 가진다!

우선 다음 글을 읽어보자. 과연 다음과 같은 설명이 있는지...

 

2. StepExecution

StepExecution은 Step의 실행에 대한 하나의 시도를 나타낸다. 새로운 StepExecution이 매번 Step이 실행될 때 생성된다. 

이것은 JobExecution과 비슷하다. 하지만, 하나의 스텝이 전에 실패한 스템 때문에 실패한다면, 실행이 저장되지는 않는다.  StepExecution은 Step이 실제로 실행됐을 때 생성된다.

 

말을 좀 다시 정리해보자.
우선 스텝이 순서대로 A -> B -> C 순으로 실행된다고 하자.
현재 A를 실행한 상태이고 B가 실행되어야 하는데 B에서 실패를 했다면!?!?
그러면 C의 Step을 실행하기 위한 StepExecution은 생성되지 않았다는 말이다.

 

Step 실행들은 StepExecution의 객체들에 의해 나타난다. 각각의 실행은 step와 JobExecution 그리고 transaction 관련 데이터와 연관이 있는 레퍼런스를 포함한다. commit, rollback 횟수 그리고 시작과 종료 시간 같은 것들 말이다.

추가적으로, 각각의 스텝 실행이 ExecutionContext 를 포함하는데 이는 개발자가 배치 실행 사이에 영속화하기 필요로 하는 어떤 데이터도 포함한다. 통계 혹은 재시작이 필요한 상태 정보처럼 말이다.

다음 테이블은 StepExecution의 속성을 보여준다.

 

Property                                                                                          Definition
Status A BatchStatus object that indicates the status of the execution. While running, the status is BatchStatus.STARTED. If it fails, the status is BatchStatus.FAILED. If it finishes successfully, the status is BatchStatus.COMPLETED.
startTime A java.time.LocalDateTime representing the current system time when the execution was started. This field is empty if the step has yet to start.
endTime A java.time.LocalDateTime representing the current system time when the execution finished, regardless of whether or not it was successful. This field is empty if the step has yet to exit.
exitStatus The ExitStatus indicating the result of the execution. It is most important, because it contains an exit code that is returned to the caller. See chapter 5 for more details. This field is empty if the job has yet to exit.
executionContext The “property bag” containing any user data that needs to be persisted between executions.
readCount The number of items that have been successfully read.
writeCount The number of items that have been successfully written.
commitCount The number of transactions that have been committed for this execution.
rollbackCount The number of times the business transaction controlled by the Step has been rolled back.
readSkipCount The number of times read has failed, resulting in a skipped item.
processSkipCount The number of times process has failed, resulting in a skipped item.
filterCount The number of items that have been “filtered” by the ItemProcessor.
writeSkipCount The number of times write has failed, resulting in a skipped item.

 

해당 테이블은 번역하기 너무 왠지 모르게 귀찮다... 그래도 대강 읽어보면 어느정도 이해하기가 쉬우니 영어공부겸 읽어보자.

 

3. ExecutionContext

ExecutionContext는 개발자에게 StepExecution 또는 JobExecution 객체에 scope된 영속화된 상태를 저장할 장소를 제공하기 위해 프레임워크에 의해 영속화되고 제어되는 key/value 한쌍의 모음이다.

최고의 사용 예시는 쉬운 재시작이다. 한 예로 flat file 입력을 사용할 때, 개별 라인들을 처리하면서 프레임워크는 주기적으로 커밋 시점(commit point)에 ExecutionContext를 영속화한다. 그렇게 함으로서 ItemReader가 전원이 꺼져도(power down) 혹은 작동하는 동안 치명적인 결함이 발생해고 그것의 상태를 저장한다. 필요한 모든 것은 context로 현재 읽은 라인의 수를 넣는것이다. 다음의 예시가 보여주듯이 프레임워크가 나머지를 처리한다.

 

executionContext.putLong(getKey(LINES_READ_COUNT), reader.getPosition());

 

한 예로 Job 스테레오타입 섹션의 EndOfDay를 사용하고 파일을 데이터베이스로 로드하는 loadData라는 한 스텝이 있다고 가정하자. 처음 시도가 실패한 후 메타데이터 테이블은 다음 예시처럼 보일 것이다.

 

Table 9. BATCH_JOB_INSTANCE

JOB_INST_ID JOB_NAME
1 EndOfDayJob

 

Table 10. BATCH_JOB_EXECUTION_PARAMS

JOB_INST_ID TYPE_CD KEY_NAME DATE_VAL
1 DATE schedule.Date 2017-01-01

 

Table 11. BATCH_JOB_EXECUTION

JOB_EXEC_ID JOB_INST_ID START_TIME END_TIME STATUS
1 1 2017-01-01 21:00 2017-01-01 21:30 FAILED

 

Table 12. BATCH_STEP_EXECUTION

STEP_EXEC_ID JOB_EXEC_ID STEP_NAME START_TIME END_TIME STATUS
1 1 loadData 2017-01-01 21:00 2017-01-01 21:30 FAILED

 

Table 13. BATCH_STEP_EXECUTION_CONTEXT

STEP_EXEC_ID SHORT_CONTEXT
1 {piece.count=40321}

 

앞의 경우에 Step은 30분 동안 작동했고 40,321 조각(pieces)을 처리했는데 이것은 이 시나리오에서 파일에 줄을 나타낼것이다.  (즉, 40,321이 파일의 40,321줄 이라는 말이다). 이 값은 프레임워크에 의해 각각 커밋되기 전에 update되고 ExecutionContext내에서 entries와 상응하는 다수의 row를 포함한다. 커밋 전에 알아야 하는 것은 커밋이 다양한 StepListener의 구현체중 하나를 필요로 한다는 것이며 그것은 이 가이드의 나중에 더 자세히 논의된다. ItemReader가 열리면 다음 예시에서 보이는 것 처럼 context에 아무 저장된 상태가 있는지 그리고 그곳으로부터 그 스스로를 초기화 할 수 있는지 확인할 수 있다.

 

if (executionContext.containsKey(getKey(LINES_READ_COUNT))) {
    log.debug("Initializing for restart. Restart data is: " + executionContext);

    long lineCount = executionContext.getLong(getKey(LINES_READ_COUNT));

    LineReader reader = getReader();

    Object record = "";
    while (reader.getPosition() < lineCount && record != null) {
        record = readLine();
    }
}

 

이 경우에 앞선 코드가 작동한 후에 현재 라인은 40,332다. Step이 실행이 실패한 곳에서부터 다시 시작할 수 있는곳이다. 

또한 실행에 대해 영속화되길 필요로하는 통계에 대한 ExecutionContext를 사용할 수 있다. 예를 들어 하나의 flat file이 다른 줄들 사이사이에 존재하는 처리를 위한 주문들을 포함한다면 얼마나 많은 주문들이 처리되왔는지 저장할 필요가 있고 처리된 주문의 총 수에 대한 이메일이 Step 의 마지막에 전송될 수 있다.

 

프레임워크는 개발자들을 위해 이것을 저장관리한다. 개별 JobInstance로 그것을 정확하게 scope처리해

프레임워크는 개발자를 위해 이를 저장하여 개별 JobInstance로 범위를 올바르게 지정하도록 처리한다.

이미 있는 ExecutionContext가  사용될지 말아야할지 아는 것은 매우 어려울 수 있다.

 

예를 들어, EndOdDay 예시를 쓸 때, 01-01 실행이 두번 째 다시 실행하면 프레임워크는 같은 JobInstance인 것을 이해하고 개별의 Step 기초에서 데이터베이스로부터 ExecutionContext를 가져와서 Step에게 그것을 넘긴다. 정반대로 01-02 실행에서 프레임 워크는 다른 instance라는것을 이해하고 empty context가 Step에게 넘겨져야만 한다. 프레임 워크게 개발자에게 만들어주는 이러한 많은 형태의 결정이 있는데 그 상태가 정확한 시간에 그들에게 보내지도록 한다.

 

ExecutionContext의 클라이언트들은 유의해야하는데 이것이 공유된 keyspace를 생성하기 때문이다. 그 결과, 관리가 필요한데 값을 넣는것이 데이터가 덮어씌워지지 않도록 확실하게 해야한다. 하지만 Step 은 context에서 데이터를 절대 저장하지 않고 프레임워크에 불리하게 영향을 끼치지 않는다. JobExecution당 하나 이상의 ExecutionContext가 있고 모든 StepExecution마다 하나가 있다는 점을 유의하라. 예를 들어, 다음 코드를 살펴보자.

 

ExecutionContext ecStep = stepExecution.getExecutionContext();
ExecutionContext ecJob = jobExecution.getExecutionContext();
//ecStep does not equal ecJob

 

이 comment에 적힌 것 처럼, ecStep은 ecJob과 같지 않다. 두 개의 다른 ExecutionContext가 있다. Step에 범위를 둔 하나는 Step 의 모든 commit에 저장된다. 반면에 Job에 범위를 둔 것은 매 Step의 실행 사이에 저장된다.

 

맨 마지막이 재미있는데 stepExetion도, jobExecution도 결국 ExecutionContext를 꺼낼 수 있는데 ExecutionContext에 데이터가 저장되는 순서가 조금씩 다르다는점에 유의하자.  (그래서 다르다는 것이다!)

공유 범위(scope)는 다음과 같다.
=> Step 범위 : 각 Step 의 StepExecution 에 저장되며 Step 간 서로 공유가 안된다.
=> Job 범위 : 각 Job의 JobExecution 에 저장되며 Job 간 서로 공유 안되며 해당 Job의 Step 간 서로 공유됨

 

4. JobRepository

JobRepository는 이전에 저장된 모든 정형화된것에 대한 영속성 메커니즘이다. (? 말이 왜이리 어려워...)

JobLauncher, Job 그리고 Step구현체들에 대한 CRUD 작업을 제공한다. Job이 처음 실행되면, JobExecution이 repository로 부터 얻어진다. 또한 실행 과정 동안 StepExecution과  JobExecution 구현체들이 그들을 repository에 넘김으로서 영속화된다. (저장된다는 말이다)

 

자바 설명을 사용할 때, @EnableBatchProcessing 어노테이션(annotation)은 자동적으로 설정된 component들 중의 하나를 JobRepository에게 제공한다. 

 

여기서 Spring Stereotype Annotation하면 @Controller, @Service, @Repository가 있을 것이다. 

@Repository는 데이터 액세스 계층에 사용되며(마커 역할) 데이터 액세스 예외를 변환한다.
그렇다면 JobRepository은? 결국 @Repository처럼 무언가를 데이터처리하는 것으로 짐작할 수 있을 것 같다.
물론 Batch에 관련해서 말이다.(JobRepository니까!)

즉, JobRepository는 배치 작업 중의 정보를 저장하는 저장소 역할을 하며(persist) Job이 언제 수행되었고, 언제 끝났으며, 몇 번이 실행되었고 실행에 대한 결과 등의 배치 작업의 수행과 관련된 모든 meta data 를 저장한다.

 

5. JobLauncher


JobLauncher는 JobParameter를 받은 Job을 실행할 때 간단한 인터페이스를 나타낸다. 다음처럼 말이다

 

public interface JobLauncher {

public JobExecution run(Job job, JobParameters jobParameters)
            throws JobExecutionAlreadyRunningException, JobRestartException,
                   JobInstanceAlreadyCompleteException, JobParametersInvalidException;
}

 

구현체가 유효한 JobExecution을 JobRepostory로 부터 얻어오고 Job을 실행하도록 예상된다.

 

마지막 문장이 이해가 안돼서 JobRepository를 읽어본 결과
"Job이 처음 실행되면, JobExecution이 repository로 부터 얻어진다" 라고 대놓고 적혀 있어서 나의 기억력에 놀랐다...!!!

 

6. ItemReader

ItemReader는 하나의 Step, 한 번에 하나의 아이템의 입력을 가져오는걸 나타내는 추상적 개념이다.

ItemReader는 제공할 수 있는 아이템을 다 소진하면 null을 반환함으로 이를 알려준다. ㅊ

 

7. ItemWriter

ItemWriter는 Step, 하나의 배치 혹은 한 번에 여러 아이템들의 chunk의 출력을 나타내는 추상적 개념이다. 일반적으로 ItemWriter는 다음에 가져와야 하는 입력에 대한 아무런 지식이 없고 오직 현재의 호출로 넘겨지는 아이템만 안다

(이 말은 현재 실행중인 ItemReader로 넘겨지는 데이터만 안다는 것으로 읽힌다.)

더 많은 내용은 다음에서 더 알 수 있다. (Readers And Writers)

 

8. ItemProcessor

ItemProcessor는 한 아이템의 비즈니스 처리를 나타내는 추상적 개념이다. ItemReader가 하나의 아이템을 읽는 동안, ItemWriter가 하나의 아이템을 쓰고, ItemProcessor가 변환할 접근 점을 제공하거나 다른 비즈니스 처리를 적용한다. 

만약 아이템을 처리하는 동안 아이템이 유효한지 않다고 판단하면 null을 반환해서 그 아이템이 쓰여지지 않아야(should not be written out) 한다고 알린다.


 

이렇게 Spring Batch Domain 부분을 한 번 다 읽어보았다.

생각보다 내용이 굉장히 길고 개념도 처음 접한다면 난해할 것이다. 

아직도 필자는 정확하게 다 기억은 못해서 그때 그때 찾아서 보려고 노력한다.

 

사이사이에 더 자세한 내용들이 있지만 내용도 많고 다음에 또 내용이 나올것이기에 여기까지 잘 기억하면 될 것 같다는 생각이 든다. 

 

출처:

https://docs.spring.io/spring-batch/docs/current/reference/html/domain.html#domainLanguageOfBatch

스프링 배치 - Spring Boot기반으로 개발하는 Spring Batch(정수원 저)