pile·
DB / 데이터·channel-talk채널톡·

DynamoDB 핫 파티션을 해결하는 3가지 방법 (3): 조회를 인덱스 테이블로 옮기기

채널톡이 DynamoDB 핫 파티션을 해결하려 GSI를 별도 인덱스 테이블로 떼어낸 연재의 마지막 편으로, 조회 경로를 GSI에서 인덱스 테이블로 옮긴 과정을 다룬다. 2편에서 쓰기 파이프라인이 완성돼 WriteThrottleEvents가 월 110만 회에서 0회가 됐지만, 조회 코드가 여전히 GSI를 읽고 있어 GSI를 지울 수 없었다. GSI가 읽기에서 대신해주던 키 해석·정확 일치·삭제 필터·페이지 채우기를 사내 공통 라이브러리의 조회 규칙으로 흡수해, 호출부는 GSI든 인덱스 테이블이든 같은 코드를 쓰게 만들었다.

핵심 포인트
  • 조회가 GSI를 읽는 한 GSI는 삭제할 수 없고, GSI가 남으면 핫 파티션 위험도 남는다. 조회 전환이 마이그레이션의 마지막 조각이었다.
  • GSI를 쿼리하는 세 곳의 요구가 존재 확인, 전량 페이지네이션, 최신순 목록으로 전부 달라 조회처마다 구현하면 같은 함정을 세 번 밟게 된다고 판단했다.
  • 인덱스를 GSI와 테이블 위의 공통 개념으로 올리고, 모델 어노테이션의 type 파라미터 한 줄을 GSI/TABLE 스위치로 삼았다.
  • 가장 큰 함정은 DynamoDB의 Limit이 FilterExpression보다 먼저 적용된다는 점이다. tombstone이 섞이면 페이지가 덜 찬 채 돌아와 존재 확인 로직이 오판한다.
  • 조회의 종료 조건은 "응답이 비었다"가 아니라 "LastEvaluatedKey가 없다"이며, 커서는 존재가 아니라 전진 여부를 확인해야 무한 루프를 막는다.
  • 첫 전환에서 함정을 규칙에 흡수해둔 덕에 다음 GSI 분리는 어노테이션 타입 한 줄 변경으로 끝난다.
상세 정리
  • 조회처 조사: managed GSI를 쿼리하는 애플리케이션 서버 코드는 세 곳이었고 접근 패턴이 전부 달랐다. 하나는 한 건이라도 있는지, 하나는 빠짐없이 전부, 하나는 최신순 정렬과 커서 호환을 요구했다.
  • 부수 발견: 존재 확인 로직이 1,000건짜리 페이지를 읽고 첫 한 건만 확인하고 있어, 전환하면서 limit=1로 바꿔 읽기 비용을 줄였다.
  • 라이브러리 설계: Index 타입이 GSI면 기존대로 GSI를 쿼리하고 TABLE이면 인덱스 테이블을 쿼리한다. 같은 쿼리 진입점, 합성 정렬 키 조립, EQ의 begins_with 변환, soft-delete 자동 필터를 라이브러리 뒤로 숨겼다.
  • 키 모양 차이: GSI는 PK=ID, SK=managedKey(Number)였지만 인덱스 테이블은 SK가 "managedKey#userId" 형태의 String이다. GSI는 (PK, SK) 중복을 허용하지만 테이블 primary key는 유니크해야 해서 userId를 붙였다.
  • 문자열 범위 비교: managedKey가 밀리초 epoch라 13자리 고정 길이여서 자릿수가 같은 숫자 문자열은 사전순과 숫자순 결과가 같다. 그래서 sortLessThan에 숫자를 문자열로 넘겨도 GSI 시절과 같은 범위 쿼리가 성립한다.
  • 다만 이 성질은 암묵적 전제다. epoch가 14자리가 되는 시점은 멀지만, 자릿수가 변하는 값을 합성 키에 넣는 순간 전제가 깨진다. 규칙을 한곳에 모아 테스트로 지키는 이유다.
  • 정확 일치 변환: SK 뒤에 #userId가 붙어 equals로는 잡히지 않는다. 키 빌더가 EQ 조건을 받으면 값 뒤에 구분자 #를 붙여 begins_with로 바꾸고, 값 자체의 #는 ##로 이스케이프한다.
  • 구분자를 안 붙이면 begins_with("100")이 1000, 1001까지 잡는다. GSI 시절 DynamoDB가 타입 차원에서 보장하던 정확 일치가 합성 키 위에서는 이스케이프와 구분자라는 약속으로 바뀌었다.
  • soft-delete 처리: 파이프라인이 REMOVE 이벤트에 물리 삭제 대신 _deleted=true 마킹을 하고 TTL이 정리하는 구조라, Index가 TABLE 타입이면 라이브러리가 _deleted FilterExpression을 자동으로 끼워 넣는다.
  • limit과 필터의 순서: limit=5로 쿼리하면 DynamoDB가 5건을 읽고 RCU도 5건만큼 소모한 뒤 필터를 적용한다. 그중 2건이 tombstone이면 3건만 반환된다. 목록 화면이면 항목 수가 들쑥날쑥한 정도지만 has-any 로직에서는 limit=1로 읽은 한 건이 tombstone일 때 0건 응답을 "대상 없음"으로 오판한다.
  • remaining 루프: 남은 건수를 기준으로 limit을 채우거나 데이터가 진짜 끝날 때까지 페이지를 순회한다. tombstone 구간에서는 아이템 0건에 LastEvaluatedKey만 있는 응답도 정상이므로 종료 조건은 LastEvaluatedKey의 부재여야 한다.
  • tombstone은 필터로 제외돼도 RCU는 소모한다. TTL 정리로 시간이 지나면 수렴하지만, 물리 삭제를 tombstone으로 바꾼 대가를 조회가 나눠 내는 셈이다.
  • 커서 신뢰 문제: LastEvaluatedKey는 Map 구조인데 조회 경로가 서비스 경계를 넘으며 JSON→Base64 커서 문자열로 HTTP를 타게 됐다. 디코딩한 startKey는 허용 필드 화이트리스트(ID, SK)로 검증하고 모르는 필드나 잘못된 타입이면 400으로 거절한다.
  • 실전 버그 1: 응답의 next 커서가 요청 커서 since와 같은 값으로 돌아와, 커서 존재만 보고 다음 페이지를 요청하면 같은 페이지를 무한히 다시 읽었다. 커서가 전진하지 않으면 순회를 끝내도록 고쳤다.
  • 실전 버그 2: 커서 생성 시 SK, 파싱 시 sk로 필드명 대소문자가 어긋난 직렬화 버그가 있었다. 증상이 "두 번째 페이지부터 이상하다"로 나타나 첫 페이지만 보는 테스트로는 안 잡혔다. 이후 limit=1, 빈 채널, 마지막 페이지 직후, 커서 재사용 같은 경계 케이스를 회귀 테스트로 고정했다.
  • 롤아웃 4단계: 사내 채널 하나에만 분기를 넣어 새 경로를 태우고, 같은 조건에서 두 경로의 응답·정렬 순서·페이지 경계·soft-delete 제외 여부를 비교 검증한 뒤, 전 채널로 확장하며 분기 상수와 GSI 쿼리 코드를 제거하고, 조회 트래픽이 0으로 수렴한 것을 확인한 다음 인프라에서 GSI를 물리 삭제했다.
  • 남은 일: 캐시 서버 우회를 걷어내고 애플리케이션 서버가 인덱스 테이블을 직접 쿼리하도록 되돌리는 작업이 남았다. 임시 코드는 지워질 때까지가 마이그레이션이라는 표현으로 정리했다.
왜 읽나DynamoDB GSI를 별도 테이블로 분리하거나 합성 정렬 키·tombstone·커서 페이지네이션을 직접 구현해야 하는 백엔드 엔지니어에게 실패 사례가 붙은 체크리스트가 된다.
channel-talk
채널톡 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. DB / 데이터·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    분석가가 직접 만든 마케팅 자동화 시스템 — 오디언스 빌더 제작기

    여기어때 데이터 분석팀(UB팀)이 개발 경험 없이 AI 에이전트 바이브 코딩으로 마케팅 자동화 시스템 '오디언스 빌더'를 구축한 과정이다. SQL 없이 UI에서 타겟 조건을 설정하면 BigQuery에서 수천만 행 대상자를 추출해 Kafka·S3를 통해 마케팅 도구로 전달하는 파이프라인을 직접 만들어, 캠페인 준비 시간을 2~3일에서 10분으로 단축했다.

    요약 이어보기
    #data-pipeline#kafka#postgres+2
  2. DB / 데이터·vercel-blogVercel Blog·

    모든 Vercel 빌드를 지탱하는 데이터베이스 마이그레이션 이야기

    Vercel이 빌드 웜 풀(warm pool) 상태 데이터를 Redis에서 DynamoDB로 마이그레이션한 과정을 다룬다. 빌드 풀의 청구 매핑 데이터가 Redis 장애 시 복구 불가능하다는 구조적 위험이 이주를 결정한 핵심 이유였고, 24/7 프로덕션 트래픽 속에서 다운타임 없이 5단계 점진적 전환을 완료했다.

    요약 이어보기
    #database-migration#zero-downtime#redis+2
  3. DB / 데이터·AWS KoreaAWS Korea Tech·

    Amazon Aurora 및 Amazon RDS의 PostgreSQL 18: 보안, 모니터링 및 개발자 기능 향상

    Amazon Aurora 및 Amazon RDS에서 지원하는 PostgreSQL 18의 주요 개선 사항을 보안·모니터링·논리적 복제·개발자 기능 네 가지 영역으로 정리한다. MD5 패스워드 인증 폐기 예고부터 `uuidv7()` 네이티브 함수 지원까지, DBA와 개발자 모두에게 실질적 영향을 주는 변경점을 AWS Aurora/RDS 컨텍스트에서 설명한다.

    요약 이어보기
    #postgres#aurora#aws-rds+2