Spring Batch Meta-Data Tables
스프링 배치에서는 각 작업이 실행될 때마다 Job 또는 Step에 대한 상태와 이력을 기록한다. 따라서 스프링 배치를 구성하기 위해서는 메타데이터 테이블을 구성해야 한다.
메타데이터 테이블 생성 쿼리문
스프링 배치 라이브러리에는 테이블 생성에 필요한 SQL 문이 포함돼있다. Spring Batch Core 4.3.7 버전 기준으로 org.springframework.batch.core
하위 경로에서 여러 sql 파일들을 확인할 수 있다. 예시로 MySQL 환경에서 사용할 수 있는 schema-mysql.sql
파일을 열어보면 아래와 같이 테이블 생성에 필요한 쿼리문을 볼 수 있다.
-- Autogenerated: do not edit this file
CREATE TABLE BATCH_JOB_INSTANCE (
JOB_INSTANCE_ID BIGINT NOT NULL PRIMARY KEY ,
VERSION BIGINT ,
JOB_NAME VARCHAR(100) NOT NULL,
JOB_KEY VARCHAR(32) NOT NULL,
constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)
) ENGINE=InnoDB;
CREATE TABLE BATCH_JOB_EXECUTION (
JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY ,
VERSION BIGINT ,
JOB_INSTANCE_ID BIGINT NOT NULL,
CREATE_TIME DATETIME(6) NOT NULL,
START_TIME DATETIME(6) DEFAULT NULL ,
END_TIME DATETIME(6) DEFAULT NULL ,
STATUS VARCHAR(10) ,
EXIT_CODE VARCHAR(2500) ,
EXIT_MESSAGE VARCHAR(2500) ,
LAST_UPDATED DATETIME(6),
JOB_CONFIGURATION_LOCATION VARCHAR(2500) NULL,
constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)
references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID)
) ENGINE=InnoDB;
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(6) 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)
) ENGINE=InnoDB;
CREATE TABLE BATCH_STEP_EXECUTION (
STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY ,
VERSION BIGINT NOT NULL,
STEP_NAME VARCHAR(100) NOT NULL,
JOB_EXECUTION_ID BIGINT NOT NULL,
START_TIME DATETIME(6) NOT NULL ,
END_TIME DATETIME(6) 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(2500) ,
EXIT_MESSAGE VARCHAR(2500) ,
LAST_UPDATED DATETIME(6),
constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID)
references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;
CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT (
STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
SHORT_CONTEXT VARCHAR(2500) NOT NULL,
SERIALIZED_CONTEXT TEXT ,
constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID)
references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID)
) ENGINE=InnoDB;
CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT (
JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
SHORT_CONTEXT VARCHAR(2500) NOT NULL,
SERIALIZED_CONTEXT TEXT ,
constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID)
references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;
CREATE TABLE BATCH_STEP_EXECUTION_SEQ (
ID BIGINT NOT NULL,
UNIQUE_KEY CHAR(1) NOT NULL,
constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;
INSERT INTO BATCH_STEP_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_STEP_EXECUTION_SEQ);
CREATE TABLE BATCH_JOB_EXECUTION_SEQ (
ID BIGINT NOT NULL,
UNIQUE_KEY CHAR(1) NOT NULL,
constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;
INSERT INTO BATCH_JOB_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_EXECUTION_SEQ);
CREATE TABLE BATCH_JOB_SEQ (
ID BIGINT NOT NULL,
UNIQUE_KEY CHAR(1) NOT NULL,
constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;
INSERT INTO BATCH_JOB_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_SEQ);
참고로 MySQL은 시퀀스(sequence) 기능을 지원하지 않기 때문에 아래와 같은 쿼리문이 schema-mysql.sql
파일에 추가돼있다.
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);
자동으로 생성할 수 없을까?
다행히도 스프링 배치 메타데이터 테이블을 자동 생성할 수 있는 설정이 있다. Spring Boot 2.7.0 버전 기준으로 아래와 같이 application.yml
파일에 spring.batch.jdbc.initialize-schema
옵션을 선언하고 always
로 값을 설정해 주면 된다. 이렇게 설정해두면 스프링 배치 애플리케이션이 구동될 때 테이블 생성 쿼리문이 실행되어 메타데이터 테이블이 자동으로 생성된다.
spring:
batch:
jdbc:
initialize-schema: always
추가적인 설정값에는 embedded
와 never
가 있다. 기본값인 embedded
는 내장형 데이터베이스를 사용할 때만 메타데이터 테이블을 생성하는 옵션이며, never
는 생성하지 않는 설정이다. 따라서 스프링 배치가 처음 구동될 때 메타데이터 테이블이 자동 생성되지 않게 하려면 해당 옵션 값을 never
로 설정해 주면 된다.
참고로 이전 버전에서 사용했던 spring.batch.initialize-schema
는 2.7.0 버전으로 삭제되었다. 이전 버전의 코드를 보면 아래와 같이 @Deprecated
어노테이션이 선언돼있는 것을 확인할 수 있다.
@Deprecated
@DeprecatedConfigurationProperty(replacement = "spring.batch.jdbc.initialize-schema")
public DataSourceInitializationMode getInitializeSchema() {
return this.jdbc.getInitializeSchema();
}
테이블 생성 과정
테이블 생성 쿼리문이 실행되는 중간 과정을 살펴보면, BatchDataSourceScriptDatabaseInitializer
빈 등록 과정에서 스키마가 위치한 곳을 탐색한다.
public class BatchDataSourceScriptDatabaseInitializer extends DataSourceScriptDatabaseInitializer {
public BatchDataSourceScriptDatabaseInitializer(DataSource dataSource, BatchProperties.Jdbc properties) {
this(dataSource, getSettings(dataSource, properties));
}
public static DatabaseInitializationSettings getSettings(DataSource dataSource, BatchProperties.Jdbc properties) {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
// 위치를 찾는다.
settings.setSchemaLocations(resolveSchemaLocations(dataSource, properties));
// ALWAYS, EMBEDDED, NEVER
settings.setMode(properties.getInitializeSchema());
// 생성 과정에서 오류가 발생하더라도 초기화 계속할지 여부
settings.setContinueOnError(true);
return settings;
}
// ...
}
그리고 상속 관계인 DataSourceScriptDatabaseInitializer
클래스가 상속한 AbstractScriptDatabaseInitializer
추상 클래스의 afterPropertiesSet
메서드가 실행되면서 쿼리문 실행이 진행된다.
public abstract class AbstractScriptDatabaseInitializer implements ResourceLoaderAware, InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
initializeDatabase();
}
public boolean initializeDatabase() {
ScriptLocationResolver locationResolver = new ScriptLocationResolver(this.resourceLoader);
boolean initialized = applySchemaScripts(locationResolver);
return applyDataScripts(locationResolver) || initialized;
}
// ...
}
public class DataSourceScriptDatabaseInitializer extends AbstractScriptDatabaseInitializer {
@Override
protected void runScripts(List<Resource> resources, boolean continueOnError, String separator, Charset encoding) {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.setContinueOnError(continueOnError);
populator.setSeparator(separator);
if (encoding != null) {
populator.setSqlScriptEncoding(encoding.name());
}
for (Resource resource : resources) {
populator.addScript(resource);
}
customize(populator);
DatabasePopulatorUtils.execute(populator, this.dataSource); // 내부적으로 SQL 스크립트를 실행한다.
}
// ...
}
아래 코드는 실제 스크립트가 수행되는 ScriptUtils
의 executeSqlScript
메서드의 일부다. 참고로 앞선 과정에서 setContinueOnError
옵션을 true
로 지정했기 때문에, 예외가 발생하더라도 초기화가 중단되지 않는다.
try {
// ...
} catch (SQLException ex) {
boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop");
if (continueOnError || (dropStatement && ignoreFailedDrops)) {
if (logger.isDebugEnabled()) {
logger.debug(ScriptStatementFailedException.buildErrorMessage(statement, stmtNumber, resource), ex);
}
}
else {
throw new ScriptStatementFailedException(statement, stmtNumber, resource, ex);
}
}
따라서 spring.batch.jdbc.initialize-schema
옵션을 always
로 설정했을 때, 초기화 과정에서 메타데이터 테이블이 이미 존재하더라도 아래 실행 화면처럼 구동 실패가 발생하지 않는다.