Django 트랜잭션 관리 완벽 가이드: 데이터 일관성을 지키는 실전 기법
데이터베이스를 다루는 모든 애플리케이션에서 트랜잭션 관리는 필수적입니다. 특히 금융 거래, 전자상거래, 재고 관리 같은 중요한 비즈니스 로직에서는 데이터의 일관성이 보장되지 않으면 심각한 문제가 발생할 수 있습니다. 이 글에서는 Django에서 트랜잭션을 효과적으로 관리하는 방법과 다양한 실전 사례를 살펴보겠습니다.
🔒 트랜잭션이란? 왜 중요한가?
트랜잭션의 정의
트랜잭션(Transaction)은 데이터베이스에서 하나의 논리적 작업 단위를 의미합니다. 여러 개의 데이터베이스 작업을 하나로 묶어서 “모두 성공하거나, 모두 실패하거나” 둘 중 하나만 발생하도록 보장합니다.
ACID 속성
트랜잭션은 다음 네 가지 핵심 속성을 보장해야 합니다:
1. Atomicity (원자성)
- 트랜잭션 내의 모든 작업이 완전히 수행되거나, 전혀 수행되지 않아야 합니다
- “All or Nothing” 원칙
2. Consistency (일관성)
- 트랜잭션이 완료되면 데이터베이스는 항상 일관된 상태를 유지해야 합니다
- 모든 제약 조건과 규칙이 위반되지 않아야 합니다
3. Isolation (격리성)
- 동시에 실행되는 트랜잭션들이 서로 간섭하지 않아야 합니다
- 각 트랜잭션은 독립적으로 실행되는 것처럼 보여야 합니다
4. Durability (지속성)
- 트랜잭션이 성공적으로 완료되면 그 결과는 영구적으로 저장되어야 합니다
- 시스템 장애가 발생해도 데이터는 보존됩니다
트랜잭션이 필요한 상황
트랜잭션 없이 데이터베이스 작업을 수행하면 어떤 문제가 발생할까요? 다음 예시를 살펴보겠습니다:
# ❌ 트랜잭션 없는 위험한 코드
def transfer_money(from_account, to_account, amount):
# 출금 계좌에서 차감
from_account.balance -= amount
from_account.save()
# 만약 여기서 에러가 발생하면?
# 네트워크 장애, 서버 크래시, 코드 에러 등
# 입금 계좌에 추가
to_account.balance += amount
to_account.save()
문제점:
- 출금은 성공했는데 입금이 실패하면 돈이 사라집니다
- 중간에 에러가 발생하면 데이터 불일치가 발생합니다
- 동시에 여러 요청이 들어오면 잔액 계산이 틀어질 수 있습니다
# ✅ 트랜잭션을 사용한 안전한 코드
from django.db import transaction
@transaction.atomic
def transfer_money(from_account, to_account, amount):
# 출금 계좌에서 차감
from_account.balance -= amount
from_account.save()
# 어떤 에러가 발생해도 위의 작업이 롤백됩니다
# 입금 계좌에 추가
to_account.balance += amount
to_account.save()
# 모든 작업이 성공해야만 커밋됩니다
해결:
- 모든 작업이 성공하거나 모두 취소됩니다
- 데이터 일관성이 보장됩니다
- 동시성 문제를 제어할 수 있습니다
🛠️ Django에서 트랜잭션 사용하기
Django는 트랜잭션 관리를 위한 강력하고 직관적인 API를 제공합니다. 가장 일반적으로 사용되는 방법들을 알아보겠습니다.
1. 데코레이터 방식: @transaction.atomic
함수나 메서드 전체를 하나의 트랜잭션으로 묶는 가장 간편한 방법입니다.
from django.db import transaction
@transaction.atomic
def create_blog_post(title, content, author):
"""블로그 포스트 생성과 관련 작업을 트랜잭션으로 처리"""
# 포스트 생성
post = Post.objects.create(
title=title,
content=content,
author=author
)
# 작성자 포스트 카운트 증가
author.post_count += 1
author.save()
# 알림 생성
Notification.objects.create(
user=author,
message=f"새 포스트 '{title}'가 게시되었습니다"
)
return post
장점:
- 코드가 간결하고 읽기 쉽습니다
- 함수가 끝나면 자동으로 커밋됩니다
- 예외 발생 시 자동으로 롤백됩니다
2. 컨텍스트 매니저 방식: with transaction.atomic()
코드의 특정 블록만 트랜잭션으로 묶고 싶을 때 사용합니다.
from django.db import transaction
def process_order(order_id):
order = Order.objects.get(id=order_id)
# 주문 정보 조회는 트랜잭션 밖에서
print(f"Processing order: {order.order_number}")
# 중요한 데이터 변경만 트랜잭션으로 보호
with transaction.atomic():
# 재고 차감
for item in order.items.all():
product = item.product
product.stock -= item.quantity
product.save()
# 주문 상태 변경
order.status = 'CONFIRMED'
order.save()
# 결제 처리
Payment.objects.create(
order=order,
amount=order.total_amount,
status='COMPLETED'
)
# 트랜잭션 밖에서 실행 (이메일 전송은 롤백 대상 아님)
send_order_confirmation_email(order)
장점:
- 트랜잭션 범위를 정확하게 제어할 수 있습니다
- 트랜잭션 밖의 작업(메일 발송, 로깅 등)을 분리할 수 있습니다
- 여러 트랜잭션 블록을 독립적으로 관리할 수 있습니다
3. 수동 트랜잭션 제어
더 세밀한 제어가 필요한 경우 수동으로 트랜잭션을 관리할 수 있습니다.
from django.db import transaction
def complex_operation():
# 수동으로 트랜잭션 시작
with transaction.atomic():
# 첫 번째 작업
user = User.objects.create(username='newuser')
try:
# 외부 API 호출
api_result = external_api_call(user.id)
# API 결과에 따라 처리
if api_result['success']:
Profile.objects.create(user=user, data=api_result['data'])
else:
# 명시적으로 롤백
raise Exception("API call failed")
except Exception as e:
# 트랜잭션 블록을 벗어나면 자동 롤백
print(f"Transaction rolled back: {e}")
raise
4. 데이터베이스별 트랜잭션 지정
여러 데이터베이스를 사용하는 경우 특정 DB에 대한 트랜잭션을 지정할 수 있습니다.
from django.db import transaction
# 기본 데이터베이스에 대한 트랜잭션
@transaction.atomic
def save_to_default_db():
User.objects.create(username='user1')
# 특정 데이터베이스에 대한 트랜잭션
@transaction.atomic(using='analytics_db')
def save_analytics_data():
AnalyticsEvent.objects.using('analytics_db').create(
event_type='page_view',
user_id=123
)
# 여러 데이터베이스에 대한 트랜잭션
def multi_db_operation():
with transaction.atomic(): # 기본 DB
User.objects.create(username='user2')
with transaction.atomic(using='analytics_db'): # 분석 DB
AnalyticsEvent.objects.using('analytics_db').create(
event_type='signup'
)
💼 실전 응용 사례
이제 실무에서 자주 접하는 다양한 상황에서 트랜잭션을 어떻게 활용하는지 살펴보겠습니다.
사례 1: 전자상거래 주문 처리
전자상거래에서 주문 처리는 여러 단계를 거칩니다. 각 단계가 모두 성공해야만 주문이 완료됩니다.
from django.db import transaction
from django.core.exceptions import ValidationError
@transaction.atomic
def process_purchase(user, cart_items, payment_method):
"""
주문 처리 프로세스:
1. 재고 확인 및 차감
2. 주문 생성
3. 결제 처리
4. 포인트 적립
"""
# 1단계: 재고 확인 및 차감
order_items = []
total_amount = 0
for cart_item in cart_items:
product = cart_item.product
quantity = cart_item.quantity
# 재고 체크
if product.stock < quantity:
raise ValidationError(
f"{product.name}의 재고가 부족합니다. "
f"(요청: {quantity}, 재고: {product.stock})"
)
# 재고 차감 (동시성 문제 방지를 위해 select_for_update 사용)
product = Product.objects.select_for_update().get(id=product.id)
product.stock -= quantity
product.save()
order_items.append({
'product': product,
'quantity': quantity,
'price': product.price
})
total_amount += product.price * quantity
# 2단계: 주문 생성
order = Order.objects.create(
user=user,
total_amount=total_amount,
status='PENDING'
)
for item in order_items:
OrderItem.objects.create(
order=order,
product=item['product'],
quantity=item['quantity'],
price=item['price']
)
# 3단계: 결제 처리
payment = Payment.objects.create(
order=order,
amount=total_amount,
method=payment_method,
status='PROCESSING'
)
# 외부 결제 게이트웨이 호출
try:
payment_result = payment_gateway.charge(
amount=total_amount,
method=payment_method
)
if not payment_result['success']:
raise ValidationError("결제 처리에 실패했습니다")
payment.transaction_id = payment_result['transaction_id']
payment.status = 'COMPLETED'
payment.save()
except Exception as e:
payment.status = 'FAILED'
payment.save()
raise ValidationError(f"결제 오류: {str(e)}")
# 4단계: 주문 완료 및 포인트 적립
order.status = 'CONFIRMED'
order.save()
points_earned = int(total_amount * 0.01) # 1% 적립
user.points += points_earned
user.save()
PointHistory.objects.create(
user=user,
points=points_earned,
reason=f"주문 #{order.id} 적립"
)
return order
핵심 포인트:
- 재고 차감, 주문 생성, 결제, 포인트 적립이 모두 성공해야 커밋됩니다
- 중간에 실패하면 모든 작업이 롤백되어 데이터 일관성이 유지됩니다
select_for_update()로 동시성 문제를 방지합니다
사례 2: 회원 가입과 프로필 생성
회원 가입 시 User, Profile, 초기 설정 등 여러 테이블에 데이터를 생성해야 합니다.
from django.db import transaction
from django.contrib.auth.models import User
@transaction.atomic
def register_user(username, email, password, profile_data):
"""
회원 가입 프로세스:
1. User 생성
2. Profile 생성
3. 기본 설정 생성
4. 환영 알림 생성
5. 추천인 포인트 지급
"""
# 1. User 생성
user = User.objects.create_user(
username=username,
email=email,
password=password
)
# 2. Profile 생성
profile = Profile.objects.create(
user=user,
full_name=profile_data.get('full_name', ''),
phone=profile_data.get('phone', ''),
birth_date=profile_data.get('birth_date'),
bio=profile_data.get('bio', '')
)
# 3. 기본 설정 생성
UserSettings.objects.create(
user=user,
email_notifications=True,
newsletter=True,
language='ko'
)
# 4. 환영 알림 생성
Notification.objects.create(
user=user,
title="가입을 환영합니다!",
message=f"{username}님, 가입을 환영합니다. 지금 바로 서비스를 이용해보세요.",
type='WELCOME'
)
# 5. 추천인 포인트 지급
referral_code = profile_data.get('referral_code')
if referral_code:
try:
referrer = User.objects.get(profile__referral_code=referral_code)
# 추천인에게 포인트 지급
referrer.points += 1000
referrer.save()
PointHistory.objects.create(
user=referrer,
points=1000,
reason=f"{username}님 추천으로 적립"
)
# 신규 가입자에게도 포인트 지급
user.points = 500
user.save()
PointHistory.objects.create(
user=user,
points=500,
reason="추천 코드 사용 보너스"
)
except User.DoesNotExist:
pass # 추천 코드가 유효하지 않으면 무시
return user
핵심 포인트:
- User, Profile, Settings, Notification 등 여러 모델이 동시에 생성됩니다
- 추천인 시스템처럼 다른 사용자의 데이터도 함께 수정됩니다
- 모든 작업이 원자적으로 처리되어 부분 가입을 방지합니다
사례 3: 금융 거래 - 송금
금융 거래는 트랜잭션이 가장 중요한 영역 중 하나입니다.
from django.db import transaction
from django.core.exceptions import ValidationError
from decimal import Decimal
@transaction.atomic
def transfer_money(from_user, to_user, amount):
"""
송금 프로세스:
1. 잔액 확인
2. 출금 계좌에서 차감
3. 입금 계좌에 추가
4. 거래 내역 기록
"""
# 금액 검증
if amount <= 0:
raise ValidationError("송금 금액은 0보다 커야 합니다")
# 동시성 문제 방지를 위해 row-level lock 획득
from_account = Account.objects.select_for_update().get(user=from_user)
to_account = Account.objects.select_for_update().get(user=to_user)
# 1. 잔액 확인
if from_account.balance < amount:
raise ValidationError(
f"잔액이 부족합니다. "
f"(잔액: {from_account.balance}, 요청: {amount})"
)
# 2. 출금
from_account.balance -= amount
from_account.save()
# 3. 입금
to_account.balance += amount
to_account.save()
# 4. 거래 내역 기록
transaction_record = Transaction.objects.create(
from_account=from_account,
to_account=to_account,
amount=amount,
type='TRANSFER',
status='COMPLETED'
)
# 송금자 거래 내역
TransactionHistory.objects.create(
account=from_account,
transaction=transaction_record,
amount=-amount,
balance_after=from_account.balance,
description=f"{to_user.username}님에게 송금"
)
# 수취인 거래 내역
TransactionHistory.objects.create(
account=to_account,
transaction=transaction_record,
amount=amount,
balance_after=to_account.balance,
description=f"{from_user.username}님으로부터 입금"
)
return transaction_record
핵심 포인트:
select_for_update()로 동시 송금 요청 시 데이터 무결성을 보장합니다- 잔액 확인부터 거래 완료까지 전체가 원자적으로 처리됩니다
- 출금과 입금이 모두 성공해야만 커밋됩니다
사례 4: 재고 관리와 벌크 업데이트
대량의 재고를 한 번에 업데이트해야 하는 경우입니다.
from django.db import transaction
@transaction.atomic
def process_inventory_adjustment(adjustments):
"""
재고 조정 프로세스:
adjustments = [
{'product_id': 1, 'quantity_change': -10, 'reason': 'DAMAGED'},
{'product_id': 2, 'quantity_change': 50, 'reason': 'RESTOCK'},
...
]
"""
adjustment_records = []
for adjustment in adjustments:
product_id = adjustment['product_id']
quantity_change = adjustment['quantity_change']
reason = adjustment['reason']
# Lock을 걸고 제품 가져오기
product = Product.objects.select_for_update().get(id=product_id)
# 현재 재고 기록
old_stock = product.stock
# 재고 변경
new_stock = old_stock + quantity_change
# 음수 재고 방지
if new_stock < 0:
raise ValidationError(
f"제품 '{product.name}'의 재고가 음수가 될 수 없습니다. "
f"(현재: {old_stock}, 변경: {quantity_change})"
)
product.stock = new_stock
product.save()
# 재고 변경 이력 기록
record = InventoryAdjustment.objects.create(
product=product,
old_stock=old_stock,
new_stock=new_stock,
quantity_change=quantity_change,
reason=reason
)
adjustment_records.append(record)
# 재고 부족 알림 체크
if new_stock < product.minimum_stock:
Notification.objects.create(
title=f"재고 부족 알림: {product.name}",
message=f"현재 재고: {new_stock}, 최소 재고: {product.minimum_stock}",
type='LOW_STOCK',
severity='WARNING'
)
return adjustment_records
핵심 포인트:
- 여러 제품의 재고를 한 번에 조정합니다
- 하나라도 실패하면 모든 조정이 롤백됩니다
- 각 조정마다 이력이 기록됩니다
사례 5: 파일 업로드와 데이터베이스 연동
파일 업로드와 데이터베이스 작업을 함께 처리하는 경우입니다.
from django.db import transaction
from django.core.files.storage import default_storage
def upload_document_with_transaction(file, user, metadata):
"""
문서 업로드 프로세스:
1. 파일 저장
2. DB 레코드 생성
3. 사용자 통계 업데이트
"""
file_path = None
try:
with transaction.atomic():
# 1. 파일 저장 (트랜잭션 안에서)
file_path = default_storage.save(
f"documents/{user.id}/{file.name}",
file
)
file_url = default_storage.url(file_path)
# 2. DB 레코드 생성
document = Document.objects.create(
user=user,
title=metadata.get('title', file.name),
file_path=file_path,
file_url=file_url,
file_size=file.size,
file_type=file.content_type,
description=metadata.get('description', '')
)
# 3. 사용자 통계 업데이트
user.document_count += 1
user.total_storage_used += file.size
# 저장공간 제한 체크
if user.total_storage_used > user.storage_limit:
raise ValidationError(
f"저장공간이 부족합니다. "
f"(사용: {user.total_storage_used}, 제한: {user.storage_limit})"
)
user.save()
# 4. 활동 로그 생성
ActivityLog.objects.create(
user=user,
action='DOCUMENT_UPLOADED',
details=f"문서 '{document.title}' 업로드 완료"
)
return document
except Exception as e:
# 트랜잭션 롤백 시 업로드된 파일도 삭제
if file_path and default_storage.exists(file_path):
default_storage.delete(file_path)
raise
핵심 포인트:
- 파일 저장과 DB 작업을 하나의 트랜잭션으로 처리합니다
- 실패 시 업로드된 파일도 함께 삭제합니다
- 저장공간 제한을 체크하여 초과를 방지합니다
사례 6: 배치 작업 - 대량 데이터 처리
대량의 데이터를 처리하되, 일부 실패가 전체에 영향을 주지 않도록 합니다.
from django.db import transaction
def process_bulk_user_updates(user_updates):
"""
대량 사용자 정보 업데이트
각 사용자는 독립적인 트랜잭션으로 처리
"""
success_count = 0
failed_items = []
for update_data in user_updates:
try:
# 각 업데이트를 독립적인 트랜잭션으로 처리
with transaction.atomic():
user_id = update_data['user_id']
user = User.objects.select_for_update().get(id=user_id)
# 사용자 정보 업데이트
if 'email' in update_data:
user.email = update_data['email']
if 'phone' in update_data:
user.profile.phone = update_data['phone']
user.profile.save()
user.save()
# 업데이트 이력 기록
UpdateHistory.objects.create(
user=user,
updated_fields=list(update_data.keys()),
timestamp=timezone.now()
)
success_count += 1
except Exception as e:
# 개별 실패는 기록하고 계속 진행
failed_items.append({
'user_id': update_data.get('user_id'),
'error': str(e)
})
return {
'success_count': success_count,
'failed_count': len(failed_items),
'failed_items': failed_items
}
@transaction.atomic
def process_monthly_subscriptions():
"""
월간 구독 갱신 처리
모든 갱신이 성공해야 커밋
"""
today = timezone.now().date()
# 오늘 갱신할 구독 조회
subscriptions = Subscription.objects.filter(
next_billing_date=today,
status='ACTIVE'
).select_for_update()
processed = []
for subscription in subscriptions:
user = subscription.user
plan = subscription.plan
# 결제 시도
payment = Payment.objects.create(
user=user,
subscription=subscription,
amount=plan.price,
status='PROCESSING'
)
try:
# 결제 처리
result = payment_gateway.charge(
user=user,
amount=plan.price
)
if result['success']:
payment.status = 'COMPLETED'
payment.transaction_id = result['transaction_id']
payment.save()
# 구독 기간 연장
subscription.next_billing_date = today + timedelta(days=30)
subscription.save()
processed.append(subscription.id)
else:
payment.status = 'FAILED'
payment.save()
# 결제 실패 알림
Notification.objects.create(
user=user,
title="구독 결제 실패",
message=f"구독 '{plan.name}' 결제에 실패했습니다.",
type='PAYMENT_FAILED'
)
except Exception as e:
payment.status = 'FAILED'
payment.error_message = str(e)
payment.save()
raise # 트랜잭션 롤백
return processed
핵심 포인트:
- 첫 번째 예제는 각 항목을 독립적으로 처리하여 부분 실패를 허용합니다
- 두 번째 예제는 모든 구독 갱신이 성공해야 커밋됩니다
- 사용 사례에 따라 적절한 트랜잭션 범위를 선택합니다
🚀 고급 트랜잭션 패턴
더 복잡한 시나리오를 위한 고급 트랜잭션 패턴들을 살펴보겠습니다.
1. 중첩 트랜잭션과 세이브포인트
Django는 중첩된 트랜잭션을 세이브포인트를 통해 지원합니다.
from django.db import transaction
@transaction.atomic
def create_blog_post_with_images(post_data, images):
"""
블로그 포스트와 이미지를 생성
이미지 업로드 실패는 허용하되, 포스트는 반드시 생성
"""
# 외부 트랜잭션: 포스트 생성
post = Post.objects.create(
title=post_data['title'],
content=post_data['content'],
author=post_data['author']
)
uploaded_images = []
failed_images = []
# 각 이미지는 독립적인 세이브포인트로 처리
for image in images:
try:
# 내부 트랜잭션 (세이브포인트 생성)
with transaction.atomic():
image_obj = PostImage.objects.create(
post=post,
image=image,
caption=image.name
)
uploaded_images.append(image_obj)
except Exception as e:
# 개별 이미지 실패는 무시하고 계속 진행
failed_images.append({'name': image.name, 'error': str(e)})
# 포스트는 반드시 생성되고, 성공한 이미지들만 연결됨
return {
'post': post,
'uploaded_images': uploaded_images,
'failed_images': failed_images
}
2. 동시성 제어: select_for_update()
여러 사용자가 동시에 같은 데이터를 수정할 때 발생하는 문제를 방지합니다.
from django.db import transaction
@transaction.atomic
def reserve_seat(user, concert_id, seat_number):
"""
콘서트 좌석 예약 (동시성 제어)
"""
# Row-level lock 획득
# nowait=True: 이미 락이 걸려있으면 즉시 에러
# skip_locked=True: 락이 걸린 행은 건너뛰기
try:
seat = Seat.objects.select_for_update(nowait=True).get(
concert_id=concert_id,
seat_number=seat_number
)
except DatabaseError:
raise ValidationError("다른 사용자가 이 좌석을 예약 중입니다.")
# 좌석 상태 확인
if seat.status != 'AVAILABLE':
raise ValidationError("이미 예약된 좌석입니다.")
# 예약 생성
reservation = Reservation.objects.create(
user=user,
seat=seat,
status='CONFIRMED'
)
# 좌석 상태 변경
seat.status = 'RESERVED'
seat.reserved_by = user
seat.reserved_at = timezone.now()
seat.save()
return reservation
def get_available_seats_and_reserve(concert_id, user, seat_count=1):
"""
사용 가능한 좌석을 찾아서 예약
skip_locked를 사용하여 경쟁 없는 좌석 선택
"""
with transaction.atomic():
# 락이 걸리지 않은 좌석만 조회
available_seats = Seat.objects.select_for_update(
skip_locked=True
).filter(
concert_id=concert_id,
status='AVAILABLE'
)[:seat_count]
if len(available_seats) < seat_count:
raise ValidationError(f"사용 가능한 좌석이 {seat_count}개 미만입니다.")
reservations = []
for seat in available_seats:
reservation = Reservation.objects.create(
user=user,
seat=seat,
status='CONFIRMED'
)
seat.status = 'RESERVED'
seat.reserved_by = user
seat.reserved_at = timezone.now()
seat.save()
reservations.append(reservation)
return reservations
3. 낙관적 잠금 (Optimistic Locking)
충돌이 적을 것으로 예상되는 경우 버전 필드를 사용한 낙관적 잠금을 사용할 수 있습니다.
from django.db import models, transaction
from django.core.exceptions import ValidationError
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
version = models.IntegerField(default=0) # 버전 필드
updated_at = models.DateTimeField(auto_now=True)
@transaction.atomic
def update_article_with_optimistic_lock(article_id, new_content, expected_version):
"""
낙관적 잠금을 사용한 아티클 업데이트
"""
article = Article.objects.get(id=article_id)
# 버전 체크
if article.version != expected_version:
raise ValidationError(
"다른 사용자가 이 문서를 수정했습니다. "
"페이지를 새로고침하고 다시 시도해주세요."
)
# 업데이트 수행
article.content = new_content
article.version += 1 # 버전 증가
article.save()
return article
# Django ORM의 F 표현식을 사용한 자동 버전 증가
@transaction.atomic
def update_article_atomic(article_id, new_content):
"""
F 표현식으로 race condition 방지
"""
from django.db.models import F
# 업데이트와 버전 증가를 하나의 쿼리로
updated_count = Article.objects.filter(
id=article_id
).update(
content=new_content,
version=F('version') + 1
)
if updated_count == 0:
raise ValidationError("아티클을 찾을 수 없습니다.")
return Article.objects.get(id=article_id)
4. 트랜잭션 후 작업 수행: on_commit()
트랜잭션이 성공적으로 커밋된 후에만 실행해야 하는 작업이 있습니다.
from django.db import transaction
@transaction.atomic
def create_order_and_send_notification(order_data):
"""
주문 생성 후 알림 전송
트랜잭션이 커밋된 후에만 알림 전송
"""
# 주문 생성
order = Order.objects.create(
user=order_data['user'],
total_amount=order_data['total_amount']
)
# 트랜잭션이 성공적으로 커밋된 후 실행
transaction.on_commit(lambda: send_order_email(order.id))
transaction.on_commit(lambda: send_push_notification(order.user.id, order.id))
transaction.on_commit(lambda: update_external_system(order.id))
return order
def send_order_email(order_id):
"""이메일 발송은 트랜잭션 커밋 후 실행"""
order = Order.objects.get(id=order_id)
# 이메일 발송 로직
print(f"Sending email for order {order_id}")
@transaction.atomic
def process_payment_with_webhook(payment_data):
"""
결제 처리 및 외부 웹훅 호출
"""
payment = Payment.objects.create(
amount=payment_data['amount'],
status='COMPLETED'
)
# 성공 후 외부 API 호출
def notify_external_system():
try:
external_api.notify_payment(payment.id)
except Exception as e:
# 외부 API 실패는 로깅만 하고 트랜잭션에 영향 없음
logger.error(f"External API failed: {e}")
transaction.on_commit(notify_external_system)
return payment
5. 격리 수준 설정
특정 상황에서는 트랜잭션 격리 수준을 조정해야 할 수 있습니다.
from django.db import transaction
# PostgreSQL 격리 수준 설정
def high_consistency_operation():
"""
높은 일관성이 필요한 작업
PostgreSQL의 SERIALIZABLE 격리 수준 사용
"""
from django.db import connection
with transaction.atomic():
# 격리 수준 설정
cursor = connection.cursor()
cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
# 중요한 비즈니스 로직
# ...
pass
# settings.py에서 전역 설정
"""
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'OPTIONS': {
'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE,
},
...
}
}
"""
⚠️ 주의사항과 베스트 프랙티스
1. 트랜잭션 범위는 최소화하기
# ❌ 나쁜 예: 트랜잭션이 너무 큼
@transaction.atomic
def bad_example():
user = User.objects.create(username='newuser')
# 외부 API 호출 (느림)
api_result = external_api.call() # 5초 소요
# 복잡한 계산 (느림)
result = complex_calculation() # 10초 소요
# 파일 업로드 (느림)
upload_to_s3(file) # 3초 소요
user.save()
# 트랜잭션이 18초 동안 유지됨 - 데이터베이스 부하!
# ✅ 좋은 예: 필요한 부분만 트랜잭션으로
def good_example():
# 외부 작업은 트랜잭션 밖에서
api_result = external_api.call()
result = complex_calculation()
s3_url = upload_to_s3(file)
# DB 작업만 트랜잭션으로
with transaction.atomic():
user = User.objects.create(username='newuser')
user.api_data = api_result
user.calculation_result = result
user.file_url = s3_url
user.save()
2. 트랜잭션 내에서 예외 처리하기
# ❌ 나쁜 예: 예외를 삼켜버림
@transaction.atomic
def bad_exception_handling():
try:
User.objects.create(username='user1')
raise Exception("Error!")
except:
pass # 트랜잭션이 커밋되어 부분 데이터 생성!
# ✅ 좋은 예: 예외를 다시 발생시킴
@transaction.atomic
def good_exception_handling():
try:
User.objects.create(username='user1')
risky_operation()
except SpecificError as e:
logger.error(f"Error: {e}")
raise # 트랜잭션 롤백
3. select_for_update() 사용 시 주의사항
# ❌ 나쁜 예: 데드락 위험
@transaction.atomic
def deadlock_risk():
# 사용자 A: 계좌1 -> 계좌2 순서로 락
account1 = Account.objects.select_for_update().get(id=1)
account2 = Account.objects.select_for_update().get(id=2)
# 사용자 B: 계좌2 -> 계좌1 순서로 락
# 데드락 발생!
# ✅ 좋은 예: 일관된 순서로 락 획득
@transaction.atomic
def avoid_deadlock(from_id, to_id):
# 항상 작은 ID부터 락 획득
first_id, second_id = sorted([from_id, to_id])
account1 = Account.objects.select_for_update().get(id=first_id)
account2 = Account.objects.select_for_update().get(id=second_id)
# 안전하게 작업 수행
4. 긴 트랜잭션 피하기
# ❌ 나쁜 예: 대량 데이터를 하나의 트랜잭션으로
@transaction.atomic
def process_all_users():
users = User.objects.all() # 100만 명
for user in users:
user.send_email() # 트랜잭션이 몇 시간 동안 유지!
user.save()
# ✅ 좋은 예: 청크 단위로 처리
def process_users_in_chunks(chunk_size=1000):
user_ids = User.objects.values_list('id', flat=True)
for i in range(0, len(user_ids), chunk_size):
chunk_ids = user_ids[i:i+chunk_size]
with transaction.atomic():
users = User.objects.filter(id__in=chunk_ids)
for user in users:
user.processed = True
user.save()
5. autocommit 모드 이해하기
from django.db import transaction
# Django의 기본 모드는 autocommit
# 각 쿼리가 즉시 커밋됨
User.objects.create(username='user1') # 즉시 커밋
User.objects.create(username='user2') # 즉시 커밋
# autocommit 비활성화 (일반적으로 권장하지 않음)
transaction.set_autocommit(False)
try:
User.objects.create(username='user3')
User.objects.create(username='user4')
transaction.commit() # 수동으로 커밋
except:
transaction.rollback() # 수동으로 롤백
finally:
transaction.set_autocommit(True)
6. 테스트 작성하기
from django.test import TransactionTestCase
from django.db import transaction
class TransactionTests(TransactionTestCase):
"""트랜잭션 테스트"""
def test_transaction_rollback(self):
"""트랜잭션 롤백 테스트"""
initial_count = User.objects.count()
with self.assertRaises(Exception):
with transaction.atomic():
User.objects.create(username='user1')
User.objects.create(username='user2')
raise Exception("Force rollback")
# 롤백되어 카운트가 증가하지 않음
self.assertEqual(User.objects.count(), initial_count)
def test_concurrent_updates(self):
"""동시성 테스트"""
from django.db import connection
from threading import Thread
account = Account.objects.create(balance=1000)
def withdraw(amount):
with transaction.atomic():
acc = Account.objects.select_for_update().get(id=account.id)
acc.balance -= amount
acc.save()
# 두 스레드가 동시에 출금 시도
t1 = Thread(target=withdraw, args=(100,))
t2 = Thread(target=withdraw, args=(200,))
t1.start()
t2.start()
t1.join()
t2.join()
# 잔액이 정확하게 계산됨
account.refresh_from_db()
self.assertEqual(account.balance, 700)
📊 성능 고려사항
트랜잭션 사용 시 성능에 미치는 영향을 이해하는 것이 중요합니다.
트랜잭션 격리 수준별 성능
| 격리 수준 | 동시성 | 일관성 | 사용 사례 |
|---|---|---|---|
| READ UNCOMMITTED | 가장 높음 | 가장 낮음 | 로그 수집, 통계 |
| READ COMMITTED | 높음 | 보통 | 대부분의 일반 작업 |
| REPEATABLE READ | 보통 | 높음 | 금융 거래, 재고 관리 |
| SERIALIZABLE | 가장 낮음 | 가장 높음 | 극도로 중요한 작업 |
성능 최적화 팁
# 1. 불필요한 쿼리 줄이기
@transaction.atomic
def optimized_bulk_create():
"""bulk_create로 한 번에 삽입"""
users = [
User(username=f'user{i}', email=f'user{i}@example.com')
for i in range(1000)
]
User.objects.bulk_create(users, batch_size=100)
# 2. select_related와 prefetch_related 사용
@transaction.atomic
def optimized_query():
"""N+1 쿼리 문제 방지"""
orders = Order.objects.select_related('user').prefetch_related('items')
for order in orders:
# 추가 쿼리 없이 접근 가능
print(order.user.username)
for item in order.items.all():
print(item.product.name)
# 3. 조건부 업데이트
@transaction.atomic
def conditional_update():
"""조건에 맞는 레코드만 업데이트"""
from django.db.models import F
# 재고가 10개 미만인 제품만 재입고
Product.objects.filter(
stock__lt=10
).update(
stock=F('stock') + 100
)
# 4. 인덱스 활용
class Product(models.Model):
sku = models.CharField(max_length=50, db_index=True)
name = models.CharField(max_length=200)
stock = models.IntegerField(db_index=True) # 자주 검색되는 필드
class Meta:
indexes = [
models.Index(fields=['sku', 'stock']), # 복합 인덱스
]
🎯 실전 체크리스트
트랜잭션을 사용할 때 다음 체크리스트를 확인하세요:
설계 단계
- 트랜잭션이 정말 필요한가?
- ACID 속성 중 어떤 것이 중요한가?
- 트랜잭션 범위가 적절한가?
- 동시성 문제가 발생할 수 있는가?
- 예상되는 트랜잭션 시간은?
구현 단계
@transaction.atomic또는with transaction.atomic()사용- 필요한 경우
select_for_update()적용 - 예외 처리가 올바른가?
- 외부 시스템 호출은 트랜잭션 밖에 있는가?
transaction.on_commit()필요한가?
테스트 단계
- 정상 케이스 테스트
- 롤백 케이스 테스트
- 동시성 테스트
- 성능 테스트
- 데드락 시나리오 테스트
모니터링 단계
- 긴 트랜잭션 모니터링
- 데드락 발생 모니터링
- 락 대기 시간 모니터링
- 트랜잭션 롤백 비율 모니터링
🔍 트러블슈팅
자주 발생하는 문제와 해결 방법입니다.
1. “database is locked” 에러 (SQLite)
# SQLite는 동시 쓰기를 지원하지 않음
# 해결책: PostgreSQL이나 MySQL 사용 권장
# 또는 timeout 늘리기
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'OPTIONS': {
'timeout': 20, # 기본값 5초에서 증가
}
}
}
2. 데드락 발생
# 데드락 발생 시 재시도 로직
def retry_on_deadlock(func, max_retries=3):
"""데드락 발생 시 자동 재시도"""
from django.db import OperationalError
import time
for attempt in range(max_retries):
try:
return func()
except OperationalError as e:
if 'deadlock' in str(e).lower() and attempt < max_retries - 1:
time.sleep(0.1 * (2 ** attempt)) # 지수 백오프
continue
raise
@transaction.atomic
def transfer_with_retry(from_id, to_id, amount):
"""데드락 방지 및 재시도"""
return retry_on_deadlock(
lambda: transfer_money(from_id, to_id, amount)
)
3. TransactionManagementError
# 에러: "Transaction managed block ended with pending COMMIT/ROLLBACK"
# ❌ 원인: 중첩된 트랜잭션에서 수동 커밋
@transaction.atomic
def wrong_way():
User.objects.create(username='user1')
transaction.commit() # 에러 발생!
# ✅ 해결: atomic 블록 안에서는 자동으로 처리
@transaction.atomic
def right_way():
User.objects.create(username='user1')
# 블록이 끝나면 자동으로 커밋됨
📚 참고 자료
Django 공식 문서와 추가 학습 자료입니다.
Django 공식 문서
데이터베이스별 특성
- PostgreSQL: 가장 강력한 트랜잭션 지원, MVCC 사용
- MySQL/MariaDB: InnoDB 엔진 필수, REPEATABLE READ가 기본
- SQLite: 파일 기반, 동시 쓰기 제한적
추천 라이브러리
django-db-locking: 추가적인 락 기능django-transaction-hooks: 다양한 트랜잭션 훅django-extensions: 디버깅 도구
🎓 결론
Django의 트랜잭션 관리는 데이터 일관성과 무결성을 보장하는 핵심 기능입니다. 이 글에서 다룬 내용을 정리하면:
핵심 요약
- 트랜잭션의 중요성
- ACID 속성으로 데이터 일관성 보장
- 금융, 전자상거래 등 중요한 비즈니스 로직에 필수
- Django 트랜잭션 사용법
@transaction.atomic데코레이터with transaction.atomic()컨텍스트 매니저- 데이터베이스별 트랜잭션 지정
- 실전 응용 사례
- 전자상거래 주문 처리
- 회원 가입 및 프로필 생성
- 금융 거래 및 송금
- 재고 관리
- 파일 업로드와 DB 연동
- 배치 작업
- 고급 패턴
- 중첩 트랜잭션과 세이브포인트
select_for_update()로 동시성 제어- 낙관적 잠금
transaction.on_commit()활용- 격리 수준 설정
- 베스트 프랙티스
- 트랜잭션 범위 최소화
- 올바른 예외 처리
- 데드락 방지
- 긴 트랜잭션 피하기
- 충분한 테스트 작성
마지막 조언
트랜잭션은 강력한 도구이지만, 과도하게 사용하면 성능 문제를 일으킬 수 있습니다. 다음 원칙을 기억하세요:
- 필요한 곳에만 사용하기: 모든 작업에 트랜잭션이 필요한 것은 아닙니다
- 범위를 최소화하기: 꼭 필요한 작업만 트랜잭션으로 묶으세요
- 테스트하기: 동시성 문제는 프로덕션에서 발견되기 전에 테스트로 찾아내세요
- 모니터링하기: 긴 트랜잭션과 데드락을 지속적으로 모니터링하세요
Django의 트랜잭션 API를 제대로 이해하고 활용하면, 안정적이고 신뢰할 수 있는 애플리케이션을 만들 수 있습니다. 여러분의 프로젝트에서 데이터 일관성이 항상 보장되기를 바랍니다! 🚀