운영 중인 서비스에서 데이터 형식을 바꿔야 하는 상황은 자주 발생한다. 컬럼 암호화, 타입 변경, JSON 스키마 수정, 정규화/비정규화. 서비스를 멈추고 한 번에 전환하는 빅뱅 방식은 위험하다. 전환 중 문제가 생기면 서비스 전체가 멈춘다.

이중 쓰기(dual write)와 fallback 읽기를 조합하면 서비스 중단 없이 데이터 형식을 전환할 수 있다. 각 단계에서 롤백 가능한 상태를 유지하는 것이 핵심이다.

이중 쓰기 + fallback 읽기

이중 쓰기는 데이터를 저장할 때 기존 형식과 새 형식 양쪽에 기록한다. 전환 기간 동안 같은 데이터가 양쪽에 공존한다.

fallback 읽기는 데이터를 읽을 때 새 형식에 값이 있으면 그 값을 쓰고, 없으면 기존 형식에서 가져온다. 아직 새 형식으로 변환되지 않은 데이터도 정상적으로 읽힌다.

flowchart TD
    W["쓰기"] --> W1["기존 형식에 저장"]
    W --> W2["새 형식에 저장"]

    R["읽기"] --> C{"새 형식에
값이 있는가?"} C -->|"있다"| D["새 형식 사용"] C -->|"없다"| E["기존 형식 사용"]

이 두 가지를 조합하면 기존 데이터와 신규 데이터가 공존하는 전환 기간을 만들 수 있다.

3단계 프로세스

전환은 세 단계로 나뉜다. 각 단계는 이전 단계가 배포된 뒤에 진행한다.

flowchart LR
    S1["Step 1: 준비
새 형식 추가
이중 쓰기 시작
fallback 읽기 적용"] S2["Step 2: 마이그레이션
기존 데이터를
새 형식으로 변환
dry-run → 실행"] S3["Step 3: 정리
fallback 제거
기존 형식 삭제"] S1 -- "배포 완료" --> S2 -- "검증 완료" --> S3

Step 1: 준비

새 형식을 추가하고 코드를 변경한다.

  • 스키마 변경: 새 컬럼이나 필드를 추가한다. 초기에는 nullable로 생성한다. 기존 데이터는 아직 새 형식이 없으므로 null이어야 한다.
  • 이중 쓰기 적용: INSERT와 UPDATE에서 기존 형식과 새 형식 양쪽에 값을 기록한다.
  • fallback 읽기 적용: SELECT에서 새 형식에 값이 있으면 사용하고, 없으면 기존 형식의 값을 반환한다.

이 단계가 배포되면 신규 데이터는 양쪽에 동시 저장된다. 기존 데이터는 여전히 기존 형식에만 존재하고, fallback 읽기가 이를 처리한다.

롤백: 새 형식을 무시하면 기존 동작 그대로다. 코드 변경만 되돌리면 된다.

Step 2: 마이그레이션

기존 데이터를 새 형식으로 일괄 변환한다.

배치 스크립트를 작성해서 실행한다. 새 형식이 비어 있는 행을 찾아 기존 형식의 값을 변환하고 새 형식에 저장한다.

dry-run을 먼저 실행한다. 대상 건수와 예상 소요 시간을 확인한다. 대량 데이터라면 배치 크기를 조절해서 DB 부하를 관리한다.

실행 후에는 검증이 필요하다. 새 형식의 값이 기존 형식과 일치하는지 확인한다. 전체 행 수도 대조한다. 검증이 마이그레이션보다 더 많은 시간을 차지하는 경우가 많다.

롤백: Step 1의 fallback 읽기가 살아 있으므로, 새 형식에 문제가 있어도 기존 형식으로 자동 전환된다.

Step 3: 정리

마이그레이션이 완료되고 검증을 통과한 뒤, 기존 형식과 fallback 로직을 제거한다.

  • 데이터 검증: 새 형식에 null이나 빈 값이 없는지 한 번 더 확인한다. Step 2 이후 신규 데이터도 빠짐없이 새 형식으로 저장되었는지 점검한다.
  • 코드 정리: 이중 쓰기를 제거하고 fallback 분기를 제거한다. 새 형식만 사용하도록 단일화한다.
  • 스키마 정리: 기존 형식의 컬럼이나 필드를 삭제한다.

롤백 불가: 기존 형식을 삭제하면 원본 데이터가 사라진다. 검증이 철저해야 하는 이유다.

적용 사례

이 패턴은 DB 컬럼 암호화에만 국한되지 않는다. 데이터 형식이 바뀌고, 서비스를 멈출 수 없는 상황이라면 동일한 구조가 적용된다.

컬럼 암호화 전환. 평문 컬럼 옆에 암호화 컬럼을 추가하고, 이중 쓰기로 양쪽에 저장한다. 기존 평문을 일괄 암호화한 뒤 평문 컬럼을 삭제한다.

컬럼 타입 변경. varchar(100)text, intbigint 같은 변경도 같은 패턴이다. 새 타입 컬럼을 추가하고, 이중 쓰기 + fallback으로 전환한 뒤 기존 컬럼을 삭제한다.

JSON 스키마 변경. JSON 컬럼의 키 이름을 바꾸거나 구조를 변경할 때, 기존 구조와 새 구조를 동시에 지원하는 전환 기간을 만든다.

데이터 정규화/비정규화. 조인을 줄이기 위해 비정규화 컬럼을 추가하거나, 반대로 정규화를 위해 별도 테이블로 분리할 때도 이중 쓰기 + fallback 구조가 유효하다.

패턴의 비용

이 패턴은 안전성을 제공하지만 비용이 따른다.

전환 기간 동안 코드 복잡도가 올라간다. 이중 쓰기와 fallback 분기가 서비스 코드 곳곳에 추가된다. 이 코드는 Step 3에서 제거되지만, 전환 기간 동안은 리뷰와 유지보수 부담이 늘어난다.

전환 대상이 여러 개라면 이 비용이 반복된다. 테이블이 10개면 같은 패턴을 10번 적용해야 한다. 구조가 동일한 반복 작업이므로 자동화를 고려할 수 있는 지점이다.

이 패턴이 적합한 조건은 명확하다. 복수의 서비스가 같은 데이터를 참조하고, 대량의 데이터가 있으며, 서비스 중단이 허용되지 않는 경우. 이 조건이 아니라면 점검 시간을 잡고 한 번에 전환하는 것이 더 단순할 수 있다.

참고

  • 봉투 암호화 — 컬럼 암호화 전환에 함께 적용되는 키 관리 구조를 다룬다.