노트북을 열고.

[spring boot batch] 3. 배치실행의 모든기록. 메타테이블 본문

SpringBoot

[spring boot batch] 3. 배치실행의 모든기록. 메타테이블

ahndy84 2019. 9. 2. 22:07

1. 메타(Meta)테이블, 존재의 이유.

이전 장에서 우리는 배치를 실행시키기 위한 총 9개의 테이블을 생성하였습니다.

사실 아주 간편하게 코드로만 배치프로세서를 구현할 줄 알았지 Batch프로세스만을 위한 테이블을 무려 9개씩이나 만들어야 한다면, 마치 배보다 배꼽이 더 큰 것 마냥 느껴지기도 합니다.

 

영화 '사랑에 대한 모든 것' (원제는 'The Theory of Everything')

이 9개의 테이블은 메타테이블이라고 불리웁니다.

meta라는 어원('~에 관하여, About)에서 알 수 있 듯 이들은 우리가 구현한 BatchJob이 실행될 때마다 그 BatchJob에 관련한 모든 것를 꼼꼼하고 세세하게 기록됩니다.

 

정확히는, 우리가 구현한 'BatchJob을 실행시키기 위한 목적을 가진 테이블은 아닙니다'.

필요에 따라 이 테이블 없이도 스프링부트 배치프레임워크로 실행가능한 배치잡들을 약간의 커스터마이징을 통해 만들 수 있습니다. 그럼에도 불구하고 이 테이블에 대한 존재의 이유를 알면 왜 이 테이블이 없으면 Batch Job실행이 안되었는지를(=안될만큼 중요한지) 알 수 있습니다.

스프링 배치잡 메타테이블

 

아, 됐고! 난 한번만 잘 돌아가면 돼.

그럴리가요.

우리가 만든 배치프로세스는 대개의 경우 어떤 주기에 따라 반복적으로 실행될 것입니다.

그 주기가 일(Day)단위일수도 있고 가까이는 분(Minute)단위일 수도 있겠죠. 경우 따라선 여러의 Batch Job이 단위를 지어 서로 상호연관지어진 순서에 따라 수행될 수도 있습니다.

 

자 이쯤이면 감이 오실 것도 같습니다. 아래의 내용의 Batch Job이 구현되었다고 가정해 봅시다.

1. 매일 미납발생일로부터 30일이 지난 회원들을 대상으로 자동결제를 시도하는 BatchJob

2. 1번이 실행된 이후 자동결제에 실패한 회원들에 한해 미납안내SMS발송을 시도하는 BatchJob

어느날 어떠한 연유였는지 '이미 30일을 넘기기 전 미납금액 결제를 완료한 회원에게서 미납안내SMS을 수신받았다는 민원'을 받게 됩니다.

 

1번에 해당하는 BatchJob이 동작하는데 어떤 문제가 발생하여 처리도중 실패로 끝나게 되고 뒤이어 2번의 BatchJob이 동작하게 된 경우입니다.

 

더 큰 문제는 이러한 이슈가 꽤 오래 전부터 발생되어 왔던 걸 최근에서야 발견하게 된 경우입니다.

 

배치잡의 스케쥴러의 이벤트로그와 같은 지표를 확인하여 확인하는 방법이 있겠지만 이벤트로그는 단순히 해당 BatchJob의 실행 성공과 실패여부만 알 수 있을 뿐 해당 배치잡 내부에 처리내역까지는 확인하는데에는 한계가 존재합니다.

 

해당 서버로그 파일을 뒤져 일일이 로그를 뒤져 확인하는 방법도 있을 수 있지만 그럴 시간에 우린 더 가치 있는 일을 하는 것이 현명할 것입니다.

 

메타테이블에서 기록되고 있는 BatchJob의 메타정보는 다음의 정보를 제공하고 있습니다.

1. 이전에 실행했던 BatchJob정보를 일련목연하게 확인할 수 있습니다.

2. 가장 최근에 실패한 BatchJob이 무엇이었는지 마지막에 성공한 BatchJob은 어떤 것이었는지 확인할 수 있습니다.

3. Job안에 Step은 어떻게 구성되어 있고 그 Step들 중 어떤 Step이 실패하고 어떤 Step이 성공했는지를 확인할 수 있습니다.

 

BatchJob에 대한 실행정보 뿐 아니라 BatchJob안에 단계별 실행여부 그리고 그 실행에 대한 성공과 실패여부까지 세세히 알수 있으니, 우리는 일련의 배치프로세스가 우리의 기대했던 것과 다르게 동작하였을 때 누구보다 빠르게 남들과는 다르게 대응할 수가 있게 됩니다.

 

가수 아웃사이더

 

결국 메타테이블의 존재의 이유는 우리가 구현한 BatchJob에 대한 운용관리상의 이슈로부터 자유로울 수 있게 됩니다. 반대로 그 존재가 없을 경우에는 운용상의 발생될 수 있는 다양한 문제들에 대한 원인과 해결을 찾는데에 많은 어려움을 봉착하게 됩니다.

'측정할 수 없으면 관리될 수 없고 관리될 수 없으면 개선될 수 없다.' 
- Michael E. Porter (마이클 포터)

 

2. 메타테이블 스키마 구조

메타테이블 스키마 (https://docs.spring.io/spring-batch/3.0.x/reference/html/metaDataSchema.html)

 

메타테이블명은 접두어만 보더라도 어떤 정보를 기록하는 테이블인지 쉽게 확인이 가능합니다.
BATCH_JOB으로 시작하는 테이블은 Job객체에 관련되는 메타정보를 관리하는 테이블이고

BATCH_STEP으로 시작하는 테이블은 Job객체안에 있는 Step객체와 관련된 메타정보를 관리하는 테이블입니다.

먼저 BATCH_JOB으로 시작하는 테이블을 살펴봅시다.

3. Job관련 정보를 저장하는 Table

  • BATCH_JOB_INSTANCE
  • BATCH_JOB_EXECUTION
  • BATCH_JOB_EXECUTION_CONTEXT
  • BATCH_JOB_EXECUTION_PARAMS
  • BATCH_JOB_EXECUTION_SEQ
  • BATCH_JOB_SEQ

 

BATCH_JOB_INSTANCE

Job이 실행될때에 생성되는 JobInstance에 관한 정보를 저장하고 있습니다.

(주의 : JobInstance는 Job이 실행될 때마다 생성되지 않습니다. Appication이 실행될 때 Argument에 할당받은 고유한 JobParameter값에 따라 새로 생성됩니다.)

CREATE TABLE BATCH_JOB_INSTANCE  (
    JOB_INSTANCE_ID BIGINT  PRIMARY KEY ,
    VERSION BIGINT,
    JOB_NAME VARCHAR(100) NOT NULL ,
    JOB_KEY VARCHAR(2500)
);

JOB_INSTANCE_ID

실행된 Job을 고유하게 식별될할 수 있는 기본 키입니다. JobInstance의 getId 메서드를 통해 값을 얻어옵니다.

VERSION

해당 레코드에 update 될때마다 1씩 증가합니다.

JOB_NAME

JobBuildFactory에서 Job을 빌드할 당시 get메서드를 사용하여 해당 Job의 이름을 부여하였습니다. 그러니 반드시 NULL이 올 수 없겠죠. 바로 그 값(이름)을 기록합니다.

JOB_KEY

동일한 Job이름의 JobInstance는 Job의 실행시점에 부여되는 고유한 JobParameter의 값을 통해 식별됩니다. 
그리고 이렇게 식별되는 값의 직렬화(serialization)된 결과를 JOB_KEY라는 값으로 기록됩니다.
(JobInstance와 JobParameter의 관계에 대해선 아래에서 더 자세히 설명하겠습니다.)

 

 

BATCH_JOB_EXECUTION

JobInstance을 Job의 실행단위를 구분짓는 것이라면 그 실행단위에 대한 실행횟수(1번 또는 1번이상)를 나타내는JobExecute에 관한 정보를 담고 있습니다.
매번 Job이 실행될때마다 이 테이블에 새로운 레코드가 쌓이겠죠.

CREATE TABLE BATCH_JOB_EXECUTION  (
    JOB_EXECUTION_ID BIGINT  PRIMARY KEY ,
    VERSION BIGINT,
    JOB_INSTANCE_ID BIGINT NOT NULL,
    CREATE_TIME TIMESTAMP NOT NULL,
    START_TIME TIMESTAMP DEFAULT NULL,
    END_TIME TIMESTAMP DEFAULT NULL,
    STATUS VARCHAR(10),
    EXIT_CODE VARCHAR(20),
    EXIT_MESSAGE VARCHAR(2500),
    LAST_UPDATED TIMESTAMP,
    JOB_CONFIGURATION_LOCATION VARCHAR(2500) NULL,
    constraint JOB_INSTANCE_EXECUTION_FK foreign key (JOB_INSTANCE_ID)
    references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID)
);

JOB_EXECUTION_ID

JobInstance에 대한 실행횟수(JobExecution)를 고유하게 식별될할 수 있는 기본 키입니다.

VERSION

DB에 Record가 터치(update)될 때마다 누적됩니다.

JOB_INSTANCE_ID

실행된 JobExecution에 대한 실행단위, JobInstance의 키를 기록합니다.

CREATE_TIME

실행(Execution)이 생성된 시점을 TimeStamp 형식으로 기록합니다.

START_TIME

실행(Execution)이 시작된 시점을 TimeStamp 형식으로 기록합니다.

END_TIME

실행이 종료된 시점을 TimeStamp으로 기록합니다.
여기서 말하는 종료는 성공 또는 실패와 상관없이 순수히 끝난 시점을 의미합니다. 실행 도중 일부유형의 오류가 발생했거나 프레임워크 내부에서 값을 저장하기도 전에 실패되었을 경우 값이 비어 있을 수 있습니다.

STATUS

실행의 상태를 COMPLETED, STARTED, ETC 와 같은 미리 정의된 Enumeration값으로 기록합니다.

EXIT_CODE

실행 종료코드를 기록합니다.

EXIT_MESSAGE

Status가 실패(Fail)일 경우 실패한 원인에 대하여 추적이 가능한 범위내에서 기술하여 문자열형태로 기록합니다.

LAST_UPDATED

실행(Execution)이 마지막으로 영속(persisted)에 놓인 시점을 TimeStamp 형식으로 기록합니다.

 

 

BATCH_JOB_EXECUTION_PARAMS

JobParameter에 대한 모든정보를 기록하고 있습니다.

JobParameter값에 따라 JobInstance가 생성됩니다. (=동일한 JobParameter값으로 JobApplication을 실행하면 BATCH_JOB_INSTANCE테이블에 기록되지 않습니다.)

CREATE TABLE BATCH_JOB_EXECUTION_PARAMS  (
    JOB_EXECUTION_ID BIGINT NOT NULL ,
    TYPE_CD VARCHAR(6) NOT NULL ,
	KEY_NAME VARCHAR(100) NOT NULL ,
	STRING_VAL VARCHAR(250) ,
	DATE_VAL DATETIME DEFAULT NULL ,
	LONG_VAL BIGINT ,
	DOUBLE_VAL DOUBLE PRECISION ,
	IDENTIFYING CHAR(1) NOT NULL ,
	constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID)
	references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
);

 

 

BATCH_JOB_EXECUTION_CONTEXT

작업의 ExecutionContext와 관련된 모든 정보를 기록합니다.

1개의 JobExecution에 각 JobExecutionContext가 있으며 특정 작업 실행에 필요한 모든 작업 레벨 데이터를 포함합니다. 일반적으로 JobInstance가 중지 된 위치에서 다시 시작할 수 있도록, 실패(Fail)이후 지점에 State를 나타냅니다.

CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT  (
    JOB_EXECUTION_ID BIGINT PRIMARY KEY,
    SHORT_CONTEXT VARCHAR(2500) NOT NULL,
    SERIALIZED_CONTEXT CLOB,
    constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID)
    references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
);

STEP_EXECUTION_ID

Step에 대한 실행횟수정보를 고유하게 식별될할 수 있는 기본 키입니다.

SHORT_CONTEXT

SERIALIZED_CONTEXT의 버전을 나타내는 문자열

SERIALIZED_CONTEXT

직렬화(serialized)된 전체 컨테스트

그외 BATCH_JOB_EXECUTE_SEQ, BATCH_JOB_SEQ는 시퀀스관리 테이블입니다.

 

4. Step관련 정보를 저장하는 Table

  • BATCH_STEP_EXECUTION
  • BATCH_STEP_EXECUTION_CONTEXT

 

BATCH_STEP_EXECUTION

JobExecution에 대한 Step객체 정보를 기록하고 있습니다.
Job내부에서 각각 실행되는 Step 정보를 순서대로 확인할 수 있습니다. BATCH_JOB_EXECUTION과 매우 흡사합니다.

CREATE TABLE BATCH_STEP_EXECUTION  (
    STEP_EXECUTION_ID BIGINT  PRIMARY KEY ,
    VERSION BIGINT NOT NULL,
    STEP_NAME VARCHAR(100) NOT NULL,
    JOB_EXECUTION_ID BIGINT NOT NULL,
    START_TIME TIMESTAMP NOT NULL ,
    END_TIME TIMESTAMP DEFAULT NULL,
    STATUS VARCHAR(10),
    COMMIT_COUNT BIGINT ,
    READ_COUNT BIGINT ,
    FILTER_COUNT BIGINT ,
    WRITE_COUNT BIGINT ,
    READ_SKIP_COUNT BIGINT ,
    WRITE_SKIP_COUNT BIGINT ,
    PROCESS_SKIP_COUNT BIGINT ,
    ROLLBACK_COUNT BIGINT ,
    EXIT_CODE VARCHAR(20) ,
    EXIT_MESSAGE VARCHAR(2500) ,
    LAST_UPDATED TIMESTAMP,
    constraint JOB_EXECUTION_STEP_FK foreign key (JOB_EXECUTION_ID)
    references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
);

STEP_EXECUTION_ID

Step에 대한 실행횟수정보를 고유하게 식별될할 수 있는 기본 키입니다.

VERSION

DB에 record가 터치(update)될 때마다 누적됩니다.

STEP_NAME

StepBuildFactory에서 Step을 빌드할 당시 get메서드를 사용하여 해당 Step의 이름을 부여하였습니다. 
그러니 반드시 NULL이 올 수 없겠죠. 바로 그 값(이름)을 기록합니다.

JOB_EXECUTION_ID

Job이 실행될 때마다 그 Job에 정의되어 있는 Step이 실행되고 그 Step의 실행정보가 이 테이블에 저장되겠죠. StepExecutionJobExecution안에 귀속됩니다.
즉,  
StepExecutionJobExecution안에 귀속됩니다. 그러니 StepExecution에 대한 JobExecution의 식별키 정보를 기록하고 있습니다.

START_TIME

실행(Execution)이 시작된 시점을 TimeStamp 형식으로 기록합니다.

END_TIME

실행이 종료된 시점을 TimeStamp으로 기록합니다.
여기서 말하는 '종료'는 '성공' 또는 '실패'와 상관없이 순수히 끝난 시점을 의미합니다. 
실행 도중 일부유형의 오류가 발생했거나 프레임워크 내부에서 값을 저장하기도 전에 실패되었을 경우 값이 비어 있을 수 있습니다.

STATUS

실행의 상태를 COMPLETED, STARTED, ETC 와 같은 미리 정의된 Enumeration값으로 기록합니다.

COMMIT_COUNT

트랜잭션 당 커밋되는 수를 기록합니다.
우리는 Chunk라는 것을 이용해 1번의 트랙잭션에 처리되는 커밋수를 정의한 기억이 있습니다.

READ_COUNT

실행시점에 Read한 Item 수를 기록합니다.

FILTER_COUNT

실행도중 필터링된 Item 수를 기록합니다.

WRITE_COUNT

실행도중 저장되고 커밋된 Item 수를 기록합니다.

READ_SKIP_COUNT

실행도중 Read가 스킵(Skip)된 Item 수를 기록합니다.

WRITE_SKIP_COUNT

실행도중 write가 스킵(Skip)된 Item 수를 기록합니다.

PROCESS_SKIP_COUNT

실행도중 Process가 스킵(Skip)된 Item 수를 기록합니다.

ROLLBACK_COUNT

실행도중 rollback이 일어난 수를 기록합니다. 프로시저복구를 생략하고 재시도된 롤백건도 포함합니다.

EXIT_CODE

실행 종료코드를 기록합니다.

EXIT_MESSAGE

Status가 실패(Fail)일 경우 실패한 원인에 대하여 추적이 가능한 범위내에서 기술하여 문자열형태로 기록합니다.

LAST_UPDATED

실행중에 마지막으로 영속(persisted)에 놓인 시점을 TimeStamp 형식으로 기록합니다.

 

 

BATCH_STEP_EXECUTION

Step의 ExecutionContext와 관련된 모든 정보를 보유합니다. 

StepExecution당 1개의 ExecutionContext가 있으며 특정 단계 실행을 위해 유지해야하는 모든 데이터가 포함됩니다. 

일반적으로 JobInstance가 중지 된 위치에서 다시 시작할 수 있도록, 실패(Fail)이후 지점에 State를 나타냅니다.

CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT  (
    STEP_EXECUTION_ID BIGINT PRIMARY KEY,
    SHORT_CONTEXT VARCHAR(2500) NOT NULL,
    SERIALIZED_CONTEXT CLOB,
    constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID)
    references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID)
);

STEP_EXECUTION_ID

컨텍스트가 속하는 StepExecution을 나타내는 외래 키.
주어진 실행과 관련된 행이 두 개 이상있을 수 있습니다.

SHORT_CONTEXT

SERIALIZED_CONTEXT의 버전을 나타내는 문자열

SERIALIZED_CONTEXT

직렬화(serialized)된 전체 컨테스트

BATCH_STEP_EXECUTION_SEQ는 시퀀스관리 테이블입니다.

 

5. JobJobInstanceJobExecuttion

쓰다보니 또 한번 구구절절한 내용이 되었습니다. 다시 돌아가서 지금까지 우리가 한 내용을 정리해보면 다음과 같습니다.

1. BatchJob 프로세스 구현(unPaidMemberJob)

2. Job을 실행할때매다 그 Job의 모든것, 즉 Meta정보를 기록하는 9개의 테이블 생성 

이제 배치잡을 실행할 수 있는 준비는 모드 끝났으니 한번 실행하여 봅시다.

(...가 아니고 이미 이전장에서 1번의 실행을 했을 것이죠.)

 

먼저, BATCH_JOB_INSTANCE 테이블을 확인하면

BATCH_JOB_INSTANCE (클릭하면 확대됩니다.)

 

최초 실행한 BatchJob에 대한 JobInstance가 생성되면서 그 JobInstance 정보가 삽입되었습니다.

...
@Bean
public Job unPaidMemberJob(
	JobBuilderFactory jobBuilderFactory,
	Step unPaidMemberJobStep
) {
	log.info("********** This is unPaidMemberJob");
	return jobBuilderFactory.get("unPaidMemberJob")
	.preventRestart()
	.start(unPaidMemberJobStep)
	.build();
}
...

그렇다면 이제 이 JobInstance의 실행단위라 할 수 있는 JobExecution가 생성되었는지를 확인해봐야겠죠?

 

이어, BATCH_JOB_EXECUTE 테이블을 확인하면

BATCH_JOB_EXECUTE (클릭하면 확대됩니다)

방금 전, 실행했던 JobInstance에 대한 JobExecution이 생성되고 그 JobExecution 정보가 삽입되었습니다.

 

Job은 실행할때에 JobInstance가 생성되고 이후 JobInstance는 Job이 실행되는 횟수만큼 JobExecution을 새롭게 생성하면서 실행됩니다. 여기까지가 Job과 관련된 내용입니다.

 

 

중복해서 실행된 JobInstance : 묻고 따블로 가!

 

여러번 언급드리지만 JobInstance와 JobExection부모(1)자식(N)관계입니다.

 

이제 Job에서 실행되는 정보를 확인하였으니 Job을 이루는 Step에 대한 정보는 과연 어떻게 삽입되었는지 확인해 봅시다.

(StepJob안에서 읽고,처리하고,쓰고자 하는 절차를 정의하고 실행하는 실질적 역할하는 객체이죠?)

@Bean
public Step unPaidMemberJobStep(
	StepBuilderFactory stepBuilderFactory
) {
	log.info("********** This is unPaidMemberJobStep");
	return stepBuilderFactory.get("unPaidMemberJobStep")
	.<Member, Member> chunk(10)
	.reader(unPaidMemberReader())
	.processor(this.unPaidMemberProcessor())
	.writer(this.unPaidMemberWriter())
	.build();
}

 

BATCH_STEP_EXECUTION 테이블을 확인하면

BATCH_STEP_EXECUTE 1/2 (클릭하면 확대됩니다)

 

BATCH_STEP_EXECUTE 2/2 (클릭하면 확대됩니다)

 

실행된 Step에 대한 이름, read_count, process_count, write_count을 비롯한 상세한 실행정보가 기록되어 있습니다.

 

Step실행정보를 통해 이제 특정 Job안에 내포해 있는 1개 혹은 여러개의 Step과 그 Step에서 정의되어 있는 읽고, 처리하고, 쓰는 정보들과 그렇지 않고 생략된 정보들까지도 모조리 다 확인할 수 있습니다.

 

그리고 실행된 Job에 의해 읽고, 처리하고, 쓰여진(업데이트) 결과가 쌓인 결과가 회원 테이블에서 고스란히 잘 반영되어 있습니다.

MEMBER Table (클릭하면 확대됩니다)

 

자 그럼 이 상태에서 BatchJoB을 한번 더 동일하게 애플리케이션을 실행해 봅시다.

방금 전 처음 실행했던 환경과 동일한 조건에서 재실행을 해보니 다음과 같이 Application run failed.. 라는 메시지와 함께 애플리케이션이 종료되었습니다.

 

흠.................무엇이 문제였을까요?

 

 

 

영화 '나쁜놈의 전성시대' 이 장면에서 민식이형님이 김성균한테 오지게 두들겨 맞죠.

방금전 종료된 애플리케이션의 console에 기록된 Log를 상세히 살펴보면 실패한 이유로 JobInstance Already exists and is not restartable 이라는 내용을 확인할 수가 있습니다.

 

Job이 실행될 때 생성되는 JobInstance는 고유하게 식별가능해야 합니다. 

 

다시말해 방금 전 실행했던 JobInstance와 그 이전의 JobInstance가 명확하게 구별이 되어야 한다는 의미입니다.

 

우리는 처음 Job 애플리케이션을 실행했을 때와 그 애플리케이션이 종료된 이후에 또한번 애플리케이션을 실행했습니다.

 

이전의 JobInstance정보가 BATCH_JOB_INSTANCE테이블에 고스란히 기록된 상태에서 동일한 Job을 실행하여 JobInstance를 생성하려고 했던 것이 문제였습니다.

 

Application Console log

 

그럼 먼저 이 에러가 안나오게끔 수정해 보고자 합니다.

(에러가 안나오게끔 수정을 해보자는 의미이지 결코 해결책은 아닙니다. 해결책은 이 설명 이후에 이어서 설명드리겠습니다.)

 

예제 코드에 정의한 Job
public class unPaidMemberConfig {
    private MemberRepository memberRepository;

    @Bean
    public Job unPaidMemberJob(
            JobBuilderFactory jobBuilderFactory,
            Step unPaidMemberJobStep
    ) {
        log.info("********** This is unPaidMemberJob");
        return jobBuilderFactory.get("unPaidMemberJob")
                //.preventRestart() 주석
                .start(unPaidMemberJobStep)
                .build();
    }
    ...
    }
.preventRestart() 이미 실행되었던 Job의 중복실행을 방지합니다.
해당 부분을 주석처리하겠습니다.

 

그리고 다시 애플리케이션을 실행해 봅시다.

Application Console log

 

이번엔 Application run failed..없이 정상적으로 실행이 되었습니다.

여기서 중요한건, 애플리케이션이 정상실행이 되었다는 의미이지 그 실행된 애플리케이션 즉, Job이 정상적으로 수행이 되었는지는 다음의 console의 log 메시지를 통해 확인할 수 있습니다.

 

Step already complete or not restartable, so no action to execute...

 

직역하자면

'Step이 이미 완료되었거나 재시작이 가능하지 않기 때문에 Step실행(StepExecution)은 수행되지 않는다.'

 

결론적으로는 애플리케이션은 실행이 되었지만 Job은 정상적으로 수행되지 않았습니다.
그럼 방금 전 주석처리한 Job을 빌드하는 시점에 preventRestart()를 주석하기 전과 효과는 동일했던 것일까요?

 

다시한번 결론적으로 말씀드리면 아닙니다. 그렇다면 어떠한 변화가 있었는지 메타테이블을 뒤져가면서 확인해 봅시다.

 

먼저 BATCH_JOB_INSTANCE 테이블을 살펴보면,

BATCH_JOB_INSTANCE (클릭하면 확대됩니다)

처음 실행하여 생성된 record외에 새로운 record가 생성되지 않는 것으로 보아 JobInstance는 생성되지 않았다는 걸 확인할 수 있습니다.

 

그리고 BATCH_JOB_EXECUTION 테이블을 살펴보면

BATCH_JOB_EXECUTION  1/3 (클릭하면 확대됩니다)

 

BATCH_JOB_EXECUTION  2/3 (클릭하면 확대됩니다)

 

BATCH_JOB_EXECUTION  3/3 (클릭하면 확대됩니다)

 

엇!? 새로운 row가 생성되었습니다.
처음의 것과 그 다음의 것을 비교해서 EXIT_CODE 칼럼과 EXIT_MESSAGE 칼럼의 값에서 차이가 납니다. 

 

EXIT_CODE : NOOP
EXIT_MESSAGE : All steps already completed or no steps configured for this job.

EXIT_CODE는 해당 JobExecution이 종료시점의 상태값을 나타냅니다.

NOOP의 표현의 사전적의미대로 불가항력의 상태로 종료가 되었다는 의미로 보면 되지 않을까 싶습니다.

 

Noop!

 

EXIT_MESSAGE는 이미 애플리케이션의 consoleLog에서 확인했던 메시지 그대로 기록이 되어 있습니다. 결국 정리하면 Step이 이미 완료되었거나 재시작이 가능하지 않기 때문에 Step실행(StepExecution)은 수행되지 않았다는 이력이 JobExecution을 통해 남겨졌다는 것입니다.

 

이쯤이면 JobInstanceJobExecution의 관계와 그 역할에 대해 머리와 가슴으로 조금 와닿아졌지 않을까요?

 

6. JobInstance를 식별하는 JobParameter

JobInstance를 고유하게 식별할수 잇게 하기 위해선 외부(또는 내부)로부터 받은 파라미터 값이 필요합니다.

 

예컨대 실행시점을 나타내는 값 (보통은 년,월,일 좁게는시,분초가 될수도 있습니다)을 Job이 실행될 때마다 파라미터로 받아 실행을 하면 그 실행시점에 성생되는 JobInstance는 명확히 구분될 수 있겠죠?

 

이렇게 하면 해당 JobInstance가 어느시점에 실행되어 데이터를 읽고 처리하고 쓰이게 하였는지도 확연하게 알 수 있을 것입니다.

 

백문이 불여일견 직접한번 JobParameterJob에 부여하여 Job을 실행해 봅시다.

오른쪽 상단에 망애플리케이션 셀렉트박스에서 Edit Configurations...를 클릭합니다.

 

[Run/Debug Configuration] 창이 표시되면 Program arguments 입력항목에 Key=value 형식으로 파라미터를 부여합니다. 저의 경우 requestDate=20190823으로 부여하였습니다. 그리고 하단의 OK버튼을 클릭합니다.

Run/Debug Configuration

이제 애플리케이션을 다시 재시작해보겠습니다.

오오옷~띠용!~ 애플리케이션이 처음 실행했던것과 같이 정상적으로 수행되었습니다.

Application Console - Process finished with exit code 0

 

BATCH_JOB_INSTANCE 테이블에 새로운 record가 삽입되었습니다.

BATCH_JOB_INSTANCE (클릭하면 확대됩니다)

 

BATCH_JOB_EXECUTION 테이블에도 새로운 JobInstance의 실행단위정보의 record가 삽입되었네요.

BATCH_JOB_EXECUTION (클릭하면 확대됩니다)

 

BATCH_STEP_EXECUTION 테이블에도 새로운 JonInstance의 Step 실행정보가 삽입되었습니다.

BATCH_STEP_EXECUTION (클릭하면 확대됩니다)

 

정리하자면 BatchJob은 실행할 때마다 내부 혹은 외부로부터 부여받은 JobParameter값에 의해 새로운 JobInstance를 생성합니다.

 

JobInstance는 완전히 완료할 까지(= 새로운 JobParameter 값을 부여받은 Job이 실행되기 전)까지 소멸되지 않고 Job이 중복되는 실행횟수만큼에 JobExecution를 생성합니다.

 

 

 

그림으로 표현해본 Spring Batch Job 관계도

 

 

여기까지입니다.
JobParameterJob, JobJobInstance 그리고 JobInstanceJobExecution의 관계와 근 관계에 따라 연쇄적으로 상세한 정보들을 기록하는 메타테이블에 대해 알아보았습니다.

 

이제 이 메타테이블을 통해  우리가 만든 복잡한 배치프로세스에 대한 실행이력을 체계적으로 관리하여 의도치 않았던 상황을 마주하였을 때 원인을 빠르고 정확하게 추적할 수 있고 향후 더 나은 배치프로세스를 고도화해 나아가기 위한 측정과 분석 또한 해 나아갈 수 있을 것입니다.

 

이 장은 메타테이블구조와 그와 관련된 동작위주로 서술하면서 무심하게 지나간 어노테이션이 있습니다. 바로 Step객체에서 '읽고' 를 담당하는 ItemReader 컴포넌트에 선언된 @StepScope라는 어노테이션이죠. 

@Bean
@StepScope // 잡았다 요놈!
public ListItemReader<Member> unPaidMemberReader() {
    log.info("********** This is unPaidMemberReader");
    List<Member> activeMembers = memberRepository.findByStatusEquals(MemberStatus.ACTIVE);
    log.info("          - activeMember SIZE : " + activeMembers.size());
    List<Member> unPaidMembers = new ArrayList<>();
    for (Member member : activeMembers) {
        if(member.isUnpaid()) {
            unPaidMembers.add(member);
        }
    }
    
    log.info("          - unPaidMember SIZE : " + unPaidMembers.size());
    return new ListItemReader<>(unPaidMembers);
    }

그간 언급을 하지 않았던 이유는 별도로 상세히 설명할 만큼 비중있는 녀석이기 때문이죠.(절대 까먹어서가 아닙니다.) 배치프로세스에서 Scope라는 개념은 그 복잡도가 올라갈 수록 그 중요도 역시 비례합니다. 

 

다음장에서는 @StepScope에 대한 소개와 함께 Scope에 대한 내용을 정리해 보도록 하겠습니다.

수고 많으셨습니다. 

 

Comments