티스토리 뷰

반응형

최근 새로운 플젝에 합류를 하게 되었는데, flyway라는 것을 사용해보자는 의견이 나왔고 flyway라는 것을 한 번도 사용해본적이 없어서 공부를 하는 시간을 가지게 되었습니다. 그래서 flyway가 무엇인지 Spring Boot 환경에서의 적용 방법에 대해서 정리하려고 합니다.

✏️ Flyway란 무엇인가?

간단하게 말해서 DB 스키마 이력을 코드로 관리하고, 자동으로 적용해 주는 도구를 의미합니다.
테이블 생성, 컬럼 추가/삭제, 인덱스/제약조건 변경, 데이터 마이그레이션 등의 작업을 서버가 실행될 때 자동으로 적용합니다.
쉽게 말해서 프로젝트를 진행할 때 코드의 버전 관리는 Git이나 Github로 관리할 수 있는 것처럼 DB의 변경사항을 버전별로 관리할 수 있도록 해주는, 즉 DB의 형상관리를 해주는 역할을 합니다.

 

✏️ Flyway가 필요한 이유

Flyway가 필요한 이유보다는 DB Migration이 필요한 이유라고 보는 것이 더 적절할 것 같습니다. 

  1. DB는 상태를 가지고 있습니다 => 코드는 git checkout을 하면 끝이지만, DB는 이미 실행된 SQL은 되돌리기 어렵습니다, 그렇기 때문에 변경 이력을 기록하지 않으면 누가, 언제 어떤 SQL을 어떤 순서로 실행했는지 알기 어렵습니다.
  2. 수동 SQL은 순서가 불명확하고, 재현이 불가하며 롤백이 어렵다는 단점을 가집니다
  3. Hibernate의 ddl-auto전략의 문제점 => JPA에서 제공하는 Hibernate에는 ddl-auto전략을 통해서 애플리케이션 시점에 Hibernate가 DB 스키마를 어떻게 다룰지 정하는 전략들이 존재하는데 이 부분은 밑에서 좀 더 자세하게 다뤄보겠습니다.

 

✏️ Spring의 Hibernate ddl-auto 전략

 

방금 위에서 언급했던 부분입니다. 우선 dll-auto 전략에는 none, validate, update, create, create-drop의 옵션들이 존재합니다.

  1. none => 이름 그대로 Hibernate가 DB에 아무런 작업을 하지 않습니다. 테이블을 생성하거나 컬럼을 수정하거나 검증들을 하지 않는 사람이 DB를 수동적으로 관리할 수 있는 환경을 제공합니다.
  2. validate => Hibernate가 프로젝트에 존재하는 Entity와 DB의 구조를 비교하여 다르면 즉시 에러를 발생시킵니다. 구조가 다르다고 해서 변경을 하지는 않습니다. 다만, Entity구조보다 DB의 구조가 더 확장이 되어 있는 경우에도 에러가 발생하지 않습니다. 쉽게 말해서 Entity에 존재하는 컬럼과 테이블이 DB의 스키마에 존재만 하게 되면, DB에 추가로 컬럼이 존재하든 테이블이 존재하든 오류 없이 실행됩니다.
  3. update => Entity의 변경 사항을 DB에 반영합니다. 즉 코드를 작성하고 Entity에 추가적인 작업을 하게 되면 실행 시 자동으로 변경 사항들이 DB에 반영됩니다. 이 경우 어떤 SQL이 나가는지 알 수 없으며 팀원마다 DB 상태가 달라질 수 있습니다.
  4. create => 애플리케이션 시작 시 기존의 테이블을 전부 삭제하고 새롭게 생성합니다. 즉 Entity에 존재하는 코드를 기반으로 해서 실제 DB의 테이블들을 모두 삭제하고(데이터들도 삭제) 새롭게 생성합니다. 테스트 용으로만 용이하기 때문에 운영 환경에서는 권장되지 않습니다.
  5. create-drop => 시작 시에는 create전략을, 종료 시에는 drop 전략을 사용하여 테스트 종료 후 깨끗한 DB 상태를 유지합니다.

이렇게 다양한 전략들이 있는데, 이 중에서 update 전략은 위험합니다. Hibernate가 DB를 몰래 수정하기 때문에 팀원들마다 DB의 불일치가 발생할 수 있게 되고 서버 배포시에 큰 문제점을 가져올 수 있습니다.

 

✏️ Flyway의 해결전략

Flyway의 내부 동작은 크게 2가지로 볼 수 있습니다. 버전을 관리하는 테이블과 실행 알고리즘입니다.

  1. Schema History Table => Flyway는 DB에 자신의 meta table을 생성합니다(보통 이름은 flyway_schema_history). 이 테이블에는 적용된 버전, 마이그레이션 이름, 체크섬, 적용 시각, 성공/실패 여부가 기록되고 Flyway는 이 테이블을 기준으로 이 DB에 뭐가 적용되었고 뭐가 적용되지 않았는지를 판단합니다.
  2. 실행 알고리즘 => 실행 알고리즘은 크게 5단계로 나눠집니다. 먼저 현재 DB의 schema_history를 읽습니다. 그런 다음 아직 적용되지 않은 마이그레이션을 찾게 되고, 버전 순서대로 실행합니다. 성공하면 schema history table에 기록이 되며 실패하면 즉시 중단합게 되며 중간 실패 시 절대 다음 버전으로 넘어가지 않습니다.

아래 사진들은 Flyway의 해결 전략에 대한 Flyway 공식 문서에서 제공하는 사진들입니다.

 

 

✏️ Flyway의 특징

Flyway는 다양한 특징들을 가지는데 Flway가 보장하는 것과, 보장하지 않는 것, 마이그레이션 파일 규칙에 대해서 살펴보겠습니다.

  1. Flyway가 보장하는 것 => 마이그레이션 순서, 마이그레이션 중복 실행 방지, 변경 이력 추적, 환경 간 스키마 동기화
  2. Flyway가 보장하지 않는 것 => SQL자체의 정합성, 비즈니스 무결성, 성능 최적화, 안전한 롤백
  3. 마이그레이션 파일 규칙 => Flyway는 Naming에 대해서 엄격한 편입니다.
    • V : Version Migration 을 의미
      Undo의 U, Repeatable의 R을 사용하기도 하는데 대부분 V를 사용한다
    • 1 : 파일의 버전(unique)
      적용되지 않는 파일 중에서 같은 버전이 존재할 경우 어떤 것을 적용할 지 몰라 에러를 뱉는다
      - 새로 적용하려는 파일은 기존의 적용된 파일의 버전보다 높아야 한다
    • __ : seperator, 항상 언더바가 두개 존재한다
    • init : 파일 설명
      history의 description 항목으로 들어가기 때문에 좀 자세히 적어야 한다
    • .sql : 확장자 명

✏️ Flyway의 비교 로직

이렇게 글로만 적게 되면 이해가 좀 어려울 수도 있으니 간단한 예제를 통해서 Flyway의 흐름에 대해서 파악해보겠습니다.
우선 프로젝트에 아래와 같은 SQL파일들과 history 테이블이 있다고 가정해보겠습니다.

db/migration/
├── V1__init_schema.sql
├── V2__add_user_table.sql
├── V3__add_index_on_user_email.sql
├── V4__add_user_onboarding.sql
├── V5__add_user_birth.sql

DB에는 flyway_schema_history 테이블이 아래 사진과 같이 저장되어 있는 상황입니다. 즉 V1~V3까지 적용되었다고 볼 수 있습니다.

 

STEP 1. history 테이블 읽기

Flyway는 먼저 DB에서 아래와 같은 SQL문을 통해서 history테이블을 읽게 되고 다음과 같은 결과가 나옵니다.

-- 실행
SELECT version, checksum
FROM flyway_schema_history
WHERE success =true;

-- 결과
(1,827364912)
(2,193847221)
(3,882734912)

 

STEP 2. 로컬 SQL 파일 스캔

로컬에서 읽은 정보 => 로컬에는 현재 5개의 파일이 존재하기 때문에 아래와 같이 5개의 키-값 형태의 결과가 나옵니다.

(1,827364912)
(2,193847221)
(3,882734912)
(4,123984721)
(5,777231004)

 

STEP 3. 버전 단위로 비교

  1. Version => DB 기록 있음, checksum 일치 => skip
  2. Version 2 => DB 기록 있음, checksum 일치 => skip
  3. Version 3 => DB 기록 있음, checksum 일치 => 스킵
  4. Version 4 => DB 기록 없음 =>  실행 대상
  5. Version 5 => DB 기록 없음 => 실행 대상

STEP 4. 실제 실행 단계

Flyway는 이제 아직 실행되지 않은 것만 실행합니다. 따라서 아래와 같은 순서로 SQL파일들이 실행된 후 history 테이블에 기록됩니다.

V4__add_user_onboarding.sql
V5__add_user_birth.sql

 

STEP 5. 실행 후 history 테이블 상태

 

✏️ Spring Boot + Flyway

Flyway에 대해서 알아보았으니 Spring Boot에서는 어떻게 사용할 수 있을지 알아보겠습니다.

패키지 구조

src/main/resources
├── application-dev.yml
├── application-prod.yml
├── application.yml
├── db
│   └── migration
│       └── V20260111204623__V1__init_schema.sql.sql
├── static
└── templates
    └── mail
        └── verificationEmail.html

application.yml

baseline-on-migrate는 이미 테이블이 존재하는 DB에 Flyway를 처음 도입할 때 사용합니다.
이 옵션이 true이면, Flyway는 기존 스키마를 baseline-version으로 간주하고 이전 마이그레이션을 실행하지 않습니다.

flyway:
  enabled: true
  baseline-on-migrate: true
  baseline-version: 1
#  locations: classpath:db/migration

build.gradle(PostgreSQL)

implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'

실행하기

최초로 실행한 모습입니다. flyway_schema_history라는 테이블이 생긴 것을 확인할 수 있고, 버전이 기록된 것을 확인할 수 있습니다.

버전 추가

Flyway 세팅을 성공했으니 이번엔 DB의 버전을 추가해보겠습니다. 간단하게 User테이블에 ranking이라는 컬럼을 추가해보도록 하겠습니다. db/migration 패키지 내에 V2__add_user_ranking.sql과 같은 파일을 만든 뒤 실행합니다.
또한 처음 세팅 시에는 application.yml에서 locations 옵션을 주석처리 하였지만, 이제부터는 버전 관리를 하기 위해서는 flyway가 실행할 SQL파일들의 위치를 알아야 하기 때문에 주석을 해제합니다. 
실행을 하게 되면 다음과 같은 오류가 발생합니다. User 테이블에 이미 데이터가 들어가 있는데, 새롭게 추가하는 컬럼을 not null 속성을 추가해서 발생한 오류입니다. NOT NULL을 다시 지우고 실행시키면 정상적으로 실행되는 것을 확인할 수 있습니다.

-- 실행 전
create table "user"
(
    id             bigint generated by default as identity
        constraint "User_pkey"
            primary key,
    oauth_id       varchar(30),
    oauth_provider oauth_provider_enum   not null,
    name           varchar(30),
    age            bigint,
    sex            sex_enum,
    email          varchar(50)           not null
        constraint "User_email_key"
            unique,
    password       varchar(255),
    is_deleted     boolean default false not null,
    deleted_at     timestamp(6),
    refresh_token  varchar(512),
    created_at     timestamp(6)          not null,
    updated_at     timestamp(6)          not null,
    birth          date
);


-- 실행 후
create table "user"
(
    id             bigint generated by default as identity
        constraint "User_pkey"
            primary key,
    oauth_id       varchar(30),
    oauth_provider oauth_provider_enum   not null,
    name           varchar(30),
    age            bigint,
    sex            sex_enum,
    email          varchar(50)           not null
        constraint "User_email_key"
            unique,
    password       varchar(255),
    is_deleted     boolean default false not null,
    deleted_at     timestamp(6),
    refresh_token  varchar(512),
    created_at     timestamp(6)          not null,
    updated_at     timestamp(6)          not null,
    birth          date,
    ranking        integer
);
반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함