[Django] Transactions: atomic, on_commit, savepoint
Django transaction, transaction.atomic, on_commit, select_for_update, ATOMIC_REQUESTS, django savepoint 1.
정의
Transaction = 여러 SQL 문을 원자적 (all-or-nothing) 으로 실행. Django 는 자동 커밋 (autocommit) 모드가 기본, transaction.atomic() 으로 명시적 트랜잭션.
일반 트랜잭션 개념은 transaction-isolation-levels 참고.
3가지 트랜잭션 모드
flowchart TD
Q{Django 트랜잭션 모드}
Q --> A["1. Autocommit (기본)"]
Q --> B["2. atomic() context"]
Q --> C["3. ATOMIC_REQUESTS (view 단위)"]
A --> A1["각 쿼리 즉시 commit"]
B --> B1["명시적 블록 원자성"]
C --> C1["매 request 자동 wrap"]
1. transaction.atomic() (권장)
from django.db import transaction
@transaction.atomic
def transfer_money(from_user_id, to_user_id, amount):
from_account = Account.objects.select_for_update().get(user_id=from_user_id)
to_account = Account.objects.select_for_update().get(user_id=to_user_id)
if from_account.balance < amount:
raise ValidationError('잔액 부족')
from_account.balance -= amount
to_account.balance += amount
from_account.save()
to_account.save()
Context manager 형태:
def signup(request):
with transaction.atomic():
user = User.objects.create_user(...)
Profile.objects.create(user=user, ...)
# 예외 발생 시 둘 다 rollback
2. Nested atomic (savepoint)
with transaction.atomic():
order = Order.objects.create(...)
try:
with transaction.atomic(): # ← savepoint
payment = charge_card(order)
except PaymentError:
# 결제만 rollback, 주문은 유지
order.status = 'pending_payment'
order.save()
sequenceDiagram
T->>DB: BEGIN
T->>DB: INSERT order
T->>DB: SAVEPOINT sp1
T->>DB: charge_card (실패!)
T->>DB: ROLLBACK TO SAVEPOINT sp1
T->>DB: UPDATE order SET status='pending_payment'
T->>DB: COMMIT
3. ATOMIC_REQUESTS
# settings.py
DATABASES = {
'default': {
...
'ATOMIC_REQUESTS': True,
},
}
각 view = 자동으로 atomic() 로 감쌈. 예외 시 자동 rollback.
CAUTION
성능 영향: 모든 요청이 트랜잭션 → DB connection 오래 점유. 대량 트래픽 시 명시적 atomic() 권장.
on_commit (커밋 후 실행)
from django.db import transaction
def signup(request):
with transaction.atomic():
user = User.objects.create_user(...)
# 즉시 실행 (트랜잭션 안!)
# send_email(user) # ← ROLLBACK 시 이미 전송됨!
# commit 후 실행
transaction.on_commit(lambda: send_email(user))
transaction.on_commit(lambda: send_welcome_task.enqueue(user.pk))
sequenceDiagram
View->>DB: BEGIN
View->>DB: INSERT user
View->>Queue[on_commit callbacks]: 등록
View->>DB: COMMIT
Queue-->>Service: send_email(user)
Queue-->>Task: enqueue task
IMPORTANT
외부 시스템 호출은 항상 on_commit - 이메일, 결제, task, webhook 등. rollback 시 부수효과 없어야 함.
select_for_update (비관적 락)
@transaction.atomic
def reserve_seat(seat_id):
seat = Seat.objects.select_for_update().get(pk=seat_id)
if seat.is_reserved:
raise AlreadyReservedError()
seat.is_reserved = True
seat.save()
SQL: SELECT ... FROM seats WHERE id = ? FOR UPDATE;
다른 트랜잭션이 같은 row 를 lock 잡을 때까지 대기. Race condition 방지.
옵션:
Seat.objects.select_for_update(
skip_locked=True, # 잠긴 행 skip (큐 pattern)
nowait=True, # 즉시 실패 (대기 X)
of=('self',), # 특정 테이블만 lock (JOIN 시)
).filter(...)
Isolation Level
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'OPTIONS': {
'isolation_level': psycopg.IsolationLevel.SERIALIZABLE,
},
}
}
자세한 건 transaction-isolation-levels.
성능 - Bulk operations
# ❌ N번 트랜잭션
for user in users:
Profile.objects.create(user=user)
# ✓ 1번 트랜잭션 (bulk_create)
profiles = [Profile(user=u) for u in users]
Profile.objects.bulk_create(profiles)
# ✓ 명시적 트랜잭션
with transaction.atomic():
for user in users:
Profile.objects.create(user=user)
Test 에서 transaction
from django.test import TestCase, TransactionTestCase
class MyTest(TestCase):
# 모든 테스트가 트랜잭션 안에서 실행 → rollback 으로 격리
pass
class TransactionTest(TransactionTestCase):
# 실제 commit/rollback 테스트 (on_commit 등)
def test_signup_sends_email(self):
with self.captureOnCommitCallbacks(execute=True):
self.client.post('/signup/', {...})
흔한 함정
WARNING
atomic()안에서 외부 API 호출 = 트랜잭션 오래 점유.on_commit으로.select_for_update를atomic()밖에서 =TransactionManagementError.ATOMIC_REQUESTS=True + long request= connection pool 고갈.- savepoint 남발 = 성능 저하. 필요한 곳만.
다른 프레임워크
| Framework | Transaction |
|---|---|
| Django | transaction.atomic() |
| Rails | ActiveRecord::Base.transaction |
| Spring | @Transactional |
| SQLAlchemy | session.begin() |
관련 위키
이 글의 용어 (5개)
- [DB] PostgreSQL: 프로세스 모델, MVCC, WAL, 확장성database-internals
- 정의 PostgreSQL 은 오픈소스 ORDBMS. 1986 UC Berkeley POSTGRES 의 후예. MVCC, 확장 가능 타입, JSONB, full-text searc…
- [DB] Transaction Isolation Levels: 완벽 가이드database-internals
- 정의 Transaction Isolation Level (트랜잭션 격리 수준) = 동시에 실행되는 여러 트랜잭션이 서로의 변경을 어디까지 볼 수 있는지 규정하는 수준. ACID …
- [Django] ORM 심화: N+1, prefetch, Subquery, Windowdjango
- 정의 Django ORM은 SQL을 잘 모르는 개발자에게도 안전하지만 비효율을 숨기기 쉽다. N+1, 비효율 JOIN, 큰 결과셋 메모리 폭발이 흔한 함정. selectrelat…
- [Django] QuerySet과 Managerdjango
- 정의 QuerySet은 DB 쿼리를 lazy하게 빌드하는 객체. , 같은 메서드를 체이닝해도 실제 SQL 실행은 평가 시점( , , , slice 등)에 한 번 발생. Manag…
- [Pattern] Outbox Pattern: DB + 메시지의 원자성distributed-systems
- 정의 Outbox Pattern = DB 변경 + 메시지 발행 의 원자성 보장. 이중 쓰기 (dual write) 문제 의 표준 해결. 문제: Dual Write | 시나리오 |…
💬 댓글