🔥 DynamoDB란 무엇인가: 키-값 스토어의 관점
강의 목차

처음 RDS for MySQL 인스턴스에 사용자 테이블을 하나 만들었다. 한 달 뒤 ALTER TABLE users ADD COLUMN ...를 실행했더니 테이블에 행이 천만 개를 넘는 순간 ALTER가 락을 잡고 오래 멈췄다. RDS가 MySQL 설치와 백업, 패치, failover는 대신하지만 스키마를 바꾸는 비용은 내가 그대로 감당한다는 걸 그때 알았다.
비슷한 시기에 사이드 프로젝트 하나에서 RDS 대신 DynamoDB를 붙였다. 같은 사용자 테이블에 컬럼을 하나 더 넣을 때도 DynamoDB에서는 새 attribute 이름을 다음 PutItem부터 같이 쓰면 끝이었다. 기존 행에는 그 attribute가 없는 상태로 남고 마이그레이션 락도 없었다. 대신 'X 사용자가 어제 가입한 사람들 중 이메일 도메인이 gmail인 사람' 같은 질문은 SQL 한 줄로 바로 답하지 못했다. 운영 비용이 한쪽에서 다른 쪽으로 옮겨간 셈이다.
먼저 볼 것은 세 가지다. DynamoDB가 어떤 서비스인지, 왜 이런 설계를 택했는지, 그리고 어떤 워크로드에 맞는지다.
DynamoDB는 키-값과 문서를 다루는 NoSQL이다
DynamoDB는 AWS가 2012년 1월 18일 출시한 fully managed NoSQL 데이터베이스다. AWS 공식 문서는 DynamoDB를 'key-value and document database'로 설명한다. 가장 흔한 사용 방식은 키 하나로 항목 하나를 가져오는 키-값 스토어다. 여기에 attribute 안에 list와 map을 중첩해 문서처럼 저장할 수도 있다.
출발점은 논문 한 편이다. Werner Vogels는 자기 블로그에서 그 논문을 따로 짚었다. 2007년 10월 ACM SOSP에서 발표한 'Dynamo: Amazon's Highly Available Key-value Store' 논문이다. 2007년의 Amazon은 검색 카탈로그, 장바구니 같은 내부 워크로드를 RDBMS로 굴리다 한계를 만났다. RDBMS의 ACID 보장과 SQL 표현력은 강력하지만, 키 한 개로 항목 하나를 빠르게 가져오는 단순한 워크로드에서는 그 보장의 비용이 너무 컸다. 그래서 SQL을 빼고, 조인을 빼고, 강한 일관성 대신 최종적 일관성(eventual consistency)을 default로 두고, 클러스터를 수평 확장하는 데 모든 자원을 쓰는 별도 시스템을 만들었다. 그게 Dynamo다. DynamoDB는 그 Dynamo가 보여 준 설계 원칙 위에서 외부 API와 관리형 인프라를 얹어 다시 내놓은 서비스다.
여기서 '키-값'이라는 형용사가 일을 한다. RDBMS는 데이터의 관계를 우선 모델링하고, 쿼리는 그 관계 위에서 자유롭게 조립한다. DynamoDB는 그 반대다. 미리 정해 둔 키 하나로 항목 하나를 가져오는 동작을 가장 빠르게 만드는 데에 모든 설계가 맞춰져 있다. 그래서 SQL이 아니라 GetItem, PutItem, Query, Scan 같은 좁은 API 집합으로 데이터에 접근한다. 각 API는 table 이름과 키, 그리고 condition expression 같은 구조화된 입력을 받는다.

인스턴스 대신 데이터 분산을 관리한다
RDS가 자동으로 굴리는 일곱 가지, provisioning, patching, backup, failure detection, recovery, monitoring, scaling은 지난 섹션의 첫 글에서 정리했다. DynamoDB는 그 자동화 범위를 인스턴스 관리보다 더 아래 단계까지 넓힌다.
RDS에서는 인스턴스 타입을 내가 고른다. db.t4g.micro인지 db.r6g.4xlarge인지, vCPU와 메모리가 몇인지 정한 뒤 그 위에서 DB 엔진을 돌린다. 트래픽이 늘면 인스턴스를 더 큰 타입으로 바꾸거나(scale up) 읽기 replica를 추가하는(scale out) 일도 내가 맡는다. DynamoDB에는 그 단계가 없다. 인스턴스 타입을 고르는 화면이 없고, vCPU 같은 단위도 청구서에 보이지 않는다. 처리량은 Provisioned 모드의 read/write capacity units나 On-Demand 모드의 read/write request units로 계산하고, 스토리지는 쓴 만큼 계속 늘어난다. 두 모드의 차이와 청구는 On-Demand vs Provisioned: 가격 모델에서 자세히 본다.
DynamoDB 내부에서는 인스턴스 대신 데이터 분산 구조가 그 역할을 맡는다. 한 테이블은 여러 partition으로 나뉜다. 각 partition은 SSD에 저장하고, AWS는 같은 region 안 여러 가용영역에 그 데이터를 자동으로 복제해 둔다. 클라이언트 요청은 AWS Database Blog가 'request router'라고 부르는 라우팅 계층이 받고, partition key 범위가 어느 storage node에 있는지 metadata subsystem에서 찾은 뒤 그 노드로 보낸다. 더 깊은 내부 구조는 일부 자료에 단편적으로 공개돼 있다. 여기서는 사용자가 직접 만지는 부분까지만 본다.

사용자는 이 구조를 직접 보지 않는다. 내가 만지는 것은 테이블 이름, partition key 이름, 그리고 GetItem / Query API다. RDS에서는 OS와 DB 엔진을 직접 다루지 않아도 되지만, DynamoDB에서는 인스턴스 개념 자체를 아예 의식하지 않는다. 대신 데이터는 partition key 기준으로 고르게 나눠야 한다. 운영 부담이 인스턴스 관리에서 데이터 설계로 옮겨간다는 말은 이 뜻이다.

설계 순서가 RDBMS와 다르다
RDBMS와 DynamoDB는 설계 순서가 반대다.
RDBMS에서는 도메인의 관계를 먼저 모델링한다. 사용자, 주문, 상품, 결제 같은 대상을 정규화 규칙에 따라 테이블로 나누고 외래키로 연결한다. 어떤 쿼리가 올라올지는 나중에 정해도 된다. SQL의 표현력이 넓어서 새 요구는 대체로 SELECT 한 줄이나 인덱스 추가로 대응할 수 있다.
DynamoDB는 반대 순서로 설계한다. 먼저 어떤 access pattern이 들어올지를 적고, 그 access pattern을 가장 빠르게 처리하는 키 구조를 만든다. GetItem은 키 하나로 항목 하나를 읽고, Query는 partition key를 고정한 채 sort key 범위를 읽는다. secondary index는 다른 조회 경로를 추가할 때 쓴다. 이 범위를 벗어난 ad-hoc 조회는 결국 Scan이 되기 쉽고, 비용도 크고 속도도 느리다.

이 차이는 access pattern이 분명한 워크로드에서는 장점이 된다. '사용자 ID로 그 사용자의 최근 주문 50건 가져오기'가 매 초 수만 번 들어오면 DynamoDB는 partition key 하나로 바로 처리할 수 있다. 그래서 카트, 세션, 게임 상태, IoT 디바이스 상태처럼 키 한 개로 나뉘는 워크로드에서 평균 응답이 단일 디지트 ms로 잘 나오는 경우가 많다. 반대로 access pattern이 자주 바뀌는 워크로드에서는 약점이 된다. 처음 나온 질문에 바로 답하기 어렵고, 별도 색인을 만들거나 데이터를 다시 적재하거나 OpenSearch / Athena / Redshift 같은 다른 도구로 넘겨야 할 수 있다.
'단일 디지트 ms 지연'이 뜻하는 것
DynamoDB 마케팅 문구에서 자주 보이는 '단일 디지트 millisecond 지연'은 서비스 내부 지표를 말한다.
CloudWatch에는 SuccessfulRequestLatency라는 메트릭이 있다. GetItem이나 Query 같은 singleton 동작이 DynamoDB 서비스 안에서 처리된 시간을 ms 단위로 기록한다. AWS 공식 문서는 대부분의 singleton 동작에서 평균 SuccessfulRequestLatency가 한 자릿수 ms 수준으로 나온다고 설명한다. 이 표현은 평균 기준이다. p99 같은 꼬리 지연까지 한 자릿수 ms라고 보장하는 말은 아니다.
중요한 점은 이 숫자가 서버 측에서 측정한 시간이라는 사실이다. 공식 문서는 클라이언트 측 동작과 네트워크 왕복 시간은 이 메트릭에 포함되지 않는다고 적는다. 그래서 'DynamoDB가 단일 디지트 ms'라는 말과 '내 애플리케이션 응답 시간이 단일 디지트 ms'라는 말은 다르다. 전자는 서비스 내부 처리 시간을 가리킨다. 후자는 클라이언트와 DynamoDB 사이 거리와 네트워크 왕복 시간까지 포함한다. 같은 region 안 EC2에서 붙을 때와 다른 region에서 붙을 때의 차이는 직접 측정해야 한다.

DAX(DynamoDB Accelerator)라는 in-memory 캐시 옵션이 있다. AWS 공식 DAX 문서는 캐시 히트에서 microsecond 단위 응답이 가능하다고 설명한다. 다만 DAX는 VPC 안에서 별도 클러스터로 운영해야 한다. 여기서는 이름과 역할만 기억하면 충분하다.
항목에는 크기 제약이 있다
DynamoDB의 한 항목(item)이 가질 수 있는 최대 크기는 400 KB다. 400 KB는 모든 attribute의 이름과 값을 합산한 항목 크기 기준이다. 정확한 계산은 'DynamoDB Item sizes and formats' 문서에 적혀 있고, 대체로 attribute 이름의 UTF-8 byte 길이와 값의 binary 길이의 합으로 보면 큰 어긋남은 없다. 키 자체에도 별도 제약이 있다. partition key의 attribute 값은 최대 2,048 bytes, sort key의 attribute 값은 최대 1,024 bytes다. 모두 공식 문서 'Constraints in Amazon DynamoDB'에 같은 숫자가 적혀 있다.
400 KB라는 숫자가 의도적으로 작다. 한 항목에 1MB짜리 이미지 파일이나 긴 로그 본문을 통째로 넣으려 하면 여기서 한도와 부딪힌다. AWS 공식 가이드 'Best practices for storing large items and attributes in DynamoDB'는 큰 객체는 S3에 두고 DynamoDB에는 그 S3 객체의 식별자만 적어 두라고 권한다. 이건 단순히 비용 절감의 문제가 아니다. DynamoDB는 한 항목을 400 KB 안에 묶어 두는 전제 위에서 그 응답 시간을 보장한다. 한 항목이 수 MB로 커지면 single-digit ms로 가져올 수 없다. 그래서 한도가 있다.
내 워크로드에서 한 항목이 50 KB 정도라면 400 KB 한도까지 8배 여유가 있다. 한 항목이 250 KB라면 여유는 1.6배다. 이 계산은 attribute를 계속 붙이는 방식이 언제 한도와 충돌하는지 보여 준다. 한 사용자의 활동 로그를 하나의 item에 계속 누적하면 결국 400 KB를 넘긴다. 그 경우에는 partition key는 유지하고 sort key로 여러 item으로 나누는 설계가 필요하다.

DynamoDB가 잘 맞지 않는 경우
DynamoDB는 이 데이터에 어떤 access pattern이 들어올지 미리 정할 수 있는 워크로드에 잘 맞는다. 반대로 처음 본 질문에 ad-hoc으로 답해야 하는 워크로드, 임의 컬럼 조합으로 조인이 필요한 워크로드, 분석 쿼리가 주된 사용 패턴인 워크로드에는 잘 맞지 않는다. 처음 access pattern을 못 정하면 DynamoDB를 고르기 어렵다.
DynamoDB를 볼 때 먼저 붙잡을 것
DynamoDB를 이해할 때는 세 가지만 먼저 붙잡으면 된다. 키-값 중심 모델이라는 점, 인스턴스 대신 partition 단위 분산 구조를 쓴다는 점, 그리고 access pattern을 미리 정해야 한다는 점이다. 이 세 가지를 잡아 두면 partition key와 sort key를 설계할 때 왜 데이터보다 access pattern을 먼저 정해야 하는지 이해하기 쉽다.









