Python(boto3)의 DynamoDB TransactWriteItems

트랜잭션은 boto3의 두 API가 가장 크게 갈라지는 지점 중 하나입니다. transact_write_items는 저수준 클라이언트에만 존재하므로, Table에서 얻던 네이티브 Python 타입의 편의는 여기서 쓸 수 없습니다. 그리고 트랜잭션이 실패했을 때 필요한 것은 대부분의 boto3 코드가 들여다보지 않는 예외의 한구석에 있습니다. (트랜잭션이 무엇을 사 주는지는 어느 SDK에서나 같습니다.)

코드

import boto3

client = boto3.client("dynamodb")

# Move one award between two songs — atomically. If the first song has no
# award to give, NEITHER update happens.
try:
    client.transact_write_items(
        TransactItems=[
            {
                "Update": {
                    "TableName": "Music",
                    "Key": {"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "Cubano Chant"}},
                    "UpdateExpression": "SET #upd0 = #upd0 - :one",
                    "ConditionExpression": "#upd0 >= :one",
                    "ExpressionAttributeNames": {"#upd0": "Awards"},
                    "ExpressionAttributeValues": {":one": {"N": "1"}},
                }
            },
            {
                "Update": {
                    "TableName": "Music",
                    "Key": {"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "A Mis Abuelos"}},
                    "UpdateExpression": "SET #upd0 = if_not_exists(#upd0, :zero) + :one",
                    "ExpressionAttributeNames": {"#upd0": "Awards"},
                    "ExpressionAttributeValues": {":one": {"N": "1"}, ":zero": {"N": "0"}},
                }
            },
        ]
    )
    print("Transaction committed")
except client.exceptions.TransactionCanceledException as e:
    # One reason per action, in TransactItems order. Code "None" means that
    # action was fine — some OTHER action sank the transaction.
    codes = [reason["Code"] for reason in e.response["CancellationReasons"]]
    print(f"Transaction canceled: {codes}")  # e.g. ['ConditionalCheckFailed', 'None']

설명

  • TransactItemsPut, Update, Delete, ConditionCheck 딕셔너리의 목록이며, 모든 값이 예외 없이 DynamoDB JSON입니다. 타입이 지정된 형태가 선택 사항이 아닌 유일한 boto3 호출이고, 이 페이지 끝의 절이 존재하는 이유이기도 합니다. 상한은 CLI 페이지에 있습니다.
  • CancellationReasonsError 안에 있지 않습니다. botocore는 모델링된 오류 필드를 응답 딕셔너리의 최상위로 끌어올리므로, 잡힌 예외의 e.response에는 CancellationReasons, Error, Message, ResponseMetadata가 나란히 담깁니다. e.response["Error"] 아래에서 찾으면 아무것도 없으며, e.response["Error"]에는 요약 코드와 메시지만 들어 있습니다.
  • None 항목에는 "Message"가 없습니다 — 성공한 작업의 사유는 키가 하나뿐인 딕셔너리 {"Code": "None"}이므로, 자연스러워 보이는 [r["Message"] for r in reasons]는 하필 잘 동작한 작업에서 KeyError: 'Message'를 일으킵니다. r.get("Message")를 쓰세요.
  • 생성된 예외 클래스 — botocore는 client.exceptions.TransactionCanceledException을 런타임에 서비스 모델로부터 만들어 냅니다. 그래서 이 클래스가 클라이언트 인스턴스에 달려 있고, from botocore.exceptions import ...로 가져올 수 없습니다. 클라이언트가 스코프에 없는 헬퍼에서는 botocore.exceptions.ClientError를 잡고 e.response["Error"]["Code"]로 분기하세요. 생성된 클래스는 그 하위 클래스입니다.
  • 구조적 실수는 취소로 도착하지 않으므로, 위 코드의 except 절은 그것들을 결코 보지 못합니다. 같은 항목을 겨냥한 작업 두 개는 코드가 ValidationException인 맨 ClientError를 일으키며, 그 e.response에는 CancellationReasons 키가 없습니다. 어떤 작업도 실행되기 전에 트랜잭션이 거부되었기 때문입니다. 그런 것들도 같은 맥락으로 로깅하고 싶다면 바깥쪽에서 ClientError를 잡으세요.
  • 작업에 붙인 ReturnValuesOnConditionCheckFailure: "ALL_OLD" 는 밀려난 항목을 해당 작업 사유의 Item 키 아래에 DynamoDB JSON으로 넣어 주므로, 이미 경합에서 진 뒤에 다시 get_item을 부를 필요가 없습니다.
  • boto3가 ClientRequestToken을 대신 채웁니다. 와이어에서 캡처해 보니, 동일한 transact_write_items 호출 두 번은 서로 다른 UUID를 달고 나갔습니다. 즉 이 토큰은 단일 호출을 덮을 뿐 직접 만든 catch-후-재시도 루프까지 덮지는 않습니다. 재시도가 프로세스보다 오래 살 수 있다면 안정적인 토큰을 직접 넘기세요.
  • TransactionConflict에는 재시도하고 ConditionalCheckFailed에는 절대 재시도하지 마세요 — 앞은 다른 누군가가 잠깐 항목을 붙들고 있었다는 뜻이고, 뒤는 전제 조건이 거짓이며 다음번에도 거짓일 것이라는 뜻입니다. 대부분의 핸들러가 갈라 두어야 할 코드는 이 둘뿐이며, 전체 집합은 TransactionCanceledException 페이지에서 해독합니다.
  • 비용 — 트랜잭션 쓰기는 같은 쓰기를 트랜잭션 밖에서 할 때의 약 두 배로 과금되며, 이는 CLI 페이지에서 측정했습니다. 항목 하나의 원자성만 필요하다면 조건부 쓰기가 절반 값에 그것을 사 줍니다.

이 호출의 리소스 API 버전은 없습니다

boto3.resource("dynamodb").Table(...)에는 transact_write_items 속성이 없습니다. resource.meta.client에만 있습니다. 그래서 Table과 네이티브 Python 타입으로 굳어진 코드베이스는 트랜잭션에서는 타입이 지정된 DynamoDB JSON으로 돌아가거나, boto3.dynamodb.types.TypeSerializer로 직접 직렬화해야 합니다:

from boto3.dynamodb.types import TypeSerializer

serialize = TypeSerializer().serialize
values = {k: serialize(v) for k, v in {":one": 1, ":zero": 0}.items()}

TypeSerializer는 리소스 API와 같은 규칙을 적용하므로, float을 거부하고 소수에는 decimal.Decimal을 요구합니다. 스크립트에 리터럴만 붙여 넣으면 될 때는 DynamoDB JSON 변환기가 브라우저에서 같은 변환을 해 줍니다. 두 형태 중 어느 것도 손으로 쓰지 않고 트랜잭션이 건드리는 항목을 편집하려면 DynoTable을 다운로드하세요.

관련 예제

참고 자료

위에 링크된 공식 AWS 문서를 기준으로 2026-07-28에 마지막으로 검증했습니다.

Console 없이 DynamoDB 작업하기

DynamoDB로는 실행할 수 없는 진짜 SQL(JOINs, GROUP BY, 집계)을 실행하는 빠른 DynamoDB 데스크톱 클라이언트. 시각적 편집과 여러분 자신의 Bedrock 키로 동작하는 AI 에이전트를 제공합니다.

30일 무료 체험, 신용카드 불필요 — 이후 기간 제한 없는 무료 요금제.