Python(boto3)의 DynamoDB PutItem
put_item은 항목 전체를 쓰고 같은 기본 키를 가진 기존 항목을 대체합니다(update_item과 어떻게 다른지는 항목 기반 작업에서 다룹니다). 저수준 클라이언트에서는 모든 속성이 DynamoDB JSON으로 전달되며, boto3가 무엇이든 전송하기 전에 그 형태를 로컬에서 검사합니다.
코드
import boto3
from botocore.exceptions import ClientError
client = boto3.client("dynamodb")
try:
client.put_item(
TableName="Music",
Item={"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "Cubano Chant"}, "AlbumTitle": {"S": "Danzon"}, "Year": {"N": "1994"}, "Awards": {"N": "0"}},
ConditionExpression="attribute_not_exists(#cond0) AND attribute_not_exists(#cond1)",
ExpressionAttributeNames={"#cond0": "Artist", "#cond1": "SongTitle"},
)
print("Song written")
except ClientError as err:
if err.response["Error"]["Code"] == "ConditionalCheckFailedException":
print("A song with that key already exists — not overwritten")
else:
raise설명
{"N": 1994}는 AWS에 닿지도 않으며, except ClientError는 이를 잡지 못합니다. botocore는 먼저 자체 서비스 모델로 요청을 검증하는데, N 타입이 문자열을 원하는 자리에 Python int가 오면 거기서 실패합니다:
ParamValidationError: Parameter validation failed:
Invalid type for parameter Item.Year.N, value: 1994, type: <class 'int'>, valid types: <class 'str'>ParamValidationError는 ClientError가 아니라 BotoCoreError를 상속하므로, 위 코드의 핸들러는 이를 그냥 통과시킵니다. 이것은 비즈니스 결과가 아니라 버그이니 대개는 원하는 동작이지만, 쓰기를 감싼 try/except ClientError가 만능이 아니라는 뜻이기도 합니다. 좋은 점은 오류가 Item.Year.N이라는 정확한 경로를 짚어 준다는 것이며, 디버깅에서는 서버 측 ValidationException보다 낫습니다. 자세한 내용은 "Parameter validation failed"에 있습니다.
조건 실패의 전체 표면. 같은 조건부 put을 두 번 잡아 예외의 모든 것을 출력하면 이렇게 나왔습니다:
type(e).__name__ ConditionalCheckFailedException
e.response["Error"]["Code"] ConditionalCheckFailedException
e.response["Error"]["Message"] The conditional request failed
e.response["ResponseMetadata"]["HTTPStatusCode"] 400
str(e) An error occurred (ConditionalCheckFailedException) when calling the PutItem operation: The conditional request failed두 가지가 따라옵니다. botocore 1.43.58에서 이 객체는 모델링된 하위 클래스이므로, 위 코드가 쓰는 err.response["Error"]["Code"] 검사와 나란히 except client.exceptions.ConditionalCheckFailedException도 동작합니다. 하나를 골라 일관되게 쓰세요. 그리고 str(e)는 서비스 메시지가 아니라 형식이 갖춰진 문장이므로, 절대 리터럴과 비교하지 마세요.
실패한 조건에도 쓰기 비용은 청구됩니다. AWS의 표현입니다: "if the expression evaluates to false, DynamoDB still consumes write capacity units from the table"(2026-07-28 확인). 생성 전용 재시도 루프는 거부된 시도마다 값을 치릅니다. 규모 감각을 위해 덧붙이면, 약 15 KB 항목의 성공한 put은 ReturnConsumedCapacity="TOTAL"에서 "CapacityUnits": 15를 보고했습니다. 쓰기는 읽기가 쓰는 4 KB가 아니라 1 KB 단위로 올림됩니다.
리소스 API는 다른 계약이며, float이 그 사실을 알려 주는 지점입니다. boto3.resource("dynamodb").Table("Music").put_item(Item={...})은 평범한 Python을 받아 대신 마셜링하지만, 이진 부동소수점은 대놓고 거부합니다:
TypeError: Float types are not supported. Use Decimal types instead.값을 decimal.Decimal("4.5")로 감싸되 float이 아니라 문자열에서 만드세요. 그러지 않으면 Decimal이 보기도 전에 이미 오차가 박혀 있습니다. 같은 API로 되읽으면 모든 숫자가 Decimal로 돌아오는데, 이는 표기상의 세부가 아니라 코드의 실제 변화입니다. "Float types are not supported"를 참고하세요.
두 API를 섞는 것은 어느 쪽도 경고해 주지 않는 함정입니다. 저수준 클라이언트는 리소스 API였다면 float으로 거부했을 {"N": "1.5"}도 군말 없이 받습니다. 한쪽으로 쓰고 다른 쪽으로 읽는 코드베이스는, 들어갈 때 Decimal을 거친 적 없는 데이터를 Decimal로 돌려받게 됩니다.
#cond0 별칭은 장식이 아닙니다. 이들은 ExpressionAttributeNames를 통해 Artist/SongTitle로 해석됩니다. 속성 이름을 인라인으로 쓰는 방식은 그중 하나가 예약어와 충돌하기 전까지만 통하며, 그때는 건드리지도 않은 이름 때문에 표현식이 실패합니다.
시각적으로 해보기
조건 표현식은 손으로 쓸 때 가장 먼저 어긋나는 곳입니다. 잘못된 조건은 구문 오류가 아니라 거부된 쓰기로 실패하기 때문입니다. 무료 DynamoDB Expression Builder는 이름 맵과 값 맵을 갖춘 ConditionExpression을 조립하고, 붙여 넣을 수 있는 boto3 호출을 내놓습니다.
자신의 테이블에 항목을 쓰고 편집하려면 — 속성별 폼, 타입 선택기, 결과를 boto3 코드로 다시 복사하기까지 — DynoTable을 다운로드하세요.
관련 가이드
- DynamoDB 조건 표현식 —
attribute_not_exists, 낙관적 잠금 등. - DynamoDB 데이터 타입 — 각 속성 타입이 DynamoDB JSON에서 표기되는 방식.
- DynamoDB ConditionalCheckFailedException — 항목이 이미 있을 때 생성 전용 조건이 던지는 것.
- DynamoDB ValidationException — 잘못된 항목이나 표현식 전반을 아우르는 오류.
참고 자료
- PutItem — Amazon DynamoDB API Reference
- put_item — Boto3 DynamoDB.Client Reference
- Error handling — Boto3 Developer Guide
- Capacity unit consumption — Amazon DynamoDB Developer Guide
- Condition expressions — Amazon DynamoDB Developer Guide
2026-07-28에 boto3 1.43.58 / botocore 1.43.58으로 포트 9000의 DynamoDB Local(amazon/dynamodb-local)에 대해 재현했습니다. 위의 예외 텍스트, 응답 필드, 용량 수치는 캡처한 출력을 그대로 옮긴 것입니다.