중급4분 분량

DynamoDB의 표현식 속성 이름과 값

DynamoDB 표현식은 템플릿입니다. 플레이스홀더를 쓰고, 실제 속성 이름과 값을 두 개의 별도 맵에 공급하죠. #name은 이름 플레이스홀더이고, :value는 값 플레이스홀더입니다. 둘을 헷갈리면 DynamoDB가 호출 전체를 거부합니다.

DynamoDB에서 #name과 :value의 차이는 무엇인가요?

#name은 속성 이름의 플레이스홀더로, ExpressionAttributeNames를 통해 공급합니다. :value는 속성 값의 플레이스홀더로, ExpressionAttributeValues를 통해 공급합니다. #name은 예약어나 점, 공백을 피하는 데 쓰고, :value는 모든 리터럴에 씁니다 — DynamoDB는 결코 값을 인라인하지 않습니다. 둘은 교환할 수 없으며, 서로 바꿔 쓰면 ValidationException이 발생합니다.

  • #name은 속성 이름을 치환합니다 ExpressionAttributeNames를 통해 — 속성이 예약어와 충돌하거나 점/공백을 담을 때마다 쓰세요.
  • :value는 값을 치환합니다 ExpressionAttributeValues를 통해 — DynamoDB는 결코 리터럴을 표현식 텍스트에 인라인하지 않으므로, 모든 값이 플레이스홀더입니다.
  • 둘은 교환 불가입니다. :가 와야 할 곳의 #는 조용한 no-op이 아니라 ValidationException입니다.

SQL에서 왔다면 둘 다 인라인합니다 — WHERE status = 'published'. DynamoDB는 둘 다 인라인하지 않습니다. 그 분리가 모든 신참을 걸어 넘어뜨리는 것입니다.

두 맵이 존재하는 이유

SQL에서는 쿼리 문자열이 모든 걸 실어 나릅니다. 컬럼 이름, 리터럴, 연산자. DynamoDB는 표현식의 모양 을 그 데이터 에서 의도적으로 분리합니다.

값은 자체 맵에 들어가, DynamoDB가 각각을 타이핑(S, N, BOOL, …)할 수 있고 파서가 문자열이 어디서 끝나는지 추측할 필요가 없게 합니다 — 잘못 잡을 따옴표나 이스케이프가 없습니다. 전체 타입 태그 목록은 DynamoDB의 데이터 타입을 보세요.

이름은 다른 이유로 같은 대우를 받습니다. DynamoDB에는 긴 예약어 목록이 있고, 그중 하나와 일치하는 속성은 표현식에 맨 이름으로 나타날 수 없습니다. 플레이스홀더가 그 예약을 통째로 비껴갑니다.

예약어 함정

CMS 기사 테이블 — 파티션 키 BLOG#<blog>, 정렬 키 ARTICLE#<slug> — 입니다. 그 속성들은 자연스럽게 읽히지만 공교롭게 예약어와 충돌합니다:

속성예약?담는 것
statusdraft / published
name작성자 표시 이름
size렌더링된 바이트 길이
ttl보관 만료(epoch)
slug아니오URL slug

status, name, size, ttl은 모두 AWS의 예약어 목록에 있으므로, 이 필터는 첫 단어에서 실패합니다:

FilterExpression  status = :s

DynamoDB는 ValidationException을 돌려줍니다 — "Attribute name is a reserved keyword; reserved keyword: status". 해결은 속성 이름을 바꾸는 게 결코 아니라 이름 플레이스홀더입니다:

FilterExpression           #status = :s
ExpressionAttributeNames   { "#status": "status" }
ExpressionAttributeValues  { ":s": { "S": "published" } }

함정: slug는 예약어가 아니므로, slug로 테스트한 쿼리는 작동하고, 다음 것도 그럴 거라 가정합니다. 그러다 status가 그것을 망가뜨립니다. 전체 목록은 움직이니 외우지 마세요 — 모든 이름을 플레이스홀더로 하면 결코 물리지 않습니다.

값은 항상 매핑하세요

값은 협상 불가입니다. 인라인 리터럴을 위한 문법이 없습니다. 맨 숫자조차 플레이스홀더를 받습니다. 이 업데이트는 기사를 발행으로 표시하고, 크기를 찍고, 30일 보관 TTL을 설정합니다:

UpdateExpression:          SET #status = :s, #size = :sz, #ttl = :exp
ExpressionAttributeNames:  { "#status": "status", "#size": "size", "#ttl": "ttl" }
ExpressionAttributeValues: {
  ":s":   { "S": "published" },
  ":sz":  { "N": "20480" },
  ":exp": { "N": "1719792000" }
}

:sz:expN 문자열로 보내지는 데 주목하세요 — DynamoDB의 숫자 타입은 선에서 문자열로 인코딩됩니다. 값 맵은 절들 사이에서 값을 재사용하는 곳이기도 합니다. :s를 한 번 정의하고, ConditionExpressionFilterExpression 양쪽에서 참조하세요.

이 두 맵을 손으로 짓는 곳이 오타가 숨는 곳입니다. 표현식 빌더는 표현식 문자열과 두 맵을, 타입 태그를 채운 채로 함께 생성하므로, 플레이스홀더가 서로 어긋날 수 없습니다.

중첩되고 까다로운 경로의 이름

# 플레이스홀더는 예약어를 비껴가는 것 이상을 합니다. 문서 경로 문법은 점과 대괄호를 쓰므로, 점을 문자 그대로 담는 속성 — 예컨대 메타데이터 키 og.title — 은 플레이스홀더 없이는 주소 지정이 불가능합니다:

ProjectionExpression       #og
ExpressionAttributeNames   { "#og": "og.title" }

그것 없이는 DynamoDB가 og.title을 "og 맵 안의 title 필드"로 읽습니다 — 완전히 다른 것이죠. 공백이나 선행 숫자가 있는 이름도 같은 이야기입니다. 중첩에는 각 세그먼트를 플레이스홀더로 합니다: #meta#author를 둘 다 정의한 #meta.#author.

이름 대 값, 나란히

#name:value
치환속성 **이름속성 값**
ExpressionAttributeNamesExpressionAttributeValues
접두사#:
필요한 경우예약어, 점, 공백항상 — 인라인 리터럴 없음
잘못 쓰면 오류ValidationExceptionValidationException

값이 이름으로 타이핑되면, DynamoDB는 published라는 속성을 찾아 헤매고 여러분의 조건은 의도한 대로 결코 일치하지 않을 겁니다 — 그래서 API가 조용히 대신 큰 소리로 실패합니다. 그 엄격함은 기능입니다. 조용한 오답이 없습니다.

함정과 다음 단계

  • 쓰지 않는 플레이스홀더 선언하기 — DynamoDB는 어느 맵에서든 쓰이지 않는 항목을 거부합니다. 맵을 표현식에서 짓되, 미리 짓지 마세요.
  • 표현식을 편집한 뒤 :v 재사용하기 — 절을 빼면 그 값이 남아, 쓰이지 않는 항목 오류를 유발할 수 있습니다. 빌더가 그것들을 발맞춰 유지합니다.
  • 한 번 작동했으니 이름이 안전하다고 가정하기 — 예약어 충돌은 속성별입니다. 한결같이 플레이스홀더로 하고 추측을 멈추세요.

이 맵들은 모든 쓰기 경로에 나타나므로, 싱글 테이블 디자인, 그리고 필터를 붙이기 전에 언제 Query하고 Scan할지 아는 것과 자연스럽게 짝지어집니다.

표현식 빌더로 표현식과 두 맵을 함께 생성한 뒤, DynoTable을 써보세요. 여러분 테이블에 실행하고 플레이스홀더가 해석되는 걸 지켜보세요.

업데이트됨