Search

Django 트랜잭션 (3)

이번에는 Atomic 객체를 뜯어보며, 왜 그렇게 동작했었는지 알아보자.

Atomic의 동작 원리

아래 링크에서 Atomic 에 대한 코드를 확인할 수 있다.
transaction.py#L142
db

atomic 메서드

우선 @transaction.atomic 로 선언하든, with transaction.atomic() 를 사용하던 atomic() 메서드는 반드시 사용된다.
코드를 확인해보면, 어떤 방식으로 호출하느냐에 따라 Atomic 객체를 다르게 반환하는 것을 알 수 있다.
def atomic(using=None, savepoint=True, durable=False): # Bare decorator: @atomic -- although the first argument is called # `using`, it's actually the function being decorated. if callable(using): return Atomic(DEFAULT_DB_ALIAS, savepoint, durable)(using) # Decorator: @atomic(...) or context manager: with atomic(...): ... else: return Atomic(using, savepoint, durable)
Python
복사
@atomic 와 같이 인자 없이 데코레이터를 쓴 경우가 아니라면, 모두 else 구문에 의해 동작될 것이다.
다양한 호출 방식을 지원하며 사용하는 메서드이니, 어려움은 없다.

Atomic 구성

크게 3개의 매직 메서드로 나뉜다.
__init__ : Atomic 객체에서 트랜잭션 상태 관리를 위해 사용한다.
__enter__ : Atomic 객체를 컨텍스트 매니저로 사용하기 위한 내용이며, 트랜잭션이 시작될 때 어떤 동작을 하는지 작성되어 있다.
__exit__ : 동일하게 Atomic 객체를 컨텍스트 매니저로 사용하기 위한 내용이며, 트랜잭션 내부 작업이 완료되거나 에러가 발생하는 경우

__init__ 메서드

큰 동작은 하지 않고, 필요한 속성들만 설정한다.
우선 usingdurableatomic 메서드의 인자로도 사용되었기 때문에, 어떤 내용인지 다른 메서드의 내용에서 나올 것 같다.
def __init__(self, using, savepoint, durable): self.using = using self.savepoint = savepoint self.durable = durable self._from_testcase = False
Python
복사

__enter__ 메서드

전체 코드는 아래 내용을 참고하면 좋을 것 같으며, 아래에서 4가지의 작업으로 나눌 것이다.
추가적으로, 커넥션 객체에 대한 속성이 어떤 것을 의미하는지도 아래 코드의 주석을 참고하면 좋다.
[ 1. 커넥션 확인(객체 가져오기) ]
인자로 입력한 using 을 사용하여 현재 연결된 DB에 대한 연결 객체를 가져온다.
여기서 사용되는 using 은 어떤 DB를 사용할지 선택할 때 사용된다.
기본적으로 Django는 단일 DB와 연결하여 사용하지만, 아래 문서와 블로그와 같이 설정을 통해 여러 DB를 사용할 수 있다.
def __enter__(self): connection = get_connection(self.using) ...
Python
복사
[ 2. 중첩 트랜잭션 확인 (durable 인자 설정 시에만 확인) ]
커넥션은 트랜잭션 블럭을 atomic_blocks 로 관리한다.
하지만, 이미 다른 트랜잭션으로 현 커넥션에 atomic_blocks 에 활성화된 트랜잭션이 존재하는 경우 에러를 발생시킨다.
즉, 현재 트랜잭션 블럭 하위에 Nested로 활성화된 트랜잭션이 없어야한다.
def __enter__(self): ... if ( self.durable and connection.atomic_blocks and not connection.atomic_blocks[-1]._from_testcase ): raise RuntimeError( "A durable atomic block cannot be nested within another " "atomic block." ) ...
Python
복사
[ 3. 최상단 트랜잭션일 때 동작 ]
현재 커넥션이 이미 Atomic 블럭 내에서 트랜잭션이 관리되고 있지 않은 경우(최상단 트랜잭션 일 때), 연결 객체의 정보를 가져와 설정을 변경한다.
(위에 작성한 속성 관련 코드의 주석을 참고하면 좋다! → base.py#L53)
connection.commit_on_exit = True → Atomic 블록이 종료 시 커밋하도록 설정
connection.needs_rollback = False → 내부 Atomic 블록에서 예외가 발생한 경우 트랜잭션을 다음 사용 가능한 저장 지점으로 롤백하지 않도록 설정
최상단 트랜잭션은 Savepoint를 사용한 롤백이 아닌 작업 전체를 롤백하기 때문에, 명령어가 다름
만약 autocommit 이 비활성화 되어있다면
connection.in_atomic_block = True → Atomic 블럭으로 관리되고 있다고 설정한다.
connection.commit_on_exit = FalseCommit 작업이 자동으로 되지 않도록 설정한다.
def __enter__(self): ... if not connection.in_atomic_block: # Reset state when entering an outermost atomic block. connection.commit_on_exit = True connection.needs_rollback = False if not connection.get_autocommit(): # Pretend we're already in an atomic block to bypass the code # that disables autocommit to enter a transaction, and make a # note to deal with this case in __exit__. connection.in_atomic_block = True connection.commit_on_exit = False ...
Python
복사
[ 4. Nested 트랜잭션일 때 ]
현재 트랜잭션이 특정 트랜잭션 내부에 중첩하여 사용되는 트랜잭션일 때 연결 객체의 정보를 가져와 설정을 변경한다.
이미 Atomic block으로 관리되고 있다면(최상단 트랜잭션이 아닐 때 OR 이미 autocommit을 껐을 때)
connection.savepoint_ids.append(sid) → 만약 savepoint 를 사용한다면, savepoint 를 생성하고 savepoint ID를 누적한다.
BEGIN -> <SQL> -> SAVEPOINT -> <SQL> -> COMMIT 와 같이 중첩된 트랜잭션은 별도의 BEGIN 으로 트랜잭션을 진행하지 않는다.
Nested된 트랜잭션은 최상단 트랜잭션의 SAVEPOINT로 설정된다.
그 외(최상단 트랜잭션일 때 등)
connection.set_autocommit(False, …) → 각 작업의 즉시 반영을 막기 위해, autocommit 을 비활성한다.
즉, Atomic 객체를 사용한 트랜잭션 관리를 위해선 무조건 autocommit 은 비활성화한다.
다만, Default로 비활성화한 상태일 경우에는 이전 코드에서 connection.commit_on_exit = False 를 통해 Commit 작업이 자동으로 되지 않도록 설정한다.
connection.in_atomic_block = True → Atomic 블럭으로 관리되고 있다고 설정한다.
현재 Atomic 블럭을 커넥션 객체의 관리 리스트에 추가한다.
def __enter__(self): ... if connection.in_atomic_block: # We're already in a transaction; create a savepoint, unless we # were told not to or we're already waiting for a rollback. The # second condition avoids creating useless savepoints and prevents # overwriting needs_rollback until the rollback is performed. if self.savepoint and not connection.needs_rollback: sid = connection.savepoint() connection.savepoint_ids.append(sid) else: connection.savepoint_ids.append(None) else: connection.set_autocommit( False, force_begin_transaction_with_broken_autocommit=True ) connection.in_atomic_block = True if connection.in_atomic_block: connection.atomic_blocks.append(self) ...
Python
복사

__exit__ 메서드

이전 글(Django 트랜잭션 (1)Django 트랜잭션 (1))에서 한번 설명하였지만, 이번엔 상세히 작성해보겠다.
[ 1. 트랜잭션 목록 / Savepoint 목록 정리 ]
현재 트랜잭션 블럭을 목록에서 제거
스택으로 관리하기 때문에, 가장 마지막 트랜잭션(depth가 가장 깊은)이 먼저 끝나며 제거될 것이다.
만약 최상단 트랜잭션이 아닌 경우
해당 트랜잭션은 savepoint로 처리되기 때문에, savepoint id를 가져온다.
그 외(최상단 트랜잭션 등)
Atomic 블럭으로 관리되지 않는 상태로 설정한다.
def __exit__(self, exc_type, exc_value, traceback): connection = get_connection(self.using) if connection.in_atomic_block: connection.atomic_blocks.pop() if connection.savepoint_ids: sid = connection.savepoint_ids.pop() else: # Prematurely unset this flag to allow using commit or rollback. connection.in_atomic_block = False ...
Python
복사
[ 2-1. 현재 트랜잭션이 DB와의 연결이 종료된 상태일 때 ]
DB 자체에서 Rollback을 진행중인 상태이기 때문에, 추가적인 작업을 요청하지 않는다.
def __exit__(self, exc_type, exc_value, traceback): ... try: if connection.closed_in_transaction: # The database will perform a rollback by itself. # Wait until we exit the outermost block. pass ...
Python
복사
[ 2-2. 트랜잭션 내 작업들이 정상적으로 완료된 경우 ]
1번 작업에서 connection.in_atomic_block 이 False로 바뀌지 않은 경우, 즉 최상단 트랜잭션이 아닐 경우
savepoint id를 사용하여 savepoint_commit(RELEASE SAVEPOINT) 진행
이 과정에서 DatabaseError Raise시, savepoint_rollback(ROLLBACK SAVEPOINT) 진행
이 마저도 에러가 난다면, 상위 트랜잭션에 롤백이 필요함을 설정
그 외(최상단 트랜잭션 등)
해당 트랜잭션을 COMMIT
이 과정에서 DatabaseError Raise시, 트랜잭션 작업 전체 ROLLBACK 진행 후 Raise 발생
ROLLBACK 작업 시 에러가 발생하면, 커넥션을 해제
def __exit__(self, exc_type, exc_value, traceback): ... try: ... elif exc_type is None and not connection.needs_rollback: if connection.in_atomic_block: # Release savepoint if there is one if sid is not None: try: connection.savepoint_commit(sid) except DatabaseError: try: connection.savepoint_rollback(sid) # The savepoint won't be reused. Release it to # minimize overhead for the database server. connection.savepoint_commit(sid) except Error: # If rolling back to a savepoint fails, mark for # rollback at a higher level and avoid shadowing # the original exception. connection.needs_rollback = True raise else: # Commit transaction try: connection.commit() except DatabaseError: try: connection.rollback() except Error: # An error during rollback means that something # went wrong with the connection. Drop it. connection.close() raise ... ...
Python
복사
[ 2-3. 그 외(트랜잭션 내부 예외 발생 등) ]
롤백이 필요하다는 것을 나타내는 Flag를 False 처리 → 현재 작업에서 ROLLBACK을 진행하기 때문에 미리 Flag만 바꿈
1번 작업에서 connection.in_atomic_block 이 False로 바뀌지 않은 경우, 즉 최상단 트랜잭션이 아닐 경우
sid(savepoint id)가 없는 경우
상단 트랜잭션에서 롤백을 처리하도록 Flag 설정
sid(savepoint id)가 존재하는 경우
savepoint id를 사용하여 ROLLBACK SAVEPOINT 처리
이 과정에서 에러 발생 시, 상단 트랜잭션에서 롤백을 처리하도록 Flag 설정
그 외(최상단 트랜잭션 등)
트랜잭션 전체 작업 ROLLBACK 처리
이 과정에서 에러 발생 시, 커넥션 종료 처리
def __exit__(self, exc_type, exc_value, traceback): ... try: ... else: # This flag will be set to True again if there isn't a savepoint # allowing to perform the rollback at this level. connection.needs_rollback = False if connection.in_atomic_block: # Roll back to savepoint if there is one, mark for rollback # otherwise. if sid is None: connection.needs_rollback = True else: try: connection.savepoint_rollback(sid) # The savepoint won't be reused. Release it to # minimize overhead for the database server. connection.savepoint_commit(sid) except Error: # If rolling back to a savepoint fails, mark for # rollback at a higher level and avoid shadowing # the original exception. connection.needs_rollback = True else: # Roll back transaction try: connection.rollback() except Error: # An error during rollback means that something # went wrong with the connection. Drop it. connection.close() ...
Python
복사
[ 3. 롤백 작업을 마친 후, 후처리 ]
1번 작업에서 connection.in_atomic_block 이 False로 바뀌지 않은 경우, 즉 최상단 트랜잭션이 아닐 경우
연결이 종료된 상태일 경우 → connection None 처리
연결이 종료된 상태가 아닐 경우 → AutoCommit 재활성화 (Atomic 블럭 진입 이전 상태로 변경)
혹은 save point가 없으면서, 자동 commit 처리가 비활성화인 경우 → AutoCommit을 처음부터 활성화하지 않은 경우
연결이 종료된 상태일 경우 → connection None 처리
연결이 종료된 상태가 아닐 경우 → Atomic 블럭으로 관리함을 표시하는 Flag를 명시적으로 False 처리
def __exit__(self, exc_type, exc_value, traceback): ... try: ... finally: # Outermost block exit when autocommit was enabled. if not connection.in_atomic_block: if connection.closed_in_transaction: connection.connection = None else: connection.set_autocommit(True) # Outermost block exit when autocommit was disabled. elif not connection.savepoint_ids and not connection.commit_on_exit: if connection.closed_in_transaction: connection.connection = None else: connection.in_atomic_block = False
Python
복사

참고