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> — 입니다. 그
속성들은 자연스럽게 읽히지만 공교롭게 예약어와 충돌합니다:
| 속성 | 예약? | 담는 것 |
|---|---|---|
status | 예 | draft / 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와 :exp가 N 문자열로 보내지는 데 주목하세요 — DynamoDB의 숫자 타입은
선에서 문자열로 인코딩됩니다. 값 맵은 절들 사이에서 값을 재사용하는 곳이기도
합니다. :s를 한 번 정의하고, ConditionExpression과 FilterExpression 양쪽에서
참조하세요.
이 두 맵을 손으로 짓는 곳이 오타가 숨는 곳입니다. 표현식 빌더는 표현식 문자열과 두 맵을, 타입 태그를 채운 채로 함께 생성하므로, 플레이스홀더가 서로 어긋날 수 없습니다.
중첩되고 까다로운 경로의 이름
# 플레이스홀더는 예약어를 비껴가는 것 이상을 합니다. 문서 경로 문법은 점과 대괄호를
쓰므로, 점을 문자 그대로 담는 속성 — 예컨대 메타데이터 키 og.title — 은
플레이스홀더 없이는 주소 지정이 불가능합니다:
ProjectionExpression #og
ExpressionAttributeNames { "#og": "og.title" }
그것 없이는 DynamoDB가 og.title을 "og 맵 안의 title 필드"로 읽습니다 — 완전히
다른 것이죠. 공백이나 선행 숫자가 있는 이름도 같은 이야기입니다. 중첩에는 각
세그먼트를 플레이스홀더로 합니다: #meta와 #author를 둘 다 정의한 #meta.#author.
이름 대 값, 나란히
#name | :value | |
|---|---|---|
| 치환 | 속성 **이름 | 속성 값** |
| 맵 | ExpressionAttributeNames | ExpressionAttributeValues |
| 접두사 | # | : |
| 필요한 경우 | 예약어, 점, 공백 | 항상 — 인라인 리터럴 없음 |
| 잘못 쓰면 오류 | ValidationException | ValidationException |
값이 이름으로 타이핑되면, DynamoDB는 published라는 속성을 찾아 헤매고 여러분의
조건은 의도한 대로 결코 일치하지 않을 겁니다 — 그래서 API가 조용히 대신 큰 소리로
실패합니다. 그 엄격함은 기능입니다. 조용한 오답이 없습니다.
함정과 다음 단계
- 쓰지 않는 플레이스홀더 선언하기 — DynamoDB는 어느 맵에서든 쓰이지 않는 항목을 거부합니다. 맵을 표현식에서 짓되, 미리 짓지 마세요.
- 표현식을 편집한 뒤
:v재사용하기 — 절을 빼면 그 값이 남아, 쓰이지 않는 항목 오류를 유발할 수 있습니다. 빌더가 그것들을 발맞춰 유지합니다. - 한 번 작동했으니 이름이 안전하다고 가정하기 — 예약어 충돌은 속성별입니다. 한결같이 플레이스홀더로 하고 추측을 멈추세요.
이 맵들은 모든 쓰기 경로에 나타나므로, 싱글 테이블 디자인, 그리고 필터를 붙이기 전에 언제 Query하고 Scan할지 아는 것과 자연스럽게 짝지어집니다.
표현식 빌더로 표현식과 두 맵을 함께 생성한 뒤, DynoTable을 써보세요. 여러분 테이블에 실행하고 플레이스홀더가 해석되는 걸 지켜보세요.