중급4분 분량

DynamoDB 조건 표현식

조건 표현식은 DynamoDB가 쓰기를 커밋하기 전에 기존 아이템에 대해 평가하는 술어입니다. 술어가 거짓이면 쓰기가 거부되고 아무것도 바뀌지 않습니다. DynamoDB가 쓰기에 대해 갖는 WHERE 절에 가장 가까운 것이며 — 불변식을 강제하는 유일하게 안전한 방법입니다.

DynamoDB 조건 표현식은 어떻게 동작하나요?

조건 표현식은 DynamoDB가 쓰기를 커밋하기 전에 현재 아이템에 대해 서버 측에서 평가하는 술어입니다. 참이면 쓰기가 진행되고, 거짓이면 쓰기가 ConditionalCheckFailedException으로 거부되며 아무것도 바뀌지 않습니다. 검사와 변경을 하나의 원자적 연산으로 접어 넣으므로, 동시 호출자가 묵은 읽기를 두고 경쟁할 수 없습니다.

  • 필터가 아니라 가드입니다. ConditionExpression은 현재 아이템에 대해 서버 측에서 실행되고, 거짓 결과는 쓰기를 ConditionalCheckFailedException으로 실패시킵니다.
  • 읽기-후-쓰기를 대체합니다. SELECT 다음 UPDATE의 왕복이 없습니다 — 검사와 변경이 하나의 원자적 연산이라, 두 호출자가 경쟁할 수 없습니다.
  • 거부는 공짜지만 실행은 공짜가 아닙니다. 실패한 조건부 쓰기도 여전히 쓰기 용량을 소비합니다. 그 보장은 그것이 막는 쓰기와 같은 값을 치릅니다.

SQL에서 왔다면 행을 읽고, 앱 코드에서 검사한 뒤, 갱신했을 겁니다. DynamoDB에서는 읽기와 쓰기 사이의 그 간극이, 동시 호출자를 기다리는 데이터 손상 버그입니다. 조건 표현식이 그 간극을 메웁니다.

어디에 적용되나

ConditionExpressionPutItem, UpdateItem, DeleteItem, 그리고 TransactWriteItems 안의 각 동작에 붙입니다. QueryScan의 일부는 아닙니다 — 그것들은 읽기 경로에서 다른 것인 FilterExpression을 씁니다.

그 구분에서 사람들이 헷갈리니, 정확히 합시다:

ConditionExpressionFilterExpression
경로쓰기(Put/Update/Delete)읽기(Query/Scan)
실패 시 효과쓰기 전체를 거부결과에서 아이템을 떨어뜨림
보는 것현재 아이템, 쓰기 전각 후보 아이템, 읽은 후
비용실패한 쓰기도 과금필터된 아이템도 여전히 읽기로 과금됨

둘 다 서버 측에서 실행됩니다. 차이는 "거짓"이 하는 일입니다. 조건은 변경을 중단시키고, 필터는 이미 읽는 데 값을 치른 행을 그냥 숨깁니다. (AWS: 조건 표현식)

실제로 쓸 함수들

조건 언어는 작습니다. 일꾼들:

  • attribute_exists(path) / attribute_not_exists(path) — 이 속성이 아이템에 존재하나? "없을 때만 생성" / "있을 때만 갱신"의 고전적 관용구.
  • 비교자 — =, <>, <, <=, >, >= — 값이나 다른 속성에 대해.
  • attribute_type, begins_with, contains, size — 타입과 문자열/집합 검사.
  • BETWEEN … AND …, IN (…) — 범위와 멤버십.
  • AND, OR, NOT, 괄호 — 위를 조합하기 위해.

파티션 키에 대한 attribute_not_existsPutItem이 기존 아이템을 덮어쓰지 않는 삽입처럼 동작하게 하는 정석입니다 — DynamoDB에는 별도의 "삽입" 연산이 없으므로, 조건 삽입 의미론입니다. (AWS: 비교 연산자 및 함수 참조)

실전 예제: 초과 인출로부터 원장 지키기

은행 원장을 봅시다. 각 계정은 하나의 아이템입니다:

PK = "ACCT#a7f3"
SK = "BALANCE"
clearedCents = 50000
holdCents    = 0

불변식: 출금은 결코 가용 잔액을 0 아래로 밀어선 안 되고, 존재하지 않는 계정을 결코 출금해선 안 됩니다. 두 규칙 모두 쓰기 자체에서 강제할 수 있습니다.

잘못된 방법 (함정)

GetItem ACCT#a7f3 / BALANCE     → clearedCents = 50000
if (50000 >= 30000) ...         ← 앱 측 검사
UpdateItem  SET clearedCents = 20000

GetItemUpdateItem 사이에서, 두 번째 출금이 같은 50000을 읽고, 자체 검사를 통과해, 또 쓸 수 있습니다. 둘 다 성공하고, 계정은 음수가 됩니다. 이건 읽기-수정-쓰기 경쟁이고, 아무리 앱 측 검증을 해도 고쳐지지 않습니다 — 검사와 쓰기가 별개의 연산이니까요.

올바른 방법

검사를 쓰기에 접어 넣으세요. 계정이 존재 하고 충분히 갖고 있다는 조건 아래 30000센트를 출금합니다:

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = clearedCents - :amt
  ConditionExpression:
    attribute_exists(PK) AND clearedCents >= :amt

:amt = 30000으로. 잔액이 너무 낮거나 아이템이 애초에 생성된 적 없으면, DynamoDB는 쓰기를 ConditionalCheckFailedException으로 거부하고 잔액은 그대로입니다. 동시 출금은 원래 잔액을 보고 그것에 대해 검사받거나, 갱신된 잔액을 보거나 — 행동의 근거로 삼은 묵은 읽기는 결코 없습니다.

ExpressionAttributeValues 맵을 손으로 조립하는 대신, DynamoDB 표현식 빌더로 정확한 표현식 — 이름, 값 전부 — 을 만들고 복사할 수 있습니다.

DynoTable에서 가드 살펴보기

조건부 쓰기가 실패하면, 추측이 아니라 아이템의 진짜 상태를 보고 싶어집니다. 계정 아이템을 끌어올려 clearedCents를 직접 읽으세요.

DynoTable의 원장 컬렉션 — BALANCE 아이템이 계정의 트랜잭션 아이템 위에 clearedCents를 보여주는 모습.
DynoTable의 원장 컬렉션 — BALANCE 아이템이 계정의 트랜잭션 아이템 위에 clearedCents를 보여주는 모습.

거부를 읽으세요, 무작정 재시도하지 말고

ConditionalCheckFailedException은 일시적 오류가 아닙니다 — 같은 쓰기를 재시도해도 아무것도 바뀌지 않습니다. 비즈니스 규칙이 발화했다는 뜻입니다: 잔액 부족, 중복 생성, 묵은 버전. 인프라 잡음이 아니라 도메인 결과로 표면화하세요.

실패를 디버깅 가능하게 만드는 두 가지:

  • ReturnValuesOnConditionCheckFailure: ALL_OLD — DynamoDB가 실패와 함께 현재 아이템을 돌려주므로, 두 번째 읽기 없이 "잔액은 20000이었고 30000을 요청했다"를 보일 수 있습니다. (AWS: 아이템 다루기)
  • 두 실패 이유 구별하기. attribute_exists(PK) AND clearedCents >= :amt는 "계정 없음"과 "자금 없음"을 하나의 예외로 합칩니다. 호출자가 둘을 구별해야 한다면 두 쓰기로 나누거나 돌려받은 아이템을 검사하세요.

낙관적 락도 같은 수법입니다

버전 번호 패턴은 다른 모자를 쓴 조건 표현식일 뿐입니다. version 속성을 저장하고, 모든 쓰기가 읽은 버전을 단언하며 그것을 올립니다:

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = :new, version = :next
  ConditionExpression: version = :seen

다른 쓰기가 먼저 움직였다면 version = :seen은 거짓이고, 쓰기가 거부되고, 여러분은 다시 읽고 재시도합니다. 이것이 DynamoDB가 락 없이 동시성 제어를 하는 방법입니다 — 본 것을 단언하고, 움직였으면 실패하기. (AWS: 버전 번호를 이용한 낙관적 락)

함정과 다음 단계

  • 예약어와 충돌하는 이름. status, size, name, 그리고 약 570개가 예약어입니다. ExpressionAttributeNames(#s = status)로 별칭을 주지 않으면 표현식이 조용히 파싱에 실패합니다.
  • 조건은 다른 아이템을 참조할 수 없습니다. 쓰이고 있는 아이템만 봅니다. 아이템 간 불변식에는 액션별 ConditionExpression을 단 TransactWriteItems, 또는 센티넬 아이템에 대한 ConditionCheck가 필요합니다.
  • 실패한 쓰기도 여전히 WCU가 듭니다. 90% 거부하는 가드도 그 거부에 과금합니다. 저렴한 보험이지만, 공짜는 아닙니다.

이 가드들이 실행되는 키를 모델링하려면 싱글 테이블 디자인Query vs Scan을 보세요. 실제 데이터에 조건부 쓰기를 낼 준비가 되면, DynoTable을 다운로드해 여러분 테이블에 실행하세요.

업데이트됨