다운타임 없는 DynamoDB 마이그레이션
SQL에서 넘어왔다면 마이그레이션은 모든 행을 다시 쓰는 동안 테이블을 잠그는 ALTER TABLE입니다. DynamoDB에는 변경할 스키마가 없습니다 — 아이템은 스키마가 없으므로, 속성이나
새 엔티티 유형을 추가하는 것은 거저입니다.
어려운 부분은 새 데이터가 처리해야 하는 액세스 패턴이며, 멈춰 세우고 다시 쓰는 일 없이 살아 있는 데이터를 그것에 맞게 재형성하는 것입니다.
다운타임 없이 DynamoDB 테이블을 마이그레이션하려면 어떻게 해야 하나요?
DynamoDB에는 ALTER TABLE이 없으므로 마이그레이션 중에 테이블이 잠기지 않습니다. UpdateTable을 통해 속성, 새 키 형태, 또는 새 를 온라인으로 추가한 뒤, 라이브 데이터를 점진적으로 재형성합니다. 읽기 시 지연 백필하거나 스로틀된 스윕으로 기존 아이템을 백필하고, 전환 기간 동안 두 형식을 이중 쓰기합니다. 일괄 전환 시점은 없습니다.
ALTER TABLE이 없습니다. 아이템은 스키마가 없습니다. "마이그레이션"은 속성, 새 키 형태, 또는 새 인덱스를 추가하는 것을 의미합니다 — 고정된 컬럼 집합을 다시 쓰는 것이 절대 아닙니다.- 새 쓰기는 쉽고, 옛 아이템이 문제입니다. 기존 행은 새 속성을 지니지 않으므로, 새 인덱스나 쿼리가 백필하기 전까지 그것들을 조용히 놓칩니다.
- 인덱스는 온라인으로 추가하고, 백필은 지연시키세요.
UpdateTable은 살아 있는 테이블에 GSI를 만듭니다. 옛 아이템은 읽기 시(지연) 또는 통제된 스윕으로 백필하세요 — 절대 한 날에 전부 전환하지 마세요. - 전환 기간 동안 이중 쓰기하세요. 두 형태가 공존하는 동안, 어느 읽기 경로도 낡지 않도록 옛 형식과 새 형식을 함께 쓰세요.
컬럼이 아니라 액세스 패턴으로 틀 잡기
하나의 테이블에서 SaaS 워크스페이스 제품을 운영한다고 합시다. 아이템은 PK = "WS#<id>"를
쓰고 SK는 엔티티별로 오버로딩됩니다:
| PK | SK | attributes |
|---|---|---|
| WS#a91 | META | name, tier |
| WS#a91 | DOC#2026-04-01#x7 | title, author, body |
| WS#a91 | DOC#2026-04-02#k2 | title, author, body |
이제 제품 팀이 문서에 댓글을 원하고, 새 읽기를 추가합니다: "한 멤버가 워크스페이스 전반에 작성한 모든 댓글을, 최신 순으로 나열하기." 그 마지막 절이 마이그레이션입니다. 새 엔티티 유형만이라면 사소합니다. 현재 키가 답할 수 없는 쿼리를 처리하는 것이 진짜 일입니다.
새 엔티티 유형을 먼저 추가하기
댓글은 그저 같은 파티션의 새 아이템입니다 — 마이그레이션 의식도, 새 테이블도 없습니다:
| PK | SK | attributes |
|---|---|---|
| WS#a91 | DOC#2026-04-01#x7#CMT#01HZ... | author, text, createdAt |
PK = "WS#a91"에 SK begins_with "DOC#2026-04-01#x7#CMT#"를 건 Query는 이미 한
문서의 댓글을 나열합니다. 기존 문서는 손대지 않습니다. 이 절반은 첫날에 출시됩니다 — 같은
파티션이 둘 다 담는 이유는
아이템 컬렉션과 오버로딩된 키를 참고하세요.
새 쿼리에는 GSI가 필요하다
"한 멤버의 모든 댓글, 최신 순"은 베이스 테이블이 처리할 수 없습니다 — memberId는 PK도
SK 접두사도 아닙니다. 그것은 새 인덱스이며, 그것을 올바르게 고르는 것은 자체적인
결정입니다: GSI vs LSI를 참고하세요(LSI는 테이블 생성 시 존재해야
하므로, 살아 있는 테이블의 마이그레이션에는 GSI가 유일한 선택지입니다).
일반적인 GSI1을 추가하고 새 댓글 아이템에 새 속성을 쓰세요:
| GSI1PK | GSI1SK |
|---|---|
| MEMBER#u44 | 2026-04-02T09:15:00Z |
Query GSI1 WHERE GSI1PK = "MEMBER#u44"에 ScanIndexForward = false를 걸면 멤버별
최신 순 댓글을 얻습니다.
인덱스를 온라인으로 빌드하기
UpdateTable은 다운타임 없이 살아 있는 테이블에 GSI를 추가합니다. DynamoDB는 기존
아이템을 백그라운드에서 인덱스로 백필합니다. 인덱스는 완료될 때까지
CREATING/백필 중을 보고하고, 그 다음 ACTIVE로 전환됩니다
(GSI 관리하기).
여기 두 가지 함정이 있습니다. 첫째, AWS는 새 키가 고르지 않게 분산되면 GSI 추가가
베이스 테이블 쓰기를 스로틀할 수 있다고 경고합니다 — 트래픽이 적은 시간대에 추가하고
CloudWatch를 지켜보세요. 둘째, 인덱스는 ACTIVE가 된 후에도 최종적 일관성을
가집니다. 쓰기가 잠시 GSI에 보이지 않을 수 있습니다.
GSI가 최종적 일관성을 가지는 이유를
참고하세요.
옛 아이템 백필하기
GSI는 GSI1PK/GSI1SK를 가진 아이템만 인덱싱합니다. 속성이 존재하기 전에 작성된
당신의 마이그레이션 이전 댓글은 — 백필이 완료된 후에도 절대 나타나지 않습니다. 온라인 GSI
백필은 기존 아이템을 복사하지만, 그것들에 없는 속성을 발명할 수는 없습니다. 값을 추가해야
합니다.
두 가지 전략:
| 전략 | 동작 방식 | 사용할 때 |
|---|---|---|
| 지연 | 옛 아이템을 읽을 때 새 속성을 되돌려 씀 | 옛 아이템을 자주 읽음; 비용을 조금씩 흘림 |
| 스윕 | 페이지를 나눈 Scan이 모든 옛 아이템을 한 번 업데이트 | 마감일까지 GSI를 완성해야 함 |
스윕의 경우, Scan으로 페이지를 넘기고, 각 옛 댓글에 대해 조건부 UpdateItem으로 인덱스
속성을 추가해 동시 쓰기를 절대 짓밟지 않도록 하세요.
조건은 속성이 아직 존재하지 않는다는 것에 가드를 겁니다. 정확한
ConditionExpression과 UpdateExpression을 attribute_not_exists(GSI1PK)를 손으로
입력하는 대신 DynamoDB Expression Builder로
만들고 복사하세요.
전환 기간 동안 이중 쓰기하기
모든 옛 아이템이 새 속성을 지닐 때까지, 두 형태가 공존합니다. 쓰기 경로는 모든 쓰기에서 — 새 댓글과 옛 댓글에 대한 어떤 업데이트든 — 새 형식을 채워야 합니다. 그래야 간극이 줄어들기만 합니다.
검증할 수 있는 백필 종료 조건을 고르세요: 스윕이 테이블 전체를 페이징했거나, 지연 경로가 충분히 오래 실행되어 변환되지 않은 아이템이 설계상 낡았거나. 그때에야 옛 읽기 경로를 제거하세요. 이를 건너뛰는 것이 일부 쿼리가 조용히 짧은 결과를 반환하는 와중에 마이그레이션이 "완료"되는 방식입니다.

함정
- 속성 추가 ≠ 백필됨. 새 GSI는 옛 아이템에 대해 비어 있는 상태로 시작합니다. 쿼리를 믿기 전에 커버리지를 검증하세요.
- 제자리에서 키 바꾸기는 마이그레이션이 아니라 재작성입니다. 아이템의
PK/SK를 변경할 수 없습니다 — 새 키로 새 아이템을 쓰고 옛 것을 삭제합니다. 복사-후-삭제로 계획하고, 그 사이에는 이중 읽기하세요. - 트랜잭션적 전환은 없습니다. 테이블 전체가 전환되는 순간은 없습니다. 두 형태가 살아 있는 동안에도 모든 단계가 안전하도록 설계하세요.
다음 단계
단일 테이블 설계에서 새 키와 오버로딩된 컬렉션을 점검하고, 살아 있는 테이블을 페이징하여 백필이 완료되었음을 확인하세요. DynoTable을 사용해 테이블을 둘러보고, 백필되지 않은 아이템을 찾아내고, 자신의 데이터에 조건부 업데이트를 실행하세요.


