spring boot 에서 schema.sql 파일을 어떻게 실행시키는 걸까?
배경
우테코에서 미션을 진행하며 테이블 초기화를 위해 schema.sql 파일을 이용하게 되었고, 그 과정에서 스프링을 시작하면 어떻게 schema.sql 파일을 실행시키는지 궁금해져 디버깅을 통해 알아보고자 이 글을 작성하게 되었다.
학습 내용
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingBean({SqlDataSourceScriptDatabaseInitializer.class, SqlR2dbcScriptDatabaseInitializer.class})
@ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnClass({DatabasePopulator.class})
class DataSourceInitializationConfiguration {
DataSourceInitializationConfiguration() {
}
@Bean
SqlDataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource, SqlInitializationProperties properties) {
return new SqlDataSourceScriptDatabaseInitializer(determineDataSource(dataSource, properties.getUsername(), properties.getPassword()), properties);
}
private static DataSource determineDataSource(DataSource dataSource, String username, String password) {
return StringUtils.hasText(username) && StringUtils.hasText(password) ? DataSourceBuilder.derivedFrom(dataSource).username(username).password(password).type(SimpleDriverDataSource.class).build() : dataSource;
}
}
먼저 Spring Boot는 SqlDataSourceScriptDatabaseInitializer class를 Bean으로 등록을 한다. 이때 SqlDataSourceScriptDatabaseInitializer class를 생성하게 된다.
@ImportRuntimeHints({SqlInitializationScriptsRuntimeHints.class})
public class SqlDataSourceScriptDatabaseInitializer extends DataSourceScriptDatabaseInitializer {
public SqlDataSourceScriptDatabaseInitializer(DataSource dataSource, SqlInitializationProperties properties) {
this(dataSource, getSettings(properties));
}
public SqlDataSourceScriptDatabaseInitializer(DataSource dataSource, DatabaseInitializationSettings settings) {
super(dataSource, settings);
}
public static DatabaseInitializationSettings getSettings(SqlInitializationProperties properties) {
return SettingsCreator.createFrom(properties);
}
}
SqlDataSourceScriptDatabaseInitializer class가 생성될 때 SettingsCreator의 createFrom 메서드가 실행된다.
static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(scriptLocations(properties.getSchemaLocations(), "schema", properties.getPlatform()));
...
}
createFrom 메서드를 보면 setSchemaLocations settings.setSchemaLocations 메서드를 호출하여 스키마 파일의 위치를 설정해주고 있는 걸 볼 수 있다. 스키마 파일의 위치는 scriptLocations 메서드를 통해 결정한다.
private static List<String> scriptLocations(List<String> locations, String fallback, String platform) {
if (locations != null) {
return locations;
} else {
List<String> fallbackLocations = new ArrayList();
fallbackLocations.add("optional:classpath*:" + fallback + "-" + platform + ".sql");
fallbackLocations.add("optional:classpath*:" + fallback + ".sql");
return fallbackLocations;
}
}
scriptLocations의 인자로 오는 locations에는 createFrom 메서드에서 알 수 있듯이 properties.getSchemaLocations()의 결과가 locations로 들어오게 되고, fallback으로는 "schema" 문자열이 들어온다.
properties.getSchemaLocations()은 application.yml 파일에 스키마 파일의 위치를 따로 설정할 수 있는데 없으면 null값이 들어오게 된다. 따라서 locations 값은 null이 되고 스키마 파일의 기본 경로는 optional:classpath*:schema.sql 파일이 되게 된다.
이렇게 해서 스키마 파일의 위치가 설정된 채로 SqlDataSourceScriptDatabaseInitializer class가 빈으로 등록되고 SqlDataSourceScriptDatabaseInitializer의 runScripts
메서드를 실행하여 schema.sql 파일이 실행되게 된다.
해당 메서드는 SqlDataSourceScriptDatabaseInitializer의 부모 class인 AbstractScriptDatabaseInitializer에
위치해 있다.
protected void runScripts(AbstractScriptDatabaseInitializer.Scripts scripts) {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.setContinueOnError(scripts.isContinueOnError());
populator.setSeparator(scripts.getSeparator());
if (scripts.getEncoding() != null) {
populator.setSqlScriptEncoding(scripts.getEncoding().name());
}
for(Resource resource : scripts) {
populator.addScript(resource);
}
this.customize(populator);
DatabasePopulatorUtils.execute(populator, this.dataSource);
}
그럼 저 메서드는 누가 실행시킬까? AbstractScriptDatabaseInitializer를 보면 InitializingBean
인터페이스를 구현하고 있어 스프링 빈 생성 후 의존 관계 주입이 완료되면 afterPropertiesSet
메서드를 호출하게 되는데 이때 initializeDatabase 메서드를 실행시킴으로써 applySchemaScripts 메서드를 실행하게 된다.
public abstract class AbstractScriptDatabaseInitializer implements ResourceLoaderAware, InitializingBean {
...
public void afterPropertiesSet() throws Exception {
this.initializeDatabase();
}
public boolean initializeDatabase() {
ScriptLocationResolver locationResolver = new ScriptLocationResolver(this.resourceLoader);
boolean initialized = this.applySchemaScripts(locationResolver);
return this.applyDataScripts(locationResolver) || initialized;
}
applySchemaScripts 메서드가 다시 runScripts를 호출함으로써 스크립트가 실행되게 된다.
private boolean applyScripts(List<String> locations, String type, ScriptLocationResolver locationResolver) {
List<Resource> scripts = this.getScripts(locations, type, locationResolver);
if (!scripts.isEmpty() && this.isEnabled()) {
this.runScripts(scripts);
return true;
} else {
return false;
}
}
추가적으로 applyScripts 메서드가 실행할 때 isEnabled 메서드를 호출하여 결괏값이 true일 때 스크립트 파일이 실행되는데 모드 설정이 NEVER라면 절대 실행 안되고 ALWAYS이거나 모드설정이 없어도 임베디드 데이터베이스라면 실행되는 것을 확인할 수 있다.
private boolean isEnabled() {
if (this.settings.getMode() == DatabaseInitializationMode.NEVER) {
return false;
} else {
return this.settings.getMode() == DatabaseInitializationMode.ALWAYS || this.isEmbeddedDatabase();
}
}
정리
DataSourceInitializationConfiguration으로 SqlDataSourceScriptDatabaseInitializer 빈이 등록되고 해당 class는 InitializingBean을 구현하고 있어 스프링 빈이 다 만들어지고 의존관계 주입이 끝날 때 afterPropertiesSet 메서드가 실행되는데 이때 스크립트 파일을 실행한다. 만약 스크립트 위치를 설정하지 않았다면 기본적으로 schema.sql 파일로 실행되게 된다.