중급4분 분량

DynamoDB 업데이트 표현식

업데이트 표현식은 UpdateItem에게 단일 아이템을 어떻게 변경할지 알려 줍니다. 어떤 속성을 쓰고, 증가시키고, 삭제하고, 집합에 접어 넣을지를요. UPDATE … SET … WHERE는 없습니다 — 아이템을 키로 지목하고 네 개의 절 키워드로 변경을 기술합니다.

DynamoDB 업데이트 표현식은 어떻게 작동하나요?

DynamoDB 업데이트 표현식은 UpdateItem에게 네 개의 절을 사용해 아이템 하나를 어떻게 변경할지 알려 줍니다. SET은 속성을 쓰거나 덮어씁니다. ADD는 숫자를 원자적으로 증가시키거나 집합에 합집합을 수행합니다. REMOVE는 속성 하나 또는 리스트 원소 하나를 삭제합니다. DELETE는 집합에서 특정 멤버를 제거합니다. 한 번의 호출에 네 절을 모두 담을 수 있습니다.

  • SET 은 속성을 쓰거나 덮어씁니다 — 스칼라, 문서, 그리고 함수 관용구 if_not_existslist_append.
  • ADD 는 원자적 숫자 증가나 집합 합집합을, 한 번의 왕복으로, 먼저 읽지 않고 합니다.
  • REMOVE 는 속성을 완전히 삭제합니다(또는 인덱스로 리스트 원소 하나를).
  • DELETE 는 집합에서 특정 멤버를 제거합니다 — 그리고 오직 집합에서만.

SQL에서 왔다면, 함정은 모든 것에 SET을 쓰려는 것입니다. ADDDELETE가 존재하는 건, 카운터나 집합에 대한 읽기-수정-쓰기가 동시성 아래에서 질 경쟁이기 때문입니다.

무엇을 바꾸는지로 절을 고르세요

하나의 UpdateItem 호출이 네 절을 한 번에, SET … REMOVE … ADD … DELETE 순서로 실을 수 있습니다. 각 키워드는 최대 한 번 나타나고, 콤마로 구분된 동작 목록을 받습니다.

작동 대상용도
SET임의 속성값이나 문서 필드를 쓰기/덮어쓰기
ADD숫자 또는 집합만원자적으로 증가, 또는 집합에 합집합
REMOVE임의 속성 또는 리스트 원소속성 삭제; 리스트 인덱스 하나 제거
DELETE집합만집합에서 특정 멤버 제거

문자열에 대한 ADD와 스칼라에 대한 DELETE는 no-op이 아니라 유효성 검사 오류입니다 — DynamoDB가 호출 전체를 거부합니다. AWS 업데이트 표현식 참조 에 따르면, ADD는 숫자와 집합으로, DELETE는 집합으로 제한됩니다.

실전 예제: 장바구니

장바구니마다 하나의 아이템으로, CartPK = "CART#c-9f21"CartSK = "SUMMARY"로 키잉됩니다. 진행 중인 OrderTotal, LineItems 리스트, PromoCodes 문자열 집합, 그리고 ItemCount를 추적합니다.

SET — 스칼라와 문서 쓰기

SET은 거기 있던 것을 덮어씁니다. 리스트에 라인 아이템을 추가하고 같은 호출에서 합계를 올리세요:

SET OrderTotal = :total,
LineItems = list_append(LineItems, :newItem),
UpdatedAt = :now

list_append(LineItems, :newItem)은 꼬리에 덧붙입니다. 인자를 뒤집으면 — list_append(:newItem, LineItems) — 앞에 붙습니다. 인자의 순서가 곧 연결의 순서이며, 그 이상은 아닙니다.

그 첫 호출에 함정이 있습니다. 장바구니가 갓 새것이면 LineItems가 아직 없고, 없는 속성에 대한 list_append는 실패합니다. if_not_exists로 가드하세요:

SET LineItems = list_append(if_not_exists(LineItems, :empty), :newItem)

if_not_exists(LineItems, :empty)는 리스트가 있으면 현재 리스트를, 없으면 폴백 :empty(빈 리스트 [])를 돌려줍니다. 그래서 첫 추가와 이후 모든 추가가 같은 표현식을 쓰게 됩니다 — 이 관용구들이 존재하는 진짜 이유죠.

ADD — 카운트를 원자적으로 증가

ItemCount를 올리려면, 그것을 읽고 코드에서 1을 더해 SET으로 되쓰지 마세요. 그건 잃어버린 갱신 경쟁입니다. 두 동시 추가가 둘 다 3을 읽고, 둘 다 4를 써서, 하나를 떨어뜨립니다. ADD는 산술을 서버 측에서 합니다:

ADD ItemCount :one

:one = 1로, 이것은 원자적 카운터입니다. 동시 호출은 아이템에서 직렬화되므로, 두 추가가 +2로 안착합니다. 음수를 넘기면 감소합니다. ItemCount가 없으면 ADD는 먼저 0으로 취급하므로 — 카운터를 미리 심을 필요가 전혀 없습니다.

DynamoDB 표현식 빌더에서 단 하나의 #name이나 :value 플레이스홀더도 손으로 이스케이프하지 않고 이 정확한 표현식 — 이름, 타입이 지정된 값, 그리고 marshalled 요청 — 을 만들 수 있습니다.

REMOVE — 속성이나 라인 아이템 하나 떨어뜨리기

REMOVE는 속성을 통째로 삭제하는 방법입니다("null로 설정"은 없습니다 — 그건 그냥 NULL 타입을 쓰는 거죠). 적용된 할인을 지우고 세 번째 라인 아이템을 한 호출에서 떨어뜨리세요:

REMOVE AppliedDiscount, LineItems[2]

LineItems[2]는 인덱스 2의 원소를 제거하고 그 뒤의 모든 것을 한 칸씩 당깁니다 — 인덱스 3이 2가 되는 식이죠. 한 표현식에서 두 인덱스를 REMOVE하면, 둘 다 원래 리스트에 대해 평가되므로, [2][3]을 함께 제거하면 기대대로 세 번째와 네 번째 원소가 떨어집니다.

DELETE — 집합 멤버 제거

PromoCodes는 문자열 집합이므로, 고객이 코드 하나를 빼는 건 REMOVE가 아니라 DELETE를 씁니다. REMOVE PromoCodes는 집합 전체를 날립니다. DELETE는 지목된 멤버를 뺍니다:

DELETE PromoCodes :pulled

:pulled = 집합 {"SAVE10"}으로, 그 멤버만 사라집니다. 여기서 두 규칙이 발목을 잡습니다. 집합은 결코 비어 있을 수 없으므로 마지막 멤버를 삭제하면 PromoCodes 속성이 통째로 제거됩니다. 그리고 값은 속성과 일치하는 집합 타입이어야 합니다 — 맨 문자열은 타입 오류입니다.

한데 모으기

"아이템 추가, 프로모 적용, 카운트 증가" 업데이트는 세 절에 걸친 하나의 호출입니다:

SET LineItems = list_append(if_not_exists(LineItems, :empty), :newItem),
OrderTotal = OrderTotal + :price
ADD ItemCount :one
DELETE PromoCodes :expiredCode

OrderTotal = OrderTotal + :price에 주목하세요 — SET 안의 산술은 기존 값에 대해 작동합니다. ADD처럼 경쟁 안전하게 원자적이진 않지만, 현재 합계를 코드로 왕복시키는 대신 서버 측에서 읽습니다.

피해야 할 함정

  • 먼저 읽은 카운터를 SET하기. ADD를 쓰세요 — 읽기-수정-쓰기는 동시성 아래에서 갱신을 잃습니다. 가장 흔한 장바구니/재고 버그입니다.
  • 없는 리스트에 list_append. 대상을 if_not_exists로 감싸지 않으면 첫 쓰기가 실패합니다.
  • REMOVEDELETE 혼동. REMOVE는 속성을 떨어뜨리고, DELETE는 집합에서 멤버를 뺍니다. 섞으면 의도보다 많이 삭제됩니다.
  • UpdateItem이 upsert라는 걸 잊기. 키가 없으면 아이템을 생성합니다. "갱신만"을 뜻한다면 ConditionExpression(attribute_exists(CartPK))을 쓰세요.

이 표현식들이 실행되는 키를 모델링하려면 싱글 테이블 디자인을, 장바구니를 어떻게 다시 읽을지 결정하려면 query vs scan을 보세요.

표현식 빌더에서 이 중 무엇이든 만들고 복사한 뒤, DynoTable을 써보세요. 여러분 테이블에 실행하고 아이템이 라이브로 바뀌는 걸 지켜보세요.

업데이트됨