Django TransactionTestCase 알아보기

한줄 요약: TestCase는 테스트 함수 전체를 트랜잭션으로 감싸기 때문에, 내부의 transaction.atomic()
은 단지 세이브포인트일 뿐이고, 완전히 독립된 트랜잭션이 아니므로 트랜잭션이나 비관적 락 테스트에는 의미가 없다.
Django에서 데이터베이스 연관 테스트를 작성할 때 주로 사용하는 두 가지 클래스는 TestCase
와 TransactionTestCase
입니다. 두 클래스의 근본적인 차이점을 실제 코드 예시와 함께 명확히 비교해 보겠습니다.
기본 개념 정리
TestCase: 각 테스트 메서드를 하나의 트랜잭션으로 감싸서 테스트 종료 후 롤백합니다. 이로 인해 테스트가 빠르고 데이터가 격리되지만, 트랜잭션 동작(예: 락 충돌)을 정확히 테스트하기 어렵습니다.
TransactionTestCase: 실제 트랜잭션을 사용하여 테스트하며, 매 테스트가 끝나면 데이터베이스를 완전히 초기화합니다. 트랜잭션 관련 동작을 정확히 테스트할 수 있으나 성능 부담이 있습니다.
TestCase에서 발생하는 문제 - 예시 코드 분석
from django.test import TestCase
from django.db import transaction
from django.contrib.auth import get_user_model
User = get_user_model()
class TestLockingWithTestCase(TestCase):
def test_select_for_update(self):
instance = User.objects.create(username="testuser", password="password")
with transaction.atomic():
locked_instance = User.objects.select_for_update().get(pk=instance.pk)
try:
with transaction.atomic():
User.objects.select_for_update(nowait=True).get(pk=instance.pk)
except Exception as e:
# 실제로 여기에 도달할 일은 없다. 😅
self.fail(f"트랜잭션 내부 충돌이 제대로 발생하지 않음: {e}")
TestCase에서 비관적 락이 제대로 동작하지 않는 이유
위 코드에서 비관적 락(select_for_update)이 제대로 작동하지 않는 이유는 다음과 같습니다.
트랜잭션 세이브포인트(Savepoint): Django의 중첩된
transaction.atomic()
은 실제 독립된 트랜잭션이 아니라, 기존 트랜잭션의 세이브포인트입니다.동일 트랜잭션 컨텍스트 공유: 내부의
transaction.atomic()
이 외부의transaction.atomic()
과 동일한 트랜잭션 컨텍스트와 데이터베이스 연결을 공유합니다.락 충돌 미발생: 같은 트랜잭션 컨텍스트 내에서는 이미 획득한 락이 있기 때문에 충돌이 발생하지 않고 중복으로 락을 획득할 수 있습니다.
TransactionTestCase를 활용한 올바른 테스트 방법 - 코드 예시
from django.test import TransactionTestCase
from django.db import transaction
from django.contrib.auth import get_user_model
from threading import Thread
User = get_user_model()
class TestLockingWithTransactionTestCase(TransactionTestCase):
def test_select_for_update_conflict(self):
instance = User.objects.create(username="testuser", password="password")
with transaction.atomic():
User.objects.select_for_update().get(pk=instance.pk)
def conflicting_transaction():
with transaction.atomic():
try:
User.objects.select_for_update(nowait=True).get(pk=instance.pk)
# 실제로 여기에 도달할 일은 없다. 😅
self.fail("락 충돌이 발생하지 않음")
except Exception:
pass # 정상적인 락 충돌
thread = Thread(target=conflicting_transaction)
thread.start()
thread.join()
TransactionTestCase가 제대로 동작하는 이유
위 코드가 제대로 동작하는 이유는 다음과 같습니다:
별도 스레드 사용: 별도의 스레드를 사용하면 각 스레드는 별도의 데이터베이스 연결을 생성합니다. 이는 완전히 독립된 트랜잭션 컨텍스트를 만들어 메인 스레드의 트랜잭션과 실제 충돌을 유발할 수 있습니다.
트랜잭션 격리: 두 트랜잭션이 완전히 독립된 컨텍스트에서 실행되어 락 충돌이 데이터베이스 수준에서 정확히 발생합니다.
TransactionTestCase 활용: 각 테스트마다 실제 트랜잭션이 새롭게 시작되고, 종료 시 실제 데이터베이스를 초기화하여 더 현실적인 운영 환경을 시뮬레이션할 수 있습니다.
TestCase와 TransactionTestCase 비교 요약
특징 | TestCase | TransactionTestCase |
트랜잭션 동작 | 하나의 큰 트랜잭션에서 롤백 | 각 테스트마다 실제 트랜잭션 사용 |
비관적 락(select_for_update) | 정확한 충돌 재현 불가능 | 정확한 충돌 재현 가능 |
데이터베이스 연결 | 동일 연결 및 트랜잭션 컨텍스트 공유 | 별도의 독립적인 연결 및 트랜잭션 컨텍스트 |
성능 | 빠름 (롤백 방식) | 느림 (트랜잭션 및 truncate 방식) |
추천 사용 사례 | 일반 유닛 테스트 | 트랜잭션 및 락 동작을 정확히 테스트할 때 |
결론
TestCase
는 빠르고 효율적이지만, 트랜잭션과 락 충돌을 정확히 테스트하려면 TransactionTestCase
를 사용해야 합니다. 비관적 락이나 동시성 문제를 테스트할 때는 별도의 스레드와 TransactionTestCase
를 사용하는 방식이 필수적입니다.
Subscribe to my newsletter
Read articles from 조정환 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
