Liquibase 데이터베이스 마이그레이션 적용하기 - Spring, Jpa, Hibernate, Liquibase Plugin, AWS private database

데이터베이스 마이그레이션이란?

데이터베이스 마이그레이션에 대한 글을 찾아보면, 데이터베이스 마이그레이션의 일반적인 정의는 다음과 같습니다.

한 운영환경에서 다른 운영환경으로 옮기는 작업

그럼 이러한 작업이 필요한 이유가 무엇일까요?

업무를 진행하다보면 최소한으로 개발 환경과 운영 환경을 철저하게 분리되서 운영됩니다.

회사마다 천차만별이겠지만, 어떤 곳은 개발 환경도 여러 스테이지로 나누어 관리하는 경우도 있죠.

여기서 문제점이 발생합니다.

데이터베이스 스키마 변경이 발생하면, 분리된 모든 환경에 마이그레이션을 해야합니다.

마이그레이션은 물론 수동으로 할 수 있지만, 사실 이 작업의 경우 휴먼 에러의 가능성이 매우 높습니다.

그래서 우리는 이러한 부분을 최소화하기 위해서 툴을 사용합니다.

마이그레이션 툴 선택

마이그레이션 툴 선택에는 툴이 지원하는 커버리지를 보고 선택하게 됩니다.

저는 스프링 기반의 개발을 자주함으로 다음과 같은 부분에 대해서 고려하고자 했습니다.

  1. 마이그레이션 코드 생성 자동화가 가능한가?
  2. 코틀린을 지원하나?
  3. gradle plugin 을 지원하는가?
  4. command line 지원하는가?

많은 툴들이 있었으나, 스프링 진영에서 가장 많이 사용하는 flyway liquibase 로 추려졌고,

두 개를 비교해보았습니다.

flyway vs liquibase

flyway나 liquibase 둘 다 코틀린 지원이나, gradle plugin, command line 지원이 가능하였으나

liquibase 만 추가적인 툴 사용하지 않고도 마이그레이션 코드 생성 자동화가 가능했습니다.

flyway 도 물론 jpa buddy 라는 intellij 플러그인을 사용하면 마이그레이션 코드 생성이 가능하나,

자동화는 어려웠고, 휴먼 에러의 가능성이 충분히 있을것이라 판단하여 liquibase 를 사용하는 것으로 마음을 굳혔습니다.

본 포스팅 내용을 숙지하게 되면 다음과 같은 업무를 수행할 수 있게 됩니다.

  1. 간단한 local migration
  2. aws 에서 private subnet에 있는 database migration 적용

스프링 프로젝트를 위한 liquibase set up

kotlin script 기준으로 진행합니다.

또한 database migration tool 도입을 고려한다는 시점에서 스프링을 일정 시간이상 사용해왔다라고 생각하고,

이미 spring + jpa + hibernate 를 사용하는 프로젝트가 이미 존재한다라는 가정을 베이스로 진행하겠습니다.

dependency 추가

plugins {
    id("org.liquibase.gradle") version "2.1.0"
}

dependencies {
    //Liquibase
    liquibaseRuntime("org.liquibase:liquibase-core:4.6.2")
    liquibaseRuntime("org.liquibase:liquibase-groovy-dsl:3.0.2")
    liquibaseRuntime("info.picocli:picocli:4.6.2")
    liquibaseRuntime("org.liquibase.ext:liquibase-hibernate5:4.6.2")
    liquibaseRuntime(sourceSets.main.get().compileClasspath)
    liquibaseRuntime(sourceSets.main.get().runtimeClasspath)
    liquibaseRuntime(sourceSets.main.get().output)
    liquibaseRuntime("org.postgresql:postgresql")
}

liquibase {
    activities.register("main") {
        this.arguments = mapOf(
            "driver" to "org.postgresql.Driver",
            "url" to "jdbc:postgresql://localhost:5432/liquibase_test",
            "username" to "liquibase",
            "password" to "liquibase",
            "changeLogFile" to "src/main/resources/db/changelog/changelog-1.0.yaml",
            "logLevel" to "debug",
            "classpath" to "src/main/resources/"
        )
    }
    runList = "main"
}
  • liquibase plugin 을 추가해줍니다. 해당 플러그인을 추가해야 liquibaseRuntime 을 사용할 수 있습니다.
  • 사용하는 데이터베이스를 추가합니다. 여기서는 postgresql 을 사용합니다.
  • jpa entity 와 데이터베이스를 비교하기 위해서는 liquibaseRuntime("org.liquibase.ext:liquibase-hibernate5:4.6.2") 가 필요합니다.
  • picocli 는 xml 파일 형식을 지원하기 위해 필요한 dependency 입니다.

Liquibase Plugin 에 대한 이해

  • 저는 업무를 진행하면서, Gradle Plugin 사용만 했었지, Plugin 의 동작에 대한 이해를 하려고 했던적이 없었습니다.
    하지만 사용 전에 플러그인의 동작 방식을 간단하게 살펴보면, 업무 진행에 대한 이해도가 높아지게 됩니다.
    Liquibase Plugin 가 어떤 방식으로 동작되는지 간단하게 짚고 넘어가도록 하겠습니다.

  • liquibase plugin 은 liquibase 라이브러리에 포함되어 있는 liquibase command line java 클래스를 실행시킵니다.
    따라서 반드시 liquibase 가 실행될때 liquibase-core 가 필요합니다.

  • liquibase plugin task 를 실행시키면 LiquibaseTask 가 동작하게 됩니다. LiquibaseTask 는 command 를 input 으로 받습니다.
    하지만 plugin 코드를 살펴보면 각각의 gradle task 들은 이미 command 를 함께 정의하여 task 로 등록된 것을 알 수 있습니다.

  • LiquibaseTask 는 LiquibaseExtension 에 등록된 activities 를 가져와서 해당되는 runList 를 실행합니다. 만약 runList 가 정의되어 있지 않으면 모든 activity 를 실행시키게 됩니다. 위 dependency 에서 LiquibaseExtension 에 대한 설정을 추가하였습니다. 위 설정대로라면 LiquibaseTask 가 실행될때 runList가 main 임으로 main 으로 등록된 activity 의 argument 를 가지고 실행하게 됩니다.

  • generateChangeLog 의 경우 반드시 필요한 argument 는 url, username, password, changeLogFile 입니다. 각각의 Task 마다 필요한 argument 가 일부 차이가 있습니다. Argument 에 대한 내용은 공식 페이지에 Commands 섹션을 참고해주세요

changlog master file 생성

  1. spring project 의 resource 디렉토리에 db/changelog 디렉토리를 생성

  2. db.changelog-master.yaml 파일 생성 (파일 형식은 선호도에 따라 생성)

    databaseChangeLog:
      - include:
          file: 파일 경로

changelog 디렉토리 구조는 liquibase docs 를 확인하면 best practice 가 존재합니다.

ChangeLog 생성

위와 같이 구성되면, 로컬에서 데이터베이스 마이그레이션 할 준비가 끝이 납니다. 일반적으로 database migration 을 적용할때는 운영 전에 migration 을 적용하는 경우도 있겠지만, 대부분의 경우 이미 운영중인 database 에 migration 을 적용할 것이라 생각합니다. 우선적으로 local database 로 changelog 를 생성하도록 하겠습니다. 추후에 aws private database 에 적용하는 방법을 알아보겠습니다.

local database 에서 changelog 생성

local database 에 이미 테이블이 생성되어 있다고 가정하겠습니다.

  • Gradle Plugin 탭에서 liquibase -> generateChangeLog 실행

그럼 resource/db/changelog 디렉토리에 changelog-1.0.yaml 이 생성되어 있을겁니다. Liquibase 는 다양한 포맷을 지원하기에 changelog 의 파일 형식을 바꿔주면 됩니다. 다만 sql 의 경우 반드시 {file-name}.{database-type}.sql 형식을 지켜줘야 합니다. Database-type 의 경우 공식 문서를 참고해주세요.

아래 Entity 가 yaml 파일 형식인 changeSet 으로 변경되면 아래와 같습니다.

  • TestEntity.kts

    package com.example.liquipoc
    
    import javax.persistence.Column
    import javax.persistence.Entity
    import javax.persistence.GeneratedValue
    import javax.persistence.GenerationType
    import javax.persistence.Id
    
    @Entity(name = "test")
    class TestEntity {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        var id: Long? = null
        @Column
        val name: String = "name"
        @Column
        val updateField: String = "name"
        @Column
        val updateField2: String = "name"
    }
  • changelog-0.1.yaml

    databaseChangeLog:
    - changeSet:
        id: 1644214866723-1
        author: jaden (generated)
        changes:
        - createTable:
            columns:
            - column:
                autoIncrement: true
                constraints:
                  nullable: false
                  primaryKey: true
                  primaryKeyName: test_pkey
                name: id
                type: BIGINT
            - column:
                name: name
                type: VARCHAR(255)
            - column:
                name: update_field
                type: VARCHAR(255)
            - column:
                name: update_field2
                type: VARCHAR(255)
            tableName: test

Changelog 동기화

위와 같은 최초 changelog 가 생성되면 데이터베이스에 liquibase migration 을 적용해야 합니다.

  • master 파일 수정
databaseChangeLog:
  - include:
      file: src/main/resource/db/changelog/changelog-0.1.yaml
  • default activity 추가 및 runList default 로 수정
liquibase {
    activities.register("main") {
        this.arguments = mapOf(
            "driver" to "org.postgresql.Driver",
            "url" to "jdbc:postgresql://localhost:5432/liquibase_test",
            "username" to "liquibase",
            "password" to "liquibase",
            "changeLogFile" to "src/main/resources/db/changelog/changelog-1.0.yaml",
            "logLevel" to "debug",
            "classpath" to "src/main/resources/"
        )
    }
  
  activities.register("default") {
        this.arguments = mapOf(
            "driver" to "org.postgresql.Driver",
            "url" to "jdbc:postgresql://localhost:5432/liquibase_test",
            "username" to "liquibase",
            "password" to "liquibase",
            "changeLogFile" to "src/main/resources/db/changelog/db.changelog-master.yaml",
            "logLevel" to "debug",
            "classpath" to "src/main/resources/"
        )
    }
    runList = "default"
}
  • liquibase gradle task exec : changelogSync
    • liquibase migration 을 적용하는 방법은 2가지가 있습니다.
      1. liquibase update : table 이 생성되지 않은 데이터베이스에 적용
      2. liquibase changelogSync : 이미 테이블이 생성되어 있는 경우에 databasechangelog 테이블 생성을 위해서 적용

liquibase changelogSync 까지 적용했으면, 데이터베이스를 확인했을때 databasechangelog table과 databasechangeloglock table이 생성되어 있을겁니다. 그럼 migration 이 잘 적용되었다고 볼 수 있죠.

DiffChangeLog 생성

이제 entity 의 변경이 발생한 경우에 변경 사항에 대한 changeset을 생성해야 합니다. 아까 사용했던 test entity에 필드 하나를 추가해보겠습니다.

  • updaetField3 필드 추가
package com.example.liquipoc

import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id

@Entity(name = "test")
class TestEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null
    @Column
    val name: String = "name"
    @Column
    val updateField: String = "name"
    @Column
    val updateField2: String = "name"
    @Column
    val updateField3: String = "name"
}
  • activity 추가, diff

    • Entity 와 데이터베이스를 비교하기 위해서는 referenceUrl 에 hibernate url 이 들어가야 합니다. 그리고 changelogfile argument 는 기존 changelog file 에 append 하려면 그대로 사용하면 되며, 다음 버전을 사용하고자 한다면 이름을 변경해주면 새롭게 생성이 됩니다.
    activities.register("diff") {
            this.arguments = mapOf(
                "driver" to "org.postgresql.Driver",
                "url" to "jdbc:postgresql://localhost:5432/liquibase_test",
                "username" to "liquibase",
                "password" to "liquibase",
                "changeLogFile" to "src/main/resources/db/changelog/changelog-0.1.yaml",
                "referenceUrl" to "hibernate:spring:com.example.liquipoc?" +
                    "dialect=org.hibernate.dialect.PostgreSQL95Dialect&" +
                    "hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&" +
                    "hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy",
                "defaultSchemaName" to "",
                "logLevel" to "debug",
                "classpath" to "$buildDir/classes/kotlin/main"
            )
        }
    
  • 프로젝트 빌드 : entity 의 변경 사항이 있다면 diffChangelog 를 실행하기 전에 반드시 빌드를 해줘야합니다.

  • gradle task : diffChangeLog

    • 해당 task 를 실행하면 다음과 같이 append 가 됩니다. databaseChangeLog 가 중복임으로 제거해줍니다.

      databaseChangeLog:
      - changeSet:
          id: 1644214866723-1
          author: jaden (generated)
          changes:
          - createTable:
              columns:
              - column:
                  autoIncrement: true
                  constraints:
                    nullable: false
                    primaryKey: true
                    primaryKeyName: test_pkey
                  name: id
                  type: BIGINT
              - column:
                  name: name
                  type: VARCHAR(255)
              - column:
                  name: update_field
                  type: VARCHAR(255)
              - column:
                  name: update_field2
                  type: VARCHAR(255)
              tableName: test
      
      databaseChangeLog: ---------------> 삭제
      - changeSet:
          id: 1644216277941-1
          author: jaden (generated)
          changes:
          - addColumn:
              columns:
              - column:
                  name: update_field3
                  type: varchar(255)
              tableName: test
      
      

변경 사항 업데이트

  • gradle task : update

해당 task 를 실행하면 새롭게 생성된 changeSet 이 데이터베이스에 적용이 됩니다.

마무리

Liquibase 의 Migration 방식에 대해서 알아보았습니다. AWS 의 private database 에 liquibase 를 적용하기 위해는 다양한 방법들이 있겠지만, 가장 간단하게는 private database에 접속할 수 있는 bastion server 를 두고, 해당 서버에서 liquibase docker image를 가지고 실행하면 됩니다.

  • bastion server 에서 실행할때 몇가지 확인해야 할 점들이 있습니다.
    1. docker container 가 해당 디렉토리에 대한 write 권한이 있는가?
    2. docker container 가 해당 디텍토리에 대한 read 권한이 있는가?

위 권한이 없는 경우 liquibase docker image가 컨테이너로 돌면서 file 관련 exception 이 발생하게 됩니다.

좋은 웹페이지 즐겨찾기