이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Schema Migration은 왜 애플리케이션 배포와 함께 설계해야 하는가?
  • 운영 DB 변경에서 위험한 SQL은 어떤 신호를 만드는가?
  • 실패한 migration을 되돌릴 때 무엇을 먼저 판단해야 하는가?

개요

Schema Migration은 운영 DB 구조를 애플리케이션 변경에 맞게 바꾸는 작업이다.

테이블, 컬럼, 인덱스, 제약 조건, 데이터 보정이 포함될 수 있다.

개발 환경에서는 migration이 한 번에 성공해도 운영에서는 데이터 양, 락, 장기 트랜잭션, 이전 애플리케이션 버전 때문에 실패할 수 있다.

따라서 migration은 SQL 파일이 아니라 배포 절차의 일부로 봐야 한다.

Spring 백엔드 개발자는 Entity 변경, Repository 쿼리, 트랜잭션 경계, 배치 작업이 스키마 변경과 어떻게 맞물리는지 설명해야 한다.

호환성 원칙

운영 migration의 핵심은 새 버전과 이전 버전이 잠시 함께 존재해도 깨지지 않게 만드는 것이다.

  • 새 컬럼은 먼저 nullable로 추가한다.
  • 애플리케이션이 새 컬럼을 쓰기 시작한 뒤 backfill을 수행한다.
  • 읽기 경로가 새 컬럼을 신뢰할 수 있을 때 제약 조건을 강화한다.
  • 더 이상 쓰지 않는 컬럼은 충분한 관찰 기간 이후 제거한다.
  • destructive change는 배포와 같은 순간에 실행하지 않는다.

이 흐름을 흔히 expand, deploy, backfill, contract로 나눈다.

이 방식은 느리지만 장애 중 되돌릴 수 있는 선택지를 남긴다.

Flyway 기준 예시

Spring Boot에서 Flyway를 쓴다면 migration 파일은 배포 artifact와 함께 움직인다.

-- V20260701_01__add_order_cancel_reason.sql
alter table orders add column cancel_reason varchar(255);
 
create index concurrently if not exists idx_orders_cancelled_at
  on orders (cancelled_at)
  where status = 'CANCELLED';

운영에서는 모든 SQL이 같은 위험도를 갖지 않는다.

인덱스 생성 방식, 기본값 채우기, not null 추가, 컬럼 삭제는 데이터 양에 따라 락과 지연을 만든다.

Flyway 실행 전에는 현재 상태를 확인한다.

./gradlew flywayInfo -Penv=prod
./gradlew flywayValidate -Penv=prod

명령 실행 권한, 대상 DB, 애플리케이션 버전은 반드시 기록한다.

위험한 변경

다음 변경은 작은 개발 DB에서는 괜찮아 보여도 운영에서는 장애로 이어질 수 있다.

  • 큰 테이블에 기본값이 있는 컬럼을 즉시 추가한다.
  • 장시간 걸리는 backfill을 하나의 트랜잭션으로 실행한다.
  • 사용 중인 컬럼을 애플리케이션 배포와 동시에 삭제한다.
  • 인덱스를 일반 방식으로 생성해 쓰기 요청을 막는다.
  • 외래 키나 unique 제약을 데이터 정리 없이 추가한다.
  • enum 값을 제거하거나 의미를 바꾼다.
  • JSON 컬럼 내부 구조를 애플리케이션 호환성 없이 변경한다.

위험한 SQL은 실행 전 예상 row 수, 락 범위, 재시도 가능성, 중단 방법을 확인해야 한다.

Backfill 설계

데이터 보정은 migration 중 가장 자주 과소평가된다.

Backfill은 애플리케이션 요청과 같은 DB 자원을 사용하므로 속도 제한이 필요하다.

update orders
set cancel_reason = 'UNKNOWN'
where status = 'CANCELLED'
  and cancel_reason is null
  and id between :start_id and :end_id;

운영에서는 한 번에 전체 테이블을 갱신하지 않는다.

  • id 범위나 생성 시각 기준으로 작게 나눈다.
  • 각 batch 후 처리 건수와 소요 시간을 기록한다.
  • 오류가 나도 재시작 가능한 조건을 둔다.
  • 애플리케이션의 쓰기 경로와 충돌하지 않는 시간대를 고른다.
  • 완료 후 검증 SQL로 남은 대상 건수를 확인한다.

Backfill은 migration 파일보다 별도 작업으로 관리하는 편이 안전한 경우가 많다.

장애 신호

  • 배포 직후 DB connection 사용량이 급증한다.
  • 특정 API가 timeout을 반환한다.
  • migration이 락을 기다리며 멈춘다.
  • Flyway metadata에는 실패 기록이 남고 애플리케이션이 시작하지 못한다.
  • 이전 버전 애플리케이션이 새 스키마를 읽지 못한다.
  • backfill 처리량이 예상보다 크게 느리다.
  • 데이터 정합성 검증 SQL에서 누락 건수가 계속 남는다.

이 신호는 migration이 단순 SQL 문제가 아니라 서비스 흐름을 막고 있다는 뜻이다.

안전한 완화 조치

  • migration 실패 후 먼저 적용된 SQL과 적용되지 않은 SQL을 분리한다.
  • 이미 데이터가 변경되었다면 rollback보다 forward fix가 안전한지 판단한다.
  • 애플리케이션 시작을 막는 metadata 실패는 수동 수정 전 DBA나 인프라 담당자와 확인한다.
  • 장기 락이 있으면 중단 가능한 세션인지 확인하고 영향 범위를 공유한다.
  • backfill은 즉시 중단할 수 있게 batch 단위로 실행한다.
  • 새 컬럼 읽기를 feature flag로 늦출 수 있으면 우선 끈다.

운영 DB의 schema 변경은 파일을 되돌리는 것처럼 간단히 원복되지 않는다.

하지 말아야 할 대응

  • 실패한 migration 파일을 수정해 같은 버전명으로 다시 배포하지 않는다.
  • 운영 DB에서 어떤 SQL이 적용됐는지 확인하지 않고 새 SQL을 추가하지 않는다.
  • 큰 테이블 전체 update를 트래픽 피크 시간에 실행하지 않는다.
  • 삭제된 컬럼을 애플리케이션이 더 이상 쓰지 않는다고 추측하지 않는다.
  • Flyway metadata를 근거 없이 직접 고치지 않는다.

인프라 담당자와 공유할 자료

  • migration 버전, 파일명, 실행 시각
  • 대상 테이블, 예상 row 수, 예상 락 범위
  • 배포된 애플리케이션 버전과 migration 의존 관계
  • 실패한 SQL과 에러 메시지
  • 현재 적용된 스키마 상태와 metadata 상태
  • 중단해도 되는 backfill 작업 범위
  • 복구 후보: 재시도, forward fix, 기능 비활성화, 읽기 전용 전환

공유 자료가 구체적일수록 운영 중 임의 SQL 실행을 줄일 수 있다.

확인 질문

확인 질문

  • Schema Migration을 애플리케이션 배포와 함께 설계해야 하는 이유는 무엇인가?
    • 이전 버전과 새 버전이 같은 DB를 쓰는 시간 동안 호환성이 깨질 수 있기 때문이다.
  • 큰 테이블에 대한 migration에서 먼저 확인할 것은 무엇인가?
    • row 수, 락 범위, 실행 시간, 중단 방법, 검증 SQL이다.
  • 실패한 migration을 되돌릴 때 가장 먼저 판단할 것은 무엇인가?
    • 이미 데이터가 변경되었는지와 forward fix가 rollback보다 안전한지 여부다.

리뷰 체크포인트

  • expand와 contract 단계가 분리되어 있는가?
  • destructive change가 충분한 관찰 기간 이후 실행되는가?
  • backfill이 재시작 가능한 작은 단위로 설계되어 있는가?
  • Flyway 실패 시 metadata 처리 방침이 문서화되어 있는가?
  • 애플리케이션 버전별 스키마 호환성이 설명되어 있는가?