스프링 배치 메타데이터 테이블 자동 생성 설정

스프링 배치(Spring Batch)에서 사용하는 메타데이터(Meta-Data) 테이블을 자동 생성하도록 설정하는 방법


Spring Batch Meta-Data Tables

스프링 배치에서는 각 작업이 실행될 때마다 Job 또는 Step에 대한 상태와 이력을 기록한다. 따라서 스프링 배치를 구성하기 위해서는 메타데이터 테이블을 구성해야 한다.

spring batch metadata ERD

Spring Batch Docs: Meta-Data Schema


메타데이터 테이블 생성 쿼리문

스프링 배치 라이브러리에는 테이블 생성에 필요한 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

추가적인 설정값에는 embeddednever가 있다. 기본값인 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 스크립트를 실행한다.
	}
	
	// ...
}

아래 코드는 실제 스크립트가 수행되는 ScriptUtilsexecuteSqlScript 메서드의 일부다. 참고로 앞선 과정에서 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로 설정했을 때, 초기화 과정에서 메타데이터 테이블이 이미 존재하더라도 아래 실행 화면처럼 구동 실패가 발생하지 않는다.

exception-catch-when-initializing-metadata-tables


댓글을 남기시려면 Github 로그인을 해주세요 :D


Hi, there!

Thanks for visiting my blog.
Please let me know if there are any mistakes in my post.