Java(AWS SDK v2)의 DynamoDB PutItem
PutItem은 항목 전체를 쓰고 같은 기본 키를 가진 기존 항목을 대체합니다(UpdateItem과 어떻게 다른지는 항목 기반 작업에서 다룹니다). AWS SDK for Java 2.x에서는 모든 속성이 타입이 지정된 AttributeValue로 PutItemRequest에 들어가는데, 빌더는 절대 유효할 수 없는 값도 만들게 놔둡니다.
코드
import java.util.HashMap;
import java.util.Map;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
public class PutItemExample {
public static void main(String[] args) {
try (DynamoDbClient ddb = DynamoDbClient.builder()
.region(Region.US_EAST_1)
.build()) {
Map<String, AttributeValue> item = new HashMap<>();
item.put("Artist", AttributeValue.builder().s("Arturo Sandoval").build());
item.put("SongTitle", AttributeValue.builder().s("Cubano Chant").build());
item.put("AlbumTitle", AttributeValue.builder().s("Danzon").build());
item.put("Year", AttributeValue.builder().n("1994").build());
item.put("Awards", AttributeValue.builder().n("0").build());
Map<String, String> names = new HashMap<>();
names.put("#cond0", "Artist");
names.put("#cond1", "SongTitle");
PutItemRequest request = PutItemRequest.builder()
.tableName("Music")
.item(item)
.conditionExpression("attribute_not_exists(#cond0) AND attribute_not_exists(#cond1)")
.expressionAttributeNames(names)
.build();
try {
ddb.putItem(request);
System.out.println("Song written");
} catch (ConditionalCheckFailedException e) {
System.out.println("A song with that key already exists — not overwritten");
}
} catch (DynamoDbException e) {
System.err.println(e.getMessage());
}
}
}설명
AttributeValue.builder().build()는 컴파일됩니다. 그리고 보낼 수 없습니다. 빌더에는 필수 필드가 없으므로, .s(...)를 빠뜨린 속성도 타입 검사를 완벽히 통과한 뒤 서비스에서 실패합니다:
DynamoDbException | ValidationException | Supplied AttributeValue is empty, must contain exactly one of the supported datatypes이는 다른 SDK에서는 아예 불가능한 실수가 Java에서만 갖는 형태입니다. Go의 types.AttributeValueMember*는 별개의 타입이라 설정하지 않고 남길 것 자체가 없습니다. 더 넓은 해결책은 "Supplied AttributeValue is empty"를 보세요.
.s(...)에 건넨 Java null은 DynamoDB NULL이 되지 않습니다. 실제로 발목을 잡는 쪽은 이 버전입니다. 값처럼 보이기 때문입니다:
AttributeValue.builder().s(customer.getNotes()).build() // getNotes() returned null생성 시점에 NullPointerException은 발생하지 않습니다. 빌더는 그저 아무것도 기록하지 않고, 요청은 똑같은 Supplied AttributeValue is empty 메시지로 실패하며, 전혀 의심하지 않던 속성을 가리킵니다. 진짜 null을 원한다면 AttributeValue.builder().nul(true).build()이고, 더 흔하게는 항목에서 아예 빼고 싶을 것입니다. 이는 nil 포인터가 NULL로 마셜링되어 조용히 속성을 만들어 내는 Go SDK와 정반대라는 점에 유의하세요. 둘 다 같은 날 같은 엔진에서 실행했습니다.
getMessage()는 서비스 메시지가 아닙니다. SDK가 자체 컨텍스트를 덧붙이므로 문자열은 이렇게 됩니다:
The conditional request failed (Service: DynamoDb, Status Code: 400, Request ID: 77a08ef1-a3a9-4f97-b309-4cb0741edd1a) (SDK Attempt Count: 1)e.awsErrorDetails().errorCode()로 매칭하고, 순수한 텍스트는 e.awsErrorDetails().errorMessage()로 읽으세요. getMessage()를 리터럴과 비교하는 코드는 재시도 한 번에 깨집니다. 시도 횟수가 바뀌기 때문입니다.
ConditionalCheckFailedException을 DynamoDbException보다 먼저 잡고, 그것이 담고 있는 것을 읽으세요. 이 예외는 DynamoDbException을 상속하므로, catch 블록 순서를 반대로 두면 구체적인 핸들러에 도달할 수 없습니다. 잡힌 객체에서 statusCode()는 400을, retryable()은 false를 반환했는데, 비즈니스 로직상의 거부에 대해서는 이것이 정직한 답입니다. 요청에 .returnValuesOnConditionCheckFailure("ALL_OLD")를 추가하면 e.item()이 쓰기를 막은 항목을 채워서 돌려주므로(위 실행에서는 속성 다섯 개, Year는 AttributeValue(N=1994)), 누가 이겼는지 알아내려고 뒤이어 getItem을 부를 필요가 없습니다.
숫자는 .n(...)을 통해 문자열로 들어갑니다. DynamoDB의 N 타입은 와이어에서 10진수 텍스트이며, 그 덕분에 1994가 double로 변하지 않습니다. .n(String.valueOf(year))이 관용구이고, 손을 뻗을 .n(int) 오버로드는 없습니다.
클라이언트는 Closeable이면서 오래 사는 객체입니다. 위의 try-with-resources는 일회성 프로그램에는 맞지만 서비스에는 틀립니다. DynamoDbClient는 HTTP 연결 풀을 소유하고 스레드에 안전하므로, 애플리케이션당 하나만 만들어 계속 살려 두세요. 요청마다 만드는 것이 이 API에서 가장 흔한 Java 성능 버그입니다.
AttributeValue 맵보다 빈(bean)이 낫습니까? DynamoDB Enhanced Client(software.amazon.awssdk.enhanced.dynamodb)는 애노테이션이 붙은 클래스를 항목으로 곧장 매핑하므로 빈 빌더 함정이 통째로 사라집니다. 대신 시작 시 리플렉션 기반의 TableSchema.fromBean 스캔 비용이 드는데, 그것이 마음에 걸린다면 StaticTableSchema가 이를 피합니다.
시각적으로 해보기
위의 모든 실패는 타입이 지정된 값을 손으로 만드는 데서 시작합니다. 무료 DynamoDB JSON 변환기는 평범한 JSON을 받아 타입이 지정된 형태로 돌려주므로, AttributeValue.builder()를 한 줄도 쓰기 전에 항목이 와이어에서 정확히 어떤 모습이어야 하는지 볼 수 있습니다.
자신의 테이블에 항목을 쓰고 편집하려면 — 속성별 폼, 타입 선택기, 결과를 Java 코드로 다시 복사하기까지 — DynoTable을 다운로드하세요.
관련 예제
- Go의 DynamoDB PutItem — AWS SDK for Go v2로 하는 같은 조건부 쓰기, nil 포인터가 정반대로 실패하는 쪽.
- Java의 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
- DynamoDbClient — AWS SDK for Java 2.x API Reference
- AttributeValue — AWS SDK for Java 2.x API Reference
- PutItemRequest — AWS SDK for Java 2.x API Reference
- Condition expressions — Amazon DynamoDB Developer Guide
2026-07-28에 OpenJDK 26.0.1 위의 AWS SDK for Java 2.49.4로 포트 9000의 DynamoDB Local(amazon/dynamodb-local)에 대해 재현했습니다. 위의 예외 텍스트, 상태 코드, 항목 내용은 캡처한 출력을 그대로 옮긴 것입니다.