1편에서 설계한 UserBadge 분리 파이프라인을 실제로 수행한 기록으로, 16억 8천만 레코드 마이그레이션의 단계별 디테일과 비용·시간 비교를 담았다. 자체 Java 마이그레이션이었다면 $366.71에 최소 7일이 걸렸을 작업이 AWS 관리형 서비스 조합으로 6시간 만에 끝났다. 실패해도 실패한 단계만 재시도할 수 있다는 점이 시간보다 더 큰 차이였다.
핵심 포인트- Dual Write에는 서로 다른 버전의 변경이 순서가 뒤바뀌어 도착할 수 있어 ConditionExpression으로 LWW 전략을 적용했다.
- Spark는 아이템에 필드가 없으면 NULL로 채우는데 DynamoDB는 NULL을 저장하지 않아, 그대로 Import하면 실패한다.
- Glue ETL 첫 실행은 Worker type G 1X에 10대로 돌렸다가 타임아웃이 났다. 1.8TB를 처리하기엔 턱없이 부족했다.
- Worker 수는 감이 아니라 CloudWatch의 필요 Worker 수 메트릭을 보고 조정하면 비용과 성능의 균형을 잡을 수 있다.
- 1.8TB 테이블이 gzip 압축으로 약 280GB가 됐고, Export는 테이블 RCU를 소모하지 않아 프로덕션에 영향이 없었다.
상세 정리- 1차 배포: 기존 Badge 업데이트가 성공한 뒤 TmpUserBadge에 upsert하는 구조이며, Dual Write가 실패해도 원본 업데이트에 영향을 주지 않도록 try-catch로 감쌌다.
- LWW 적용: 예컨대 User version 5의 업데이트가 version 3보다 먼저 도착할 수 있어, 저장된 것보다 새로운 버전일 때만 업데이트되고 아이템이 아직 없으면 그대로 쓰이도록 조건을 걸었다. 이 전략은 3차 마이그레이션에서도 그대로 쓰인다.
- Export 준비: PITR을 활성화하고 콘솔에서 S3 버킷 경로를 지정해 실행하면 각 줄이 하나의 아이템인 JSON Lines 형식의 gzip 파일이 생성된다.
- Glue 스크립트: Visual 에디터의 드래그앤드롭 대신 LLM의 도움을 받아 PySpark 커스텀 스크립트를 작성했다.
- DynamicFrame을 쓴 이유는 스키마가 불완전하거나 필드가 누락된 데이터도 유연하게 처리할 수 있기 때문이다. 16억 8천만 레코드 중 일부 아이템에만 존재하는 필드가 있어 이 유연성이 중요했다.
- 필드 추출: DynamoDB JSON은 타입 정보가 포함된 구조라 수십 개 필드 중 Badge에 해당하는 것만 골라 적절한 타입으로 매핑해야 한다.
- NULL 처리: Spark가 없는 필드를 NULL로 채우는 것과 DynamoDB가 NULL을 저장하지 않는 것의 차이를 맞추지 않으면 Import가 실패한다.
- 품질 검증: Glue의 Data Quality 기능으로 변환 데이터를 검증하되, 규칙이 맞지 않아도 Job 자체를 중단하지 않는 설정으로 두고 콘솔의 Data Quality 탭에서 실패 데이터를 확인했다.
- Glue 튜닝 항목: Worker type을 대용량 I/O에 유리한 쪽으로 변경하고, 비용 효율을 위해 실제 필요한 만큼만 투입되는 옵션을 켜고, 타임아웃 경험 때문에 안전하게 최댓값으로 설정했다.
- Import 결과: 16억 8천만 레코드가 모두 들어갔고 새 테이블은 S3 데이터를 기반으로 스키마가 자동 감지돼 별도 정의가 필요 없었다.
- 나중에 알게 된 함정: Import 시 데이터가 특정 순서로 정렬돼 있으면 같은 파티션 키의 레코드가 물리 파티션에 순차적으로 몰려 쓰로틀링이 발생할 수 있다. Glue ETL 단계에서 데이터를 랜덤하게 섞으면 방지된다.
- 이번 마이그레이션은 PK만 있고 SK가 없어 이 문제가 없었지만 PK+SK 구조 테이블을 Import할 때는 기억해 두라고 덧붙인다.
- 2차 배포: Dual Write 대상을 TmpUserBadge에서 UserBadge로 전환하는데, DAO 클래스명만 바뀌고 LWW 전략과 조건식은 그대로다. 1편에서 동기 Dual Write를 고른 이유가 최종 구조 전환의 사전 리허설이었고 이 단계가 그 리허설이라고 밝힌다.
- 3차 마이그레이션: Export 시점과 UserBadge Dual Write 시작 시점 사이의 변경분은 TmpUserBadge에만 있으므로, Full Scan으로 옮기되 UserBadge가 이미 Dual Write로 갱신 중이라 여기서도 LWW를 적용했다.
- TmpUserBadge는 마이그레이션 기간의 변경분만 담아 약 1,100만 건으로 원본에 비하면 매우 적었고, 서비스에 영향을 주지 않는 테이블이라 온디맨드로 빠르게 처리했다.
- 3차 배포: 애플리케이션에서 Badge 관련 조회와 수정 로직을 모두 제거하고 UserBadge를 바라보게 일괄 변경하면서, User 테이블이 Badge 트래픽으로부터 완전히 분리됐다.
- 비용 비교 상세: 기존 방식의 $366.71은 User 테이블 Full Scan의 RCU 비용 $11.84와 UserBadge Full Write의 WCU 비용 $354.87을 합산한 값으로, 1.8TB를 500 RCU/초로 스캔하고 16억 8천만 레코드를 3,000 WCU/초로 쓰는 기준이다.
- 새 방식에서 가장 큰 비용은 Export의 $183.55로 GB당 약 $0.1 수준이다. Glue ETL은 G 2X Worker 80대로 30분 돌려 $19.93이었고, Import는 압축 해제 기준 용량으로 과금돼 $41.93이었다.
- 운영상의 차이: 기존 방식은 마이그레이션 트래픽이 운영 트래픽과 경합하지만 새 방식은 그렇지 않고, Glue ETL에서 타임아웃이 났을 때도 Export부터 다시 할 필요 없이 Glue Job 설정만 고쳐 재실행하면 됐다.
- 최종 효과: 메시지 전송 트래픽이 100배 이상 늘어도 UserBadge 테이블의 프로비저닝 용량만 조절하면 대응할 수 있는 구조가 됐다.
- 기존에 프로비저닝을 높여도 안 되던 이유 둘을 다시 짚는다. 트랜잭션 충돌 시 재시도로 WCU가 누적되는 문제는 용량을 높여도 막을 수 없고, GSI 파티션당 1,000 WCU 제한은 테이블 전체 용량 설정과 무관하다.
왜 읽나DynamoDB 대용량 테이블을 무중단으로 쪼개야 하는 팀에게 Export·Glue·Import 각 단계의 실제 설정값과 함정, 그리고 자체 스크립트 대비 비용·시간 비교를 숫자로 준다.