Spring Batch, SEQ ID 제대로 이해하기

2022. 8. 21. 13:04Spring

Spring Batch에서 Job, Step Id를 관리하는 방법과 Batch Meta Table 중 SEQ Table에 대한 이해가 본 포스팅의 목표입니다.

 

 

안녕하세요.

Deep Dive를 즐기는 입장으로 Spring Batch가 Job, Step의 Id 값을 관리하는 방식에 대해 궁금해져서 디깅을 하게 되었습니다. 배치를 사용하면서 Id 값에 대해 오류를 하나 만났는데, 문득 그 흐름이 궁금해지더라구요. 해당 오류는 포스팅 하단에서 확인할 수 있습니다 ㅎㅎ

 

 

FYI:: Spring Batch Series

Spring Batch, 제대로 이해하기 (1) - 개념 이해

Spring Batch, 제대로 이해하기 (2) - 동작 원리

Spring Batch, 제대로 이해하기 (3) - Meta Data

Spring Batch, 제대로 이해하기 (4) - 데이터 처리 활용

 

✔️ 해당 포스팅은 Spring Batch, 제대로 이해하기 (3) - Meta Data 와 밀접하게 이어집니다.

 


 

 

Spring Batch는 Meta Table을 아주 잘 활용하면서 Job 생성, 실행, 상태 이력 등을 관리합니다.

그 중, MySQL이나 MariaDB는 Job이나 Step의 Id 값을 테이블로 직접 관리하는데요.

 

BATCH_JOB_EXECUTION_SEQ, BATCH_STEP_EXECUTION_SEQ, BATCH_JOB_SEQ 테이블이 해당됩니다.

이번 포스팅은 이 SEQ 값에 대해 알아보도록 하겠습니다.

 

SEQUENCE TABLE

위에서 보았던 BATCH_JOB_INSTANCE, BATCH_JOB_EXECUTION 및 BATCH_STEP_EXECUTION에는 각각 _ID로 끝나는 컬럼이 포함되어 있었고, 해당 필드는 각 테이블의 Primary Key 역할을 합니다.

 

이 ID 값은 도메인 개체 중 하나를 데이터베이스에 삽입한 후, 해당 키가 Java에서도 고유하게 식별될 수 있도록 실제 개체에 설정되어야 하기 때문에 필수적인데요. 최신 데이터베이스 드라이버(JDBC 3.0 이상)에서는 데이터베이스 생성 PK로 사용할 수 있도록 지원하지만, 기본적으로 아래와 같은 시퀀스를 사용하고 있습니다.

 

CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ;
CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ;
CREATE SEQUENCE BATCH_JOB_SEQ;

 

schema-mysql.sql

하지만 SEQUENCE를 지원하지 않는 DBMS가 존재하죠.

대표적으로 MySQL은 SEQUNCE를 지원하지 않아 SEQUNCE 테이블을 생성해서 사용하곤 합니다.

Spring Batch에서도 아래와 같이 테이블을 생성해 사용합니다.

 

CREATE TABLE BATCH_STEP_EXECUTION_SEQ (ID BIGINT NOT NULL) type=InnoDB;
INSERT INTO BATCH_STEP_EXECUTION_SEQ values(0);
CREATE TABLE BATCH_JOB_EXECUTION_SEQ (ID BIGINT NOT NULL) type=InnoDB;
INSERT INTO BATCH_JOB_EXECUTION_SEQ values(0);
CREATE TABLE BATCH_JOB_SEQ (ID BIGINT NOT NULL) type=InnoDB;
INSERT INTO BATCH_JOB_SEQ values(0);

 

필자가 궁금했던 부분은 바로 INSERT 구문이었습니다.

각 SEQ Table에 0값을 추가하며 Spring Batch의 어떤 부분에서 어떤 순서로 처리할지 궁금증을 가지게 되었습니다.

초기 값으로 0을 추가하는 것 같은데, 그 이후 어떻게 관리할지가 가장 궁금했고

아래 디깅 내용을 확인하면서 궁금증을 해결할 수 있었습니다.

 

Job, Step Id

Spring Batch는 JobExecution, StepExecution을 생성할 때 BATCH_JOB_EXECUTION_SEQ, BATCH_STEP_EXECUTION_SEQ 에서 last_insert_id + 1 를 조회해서 가져옵니다.

(정확히는 키 값들의 캐시 버퍼 값을 더해줍니다. 아래 코드를 참고하면 getCacheSize에 해당합니다. default 1)

MyISAM, InnoDB에서의 Auto Increment를 사용하지 않고 seq를 해당 테이블이 가지고 있는 형태입니다.

 

여기서 핵심 개념은 SEQ 테이블에는 기본적으로 아래와 같이 하나의 엔트리가 반드시 존재해야한다는 점입니다.

초기 세팅이 끝나고 배치를 동작할 때는 이 엔트리에 대해 UPDATE 만 수행하게 됩니다.

 

mysql> SELECT * FROM BATCH_JOB_EXECUTION_SEQ;
+-----+------------+
| ID  | UNIQUE_KEY |
+-----+------------+
| 0   | 0          |
+-----+------------+

 

Spring Batch가 제공하는 schema-mysql.sql 를 확인해보면 INSERT INTO BATCH_STEP_EXECUTION_SEQ values(0); 하는 이유도 바로 여기에 있습니다. 초기 데이터 값을 세팅해주는 것이죠.

 

 

가령 Job을 생성해서 실행할 때, 해당 Execution의 ID 값을 구할 때,

BATCH_JOB_EXECUTION_SEQ의 유일한 엔트리 값이 ID: 312, UNIQUE_KEY: '0' 이라고 해볼게요.

 

mysql> SELECT * FROM BATCH_JOB_EXECUTION_SEQ;
+-----+------------+
| ID  | UNIQUE_KEY |
+-----+------------+
| 312 | 0          |
+-----+------------+

 

(마지막 실행 Job의 JOB_EXECUTION_ID 가 312이라는 의미입니다.) 

먼저, update BATCH_JOB_SEQ set ID = last_insert_id(ID + 1) limit 1 를 실행합니다.

그럼 ID, UNIQUE_KEY가 각각 313, 0으로 업데이트 됩니다.

 

mysql> SELECT * FROM BATCH_JOB_EXECUTION_SEQ;
+-----+------------+
| ID  | UNIQUE_KEY |
+-----+------------+
| 313 | 0          |
+-----+------------+

 

그 이후 select last_insert_id() 를 실행하는데, 위에서 업데이트한 값인 313을 가져오는 것이죠.

그럼 새로운 JobExecution을 생성할 때에는 이 313값을 가져와서 JOB_EXECUTION_ID 로 사용합니다.

그럼 JOB_EXECUTION_ID 가 313인 Job이 실행되겠죠.

 

 

⛏  Digging

흐름에 대해 분석해보면 다음과 같습니다.

 

1. 이미 실행된 동일한 JobExecution 판단

가장 먼저, SimpleJobRepository Class의 createJobExecution method에서 이전에 실행된 JobExecution이 존재하는지 확인합니다.

이 때 분별 값은 jobName값과 JobParameter 값입니다.

정확히 JobParameter는 Batch Job을 실행시킬 때 넣은 JobParameter 들의 해시 값입니다.

Spring Batch는 이 해시 값을 기준으로 이전 Job과의 동일성을 판단합니다.

즉, jobName을 Key로 JobParameter 해시 값을 동일 여부 판단 값으로 사용하죠.

 

2. (없다면) JobInstance 생성

만약 jobName, JobParameter이 동일한 이전 실행 Job이 없다면, 새로운 JobInstance를 생성합니다.

생성을 위해서 jobInstanceDao.createJobInstance 을 호출합니다.

 

3. JobInstance를 등록할 ID값 생성

가장 먼저, jobIncrementer.nextLongValue 로 Job Id로 사용할 Key 를 가져옵니다. 

nextLongValue는 추상화된 Abstract 객체에서 abstract method인 getNextKey를 호출합니다.

그럼 최종적으로 구현체인 MySQLMaxValueIncrementer의 getNextKey가 호출되죠.

 

4. Id로 사용할 Incrementer key 생성

MySQLMaxValueIncrementer의 getNextKey 확인해보면 아래와 같은 쿼리를 실행합니다.

 

1. stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName + " = last_insert_id(" + columnName + " + " + getCacheSize() + ") limit 1");
// update BATCH_JOB_SEQ set ID = last_insert_id(ID + 1) limit 1
// ...

private static final String VALUE_SQL = "select last_insert_id()";
// ...
2. stmt.executeQuery(VALUE_SQL);
// select last_insert_id()

 

 

Duplicate PK

Duplicate entry '0' for key 'PRIMARY'

TLDR; 새로운 JobExecution을 실행할 때 생성하는 Sequence Id의 업데이트가 이루어지지 않아 항상 같은 Id를  발생하는 문제

 

종종 Duplicate entry '0' for key 'BATCH_JOB_INSTANCE.PRIMARY' 와 같은 오류가 발견하는 경우가 있습니다.

발생 원인은 바로 *_SEQ 테이블의 초기 설정 문제입니다.

 

위의 디깅 내용을 기반으로 Duplicate entry '0' for key 'PRIMARY' 발생 원인을 생각해볼게요.

만약 SEQ 테이블에 데이터가 없으면 UPDATE 문을 실행했을 때 affected rows가 0이 됩니다. 

즉, 변경된 사항이 전혀 없다는 내용입니다.

 

이 상태에서 last_insert_id를 하게되면 항상 0을 반환합니다.

그럼 첫 번째, 두 번째, 세 번째 ... 모든 Job execution ID를 0으로 발급하기 때문에 당연히 PK인 JOB_EXECUTION_ID값이 중복되고 오류가 발생하게 됩니다.

 

Spring Batch 초기 테이블을 다른 테이블에서 구조만 복사해오면 이런 문제가 발생하게 되겠죠.

반드시 초기 엔트리(데이터 값)를 하나 INSERT 해주어야 합니다.

 

 

 

이상으로 Spring Batch에서의 SEQ 테이블과 Id 관리 방식에 대해 다뤘습니다.

오타나 잘못된 내용을 댓글로 남겨주세요!

감사합니다 ☺️