Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Seongbeen Kim edited this page Dec 14, 2021 · 1 revision

Flyway

  • 오픈소스 Database 마이그레이션 툴
  • Local 개발 환경의 DB 변경사항을 다른 배포단계의 DB에 적용 하려면 배포하기 전에 변경사항을 수동으로 처리한 뒤 배포해야한다. 이러한 과정 속에서 변경해야할 사항을 하나라도 빠뜨리게 될 경우 문제를 일으키게 되는데 이러한 수동 작업 + 수동 작업으로 인한 문제점을 해결해줄 수 있는 것이 바로 flyway다.

적용한 이유

  • DB schema의 변경 이력이 생긴다.
    • 변경 후 DB schema 에서 어떤 문제가 발견되었다면, 이전 이력을 통해서 문제를 해결하기 훨씬 쉬워진다.
  • DB schema 변경 작업이 더 안전해진다.
    • 수동으로 하나씩 DB를 변경하지 않기 때문에 빼먹거나 실수할 여지가 줄어들고, 배포 시에 자동화도 가능해진다.
  • 협업 하기에 수월해진다.
    • 문제 발생 시 문제를 파악하기 좋고, 실제 환경과 동일한 local 환경을 구축하기 쉬워져서 local 에서 개발하기가 훨씬 수월해진다.

동작 방식

두 개의 마이그레이션을 가지고 있고 DB는 비워져있다고 가정

flyway-flow-1

  1. flyway를 통해서 마이그레이션을 할 때, 마이그레이션 대상 Shiny DBSCHEMA_VERSION table이 존재하는지 판단하고 없다면 flyway가 자동으로 신규로 생성한다.

    • 이용 가능한 마이그레이션을 application classpath에서 스캔한다.

    EmptySchemaVersion

  2. SCHEMA_VERSION 이 이미 존재해있거나 새로 생성되었다면 flyway는 마이그레이션을 위해 지정된 파일(sql or Java)을 지정된 classpath에서 탐색하여 버전 순서대로 실행한다.

    • SCHEMA_VERSION은 마이그레이션 체크섬을 추척하여, 마이그레이션이 성공적으로 완료되었는지도 확인한다.
    • SCHEMA_VERSION을 확인하여 마이그레이션 버전이 현재 버전과 같거나 더 낮을 경우, 무시한다.
    • 버전을 통해서 정렬 되어있고 순차적으로 실행한다.

    flyway-flow-2

  3. 마이그레이션이 실행되고 나면 SCHEMA_VERSION table에 실행이력을 저장하게 된다.

    SchemaVersion

명령어

  • flyway [option] 명령어
    • ex) flyway -url="db주소" -user="user" clean

1. migrate

  • DB 스키마를 현재 버전으로 마이그레이션한다.

    • 이용가능한 마이그레이션을 classpath에서 스캔한 뒤 pending된 마이그레이션들을 적용한다

    command-migrate

2. info

  • 모든 마이그레이션 상세 정보 출력한다.

    • DB 스키마의 현재 status 또는 version을 출력한다.
    • 어떤 마이그레이션이 적용되었는지, pending된 상태인지도 출력한다.

    command-info

3. validate

  • DB에 적용된 마이그레이션 정보의 유효성을 검증한다.

    • 현재 DB 스키마를 이용가능한 마이그레이션을 통해 검증한다.

    command-validate

4. baseline

  • flyway로 관리하기 이전에 DB가 존재시 해당 DB를 flyway baseline 으로 설정할 수 있다.

    • 기존 존재하는 DB에서 Flyway를 시작하는데 도움을 주는 명령어이다.
      • 기존 존재하는 DB의 현재 상태를 기준으로 flyway_schema_history을 table 생성한다.

    command-baseline

5. repair

  • 메타 데이터 테이블 문제를 해결하기 위해 사용하는데 두가지 용법이 존재한다.
    1. 실패한 마이그레이션 항목 제거( DDL 트랜잭션을 지원하지 않는 DB에만 해당)

    2. 적용된 마이그레이션의 체크섬을 이용 가능한 마이그레이션의 체크섬으로 재정렬한다.

      command-repair

6. clean

  • DB의 SCHEMA_VERSION 테이블 포함한 모든 objects(tables, views, procedures, ...)를 drop시킨다.

    • 운영 DB에서는 절대 사용하지 말아야한다.

    command-clean

SCHEMA_VERSION

  • SCHEMA_VERSION 파일 이름 컨벤션

    • <Prefix><Version>__<Description>.sql

      • <Prefix> – default prefix V (Version) 는 버전 마이그레이션이다.
        • 만약 V 외의 prefix로 변경하고 싶다면 설정파일에서 flyway.sqlMigrationPrefix 속성을 정의해주면 된다.
        • R (Repeatable)은 반복 마이그레이션을 뜻한다. 버전 상관없이 항상 실행되는 스크립트를 의미한다.
      • <Version> – 버전 마이그레이션 번호를 뜻하며, _ 로 주 버전, 부 버전을 나눌 수 있다. 마이그레이션 버전은 항상 1로 시작해야한다.
      • <Description> – 해당 마이그레이션 설명을 뜻하며, 버전 마이그레이션 번호 사이에서 반드시 __ 로 분리되어야한다.
    • ex) V1_1_0__my_first_migration.sql

      • V : prefix
      • 1_1_0 : version
      • __my_first_migration.sql : description

      SqlMigrationNaming

  • Flyaway가 관리하는 SCHEMA_VERSION 역할을 하는 테이블이 된다.

  • 적용된 스크립트는 절대로 변경하면 안된다.

기존 DB 존재 시 Flyway 적용 방법 (baseline)

Flyway 라이브러리 추가 및 설정

  1. gradle에 flyway 라이브러리 추가 : implementation "org.flywaydb:flyway-core:7.14.0"

  2. application 설정 파일에 flyway 설정 추가

    application.yml
    
    spring:
        jpa:
            hibernate:
                ddl-auto: validate
        flyway:
            baseline-on-migrate: true
            baseline-version: 0
            locations: classpath:db/migration/{vendor}
    • ddl-auto: validate : JPA entity 정보를 바탕으로 생성된 스키마가 실제 관계형 DB 스키마와 일치하는지 검증해준다.
    • baseline-on-migrate: true : SCHEMA_VERSION 이 존재하지 않는 DB 스키마에 대해서 마이그레이션이 실행될 때, baseline을 호출하게 함으로써 SCHEMA_VERSION 을 생성해준다.
      • 기존 DB 스키마가 존재하지 않을 경우, SCHEMA_VERSION 이 생성되지 않는다.
    • baseline-version: 0 : 일반적으로 V1__ 형태로 버전 1부터 시작하기 때문에 baseline을 버전 0으로 지정하여 버전 1부터 마이그레이션들을 적용하게 한다.
      • default = 1
    • locations: classpath:db/migration/{vendor} : db 종류를 기준으로 마이그레이션을 따로 적용한다.
      • h2를 사용할 경우에는 db/migration/h2, mysql을 사용할 경우에는 db/migration/mysql 경로를 참조하게 된다.
      • Spring boot default scan은 classpath:db/migration 임으로 경로 변경을 원할시에는 spring.flyway.locations: 경로 를 해주면 된다.
    • Spring boot의 문서를 보면 default로 컨텍스트에 존재하는 @Primary DataSource를 autowired하기 때문에 spring.datasource 속성이 설정이 되어있다면 flyway를 위한 속성 설정을 하지 않아도 된다.
      • 다른 Datasource를 사용하고 싶다면 @FlywayDataSource 으로 빈을 생성해주거나 아래와 같이 설정 파일에서 설정을 해주면 된다.

        application.yml
        
        spring:
            flyway:
                url: jdbc:h2:mem:kodesalon
            user: sa
            password:
            schemas: kodesalon
      • 운영, 개발 환경 등에 다른 마이그레이션을 적용하고 싶을 경우

        application-dev.yml
        
        spring:
            flyway:
                locations: classpath:/db/migration,classpath:/dev/db/migration
        • 이와 같이 설정을 하여 dev profile이 활성화될 때는 dev/db/migration 만 실행하도록 할 수 있다.
    • Flyway 설정 오버라이딩 적용 우선순위
      1. System properties
      2. Environment variables
      3. Custom config files
      4. Gradle properties
      5. Flyway configuration section in build.gradle
      6. <user-home>/flyway.conf
      7. Flyway Gradle plugin defaults
  3. ${project_root}/src/main/resources/db/migration/mysql 디렉터리 추가

  4. 추가한 경로에 빈 스크립트 파일 생성

    • V1__init.sql 파일 생성
      • baseline을 사용할 경우 반드시 빈 script 파일이 필요하다.
  5. flyway 적용 확인

    • DB에 스키마 변경 이력을 확인할 경우

      select *
      from flyway_schema_history;
      flyway-baseline-applied
      • 제대로 적용된 것을 확인할 수 있다.
  6. 새로운 데이터 갱신

    • Flyway를 실행하여 생성된 DB 스키마와 JPA entity 정보를 바탕으로 생성된 스키마와 비교
      • 일치하지 않을 경우 오류가 발생한다.

      • IntelliJ의 JPA Buddy 플러그인을 이용하면 더 쉽게 Flyway를 다룰 수 있다.

        flyway-version-pattern

        • flyway 파일 생성 시 V1, V2V202108300911 처럼 타임스탬프 형식으로도 자동으로 생성하도록 할 수 있다.
          • 타임스탬프 패턴의 이점
            • V1 → V2 → V3처럼 연속된 숫자로 할 경우 같은 프로젝트를 진행하는 둘 이상의 개발자가 동일 숫자를 지정할 수도 있다.
            • outOfOrder = true Migration을 해도 이미 초 단위로 나누어져 있기 때문에 따로 버저닝을 위한 숫자를 추가하지 않아도 된다.
  • 기존 테이블에 새로운 컬럼 추가할 경우

    • ex) member entity에 address 칼럼을 추가할 경우

      @Entity
      @Where(clause = "deleted = 'false'")
      @NoArgsConstructor(access = AccessLevel.PROTECTED)
      @Table(name = "member", uniqueConstraints = {
              @UniqueConstraint(name = "member_unique_constraint", columnNames = {"alias"})})
      public class Member extends BaseEntity {
      
          .....
      
          @Column(name = "address", nullable = false)
          private String address;
      • 추가한 address 컬럼에 대한 스크립트 작성하지 않고 실행할 경우 아래와 같은 예외가 발생한다.

        member_add_address_error
        • member 테이블에서 address 칼럼을 찾지 못한다.
      • mysql 폴더에서 마우스 오른쪽 버튼 클릭하여 JPA Buddy 를 사용한 스크립트 생성

        jpa_buddy_flyway_1
        • New - Flyway - Diff Versioned Migration : Entity 스키마와 실제 DB를 비교하여 다른 점을 모두 자동으로 찾아주고 SQL문까지 모두 작성해준다.
      • 버전 마이그레이트 적용할 DB 선택

        jpa_buddy_flyway_2
        • Target(URL)에서 어느 DB와 비교할 지 선택한다.
          • 현재는 테스트이기 때문에 로컬용 DB(kodesalon-mysql)에 연결했다.
      • 마이그레이션해야 할 목록 확인

        jpa_buddy_flyway_3
        • member 테이블에 address 칼럼 추가 SQL문이 자동적으로 작성되어 있다.
        jpa_buddy_flyway_4
        • member 테이블에 address 칼럼을 nullable하지 않게 수정하는 SQL문이 자동적으로 작성되어 있다.
      • 목록에서 확인한 SQL문이 작성된 스크립트 파일 생성 및 이름 변경

        jpa_buddy_flyway_5
        • V2__.sql 이 생성되었고 목록에서 확인한 SQL문들이 작성되어 있다.
        jpa_buddy_flyway_6
        • V2__.sqlV2_member_add_address.sql 로 이름 변경 및 SQL문도 하나로 합쳐주었다.
      • 실행 및 flyway_schema_history 확인

        jpa_buddy_flyway_7
        • 정상적으로 작동하며 마이그레이션이 성공했다는 것을 확인할 수 있다.
  • 새로운 테이블이 추가될 경우

    • ex) image entity를 생성할 경우

      @Entity
      @NoArgsConstructor(access = AccessLevel.PROTECTED)
      @Table(name = "image", uniqueConstraints = {
              @UniqueConstraint(name = "image_unique_constraint", columnNames = {"url"})})
      public class Image {
          
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Long id;
      
          @Column(name = "url", nullable = false)
          private String url;
      }
      • 추가한 image entity에 대한 스크립트 작성하지 않고 실행할 경우 아래와 같은 예외가 발생한다.

        jpa_buddy_flyway_8
        • image 테이블을 찾지 못한다.
      • 마이그레이션해야 할 목록 확인

        jpa_buddy_flyway_9
        • image table 생성 및 unique constraint SQL문이 작성되어 있는 것을 확인할 수 있다.
          • 오른쪽 상단 File name에서 이름을 변경해줄 수 있다.
      • 목록에서 확인한 SQL문이 작성된 스크립트 파일 생성 및 이름 변경

        jpa_buddy_flyway_10
        • V3__.sql 이 생성되었고 목록에서 확인한 SQL문들이 작성되어 있다.
        jpa_buddy_flyway_11
        • V3__.sqlV3_image_create.sql 로 이름 변경해주었다.
      • 실행 및 flyway_schema_history 확인

        jpa_buddy_flyway_12
        • 정상적으로 작동하며 마이그레이션이 성공했다는 것을 확인할 수 있다.

SchemaManagementException 오류

  • flyway 적용 중 BOOLEAN 타입 관련 validation 오류가 발생했다.

    [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: wrong column type encountered in column [deleted] in table [board]; found [bit (Types#BIT)], but expecting [boolean default false (Types#BOOLEAN)
    public class Board extends BaseEntity {
    
    		... 
    
        @Column(name = "deleted", nullable = false, columnDefinition = "boolean default false")
        private boolean deleted;
    • columnDefinition = "boolean default false" 로 설정 되어있는데 실제로 동작하는 과정에서는 문제가 없으나, ddl-auto = validate 를 함으로써 JPA가 생성한 스키마와 실제 DB 스키마와 차이가 있어 생기는 문제였다.
  • org.hibernate.tool.schema.internal.AbstractSchemaValidator#validateColumnType

    protected void validateColumnType(
    			Table table,
    			Column column,
    			ColumnInformation columnInformation,
    			Metadata metadata,
    			ExecutionOptions options,
    			Dialect dialect) {
    		boolean typesMatch = dialect.equivalentTypes( column.getSqlTypeCode( metadata ), columnInformation.getTypeCode() )
    				|| column.getSqlType( dialect, metadata ).toLowerCase(Locale.ROOT).startsWith( columnInformation.getTypeName().toLowerCase(Locale.ROOT) );
    		if ( !typesMatch ) {
    			throw new SchemaManagementException(
    					String.format(
    • 그래서 해당 함수에 breakpoint를 찍어 놓고 board.deleted가 들어왔을 때 어떻게 매칭이 되는 지 확인을 해봤다.

      flyway-validation-error-1

      flyway-validation-error-2

      • column sqlType = "boolean default false"sqlTypeCode = null
        • JPA로 생성된 column
      • columnInformation typeName = "BIT"typeCode = -7이라는 것을 볼 수 있다.
        • 실제 DB column
      public int getSqlTypeCode(Mapping mapping) throws MappingException {
      		org.hibernate.type.Type type = getValue().getType();
      		try {
      			int sqlTypeCode = type.sqlTypes( mapping )[getTypeIndex()];
      			if ( getSqlTypeCode() != null && getSqlTypeCode() != sqlTypeCode ) {
      				throw new MappingException( "SQLType code's does not match. mapped as " + sqlTypeCode + " but is " + getSqlTypeCode() );
      			}
      			return sqlTypeCode;
      
      		...
      }
      
      public boolean equivalentTypes(int typeCode1, int typeCode2) {
      	return typeCode1==typeCode2
      		|| isNumericOrDecimal(typeCode1) && isNumericOrDecimal(typeCode2)
      		|| isFloatOrRealOrDouble(typeCode1) && isFloatOrRealOrDouble(typeCode2);
      }
      • column은 column.getSqlTypeCode(metadata) 에서 BooleanType(16)을 받아와 sqlTypeCode = 16 이 된다.
      • 그렇기 때문에 dialect.equivalentTypes(column.getSqlTypeCode(metadata), columnInformation.getTypeCode()) 에서 column 16, columnInformation -7이 일치한지 비교한 결과인 false를 리턴한다.
      • 그리고 column.getSqlType( dialect, metadata ).toLowerCase(Locale.ROOT).startsWith( columnInformation.getTypeName().toLowerCase(Locale.ROOT) 에서 column의 sqlType = "boolean default false" 이 columnInformation의 typeName = "BIT"bit 로 시작하지 않기 때문에 false를 반환하여 에러가 발생한다.

    해결 방법

    1. columnDefinition을 boolean default falsebit defalut 0
      • 에러가 발생했던 앞의 과정과 모두 똑같았지만 column.getSqlType(dialect, metadata ).toLowerCase(Locale.ROOT).startsWith(columnInformation.getTypeName().toLowerCase(Locale.ROOT) 에서 column의 sqlType = "bit default 0" 이 columnInformation의 typeName = "BIT"bit 로 시작하기 때문에 true를 반환하여 에러가 발생하지 않는다.
    2. columnDefinition 적용 X;
      • 에러가 발생했던 앞의 과정과 모두 똑같았지만column.getSqlType(dialect, metadata ).toLowerCase(Locale.ROOT).startsWith(columnInformation.getTypeName().toLowerCase(Locale.ROOT) 에서 column의 sqlType = "bit" 이 columnInformation의 typeName = "BIT"bit 로 시작하기 때문에 true를 반환하여 에러가 발생하지 않는다.

참조

Flyway Documentation - Community Plugins and Integrations: Spring Boot

Database Migrations with Flyway

나만 모르고 있던 - Flyway (DB 마이그레이션 Tool)

Flyway 로 Java 에서 DB schema, seed 관리하기

Docker Compose 로 local 개발 환경 쉽게 관리하기

Execute Flyway Database Migrations on Startup

Flyway 기능소개

flyway - Java 용 Database migration framework

Hibernate/JPA – Default Values

초기 어플리케이션 기동이 오래 걸립니다.

Clone this wiki locally