무중단 테이블 재구성 전략

아이스__아메리·2026년 6월 22일

DB

목록 보기
8/10

▎ IDENTITY 컬럼 재설계와 스키마 분리를 서비스 중단 없이 처리하는 Ghost Table 패턴


배경 — 왜 이 전략이 필요한가

운영 중인 items 테이블에 구조적 변경이 필요했다.

  • 문제 1. 하나의 테이블에 성격이 다른 데이터(일반 아이템 / 가챠 아이템)가 혼재

  • 문제 2. IDENTITY 컬럼 재설정, 신규 컬럼 추가, 제약조건 변경이 필요

  • 문제 3. 해당 테이블은 게임 메인 BM 매출의 95% 이상을 담당하는 핵심 테이블 — 단 몇 분의 중단도 직접적인 매출 손실로 이어지므로 무중단이 절대 조건이었다

    MSSQL Server에서 다음과 같은 작업들은 모두 테이블 전체 락(Sch-M Lock)을 유발한다.

┌────────────────────────────────┬────────────────┬───────────┐
│              방법            │  서비스 중단   │ 롤백 가능  │
├────────────────────────────────┼────────────────┼───────────┤
│ DROP → RECREATE             │ ❌  중단 발생  │ ❌  불가  │
├────────────────────────────────┼────────────────┼───────────┤
│ ALTER TABLE (IDENTITY 변경)  │ ❌  전체 락   │ ❌  어려움│
├────────────────────────────────┼────────────────┼───────────┤
│ ALTER COLUMN NOT NULL → NULL│ ❌  전체 락    │ ❌  어려움│
├────────────────────────────────┼────────────────┼───────────┤
│ Ghost Table 패턴             │ ✅   중단 없음 │ ✅  가능 │
└────────────────────────────────┴────────────────┴───────────┘

특히 NOT NULL → NULL과 같은 제약조건 변경은 단순해 보이지만, SQL Server는 내부적으로 테이블 메타데이터 재구성을 위해 Sch-M Lock을 획득한다. 이 락은 모든 읽기/쓰기를 차단하므로 대용량 운영 테이블에서는 사실상 사용 불가다.


사전 작업 — 마이그레이션 전 준비

본 마이그레이션에 앞서 두 가지 선행 작업이 완료되어 있어야 했다.

  1. 신규 컬럼 추가 요청 반영

    가챠 테이블 분리 시 attribute_01 ~ attribute_06 컬럼을 추가하는 요구사항이 있었다. Ghost Table 패턴 특성상 새 테이블 생성 시점에 원하는 스키마를 자유롭게 정의할 수 있어 이 요구사항을 자연스럽게 포함할 수 있었다.

  2. 정규화 코드 테이블 선행 적용

    Step 3 후속에서 문자열 값을 코드로 치환하는 작업에 item_types, item_categories 테이블이 사용된다. 이 코드 테이블들은 레거시 이슈가 있어서 정규화 작업을 하고 난 뒤 메인 마이그레이션보다 먼저 생성되고 데이터가 정비되어 있어야 한다. 코드 테이블 누락 상태에서 치환
    UPDATE를 실행하면 전체 데이터가 오염될 수 있다.

사전 작업 순서:
1. item_types 생성 및 데이터 입력
2. item_categories 생성 및 데이터 입력
3. 데이터 검증 완료

본 마이그레이션 진행


전략의 핵심 아이디어

▎ 원본은 건드리지 않는다 → 복사본에서 작업한다 → 원자적으로 교체한다

원본 items 테이블은 마이그레이션 내내 살아있다. 새 테이블을 옆에서 조용히 준비한 뒤, sp_rename의 원자성을 이용해 순간적으로 교체한다.


단계별 실행 흐름

Step 1 — Ghost 테이블 생성 (IDENTITY 없이)

새 테이블을 IDENTITY 컬럼 없이 먼저 만든다. IDENTITY가 있으면 기존 id 값을 그대로 INSERT할 수 없기 때문이다. 이 시점에 원본 items 테이블은 정상 운영 중이다.

  -- IDENTITY 없이 생성 → 기존 id 값을 자유롭게 INSERT 가능
  CREATE TABLE [dbo].[items_new]
  (
      [id]   [bigint] NOT NULL,  -- IDENTITY(1,1) 없음
      [type] [nvarchar](20) NOT NULL,
      ...
      CONSTRAINT [PK_items_new] PRIMARY KEY CLUSTERED ([id] ASC)
  )

Step 2 — 데이터 이동 (타입 기준 분리)

원본 items에서 조건을 걸어 각 Ghost 테이블로 복사한다. WITH (NOLOCK)으로 읽기 잠금을 최소화하고, 데이터 변환도 이 시점에 함께 처리한다.

  -- 유료 아이템 → items_new
  INSERT INTO [dbo].[items_new] (id, [type], name, price, ...)
  SELECT
      id, [type], name,
      TRY_CAST(price AS int) AS price,  -- 타입 변환
      ...
  FROM [dbo].[items] WITH (NOLOCK)
  WHERE type = N'유료 아이템';

  -- 가챠 아이템 → gacha_items (컬럼 구조가 다름)
  INSERT INTO [dbo].[gacha_items] (id, [type], name, img, ...)
  SELECT id, [type], name, img, ...
  FROM [dbo].[items] WITH (NOLOCK)
  WHERE type = N'트레져 체스트';

실패해도 원본은 그대로이므로 안전하다.

Step 3 — IDENTITY 복원

데이터가 채워진 Ghost 테이블을 _bak으로 이름을 바꾸고, IDENTITY가 포함된 최종 테이블을 새로 만든 뒤 SET IDENTITY_INSERT ON으로 기존 id를 보존하며 다시 INSERT한다.

  -- ① Ghost 테이블을 bak으로 보존 (원자적 이름 변경)
  EXEC sp_rename 'items_new', 'items_new_bak'
  ALTER TABLE [items_new_bak] DROP CONSTRAINT [PK_items_new]

  -- ② IDENTITY 포함 최종 테이블 생성 (+ 신규 컬럼 attribute_01~06 추가)
  CREATE TABLE [dbo].[items_new]
  (
      [id] [bigint] IDENTITY(1,1) NOT NULL,
      ...
  )

  -- ③ 기존 id 값 보존하며 INSERT
  SET IDENTITY_INSERT [dbo].[items_new] ON
  INSERT INTO [dbo].[items_new] (id, type, name, ...)
  SELECT id, type, name, ...
  FROM [dbo].[items_new_bak]
  SET IDENTITY_INSERT [dbo].[items_new] OFF

  -- ④ IDENTITY 시드를 현재 최댓값으로 재설정
  DBCC CHECKIDENT ('items_new', RESEED)

Step 4 후속 — 레거시 문자열 코드 치환

마지막으로 category, duration, usage 컬럼의 한글 문자열 값을 코드 테이블 기준으로 일괄 치환한다.

  -- "지속시간" 같은 레거시 문자열 → 코드값으로 치환
  UPDATE A
  SET    A.duration = B.code
  FROM   items_new       AS A WITH (NOLOCK)
  JOIN   item_categories AS B WITH (NOLOCK) ON A.duration = B.name
  WHERE  B.code_parent = 'duration'

전체 흐름 요약

  [사전 작업]
    item_types, item_categories 생성 및 검증
         ↓
  [items] 원본 테이블 (서비스 중)
       │
       ├─ Step 1: Ghost 테이블 생성 (IDENTITY 없음)
       │
       ├─ Step 2: 데이터 복사 (NOLOCK, 타입 변환 포함)
       │          items → items_new    (아이템)
       │          items → gacha_items  (가챠)
       │
       └─ Step 3: IDENTITY 복원
                  sp_rename → _bak 으로 보존   ← 롤백 보험
                  CREATE 최종 테이블 (IDENTITY 포함, 신규 컬럼 포함)
                  IDENTITY_INSERT ON → INSERT from bak
                  DBCC CHECKIDENT (RESEED)
                  UPDATE: 문자열 → 코드 치환
                  (DROP bak은 검증 후 수동 실행)

핵심 포인트 3가지

  1. sp_rename의 원자성

    카탈로그 메타데이터만 변경하므로 거의 즉각적이며 테이블 잠금이 최소화된다. Ghost 테이블이 준비된 순간 rename 한 번으로 교체가 완료된다.

  2. SET IDENTITY_INSERT ON으로 id 보존

    IDENTITY 컬럼이 있는 테이블에 id 값을 직접 지정해 INSERT할 수 있는 유일한 방법이다. 세션 단위로 동작하며, 작업 후 반드시 OFF로 되돌려야 한다.

  3. DBCC CHECKIDENT (RESEED)

    IDENTITY_INSERT로 기존 id를 삽입하면 IDENTITY 카운터가 갱신되지 않는다. 반드시 재설정해야 이후 INSERT에서 중복 키 오류가 발생하지 않는다.


롤백 전략

┌─────────────┬─────────────────────────────────────────────────────────────┐
│  실패 시점  │                       롤백 방법                         │
├─────────────┼─────────────────────────────────────────────────────────────┤
│ Step 2 실패 │ Ghost 테이블만 DROP. 원본 items 무결                     │
├─────────────┼─────────────────────────────────────────────────────────────┤
│ Step 3 실패 │ items_new_bak을 items_new로 rename하면 Step 2 상태로 복원 │
└─────────────┴─────────────────────────────────────────────────────────────┘

주의사항

DML 발생 구간

이 패턴은 복사 기간 동안 원본에 DML이 없거나 무시 가능할 때 안전하다. Step 2 ~ Step 3 사이에 원본에 INSERT/UPDATE가 발생하면 해당 변경분은 반영되지 않는다. 실시간 DML이 지속되는 환경에서는 점검 시간을 짧게 잡거나 CDC(Change Data Capture)와 병행해야 한다.

NOT NULL → NULL 제약조건 변경

단순해 보이는 ALTER COLUMN NOT NULL → NULL 변경도 SQL Server에서는 Sch-M Lock(스키마 수정 락)을 획득한다. 이 락은 해당 테이블의 모든 읽기/쓰기를 차단하며, 대용량 테이블에서는 수 분~수십 분이 소요될 수 있다. 매출 95% 이상을 담당하는 테이블에서 이 방법은 선택지가 될 수 없었고, Ghost Table 패턴으로 새 테이블 정의 시점에 원하는 제약조건을 처음부터 올바르게 지정하는 것으로 해결했다.

사전 작업 미완료 시 데이터 오염

Step 3 후속의 코드 치환 UPDATE는 item_types, item_categories가 정확히 준비된 상태를 전제한다.(운영 미리 적용) 코드 테이블에 누락된 값이 있으면 해당 행의 컬럼이 NULL로 치환되거나 JOIN 미매칭으로 업데이트가 누락된다. 반드시 사전 작업 검증 후 본 마이그레이션을 진행해야 한다.

profile
츠케멘 좋아

0개의 댓글