인식한 문제

개발 과정에서 데이터베이스 관리의 어려움이 있었다. 팀은 EC2 인스턴스에 백엔드 서버를 구축하여 배포하고, 프론트엔드가 해당 인스턴스의 엔드포인트로 API 요청을 전송하는 방식으로 통합 테스트를 수행했다.
기존에는 서버 재배포를 할 때마다 Hibernate의 spring.jpa.hibernate.ddl-auto 옵션을 create
를 사용히여 데이터베이스가 초기화
되었다. 따라서 프론트엔드 분이 테스트에 사용하던 데이터가 모두 사라지는 상황
이 반복돼서 테스트하기 번거롭다고 하셨다.
처음에는 api 연동 테스트할 때 재배포 시 DB가 초기화 되어도 서버에서 목 데이터를 삽입하여 테스트 할 수 있도록 환경을 구성하였는데 깊이가 있는 테스트트의 경우 목 데이터 이외 프론트 분이 직접 데이터를 작성하고 주기적으로 테스트를 진행해야 했다.
사실 이전에 우리 팀원들은 서비스가 출시된 이후에는 DB 테이블에 컬럼을 추가하거나 스키마를 변경할 때 사용자의 중요한 데이터가 손상되지 않도록 해야한다
라는 문제를 인식하고 있었고 출시 후 DB 마이그레이션을 적용하기로 하였다.
하지만 출시 전 다음과 같은 문제 상황이 미리 발생하였기에 개발 환경에서 미리 도입하여 안정성을 확보하기로 했다.
해결 과정
먼저 도입하기에 앞서 조금 더 DB 마이그레이션이 어떤 역할을 하는지, 어떤 상황에서 쓰이는지 찾아보고 적용을 하였다.
데이터베이스 마이그레이션은 왜 해야할까?

프로젝트를 진행하다 보면, 데이터베이스가 위 그림처럼 여러 환경에서 존재
하게 된다. 각자의 로컬 환경에 데이터베이스가 존재하고, 테스트 서버, 프로덕션 서버에서도 각각 별도의 데이터베이스가 존재한다. 각자의 로컬 환경에서 개발을 하게 되면, 엔티티의 구조가 변경되고 이로 인해 데이터베이스의 스키마도 변경
된다.
소프트웨어의 소스 코드와 같은 경우에는 Git과 같은 형상관리 툴을 사용하였지만 데이터베이스는 그렇지 않다.
우리 프로젝트로 예시를 들면 JPA를 사용하는 환경이므로 엔티티 구조가 변경되면 이 변경된 구조를 다른 배포 환경의 데이터베이스에도 적용
해야 한다. 즉, 일일이 스키마 수정을 위한 DDL을 각 환경별로 모두 실행해 줘야 한다.
로컬 개발 환경이나 개발 서버에서는 Hibernate 설정 중 ddl-auto를 create , create-drop , update 등으로 설정하여 DDL을 변경된 엔티티 구조에 맞춰 실행할 수 있지만, 프로덕션에서는 이와 같은 설정이 불가능
하다.(실제 서비스 운영 환경에서 create를 사용하면 데이터베이스를 DROP 후 생성하기 때문에 사용자의 데이터가 날라가 버린다! 생각만 해도 끔찍하다..!)
이러한 문제를 해결하기 위해 체계적인 데이터베이스 마이그레이션 도구의 필요성을 느끼게 되었다.
처음에는 “직접 DB 서버에 접속해서 일일이 명령어를 쳐서 수정하면 되지 않나?” 라는 생각이 들었다. 하지만 이렇게 하면 오타를 낼 수도 있고, 실수로 명령어를 하나 빼먹는 것과 같은 휴먼 에러
가 발생할 가능성이 커진다고 한다.
따라서 데이터베이스 마이그레이션의 필요성을 깨달은 후, 이를 해결할 도구를 찾아보았고, 그 과정에서 쉽고 안전하게 변경사항을 반영할 수 있고, 버전 관리도 해주는 Flyway를 찾게 되었다.
Flyway란?
Flyway는 오픈소스 데이터베이스 마이그레이션 툴로, 데이터베이스의 변경 사항을 추적하고, 업데이트나 롤백을 쉽게 할 수 있도록 도와주는 도구
이다.
Flyway를 사용하면 SQL 기반의 마이그레이션 파일을 작성하여, 데이터베이스의 변경 사항을 체계적으로 관리
할 수 있다. 이를 통해 각 환경의 데이터베이스를 동일한 상태로 유지하면서도, 운영 환경에서도 안정적인 스키마 변경을 보장할 수 있다.
우리 서비스에에 Flyway 도입하기
의존성 추가
먼저 build.gradle에 Flyway 의존성을 추가했다
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'
설정 추가
spring.jpa.hibernate.ddl-auto=validate # create x
spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=false
spring.flyway.locations=classpath:db/migration
spring.flyway.validate-on-migrate=true
spring.flyway.fail-on-missing-locations=true
spring.flyway.user=flyway_user
spring.flyway.password={flyway_user_password}
ddl-auto 옵션을 validate로 설정하여 DB 스키마와 JPA 엔티티 매핑이 일치하는지 검사만 한다 → 불일치 시 에러 발생
- enabled: Flyway 활성화
- baseline-on-migrate: 기존 데이터베이스에 마이그레이션 이력 테이블이 없을 때 자동으로 베이스라인 설정하는데 우리 서비스는 DB를 초기화하고 처음부터 flyway를 적용하였으므로 사용 x
- baseline-version: 베이스라인 버전
- locations: 마이그레이션 스크립트 위치
DB 계정 권한 분리
기존에 테스트할 때는 root 계정을 애플리케이션 계정으로 사용하였지만 실제 운영 환경에서는 사용자에게 모든 권한을 부여해서는 안되고
데이터에 대한 CRUD(create, select, update, delete)만 부여해줘야 한다고 한다.
spring.datasource.username=user
spring.datasource.password={user_password}
따라서 애플리케이션에서 DB에 접근하는 계정을 root가 아닌 다음과 같이 DML 권한만 부여한 user 계정
을 사용하였다.
반면 Flyway는 테이블을 생성해야하기 때문에 user보다 더 많은 권한이 필요하다. Flyway는 스키마 변경과 테이블 생성을 해야 하므로 DDL 권한을 추가하였다.
(IP접근 제한을 설정하여 내 컴퓨터에서만 DB를 접근할 수 있도록 보안을 강화할 수 있다고 하는데 추후에 적용해 봐야겠다.)
디렉토리 구조 생성
src/main/resources/
db/
migration/
V1__init.sql
V2__Add_student_column_is_deleted.sql
설정에서 locations=classpath:db/migration
으로 설정하였으므로 다음과 같은 db/migration 하위 파일에 sql 파일을 작성하면 된다.
스크립트 파일 작성 규칙

flyway의 마이그레이션 스크립트 파일 이름 규칙이 있다.
V<버전번호>__<설명>.sql
숫자가 작은 버전의 마이그레이션부터 숫자가 큰 버전 순서대로 스크립트가 실행되고 1, 1.1, 2, 3 …처럼 순차적 증가도 가능하고 20250101, 20250203 … 처럼 날짜 형태로 사용해도 괜찮다.
- V : 현재 버전을 새로운 버전으로 업데이트
- U : 현재 버전을 이전 버전으로 되돌리는 경우
- R : 버전에 관계없이 매번 실행하는 경우
예시를 보면 다음과 같다
ex)
V1__init.sql
V2__Add_student_column_is_deleted.sql
...
_가 2개
인 걸 꼭 주의하자 _로 작성 후 테스트하다 한참동안 에러를 못 찾았다..!
스크립트 작성
먼저 Student 엔티티를 구성하자.
@Entity
public class Student{
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@Column(length = 11, nullable = false, unique = true)
private String phoneNumber;
@Column(length = 64, nullable = false)
private String name;
@Column(length = 64, nullable = false)
private String country;
}
이 엔티티에 대한 테이블을 만들기 위해 스크립트를 작성하자. 실행하면 student 초기 테이블이 만들어진다.
V1__init.sql
CREATE TABLE `student`
(
`id` BIGINT NOT NULL AUTO_INCREMENT,
`phone_number` VARCHAR(11) NOT NULL UNIQUE,
`country` VARCHAR(64) NOT NULL,
`name` VARCHAR(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
...
이후 개발중 student 테이블에 deleted 필드를 추가할 일이 생겨 필드를 다음과 같이 수정하였다.
@Entity
public class Student{
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@Column(length = 11, nullable = false, unique = true)
private String phoneNumber;
@Column(length = 64, nullable = false)
private String name;
@Column(length = 64, nullable = false)
private String country;
@Column(name = "deleted", nullable = false)
private Boolean isDeleted;
}
V1__init.sql에 이어 추가될 스키마에 대한 스크립트를 작성해줘야 한다. 파일 이름을 V2__Add_student_column_is_deleted.sql
로 추가 내용과 관련지어 명명하고 스크립트를 다음과 같이 작성해주었다.
V2__Add_student_column_is_deleted.sql
ALTER TABLE student
ADD COLUMN deleted BOOLEAN DEFAULT FALSE;

다음과 같이 deleted 필드가 잘 적용된 것을 확인할 수 있다.
또한 위에서 변경 사항을 git처럼 추적해준다고 하였는데 flyway가 자동으로 flyway_schema_history
테이블을 생성하고 관리한다.

flyway_schema_history 테이블을 보면 작성한 script에 대해 버전별로 관리되는 것을 볼 수 있다.

결과
실제 운영 환경에서 DB 마이그레이션이 왜 필요한지 깨달았고 재배포 시에도 테스트하던 데이터를 유지하며 빠르게 할 수 있었다. 일일이 스크립트를 작성해야 한다는 번거로움은 있었지만 버전 관리를 효율적으로 할 수 있었다. 적용하면서 많은 시행착오가 있었는데 그중 가장 헤맸던 명명 규칙을 꼭.. 주의하자..!