본문으로 건너뛰기
김신건의 로그

[Django] Transactions: atomic, on_commit, savepoint

· 수정 · 📖 약 1분 · 319자/단어 #django #transaction #database #orm
Django transaction, transaction.atomic, on_commit, select_for_update, ATOMIC_REQUESTS, django savepoint

정의

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

  1. atomic() 안에서 외부 API 호출 = 트랜잭션 오래 점유. on_commit 으로.
  2. select_for_updateatomic() 밖에서 = TransactionManagementError.
  3. ATOMIC_REQUESTS=True + long request = connection pool 고갈.
  4. savepoint 남발 = 성능 저하. 필요한 곳만.

다른 프레임워크

FrameworkTransaction
Djangotransaction.atomic()
RailsActiveRecord::Base.transaction
Spring@Transactional
SQLAlchemysession.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 | 시나리오 |…

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기