실제 서비스 운영 중 DB 스키마가 변경되면 어떡하지?

What if the database changes while the service is running?

이전 프로젝트들에서 배포 후에는 DB 스키마를 변경하고 유지 보수한 적이 없어서 생각을 못 했다. 생각해 보면 귀찮아서 ALL DROP 후 CREATE 때린 거 같기도 하고ㅋㅋㅋ. 근데 만약 실제 운영하는 서비스이고 실제 데이터가 들어있다면? 하나라도 삭제되거나 잘못 변경되면 큰일 나기 때문에 신경 쓸게 많을 것이다. 일일이 각 배포 환경 돌아다니며 직접 schema를 변경할 수도 있겠지만 여간 귀찮은 게 아닐 것이다. 그리고 그러다 실수하면? 물론 실수한 게 문제가 아니다. 사람은 누구나 실수를 할 수 있다고 생각한다. 그런 환경이 나오지 않도록 하는 게 중요한 것 같다.

이와 관련해서 Flyway가 떠오를 것이다. 전부터 Flyway가 무엇인지는 알고 있었지만 미리 적용하진 않았다. 필요성을 체감을 하고 그때 도입을 하자라고 생각을 했었기 때문이다. 그러다 드디어 우리 집사의고민에 적용할 때가 왔기 때문에 Flyway에 대해 한번 알아보자


Flyway

Flyway는 오픈소스 데이터베이스 마이그레이션 툴이라고 하는데 쉽게 말하면 데이터베이스 형상관리 툴이라고 생각하면 좀 더 쉬울 거 같다. 우리 소스 코드 같은 경우는 git 형상관리 툴을 이용하여 코드를 잘 관리하고 있는데 데이터베이스도 Flyway를 통해 잘 관리해 줄 수 있다.

적용 과정

실제 적용해보는 과정을 보면서 알아가봅시다.

프로젝트 생성

우선 Spring Initializr로 가서

와 같이 의존성을 추가한 후 generate 버튼을 눌러 프로젝트를 생성해서 열어줍니다. 저렇게 하면 다음과 같은 의존성이 나오게 됩니다.

	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.flywaydb:flyway-core'
	implementation 'org.flywaydb:flyway-mysql'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	runtimeOnly 'com.mysql:mysql-connector-j'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

application.yml 생성

그리고 엔티티를 만들기 앞서 환경설정을 해줍니다.

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/flyway-test
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: none
  # Flyway 활성화
  flyway:
    enabled: true

MYSQL 데이터베이스 생성

직접 MYSQL을 설치하여도 되지만 저는 간단하게 하기 위해 컨테이너로 띄워줬습니다. 다음과 같이 docker-compose.yml 파일을 생성하고 docker-compose up -d를 이용해 실행시켜주면 간단하게 mysql을 띄울 수 있습니다.

version: "3"
services:
  mysql-db:
    image: mysql:8.0
    volumes:
      - ./mysql:/var/lib/mysql
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: flyway-test
    platform: linux/x86_64

기본 엔티티 생성

다음과 같은 기본 엔티티가 있습니다.

package entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private String password;
    private String age;
    private String bio;

}

V1 마이그레이션 스크립트 생성

Flyway는 마이그레이션 스크립트의 버전 순서대로 SQL 스크립트를 실행한다. 우선, 제일 첫 번째 스크립트므로 다음과 같은 내용을 /resources/db/migration 위치에 V1__init.sql 파일명으로 생성해 준다. (언더스코어(_)가 2번!)

CREATE TABLE member (
    id BIGINT AUTO_INCREMENT,
    email VARCHAR(255),
    password VARCHAR(255),
    age VARCHAR(255),
    PRIMARY KEY (id)
);

이렇게 애플리케이션을 실행시켜주시면 member 테이블을 생성하고 데이터베이스 이력을 관리하는 테이블인 flyway_schema_history가 생성됩니다.

컬럼을 보시면 현재 version1이라고 잘 설정된 게 보입니다.

엔티티 변경

그렇게 기능 개선이나 유지 보수를 하다 보면 엔티티 구조가 변경될 수 있겠죠?

package entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private String password;
    private String age;
    private String bio; //새로 추가된 필드

}

그러면 저희는 간단하게 그다음 버전의 마이그레이션 스크립트만 작성하면 됩니다!

V2 마이그레이션 스크립트 생성

V1 스크립트와 마찬가지로 동일한 위치(/resources/db/migration)에 다음과 같은 내용으로 V2__add_bio.sql 파일을 생성해 줍니다. 내용에는 전체 내용을 다 적는 게 아니라 변경사항만 적어주도록 합니다. 여기서 마이그레이션 스크립트 명명법이 궁금할 텐데 조금 있다 설명하겠습니다.

ALTER TABLE member ADD COLUMN bio VARCHAR(255);

작성 후 애플리케이션을 실행시켜주면 다음과 같이 version2의 히스토리와 멤버 테이블이 변경된 걸 확인할 수 있습니다. 즉, 이제는 스키마가 변경되어도 직접 배포 서버 DB에 들어가 수정하는 게 아니라 변경 사항을 코드로 관리할 수 있게 된 것입니다. 🙌

마이그레이션 스크립트 명명법

버전 변경 마이그레이션(V), 실행 취소 마이그레이션(U), 반복 가능한 마이그레이션(R) 등 파일 유형을 결정하는 접두사가 있고 이 접두사는 파일 이름 앞에 붙는다. 다음으로 버전 번호가 오는데 이 버전 번호는 원하는 모든 형식이 가능하지만, 특정 마이그레이션에 대해 고유해야 하며(버전이 지정된 마이그레이션과 실행 취소 마이그레이션은 공통 버전 번호를 공유해야 함) 논리적으로 순서가 맞아야 한다.

그다음 밑줄(_) 두 개를 추가하여 파일의 기능적 명명 측면과 순수하게 설명적인 측면을 구분한다. 이 이후에는 그냥 텍스트로 단어 사이에 밑줄을 사용하면 공백으로 번역된다. 이 부분이 flyway_schema_history의 description 부분에 들어가는 텍스트이다.


그렇다면 기존 테이블과 데이터가 있는 경우에는?

위의 경우를 보면 아직 운영 서버를 띄우기 전부터 flyway 사용을 결정하여 init부터 주입하는 경우이다. 그렇다면 이미 운영서버가 돌아가고 있고(기존 테이블과, 데이터 존재) 아직 flyway가 적용되어 있지 않은 경우는 어떻게 해야 될까?? (처음에 바로 도입하지 않는 이상 다 이렇지 않을까?)

baselineOnMigrate

똑같이 상황을 주기 위해 flyway_schema_history 테이블을 드랍하고 일단 마이그레이션 스크립트들도 삭제해 보자. 현재 멤버 테이블만 남아있는 상태이고 처음 flyway를 연동시킨 상태이다. 이 상태로 한번 실행해 보자. 다음과 같은 에러가 발생한다.

baseline-on-migrate의 경우 기본 값이 false인데 schema history가 없고 기존 schema도 없으면(우리가 위에서 실습했던 방법, baseline-on-migrate를 따로 안 건듬) 잘 실행되었다. 하지만 여기서 기존 schema가 있는 경우 따로 설정해 주지 않을 시 에러가 뜬다.

그래서 기존의 데이터베이스 스키마를 Flyway의 버전 관리 아래로(초기 버전 설정) 가져오기 위해 baseline-on-migrate를 true로 설정해준다.

spring:
  flyway:
    baseline-on-migrate: true

그렇게 애플리케이션을 실행하게 되면 다음과 같이 version1에 flyway가 기존의 스키마를 baseline으로 지정한다.

그리고 버전에 맞게 마이그레이션 스크립트를 잘 넣어주면 flyway가 잘 작동하는 걸 볼 수 있다.

근데 지금 이렇게 하면 한 가지 찝찝한 곳이 있다. 지금 version1을 baseline에 쓰고 있기 때문에 앞으로 /resources/db/migration에 V2부터 넣어야 된다는 것이다. 실제로 V1 스크립트를 넣고 실행을 해봐도 안 되는 것을 확인할 수 있을 것이다. 그래서 나중에 본 사람은 왜 V1은 없을까 하고 의문을 품을 수 있을 것 같다.(무엇보다 2부터 시작하는 게 불편함ㅎ)

이는 flyway의 baseline-version의 기본값이 1부터이기 때문인데 이를 0으로 바꿔주면 된다.

spring:
  flyway:
    baseline-version: 0

이렇게 되면 이제 편안하게 V1, V2, V3… 적용할 수 있다!


참고:

*틀린 부분이 있으면 언제든지 말씀해 주시면 공부해서 수정하겠습니다.


© 2022. All rights reserved.

Powered by 애송이