-
Notifications
You must be signed in to change notification settings - Fork 1
[REFACTOR] Flyway 날짜 기반 마이그레이션 버전 관리 적용 #310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
kjhyeon0620
merged 2 commits into
develop-demo
from
refactor/#302-date-based-flyway-versioning
Aug 17, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # Flyway 마이그레이션 가이드 | ||
|
|
||
| NOOK 서버의 PostgreSQL 스키마는 Flyway 마이그레이션으로 관리합니다. 모든 마이그레이션은 `src/main/resources/db/migration`에 둡니다. | ||
|
|
||
| ## 버전 규칙 | ||
|
|
||
| 신규 마이그레이션은 UTC 기준 날짜와 시간을 사용합니다. | ||
|
|
||
| ```text | ||
| VyyyyMMdd_HHmmss__snake_case_description.sql | ||
| ``` | ||
|
|
||
| 예시는 다음과 같습니다. | ||
|
|
||
| ```text | ||
| V20260814_063000__add_library_status_index.sql | ||
| ``` | ||
|
|
||
| - `yyyyMMdd`는 UTC 날짜입니다. | ||
| - `HHmmss`는 UTC 시간입니다. | ||
| - 설명은 변경 목적이 드러나는 영문 소문자 `snake_case`로 작성합니다. | ||
| - 동일한 초에 여러 마이그레이션을 만들었다면 각각 다른 타임스탬프를 사용합니다. | ||
|
|
||
| 기존 `V1`부터 `V6`까지는 날짜 규칙 도입 전에 생성된 레거시 마이그레이션입니다. Flyway 적용 이력을 보존하기 위해 이름과 내용을 변경하지 않습니다. | ||
|
|
||
| ## 파일 생성 | ||
|
|
||
| 저장소 루트에서 다음 명령을 실행하고 `description`을 실제 변경 내용으로 바꿉니다. | ||
|
|
||
| ```bash | ||
| touch "src/main/resources/db/migration/V$(date -u +%Y%m%d_%H%M%S)__description.sql" | ||
| ``` | ||
|
|
||
| 예를 들어 서재 상태 인덱스를 추가한다면 다음과 같이 생성합니다. | ||
|
|
||
| ```bash | ||
| touch "src/main/resources/db/migration/V$(date -u +%Y%m%d_%H%M%S)__add_library_status_index.sql" | ||
| ``` | ||
|
|
||
| ## 작성 원칙 | ||
|
|
||
| 1. 하나의 마이그레이션에는 하나의 명확한 목적만 담습니다. | ||
| 2. 공유 브랜치에 병합된 마이그레이션은 수정, 삭제하거나 이름을 바꾸지 않습니다. | ||
| 3. 이미 적용된 변경을 보완해야 한다면 더 높은 버전의 새 마이그레이션으로 roll-forward 합니다. | ||
| 4. 애플리케이션 코드와 스키마 변경의 배포 순서를 고려해 이전 버전과의 호환성을 유지합니다. | ||
| 5. Flyway의 `out-of-order` 옵션은 활성화하지 않습니다. | ||
|
|
||
| ## PR 전 검증 | ||
|
|
||
| 먼저 작업 브랜치를 대상 브랜치의 최신 상태로 갱신합니다. 그다음 대상 브랜치를 인자로 전달해 마이그레이션을 검증합니다. | ||
|
|
||
| ```bash | ||
| ./scripts/validate-flyway-migrations.sh origin/develop-demo | ||
| ./gradlew clean build | ||
| ``` | ||
|
|
||
| 검증 스크립트는 다음 조건을 확인합니다. | ||
|
|
||
| - 신규 파일이 날짜·시간 버전 형식을 따르는지 | ||
| - 날짜와 시간이 실제로 유효한지 | ||
| - 버전이 중복되지 않았는지 | ||
| - 기존 마이그레이션이 변경 또는 삭제되지 않았는지 | ||
| - 신규 버전이 대상 브랜치의 최신 버전보다 큰지 | ||
|
|
||
| 오래된 작업 브랜치의 버전이 대상 브랜치의 최신 버전보다 낮다면, 아직 공유 환경에 적용되지 않았는지 확인한 뒤 현재 UTC 시각으로 파일명을 다시 생성합니다. 공유 환경에 적용된 파일은 이름을 바꾸지 않고 새 마이그레이션으로 보완합니다. | ||
|
|
||
| GitHub Actions에서도 Pull Request 대상 커밋을 기준으로 같은 검증을 실행합니다. | ||
|
|
||
| ## 실패 대응 | ||
|
|
||
| - 파일명 오류: `VyyyyMMdd_HHmmss__snake_case_description.sql` 형식으로 수정합니다. | ||
| - 중복 또는 낮은 버전: 대상 브랜치를 최신화한 뒤 현재 UTC 시각으로 버전을 다시 생성합니다. | ||
| - 기존 파일 변경: 변경을 되돌리고 새 마이그레이션으로 작성합니다. | ||
| - 적용 실패: 실패 원인을 수정한 새 마이그레이션으로 roll-forward 합니다. 공유 DB의 `flyway_schema_history`를 임의로 수정하지 않습니다. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||
| migration_relative_path="src/main/resources/db/migration" | ||
| migration_directory="${repository_root}/${migration_relative_path}" | ||
| base_ref="${1:-}" | ||
|
|
||
| legacy_migrations=( | ||
| "V1__init_schema.sql" | ||
| "V2__add_unique_index_for_aladin_book_isbn.sql" | ||
| "V3__cleanup_legacy_hibernate_schema.sql" | ||
| "V4__create_book_view_history.sql" | ||
| "V5__add_on_delete_cascade.sql" | ||
| "V6__add_users_status_deleted_at_index.sql" | ||
| ) | ||
|
|
||
| fail() { | ||
| echo "Flyway migration validation failed: $1" >&2 | ||
| exit 1 | ||
| } | ||
|
|
||
| is_legacy_migration() { | ||
| local filename="$1" | ||
| local legacy_migration | ||
|
|
||
| for legacy_migration in "${legacy_migrations[@]}"; do | ||
| if [[ "$filename" == "$legacy_migration" ]]; then | ||
| return 0 | ||
| fi | ||
| done | ||
|
|
||
| return 1 | ||
| } | ||
|
|
||
| is_valid_timestamp() { | ||
| local date_part="$1" | ||
| local time_part="$2" | ||
| local year=$((10#${date_part:0:4})) | ||
| local month=$((10#${date_part:4:2})) | ||
| local day=$((10#${date_part:6:2})) | ||
| local hour=$((10#${time_part:0:2})) | ||
| local minute=$((10#${time_part:2:2})) | ||
| local second=$((10#${time_part:4:2})) | ||
| local max_day | ||
|
|
||
| if (( year < 1970 || month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59 )); then | ||
| return 1 | ||
| fi | ||
|
|
||
| case "$month" in | ||
| 1|3|5|7|8|10|12) max_day=31 ;; | ||
| 4|6|9|11) max_day=30 ;; | ||
| 2) | ||
| max_day=28 | ||
| if (( year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) )); then | ||
| max_day=29 | ||
| fi | ||
| ;; | ||
| esac | ||
|
|
||
| (( day >= 1 && day <= max_day )) | ||
| } | ||
|
|
||
| timestamp_version() { | ||
| local filename="$1" | ||
|
|
||
| if [[ "$filename" =~ ^V([0-9]{8})_([0-9]{6})__([a-z0-9]+(_[a-z0-9]+)*)\.sql$ ]]; then | ||
| printf '%s%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" | ||
| return 0 | ||
| fi | ||
|
|
||
| return 1 | ||
| } | ||
|
|
||
| validate_filename() { | ||
| local filename="$1" | ||
| local date_part | ||
| local time_part | ||
|
|
||
| if is_legacy_migration "$filename"; then | ||
| return 0 | ||
| fi | ||
|
|
||
| if [[ ! "$filename" =~ ^V([0-9]{8})_([0-9]{6})__([a-z0-9]+(_[a-z0-9]+)*)\.sql$ ]]; then | ||
| fail "${filename} must match VyyyyMMdd_HHmmss__snake_case_description.sql" | ||
| fi | ||
|
|
||
| date_part="${BASH_REMATCH[1]}" | ||
| time_part="${BASH_REMATCH[2]}" | ||
|
|
||
| if ! is_valid_timestamp "$date_part" "$time_part"; then | ||
| fail "${filename} contains an invalid UTC date or time" | ||
| fi | ||
| } | ||
|
|
||
| validate_repository_files() { | ||
| local migration_file | ||
| local legacy_migration | ||
| local filename | ||
| local version | ||
| local seen_versions=" " | ||
| local migration_count=0 | ||
|
|
||
| [[ -d "$migration_directory" ]] || fail "missing migration directory: ${migration_relative_path}" | ||
|
|
||
| for legacy_migration in "${legacy_migrations[@]}"; do | ||
| [[ -f "${migration_directory}/${legacy_migration}" ]] \ | ||
| || fail "missing legacy migration: ${legacy_migration}" | ||
| done | ||
|
|
||
| while IFS= read -r -d '' migration_file; do | ||
| filename="$(basename "$migration_file")" | ||
| validate_filename "$filename" | ||
|
|
||
| if is_legacy_migration "$filename"; then | ||
| version="${filename%%__*}" | ||
| version="${version#V}" | ||
| else | ||
| version="$(timestamp_version "$filename")" | ||
| fi | ||
|
|
||
| if [[ "$seen_versions" == *" ${version} "* ]]; then | ||
| fail "duplicate migration version: ${version}" | ||
| fi | ||
|
|
||
| seen_versions+="${version} " | ||
| ((migration_count += 1)) | ||
| done < <(find "$migration_directory" -type f -print0) | ||
|
|
||
| (( migration_count > 0 )) || fail "no migration files found" | ||
| printf '%s\n' "$migration_count" | ||
| } | ||
|
|
||
| base_max_version() { | ||
| local ref="$1" | ||
| local path | ||
| local filename | ||
| local version | ||
| local max_version=0 | ||
|
|
||
| while IFS= read -r path; do | ||
| [[ -n "$path" ]] || continue | ||
| filename="$(basename "$path")" | ||
|
|
||
| if is_legacy_migration "$filename"; then | ||
| version="${filename%%__*}" | ||
| version="${version#V}" | ||
| elif version="$(timestamp_version "$filename")"; then | ||
| : | ||
| else | ||
| fail "base ref ${ref} contains an unsupported migration filename: ${filename}" | ||
| fi | ||
|
|
||
| if (( 10#$version > 10#$max_version )); then | ||
| max_version="$version" | ||
| fi | ||
| done < <(git -C "$repository_root" ls-tree -r --name-only "$ref" -- "$migration_relative_path") | ||
|
|
||
| printf '%s\n' "$max_version" | ||
| } | ||
|
|
||
| validate_changes_from_base() { | ||
| local ref="$1" | ||
| local path | ||
| local repository_path | ||
| local filename | ||
| local version | ||
| local max_version | ||
| local added_count=0 | ||
|
|
||
| git -C "$repository_root" rev-parse --verify --quiet "${ref}^{commit}" >/dev/null \ | ||
| || fail "base ref does not resolve to a commit: ${ref}" | ||
|
|
||
| max_version="$(base_max_version "$ref")" | ||
|
|
||
| while IFS= read -r path; do | ||
| [[ -n "$path" ]] || continue | ||
| filename="$(basename "$path")" | ||
| [[ -f "${repository_root}/${path}" ]] \ | ||
| || fail "existing migration files are immutable (deleted: ${path})" | ||
|
|
||
| if ! git -C "$repository_root" show "${ref}:${path}" | cmp -s - "${repository_root}/${path}"; then | ||
| fail "existing migration files are immutable (modified: ${path})" | ||
| fi | ||
| done < <(git -C "$repository_root" ls-tree -r --name-only "$ref" -- "$migration_relative_path") | ||
|
|
||
| while IFS= read -r -d '' path; do | ||
| repository_path="${path#"$repository_root"/}" | ||
| filename="$(basename "$path")" | ||
|
|
||
| if git -C "$repository_root" cat-file -e "${ref}:${repository_path}" 2>/dev/null; then | ||
| continue | ||
| fi | ||
|
|
||
| if is_legacy_migration "$filename"; then | ||
| fail "legacy migration cannot be added again: ${filename}" | ||
| fi | ||
|
|
||
| version="$(timestamp_version "$filename")" | ||
| if (( 10#$version <= 10#$max_version )); then | ||
| fail "${filename} must have a version greater than the base maximum ${max_version}" | ||
| fi | ||
|
|
||
| ((added_count += 1)) | ||
| done < <(find "$migration_directory" -type f -print0) | ||
|
|
||
| printf '%s\n' "$added_count" | ||
| } | ||
|
|
||
| migration_count="$(validate_repository_files)" | ||
|
|
||
| if [[ -n "$base_ref" ]]; then | ||
| added_count="$(validate_changes_from_base "$base_ref")" | ||
| echo "Validated ${migration_count} Flyway migrations and ${added_count} migration changes against ${base_ref}." | ||
| else | ||
| echo "Validated ${migration_count} Flyway migrations. Pass a base ref to validate changed migrations." | ||
| fi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.