DB에서의 기본적인 작업 단위는 하나의 SQL 쿼리이다. 그러나 하나의 작업을 처리하기 위해 여러 쿼리가 필요한 경우가 있다. 이럴 때 여러개의 쿼리를 마치 하나의 작업 단위처럼 처리 할 수 있도록 하는 것이 바로 트랜잭션(Transaction)이며, 이 트랜잭션을 제어하기 위한 SQL 명령문을 TCL(Transaction Control Language)라고 한다.
안정적인 데이터 작업을 위해 트랜잭션 사용은 선택이 아닌 필수이다.
보다 탄탄한 웹 서비스를 만들기 위해 django 에서 제공하는 TCL에 대해서 알아보자,
트랜잭션의 특성과 락, 격리성과 이글에서 다루지 않습니다.
일반적으로 DB에서 별도로 트랜잭션을 생성하지 않으면 한 줄 한 줄의 쿼리를 작업 단위로 보며, 해당 쿼리가 정상적으로 수행 되었을 경우 자동으로 Commit 하게 된다.
django 에서도 기본적으로 auto commit 옵션을 사용 중이다.
settings의 Database 설정에서 변경이 가능하나 모든 커밋으로 수동으로 해야 하는 번거로움이 발생함으로 정말 특이한 상황이 아니라면 굳이 건들지 않는 것이 건강에 이롭다.
MySQL 기준으로 트랜잭션을 생성하기 위해서 다음과 같은 쿼리를 실행해야 한다.
START TRANSACTION;
django 에서는 기본적으로 데코레이터와 컨텍스트 매니저를 사용하여 트랜잭션을 만들 수 있도록 기능을 제공한다.
from django.db import transaction
@transaction.atomic
def viewfunc(request):
do_something()
from django.db import transaction
def viewfunc(request):
with transaction.atomic():
do_something()
데코레이터로 랩핑된 함수 또는 컨텍스트 매니저 내부 스코프에서 실행되는 모든 쿼리에 대해서 하나의 트랜잭션으로 실행되게 된다.
auto commit이 켜져 있음으로 일반적인 상황에선 굳이 수동으로 커밋을 할 필요가 없지만, 특정 경우에 따라 수동 커밋을 통해 특정 구간까지의 작업에 대한 상태를 확정 지어 DB에 적용해야 될 수도 있다.
(개인적으론 이런 경우라면 별도의 트랜잭션을 만드는게 차라리 관리하기 편할 것 같다..)
from django.db import transaction
def viewfunc(request):
with transaction.atomic():
need_commit = do_something()
if need_commit:
transaction.commit()
do_anotherthing()
참고로 커밋이 되는 순간 트랜잭션은 거기서 종료된다고 보면 된다.
즉 그 이후의 작업에 대해서는 별도의 트랜잭션으로 취급 된다.
커밋과 마찬가지로 롤백도 수동으로 제어할 수 있다.
def viewfunc(request):
with transaction.atomic():
try:
do_something()
except:
transaction.rollback()
do_anotherthing()
이 또한 개인적인 의견으론 트랜잭션의 작업 단위를 쪼개든 다른 방법으로 구현 하는 것이 조금 더 관리하기 용이할 것 같다란 생각이 있다.
만약에 트랜잭션 중간에 에러가 나거나 기대한 결과가 아닐 경우 rollback을 사용하게 되면 트랜잭션이 실행되기 직전의 마지막 commit으로 상태를 되돌린다.
savepoint를 사용할 경우 마지막 커밋이 아닌 트랜잭션 안에서 특정 시점(상태)으로 돌아가는 것이 가능하다. 게임으로 치면 스테이지 내에서 중간 세이브 포인트를 만든다고 이해하면 쉽다.
def viewfunc(request):
with transaction.atomic():
do_something()
sid = transaction.savepoint()
try:
do_anotherthing()
transaction.savepoint_commit(sid)
except:
transaction.savepoint_rollback(sid)
위와 같이 특정 쿼리에서 에러가 발생해도 savepoint 까지의 작업 상태는 보존 할 수 있다.
on_commit은 커밋 이후에 특정 작업을 하도록 설정할 수 있다.
def hello():
print('hello world')
def viewfunc(request):
with transaction.atomic():
transaction.on_commit(hello)
do_something()
트랜잭션을 매번 관리하는 것이 지겹고 힘들다면 settings에 DATABASES 설정의 ATOMIC_REQUESTS 설정을 True로 설정하면 Request 단위로 모든 쿼리를 하나의 트랜잭션으로 처리 가능하다.
DB 설정(격리성, 동시처리..)에 따라 차이가 있겠지만 모든 view를 트랜잭션 단위로 처리할 경우 상당한 부하가 될 수도 있음으로 추천하는 방법은 아니다.
서로 관계 있는 2개 이상의 쿼리가 요구되는 작업이라면 필수적으로 트랜잭션으로 묶는 것이 옳다고 생각한다.
위에서 여러 TCL 및 부가 기능에 대해서 설명하긴 했지만 트랜잭션을 생성하는 것 이외에는 개발자가 코드로 수동 처리하는 것이 이상적인 패턴으로 보이진 않는다.
트랜잭션이란 하나의 작업이며 그 작업 안에서 상황에 따라 트랜잭션의 작업이 달라진다면 애초에 복수의 트랜잭션으로 나눠 처리하는 것이 유지보수 하기 수월하고, 안전한 서비스를 만드는 방법일 것 같다.