이전 포스트에서 Flyway를 사용해 프로덕션 DB의 마이그레이션 버전 관리를 했던 경험을 기록했다.
로컬 환경에서 마이그레이션 버전 관리가 필요할까?
프로덕션 서버의 DB 관리 시 빛을 발할 Flyway이지만, 동작을 보장하기 위해 로컬 환경의 DB와 개발 서버의 DB에서도 똑같이 적용할 필요가 있다. 개발 서버의 DB에도 데이터는 들어가기 쉽다. 프로덕션 서버라고 생각하고 올바른 데이터의 CRUD만을 최대한 다루면 유효한 데이터만을 개발 DB에 담고 개발 DB의 버전 관리 또한 유의미하게 할 수 있다.
그렇다면 로컬 환경의 DB는 어떨까? 개발자들이 각자 개발하는 만큼 기능이나 스키마의 추가나 삭제, 변경 등이 각자의 컴퓨터에서 무분별하게 이루어질 것이다. 버전 관리가 유의미하진 않지만, Flyway의 기능성을 보장하기 위해 로컬 환경에서
도 적용하는 것이 바람직하다고 생각한다.
만약 어떠한 기능이나 도메인 개발을 완료했다면 다른 개발자들이 각자의 개발 환경에서 사용하거나 이해하기 쉽게끔 더미 데이터도 추가하면 좋다고 생각한다.
Spring Boot의 스키마 및 더미 데이터 추가
Spring Boot는 기본적으로 스키마와 더미 데이터의 경로를 등록해 auto configure 할 수 있는 기능이 있다.
spring:
sql:
init:
schema-locations: classpath:db/migration/initial_schema.sql
data-locations: classpath:db/data/data.sql
위와 같이 애플리케이션이 실행될 때 스키마 파일과 데이터 파일의 경로를 등록해 자동으로 스크립트를 실행해준다.
org.springframework.boot.autoconfigure.sql.init
패키지 아래 SqlInitializationProperties.java
클래스 파일을 들여다보면 아래와 같이 SQL 초기 설정을 할 수 있다.
package org.springframework.boot.autoconfigure.sql.init;
...
/**
* {@link ConfigurationProperties Configuration properties} for initializing an SQL
* database.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
@ConfigurationProperties("spring.sql.init")
public class SqlInitializationProperties {
/**
* Locations of the schema (DDL) scripts to apply to the database.
*/
private List<String> schemaLocations;
/**
* Locations of the data (DML) scripts to apply to the database.
*/
private List<String> dataLocations;
...
/**
* Mode to apply when determining whether initialization should be performed.
*/
private DatabaseInitializationMode mode = DatabaseInitializationMode.EMBEDDED;
public List<String> getSchemaLocations() {
return this.schemaLocations;
}
public void setSchemaLocations(List<String> schemaLocations) {
this.schemaLocations = schemaLocations;
}
public List<String> getDataLocations() {
return this.dataLocations;
}
public void setDataLocations(List<String> dataLocations) {
this.dataLocations = dataLocations;
}
...
}
이 클래스가 sql init properties를 yaml에서 읽어 settings 설정을 하면 Spring Boot의 auto configure 기능이 해당 설정을 주입해 Initializer를 실행한다.
package org.springframework.boot.sql.init;
...
/**
* Base class for an {@link InitializingBean} that performs SQL database initialization
* using schema (DDL) and data (DML) scripts.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public abstract class AbstractScriptDatabaseInitializer implements ResourceLoaderAware, InitializingBean {
...
protected AbstractScriptDatabaseInitializer(DatabaseInitializationSettings settings) {
this.settings = settings;
}
...
public boolean initializeDatabase() {
ScriptLocationResolver locationResolver = new ScriptLocationResolver(this.resourceLoader);
boolean initialized = applySchemaScripts(locationResolver);
return applyDataScripts(locationResolver) || initialized;
}
...
}
추상 클래스인 AbstractScriptDatabaseInitializer
가 setting을 기반으로 DB의 초기화를 진행한다. DB는 여러 방식으로 연동될 수 있기에 이 추상 클래스는 다양한 구현체가 존재하는데, 기본으로 보이는
SqlDataSourceScriptDatabaseInitializer
를 살펴봤다.
package org.springframework.boot.autoconfigure.sql.init;
...
/**
* {@link DataSourceScriptDatabaseInitializer} for the primary SQL database. May be
* registered as a bean to override auto-configuration.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.6.0
*/
@ImportRuntimeHints(SqlInitializationScriptsRuntimeHints.class)
public class SqlDataSourceScriptDatabaseInitializer extends DataSourceScriptDatabaseInitializer {
/**
* Create a new {@link SqlDataSourceScriptDatabaseInitializer} instance.
* @param dataSource the primary SQL data source
* @param properties the SQL initialization properties
* @see #getSettings
*/
public SqlDataSourceScriptDatabaseInitializer(DataSource dataSource, SqlInitializationProperties properties) {
this(dataSource, getSettings(properties));
}
...
}
이런 흐름의 Spring Boot의 기본 auto configure 기능으로 DB 스키마와 데이터 추가가 동작하는 것을 확인했다.
실제 DB와 동일한 환경의 테스트 DB 사용 시
위와 같은 기능을 통해 더미 데이터를 추가할 수 있는 것을 확인했다. 마음에 들지 않는 것이 있다. 테스트 DB로 H2 같은 임베디드 DB를 사용할 수 있지만, 실제로 사용되는 DB가 MySQL이라면 기능성을 정확히 테스트하기 위해 테스트 DB도 임베디드 DB가 아닌 MySQL을 사용하는 것이 바람직하다고 생각한다. 테스트 DB가 임베디드가 아니라면 매번 실행했을 때 DB가 비어있음을 보장해주어야 한다.
ddl-auto: create-drop
JPA를 사용하면 ddl-auto: create-drop
property를 통해 매번 테스트 실행 시 테이블을 모두 초기화 할 수 있다. 하지만 이는 바람직한가?
실제 DB에 해당 옵션을 주고 개발할 수는 없다. create-drop
은 JPA 엔티티들의 설정을 기반으로 자동으로 ddl을 실행한다. 즉, 개발자가 의도하지 않은 무언가가 추가될 수도 있고, 생각하지 못했던 실수가 나올 수 있다. 그렇기 때문에 DDL 스크립트를 개별적으로 작성하고 ddl-auto: validate
옵션을 통해 JPA가 DB에 의도치않게 개입하는 것을 방지해야한다.
테스트는 어차피 일회성이기 때문에 상관없지 않을까? 테스트 DB를 임베디드 DB가 아닌 실제 DB와 맞추어 기능성을 정확히 테스트하고자 하기 때문에 상관이 있다. 테스트에선 발생하지 않았던 고려사항이 실제 운영 시 발생할 수도 있는 가능성이 다분하게 된다. 그러면 실제 DB와 동일한 환경을 맞춘 의미도 없어진다. 그렇기 때문에 테스트 또한 ddl-auto: validate
옵션으로 진행되어야 하는 것이 바람직하다고 생각한다.
ddl-auto: create-drop
은 임베디드 DB를 사용하는 것이 아니라면 쓰지 않는 것이 좋지 않을까?
테스트 디렉토리의 application.yaml
메인 디렉토리가 아닌 테스트 디렉토리에 application.yaml
은 따로 필요하다. 해당 yaml properties는 테스트 실행시에만 적용될 수 있다. 더미 데이터를 로컬 환경에서만 적용시키고 테스트에서 적용시키지 않으려면 메인 디렉토리의 application.yaml
과 테스트 디렉토리의 application.yaml
을 따로 작성하면 된다
/src/main/resources/application.yaml
spring:
datasource:
...
jpa:
hibernate:
ddl-auto: validate
...
sql:
init:
mode: always
schema-locations: classpath:db/migration/initial_schema.sql
...
/src/main/resources/application-local.yaml
spring:
sql:
init:
data-locations: classpath:db/data/data.sql
/src/test/resources/application.yaml
spring:
datasource:
...
jpa:
hibernate:
ddl-auto: validate
...
sql:
init:
mode: always
schema-locations: classpath:db/migration/initial_schema.sql
...
위와 같이 배포 환경과 테스트 환경에선 더미 데이터가 추가되지 않고 로컬 프로파일때만 추가할 수 있도록 개별적으로 설정해줄 수 있다. 기본적으로 SQL Initializer를 사용하기 위해선 임베디드 DB를 사용할 때만 적용된다. 그렇기 때문에 spring.sql.init.mode: always
라는 옵션을 꼭 넣어주어야 작동한다.
내가 원하던 설정을 이렇게 할 수 있지만, 프로덕션 서버에서 애플리케이션이 재시작될 때 마이그레이션 버전 관리를 위해 Flyway를 도입했다. 그렇게되면 설정이 꼬일 수 있다. Flyway와 sql.init 모두 초기 설정을 관여하기 때문이다.
If you are using a Higher-level Database Migration Tool, like Flyway or Liquibase, you should use them alone to create and initialize the schema. Using the basic schema.sql and data.sql scripts alongside Flyway or Liquibase is not recommended and support will be removed in a future release.
If you need to initialize test data using a higher-level database migration tool, please see the sections about Flyway and Liquibase.
스프링 공식 문서에서도 언급된 것처럼, 높은 수준의 마이그레이션 툴을 사용한다면 해당 툴의 설정을 따르는 것이 바람직하다.
Flyway 설정으로 로컬 환경 더미 데이터 추가
Flyway로 마이그레이션을 할 때 afterMigrate.sql
스크립트를 작성하면 애플리케이션이 재시작될 때마다 자동으로 마이그레이션 버전 관리가 끝나고 해당 스크립트를 실행한다.
Flyway가 마이그레이션을 위해 읽는 기본 경로는 /src/main/resources/db/migration
이다. 그 아래 마이그레이션을 위한 스크립트를 우선 추가했다.
V1__initial_schema.sql
CREATE TABLE users
(
id BIGINT AUTO_INCREMENT,
nickname VARCHAR(20) UNIQUE,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
CONSTRAINT users_pk PRIMARY KEY (id)
);
CREATE TABLE posts
(
id BIGINT AUTO_INCREMENT,
user_id BIGINT NOT NULL,
title VARCHAR(30) NOT NULL,
content TEXT,
thumbnail_url VARCHAR(1024),
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
CONSTRAINT posts_pk PRIMARY KEY (id)
);
V2__create_post_like_table.sql
ALTER TABLE posts ADD COLUMN like_count INTEGER NOT NULL DEFAULT 0;
CREATE TABLE post_likes
(
user_id BIGINT NOT NULL,
post_id BIGINT NOT NULL,
is_like TINYINT(1) NOT NULL,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
CONSTRAINT post_likes_pk PRIMARY KEY (user_id, post_id),
CONSTRAINT post_likes_users_fk FOREIGN KEY (user_id) REFERENCES users (id),
CONSTRAINT post_likes_posts_fk FOREIGN KEY (post_id) REFERENCES posts (id)
);
이 스크립트들이 실행되거나 확인하는 과정이 끝나면 실행될 afterMigrate.sql을 작성하면 더미 데이터를 추가할 수 있다. 하지만 테스트 환경에서도 이 기본 경로를 읽기 때문에 더미 데이터가 추가될 것이다. 그렇기 때문에 /db/migration
이 아닌 /db/data
디렉토리를 새로 만들어주었다.
/src/main/resources/db/data/afterMigrate.sql
SET foreign_key_checks = 0;
TRUNCATE TABLE users;
TRUNCATE TABLE posts;
TRUNCATE TABLE post_likes;
SET foreign_key_checks = 1;
# users
INSERT INTO users(id, nickname) VALUES (1, '더미 회원 1');
INSERT INTO users(id, nickname) VALUES (2, '더미 회원 2');
INSERT INTO users(id, nickname) VALUES (1, '더미 회원 3');
INSERT INTO users(id, nickname) VALUES (2, '더미 회원 4');
# posts
INSERT INTO posts(id, user_id, title, content) VALUES (1, 1, '더미 포스트 1', '더미 포스트 내용');
INSERT INTO posts(id, user_id, title, content) VALUES (2, 1, '더미 포스트 2', '더미 포스트 내용');
INSERT INTO posts(id, user_id, title, content) VALUES (3, 2, '더미 포스트 3', '더미 포스트 내용');
INSERT INTO posts(id, user_id, title, content) VALUES (4, 2, '더미 포스트 4', '더미 포스트 내용');
# post likes
INSERT INTO post_likes(user_id, post_id, is_like) VALUES (2, 1, true);
INSERT INTO post_likes(user_id, post_id, is_like) VALUES (3, 1, true);
INSERT INTO post_likes(user_id, post_id, is_like) VALUES (4, 1, true);
INSERT INTO post_likes(user_id, post_id, is_like) VALUES (1, 2, true);
INSERT INTO post_likes(user_id, post_id, is_like) VALUES (4, 2, false);
이렇게 공유하기 위한 더미 데이터를 작성할 수 있다.
Flyway의 기본 경로는 /db/migration
이다. /db/data
는 적용이 되지 않는다. 이를 적용시키기 위해 원하는 환경에 설정을 추가해주어야 한다.
/src/main/resources/application.yaml
spring:
datasource:
...
jpa:
hibernate:
ddl-auto: validate
...
/src/main/resources/application-local.yaml
spring:
flyway:
locations: classpath:db/migration, classpath:db/data
/src/test/resources/application.yaml
spring:
datasource:
...
jpa:
hibernate:
ddl-auto: validate
...
Flyway의 기본 작동 여부는 enabled: true
이다. 아무 설정 변경이 없다면 기본적으로 작동하고 기본 경로인 /db/migration
디렉토리 아래의 형식에 맞는 스크립트들을 실행하거나 확인한다. 그러니 프로덕션을 위한 설정과 테스트를 위한 설정은 변경이 필요없다. 더미 데이터를 적용하기 위한 로컬 환경에서만 spring.flyway.locations
에 db/data
를 추가해주기만 하면 된다.
'Spring > 라이브러리' 카테고리의 다른 글
Flyway로 DB 마이그레이션 (0) | 2024.04.05 |
---|