마이그레이션 시스템

버전 게이트, 멱등성 패턴, 설정 레이어 규율 — 사용자 설정을 깨지 않고 업그레이드.

01

개요

Claude Code의 마이그레이션 시스템은 데이터베이스 스키마 마이그레이션이 아닙니다. 마이그레이션 테이블도, 롤백도, 프레임워크도 없습니다. 대신, 모든 마이그레이션 함수는 설계상 멱등이며, 단일 버전 번호로 보호됩니다.

5가지 마이그레이션 유형

02

러너 — runMigrations()

main.tsx에서 CURRENT_MIGRATION_VERSION = 11로 정의되며, 전체 동기 마이그레이션 블록을 게이팅합니다:

const CURRENT_MIGRATION_VERSION = 11;

function runMigrations(): void {
  if (getGlobalConfig().migrationVersion !== CURRENT_MIGRATION_VERSION) {
    migrateAutoUpdatesToSettings();
    migrateBypassPermissionsAcceptedToSettings();
    migrateEnableAllProjectMcpServersToSettings();
    resetProToOpusDefault();
    migrateSonnet1mToSonnet45();
    migrateLegacyOpusToCurrent();
    migrateSonnet45ToSonnet46();
    migrateOpusToOpus1m();
    migrateReplBridgeEnabledToRemoteControlAtStartup();
    // feature-gated migrations...
    saveGlobalConfig(prev => ({ ...prev, migrationVersion: CURRENT_MIGRATION_VERSION }));
  }
  migrateChangelogFromConfig().catch(() => {});
}

핵심 구조:

주의사항

migrationVersion === CURRENT_MIGRATION_VERSION이면 전체 동기 블록이 건너뛰어지고, 비동기 마이그레이션(migrateChangelogFromConfig)만 실행됩니다. 다운그레이드하면 이미 올라간 마이그레이션 버전이 남아 재실행을 방지하여 미묘한 설정 불일치가 발생할 수 있습니다.

03

마이그레이션 카탈로그

11개 동기 마이그레이션 + 1개 비동기 마이그레이션의 전체 카탈로그:

# 함수명 카테고리 수행 내용 멱등성 가드
1 migrateAutoUpdatesToSettings 설정 프로모션 자동 업데이트 설정을 GlobalConfig에서 userSettings로 이동 완료 플래그
2 migrateBypassPermissionsAcceptedToSettings 설정 프로모션 권한 우회 승인 플래그를 userSettings로 이동 완료 플래그
3 migrateEnableAllProjectMcpServersToSettings 설정 프로모션 프로젝트 MCP 서버 활성화 플래그를 userSettings로 이동 완료 플래그
4 resetProToOpusDefault 원샷 리셋 Pro 사용자의 기본 모델을 Opus로 리셋 완료 플래그
5 migrateSonnet1mToSonnet45 모델 앨리어스 Sonnet 1M 앨리어스를 Sonnet 4.5로 업그레이드 자기멱등 데이터 체크
6 migrateLegacyOpusToCurrent 모델 앨리어스 레거시 Opus 식별자를 현재 버전으로 업그레이드 자기멱등 데이터 체크
7 migrateSonnet45ToSonnet46 모델 앨리어스 Sonnet 4.5를 Sonnet 4.6으로 업그레이드 + 인메모리 상태 업데이트 자기멱등 데이터 체크
8 migrateOpusToOpus1m 모델 앨리어스 Opus를 Opus 1M으로 업그레이드 자기멱등 데이터 체크
9 migrateReplBridgeEnabledToRemoteControlAtStartup 설정 키 이름변경 REPL bridge 설정 키를 remote control at startup으로 변경 자기멱등 데이터 체크
10-11 feature-gated 다양 기능 플래그 조건부 마이그레이션 다양
비동기 migrateChangelogFromConfig 비동기 파일 변경 로그를 GlobalConfig에서 별도 파일로 이동 파일 존재 여부 체크
04

2가지 멱등성 패턴

패턴 A: GlobalConfig의 완료 플래그

데이터만 보고 "이미 완료되었는지" 판단할 수 없을 때 사용합니다. 마이그레이션 완료 후 GlobalConfig에 플래그를 기록합니다:

// 패턴 A — 완료 플래그로 멱등성 보장
function migrateAutoUpdatesToSettings(): void {
  const config = getGlobalConfig()
  if (config.migratedAutoUpdates) return  // 이미 완료 — 건너뜀

  const currentValue = config.autoUpdatesEnabled
  if (currentValue !== undefined) {
    saveUserSettings(prev => ({
      ...prev,
      autoUpdatesEnabled: currentValue,
    }))
  }

  // 환경 변수도 설정 — userSettings 쓰기는 다음 실행까지 효과 없으므로
  process.env.CLAUDE_AUTO_UPDATES = String(currentValue)

  saveGlobalConfig(prev => ({
    ...prev,
    migratedAutoUpdates: true,  // 완료 플래그 기록
  }))
}

패턴 B: 자기멱등 데이터 체크

현재 데이터 값 자체가 "마이그레이션이 필요한지" 직접 알려줄 때 사용합니다. 별도 플래그가 불필요합니다:

// 패턴 B — 데이터 값이 직접 조건이 됨
function migrateSonnet45ToSonnet46(): void {
  const settings = getUserSettings()
  const model = settings.preferredModel

  // 현재 값이 'sonnet-4.5'일 때만 마이그레이션 — 이미 '4.6'이면 건너뜀
  if (model === 'claude-sonnet-4-5-20250514') {
    saveUserSettings(prev => ({
      ...prev,
      preferredModel: 'claude-sonnet-4-6-20250715',
    }))
    // 인메모리 런타임 상태도 업데이트
    setCurrentModel('claude-sonnet-4-6-20250715')
  }
}
딥 다이브 — 패턴 선택 기준

패턴 A는 원본 값을 삭제하거나, 마이그레이션 후 원본과 결과가 동일할 수 있을 때 사용합니다 (예: 설정 프로모션 — 원본 키를 유지하면서 새 위치에 복사). 패턴 B는 값 자체가 "구 버전"인지 "신 버전"인지 명확히 구분될 때 사용합니다 (예: 모델 앨리어스 — sonnet-4.5는 구 버전, sonnet-4.6은 신 버전).

05

설정 레이어 규율

모든 모델 마이그레이션은 userSettings만 읽기/쓰기합니다. 병합된 설정은 절대 읽지 않습니다.

위험: 병합 설정 읽기

병합 설정을 읽으면 프로젝트 범위 모델 핀을 전역 기본으로 잘못 승격할 위험이 있습니다. 예를 들어:

  1. 사용자가 프로젝트 A에서 .claude/settings.jsonpreferredModel: "claude-opus-4-20250918"을 설정
  2. 마이그레이션이 병합 설정을 읽으면 이 프로젝트 핀이 보임
  3. 마이그레이션이 이 값을 userSettings(전역)에 쓰면, 프로젝트 A 전용 설정이 모든 프로젝트의 기본값이 됨

userSettings만 읽으면 전역 사용자 설정만 보이므로 이 문제가 발생하지 않습니다.

06

인메모리 설정 마이그레이션

config.tsmigrateConfigFields()디스크에서 설정을 읽을 때마다 실행됩니다. runMigrations()와는 별개로, 가장 오래된 스키마 변경을 처리하는 인메모리 변환 레이어입니다.

이 함수의 역할:

딥 다이브 — runMigrations()와의 차이

runMigrations()는 프로세스 시작 시 한 번 실행되며 디스크에 쓰기를 합니다. migrateConfigFields()는 설정을 읽을 때마다 실행되며 디스크에 쓰지 않습니다. 전자는 "영구 마이그레이션"이고 후자는 "호환성 어댑터"입니다.

07

분석 계측

각 마이그레이션은 실행 시 분석 이벤트를 기록합니다:

마이그레이션 이벤트 추적 데이터
모델 앨리어스 업그레이드 model_migrated 이전 모델, 새 모델, 마이그레이션 함수명
설정 프로모션 setting_promoted 설정 키, 원본 위치, 대상 위치
원샷 리셋 setting_reset 리셋된 키, 이전 값
설정 키 이름변경 setting_renamed 이전 키, 새 키

이 이벤트들은 마이그레이션 도달률과 성공률을 모니터링하는 데 사용됩니다. 특정 마이그레이션이 예상보다 적게 실행되면 배포 문제나 다운그레이드를 의심할 수 있습니다.

08

새 마이그레이션 추가 레시피

7단계 체크리스트

  1. 멱등성 패턴 선택 — 패턴 A(완료 플래그)와 패턴 B(자기멱등 데이터 체크) 중 선택
  2. 마이그레이션 함수 작성userSettings만 읽기/쓰기, 병합 설정 사용 금지
  3. runMigrations()에 함수 호출 추가saveGlobalConfig 호출 전에 배치
  4. CURRENT_MIGRATION_VERSION 증가 — 필수: 버전을 올리지 않으면 기존 사용자에게 마이그레이션이 실행되지 않음
  5. 분석 이벤트 추가 — 마이그레이션 실행 시 이벤트 기록
  6. 인메모리 상태 업데이트 고려userSettings 쓰기는 다음 실행까지 효과 없으므로, 현재 프로세스에서 즉시 적용이 필요하면 인메모리 상태도 업데이트
  7. 테스트 — 멱등성 검증 (2회 연속 실행 시 부작용 없음), 깨끗한 상태에서 실행, 이미 마이그레이션된 상태에서 실행

템플릿 코드

// 새 마이그레이션 템플릿 — 패턴 B (자기멱등 데이터 체크)
function migrateOldValueToNewValue(): void {
  const settings = getUserSettings()  // userSettings만 — 병합 설정 금지!

  if (settings.someKey === 'old-value') {
    logEvent('setting_migrated', {
      key: 'someKey',
      from: 'old-value',
      to: 'new-value',
    })

    saveUserSettings(prev => ({
      ...prev,
      someKey: 'new-value',
    }))

    // 필요시 인메모리 상태도 업데이트
    // setCurrentSomeKey('new-value')
  }
}
09

핵심 요약

핵심 포인트

  • 마이그레이션 시스템은 DB 마이그레이션이 아님 — 마이그레이션 테이블, 롤백, 프레임워크 없이 멱등 함수 + 단일 버전 번호로 동작
  • CURRENT_MIGRATION_VERSION이 일치하면 전체 동기 블록이 건너뛰어지고, 비동기 마이그레이션만 실행
  • 5가지 마이그레이션 유형: 설정 프로모션, 모델 앨리어스 업그레이드, 설정 키 이름변경, 원샷 리셋, 비동기 파일 마이그레이션
  • 2가지 멱등성 패턴: 완료 플래그(패턴 A)와 자기멱등 데이터 체크(패턴 B)
  • 모든 모델 마이그레이션은 userSettings만 읽기/쓰기 — 병합 설정을 읽으면 프로젝트 핀이 전역으로 승격되는 위험
  • migrateConfigFields()는 디스크 읽기 시마다 인메모리 변환을 수행하는 호환성 어댑터로, runMigrations()와 별개
  • migrateAutoUpdatesToSettingsprocess.env를 설정 — userSettings 쓰기는 다음 실행까지 효과 없으므로 현재 프로세스에서 즉시 적용
  • 새 동기 마이그레이션 추가 시 CURRENT_MIGRATION_VERSION 증가 필수
10

지식 확인

퀴즈 — 5문제

Q1. migrationVersion === CURRENT_MIGRATION_VERSION일 때 어떻게 되나요?

  • A) 모든 마이그레이션 실행 + 플래그로 중단
  • B) 전체 동기 블록 건너뜀, 비동기만 실행
  • C) 오류 종료
  • D) 검증 후 종료
runMigrations()의 if 문이 !==로 게이팅하므로, 버전이 일치하면 동기 블록 전체가 건너뛰어집니다. 게이트 밖의 migrateChangelogFromConfig()(비동기)만 실행됩니다.

Q2. 모델 마이그레이션이 병합 설정 대신 userSettings를 읽는 이유는?

  • A) REPL 실행 후에만 가능해서
  • B) 프로젝트 범위 모델 핀을 전역 기본으로 잘못 승격할 위험
  • C) 앨리어스 제한 때문에
  • D) 암호화 요구사항
병합 설정은 프로젝트별 .claude/settings.json의 값을 포함합니다. 이를 읽어 전역 userSettings에 쓰면, 특정 프로젝트에서만 의도한 모델 핀이 모든 프로젝트의 기본값으로 승격됩니다.

Q3. 인메모리 런타임 상태도 업데이트하는 마이그레이션은?

  • A) migrateLegacyOpusToCurrent
  • B) migrateSonnet45ToSonnet46
  • C) migrateSonnet1mToSonnet45
  • D) migrateReplBridge
migrateSonnet45ToSonnet46userSettings에 새 모델을 쓰면서 동시에 setCurrentModel()로 인메모리 런타임 상태도 업데이트합니다. userSettings 쓰기만으로는 현재 실행 중인 프로세스에 즉시 반영되지 않기 때문입니다.

Q4. 새 동기 마이그레이션을 추가할 때 필수인 것은?

  • A) DB 행 추가
  • B) CURRENT_MIGRATION_VERSION 증가
  • C) DEFAULT_GLOBAL_CONFIG 수정
  • D) init.ts 등록
CURRENT_MIGRATION_VERSION을 증가시키지 않으면 이미 이전 버전으로 마이그레이션된 사용자의 migrationVersion이 일치하여 새 마이그레이션이 실행되지 않습니다.

Q5. migrateAutoUpdatesToSettingsprocess.env를 설정하는 이유는?

  • A) userSettings 쓰기는 다음 실행까지 효과 없으므로 현재 프로세스에 즉시 적용
  • B) OS 데몬이 요구
  • C) 쓰기 실패 백업
  • D) 분석 전용
saveUserSettings()는 디스크에 쓰지만, 현재 프로세스의 설정 캐시는 이미 로드된 상태입니다. process.env를 설정하면 현재 프로세스에서 즉시 새 값이 적용됩니다. 다음 실행 시에는 userSettings에서 올바른 값이 읽힙니다.
0 / 5