Go의 DynamoDB PutItem (AWS SDK v2)
PutItem은 항목 전체를 쓰고 동일한 기본 키를 가진 기존 항목을 대체합니다(항목 기반 작업에서 이것이 UpdateItem과 어떻게 다른지 다룹니다). AWS SDK for Go v2에서 흥미로운 부분은 호출 자체가 아니라, 여러분의 Go 값이 나가는 길에 무엇으로 바뀌는가입니다.
코드
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
func main() {
ctx := context.TODO()
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("us-east-1"))
if err != nil {
log.Fatalf("load config: %v", err)
}
client := dynamodb.NewFromConfig(cfg)
_, err = client.PutItem(ctx, &dynamodb.PutItemInput{
TableName: aws.String("Music"),
Item: map[string]types.AttributeValue{
"Artist": &types.AttributeValueMemberS{Value: "Arturo Sandoval"},
"SongTitle": &types.AttributeValueMemberS{Value: "Cubano Chant"},
"AlbumTitle": &types.AttributeValueMemberS{Value: "Danzon"},
"Year": &types.AttributeValueMemberN{Value: "1994"},
"Awards": &types.AttributeValueMemberN{Value: "0"},
},
ConditionExpression: aws.String("attribute_not_exists(#cond0) AND attribute_not_exists(#cond1)"),
ExpressionAttributeNames: map[string]string{
"#cond0": "Artist",
"#cond1": "SongTitle",
},
})
if err != nil {
var ccf *types.ConditionalCheckFailedException
if errors.As(err, &ccf) {
fmt.Println("A song with that key already exists — not overwritten")
return
}
log.Fatalf("put item: %v", err)
}
fmt.Println("Song written")
}설명
attributevalue.MarshalMap이 지름길이지만, 나름의 고집이 있습니다. 위처럼 map[string]types.AttributeValue를 손으로 만드는 대신 구조체를 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue에 넘기는 것이 일반적인 방식입니다. time.Time, 손대지 않은 string 필드, nil인 *int를 가진 구조체에 대해 실제로 만들어 낸 결과는 이렇습니다:
Artist => &types.AttributeValueMemberS{Value:"Arturo Sandoval"}
SongTitle => &types.AttributeValueMemberS{Value:"Cubano Chant"}
Released => &types.AttributeValueMemberS{Value:"1994-01-01T00:00:00Z"}
Notes => &types.AttributeValueMemberS{Value:""}
Rating => &types.AttributeValueMemberNULL{Value:true}여기서 세 가지를 챙기세요. time.Time은 Unix 숫자가 아니라 RFC 3339 문자열이 되므로, 타임스탬프를 정렬 키로 쓰면 사전순으로 정렬되며 모든 값이 0으로 자리를 채우고 동일한 시간대일 때만 제대로 동작합니다. 손대지 않은 문자열은 빠지는 대신 진짜 빈 문자열 속성이 됩니다. 그리고 nil 포인터는 NULL이 되는데, 이는 존재하는 속성입니다.
NULL 속성은 attribute_not_exists를 무력화합니다. 오후 한나절을 잡아먹는 것이 바로 이것입니다. nil인 *int에서 온 Rating을 가진 항목을 쓴 다음, 그다음 쓰기를 attribute_not_exists(Rating)으로 막아 보면 실패합니다:
ConditionalCheckFailedException: The conditional request failedDynamoDB가 맞습니다. 속성은 거기 있고, NULL을 담고 있습니다. 해결책은 구조체 태그 dynamodbav:"Rating,omitempty"이며, 필드를 null로 만드는 대신 아예 뺍니다. 같은 태그가 빈 문자열 속성도 막아 주는데, 그것이 희소 인덱스를 망가뜨리고 키 속성에서는 아예 거부되기 때문에 중요합니다:
ValidationException: One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: Artist문자열 비교가 아니라 errors.As를 쓰세요. Go SDK는 모든 서비스 오류를 smithy의 작업 오류로 감싸므로, err.Error()에서 얻는 문자열은 DynamoDB가 보낸 메시지가 아닙니다:
operation error DynamoDB: PutItem, https response error StatusCode: 400, RequestID: 6886fce0-b246-4762-8a8b-0c6dab813040, ConditionalCheckFailedException: The conditional request failederrors.As(err, &ccf)가 그것을 풀어내며 위 실행에서 true를 반환했습니다. 그 뒤 ccf.ErrorMessage()는 순수한 The conditional request failed를 줍니다. 바깥쪽 문자열에 대한 strings.Contains 검사는 오늘은 동작하지만 SDK가 래퍼 형식을 바꾸는 날 깨집니다.
타입이 지정된 오류는 여러분을 막은 항목을 실어 올 수 있습니다. 입력에 ReturnValuesOnConditionCheckFailure: types.ReturnValuesOnConditionCheckFailureAllOld를 설정하면 ccf.Item이 채워져 돌아옵니다. 위 실행에서는 속성 다섯 개였고, 그중 Year는 &types.AttributeValueMemberN{Value:"1994"}였습니다. 덕분에 대부분의 재시도 루프가 손으로 하는, 실패 후 다시 읽는 왕복을 아낄 수 있습니다. 성공 경로의 대응물은 ReturnValues: types.ReturnValueAllOld입니다(ReturnValues).
숫자는 문자열이며, 이것은 Go의 별난 점이 아닙니다. AttributeValueMemberN{Value: "1994"}는 타입이 있는 언어에서 온 사람에게는 틀려 보이지만, DynamoDB의 N 타입은 float64를 거치는 왕복이 아예 필요 없도록 텍스트로 전송되는 십진수입니다. 변환은 strconv.FormatInt와 strconv.FormatFloat이 담당하고, attributevalue가 대신 해 줍니다.
마셜링한 만큼 비용을 냅니다. 약 15 KB 항목의 put은 ReturnConsumedCapacity와 함께 "CapacityUnits": 15를 보고했습니다. 쓰기는 1 KB 단위로 올림되므로, 실수로 들어간 블롭 필드나 빼려던 속성을 내보낸 MarshalMap은 곧바로 청구서에 나타납니다.
시각적으로 해보기
MarshalMap이 항목에 실제로 무엇이 들어갈지를 결정하므로, 그 항목의 무게를 아는 것이 중요합니다. 무료 DynamoDB 항목 크기 계산기는 마셜링된 JSON을 받아 바이트 크기와 그것이 올림되는 쓰기 단위를 알려 줍니다.
여러분의 테이블에 대해 항목을 쓰고 편집하려면 — 속성마다 폼, 타입 선택기, 결과를 Go 코드로 다시 복사 — DynoTable을 다운로드하세요.
관련 예제
- Java의 DynamoDB PutItem — AWS SDK for Java 2.x로 하는 동일한 조건부 쓰기이며, 설정하지 않은 값이 반대 방향으로 실패합니다.
- Go의 DynamoDB UpdateItem — 항목을 대체하는 대신 특정 속성만 변경합니다.
- DynamoDB 조건 표현식 —
attribute_not_exists, 낙관적 잠금 등. - DynamoDB ConditionalCheckFailedException — 항목이 이미 존재할 때 생성 전용 조건이 던지는 것.
- DynamoDB ValidationException — 잘못된 형식의 항목이나 표현식을 아우르는 오류.
참고 자료
- PutItem — Amazon DynamoDB API Reference
- Use PutItem with an AWS SDK or CLI — Amazon DynamoDB Developer Guide
- dynamodb package — AWS SDK for Go v2 (pkg.go.dev)
- attributevalue package — AWS SDK for Go v2 (pkg.go.dev)
- Handling errors — AWS SDK for Go v2 Developer Guide
- Condition expressions — Amazon DynamoDB Developer Guide
2026-07-28에 go1.26.5에서 aws-sdk-go-v2/service/dynamodb v1.62.1과 feature/dynamodb/attributevalue v1.20.55로, 포트 9000의 DynamoDB Local(amazon/dynamodb-local)에 대해 재현했습니다. 마셜링된 값, 오류 문자열, 용량 수치는 캡처한 출력을 그대로 옮긴 것입니다.