안녕!
우아한테크코스 5기 [스탬프크러쉬]팀 깃짱이라고 합니다.
사장모드: stampcrush.site/admin
고객모드: stampcrush.site
💋 인트로
프로젝트 진행 중에 데이터베이스의 스키마가 변경되는 상황이 자주 발생했다.
null 제약조건 추가 정도의 가벼운 상황이었을 때는, mysql 명령어를 통해서 직접 alter 컬럼을 해주었는데, 점점 변경이 커지고 데이터 구조 자체가 변화하다 보니 형상 관리가 필요한 지경에 이르렀다.
그중에서 우리 팀은 가장 익숙한 Flyway를 도입해서 데이터베이스 형상 관리를 하도록 했다.
Flyway가 처음에는 좀 어렵게 느껴졌지만, 본질적으로는 '형상 관리'이므로 변경에 대한 내용을 추적할 수 있도록 저장하는 것이었다.
이 포스팅은 우리 팀의 Flyway 적용 시에, 검색해도 잘 나오지 않았던 이해하기 어려웠던 점을 중점적으로 포스팅을 작성했기 때문에,
Flyway 기본 설정에 대해서 궁금하다면 다른 개발자께서 잘 정리해 놓은 글이 많기 때문에 아래 링크를 참고하면 좋을 것 같다.
💋 Flyway란?
✔️ 개념
Flyway는 오픈소스 데이터베이스 마이그레이션 툴이다.
데이터베이스 마이그레이션 툴이란 데이터베이스의 변경 사항을 추적하고, 업데이트나 롤백을 보다 쉽게 할 수 있도록 도와주는 도구다.
위의 기본 설정과 사용법에 대해 이해했다는 전제 하에서,
곧바로 우리 팀의 버전별 sql 파일에 대해서 소개하겠다.
(계속해서 설명을 읽는 것보다 한 번 따라해 보는 것이 더 이해가 쉬울 수도 있음)
✔️ 사용 방법
도입은 간단하다. 아래의 내용을 build.gradle에 넣고 재빌드하면 됨.
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'
💋 V1__init.sql: 최초의 데이터베이스 스키마 등록
- 해당 파일을
main/resources/db/migration
폴더 아래에 생성한다. 위치가 중요함! - 최초의 데이터베이스 스키마를 등록한다.
- 데이터베이스 스키마 변경 이전의 스키마를 등록해주면 된다.
- 우리팀의 최초 데이터베이스 스키마는 테이블 create문과 foreign key 설정에 대한 명령어였다.
- 전문은 매우매우 길어서 접는 글에 넣어놓았고, 아래 코드는 그중 일부다!
create table customer (
customer_id bigint not null auto_increment,
dtype varchar(31) not null,
nickname varchar(255),
phone_number varchar(255),
primary key (customer_id)
) engine=InnoDB;
// ...
create table cafe (
close_time time(6),
open_time time(6),
created_at datetime(6),
id bigint not null auto_increment,
owner_id bigint,
updated_at datetime(6),
business_registration_number varchar(255),
cafe_image_url varchar(255),
detail_address varchar(255),
name varchar(255),
road_address varchar(255),
telephone_number varchar(255),
introduction tinytext,
primary key (id)
) engine=InnoDB;
create table cafe_coupon_design (
deleted bit,
cafe_id bigint,
created_at datetime(6),
id bigint not null auto_increment,
updated_at datetime(6),
back_image_url varchar(255),
front_image_url varchar(255),
stamp_image_url varchar(255),
primary key (id)
) engine=InnoDB;
create table cafe_policy (
deleted bit,
expire_period integer,
max_stamp_count integer,
cafe_id bigint,
created_at datetime(6),
id bigint not null auto_increment,
updated_at datetime(6),
reward varchar(255),
primary key (id)
) engine=InnoDB;
create table cafe_stamp_coordinate (
stamp_order integer,
x_coordinate integer,
y_coordinate integer,
cafe_coupon_design_id bigint,
created_at datetime(6),
id bigint not null auto_increment,
updated_at datetime(6),
primary key (id)
) engine=InnoDB;
create table coupon (
deleted bit,
expired_date date,
cafe_id bigint,
coupon_design_id bigint,
coupon_policy_id bigint,
created_at datetime(6),
customer_id bigint,
id bigint not null auto_increment,
updated_at datetime(6),
status enum ('ACCUMULATING','EXPIRED','REWARDED'),
primary key (id)
) engine=InnoDB;
create table coupon_design (
created_at datetime(6),
id bigint not null auto_increment,
updated_at datetime(6),
back_image_url varchar(255),
front_image_url varchar(255),
stamp_image_url varchar(255),
primary key (id)
) engine=InnoDB;
create table coupon_policy (
expired_period integer,
max_stamp_count integer,
created_at datetime(6),
id bigint not null auto_increment,
updated_at datetime(6),
reward_name varchar(255),
primary key (id)
) engine=InnoDB;
create table coupon_stamp_coordinate (
stamp_order integer,
x_coordinate integer,
y_coordinate integer,
coupon_design_id bigint,
created_at datetime(6),
id bigint not null auto_increment,
updated_at datetime(6),
primary key (id)
) engine=InnoDB;
create table customer (
customer_id bigint not null auto_increment,
dtype varchar(31) not null,
nickname varchar(255),
phone_number varchar(255),
primary key (customer_id)
) engine=InnoDB;
create table favorites (
is_favorites bit,
cafe_id bigint,
created_at datetime(6),
customer_id bigint,
id bigint not null auto_increment,
updated_at datetime(6),
primary key (id)
) engine=InnoDB;
create table owner (
created_at datetime(6),
id bigint not null auto_increment,
oauth_id bigint,
updated_at datetime(6),
email varchar(255),
encrypted_password varchar(255),
login_id varchar(255),
name varchar(255),
oauth_provider enum ('KAKAO','NAVER'),
phone_number varchar(255),
primary key (id)
) engine=InnoDB;
create table register_customer (
customer_id bigint not null,
oauth_id bigint,
email varchar(255),
encrypted_password varchar(255),
login_id varchar(255),
oauth_provider enum ('KAKAO','NAVER'),
primary key (customer_id)
) engine=InnoDB;
create table reward (
used bit,
cafe_id bigint,
created_at datetime(6),
customer_id bigint,
id bigint not null auto_increment,
updated_at datetime(6),
name varchar(255),
primary key (id)
) engine=InnoDB;
create table sample_back_image (
created_at datetime(6),
id bigint not null auto_increment,
updated_at datetime(6),
image_url varchar(255),
primary key (id)
) engine=InnoDB;
create table sample_front_image (
created_at datetime(6),
id bigint not null auto_increment,
updated_at datetime(6),
image_url varchar(255),
primary key (id)
) engine=InnoDB;
create table sample_stamp_coordinate (
stamp_order integer,
x_coordinate integer,
y_coordinate integer,
created_at datetime(6),
id bigint not null auto_increment,
sample_back_image_id bigint,
updated_at datetime(6),
primary key (id)
) engine=InnoDB;
create table sample_stamp_image (
created_at datetime(6),
id bigint not null auto_increment,
updated_at datetime(6),
image_url varchar(255),
primary key (id)
) engine=InnoDB;
create table stamp (
coupon_id bigint,
created_at datetime(6),
id bigint not null auto_increment,
updated_at datetime(6),
primary key (id)
) engine=InnoDB;
create table temporary_customer (
customer_id bigint not null,
primary key (customer_id)
) engine=InnoDB;
create table visit_history (
stamp_count integer not null,
cafe_id bigint,
created_at datetime(6),
customer_id bigint,
id bigint not null auto_increment,
updated_at datetime(6),
primary key (id)
) engine=InnoDB;
alter table coupon
add constraint UK_pysmwvb6mxft5ecat7qc9fr2w unique (coupon_design_id);
alter table coupon
add constraint UK_e7irxxxhrmoa16lyw1b4857oh unique (coupon_policy_id);
alter table customer
add constraint UK_rosd2guvs3i1agkplv5n8vu82 unique (phone_number);
alter table cafe
add constraint FKjmhmwpl2qo6olo6uu8k9wuqed
foreign key (owner_id)
references owner (id);
alter table cafe_coupon_design
add constraint FK9p5ooklyx21r1orjl9m8m2ise
foreign key (cafe_id)
references cafe (id);
alter table cafe_policy
add constraint FKp6lg1cur82wk5oe2abhw9xssw
foreign key (cafe_id)
references cafe (id);
alter table cafe_stamp_coordinate
add constraint FK96sbxq3avnw1v2vhs7rn3n0wq
foreign key (cafe_coupon_design_id)
references cafe_coupon_design (id);
alter table coupon
add constraint FKikrgfufc6m55rorf3aer857ux
foreign key (cafe_id)
references cafe (id);
alter table coupon
add constraint FKflo5yxa4jv8c86frlq65443d9
foreign key (coupon_design_id)
references coupon_design (id);
alter table coupon
add constraint FKjnahg3a5otcynjdmxl7cq8p5k
foreign key (coupon_policy_id)
references coupon_policy (id);
alter table coupon
add constraint FKhdsrf5sywjt5uh4po9g4h0qk1
foreign key (customer_id)
references customer (customer_id);
alter table coupon_stamp_coordinate
add constraint FK87rmtx0edcurkt727vpsq6xnb
foreign key (coupon_design_id)
references coupon_design (id);
alter table favorites
add constraint FKfv5m9oby4kukp5df1mjcp8ubb
foreign key (cafe_id)
references cafe (id);
alter table favorites
add constraint FKoyt0sxepwr5sys7a8w334epdg
foreign key (customer_id)
references customer (customer_id);
alter table register_customer
add constraint FKo86p65nwx0rgly9rv4ilw0xc9
foreign key (customer_id)
references customer (customer_id);
alter table reward
add constraint FKdqng5418xlqtmonlatkunudd2
foreign key (cafe_id)
references cafe (id);
alter table reward
add constraint FKq4a0acffv4i9l135k43n7dwr9
foreign key (customer_id)
references customer (customer_id);
alter table sample_stamp_coordinate
add constraint FKed9uy7avcvd4c27k7ug6f7f3t
foreign key (sample_back_image_id)
references sample_back_image (id);
alter table stamp
add constraint FKmtict4k20alprsw09x2tfvdgx
foreign key (coupon_id)
references coupon (id);
alter table temporary_customer
add constraint FKclqkafnemlkpnb0iojfbgms6c
foreign key (customer_id)
references customer (customer_id);
alter table visit_history
add constraint FK1dcn8j39322uea3105ilc6tds
foreign key (cafe_id)
references cafe (id);
alter table visit_history
add constraint FK3a0ps2neuswc89qdj333wmh8v
foreign key (customer_id)
references customer (customer_id);
💋 baseline-on-migrate
설정
✔️ baseline-on-migrate
설정이란?
기본적으로 flyway는 마이그레이션 스크립트를 순차적으로 적용하기 때문에, 마이그레이션 스크립트에 작성한 내용이 이미 데이터베이스에 반영되어 있다면 에러가 발생한다.
기준 버전 이전의 스크립트를 무시하고 새로운 스크립트만 적용하고자 할 때 baseline-on-migrate 설정을 사용할 수 있다.
만약 마이그레이션 스크립트가 V2까지 이미 적용되어 있다면, baseline-on-migrate 설정을 사용하지 않으면 flyway가 새로운 마이그레이션 스크립트를 적용하려고 할 때 에러가 발생할 수 있다.
만약, 아직 마이그레이션 스크립트가 V1부터 적용된 적이 없는 새로운 데이터베이스의 경우, baseline-on-migrate 설정을 사용하든 사용하지 않든 결과는 동일하다. 이 경우에는 모든 마이그레이션 스크립트가 순서대로 적용된다.
✔️ baseline-on-migrate: true
우리 프로젝트는 처음부터 flyway를 적용하지 않았기 때문에 V1__init.sql
에서 정의한 데이터베이스 스키마와 데이터가 이미 서버에 존재하고 있었다.
yml 파일을 통해서 flyway의 baseline-on-migrate 설정을 아래와 같이 설정했다.
spring:
flyway:
baseline-on-migrate: true
baseline-on-migrate: true
로 설정할 경우, Flyway는 자동으로 현재 존재하는 마이그레이션 스크립트 중 가장 최신의 버전을 기준으로 설정한다.
이 설정은 데이터베이스에 이미 적용된 마이그레이션 스크립트를 기준으로 삼아 미래에 새로운 마이그레이션 스크립트를 실행할 때 충돌을 방지한다.
Flyway는 마이그레이션 스크립트 중 가장 최신의 버전을 기준으로, 해당 버전 이전에 실행된 모든 마이그레이션 스크립트를 무시하고 새로운 마이그레이션 스크립트만 실행한다.
💋 flyway의 flyway_schema_history
테이블 생성
✔️ flyway_schema_history
테이블이란?
flyway를 적용한 채로 코드를 실행하게 되면, 아래와 같은 테이블이 자동으로 생성된다.
flyway_schema_history
테이블은 Flyway가 데이터베이스 마이그레이션을 추적하고 관리하기 위해 사용하는 특별한 테이블이다.
이 테이블은 Flyway에서 사용하는 메타데이터 테이블로, 데이터베이스에 적용된 각 마이그레이션 스크립트의 상태와 버전 정보를 저장합니다.
flyway_schema_history
테이블을 통해 Flyway는 데이터베이스에 적용된 마이그레이션 스크립트의 상태를 추적하고, 새로운 마이그레이션 스크립트를 실행할 때 이전에 적용된 스크립트를 건너뛰지 않고 진행할 수 있다.
flyway_schema_history 테이블 칼럼의 의미
- installed_rank: 마이그레이션 스크립트가 적용된 순서를 나타냅니다.
- version: 마이그레이션 스크립트의 버전을 나타냅니다.
- description: 마이그레이션 스크립트의 설명을 나타냅니다.
- type: 마이그레이션 스크립트의 유형을 나타냅니다. (SQL, JAVA, ...)
- script: 마이그레이션 스크립트의 파일명을 나타냅니다.
- checksum: 마이그레이션 스크립트의 체크섬 값입니다.
- installed_by: 마이그레이션 스크립트를 실행한 사용자를 나타냅니다.
- installed_on: 마이그레이션 스크립트를 실행한 일시를 나타냅니다.
- execution_time: 마이그레이션 스크립트의 실행 시간을 나타냅니다.
- success: 마이그레이션 스크립트의 실행 결과를 나타냅니다. (true, false)
아래 두 가지는 내 개인적인 궁금증으로 고민해봤다.
✔️ flyway_schema_history 테이블이 없는 상태에서 현재 내 버전을 어떻게 알아낼 수 있을까?
우리 프로젝트는 처음부터 flyway를 적용하지 않았기 때문에 V1__init.sql
에서 정의한 데이터베이스 스키마와 데이터가 이미 서버에 존재하고 있었다.
위에서 말한대로 따라서 baseline-on-migrate: true
설정을 해주긴 했지만, Flyway는 어떻게 내 데이터베이스가 V1을 실행한 상태라는 것을 알 수 있었을까?
flyway는 flyway_schema_history
테이블이 없는 상태에서는 현재 버전이 어디까지 적용되어 있는지 직접 sql 파일과 데이터베이스 스키마를 비교하여 확인한다. 이를 통해 flyway는 버전이 존재하지 않는 경우 해당 버전의 SQL 파일을 실행하여 데이터베이스를 업데이트한다.
✔️ 특정 버전의 마이그레이션 스크립트를 다시 실행하고 싶다면?
특정 버전의 마이그레이션 스크립트를 다시 실행하고 싶다면 해당 버전의 레코드를 flyway_schema_history
테이블에서 삭제하면 됩니다.
만약 V3부터 다시 실행하고 싶다면, flyway_schema_history
테이블에서 V3에 해당하는 레코드를 삭제하면 Flyway는 V3 마이그레이션 스크립트를 다시 실행하게 됩니다. Flyway는 데이터베이스에 저장된 버전 정보와 flyway_schema_history
테이블의 레코드를 비교하여 마이그레이션을 실행하므로, 해당 버전의 레코드가 삭제되면 다시 실행됩니다.
단, 이 작업은 주의해야 합니다. 마이그레이션 스크립트를 삭제하면 해당 스크립트에 의해 변경된 데이터베이스 상태가 롤백되므로, 이에 따른 데이터 유실이 발생할 수 있습니다. 따라서 데이터베이스 상태와 일치하도록 적절한 조치를 취한 후에 해당 레코드를 삭제하는 것이 좋습니다.
💋 V2__xxxx.sql: 변경된 데이터베이스 스키마 관리
- 해당 파일도 역시
main/resources/db/migration
폴더 아래에 생성한다. xxxx
자리에는 원하는 이름을 설정해주면 된다.
우리 팀은 지난 포스팅에서 설명했던 내용 대로, Customer
테이블에 몇몇 column을 추가해야 한다.
alter table customer
add `login_id` varchar(255);
alter table customer
add `encrypted_password` varchar(255);
alter table customer
add `email` varchar(255);
alter table customer
add `oauth_provider` varchar(255);
alter table customer
add `oauth_id` bigint;
alter table customer
add `customer_type` varchar(255);
✔️ 데이터 스키마가 아닌, 데이터 자체를 변경하는 명령어도 관리해야 할까?
Flyway는 기본적으로 스키마 변경을 위한 SQL 스크립트를 관리하는데 초점을 맞추고 있습니다.
데이터 레코드의 변경은 데이터베이스 스키마 변경과는 다른 성격을 가지기 때문에, Flyway와 같은 도구를 사용하여 데이터 레코드를 변경하는 것은 일반적으로 권장되지 않는 접근 방식입니다.
따라서 Flyway를 통해 레코드에 대한 Update 명령어를 관리하는 것은 일반적으로 권장되지 않습니다.
Flyway는 데이터베이스 스키마의 버전 관리와 마이그레이션을 위해 설계되었으며, 데이터 자체의 변경은 Flyway의 주된 목적이 아닙니다. 레코드에 대한 Update 작업은 보다 유연한 방법을 통해 관리하는 것이 일반적입니다.
Flyway와 같은 버전 관리 도구를 사용하여 데이터베이스 스키마 변경에 필요한 SQL 스크립트를 관리하는 데 사용하고, 데이터 레코드의 변경은 애플리케이션 코드나 ORM(Object-Relational Mapping) 도구 등을 사용하여 처리하는 것이 좋습니다.
✔️ 이미 실행된 버전의 sql 파일을 수정해도 될까?
이런! 방금 실행한 V2 파일의 customer_type
column의 타입이 enum인데, varchar로 잘못 설정해 버렸다는걸 알아버렸다.
슬쩍 V2_xxxx.sql
파일을 수정하면 되지 않을까…?
안된다!
Flyway는 버전이 올라가기만 한다는걸 잊으면 안된다.
만약에 V3__xxx.sql
을 실행한 뒤에, 해당 스키마가 잘못 되었다는 걸 깨달았다고 하자! 그렇다면 이미 적용이 완료된 V3의 파일을 수정해서는 안되고, 해당 스키마에서 내가 원하는 스키마로 갈 수 있도록 새로운 V4를 만들어서 수정을 하도록 하자.
그러면, 어떻게 수정하면 될까?
V3__alter_customer_type.sql
과 같이 새로운 sql 파일을 생성하고, alter문을 통해서 해당 칼럼의 타입을 수정하면 된다.
V3__alter_customer_type.sql
ALTER TABLE customer
MODIFY COLUMN customer_type ENUM('REGISTER', 'TEMPORARY');
이런 식으로, 이후에 있을 스키마의 변화는 모두 버전을 up한 파일 안에 적어야 한다는 것을 명심해야 한다!
💋 스탬프크러쉬의 형상 관리
우리팀의 이야기로, 자세한 플로우가 알고 싶다면, 아래 글을 펴서 나오는 포스팅을 참고하면 좋을 것 같다.
1. temporary_customer, register_customer 테이블의 정보 customer 테이블로 update한다.
이 과정은 스키마에 변화가 없기 때문에 직접 mysql에 접속해서 실행했다.
실제로는 직접 실행하는 방식은 실수가 발생할 수 있어 위험하기 때문에, 다른 유연한 방법을 더 알아보는 것을 추천한다.
UPDATE customer
INNER JOIN register_customer ON customer.customer_id = register_customer.customer_id
SET customer.login_id = register_customer.login_id,
customer.encrypted_password = register_customer.encrypted_password,
customer.email = register_customer.email,
customer.oauth_provider = register_customer.oauth_provider,
customer.oauth_id = register_customer.oauth_id,
customer.customer_type = 'REGISTER';
UPDATE customer
INNER JOIN temporary_customer ON customer.customer_id = temporary_customer.customer_id
SET customer.customer_type = 'TEMPORARY';
2. Customer 테이블의 dtype 칼럼을 삭제한다.
이 과정은 스키마에 변화가 생기기 때문에 새로운 버전의 sql 파일을 만들어 Flyway를 사용해서 관리했다!
ALTER TABLE customer
DROP COLUMN dtype;
3. temporary_customer, register_customer 테이블을 삭제한다.
이 과정 역시 스키마에 변화가 생기기 때문에 새로운 버전의 sql 파일을 만들어 Flyway를 사용해서 관리했다!
drop table temp_customer;
💋 개인적 궁금증
✔️ resources/db/migration 폴더 하위에서 마음대로 파일 위치를 정해도 될까?
Flyway는 스크립트 파일의 이름에 따라 버전을 관리합니다. 만약 스크립트 파일의 위치를 변경하더라도, Flyway는 이미 실행된 스크립트들의 버전을 기억하고 있어서 중복 실행되지 않습니다.
💋 참고자료
- https://www.baeldung.com/database-migrations-with-flyway
- https://documentation.red-gate.com/fd/
- https://dolsup.work/tech-blog/update-db-schema-without-downtime/
- https://velog.io/@suhongkim98/flyway로-데이터베이스-스키마-형상관리-하기
- https://hudi.blog/dallog-flyway/
- https://sujinnaljin.medium.com/software-engineering-형상-관리에-대하여-932d14f6f341
- https://meetup.nhncloud.com/posts/173
도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
'PROJECT > Stamp Crush' 카테고리의 다른 글
[우테코] 스탬프크러쉬에 실제 사용자(카페 사장)가 생겼어요! (10) | 2023.09.26 |
---|---|
[우테코] 스탬프크러쉬의 인프라 개선: 현재 아키텍처와 개선 아키텍처 2가지 (0) | 2023.09.14 |
[우테코] 임시 회원 ↔ 가입회원 데이터 연동기(2): 테이블 구조 대공사, 데이터 연동 API 구현! (0) | 2023.09.11 |
[우테코] 임시 회원 ↔ 가입회원 데이터 연동기(1): 6가지 시도와 실패한 이유(JPA 상속 관계 매핑의 한계) (2) | 2023.09.07 |
[우테코] 스탬프크러쉬 서비스 API 설계 (복잡한 요청은 여러 개의 요청으로 나눠서, 조회 API 예외 상황에 대한 정의, API의 재사용성을 통해 10시간을 절약했다) (0) | 2023.08.01 |