기존 사용자에 영향을 주지 않고 Column Datatype을 변경하는 방법 (Postgresql)

발행: (2025년 12월 23일 오후 10:08 GMT+9)
5 min read
원문: Dev.to

Source: Dev.to

데이트 앱을 위한 컬럼 데이터 타입 변경, 기존 사용자에 영향 없이

당신이 데이팅 앱을 만든다고 상상해 보세요. 이제 백만 명의 사용자라는 큰 이정표에 도달했습니다. 축하합니다!

초기 단계에서는 단순하게 처리했습니다. 사용자가 아이가 있는지를 추적하기 위해 kids 라는 단일 컬럼에 문자열을 저장했었습니다. 예를 들면:

const updatePreferencesSchema = Joi.object({
  kids: Joi.string().valid(
    "Yes- they live with me",
    "Yes- they don't live with me",
    "no"
  ).optional()
});

나중에 제품 팀은 더 풍부한 데이터가 필요하다고 요구했습니다. 사용자가 아이가 있는지, 그 아이가 집에 사는지, 그리고 사용자의 아이에 대한 의향을 모두 알아야 합니다. 새로운 스키마는 다음과 같습니다:

const updatePreferencesSchema = Joi.object({
  kids: Joi.object({
    hasKids: Joi.boolean().required(),
    liveAtHome: Joi.boolean().required(),
    wantsKids: Joi.string().valid("Yes", "No", "Not Decided")
  }).optional()
});

간단한 ALTER TABLE을 실행하고 새 코드를 배포하면, 아직 오래된 문자열 값을 가지고 있는 기존 행이 깨집니다. 애플리케이션은 "no" 라는 문자열이 저장된 행에서 user.kids.hasKids 를 읽으려 할 때 TypeError 를 발생시킵니다.

중간 상태를 처리하기 위해 Expand and Contract Pattern을 사용할 수 있습니다.

Phase 1: 확장 단계 (안전 브리지)

새 형식의 데이터를 저장할 새로운 열을 추가하고 기존 열은 그대로 유지합니다.

-- Step 1: Add the new column (defaulting to an empty object)
ALTER TABLE users ADD COLUMN kids_v2 JSONB DEFAULT '{}';

이중 쓰기 로직

애플리케이션을 업데이트하여 두 열에 모두 데이터를 기록하도록 합니다. 이렇게 하면 새 데이터가 기존 코드(kids를 계속 읽음)와 새 코드(kids_v2를 읽음) 모두와 호환됩니다.

const oldValue = req.body.kids; // e.g., "yes- they live with me."

const updateData = {
  kids: oldValue,
  kids_v2: {
    hasKids: oldValue.toLowerCase().includes("yes"),
    liveAtHome: oldValue === "yes- they live with me",
    wantsKids: "Not Decided" // default for the new requirement
  }
};

Phase 2: 마이그레이션 (과거 데이터 채우기)

이제 kids_v2가 단순히 {}인 행이 백만 개 정도 있습니다. 한 번에 모두 업데이트하면 테이블이 잠길 수 있으므로, 데이터를 배치 단위로 처리하세요.

안전한 배치 업데이트 스크립트 (Node.js)

const { Pool } = require('pg');
const pool = new Pool();

async function backfillKidsData() {
  const batchSize = 5000;
  let hasMore = true;

  while (hasMore) {
    const res = await pool.query(`
      UPDATE users
      SET kids_v2 = jsonb_build_object(
        'hasKids', CASE WHEN kids ILIKE 'yes%' THEN true ELSE false END,
        'liveAtHome', CASE WHEN kids = 'yes- they live with me' THEN true ELSE false END,
        'wantsKids', 'Not Decided'
      )
      WHERE id IN (
        SELECT id FROM users
        WHERE kids_v2 = '{}' AND kids IS NOT NULL
        LIMIT ${batchSize}
      )
      RETURNING id;
    `);

    if (res.rowCount === 0) hasMore = false;
    console.log(`Migrated ${res.rowCount} rows...`);

    // Give the DB and event loop a short breather
    await new Promise(r => setTimeout(r, 100));
  }
}

이 스크립트를 더 이상 업데이트할 행이 없다고 보고될 때까지 실행하세요.

3단계: 계약 (클린 컷)

모든 행에 kids_v2에 유효한 객체가 포함되면 스키마 패리티를 달성한 것입니다.

  1. API 전환 – 코드를 kids_v2만 읽도록 업데이트합니다.
  2. 모니터링 – 며칠 동안 로그에서 “undefined property” 오류가 있는지 확인합니다.
  3. 삭제 – 기존 컬럼을 삭제하고 이중 쓰기 로직을 제거합니다.
-- Final cleanup
ALTER TABLE users DROP COLUMN kids;

결론

Expand and Contract 패턴을 사용하면 컬럼의 데이터 타입을 다운타임이나 데이터 무결성 문제 없이 진화시킬 수 있습니다. 스키마를 확장하고, 데이터를 배치로 안전하게 마이그레이션한 다음, 다시 축소(정리)함으로써, 런타임 오류를 일으킬 수 있는 공백을 피할 수 있습니다.

Back to Blog

관련 글

더 보기 »

Postgres 18이 이제 사용 가능

Postgres 18이 이제 PlanetScale에서 제공됩니다. 오늘부터 새 데이터베이스를 만들면 기본 버전이 18.1이 됩니다. 이전 버전을 선택할 수도 있습니다.