, ,

“마이그레이션이 잘 됐다”는 어떻게 증명하는가 — 5가지 필수 항목

“마이그레이션이 잘 됐다”는 말은 증명되어야 합니다. 핵심 세 축 — 숫자로 일치한다 + 샘플로 일치한다 + 실패가 없다 — 이 셋이 보고서/메시지에 담기면 “잘 됐다”는 자연스럽게 성립합니다.

왜 세 축인가

Row 수가 맞아도 데이터가 깨질 수 있고, 데이터가 맞아도 실패 건이 있으면 재처리가 남은 것이고, 둘 다 맞아도 운영 영향(다운타임, 롤백 불가)이 있으면 “잘 됐다”고 말하기 어렵습니다. 따라서 (A) 정합성-Row Count + (B) 정합성-Checksum + (C) 경계 케이스 + (D) 실패·스킵 + (E) 운영 영향, 다섯 항목이 완성된 마이그레이션 보고의 최소 단위입니다.

핵심 질문한 줄 요약
(A) Row Count원본과 신규의 건수가 같은가?숫자가 일치한다
(B) Checksum / 샘플내용도 같은가?샘플이 일치한다
(C) 경계·엣지 케이스까다로운 row도 정상인가?극단값도 OK
(D) 실패·스킵처리 못 한 건이 있는가?실패가 없다(또는 사유 명시)
(E) 운영 영향서비스에 영향이 있었는가?다운타임·롤백 상태 명시

(A) 정합성 — Row Count

가장 먼저, 가장 쉽게 확인할 수 있는 지표입니다. 전체 건수는 물론 주요 파티션·상태 별로도 나눠서 보여줘야 신뢰도가 올라갑니다.

보고 형식 (표 한 줄이면 충분)

테이블        src           dst           diff   비고
-----------   -----------   -----------   ----   ----------------------------
messages      1,234,567     1,234,567        0
attachments     312,089       312,089        0
user_events   4,001,200     3,998,700   -2,500   만료(90일) 메시지 제외 — 의도된 차이

diff가 있을 때는 반드시 사유를 적습니다. “의도된 차이”임을 명시하지 않으면 의심을 삽니다.

SQL 예시

-- 원본
SELECT COUNT(*) AS src_count FROM src_db.messages;

-- 신규 (의도적 제외 조건이 있으면 동일 조건 적용)
SELECT COUNT(*) AS dst_count FROM dst_db.messages
WHERE created_at > NOW() - INTERVAL '90 days';  -- 만료 제외 시

-- 파티션별 (status 기준)
SELECT status, COUNT(*) FROM src_db.messages GROUP BY status;
SELECT status, COUNT(*) FROM dst_db.messages GROUP BY status;

(B) 정합성 — Checksum / 샘플 비교

건수가 맞아도 내용이 깨질 수 있습니다. Encoding 문제, NULL 처리 차이, 타임존 변환 오류가 대표적입니다. 전수 비교는 비싸므로 두 가지 전략 중 하나를 씁니다.

전략 1: 집계 해시 (빠름, 간단)

-- 핵심 컬럼을 이어 붙여 MD5 집계 — 한 쿼리로 전체 비교
-- (Postgres 기준)
SELECT MD5(STRING_AGG(
    CONCAT(id::text, '|', content, '|', created_at::text),
    ',' ORDER BY id
)) AS checksum
FROM messages
WHERE id BETWEEN 1 AND 1000000;

원본과 신규에서 같은 쿼리를 실행해 해시가 일치하면 OK. 불일치 시 범위를 절반씩 좁혀 이진 탐색으로 문제 행을 찾습니다.

전략 2: 무작위 N건 샘플링 (직관적)

-- 무작위 1,000건 샘플 추출
SELECT id, content, status, created_at
FROM messages
ORDER BY RANDOM()
LIMIT 1000;
# Python으로 src/dst 비교
import hashlib

def row_hash(row):
    key = f"{row['id']}|{row['content']}|{row['status']}|{row['created_at']}"
    return hashlib.md5(key.encode()).hexdigest()

src_hashes = {r['id']: row_hash(r) for r in src_sample}
dst_hashes = {r['id']: row_hash(r) for r in dst_sample}

mismatches = [id for id in src_hashes if src_hashes[id] != dst_hashes.get(id)]
print(f"샘플 {len(src_sample)}건 중 불일치: {len(mismatches)}건")
# → 샘플 1,000건 중 불일치: 0건

보고 형식:

무작위 샘플 1,000건 검증: id, content, status, created_at 4개 컬럼 hash 비교 → 불일치 0건


(C) 경계·엣지 케이스

무작위 샘플은 “평범한 row”에만 걸릴 확률이 높습니다. 실제 마이그레이션 사고는 대부분 엣지 케이스에서 납니다. 아래 항목을 명시적으로 확인해 보고에 포함하면 신뢰도가 크게 올라갑니다.

케이스확인 쿼리 힌트왜 중요한가
가장 오래된 rowORDER BY id ASC LIMIT 1초기 데이터, 스키마 변경 이전 레거시 포맷
가장 최근 rowORDER BY id DESC LIMIT 1마이그레이션 cut-off 직전 데이터
가장 큰 페이로드ORDER BY LENGTH(content) DESC LIMIT 5BLOB/TEXT 크기 제한, 버퍼 overflow
NULL 많은 rowWHERE col IS NULL 각 컬럼별NULL 처리 방식 차이(빈 문자열 vs NULL)
한글·이모지·특수문자WHERE content ~ '[가-힣🎉<>&]'Encoding(utf8mb4 vs utf8), collation 차이
타임존 경계값DST 전환일 전후 created_at타임존 변환 오류로 1~2시간 오차 발생 가능
-- 한글/이모지 포함 row 샘플 확인 (Postgres)
SELECT id, LEFT(content, 80) AS preview, LENGTH(content) AS len
FROM messages
WHERE content ~ '[가-힣]' OR content ~ '[^-]'
ORDER BY RANDOM()
LIMIT 20;

보고 형식:

엣지 케이스 확인: 최고령(2019-03-01) / 최신(2026-05-19) / 최대 페이로드(128KB) / NULL 다수 row / 한글+이모지 포함 20건 → 전부 정상 확인


(D) 실패·스킵 처리

0건이면 “0건”이라고 명시해야 합니다. 말하지 않으면 “확인 안 했거나 숨기는 것”으로 의심받습니다.

실패가 있을 때

항목내용
실패 건수23건
사유FK 참조 무결성 위반 (삭제된 user_id 참조)
후속 조치별도 테이블(migration_failures)에 적재 후 수동 검토 예정 / 또는 NULL로 대체 후 재처리
서비스 영향해당 메시지 23건은 이미 서비스에서 노출 불가 상태였으므로 무영향

실패가 없을 때 (꼭 명시)

마이그레이션 실패·스킵: 0건. 전 row 정상 처리 완료.

실패 row 추적 패턴

-- 마이그레이션 실패 테이블 (사전 준비)
CREATE TABLE migration_failures (
    id          BIGSERIAL PRIMARY KEY,
    src_id      BIGINT NOT NULL,
    error_code  TEXT,
    error_msg   TEXT,
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- 마이그레이션 코드에서 실패 시 INSERT
INSERT INTO migration_failures (src_id, error_code, error_msg)
VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING;
-- 마이그레이션 완료 후 집계
SELECT error_code, COUNT(*) AS cnt, MIN(src_id), MAX(src_id)
FROM migration_failures
GROUP BY error_code
ORDER BY cnt DESC;

(E) 운영 영향

항목확인 포인트보고 예시
소요 시간시작~완료 timestamp2026-05-19 02:00 ~ 03:47 (107분)
다운타임서비스 중단 여부다운타임 없음 (읽기는 원본에서 계속 서비스)
트래픽 영향p99 latency, DB CPU 변화마이그레이션 중 원본 DB CPU 피크 67% (평소 대비 +20%p), p99 응답시간 +40ms — 허용 범위 내
롤백 준비원본 보존 기간, 롤백 절차원본 테이블 30일 보존 후 DROP (2026-06-18 이후). 롤백 시 애플리케이션 config만 변경으로 원본 복귀 가능.

완성된 보고 템플릿

Slack 메시지나 노션 문서에 아래 형식을 그대로 써도 됩니다.

## 마이그레이션 완료 보고 — messages 테이블 (2026-05-19)

### (A) Row Count
| 테이블       | src           | dst           | diff | 비고          |
|------------|---------------|---------------|------|---------------|
| messages   | 1,234,567     | 1,234,567     |    0 |               |
| attachments|   312,089     |   312,089     |    0 |               |

### (B) 샘플 검증
- 무작위 1,000건: id / content / status / created_at hash 비교 → **불일치 0건**
- 집계 해시(MD5): src == dst ✅

### (C) 엣지 케이스
- 최고령 row (2019-03-01) ✅ / 최신 row (2026-05-19) ✅
- 최대 페이로드 128KB ✅ / NULL 다수 row ✅
- 한글·이모지·특수문자 포함 20건 ✅

### (D) 실패·스킵
- **0건** — 전 row 정상 처리 완료

### (E) 운영 영향
- 소요: 02:00~03:47 (107분) / 다운타임: 없음
- DB CPU 피크 +20%p, p99 latency +40ms — 허용 범위 내
- 원본 테이블 30일 보존 (2026-06-18 이후 DROP 예정)
- 롤백: config 변경만으로 원본 복귀 가능

자주 빠뜨리는 것들

흔한 실수결과해결
“건수 맞음” 만 보고내용 깨진 것을 뒤늦게 발견반드시 샘플 hash 비교 추가
실패 0건을 명시 안 함“혹시 숨기는 거 아니야?” 의심“0건”을 명시적으로 적는다
전체 건수만, 파티션 없음특정 상태·기간 데이터 누락 못 잡음status / date range 별로 나눠서 확인
의도된 diff 설명 없음diff가 오류인지 정책인지 모름diff > 0이면 사유를 반드시 기재
엣지 케이스 생략이모지·한글·대용량 row에서 나중에 사고명시적 항목 체크리스트 만들기
롤백 계획 미언급문제 발생 시 대응이 느림, 불안감원본 보존 기간·롤백 방법을 미리 합의

요약: “숫자로 일치한다 + 샘플로 일치한다 + 실패가 없다” 세 축이 보고서에 담기면 “마이그레이션이 잘 됐다”는 증명됩니다. (C) 엣지 케이스와 (E) 운영 영향은 신뢰도를 높이는 보완 축입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다