DynamoDB Condition Expression 완전 가이드 (예제 포함)
조건 표현식은 DynamoDB가 쓰기를 커밋하기 전에 기존 아이템에 대해 평가하는 술어입니다.
술어가 거짓이면 쓰기는 거부되고 아무것도 바뀌지 않습니다. DynamoDB가 쓰기 경로에서 가지는
WHERE 절에 가장 가까운 것이며 — 불변식을 강제하는 유일하게 안전한 방법입니다.
DynamoDB 조건 표현식은 어떻게 동작하나요?
조건 표현식은 쓰기를 커밋하기 전에 DynamoDB가 서버 측에서 현재 아이템에 대해 평가하는
술어입니다. 참이면 쓰기가 진행되고, 거짓이면 쓰기가 ConditionalCheckFailedException으로
거부되며 아무것도 바뀌지 않습니다. 검사와 변경을 하나의 원자적 연산으로 접어 넣으므로, 동시에
접근하는 호출자가 묵은 읽기로 경쟁할 수 없습니다.
- 필터가 아니라 가드입니다.
ConditionExpression은 현재 아이템에 대해 서버 측에서 실행되며, 거짓이면 쓰기가ConditionalCheckFailedException으로 실패합니다. - 읽기-후-쓰기를 대체합니다.
SELECT후UPDATE로 왕복하지 않습니다 — 검사와 변경이 하나의 원자적 연산이므로 두 호출자가 경쟁할 수 없습니다. - 거부는 공짜여도, 실행은 공짜가 아닙니다. 실패한 조건부 쓰기도 여전히 쓰기 용량을 소모합니다. 거부된 쓰기는 검사 대상이 된 기존 아이템의 크기만큼 WCU를 청구합니다(최소 1) — 없을 때만 생성하는 쓰기가 실패하면 1 WCU가 듭니다.
SQL에서 넘어왔다면 행을 읽고, 앱 코드에서 검사한 뒤 갱신할 겁니다. DynamoDB에서는 읽기와 쓰기 사이의 그 틈이 동시 호출자를 기다리는 데이터 손상 버그입니다. 조건 표현식이 그 틈을 닫습니다.
어디에 적용되나
ConditionExpression은 PutItem, UpdateItem, DeleteItem, 그리고
TransactWriteItems 안의 각 액션에 붙입니다. Query나 Scan의 일부는 아닙니다 — 그것들은
읽기 경로에서 별개의 것인 FilterExpression을 씁니다.
이 구분이 사람들을 헷갈리게 하므로, 정확히 짚어봅시다:
ConditionExpression | FilterExpression | |
|---|---|---|
| 경로 | 쓰기 (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_exists는 PutItem을 기존 아이템을 덮어쓰지
않는 삽입처럼 동작하게 만드는 정석입니다 — DynamoDB에는 별도의 "insert" 연산이 없으므로 조건이
곧 삽입 의미가 됩니다.
(AWS: 비교 연산자 및 함수 참조)
실전 예제: 원장을 잔액 초과 인출로부터 지키기
은행 원장을 예로 들어봅시다. 각 계좌는 하나의 아이템입니다:
PK = "ACCT#a7f3"
SK = "BALANCE"
clearedCents = 50000
holdCents = 0불변식: 출금은 절대 사용 가능 잔액을 0 아래로 밀어서는 안 되고, 존재하지 않는 계좌에서 절대 출금해서는 안 됩니다. 두 규칙 모두 쓰기 자체에서 강제할 수 있습니다.
잘못된 방법(지뢰)
GetItem ACCT#a7f3 / BALANCE → clearedCents = 50000
if (50000 >= 30000) ... ← app-side check
UpdateItem SET clearedCents = 20000
GetItem과 UpdateItem 사이에서, 두 번째 출금이 같은 50000을 읽고 자기 검사를 통과한 뒤
써버릴 수 있습니다. 둘 다 성공하고, 계좌는 마이너스가 됩니다. 이것이 읽기-수정-쓰기 경쟁이고,
아무리 앱 측 검증을 해도 고쳐지지 않습니다 — 검사와 쓰기가 별개의 연산이기 때문입니다.
올바른 방법
검사를 쓰기 안으로 접어 넣으세요. 계좌가 존재하고 또한 충분한 잔액을 보유하는 것을 조건으로, 30000센트를 출금하세요:
UpdateItem ACCT#a7f3 / BALANCE
SET clearedCents = clearedCents - :amt
ConditionExpression:
attribute_exists(PK) AND clearedCents >= :amt여기서 :amt = 30000. 잔액이 너무 적거나 아이템이 애초에 생성된 적이 없으면, DynamoDB는 쓰기를
ConditionalCheckFailedException으로
거부하고 잔액은 그대로 유지됩니다. 동시 출금은 원래
잔액을 보고 그것에 대해 검사되거나, 갱신된 잔액을 봅니다 — 자기가 행동의 근거로 삼은 묵은 읽기는
결코 없습니다.
ExpressionAttributeValues 맵을 손으로 조립하는 대신,
DynamoDB 표현식 빌더로 정확한 표현식 — 이름, 값, 전부 —
을 만들고 복사할 수 있습니다.
바로 여기서 해보세요 — 이 빌더는 가드가 걸린 PutItem(attribute_not_exists)으로 미리
설정되어 있어, 생성된 ConditionExpression을 읽어볼 수 있습니다:
DynoTable에서 가드 들여다보기
조건부 쓰기가 실패하면, 아이템의 실제 상태를 추측하지 말고 직접 봐야 합니다. 계좌 아이템을 띄워
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: 버전 번호를 이용한 낙관적 잠금)
DynoTable의 스테이징 영역은 이 패턴을 대신 실행해 줍니다 — 동시 편집은
잃어버린 쓰기가 아니라 해결할 충돌로 나타납니다.
함정과 다음 단계
- 예약어와 충돌하는 이름.
status,size,name을 비롯해 약 570개가 예약어입니다.ExpressionAttributeNames(#s = status)로 별칭을 주지 않으면 요청이 ValidationException('Attribute name is a reserved keyword')으로 거부됩니다. 예약어 검사기는 속성 이름을 받아 바로 붙여넣을 수 있는 별칭 맵을 돌려줍니다. - 조건은 다른 아이템을 참조할 수 없습니다. 쓰이는 아이템만 봅니다. 아이템 간 불변식에는
액션마다
ConditionExpression을 붙인TransactWriteItems나, 센티널 아이템에 대한ConditionCheck가 필요합니다. - 실패한 쓰기도 WCU가 듭니다. 90%의 시간을 거부하는 가드도 그 거부들에 대해 과금됩니다. 값싼 보험이지만, 공짜는 아닙니다.
이 가드들이 실행되는 대상인 키를 모델링하는 방법은 단일 테이블 설계와 Query vs Scan을 보세요. 실제 데이터에 조건부 쓰기를 실행할 준비가 되었다면, DynoTable을 내려받아 자신의 테이블에 실행하세요.


