DynamoDB 원자적 카운터
원자적 카운터는 단일 UpdateItem 호출로 제자리에서 올리는 숫자 속성입니다 — 먼저
읽기도, 읽기-수정-쓰기 경쟁도 없습니다. DynamoDB는 각 증가를 도착 순서대로 적용하며, 두
작성자가 서로의 카운트를 절대 짓밟지 못하게 합니다.
DynamoDB 원자적 카운터란?
DynamoDB 원자적 카운터는 ADD(또는 SET x = x + :n) 업데이트 표현식을 사용해 단일 UpdateItem 호출로 제자리에서 올리는 숫자 속성입니다. DynamoDB가 서버 쪽에서 값을 읽고, 더하고, 쓰기 때문에 동시 작성자가 손실된 업데이트 없이 직렬화됩니다 — 다만 멱등하지 않아서, 재시도된 호출은 두 번 증가시킵니다.
ADD(또는SET x = x + :n)를 써서 한 호출로 증가시키세요. DynamoDB가 서버 쪽에서 읽고, 더하고, 씁니다 — 동시 호출자가 직렬화되어 손실된 업데이트가 없습니다.- 먼저 읽지 않습니다. SQL에서라면
SELECT후UPDATE를 하겠지만, 여기서는 읽기를 완전히 건너뛰며 동시성 아래에서도 작업이 여전히 안전합니다. - 원자적 카운터는 멱등하지 않습니다. 재시도된
UpdateItem은 다시 증가시킵니다. 과다·과소 카운트를 용납할 수 없다면 조건부 업데이트를 쓰세요. - 없는 속성에
ADD하면 0에서 시작하므로, 맨 첫 증가가 그냥 동작합니다 — 시드 쓰기가 필요 없습니다.
읽기-수정-쓰기의 문제
비디오의 조회수를 추적한다고 합시다. SQL에서 곧장 온 순진한 본능은: GetItem, 앱에서 1을
더한 뒤, 새 총합을 PutItem으로 되돌려 쓰기.
두 시청자가 동시에 재생을 누릅니다. 둘 다 views = 41을 읽습니다. 둘 다 42를 씁니다.
당신은 둘이 아니라 한 번의 조회를 센 것입니다. 그것이 손실된 업데이트입니다 — 고전적인
동시성 지뢰이며, 트래픽이 생기기 전까지는 드러나지 않습니다.
SQL에서라면 산술을 데이터베이스로 밀어 넣는 UPDATE videos SET views = views + 1로
이를 피할 것입니다. DynamoDB에도 같은 수가 있으며, 그것이 원자적 카운터의 핵심
전부입니다.
한 호출로 증가시키기
비디오별 통계 아이템을 모델링하세요. 파티션 키 VID#<id>, 정렬 키 STATS#TOTAL, 숫자
play_count와 함께:
| PK | SK | play_count |
|---|---|---|
| "VID#9f3a" | "STATS#TOTAL" | 41 |
재생을 기록하려면 ADD 절이 들어간 UpdateItem 하나를 보내세요:
# UpdateItem
Key PK = "VID#9f3a", SK = "STATS#TOTAL"
UpdateExpression ADD play_count :one
Values :one = 1
DynamoDB는 단일 서버 쪽 작업 안에서 play_count를 읽고, 1을 더하고, 결과를 씁니다.
다른 작성자가 끼어들 창이 없습니다. 열 번의 동시 재생은 매번 +10을 만들어 냅니다 —
그것이 "원자적"이 사주는 것입니다.
이 정확한 표현식 — 이름, 값, 그리고 네 가지 절 유형 전부 — 을 DynamoDB Expression Builder로 만들고 복사할 수 있습니다.
ADD는 play_count가 아직 없을 때도 동작합니다: DynamoDB는 없는 숫자 속성을 0으로
취급하므로, 첫 재생이 그것을 1로 생성합니다. 별도의 시드 쓰기가 없습니다.
(AWS: 업데이트 표현식 사용하기)
ADD vs SET +: 하나를 고르세요
두 표현식은 같은 산술을 합니다. AWS는 일반적인 용도로 SET을 권장하는데, 다른 SET
액션과 조합되고 더 명시적으로 읽히기 때문입니다. (AWS: 업데이트 표현식 사용하기)
ADD play_count :one | SET play_count = play_count + :one | |
|---|---|---|
| 없는 속성 | 생성, 0에서 시작 | 오류 — if_not_exists 필요 |
| 데이터 타입 | 숫자와 집합만 | SET을 통해 숫자(그리고 그 이상) |
SET과 결합 | 별도 절 | 콤마로 구분된 하나의 SET 절 |
| AWS 권고 | 카운터에 적합 | 권장 기본값 |
속성이 없을 수도 있는데 SET을 원한다면, 가드를 거세요:
SET play_count = if_not_exists(play_count, :zero) + :one. ADD라면 그것을
건너뜁니다 — 거저 0에서 시드합니다.
DynoTable에서 해보기
아이템을 열고 play_count를 편집하면, JSON을 손으로 쓰지 않고 원자적 증가가 떨어지는
모습을 지켜볼 수 있습니다 — 업데이트 패널이 ADD 표현식을 대신 내보내고 커밋되는 순간
새 값을 보여줍니다.
함정: 카운터는 멱등하지 않다
여기가 프로덕션에서 팀을 무는 부분입니다. 원자적 카운터는 UpdateItem이 실행될 때마다
증가합니다. (AWS: 아이템 다루기)
네트워크 깜박임을 상상해 보세요: 증가를 보내는데, 응답이 돌아오기 전에 연결이 끊기고, 그것이 떨어졌는지 알 수 없습니다. 재시도합니다. 첫 호출이 성공했다면, 이제 그 재생을 두 번 센 것입니다.
비디오 조회수에는 괜찮습니다 — 백만 재생 중 몇 번의 중복 카운트는 아무에게도 해를 끼치지 않으며, AWS는 바로 이 "방문자 추적" 케이스를 원자적 카운터의 전형적 용도로 부릅니다. (AWS: 아이템 다루기)
정확해야 하는 것에는 괜찮지 않습니다: 초과 판매할 수 있는 재고, 이중 지출할 수 있는 크레딧, 손상시킬 수 있는 잔액. 거기서는 조건부 업데이트에 손을 뻗으세요.
정확성이 필요할 때: 조건부 업데이트
조건부 업데이트는 당신이 바꾸려는 속성과 같은 속성에 조건을 걸면 멱등합니다.
play_count를 42로 증가시키되, 현재 41일 때만:
# UpdateItem
Key PK = "VID#9f3a", SK = "STATS#TOTAL"
UpdateExpression SET play_count = :next
ConditionExpression play_count = :current
Values :next = 42, :current = 41
이제 재시도가 안전합니다: 첫 쓰기가 이미 play_count를 42로 옮겼다면, 두 번째에는
조건 play_count = 41이 실패하고 아무것도 바뀌지 않습니다. (AWS: 아이템 다루기)
비용은 동시성입니다. 같은 조건에서 경쟁하는 두 작성자는 하나가 이기고 하나는 재시도할
ConditionalCheckFailedException을 받습니다 — 무조건적 카운터의 처리량을 정확성과
맞바꾼 것입니다. 정확하고 경합이 심한 카운터에는 그것이 옳은 거래입니다. 조회수에는
과합니다.
함정
- 하나의 핫 아이템. 단일 카운터 행은 하나의 파티션 키입니다.
VID#9f3a/STATS#TOTAL을 두들기는 바이럴 비디오는 파티션당 쓰기 상한에 부딪힐 수 있습니다. 샤딩하세요:STATS#TOTAL#0..N에 쓰기를 분산하고 읽을 때 합산하세요. - 배치 증가는 없습니다.
BatchWriteItem은 put/delete만 가능합니다 — 업데이트 표현식을 실행할 수 없습니다. 카운터는UpdateItem을 통해, 호출당 아이템 하나씩 처리됩니다. ADD는 숫자와 집합만입니다. 문자열이나 불리언은 건드리지 않습니다 — 그것은SET입니다. 전체 속성 모델은 DynamoDB 데이터 타입을 참고하세요.
다음 단계
원자적 카운터는 쓰기 패턴입니다. 집계를 어떻게 읽어 들이는가는 모델링 질문입니다 —
통계 아이템을 부모 옆에 두는 것에 대해서는
단일 테이블 설계를, 샤딩된 카운터를 롤업하는 것이 Query로
남도록 하는 것에 대해서는 Query vs Scan을 참고하세요.
DynamoDB Expression Builder에서 증가를 작성하고 복사한 다음, DynoTable을 사용해 자신의 테이블에 원자적 업데이트를 실행하고 카운트가 움직이는 모습을 지켜보세요.