이번에는 Atomic 객체를 뜯어보며, 왜 그렇게 동작했었는지 알아보자.
Atomic의 동작 원리
아래 링크에서 Atomic 에 대한 코드를 확인할 수 있다.
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__ 메서드
큰 동작은 하지 않고, 필요한 속성들만 설정한다.
우선 using 과 durable 은 atomic 메서드의 인자로도 사용되었기 때문에, 어떤 내용인지 다른 메서드의 내용에서 나올 것 같다.
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 블럭 내에서 트랜잭션이 관리되고 있지 않은 경우(최상단 트랜잭션 일 때), 연결 객체의 정보를 가져와 설정을 변경한다.
•
connection.commit_on_exit = True → Atomic 블록이 종료 시 커밋하도록 설정
•
connection.needs_rollback = False → 내부 Atomic 블록에서 예외가 발생한 경우 트랜잭션을 다음 사용 가능한 저장 지점으로 롤백하지 않도록 설정
◦
최상단 트랜잭션은 Savepoint를 사용한 롤백이 아닌 작업 전체를 롤백하기 때문에, 명령어가 다름
•
만약 autocommit 이 비활성화 되어있다면
◦
connection.in_atomic_block = True → Atomic 블럭으로 관리되고 있다고 설정한다.
◦
connection.commit_on_exit = False → Commit 작업이 자동으로 되지 않도록 설정한다.
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__ 메서드
[ 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
복사