고급5분 분량

DynamoDB 트랜잭션

DynamoDB 트랜잭션은 여러 쓰기를 하나의 전부-아니면-전무(all-or-nothing) 작업으로 묶습니다: 모든 동작이 커밋되거나, 아무것도 커밋되지 않거나입니다. 쓰기가 절반만 실패할 때 관련된 두 항목이 어긋나지 않게 유지하는 방법입니다.

감사 로그 시나리오에서, 추가하는 모든 이벤트는 테넌트별 eventCount도 함께 올려야 합니다. 이벤트는 도착했는데 카운터는 안 되거나 — 또는 그 반대거나 — 하면, 로그와 카운트가 영원히 어긋납니다. 트랜잭션은 그것을 불가능하게 만듭니다.

DynamoDB는 트랜잭션을 지원하나요?

네. DynamoDB는 TransactWriteItemsTransactGetItems를 통해 ACID 트랜잭션을 지원합니다. 이를 통해 하나 이상의 테이블에 걸쳐 최대 100개의 작업을 하나의 전부-아니면-전무(all-or-nothing) 작업으로 묶을 수 있습니다. 모든 쓰기가 커밋되거나 아무것도 커밋되지 않으므로 관련 항목이 어긋날 수 없습니다. 트랜잭션 쓰기는 일반 쓰기보다 두 배의 용량을 소비하며, 조건 실패나 충돌이 발생하면 전체 요청이 취소됩니다.

  • TransactWriteItems는 최대 100개의 쓰기 동작을 묶습니다 — 하나 이상의 테이블에 걸쳐 전부-아니면-전무로요. 항목들의 총 크기는 4 MB를 초과할 수 없습니다.
  • 동작은 Put, Update, Delete, ConditionCheck입니다. ConditionCheck는 쓰지 않는 항목에 대해 무언가를 단언합니다.
  • 비용이 두 배입니다. 트랜잭션 쓰기는 일반 쓰기의 두 배 용량을 소비합니다 — DynamoDB가 준비한 뒤 커밋하기 때문입니다.
  • 충돌과 실패한 조건은 전체를 취소시킵니다TransactionCanceledException과 함께이며, 부분적으로 남는 것은 없습니다.

문제: 반드시 일치해야 하는 두 쓰기

새 감사 이벤트마다 테넌트의 누적 카운트도 증가시키고 싶습니다. 이를 별개의 두 호출로 하면, 그 사이의 어떤 실패라도 데이터를 손상시킵니다:

  1. EVENT#… 항목을 PutItem — 성공.
  2. ADD eventCount 1을 위한 UpdateItem — 타임아웃.

이제 로그에는 카운터가 주장하는 것보다 행이 하나 더 많습니다. 무작정 2단계를 재시도하면 이중 카운트 위험이 있고, 재시도하지 않으면 불일치 상태로 둡니다. 두 쓰기가 애초에 연결된 적이 없으므로 안전한 복구가 없습니다.

SQL 출신이라면 둘 다 BEGIN … COMMIT으로 감쌌을 것입니다. DynamoDB의 답은 두 쓰기를 함께 담는 단일 트랜잭션 요청입니다.

TransactWriteItems의 작동 방식

AWS 개발자 가이드에 따르면, TransactWriteItems는 최대 100개의 고유 항목을 대상으로 "최대 100개의 쓰기 동작을 하나의 전부-아니면-전무 작업으로 묶"으며, "트랜잭션 내 항목들의 총 크기는 4 MB를 초과할 수 없습니다." 동작들은 원자적으로 완료됩니다 — 모두 성공하거나 아무것도 안 되거나입니다.

한 트랜잭션에서 네 가지 동작 유형을 섞을 수 있습니다:

  • Put — 항목을 생성하거나 교체합니다.
  • Update — 속성을 편집합니다(우리 카운터를 위한 ADD 포함).
  • Delete — 키로 항목을 제거합니다.
  • ConditionCheck — 그 외에는 쓰지 않는 항목에 대해 조건을 단언합니다(예: "이 테넌트가 여전히 활성 상태").

실전에서 영향을 주는 규칙이 두 개 더 있습니다. 첫째, 트랜잭션은 동등한 비트랜잭션 쓰기의 두 배 용량을 소비합니다 — DynamoDB가 준비 단계와 커밋 단계를 수행하기 때문입니다. 둘째, 한 트랜잭션에서 같은 항목을 두 번 대상으로 삼을 수 없으며, 트랜잭션은 인덱스에 대해 수행될 수 없습니다.

"DynamoDB"App"DynamoDB"Appprepare both, checkconditions"TransactWriteItems [Put EVENT,Update counter]""both commit, orTransactionCanceledException"

실전 예시: 추가 + 카운트, 원자적으로

감사 로그로 돌아갑니다. 테넌트 acme의 이벤트를 추가하고 그 카운터를 올리는 것은 두 동작을 가진 하나의 트랜잭션입니다:

actionitemeffect
PutTENANT#acmeEVENT#2026-06-24T09:14Z#a1write the new audit row
UpdateTENANT#acmeCOUNTERADD eventCount 1

어느 동작의 조건이라도 실패하면 — 예를 들어 테넌트가 정지되지 않았다는 ConditionCheck — 전체 요청이 TransactionCanceledException과 함께 취소되고 어느 쓰기도 일어나지 않습니다. 로그와 카운터는 결코 어긋날 수 없습니다.

각 동작의 ConditionExpression이 그 지렛대입니다. 이벤트 행이 아직 존재하지 않는다는 것(재시도가 그것을 중복할 수 없도록)과 테넌트가 활성 상태임을 단언하려면, Putattribute_not_exists(SK)를, ConditionCheckstatus = :active를 조합합니다.

이런 타입이 지정된 조건 표현식을 ExpressionAttributeNames:val 플레이스홀더를 손으로 조립하는 대신 DynamoDB Expression Builder에서 빌드하고 복사하세요 — 조건부 쓰기 모드가 TransactWriteItems가 원하는 형태를 정확히 내보냅니다.

불안정한 연결에서의 안전한 재시도를 위해서는 클라이언트 토큰을 첨부하세요: 10분 이내에 같은 토큰으로 반복된 TransactWriteItems는 쓰기를 다시 적용하지 않고 성공을 반환합니다 (멱등성).

DynoTable에서 해보기

DynoTable은 자체 쓰기에 내부적으로 트랜잭션을 사용합니다: 여러 항목 편집을 준비(stage)한 뒤 커밋하면, 낙관적 잠금 조건 표현식과 함께 TransactWriteItems로 보냅니다. 그래서 편집 배치가 전부-아니면-전무가 됩니다 — 다중 항목 변경을 절대 절반만 적용하지 않습니다.

즉, 이벤트 행과 카운터를 같은 준비된 배치에서 편집하고, 차이를 검토한 뒤, 어떤 SDK 코드도 쓰지 않고 둘 다 원자적으로 커밋할 수 있습니다.

DynoTable에서 새 감사 이벤트와 테넌트 카운터 편집을 준비한 뒤, 둘 다 하나의 전부-아니면-전무 트랜잭션으로 커밋하는 모습.
DynoTable에서 새 감사 이벤트와 테넌트 카운터 편집을 준비한 뒤, 둘 다 하나의 전부-아니면-전무 트랜잭션으로 커밋하는 모습.

함정과 다음 단계

  • 두 배 용량을 예산에 반영하세요. 트랜잭션 쓰기는 일반 쓰기의 두 배 WCU를 청구합니다 — 일관성이 핵심인 가끔의 쌍에는 괜찮지만, 모든 단일 쓰기를 트랜잭션으로 감싸면 비쌉니다. 원자성이 실제로 중요한 곳에 사용하세요.
  • TransactionCanceledException을 명시적으로 처리하세요. 실패한 조건이나, 같은 항목에 대한 다른 진행 중인 트랜잭션과의 충돌 둘 중 하나로 반환됩니다. 취소 이유가 어느 동작이 실패했는지 알려줍니다 — 무작정 재시도하지 말고 살펴보세요.
  • 스트림 레코드는 트랜잭션을 인지하지 않습니다. 한 트랜잭션의 변경은 Streams로 점진적으로 전파되며 다른 것들과 뒤섞일 수 있습니다. 소비자는 원자성이나 정렬을 가정할 수 없습니다 — DynamoDB Streams를 참고하세요.
  • 고처리량 카운터에는 맞지 않습니다. 무거운 동시 트랜잭션 부하 하의 단일 핫 카운터는 스로틀링됩니다. 그런 경우에는 원자적 카운터나 카운터 샤딩을 살펴보세요.

트랜잭션은 "이 쓰기들은 반드시 일치해야 한다"를 위한 도구입니다. 이벤트가 일관되게 도착하기 시작하면, 다음 관심사는 그것들에 반응하는 것이며 — 그것이 DynamoDB Streams입니다.

자신의 테이블에 대해 다중 항목 편집을 준비하고 단일 트랜잭션으로 커밋하려면 DynoTable을 다운로드하세요.

업데이트됨