데이터베이스 마이그레이션: Caterpillar를 나비로 전환

게시 됨: 2022-03-11

사용자는 자신이 사용하는 소프트웨어 내부에 무엇이 있는지 신경 쓰지 않습니다. 원활하고 안전하며 눈에 거슬리지 않게 작동합니다. 개발자는 이를 실현하기 위해 노력하고 있으며 해결하려는 문제 중 하나는 데이터 저장소가 제품의 현재 버전에 적합한 상태인지 확인하는 방법입니다. 소프트웨어는 진화하고 데이터 모델은 설계 실수를 수정하기 위해 시간이 지남에 따라 변경될 수도 있습니다. 문제를 더욱 복잡하게 만들기 위해 다양한 속도로 제품의 최신 버전으로 마이그레이션하는 여러 테스트 환경이나 고객이 있을 수 있습니다. 단일 관점에서 반짝이는 새 버전을 사용하려면 상점 구조와 어떤 조작이 필요한지 문서화할 수 없습니다.

데이터베이스 마이그레이션: Caterpillar를 나비로 전환

나는 한때 개발자가 직접 요청에 따라 업데이트한 구조를 가진 몇 개의 데이터베이스가 있는 프로젝트에 참여했습니다. 이는 구조를 최신 버전으로 마이그레이션하기 위해 어떤 변경 사항을 적용해야 하는지 알아낼 수 있는 분명한 방법이 없었고 버전 관리의 개념이 전혀 없었음을 의미합니다! 이것은 DevOps 이전 시대였으며 오늘날에는 총체적 난국으로 간주될 것입니다. 우리는 주어진 데이터베이스에 모든 변경 사항을 적용하는 데 사용할 도구를 개발하기로 결정했습니다. 마이그레이션이 있었고 스키마 변경 사항을 문서화했습니다. 이를 통해 우발적인 변경이 없고 ​​스키마 상태를 예측할 수 있다는 확신을 갖게 되었습니다.

이 기사에서는 관계형 데이터베이스 스키마 마이그레이션을 적용하는 방법과 수반되는 문제를 극복하는 방법을 살펴보겠습니다.

먼저 데이터베이스 마이그레이션이란 무엇입니까? 이 문서의 컨텍스트에서 마이그레이션 은 데이터베이스에 적용해야 하는 일련의 변경 사항입니다. 테이블, 열 또는 인덱스 생성 또는 삭제는 마이그레이션의 일반적인 예입니다. 스키마의 형태는 시간이 지나면서 극적으로 변할 수 있습니다. 특히 요구 사항이 아직 모호할 때 개발이 시작된 경우에는 더욱 그렇습니다. 따라서 릴리스로 가는 길에 여러 이정표가 있는 과정에서 데이터 모델이 발전하고 처음과 완전히 다를 수 있습니다. 마이그레이션은 대상 상태로 가는 단계일 뿐입니다.

시작하려면 이미 잘 수행된 것을 재발명하지 않도록 도구 상자에 있는 내용을 살펴보겠습니다.

도구

널리 사용되는 모든 언어에는 데이터베이스 마이그레이션을 쉽게 만드는 라이브러리가 있습니다. 예를 들어 Java의 경우 인기 있는 옵션은 Liquibase와 Flyway입니다. 예제에서 Liquibase를 더 많이 사용하지만 개념은 다른 솔루션에 적용되며 Liquibase와 관련이 없습니다.

일부 ORM이 이미 자동으로 스키마를 업그레이드하고 매핑된 클래스의 구조와 일치하도록 만드는 옵션을 이미 제공하는 경우 별도의 스키마 마이그레이션 라이브러리를 사용하는 것을 귀찮게 할 이유가 무엇입니까? 실제로 이러한 자동 마이그레이션은 테이블 및 열 생성과 같은 간단한 스키마 변경만 수행하며 데이터베이스 개체 삭제 또는 이름 변경과 같은 잠재적으로 파괴적인 작업은 수행할 수 없습니다. 따라서 자동이 아닌(여전히 자동화된) 솔루션은 마이그레이션 논리를 직접 설명해야 하고 데이터베이스에 정확히 어떤 일이 일어날지 알기 때문에 일반적으로 더 나은 선택입니다.

수동 변경 사항이 잘못된 순서로 적용되거나 필요한 경우에도 전혀 적용되지 않으면 고유하고 예측할 수 없는 스키마가 생성될 수 있으므로 자동화된 스키마 수정과 수동 스키마 수정을 혼합하는 것도 매우 나쁜 생각입니다. 도구가 선택되면 이 도구를 사용하여 모든 스키마 마이그레이션을 적용합니다.

일반적인 데이터베이스 마이그레이션

일반적인 마이그레이션에는 시퀀스, 테이블, 열, 기본 및 외래 키, 인덱스, 기타 데이터베이스 개체 생성이 포함됩니다. 가장 일반적인 유형의 변경에 대해 Liquibase는 수행해야 하는 작업을 설명하는 고유한 선언적 요소를 제공합니다. Liquibase 또는 기타 유사한 도구가 지원하는 모든 사소한 변경에 대해 읽는 것은 너무 지루할 것입니다. 변경 집합이 어떻게 보이는지에 대한 아이디어를 얻으려면 테이블을 생성하는 다음 예를 고려하십시오(간단함을 위해 XML 네임스페이스 선언은 생략됨).

 <?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog> <changeSet author="demo"> <createTable tableName="PRODUCT"> <column name="ID" type="BIGINT"> <constraints primaryKey="true" primaryKeyName="PK_PRODUCT"/> </column> <column name="CODE" type="VARCHAR(50)"> <constraints nullable="false" unique="true" uniqueConstraintName="UC_PRODUCT_CODE"/> </column> </createTable> </changeSet> </databaseChangeLog>

보시다시피 변경 로그는 변경 집합의 집합이고 변경 집합은 변경으로 구성됩니다. createTable 과 같은 간단한 변경 사항을 결합하여 보다 복잡한 마이그레이션을 구현할 수 있습니다. 예를 들어, 모든 제품에 대한 제품 코드를 업데이트해야 한다고 가정합니다. 다음 변경으로 쉽게 달성할 수 있습니다.

 <sql>UPDATE product SET code = 'new_' || code</sql>

수많은 제품이 있는 경우 성능이 저하됩니다. 마이그레이션 속도를 높이기 위해 다음 단계로 다시 작성할 수 있습니다.

  1. 앞에서 본 것처럼 createTable 을 사용하여 제품에 대한 새 테이블을 만듭니다. 이 단계에서는 가능한 한 적은 수의 제약 조건을 만드는 것이 좋습니다. 새 테이블의 이름을 PRODUCT_TMP 로 지정하겠습니다.
  2. sql 변경을 사용하여 INSERT INTO ... SELECT ... 형식의 SQL로 PRODUCT_TMP 를 채웁니다.
  3. 필요한 모든 제약 조건( addNotNullConstraint , addUniqueConstraint , addForeignKeyConstraint ) 및 인덱스( createIndex )를 생성합니다.
  4. PRODUCT 테이블의 이름을 PRODUCT_BAK 와 같은 이름으로 바꿉니다. Liquibase는 renameTable 을 사용하여 이를 수행할 수 있습니다.
  5. PRODUCT_TMP 의 이름을 PRODUCT 로 변경합니다(다시, renameTable 사용).
  6. 선택적으로 dropTable 을 사용하여 PRODUCT_BAK 를 제거하십시오.

물론 이러한 마이그레이션은 피하는 것이 좋지만 드물게 필요한 경우에 대비하여 마이그레이션을 구현하는 방법을 아는 것이 좋습니다.

XML, JSON 또는 YAML이 변경 사항을 설명하는 작업에 너무 이상하다고 생각한다면 일반 SQL을 사용하고 모든 데이터베이스 공급업체별 기능을 활용하십시오. 또한 일반 Java로 모든 사용자 정의 논리를 구현할 수 있습니다.

Liquibase가 실제 데이터베이스별 SQL 작성을 면제하는 방식은 과신으로 이어질 수 있지만 대상 데이터베이스의 단점을 잊어서는 안됩니다. 예를 들어, 외래 키를 생성할 때 사용 중인 특정 데이터베이스 관리 시스템에 따라 인덱스가 생성되거나 생성되지 않을 수 있습니다. 그 결과 곤란한 상황에 처할 수 있습니다. Liquibase를 사용하면 변경 집합이 PostgreSQL, Oracle 또는 MySQL과 같은 특정 유형의 데이터베이스에 대해서만 실행되도록 지정할 수 있습니다. 이를 통해 다른 데이터베이스에 대해 동일한 공급업체에 구애받지 않는 변경 집합을 사용하고 공급업체별 구문과 기능을 사용하여 다른 변경 집합을 사용할 수 있습니다. 다음 변경 집합은 Oracle 데이터베이스를 사용하는 경우에만 실행됩니다.

 <changeSet dbms="oracle" author="..."> ... </changeSet>

Oracle 외에도 Liquibase는 기본적으로 몇 가지 다른 데이터베이스를 지원합니다.

데이터베이스 개체 이름 지정

생성하는 모든 데이터베이스 개체는 이름을 지정해야 합니다. 제약 조건 및 인덱스와 같은 일부 유형의 개체에 대해 명시적으로 이름을 제공할 필요는 없습니다. 그러나 그 객체에 이름이 없는 것은 아닙니다. 그들의 이름은 어쨌든 데이터베이스에 의해 생성됩니다. 문제는 개체를 삭제하거나 변경하기 위해 해당 개체를 참조해야 할 때 발생합니다. 따라서 명시적인 이름을 지정하는 것이 좋습니다. 하지만 어떤 이름을 지어야 하는지에 대한 규칙이 있습니까? 대답은 간단합니다. 일관성을 유지하십시오. 예를 들어, 인덱스 이름을 IDX_<table>_<columns> 와 같이 지정하기로 결정했다면 앞서 언급한 CODE 열의 인덱스 이름은 IDX_PRODUCT_CODE 로 지정해야 합니다.

명명 규칙은 매우 논쟁의 여지가 있으므로 여기에서 포괄적인 지침을 제공한다고 가정하지 않습니다. 일관성을 유지하고, 팀 또는 프로젝트 규칙을 존중하거나, 없는 경우 그냥 만들어 보세요.

변경 집합 구성

가장 먼저 결정해야 할 것은 변경 집합을 저장할 위치입니다. 기본적으로 두 가지 접근 방식이 있습니다.

  1. 응용 프로그램 코드로 변경 집합을 유지합니다. 변경 집합과 응용 프로그램 코드를 함께 커밋하고 검토할 수 있으므로 이렇게 하는 것이 편리합니다.
  2. 예를 들어 별도의 VCS 저장소에 변경 집합과 응용 프로그램 코드를 별도로 보관 합니다. 이 접근 방식은 데이터 모델이 여러 애플리케이션에서 공유될 때 적합하며 모든 변경 세트를 전용 리포지토리에 저장하고 애플리케이션 코드가 있는 여러 리포지토리에 분산하지 않는 것이 더 편리합니다.

변경 집합을 저장하는 위치에 관계없이 일반적으로 다음 범주로 나누는 것이 합리적입니다.

  1. 실행 중인 시스템에 영향을 주지 않는 독립적인 마이그레이션. 현재 배포된 응용 프로그램이 아직 인식하지 못하는 경우 일반적으로 새 테이블, 시퀀스 등을 만드는 것이 안전합니다.
  2. 저장소의 구조를 변경하는 스키마 수정 (예: 열 및 인덱스 추가 또는 삭제). 이러한 변경 사항은 이전 버전의 응용 프로그램이 아직 사용 중인 동안 적용하면 안 됩니다. 그렇게 하면 스키마 변경으로 인해 잠금 또는 이상한 동작이 발생할 수 있기 때문입니다.
  3. 소량의 데이터를 삽입하거나 업데이트하는 빠른 마이그레이션. 여러 응용 프로그램이 배포되는 경우 이 범주의 변경 집합을 데이터베이스 성능 저하 없이 동시에 실행할 수 있습니다.
  4. 많은 데이터를 삽입하거나 업데이트하는 마이그레이션이 느려질 수 있습니다. 이러한 변경 사항은 다른 유사한 마이그레이션이 실행되고 있지 않을 때 적용하는 것이 좋습니다.

네 가지 범주의 그래픽 표현

이러한 마이그레이션 세트는 최신 버전의 애플리케이션을 배포하기 전에 연속적으로 실행해야 합니다. 이 접근 방식은 시스템이 여러 개의 개별 응용 프로그램으로 구성되어 있고 그 중 일부가 동일한 데이터베이스를 사용하는 경우 훨씬 더 실용적입니다. 그렇지 않으면 실행 중인 응용 프로그램에 영향을 주지 않고 적용할 수 있는 변경 집합만 분리하는 것이 좋습니다. 나머지 변경 집합은 함께 적용될 수 있습니다.

더 간단한 응용 프로그램의 경우 응용 프로그램 시작 시 필요한 마이그레이션의 전체 집합을 적용할 수 있습니다. 이 경우 모든 변경 집합은 단일 범주에 속하며 응용 프로그램이 초기화될 때마다 실행됩니다.

마이그레이션을 적용하기 위해 선택한 단계에 관계없이 여러 애플리케이션에 대해 동일한 데이터베이스를 사용하면 마이그레이션이 적용될 때 잠금이 발생할 수 있다는 점을 언급할 가치가 있습니다. Liquibase(다른 많은 유사한 솔루션과 마찬가지로)는 메타데이터를 기록하기 위해 두 개의 특수 테이블인 DATABASECHANGELOGDATABASECHANGELOGLOCK 을 사용합니다. 전자는 적용된 변경 집합에 대한 정보를 저장하는 데 사용되며 후자는 동일한 데이터베이스 스키마 내에서 동시 마이그레이션을 방지하는 데 사용됩니다. 따라서 여러 응용 프로그램이 어떤 이유로 동일한 데이터베이스 스키마를 사용해야 하는 경우 잠금을 방지하기 위해 메타데이터 테이블에 기본이 아닌 이름을 사용하는 것이 좋습니다.

높은 수준의 구조가 명확해지면 각 범주 내에서 변경 집합을 구성하는 방법을 결정해야 합니다.

샘플 변경 집합 조직

특정 응용 프로그램 요구 사항에 따라 크게 달라지지만 일반적으로 다음 사항이 합리적입니다.

  1. 제품 릴리스별로 그룹화된 변경 로그를 유지합니다. 각 릴리스에 대한 새 디렉토리를 만들고 해당 변경 로그 파일을 이 디렉토리에 넣습니다. 루트 변경 로그가 있고 릴리스에 해당하는 변경 로그를 포함합니다. 릴리스 변경 로그에 이 릴리스를 구성하는 다른 변경 로그를 포함합니다.
  2. 변경 로그 파일 및 변경 집합 식별자에 대한 명명 규칙이 있고 물론 따라야 합니다.
  3. 변경 사항이 많은 변경 집합을 피하십시오. 하나의 긴 변경 집합보다 여러 변경 집합을 선호합니다.
  4. 저장 프로시저를 사용하고 업데이트해야 하는 경우 해당 저장 프로시저가 추가된 변경 집합의 runOnChange="true" 특성을 사용하는 것이 좋습니다. 그렇지 않으면 업데이트될 때마다 저장 프로시저의 새 버전으로 새 변경 집합을 만들어야 합니다. 요구 사항은 다양하지만 이러한 기록을 추적하지 않는 것이 허용되는 경우가 많습니다.
  5. 기능 분기를 병합하기 전에 중복 변경 사항을 스쿼시하는 것을 고려하십시오. 때로는 기능 분기(특히 수명이 긴 분기)에서 나중에 변경 집합이 이전 변경 집합에서 변경된 내용을 수정하는 경우가 있습니다. 예를 들어, 테이블을 만든 다음 더 많은 열을 추가하기로 결정할 수 있습니다. 이 기능 분기가 아직 주 분기에 병합되지 않은 경우 초기 createTable 변경에 해당 열을 추가할 가치가 있습니다.
  6. 동일한 변경 로그를 사용하여 테스트 데이터베이스를 만듭니다. 그렇게 하려고 하면 모든 변경 집합이 테스트 환경에 적용되지 않거나 해당 특정 테스트 환경에 추가 변경 집합이 필요하다는 것을 곧 알게 될 것입니다. Liquibase를 사용하면 컨텍스트 를 사용하여 이 문제를 쉽게 해결할 수 있습니다. 테스트로만 실행해야 하는 변경 집합에 context="test" 속성을 추가한 다음 test 컨텍스트가 활성화된 Liquibase를 초기화합니다.

롤백

다른 유사한 솔루션과 마찬가지로 Liquibase는 "위" 및 "아래로" 스키마 마이그레이션을 지원합니다. 그러나 경고: 마이그레이션을 취소하는 것은 쉽지 않을 수 있으며 항상 노력할 가치가 있는 것은 아닙니다. 애플리케이션에 대한 마이그레이션 실행 취소를 지원하기로 결정했다면 일관성을 유지하고 실행 취소해야 하는 모든 변경 집합에 대해 수행하십시오. Liquibase를 사용하면 롤백을 수행하는 데 필요한 변경 사항이 포함된 rollback 태그를 추가하여 변경 집합을 실행 취소합니다. 다음 예를 고려하십시오.

 <changeSet author="..."> <createTable tableName="PRODUCT"> <column name="ID" type="BIGINT"> <constraints primaryKey="true" primaryKeyName="PK_PRODUCT"/> </column> <column name="CODE" type="VARCHAR(50)"> <constraints nullable="false" unique="true" uniqueConstraintName="UC_PRODUCT_CODE"/> </column> </createTable> <rollback> <dropTable tableName="PRODUCT"/> </rollback> </changeSet>

Liquibase가 동일한 롤백 작업을 수행하기 때문에 명시적 롤백은 여기에서 중복됩니다. Liquibase는 지원되는 대부분의 변경 유형(예: createTable , addColumn 또는 createIndex )을 자동으로 롤백할 수 있습니다.

과거를 고치다

아무도 완벽하지 않으며 우리는 모두 실수를 합니다. 일부는 손상된 변경 사항이 이미 적용된 경우 너무 늦게 발견될 수 있습니다. 그 날을 구하기 위해 무엇을 할 수 있는지 알아봅시다.

수동으로 데이터베이스 업데이트

다음과 같은 방법으로 DATABASECHANGELOG 및 데이터베이스를 엉망으로 만드는 작업이 포함됩니다.

  1. 잘못된 변경 집합을 수정하고 다시 실행하려면 다음을 수행합니다.
    • 변경 집합에 해당하는 DATABASECHANGELOG 에서 행을 제거합니다.
    • 변경 집합에 의해 도입된 모든 부작용을 제거합니다. 예를 들어, 테이블이 삭제된 경우 테이블을 복원합니다.
    • 잘못된 변경 집합을 수정합니다.
    • 마이그레이션을 다시 실행하십시오.
  2. 잘못된 변경 집합을 수정하지만 다시 적용을 건너뛰려면 다음을 수행합니다.
    • 잘못된 변경 집합에 해당하는 행에 대해 MD5SUM 필드 값을 NULL 로 설정하여 DATABASECHANGELOG 를 업데이트합니다.
    • 데이터베이스에서 잘못된 부분을 수동으로 수정합니다. 예를 들어 잘못된 유형의 열이 추가된 경우 쿼리를 실행하여 해당 유형을 수정합니다.
    • 잘못된 변경 집합을 수정합니다.
    • 마이그레이션을 다시 실행하십시오. Liquibase는 새 체크섬을 계산하여 MD5SUM 에 저장합니다. 수정된 변경 집합은 다시 실행되지 않습니다.

분명히 개발 중에 이러한 트릭을 수행하는 것은 쉽지만 변경 사항이 여러 데이터베이스에 적용되면 훨씬 더 어려워집니다.

수정 변경 세트 작성

실제로는 일반적으로 이 접근 방식이 더 적합합니다. 원래 변경 집합을 편집하지 않는 이유가 무엇인지 궁금할 수 있습니다. 진실은 그것은 변화해야 할 것에 달려 있다는 것입니다. Liquibase는 각 변경 집합에 대한 체크섬을 계산하고 이전에 적용된 변경 집합 중 하나 이상에 대해 체크섬이 새로운 경우 새 변경 적용을 거부합니다. 이 동작은 runOnChange="true" 속성을 지정하여 변경 집합별로 사용자 지정할 수 있습니다. 체크섬은 전제 조건 또는 선택적 변경 집합 속성( context , runOnChange 등)을 수정하는 경우 영향을 받지 않습니다.

이제 어떻게 하면 실수로 변경 세트를 수정하는지 궁금할 것입니다.

  1. 이러한 변경 사항을 새 스키마에 계속 적용하려면 수정 변경 집합을 추가하기만 하면 됩니다. 예를 들어 잘못된 유형으로 추가된 열이 있는 경우 새 변경 집합에서 해당 유형을 수정합니다.
  2. 잘못된 변경 집합이 존재하지 않은 것처럼 가장하려면 다음을 수행하십시오.
    • 이러한 컨텍스트를 사용하여 마이그레이션을 다시 적용하지 않도록 보장하는 값으로 변경 세트를 제거하거나 context 속성을 추가하십시오(예: context="graveyard-changesets-never-run" ).
    • 잘못된 작업을 되돌리거나 수정하는 새 변경 집합을 추가합니다. 잘못된 변경 사항이 적용된 경우에만 이러한 변경 사항을 적용해야 합니다. changeSetExecuted 와 같은 전제 조건으로 달성할 수 있습니다. 왜 그렇게 하는지 설명하는 주석을 추가하는 것을 잊지 마십시오.
    • 스키마를 올바른 방식으로 수정하는 새 변경 집합을 추가합니다.

보시다시피 과거를 고칠 수는 있지만 항상 간단하지는 않습니다.

성장통 완화

애플리케이션이 오래되면 변경 로그도 증가하여 경로를 따라 모든 스키마 변경이 누적됩니다. 이것은 의도된 것이며 본질적으로 잘못된 것은 없습니다. 예를 들어 제품의 각 버전을 출시한 후 마이그레이션을 정기적으로 스쿼시하면 긴 변경 로그를 더 짧게 만들 수 있습니다. 어떤 경우에는 새로운 스키마를 더 빠르게 초기화할 수 있습니다.

찌그러지는 변경 로그의 그림

스쿼싱은 항상 사소하지 않으며 많은 이점을 가져오지 않고 회귀를 일으킬 수 있습니다. 또 다른 훌륭한 옵션은 모든 변경 집합을 실행하지 않도록 시드 데이터베이스를 사용하는 것입니다. 일부 테스트 데이터가 있는 경우에도 가능한 한 빨리 데이터베이스를 준비해야 하는 경우 테스트 환경에 매우 적합합니다. 이것을 변경 집합에 대한 스쿼싱(squashing)의 한 형태로 생각할 수 있습니다. 특정 시점(예: 다른 버전을 릴리스한 후)에서 스키마를 덤프합니다. 덤프를 복원한 후 평소와 같이 마이그레이션을 적용합니다. 덤프를 만들기 전에 이전 변경 사항이 이미 적용되었기 때문에 새 변경 사항만 적용됩니다. 따라서 덤프에서 복원되었습니다.

종자 데이터베이스의 그림

결론

우리는 의도적으로 Liquibase의 기능에 대해 더 깊이 파고드는 것을 피하여 일반적으로 진화하는 스키마에 초점을 맞춘 짧고 요점적인 기사를 제공했습니다. 데이터베이스 스키마 마이그레이션의 자동화된 적용으로 인해 어떤 이점과 문제가 발생하는지, 그리고 DevOps 문화에 얼마나 잘 맞는지 명확하기를 바랍니다. 좋은 아이디어라도 교리로 바꾸지 않는 것이 중요합니다. 요구 사항은 다양하며 데이터베이스 엔지니어로서 우리의 결정은 인터넷상의 누군가의 권장 사항을 따르는 것이 아니라 제품을 발전시키는 데 도움이 되어야 합니다.