가역적 마이그레이션으로 복원력 있는 데이터베이스 구축
Lukas Schneider
DevOps Engineer · Leapcell

소개
빠르게 변화하는 소프트웨어 개발 세계에서 데이터베이스 스키마 변경은 불가피하고 빈번하게 발생합니다. 새로운 기능을 추가하거나 성능을 최적화하는 등 데이터베이스는 끊임없이 진화합니다. 그러나 이러한 변경 사항을 배포하는 프로세스, 즉 데이터베이스 마이그레이션은 위험으로 가득 차 있습니다. 부실하게 설계되었거나 제대로 실행되지 않은 마이그레이션 스크립트는 데이터 손실, 애플리케이션 중단 및 기타 프로덕션에 치명적인 사고를 초래할 수 있습니다. 이는 종종 선견지명의 부족에서 비롯됩니다. 마이그레이션이 프로덕션에서만 나타나는 버그를 도입하면 어떻게 될까요? 배포 직후 성능 퇴행이 관찰되면 어떻게 될까요? 이러한 중요한 순간에 데이터베이스 변경을 빠르고 안전하게 되돌리는 능력은 단순한 편의가 아니라 생명줄입니다. 이 문서는 '가역적 데이터베이스 마이그레이션'이라는 매우 중요한 개념을 탐구합니다. 즉, 안정적인 상태로 항상 롤백할 수 있도록 설계하고 구현하는 방법을 다루며, 이를 통해 프로덕션 사고의 위험을 크게 완화합니다.
안전한 스키마 진화의 기반
가역적 마이그레이션의 메커니즘을 자세히 알아보기 전에, 우리의 논의를 뒷받침할 핵심 용어에 대한 공통된 이해를 정립해 봅시다.
- 데이터베이스 마이그레이션: 데이터베이스의 구조(스키마) 또는 데이터를 체계적으로 변경하는 스크립트 또는 스크립트 모음입니다. 여기에는 테이블 생성, 열 추가, 데이터 유형 수정 또는 데이터 삽입/업데이트/삭제가 포함될 수 있습니다.
- 스키마 버전 관리: 시간이 지남에 따라 데이터베이스 스키마 변경을 추적하는 관행입니다. 각 마이그레이션은 일반적으로 스키마의 새 버전을 나타냅니다.
- 마이그레이션 도구: 데이터베이스 마이그레이션의 적용 및 관리를 자동화하는 소프트웨어(예: Flyway, Liquibase, Alembic, Django Migrations)입니다. 이러한 도구는 일반적으로 적용된 마이그레이션의 기록을 유지합니다.
- 정방향 마이그레이션(상향 마이그레이션): 데이터베이스에 변경 사항을 적용하여 이전 버전에서 새 버전으로 이동하는 스크립트입니다.
- 역방향 마이그레이션(하향 마이그레이션 / 롤백 마이그레이션): 해당하는 정방향 마이그레이션에서 도입된 변경 사항을 취소하여 본질적으로 데이터베이스를 이전 상태로 되돌리는 스크립트입니다. 이것이 가역성의 초석입니다.
- 멱등성: 어떤 연산을 여러 번 적용해도 한 번 적용한 것과 동일한 결과를 얻으면 해당 연산을 멱등하다고 합니다. 모든 경우에 가역성을 위해 엄격하게 요구되지는 않지만, 멱등한 마이그레이션은 일반적으로 더 안전하고 관리하기 쉽습니다.
가역적 마이그레이션의 핵심 원칙은 데이터베이스에 대해 수행하는 모든 변경에 대해 해당 변경을 취소하는 방법을 명시적으로 정의해야 한다는 것입니다. 이 '취소' 작업이 역방향 마이그레이션입니다. 롤백 기능은 중요한 안전망을 제공합니다. 정방향 마이그레이션으로 인해 문제가 발생하면 해당 역방향 마이그레이션을 신속하게 실행하여 데이터베이스를 작동 가능한 상태로 복원하고 중단 시간 및 데이터 손실을 최소화할 수 있습니다.
가역성을 위한 설계
가역적 마이그레이션을 구현하려면 마이그레이션 스크립트를 설계하고 작성하는 방식에 대한 규율 잡힌 접근 방식이 필요합니다. 주요 전략은 모든 '상향' 마이그레이션에 해당하는 '하향' 마이그레이션을 쌍으로 만드는 것입니다.
1. 쌍으로 된 상향 및 하향 스크립트:
대부분의 최신 마이그레이션 도구는 이 개념을 직접 지원합니다. 일반적으로 각 마이그레이션에 대해 두 개의 파일 또는 단일 파일 내의 섹션이 있습니다. 하나는 정방향 변경용이고 다른 하나는 역방향 변경용입니다.
예제: 새 열 추가
users 테이블에 새 email 열을 추가하고 싶다고 가정해 봅시다.
-- V1__add_email_to_users_up.sql ALTER TABLE users ADD COLUMN email VARCHAR(255);
이에 해당하는 하향 스크립트는 단순히 해당 열을 제거하면 됩니다.
-- V1__add_email_to_users_down.sql ALTER TABLE users DROP COLUMN email;
2. 데이터 변경 처리:
데이터 손실은 영구적일 수 있으므로 데이터 수정은 가역성의 가장 까다로운 부분인 경우가 많습니다.
-
비파괴
ALTER TABLE연산: Nullable 열을 추가하거나VARCHAR의 길이를 늘리는 것은 일반적으로 데이터 손실 없이 가역적입니다(하향 마이그레이션은 스키마 변경 사항을 되돌리기만 합니다). -
파괴적
ALTER TABLE연산: 열을 삭제하거나, 데이터 유형을 덜 허용적인 유형(예:VARCHAR에서INT)으로 변경하거나, 기본값이 없는NOT NULL열을 추가하는 것은 하향 마이그레이션에서 데이터 손실의 위험이 있습니다. 이러한 작업은 극도로 신중하게 접근해야 합니다.- 데이터가 있는 파괴적 변경에 대한 전략: 열 삭제가 실제로 필요하고 데이터를 다시 사용할 가능성이 있다면, 열을 삭제하기 전에 데이터를 보관하는 것을 고려하세요. 그러면 하향 마이그레이션은 해당 열과 보관된 데이터를 복원합니다. 이는 복잡성을 더하지만 규정 준수 또는 중요한 데이터에 매우 중요할 수 있습니다.
- Nullable 열 추가에 대한 전략:
- 상향 마이그레이션 단계 1: 새 Nullable 열을 추가합니다.
- 상향 마이그레이션 단계 2: 새 열에 데이터를 채웁니다(예: 기존 열의 데이터 또는 기본값 사용).
- 상향 마이그레이션 단계 3:(선택 사항) 열을 Nullable이 아닌 열로 변경합니다.
하향 마이그레이션은 단순히 열을 삭제합니다. 이 다단계 접근 방식은
NOT NULL제약 조건을 강제하기 전에 데이터를 채울 기회를 제공합니다.
예제: 열 이름 변경
SQL에서 열 이름을 직접 변경하는 것은 일반적으로 가역적입니다.
-- V2__rename_username_to_name_up.sql ALTER TABLE users RENAME COLUMN username TO name;
-- V2__rename_username_to_name_down.sql ALTER TABLE users RENAME COLUMN name TO username;
예제: 테이블 삭제(데이터 복구 고려 사항 포함)
테이블을 삭제하는 것은 매우 파괴적인 작업입니다. 프로덕션 롤백에는 테이블과 해당 데이터를 복원해야 합니다.
-- V3__drop_temp_table_up.sql -- 삭제하기 전에 데이터를 다시 볼 필요가 있을 수 있으므로 데이터를 백업하는 것을 고려하십시오. -- 예: CREATE TABLE temp_table_backup AS SELECT * FROM temp_table; DROP TABLE temp_table;
-- V3__drop_temp_table_down.sql -- 이 하향 마이그레이션은 이전 스키마 및 데이터 백업 없이는 불가능합니다. -- 이는 특정 작업의 어려움과 잠재적 비가역성을 강조합니다. -- 스키마가 간단했다면 테이블을 다시 만들 수 있습니다: -- CREATE TABLE temp_table (...) -- 데이터가 백업되었다면: INSERT INTO temp_table SELECT * FROM temp_table_backup; -- DROP TABLE temp_table_backup;
이 예제는 엄격한 가역성이 어디에서 어려워지거나 외부 데이터 관리가 필요한지 명확하게 보여줍니다. 진정으로 파괴적인 작업의 경우 광범위한 테스트 및 때로는 수동 개입 또는 백업에서 데이터 복원이 유일한 '되돌리기' 옵션일 수 있습니다.
3. 도구 지원
마이그레이션 도구를 활용하면 프로세스가 크게 단순화됩니다.
- Flyway: 각 마이그레이션은 일반적으로
.sql파일입니다. 가역성을 위해 해당 '하향' 스크립트를 수동으로 작성합니다. Flyway는 주로 새로운 마이그레이션 적용에 중점을 둡니다. 롤백은 종종 외부 프로세스를 사용하거나 마이그레이션이 중간에 실패한 경우 '수리' 메커니즘을 사용하여 수행됩니다. - Liquibase: XML, YAML, JSON 또는 SQL 형식을 사용합니다.
<rollback>태그 또는 대부분의 변경 유형에 대한 속성을 지원하여 동일한 마이그레이션 스크립트 내에서 직접 복구 논리를 정의할 수 있습니다. 이것은 가역성을 위해 명시적으로 설계되었습니다.<!-- example.xml --> <changeSet id="1" author="dev"> <addColumn tableName="users"> <column name="email" type="VARCHAR(255)"/> </addColumn> <rollback> <dropColumn tableName="users" columnName="email"/> </rollback> </changeSet> - Alembic (Python의 SQLAlchemy용):
upgrade()및downgrade()함수를 생성합니다.# env.py (또는 마이그레이션 파일) from alembic import op import sqlalchemy as sa def upgrade(): op.add_column('users', sa.Column('email', sa.String(255), nullable=True)) def downgrade(): op.drop_column('users', 'email')
이러한 도구는 본질적으로 정방향 및 역방향 작업의 쌍을 이룰 것을 장려하거나 강제하여 가역적 마이그레이션을 표준 관행으로 만듭니다.
적용 시나리오
가역적 마이그레이션의 이점은 특히 중요도가 높은 환경에서 가장 두드러집니다.
- 지속적 통합/지속적 배포(CI/CD): CI/CD 파이프라인에서 마이그레이션은 종종 자동화됩니다. 자동화된 테스트 또는 모니터링이 문제를 감지하면 배포(데이터베이스 변경 포함)를 자동으로 롤백하는 능력은 안정성을 손상시키지 않으면서 빠른 배포 주기를 유지하는 데 중요합니다.
- 기능 플래그 및 A/B 테스팅: 기능 플래그 뒤에 기능을 배포할 때 데이터베이스 스키마는 이전 및 새 논리 모두를 지원해야 할 수 있습니다. 기능을 비활성화하거나 제거하면 관련 스키마 변경 사항을 되돌리거나 정리해야 할 수 있습니다.
- 긴급 수정 및 긴급 패치: 긴급 수정으로 인해 스키마 변경이 필요하지만 예상치 못한 부작용을 초래하는 경우 신속한 롤백 메커니즘을 통해 더 깊은 비즈니스 영향을 방지할 수 있습니다.
- 리팩토링 및 대규모 스키마 변경: 여러 테이블을 포함하거나 상당한 데이터 변환을 포함하는 복잡한 리팩토링의 경우, 가역적 접근 방식을 사용하면 반복적인 개발이 가능하고 전체 프로세스에서 안전망을 제공합니다.
결론
가역적 데이터베이스 마이그레이션은 단순한 좋은 관행이 아니라, 특히 안정성이 가장 중요한 프로덕션 환경에서 견고하고 복원력 있는 소프트웨어 개발의 필수 구성 요소입니다. 각 정방향 마이그레이션에 해당 역방향 롤백 스크립트를 세심하게 쌍으로 만들고 전용 마이그레이션 도구를 활용함으로써 팀은 프로덕션 사고의 위험을 크게 줄이고 예상치 못한 문제로부터 신속하게 복구할 수 있는 능력을 보장할 수 있습니다. 가역성을 채택하면 데이터베이스 변경을 고위험 도박에서 애플리케이션의 가장 중요한 자산인 데이터의 제어되고 자신감 있는 진화로 바꿀 수 있습니다.