python/Django

transaction.atomic() 트랙잭션 처리하기

Balang 2025. 4. 30. 16:17
728x90

회원가입 시 사용자(User)와 프로필(Profile) 데이터를
같은 요청에서 동시에 저장하려고 했습니다.

 

처음에는 별 문제가 없었는데

중간에 Profile.objects.create() 단계에서 오류가 나면

User는 저장됐고 Profile은 저장이 안 되는 데이터 불일치 현상이 발생했습니다.

 

즉, 가입은 성공 했는데 프로필이 없는 유저가 생겨난 겁니다.

 

원인 분석

Django ORM은 기본적으로 각 쿼리를 독립적으로 실행합니다.

따라서 여러 개 쿼리를 실행하다가

중간에 하나라도 실패하면 앞에서 실행된 쿼리는 그대로 DB에 남게 됩니다.

→ 트랜잭션을 명시적으로 걸지 않으면, 중간 실패 시 롤백이 일어나지 않습니다.

 

예를 들어

django.db.utils.IntegrityError: NOT NULL constraint failed: profile.user_id

이런식으로 프로필 생성에 실패해도 이미 저장된 User는 DB에 남아 있게 되는겁니다.

 

위험성을 보게되면

  • 데이터 불일치 (User는 있는데 Profile이 없다)
  • 시스템 복구 난이도 Up (수동으로 이상 데이터를 찾아야 함)
  • 서비스 품질 저하 (가입된 줄 알았는데, 실제로는 반쯤 등록된 상태)
  • 장애 발생 시 대규모 데이터 오류

 

해당 문제를 해결하기 위해서는 아래와 같이 진

  • transaction.atomic()으로 모든 작업 묶기
    • 하나라도 실패하면 전부 롤백
from django.db import transaction

def create_user_and_profile(user_data, profile_data):
    try:
        with transaction.atomic():
            user = User.objects.create(**user_data)
            Profile.objects.create(user=user, **profile_data)
    except Exception as e:
        logger.error(f"회원가입 실패: {str(e)}")
        raise

 

 

  • savepoint를 명식적으로 관리
    • 필요한 경우 savepoint 단위로 롤백 제어
from django.db import transaction

def create_user_and_profile(user_data, profile_data):
    try:
        with transaction.atomic():
            user = User.objects.create(**user_data)
            sid = transaction.savepoint()  # savepoint 생성
            try:
                Profile.objects.create(user=user, **profile_data)
            except Exception:
                transaction.savepoint_rollback(sid)  # Profile 저장 실패 시 savepoint까지 롤백
                raise
    except Exception as e:
        logger.error(f"회원가입 실패: {str(e)}")
        raise

 

 

  • 예외처리 (try-except)와 함께 사용
    • 반드시 except 블럭에서 오류 처리 + 로깅
  • atomic 블럭 내부에서만 DB 조작
    • 블럭 밖에서 DB 작업하면 트랜잭션 관리가 깨질 수 있다.

 

  • Decorator 사용 예시
from django.db import transaction
from django.views.decorators.http import require_POST

@require_POST
@transaction.atomic
def signup_view(request):
    # request.POST에서 데이터 추출
    # User와 Profile 생성
    ...

 

 

함수형 뷰에서는 @transaction.atomic 데코레이터로 깔끔하게 적용 가능하다.

 

다만 주의 할 점이 있다.

  • 트랜잭션 블럭 안에서는 가능한 한 최소 작업만 진행
    • 오래 걸리면 DB 락이 길어져서 다른 쿼리에 영향을 줄 수 있다.
  • 외부 API 호출은 트랜잭션 블럭 밖에서 진행
    • 외부 통신 지연 때문에 DB 트랜잭션을 불필요하게 오래 잡고 있을 수 있다.
  • atomic() 블럭 중첩 시 내부 블럭이 실패해도 외부 블럭까지 롤백된다. (주의)
  • Celery 같은 비동기 작업과 트랜잭션은 잘 분리해서 다루자.
  • bulk_create/bulk_update 등 대량 작업도 atomic() 안에서 묶어야 안전하다.

 

Django에서 여러 DB 작업을 할 때는 반드시 transaction.atomic()으로 트랜잭션을 묶어야 합니다..
하나라도 실패하면 전체를 롤백해서 데이터 일관성을 지키는 것이 필수입니다..

728x90
반응형