트랜잭션

참고: 새로운 애플리케이션을 빌드하는 개발자에게는 NDB 클라이언트 라이브러리 사용을 적극 권장합니다. 기존 클라이언트 라이브러리에 비해 Memcache API를 통한 자동 항목 캐싱과 같은 여러 이점이 있습니다. 현재 기존 DB 클라이언트 라이브러리를 사용하는 중이라면 DB에서 NDB로의 이전 가이드를 참조하세요.

Cloud Datastore는 트랜잭션을 지원합니다. 트랜잭션은 원자성, 즉 트랜잭션에 포함된 작업이 모두 발생하거나 하나도 발생하지 않는 성질을 갖는 하나의 작업 또는 작업 집합입니다. 애플리케이션은 단일 트랜잭션으로 여러 작업과 계산을 수행할 수 있습니다.

트랜잭션 사용

트랜잭션은 하나 이상의 항목에 대한 Cloud Datastore 작업 집합입니다. 각 트랜잭션에서는 원자성이 보장되어 트랜잭션이 일부분만 적용되는 경우는 없습니다. 트랜잭션의 모든 작업이 적용되거나 아니면 모두 적용되지 않습니다. 트랜잭션의 최대 지속 시간은 60초이며, 30초 후에 10초의 유휴 만료 시간이 있습니다.

다음과 같은 경우 작업이 실패할 수 있습니다.

  • 동일한 항목 그룹에서 동시에 실행되는 수정 시도가 너무 많은 경우
  • 트랜잭션이 리소스 한도를 초과하는 경우
  • Cloud Datastore에 내부 오류가 발생하는 경우

이러한 모든 경우 Cloud Datastore API에서 예외가 발생합니다.

트랜잭션은 Cloud Datastore의 선택적 기능이므로 Cloud Datastore 작업을 수행하기 위해 반드시 트랜잭션을 사용할 필요는 없습니다.

애플리케이션은 단일 트랜잭션에서 일련의 구문과 데이터 저장 작업을 실행할 수 있습니다. 따라서 예외를 일으키는 구문이나 작업이 있으면 해당 집합의 어떠한 Cloud Datastore 작업도 적용되지 않습니다. 애플리케이션은 Python 함수를 사용하여 트랜잭션에서 수행할 작업을 정의합니다. 애플리케이션은 트랜잭션이 단일 항목 그룹 내의 항목에 액세스하는지 아니면 트랜잭션이 교차 그룹 트랜잭션인지에 따라 run_in_transaction 메소드 중 하나를 사용하여 트랜잭션을 시작합니다.

트랜잭션 내에서만 사용되는 일반적인 함수 사용 사례는 @db.transactional 데코레이터를 사용하세요.

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

@db.transactional
def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

increment_counter(acc.key(), 5)

함수가 트랜잭션 없이 호출되는 경우 데코레이터를 사용하는 대신 함수를 인수로 하여 db.run_in_transaction()을 호출하세요.

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

db.run_in_transaction(increment_counter, acc.key(), 5)

db.run_in_transaction()은 함수 객체, 그리고 함수에 전달할 위치 및 키워드 인수를 받습니다. 함수가 값을 반환하면 db.run_in_transaction()은 그 값을 반환합니다.

함수가 반환하면 트랜잭션이 커밋되고 Cloud Datastore 작업의 모든 효과가 적용됩니다. 함수에서 예외가 발생하면 트랜잭션이 '롤백'되고 효과가 적용되지 않습니다. 예외에 대해서는 위 메모를 참조하세요.

다른 트랜잭션 내에서 트랜잭션 함수가 호출되는 경우 @db.transactionaldb.run_in_transaction()의 기본 동작이 다릅니다. @db.transactional은 이를 허용하며 내부 트랜잭션이 외부 트랜잭션과 동일한 트랜잭션이 됩니다. db.run_in_transaction()을 호출하면 기존 트랜잭션 내에 다른 트랜잭션을 '중첩'하려는 시도가 수행되지만 이 동작은 아직 지원되지 않으며 db.BadRequestError 예외가 발생합니다. 다른 동작을 지정할 수 있습니다. 자세한 내용은 트랜잭션 옵션을 참조하세요.

교차 그룹(XG) 트랜잭션 사용

여러 항목 그룹에 걸쳐 작동하는 교차 그룹 트랜잭션은 단일 그룹 트랜잭션과 동일하게 작동하지만 코드가 둘 이상의 항목 그룹에서 항목을 업데이트하려고 시도하더라도 실패하지 않습니다. 교차 그룹 트랜잭션을 호출하려면 트랜잭션 옵션을 사용합니다.

@db.transactional 사용:

from google.appengine.ext import db

@db.transactional(xg=True)
def make_things():
  thing1 = Thing(a=3)
  thing1.put()
  thing2 = Thing(a=7)
  thing2.put()

make_things()

db.run_in_transaction_options 사용:

from google.appengine.ext import db

xg_on = db.create_transaction_options(xg=True)

def my_txn():
    x = MyModel(a=3)
    x.put()
    y = MyModel(a=7)
    y.put()

db.run_in_transaction_options(xg_on, my_txn)

트랜잭션에서 가능한 작업

Cloud Datastore는 단일 트랜잭션 내에서 수행 가능한 작업을 제한합니다.

트랜잭션이 단일 그룹 트랜잭션인 경우 트랜잭션의 모든 Cloud Datastore 작업은 같은 항목 그룹의 항목을 대상으로 해야 하며, 트랜잭션이 교차 그룹 트랜잭션인 경우에는 최대 25개 항목 그룹의 항목을 대상으로 해야 합니다. 여기에는 상위 그룹별 항목 쿼리, 키별 항목 쿼리, 항목 업데이트, 항목 삭제가 포함됩니다. 각 루트 항목은 별도의 항목 그룹에 속하므로 교차 그룹 트랜잭션이 아닌 한 두 개 이상의 루트 항목에서 단일 트랜잭션을 생성 또는 작동할 수 없습니다.

2개 이상의 트랜잭션이 하나 이상의 공통된 항목 그룹에 있는 항목을 수정하려고 동시에 시도하면 변경 사항을 커밋한 첫 번째 트랜잭션만 성공하고, 나머지 모든 항목은 커밋에 실패합니다. 이러한 설계로 인해, 항목 그룹을 사용하면 해당 그룹에 속하는 항목을 대상으로 동시에 실행할 수 있는 쓰기 작업 수가 제한됩니다. 트랜잭션이 시작되면 Cloud Datastore는 트랜잭션에 사용된 항목 그룹의 최종 업데이트 시간을 확인하여 낙관적 동시 실행 제어를 사용합니다. 항목 그룹에 대한 트랜잭션을 커밋하면 Cloud Datastore가 트랜잭션에 사용되는 항목 그룹의 최종 업데이트 시간을 다시 확인합니다. 최초 확인 이후로 변경된 내역이 있으면 예외가 발생합니다. 항목 그룹에 대한 설명은 Cloud Datastore 개요 페이지를 참조하세요.

앱은 상위 필터를 포함한 경우에만 트랜잭션 중에 쿼리를 수행할 수 있습니다. 또한 앱은 트랜잭션 중에 키별로 Datastore 항목을 가져올 수 있습니다. 트랜잭션 전에 키를 준비하거나 트랜잭션 내에서 키 이름 또는 ID를 사용하여 키를 만들 수 있습니다.

다른 모든 Python 코드는 트랜잭션 함수 내에서 허용됩니다. db.is_in_transaction()을 사용하여 현재 범위가 트랜잭션 함수에 중첩되는지 여부를 확인할 수 있습니다. 트랜잭션 함수에는 Cloud Datastore 작업 이외의 부수적 효과는 없어야 합니다. 다른 사용자가 동시에 항목 그룹에 항목을 업데이트 중이라서 Cloud Datastore 작업이 실패하는 경우 트랜잭션 함수가 여러 번 호출될 수 있습니다. 이러한 경우가 발생하면 Cloud Datastore API는 고정된 횟수만큼 트랜잭션을 재시도합니다. 모두 실패할 경우 db.run_in_transaction()에서 TransactionFailedError가 발생합니다. db.run_in_transaction() 대신 db.run_in_transaction_custom_retries()를 사용하여 트랜잭션 재시도 횟수를 조정할 수 있습니다.

마찬가지로, 트랜잭션 함수를 호출하는 코드가 부수적 효과를 되돌리는 방법을 아는 경우를 제외하고 트랜잭션 함수에는 트랜잭션의 성공에 따른 부작용이 없어야 합니다. 예를 들어 트랜잭션이 새 Cloud Datastore 항목을 저장하고, 만들어진 항목의 ID를 나중에 사용하기 위해 저장하는 경우 트랜잭션은 실패하며 항목 생성이 롤백되므로 저장된 ID는 해당 항목을 참조하지 않습니다. 이 경우 호출하는 함수는 저장된 ID를 사용하지 않도록 주의를 기울여야 합니다.

격리 및 일관성

트랜잭션 외에 Cloud Datastore의 격리 레벨은 커밋된 읽기에 가장 근접합니다. 트랜잭션 내에서 직렬 가능한 격리가 적용됩니다. 즉, 다른 트랜잭션은 이 트랜잭션에서 읽거나 수정한 데이터를 동시에 수정할 수 없습니다. 격리 레벨에 대한 자세한 내용은 직렬화 가능한 격리를 다룬 위키백과 정보와 트랜잭션 격리 문서를 읽어보세요.

트랜잭션의 모든 읽기에는 트랜잭션이 시작된 시점에 Cloud Datastore가 지닌 최신의 일관된 상태가 반영됩니다. 트랜잭션에 포함된 쿼리 및 가져오기는 트랜잭션 시작 시점을 기준으로 Cloud Datastore의 일관된 단일 스냅샷을 인식하도록 보장됩니다. 트랜잭션의 항목 그룹에 속하는 항목 및 색인 행은 완전히 업데이트되므로 쿼리는 완전하고 정확한 결과 항목 집합을 반환하며, 트랜잭션 격리의 설명과 같이 트랜잭션 외부의 쿼리에서 발생할 수 있는 거짓양성 또는 거짓음성이 나타나지 않습니다.

이러한 일관된 스냅샷 보기는 트랜잭션에 포함된 쓰기 후 읽기까지도 확장됩니다. 대부분의 데이터베이스와 달리, Cloud Datastore 트랜잭션에 포함된 쿼리 및 가져오기에서는 해당 트랜잭션에 포함된 이전의 쓰기 결과가 인식되지 않습니다. 특히 트랜잭션 내에서 항목이 수정되거나 삭제된 경우 쿼리 또는 가져오기를 수행하면 트랜잭션 시작 시점의 원래 버전 항목이 반환되거나, 그 당시에 항목이 존재하지 않았다면 아무것도 반환되지 않습니다.

트랜잭션 용도

다음 예에서는 트랜잭션의 용도 중 하나로서 항목의 현재 값을 기준으로 항목을 새 속성 값으로 업데이트하는 방법을 보여줍니다.

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

여기에서 트랜잭션이 필요한 이유는 이 코드에서 객체를 가져온 후 수정된 객체를 저장하기 전에 다른 사용자가 값을 업데이트할 수도 있기 때문입니다. 트랜잭션을 사용하지 않으면 다른 사용자가 count 값을 업데이트하기 전에 사용자의 요청에 해당 값이 사용되고, 저장 시 새로운 값을 덮어씁니다. 트랜잭션이 있으면 애플리케이션에 다른 사용자의 업데이트가 전달됩니다. 트랜잭션 중에 항목이 업데이트되는 경우 모든 단계가 중단 없이 완료될 때까지 트랜잭션이 재시도됩니다.

트랜잭션의 다른 일반적인 용도는 명명된 키로 항목을 가져오거나, 항목이 아직 존재하지 않으면 생성하는 것입니다.

class SalesAccount(db.Model):
    address = db.PostalAddressProperty()
    phone_number = db.PhoneNumberProperty()

def get_or_create(parent_key, account_id, address, phone_number):
    obj = db.get(db.Key.from_path("SalesAccount", account_id, parent=parent_key))
    if not obj:
        obj = SalesAccount(key_name=account_id,
                           parent=parent_key,
                           address=address,
                           phone_number=phone_number)
        obj.put()
    else:
        obj.address = address
        obj.phone_number = phone_number

여기에서도 트랜잭션이 필요한 이유는 다른 사용자가 동일한 문자열 ID를 갖는 항목을 만들거나 업데이트하려고 시도하는 경우를 처리하기 위해서입니다. 트랜잭션을 사용하지 않을 경우 항목이 존재하지 않는 상태에서 두 사용자가 항목을 만들려고 하면 두 번째 항목이 첫 번째 항목을 덮어쓰게 되며, 두 번째 사용자는 이러한 사실을 알 수 없습니다. 트랜잭션을 사용하면 두 번째 시도가 재시도되면서 항목이 현재 존재함을 인식하고 항목을 덮어쓰는 대신 업데이트합니다.

트랜잭션이 실패하는 경우 성공할 때까지 재시도하도록 앱을 설정하거나 앱의 사용자 인터페이스 수준에 전달하여 사용자가 오류에 대처하도록 할 수 있습니다. 모든 트랜잭션에서 재시도 루프를 생성할 필요는 없습니다.

가져오기 또는 만들기는 매우 유용해서 이를 위한 기본 메소드가 있습니다. Model.get_or_insert()는 키 이름, 선택사항인 상위 요소, 그리고 해당 이름 및 경로를 가진 항목이 없는 경우 모델 생성자에 전달할 인수를 취합니다. 가져오기 시도와 만들기는 하나의 트랜잭션에서 일어나므로 트랜잭션이 성공적인 경우 메소드는 항상 실제 항목을 나타내는 모델 인스턴스를 반환합니다.

마지막으로, 트랜잭션을 사용하여 Cloud Datastore의 일관된 스냅샷을 읽을 수 있습니다. 페이지를 렌더링하거나 일관성이 필요한 데이터를 내보내기 위해 여러 읽기 작업이 필요한 경우 이 방법이 유용할 수 있습니다. 이러한 종류의 트랜잭션은 쓰기를 수행하지 않으므로 보통 읽기 전용 트랜잭션이라고 합니다. 읽기 전용 단일 그룹 트랜잭션은 동시 수정으로 인해 실패하는 경우가 없으므로 실패 시 재시도를 구현할 필요가 없습니다. 그러나 교차 그룹 트랜잭션은 동시 수정으로 인해 실패할 수 있으므로 재시도를 구현해야 합니다. 읽기 전용 트랜잭션의 커밋과 롤백은 모두 무운영입니다.

class Customer(db.Model):
    user = db.StringProperty()

class Account(db.Model):
    """An Account has a Customer as its parent."""
    address = db.PostalAddressProperty()
    balance = db.FloatProperty()

def get_all_accounts():
    """Returns a consistent view of the current user's accounts."""
    accounts = []
    for customer in Customer.all().filter('user =', users.get_current_user().user_id()):
        accounts.extend(Account.all().ancestor(customer))
    return accounts

트랜잭션 작업 대기열에 추가

Cloud Datastore 트랜잭션의 일부로 대기열에 작업을 추가하면 트랜잭션이 성공적으로 커밋되는 경우에만 대기열에 작업이 추가됩니다. 트랜잭션이 커밋되지 않으면 작업은 대기열에 추가되지 않습니다. 트랜잭션이 커밋되면 작업이 대기열에 추가됩니다. 대기열에 추가된 작업은 즉시 실행되지 않으므로 작업은 트랜잭션에서 원자성을 갖지 않습니다. 일단 대기열에 추가되면 작업은 성공할 때까지 재시도됩니다. 이는 run_in_transaction() 함수 중에 대기열에 추가된 모든 작업에 적용됩니다.

트랜잭션 작업은 구매 확인 이메일 보내기와 같이 트랜잭션 성공에 의존하는 Cloud Datastore 이외의 작업을 트랜잭션에 결합할 수 있다는 면에서 유용합니다. Cloud Datastore 작업을 트랜잭션에 연결하여 트랜잭션이 성공하는 경우에만 트랜잭션 외부에서 항목 그룹의 변경사항을 커밋할 수도 있습니다.

애플리케이션은 단일 트랜잭션 중에 작업 대기열에 6개 이상의 트랜잭션 작업을 삽입할 수 없습니다. 트랜잭션 작업의 이름은 사용자가 지정한 이름이 아니어야 합니다.

def do_something_in_transaction(...)
    taskqueue.add(url='/path/to/my/worker', transactional=True)
  ...

db.run_in_transaction(do_something_in_transaction, ....)
이 페이지가 도움이 되었나요? 평가를 부탁드립니다.

다음에 대한 의견 보내기...

App Engine standard environment for Python