프렌들리AI, 업스테이지, 채널지금 글로벌 AI 최전선 이야기가 궁금하다면?대표가 말하는 글로벌 AI의 최전선자세히 보기

DynamoDB 핫 파티션을 해결하는 3가지 방법 (4): 쓰기 샤딩

단건 조회를 유지한 쓰기 샤딩과 약 20억 건의 단계적 마이그레이션

Jayon • Jinyoung Park, Backend Enginner

  • Backend

안녕하세요, 채널톡 백엔드 엔지니어 제이온입니다.

지난 1~3편에서는 User 테이블의 managed GSI(Global Secondary Index)에서 발생한 핫 파티션(Hot Partition)을 다뤘습니다. 이를 해결하기 위해 GSI를 별도의 인덱스 테이블로 분리했죠.

앞선 managed 전환에서는 약 22억 건을 인덱스 테이블로 옮기고, 쓰기 전파 파이프라인과 공통 조회 규칙을 만들었습니다. 3편에서는 기존 GSI까지 제거하면서 인덱스 테이블 분리를 마무리했습니다.

1편에서 쓰기 샤딩도 후보로 검토했지만, managed는 목록 조회가 중심이라 16개 샤드의 결과를 다시 합치고 정렬해야 했습니다. 당시에는 적용하지 않았고, 대신 단건 조회가 중심인 다른 GSI라면 좋은 선택이 될 수 있다고 남겨두었습니다.

User 테이블의 member GSI가 바로 그런 경우였습니다.

member GSI는 channelId를 파티션 키로 사용하고 있었는데요, 사용자가 채널톡을 시작할 때 발생하는 초기화 요청인 Boot가 특정 채널에 몰리면 모든 쓰기가 같은 파티션 키로 향했습니다.

한 번의 Boot가 member GSI에 1 WCU(Write Capacity Unit)의 쓰기 한 건을 만든다고 가정하면, 한 채널에서 이론상 처리할 수 있는 Boot 요청이 초당 약 1,000건에 머문다는 뜻이었습니다.

여기서 쓰로틀링이 발생하면 Back-Pressure로 User 테이블 쓰기까지 거부되고, 최종적으로 고객사 사이트에서 채널톡이 정상적으로 시작되지 않을 수 있었습니다.

“단건 조회는 그대로 유지하면서 쓰기만 여러 파티션으로 나눌 수 없을까요?”

저희는 memberId로 suffix를 계산하고 channelId와 조합해 하나의 파티션 키를 16개로 분산하는 쓰기 샤딩 방식을 적용했습니다.

같은 memberId에서는 항상 같은 suffix가 나오므로, 저장과 조회의 키 계산 규칙이 일치하는 정상 상태에서는 Query 한 번으로 조회할 수 있었습니다.

이 글에서는 쓰기 샤딩의 동작 원리와 사내 공통 프레임워크를 설계한 이유, User 테이블에 적용한 과정을 공유합니다.

User 쓰기와 member 조회를 유지한 채 약 20억 건을 백필하면서 만난 해시 호환성 문제와 롤링 핫 파티션(Rolling Hot Partition)도 함께 다룹니다.

마지막 편인 만큼 DynamoDB 핫 파티션을 해결하는 세 가지 해결 방법이 각각 어떤 조건에 맞는지도 정리했으니 꼭 끝까지 정독해주시면 감사하겠습니다!


member GSI의 Hot Partition

먼저 member GSI가 어떤 인덱스인지부터 살펴보겠습니다.

User 테이블의 아이템은 userId를 기준으로 저장되지만, 특정 채널의 특정 멤버를 찾을 때는 channelIdmemberId가 필요합니다. member GSI가 이 조회를 담당하고 있었습니다.

구분

Partition Key

Sort Key

User 테이블

id (= userId)

-

member GSI

channelId

memberId

평상시에는 단순한 구조입니다. 문제는 고객사가 선착순 이벤트나 대규모 프로모션을 진행해 짧은 시간에 많은 사용자가 채널톡을 시작할 때 발생했습니다.

각 사용자의 userId가 다르므로 User 테이블 쓰기는 여러 파티션 키로 흩어집니다.

반면 같은 고객사에서 발생한 쓰기는 member GSI에서 모두 같은 channelId로 모입니다. 메인 테이블에서는 분산된 트래픽이 GSI에서 다시 하나로 합쳐지는 셈이죠.

이때 member GSI가 쓰로틀링되면 해당 인덱스 쓰기만 늦어지는 것으로 끝나지 않습니다. DynamoDB의 Back-Pressure가 User 테이블 쓰기까지 거부하고, 최종적으로 사용자의 Boot 요청이 실패할 수 있습니다.

위 그림의 빨간 흐름이 이번에 문제가 되었던 경로입니다.

Boot 트래픽 집중 → GSI 핫 파티션 → GSI 쓰로틀링 → Back-Pressure → User 쓰기 거부가 하나의 장애로 이어지고 있었습니다.

channelId에 집중된 Boot 트래픽과 1,000 WCU의 한계

DynamoDB의 물리 파티션 하나가 처리할 수 있는 쓰기는 초당 최대 1,000 WCU입니다.

테이블 전체에 이보다 훨씬 많은 쓰기 용량이 남아 있어도, 지금 요청을 받은 파티션이 한계에 도달하면 해당 쓰기는 쓰로틀링됩니다.

한 번의 Boot가 member GSI에 1 WCU의 쓰기 한 건을 만든다고 가정해보겠습니다.

그러면 한 채널이 이론상 처리할 수 있는 Boot는 초당 약 1,000건에서 먼저 막힙니다. User 테이블 전체 WCU를 늘리는 것만으로는 이 한계를 넘을 수 없었습니다.

더 큰 문제는 고객사 하나의 트래픽이 다른 고객사의 User 쓰기에도 영향을 줄 수 있다는 점이었습니다.

특정 channelId에 집중된 부하를 여러 파티션 키로 흩뜨리지 않는 한, 같은 문제가 반복될 수 있는 구조였습니다.


쓰기 샤딩이란?

쓰기 샤딩(Write Sharding)은 하나의 파티션 키에 suffix를 붙여 여러 개의 파티션 키로 나누는 방식입니다.

channel-A에 모든 쓰기를 보내는 대신 channel-A-0, channel-A-1, …, channel-A-f 중 하나로 보내는 식입니다.

위 그림처럼 논리적으로는 여전히 같은 채널의 데이터지만, DynamoDB가 여러 물리 파티션으로 나눠 저장할 수 있는 키 공간이 생깁니다.

별도의 테이블이나 쓰기 전파 파이프라인 없이 트래픽이 집중되는 키 자체를 분산한다는 점이 가장 큰 장점입니다.

Partition Key + Shard Suffix를 통한 쓰기 분산

샤딩된 키는 기준이 되는 파티션 키와 shard suffix를 조합해 만듭니다.

Plaintext
shardedKey = partitionKey + "-" + shardSuffix

channel-A + 7  → channel-A-7
channel-A + f  → channel-A-f

여기서 중요한 결정은 suffix를 어떻게 선택할 것인가입니다.

무작위로 고르면 쓰기를 단순하게 분산할 수 있지만, 나중에 특정 아이템이 어느 샤드에 있는지 다시 계산할 수 없습니다. 반대로 입력값으로 suffix를 계산하면 저장할 때와 조회할 때 같은 위치를 재현할 수 있습니다.

쓰기 샤딩의 비용은 주로 읽기에서 드러납니다.

조회할 샤드를 알 수 없다면 모든 샤드를 조회하고 결과를 합쳐야 하기 때문입니다. 따라서 샤드 수보다 먼저 확인해야 할 것은 조회 시점에 대상 샤드를 계산할 수 있는가였습니다.

결정적 샤딩과 비결정적 샤딩

저희는 suffix를 선택하는 방식을 크게 결정적 샤딩과 비결정적 샤딩으로 나눴습니다.

구분

suffix 선택

조회 입력으로 위치 재현

Deterministic

입력값으로 계산

가능

Non-Deterministic

Random 또는 Round-Robin

불가능, 위치를 별도로 저장하지 않으면 전체 샤드 조회

결정적 샤딩(Deterministic Sharding)은 입력값이 같으면 항상 같은 suffix를 반환합니다.

저장할 때 사용한 값을 조회 시점에도 알고 있다면, 특정 아이템이 들어 있는 샤드를 바로 계산할 수 있습니다. 기존 데이터에서 키를 다시 계산해야 하는 백필에도 유리합니다.

비결정적 샤딩(Non-Deterministic Sharding)은 저장할 때마다 Random 또는 Round-Robin으로 suffix를 고릅니다.

입력 데이터가 편향되어 있어도 쓰기를 고르게 나누기 쉽지만, 위치를 별도로 저장하지 않으면 조회 입력만으로 특정 아이템의 샤드를 다시 계산할 수 없습니다.

비결정적 샤딩이 항상 불리한 것은 아닙니다.

쓰기 비중이 높고 조회가 언제나 전체 샤드를 대상으로 한다면 오히려 더 단순한 선택이 될 수 있습니다. 저희는 member GSI에서 특정 아이템의 위치를 다시 계산해야 했기 때문에 결정적 샤딩을 선택했습니다.

단건 조회 vs Scatter-Gather

쓰기 샤딩을 검토할 때는 샤드 선택 방식과 조회 방식을 섞어서 생각하면 안 됩니다.

결정적 샤딩을 사용하더라도 한 그룹 전체를 읽으려면 모든 샤드를 조회해야 할 수 있습니다.

예를 들어 지난 1편에서 다룬 managed GSI는 한 채널의 사용자를 최근 접속순으로 나열하는 조회가 중심 워크로드였습니다. managed처럼 channelId만으로 User 목록을 조회할 때는 memberId를 미리 알 수 없어 대상 샤드를 하나로 계산할 수 없습니다.

따라서 16개 샤드를 모두 조회한 뒤 결과를 합쳐 정렬해야 합니다. 다음 페이지를 조회할 때도 샤드마다 생성되는 커서를 함께 관리해야 합니다.

반면 member GSI 조회에는 처음부터 channelIdmemberId가 모두 들어옵니다.

memberId로 suffix를 계산한 뒤 channelId와 조합하면 channel-A-7처럼 대상 파티션 키 하나를 바로 만들 수 있습니다. 저장과 조회의 키 계산 규칙이 일치하면 샤드가 16개여도 Query는 한 번이면 충분했습니다.

같은 쓰기 샤딩도 조회 패턴에 따라 비용이 완전히 달라지는 이유입니다.

이 조회 패턴의 차이가 저희가 managed에는 인덱스 테이블 분리를, member에는 쓰기 샤딩을 선택한 결정적인 이유였습니다.

왼쪽의 Query 한 번은 member처럼 조회 입력으로 위치를 재현할 수 있는 단건 조회 기준입니다.

반면, 오른쪽처럼 결정적 샤딩도 한 그룹 전체를 읽는다면 Scatter-Gather가 필요합니다.


채널톡만의 쓰기 샤딩 프레임워크 설계

쓰기 샤딩 자체는 키 뒤에 suffix를 붙이는 단순한 패턴입니다. 하지만 구현을 시작해보니 같은 계산이 저장 코드, 조회 코드, 백필, 운영 도구에 반복되고 있었습니다.

처음에는 모두 같은 수식처럼 보여도 시간이 지나면 조금씩 달라질 수 있습니다.

한쪽만 샤드 수가 바뀌거나, 해시 함수와 구분자 규칙이 달라지거나, 일부 저장 경로에서 새 키를 빠뜨릴 수 있죠.

이때 가장 위험한 문제는 데이터는 저장됐지만 조회 코드가 다른 샤드를 찾는 상태입니다. 그래서 저희는 특정 DynamoDB 테이블에 종속되지 않는 공통 쓰기 샤딩 프레임워크를 만들었습니다.

샤딩 전략의 공통 인터페이스

프레임워크는 모든 전략이 제공해야 하는 공통 기능과 결정적/비결정적 전략의 차이를 타입으로 구분합니다.

실제 코드에서 핵심 인터페이스만 추리면 다음과 같습니다.

Java

공통 인터페이스의 allSuffixes()allShardKeys()는 Scatter-Gather에 필요한 전체 키를 열거합니다.

결정적 전략은 입력값을 받는 shardKey()를, 비결정적 전략은 입력값 없이 suffix를 고르는 별도 shardKey()를 제공합니다. 덕분에 조회 입력만으로 suffix를 재계산해야 하는 경로에서 비결정적 전략을 잘못 호출하는 문제를 타입으로 구분할 수 있습니다.

기본 구현은 입력값을 해시하는 HashBucketStrategy, ID의 일부를 사용하는 IdSuffixStrategy, 무작위로 고르는 RandomStrategy, 순서대로 순환하는 RoundRobinStrategy 네 가지입니다.

저희는 데이터 분포와 조회 패턴에 맞춰 전략을 골랐습니다.

저장/조회/백필을 연결하는 하나의 샤딩 규칙

HashBucketStrategy는 여러 입력값을 하나의 문자열로 조합한 뒤 주입된 해시 함수를 적용합니다.

이번 member 전략에는 Murmur3를 사용했습니다. 아래 코드는 실제 구현에서 suffix 계산에 필요한 부분만 추린 것입니다.

Java

입력값을 단순히 이어 붙이면 ("abc", "123")("abc1", "23")이 같은 문자열이 될 수 있습니다.

그래서 값 사이에 :를 넣고, 값 자체에 :\가 들어오면 먼저 escape합니다.

Math.floorMod()는 해시 결과가 음수여도 항상 유효한 버킷 번호를 만들기 위해 사용합니다.

계산식을 공통 라이브러리로 옮기는 것만으로는 충분하지 않았습니다. 어떤 전략과 입력을 사용할지는 엔티티가 소유하고, 저장/조회/백필 도구가 그 규칙을 함께 바라보도록 만들었습니다.


User 테이블에 쓰기 샤딩 적용

프레임워크를 만든 뒤 User 테이블의 member GSI에 적용했습니다.

기존 인덱스의 파티션 키는 channelId, 정렬 키는 memberId였고, 신규 인덱스는 파티션 키만 memberShardedKey로 바꿨습니다.

구분

Partition Key

Sort Key

기존 member GSI

channelId

memberId

신규 member-sharded GSI

memberShardedKey

memberId

16개 샤드로 분산된 member GSI

memberShardedKeymemberId로 suffix를 계산한 뒤 channelId와 조합합니다. 애플리케이션에서 사용한 규칙은 다음과 같습니다.

Plaintext
source = escape(memberId)
suffix = floorMod(murmur3_x86_32(source, seed=0), 16)
memberShardedKey = channelId + "-" + hex(suffix)

member에는 HashBucketStrategy를 적용했습니다. 실제 엔티티 코드에서 핵심만 추리면 다음과 같습니다.

Java

첫 번째 인자인 channelId는 최종 키의 기준 키(baseKey)이고, 두 번째 인자인 memberId만 suffix를 계산할 해시 입력으로 들어갑니다.

shardKey(channelId, memberId)는 내부에서 deriveSuffix(memberId)를 호출한 뒤 두 값을 -로 조합합니다.

DAO는 저장 직전에 applyShardKeys()를 호출하고, 단건 조회도 같은 UserMeta.MEMBER_GSI_STRATEGY.shardKey(channelId, memberId)를 사용합니다.

User 테이블 엔티티는 전략과 입력을 소유하고, 공통 라이브러리는 suffix 계산과 키 조합을 담당하도록 책임을 나눴습니다.

예를 들어 같은 채널의 멤버들은 channel-A-0부터 channel-A-f까지 16개의 키로 나뉩니다.

조회할 때도 같은 channelIdmemberId로 suffix를 계산하므로, 저장된 파티션 키를 그대로 재현할 수 있습니다.

샤드 수를 늘리면 쓰기는 더 넓게 분산되지만 Scatter-Gather와 마이그레이션 비용도 함께 늘어납니다.

이번 사례에서는 16개 샤드를 사용했고, 파티션당 1,000 WCU 한계를 기준으로 쓰기를 이론상 최대 16배까지 분산할 수 있었습니다.


약 20억 건의 단계적 마이그레이션 전략

새로 생성되거나 수정되는 User에는 애플리케이션이 memberShardedKey를 기록할 수 있습니다. 하지만 member 백필 대상 User 약 20억 건에는 신규 키가 없었습니다.

설계는 끝났습니다. 이제 문제는 기존 데이터에 신규 키를 채우고, 계획된 전체 서비스 중단 없이 읽기 경로를 전환하는 것이었습니다.

저희는 빠른 완료보다 안전한 전환을 우선했습니다. User 쓰기와 member 조회를 유지하고, 문제가 생기면 기존 GSI로 돌아갈 수 있도록 마이그레이션 단계를 나눴습니다.

기본 뼈대는 이전 DynamoDB Export + AWS Glue 마이그레이션에서 검증한 패턴을 재사용했습니다.

이번에는 새 테이블을 만드는 대신 기존 User에 신규 속성을 추가한다는 차이가 있었습니다.

기존 GSI와 신규 GSI의 단계적 전환

신규 GSI를 만들기 전에 애플리케이션부터 배포했습니다. 마이그레이션 도중 생성되거나 수정되는 User에도 신규 키가 들어가야 했기 때문입니다.

전체 과정은 다음 여섯 단계로 진행했습니다.

  1. 1차 애플리케이션 배포: User를 저장할 때 memberShardedKey도 함께 기록

  2. 백필: 기존 User에 memberShardedKey 추가

  3. GSI 생성: 신규 member-sharded GSI의 빌드 완료까지 대기

  4. 검증: 기존 GSI와 신규 GSI의 조회 결과 비교

  5. 2차 애플리케이션 배포: 읽기 경로를 신규 GSI로 전환

  6. 정리: 관찰 기간이 끝난 뒤 기존 member GSI와 참조 코드 제거

애플리케이션은 GSI 두 곳에 직접 Dual Write 할 수 없습니다.

대신 하나의 User 아이템에 기존 속성과 memberShardedKey를 함께 기록했고, 신규 GSI를 만든 뒤에는 DynamoDB가 각 GSI를 갱신했습니다.

신규 GSI가 활성화된 뒤에도 바로 읽기를 바꾸지는 않았습니다. 두 GSI의 조회 결과를 비교하고, 최종적 일관성으로 인한 전파 지연을 고려해 일정 시간 뒤 다시 확인했습니다.

읽기를 전환한 뒤에도 기존 GSI는 관찰 기간 동안 롤백 경로로 남겼습니다. 안정성을 확인한 다음에야 기존 GSI를 제거했습니다.

DynamoDB Export + AWS Glue 백필

기존 User에 신규 속성을 추가하려면 테이블 전체를 읽어야 합니다. 프로덕션 User 테이블을 직접 Full Scan하면 서비스 요청과 RCU(Read Capacity Unit)를 두고 경쟁합니다.

그래서 특정 시점의 데이터를 DynamoDB Export로 S3에 내보냈습니다. AWS Glue의 Spark Job은 Export 파일을 읽고 memberShardedKey를 계산한 뒤 User 테이블에 UpdateItem을 보냈습니다.

위 그림처럼 Glue는 id, channelId, memberId가 존재하고 memberShardedKey가 없는 아이템만 처리했습니다. 실시간 애플리케이션이 먼저 신규 키를 기록한 아이템은 조건부 업데이트에서 건너뛰었습니다.

서비스 트래픽을 우선하기 위해 백필 TPS(Transactions Per Second)는 시간대별로 제한했습니다.

S3 파일 단위로 진행 상태를 저장해 중단된 작업을 이어서 처리했고, 전체 백필에는 약 2주가 걸렸습니다.

예상치 못한 Rolling Hot Partition

저희는 전체 백필 TPS를 제한하면 User 테이블도 안전할 것으로 예상했습니다. 하지만 첫 실행부터 다시 쓰로틀링이 발생했습니다.

테이블 전체 용량을 넘지 않았는데도 핫 파티션이 생긴 이유는 무엇이었을까요?

바로 DynamoDB Export 결과의 데이터 순서가 문제였습니다.

Export 파일에는 비슷한 키 범위의 아이템이 가까운 위치에 모여 있었습니다. 파일을 앞에서부터 처리하자 일정 시간 동안 특정 물리 파티션으로 쓰기가 집중되는 패턴이 나타났습니다.

처리가 진행되면서 Hot한 지점이 첫 번째 파티션에서 두 번째, 다시 세 번째 파티션으로 이동했습니다. 배치의 진행에 따라 핫 파티션이 이동하는 Rolling Hot Partition이었습니다.

그림의 P1~P4는 현상을 설명하기 위한 개념적인 파티션 그룹입니다. DynamoDB는 실제 물리 파티션 ID를 노출하지 않습니다.

파일의 진행에 따라 쓰로틀링 지점이 이동했고, 같은 TPS에서 처리 순서를 섞은 뒤 현상이 사라졌습니다.

결국 핫 파티션을 해결하기 위한 백필이 다시 핫 파티션을 만든 셈이었습니다!

Rate Limit의 한계와 Shuffle을 통한 쓰기 분산

처음 적용한 Rate Limit은 백필의 전체 쓰기량만 제한했습니다. 같은 파티션의 아이템이 연속으로 처리되는 순서까지 바꾸지는 못했습니다.

처리 속도만 더 낮추면 쓰로틀링은 줄일 수 있었지만, 전체 백필 시간도 함께 늘어납니다. 전체 쓰기량과 파티션별 집중을 따로 제어해야 했습니다.

저희는 Glue에서 Export 레코드의 순서를 섞어 서로 다른 물리 파티션의 아이템이 같은 시간대에 처리되도록 변경했습니다. 여기에 기존 Rate Limit을 함께 적용했습니다.

Rate Limit이 얼마나 많이 쓰는가를 제어한다면, Shuffle은 어떤 순서로 쓰는가를 제어합니다. 대규모 백필에서는 두 가지를 함께 고려해야 했습니다.


성과

저희는 백필과 GSI 결과 비교를 거쳐 읽기 경로를 member-sharded GSI로 전환했습니다. 신규 GSI를 관찰한 뒤 기존 member GSI를 제거했습니다.

적용 결과는 다음과 같습니다.

구분

AS-IS

TO-BE

성격

한 채널의 Boot 처리 상한

약 1,000건/s

약 16,000건/s

아이템당 1 WCU를 가정한 이론상 값

단건 조회 Query 횟수

1회

1회

실제 조회 경로

기존 데이터 백필 대상

-

약 20억 건

실제 수행 규모

계획된 전체 서비스 중단

-

없음

실제 수행 결과

가장 중요한 변화는 한 고객사의 성장이 하나의 channelId에 묶여 서비스의 한계가 되던 구조를, 16개의 파티션 키로 분산할 수 있는 구조로 바꿨다는 점이죠.

이로써 한 고객사당 부트 트래픽을 1000/s 대비 최대 16,000/s 까지 견딜 수 있게 되었습니다!


핫 파티션을 해결하는 세 가지 방법의 선택 기준

지금까지 같은 User 테이블에서 비롯된 서로 다른 병목을 세 가지 방법으로 해결했습니다. 세 방법은 우열 관계가 아니라, 데이터의 책임과 조회/쓰기 패턴에 따라 선택해야 하는 도구였습니다.

기준

도메인 테이블 분리

인덱스 테이블 분리

쓰기 샤딩

분리 대상

데이터와 도메인 책임

GSI와 동기화 경로

파티션 키 공간

쓰기 주체

애플리케이션 비즈니스 로직

Streams/이벤트 파이프라인

애플리케이션/공통 프레임워크

잘 맞는 문제

독립적인 쓰기 패턴과 책임

GSI Back-Pressure 격리

같은 키로 집중되는 쓰기

읽기 비용

새 테이블 조회로 전환

별도 인덱스 조회

단건 계산 또는 Scatter-Gather

주요 운영 비용

도메인 경계와 쓰기 경로 변경

최종적 일관성과 파이프라인 운영

샤드 수 관리와 기존 데이터 백필

도메인 테이블 분리는 특정 데이터가 독립적인 책임과 쓰기 패턴을 가질 때 먼저 검토할 수 있습니다.

Badge를 User에서 분리한 사례가 여기에 해당하며, 자세한 과정은 메시지 트래픽 100배에도 끄떡 없게 고객 테이블 뜯어고치기에서 다뤘습니다.

인덱스 테이블 분리는 문제 범위가 GSI이고 비동기 동기화를 허용할 수 있을 때 유효합니다. 쓰로틀링을 메인 테이블에서 격리할 수 있지만, 쓰기 전파 파이프라인과 최종적 일관성을 운영해야 합니다. 이 방식은 지난 1~3편에서 구현과 전환까지 마무리했습니다.

쓰기 샤딩은 별도 테이블이나 쓰기 전파 파이프라인을 추가하지 않고, 기존 User 테이블의 저장 경로에 샤드 키 계산을 넣어 핫 키를 나눌 때 유효합니다. 다만 대상 샤드를 계산할 수 없다면 읽기 비용이 샤드 수만큼 커질 수 있으므로, 쓰기량만 보고 선택해서는 안 됩니다.


마무리

User 테이블 하나에서 시작한 이야기가 네 편까지 이어졌습니다!

처음에는 managed GSI의 핫 파티션이 Boot까지 막는 구조를 다뤘고, 이를 별도 인덱스 테이블로 옮기기까지 세 편이 필요했습니다. 마지막 글에서는 당시 다른 후보로 남겨두었던 쓰기 샤딩을 member GSI에 적용한 이야기까지 담았습니다.

지금 와서 보면 답은 간단해 보입니다. 하지만 당시에는 어떤 선택이 맞는지 하나씩 확인해야 했고, 데이터를 옮기는 과정에서도 예상하지 못한 문제를 계속 만났습니다. 그래서 이번 연재에서는 최종 결과보다 그 사이의 판단과 시행착오를 더 자세히 적었습니다.

실제 문제를 풀고 그 과정을 다시 글로 옮기다 보니 생각보다 긴 작업이 됐습니다. 네 편을 모두 쓰고 나니 조금 후련하네요 😊

DynamoDB 핫 파티션 연재는 여기서 마칩니다. 끝까지 읽어주셔서 감사합니다!


채널톡에서는 수십억 건 규모의 테이블을 직접 운영하며, 대규모 분산 시스템의 한계를 구조적으로 해결하는 경험을 쌓을 수 있습니다. 또한 감이나 관행이 아닌 데이터와 기술적 근거를 바탕으로 의사 결정을 내리고, AWS의 관리형 서비스를 적극 활용하되 한계가 보이는 지점에서는 직접 구조를 확장해 나가는 방식으로 비용 최적화와 시스템 안정성을 함께 달성하는 것을 중요하게 생각합니다.

대규모 트래픽과 복잡한 문제를 함께 고민하고 해결해 나가고 싶다면, 언제든 채널톡 엔지니어링 팀의 문을 두드려 주세요! https://channel.io/ko/careers

We Make a Future Classic Product