
회고: cdk deploy 한 줄에 테이블 전체를 날려먹은 날
오늘은 오랫동안 기억에 남을, 아니, 잊어서는 안 될 실수를 저질렀다. 블로그에 '좋아요' 기능을 추가하려다, 몇 주간 열심히 써왔던 모든 게시글 데이터를 내 손으로 날려버렸다. cdk deploy 명령어 한 줄이 이렇게 파괴적인 결과를 낳을 수 있다는 사실을 온몸으로, 그리고 텅 빈 데이터베이스로 마주한 날이다.
모든 것은 '좋아요'로부터 시작되었다
블로그에 생기를 불어넣고 싶었다. 그 첫걸음으로 '좋아요' 기능을 추가하기로 했다. 설계는 순조로웠다. Post 테이블에 likeCount를 추가하고, 사용자별 좋아요 기록을 위한 Like 아이템을 정의했다. 여기까지는 평범한 백엔드 기능 개발이었다.
문제는 성능 최적화를 고민하면서 시작됐다. 게시물 목록을 보여주는 카드(PostCard)에서도 '좋아요' 수를 보여주려면, 목록 조회 시 likeCount를 함께 가져오는 게 효율적이다. 현재 목록 조회는 GSI3를 사용하고 있었으니, 이 GSI의 Projection에 likeCount만 추가하면 되겠다고 생각했다. 지극히 합리적인 판단이었다.
나는 자신 있게 InfraStack.ts의 GSI3 정의를 수정했다.
// GSI3의 nonKeyAttributes에 'likeCount'를 추가했다. postsTable.addGlobalSecondaryIndex({ indexName: 'GSI3', // ... projectionType: dynamodb.ProjectionType.INCLUDE, nonKeyAttributes: [ // ... 기존 속성들 ... 'likeCount' ] });
그리고 터미널에 익숙한 명령어를 입력했다. 모든 것이 좋아질 것이라 믿으면서.
pnpm --filter infra cdk deploy BlogInfraStack
하지만 터미널은 나에게 붉은색 에러 메시지를 뱉어냈다.
첫 번째 오판: "GSI는 수정할 수 없다"
"Cannot update GSI's properties... You can create a new GSI with a different name."
아차 싶었다. DynamoDB의 GSI는 한번 만들면 키 스키마나 프로젝션 같은 핵심 속성을 바꿀 수 없다는 사실을 잠시 잊고 있었다. 마치 건물의 기둥을 옮기려는 시도처럼, 근본적으로 불가능한 작업이었다.
여기서 나는 첫 번째 잘못된 선택을 했다. "수정이 안 된다면, 부수고 새로 만들게 하면 되지."
나는 CDK가 기존 테이블을 삭제하고 새 설정으로 다시 만들도록 유도하기 위해, 테이블의 논리적 ID와 물리적 이름을 임시로 변경하는 꼼수를 썼다. BlogPostsTable을 BlogPostsTableV2로, tableName을 BlogPosts-BlogInfraStack-temp로 바꾸고 배포했다. CloudFormation은 내 의도대로 기존 테이블을 지우고 새 임시 테이블을 만들었다. 성공한 줄 알았다.
그리고 모든 것이 계획대로라 믿으며, 다시 코드의 이름을 원래대로 되돌리고 재배포했다. 임시 테이블이 사라지고, 내가 원하던 최종 이름과 스키마를 가진 테이블이 깨끗하게 생성될 것이라고.
그리고 찾아온 정적
배포는 성공했다. 초록색 Stack deployment finished 메시지를 보고 안심하며 블로그에 접속했다.
화면은 텅 비어 있었다.
아무것도 없었다. 게시글 목록이 있어야 할 자리는 허공뿐이었다. 심장이 쿵 하고 내려앉는 기분과 함께 DynamoDB 콘솔로 달려갔다. BlogPosts-BlogInfraStack 테이블은 존재했지만, Items returned는 0이었다. 모든 데이터가 사라졌다.
원인은 단 한 줄, 내가 개발 초기 편의를 위해 설정해두고 까맣게 잊고 있던 코드에 있었다.
const postsTable = new dynamodb.Table(this, 'BlogPostsTable', { // ... removalPolicy: RemovalPolicy.DESTROY, });
removalPolicy: RemovalPolicy.DESTROY.
CDK 스택에서 리소스가 사라지면, AWS 상에서도 영구적으로 '파괴'하라는 이 무시무시한 명령어를, 나는 내 손으로 직접 실행시켰던 것이다. 테이블을 재생성하는 과정에서 CDK는 내 지시에 따라 충실하게 기존 테이블을 파괴했고, 그 안의 모든 데이터는 복구 불가능한 상태로 먼지가 되어 사라졌다.
무엇을 놓쳤는가
1. removalPolicy에 대한 안일함: "개발 중이니까 괜찮아"라는 생각이 모든 문제의 시작이었다. 데이터가 쌓이기 시작한 순간부터는 더 이상 개발 환경이 아니다. DESTROY는 언제나 시한폭탄과 같다. 프로덕션 코드는 물론이고, 조금이라도 데이터가 쌓인 개발 환경의 데이터 리소스는 무조건 RETAIN을 기본으로 생각해야 했다.
2. 백업의 부재: DynamoDB의 PITR(Point-in-Time Recovery)은 비용이 드는 고급 기능이 아니다. 데이터를 지키기 위한 최소한의 안전벨트다. pointInTimeRecovery: true 단 한 줄을 추가하는 것을 미뤘던 대가는 너무나도 컸다.
3. cdk diff에 대한 맹신: 배포 전 cdk diff를 확인했지만, 나는 DELETE와 CREATE라는 단어의 무게를 제대로 인지하지 못했다. 코드가 어떻게 변하는지만 봤을 뿐, 그 변경이 데이터에 어떤 영향을 미칠지에 대한 깊은 고민이 없었다.
4. 더 안전한 길을 택하지 않은 조급함: GSI를 변경하는 가장 안전한 방법은 새로운 GSI를 추가하고, 애플리케이션이 새 GSI를 바라보게 수정한 뒤, 마지막에 이전 GSI를 제거하는 점진적인 방식이다. 나는 이 길을 알면서도, 테이블을 한 번에 갈아엎는 더 빠르고 쉬운 길을 택했다. 그 조급함이 모든 것을 앗아갔다.
다짐하며
이번 실패는 변명의 여지가 없는 나의 과실이다. 코드를 작성하는 것 만큼이나, 내가 다루는 인프라와 데이터의 특성을 깊이 이해하고 존중하는 것이 얼마나 중요한지 뼈저리게 느꼈다.
앞으로 인프라 코드를 변경할 때는, 특히 데이터와 관련된 리소스를 다룰 때는 항상 최악의 시나리오를 먼저 생각할 것이다. removalPolicy를 세 번 확인하고, 백업 여부를 먼저 체크하며, cdk diff의 DELETE 한 줄에 담긴 무게를 항상 기억할 것이다.
사라진 데이터는 돌아오지 않는다. 하지만 이 경험으로 얻은 교훈은 내 개발자 인생에 영원히 남아, 앞으로 내가 만들 서비스들의 데이터를 지켜주는 방어막이 될 것이다. 텅 빈 데이터베이스 앞에서, 나는 오늘 가장 비싼 수업료를 내고 가장 중요한 것을 배웠다.
