운영 환경 구축기 (2) - Flyway 적용하기
이 글은 우아한테크코스 백엔드 6기 초코칩에 의해 작성되었습니다.
데이터베이스 마이그레이션 툴의 필요성
왜 데이터베이스 마이그레이션 툴이 필요할까요?
필요성을 알아보기 전에, 팀 크루루의 현재 배포 환경을 알아봅시다.
팀 크루루의 프로젝트는 로컬 환경, 개발 환경, 운영 환경으로 총 3가지의 구동 환경을 가지고 있습니다.
개발자의 로컬 환경에서 구현되어 테스트가 마친 소스 코드는 형상관리 툴인 Git을 통해 변경 이력을 관리하고 있습니다. 개발이 완료된 코드가 be/dev
브랜치로 push되면 CI/CD를 통해 개발 서버로 배포됩니다.
개발 서버에서 QA test를 통과한 코드는 be/main
브랜치에 되고, CD를 통해 운영 서버로 배포됩니다.
코드는 Git에 의해 형상 관리되지만, 데이터베이스는 어떻게 관리될까요?
각 개발자의 로컬 환경에서 개발 시 사용되는 데이터베이스의 스키마 변경에 대한 마땅한 이력관리 방법이 존재하지 않습니다.
로컬 개발환경의 데이터베이스 스키마 변경사항을 다른 배포 환경의 데이터베이스에 적용하기 위해서 배포 전 해당 환경에 변경사항을 수동으로 적용해야합니다. 매번 DDL을 작성할 수도 있지만, 개발자가 직접 적용하는 과정에서 휴먼 에러가 발생할 수도 있죠. 최악의 경우에는 운영 환경에 코드가 배포되더라도 이전 스키마의 테이블과 충돌이 발생해 장애로 이어질 수 있습니다.
또한 개발자가 데이터베이스의 서버에 DDL을 잘못을 적용하게 되어도 이전 버전을 추적할 수 없습니다. Git으로 관리되는 코드처럼 누가, 언제, 어느 시점에서, 어떤 DDL을 적용했는지 찾아보기가 힘들다는 뜻입니다.
이러한 문제점은 데이테베이스 마이그레이션 툴을 통해 해결할 수 있습니다.
Flyway vs Liquibase
데이터베이스 관리 툴은 대표적으로 두 가지가 있습니다. 비교하면 다음과 같겠네요!
깃헙 레포 업데이트
구글 트렌드
그 중, Flyway란?
Flyway는 데이터베이스 마이그레이션을 관리하는 오픈 소스 도구입니다.
Flyway Naming Patterns
Flyway는 naming pattern으로 그 기능을 구사할 수 있습니다. 아래는 명명 규칙에 대한 간략한 정리입니다.
prefix
: V는 버전 마이그레이션, U는 롤백 마이그레이션, R은 반복 마이그레이션 접두사입니다.version
: version은 버전 마이레이션에서만 사용되며 숫자와 dots(점)이나 underscore(언더바) 조합으로 구성됩니다. 버전 번호는 각 마이그레이션에 대해 고유해야 하며 논리적으로 정렬되어야 합니다.- 반복 마이그레이션에서 version을 명시하면 filename제약 위반으로 에러 발생합니다.
separator
: 설명부분을 구분하기 위한 구분자이며 반드시 underscore(언더바)를 2개( __ ) 써야합니다.description
: 이 부분은 schema_version 테이블에 저장되는 설명으로 사용됩니다.suffix
: File 확장자를 명시하는 부분입니다. 확장자 기본은.sql
을 사용합니다.
Naming Pattern의 접두사인 prefix에 대해 조금 더 자세하게 설명해보고자 합니다
Prefix
Flyway에서 사용하는 V
, U
, R
접두어는 각기 다른 유형의 마이그레이션 파일을 구분하기 위해 사용됩니다. 반드시 V, U 또는 R로 시작해야만 flyway가 인식합니다.
V (Versioned Migration)
V
는 버전 관리된 마이그레이션 파일을 의미합니다. 이 마이그레이션 파일은 데이터베이스의 특정 스키마 변경을 의미하며, 특정 버전 번호와 연결되어 있습니다.
- 예시:
V1__Create_users_table.sql
- 특징:
- 버전 번호를 기준으로 순차적으로 적용됩니다.
- 각 버전 번호는 고유해야 하며, 이전 버전보다 높은 번호를 가져야 합니다.
- 데이터베이스에 기록된 버전보다 높은 버전이 있을 경우, 해당 마이그레이션이 실행됩니다.
U (Undo Migration)
U
는 롤백 마이그레이션 파일을 의미하며, V
로 정의된 마이그레이션을 되돌리는 데 사용됩니다. 이를 통해 데이터베이스 스키마를 이전 상태로 복구할 수 있습니다.
- 예시:
U1__Drop_users_table.sql
- 특징:
V
마이그레이션과 동일한 버전 번호를 사용해야 합니다.- 롤백을 통해 특정 마이그레이션의 효과를 되돌리고자 할 때 사용됩니다.
undo
명령어를 사용하여 실행할 수 있습니다.
R (Repeatable Migration)
R
은 반복 가능한 마이그레이션 파일을 의미합니다. 이 파일은 버전 번호 없이 동일한 파일명이 존재하지 않으면 항상 실행됩니다. 보통은 뷰, 저장 프로시저, 트리거 등과 같은 객체를 정의하는 데 사용됩니다.
- 예시:
R__Refresh_views.sql
- 특징:
- 버전 번호가 없으며, 파일 내용이 변경되면 다시 실행됩니다.
- 주기적으로 실행하여 데이터베이스 객체의 상태를 최신으로 유지합니다.
- Flyway는 이 파일의 내용을 체크섬하여 변화를 감지하고 필요 시 다시 적용합니다.
실습하기
실습 환경
- Spring 3.2.4
- MySQL 8.0
- Flyway 9.22.3
MySQL 설치하기
MySQL은 실습의 편의성을 위해 도커로 설치합니다.
build.gradle
Flyway와 MySQL을 사용하기 위해 build.gradle
파일에 다음과 같이 의존성을 추가합니다.
// Database
implementation 'org.flywaydb:flyway-core:9.22.3'
implementation 'org.flywaydb:flyway-mysql'
9.22.3
버전을 선택한 이유는 Maven Central에서 다른 버전들에 비해 높은 Usage가 존재하기 때문입니다. Maven Central에서의 Usage는 해당 버전이 많은 사용자들에 의해 사용되고 있다는 것을 의미하며, 이는 버전의 안정성을 간접적으로 보여줍니다.
application.yml
application.yml은 다음과 같이 설명합니다.
spring:
config:
activate:
on-profile: local
datasource:
url: jdbc:mysql://localhost:13306/cruru
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
flyway:
enabled: true
baseline-on-migrate: true
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
hibernate:
ddl-auto: none
- enables: Flyway를 활성화하는 설정입니다. 이 값이
true
로 설정되면, 애플리케이션이 시작될 때 Flyway가 자동으로 데이터베이스 마이그레이션을 수행합니다. - baseline-on-migrate: Flyway가 데이터베이스에 기존 스키마가 있을 때, 이를 기준선(Baseline)으로 간주하고 새로운 마이그레이션을 적용하도록 합니다. 즉, Flyway가 처음 실행될 때 기존 데이터베이스 상태를 기준선으로 설정하고, 이후 마이그레이션 파일들을 적용합니다. 이는 기존 데이터베이스와 새로운 마이그레이션 간의 충돌을 피하는 데 유용합니다
V1__init.sql 생성
저희 크루루에서 사용하고 있는 Answer, Applicant 엔티티의 구성은 다음과 같습니다. 이를 .sql 파일로 정의하면 아래와 같게 됩니다.
CREATE TABLE answer
(
answer_id BIGINT NOT NULL AUTO_INCREMENT,
applicant_id BIGINT NOT NULL,
question_id BIGINT NOT NULL,
content TEXT,
PRIMARY KEY (answer_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
CREATE TABLE applicant
(
applicant_id BIGINT NOT NULL AUTO_INCREMENT,
created_date DATETIME(6),
process_id BIGINT NOT NULL,
updated_date DATETIME(6),
email VARCHAR(255),
name VARCHAR(255),
phone VARCHAR(255),
state VARCHAR(255),
PRIMARY KEY (applicant_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
// CREATE DDL ...
실행
Spring Boot의 Application을 실행시켜봅시다.
Flyway를 통해 데이터베이스 스키마가 정상적으로 적용되었습니다.
flyway_schema_history 테이블
Flyway가 성공적으로 실행되면, flyway_schema_history
테이블에 마이그레이션 기록이 남습니다.
flyway_schema_history
테이블 중 주요 칼럼의 의미를 알아봅시다.
- installed_rank: 각 마이그레이션이 적용된 순서를 나타냅니다. 숫자가 작을수록 먼저 적용된 마이그레이션입니다.
- version: 마이그레이션의 버전 번호를 나타냅니다. Flyway는 이 버전번호를 사용하여 마이그레이션의 순서를 결정합니다.
- checksum: Flyway는 이 값을 사용하여 마이그레이션 파일의 무결성을 검증합니다. 만약 동일한 버전의 마이그레이션 파일이 변경되면, Flyway는 이를 감지하고 오류를 발생시킵니다.
- execution_time: 마이그레이션이 실행되는 데 걸린 시간을 초 단위로 기록합니다.
V1_1__init_constraints.sql 생성
이제 두 번째 SQL을 작성해보겠습니다. 두 번째 SQL에서는 이전에 생성한 테이블들의 외래키 무결성을 설정하는 작업을 수행합니다.
ALTER TABLE answer
ADD CONSTRAINT fk_answer_to_applicant
FOREIGN KEY (applicant_id)
REFERENCES applicant(applicant_id);
ALTER TABLE answer
ADD CONSTRAINT fk_answer_to_question
FOREIGN KEY (question_id)
REFERENCES question(question_id);
// ...
Flyway Desktop 활용하기
Flyway Desktop을 설치하여 데이터베이스 마이그레이션을 시각적으로 관리할 수 있습니다.
- 아래 사이트에 접속하여 OS에 맞게 설치합니다.
https://www.red-gate.com/products/flyway/community/download/
- Community 플랜을 선택합니다.
- New Project를 선택합니다.
- Flyway에서 구분할 프로젝트 정보와 연동할 데이터베이스의 정보를 입력합니다.
주의사항
yaml
application.yaml의 설정을 변경합니다.
Before)
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
hibernate:
ddl-auto: none
defer-datasource-initialization: true
After)
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
hibernate:
ddl-auto: none
defer-datasource-initialization: false
Flyway 버전 번호
Flyway는 마이그레이션 파일의 버전을 엄격히 관리합니다. 새 마이그레이션 파일을 작성할 때는 항상 이전 파일의 버전보다 높은 버전 번호를 사용해야 합니다.
V2__add_member_role.sql
ALTER TABLE member ADD COLUMN role varchar(255) default "";
Repository Test
@DisplayName("동아리 레포지토리 테스트")
@DataJpaTest
class ClubRepositoryTest {
@Autowired
private ClubRepository clubRepository;
@BeforeEach
void setUp() {
clubRepository.deleteAllInBatch();
}
@DisplayName("이미 DB에 저장되어 있는 ID를 가진 동아리를 저장하면, 해당 ID의 동아리는 후에 작성된 정보로 업데이트한다.")
@Test
void sameIdUpdate() {
//given
...
//when
...
//then
...
}
}
DataJpatTest와 Flyway를 사용하는 경우, auto configuration에서 제외해 주어야 한다.
@DataJpaTest(excludeAutoConfiguration = { FlywayAutoConfiguration.class })
public class RepositoryTest {
}
@DisplayName("동아리 레포지토리 테스트")
class ClubRepositoryTest extends RepositoryTest {
//...
}