Devin AI 사례 연구: 중견 이커머스 기업의 Django 테스트 스위트 1,200개 파일 자동 마이그레이션 (unittest → pytest)
개요: 3개월 예상 작업을 10일 만에 완료한 Devin AI
중견 이커머스 기업 S사는 Django 기반 백엔드 시스템에서 1,200개 이상의 테스트 파일을 운영하고 있었습니다. 레거시 unittest 프레임워크로 작성된 이 테스트들은 유지보수 비용이 높고, fixture 재사용성이 낮아 pytest로의 전환이 시급했습니다. 수동 마이그레이션 예상 기간은 시니어 엔지니어 3명이 풀타임으로 투입해도 3개월이었습니다. Devin AI를 도입하여 이 작업을 자율적으로 수행한 결과, 단 10일 만에 전체 마이그레이션을 완료했습니다.
프로젝트 배경 및 과제
| 항목 | 상세 |
|---|---|
| 기업 규모 | 직원 350명, 개발팀 45명 |
| 코드베이스 | Django 4.2 / Python 3.11 |
| 테스트 파일 수 | 1,247개 파일, 8,400+ 테스트 케이스 |
| 기존 프레임워크 | unittest + Django TestCase |
| 목표 프레임워크 | pytest + pytest-django + factory_boy |
| CI 환경 | GitHub Actions |
| 예상 수동 작업 기간 | 3개월 (시니어 3명 풀타임) |
| Devin 실제 소요 기간 | 10일 |
setUp/tearDown 패턴을 pytest fixture로 변환- Django TestCase의 self.client, self.assertEqual 등 내장 메서드 변환- 테스트 간 의존성 분석 및 자동 해결- factory_boy 기반 fixture 자동 생성- CI 파이프라인(GitHub Actions) 자동 업데이트
## Devin 설정 및 워크플로우
1단계: Devin 세션 초기화 및 저장소 연결
Devin에 프로젝트를 연결하고 마이그레이션 지시를 제공합니다.
# Devin 세션에서 저장소 연결
Devin Dashboard → New Session → Repository 연결
또는 Slack 연동을 통한 세션 시작
Devin에게 전달한 프롬프트 예시:
"""
Migrate all unittest-based test files in /tests/ directory
from unittest/Django TestCase to pytest.
Requirements:
- Convert all setUp/tearDown to pytest fixtures
- Replace self.assert* with plain assert statements
- Generate factory_boy factories for all Django models
- Update conftest.py with shared fixtures
- Update GitHub Actions CI pipeline
Ensure all tests pass after migration """
2단계: 의존성 설치 및 환경 구성
Devin이 자율적으로 수행한 의존성 설치 과정입니다.
# Devin이 자동으로 실행한 명령어들
pip install pytest pytest-django pytest-cov factory-boy pytest-xdist
pytest.ini 또는 pyproject.toml 자동 생성
pyproject.toml
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = “config.settings.test”
python_files = [“test_.py”, “test.py”]
python_classes = [“Test*”]
python_functions = [“test*”]
addopts = “-v —tb=short —strict-markers -p no:warnings”
markers = [
“slow: marks tests as slow”,
“integration: marks integration tests”,
]
3단계: 자동 변환 패턴
Devin이 적용한 핵심 변환 패턴의 예시입니다.
변환 전 (unittest)
from django.test import TestCase
from myapp.models import Product, Category
class ProductTestCase(TestCase):
def setUp(self):
self.category = Category.objects.create(
name=“Electronics”, slug=“electronics”
)
self.product = Product.objects.create(
name=“Laptop”, price=1200000,
category=self.category, stock=50
)
def test_product_str(self):
self.assertEqual(str(self.product), "Laptop")
def test_product_discount(self):
self.product.apply_discount(10)
self.assertEqual(self.product.price, 1080000)
def tearDown(self):
Product.objects.all().delete()
Category.objects.all().delete()</code></pre>
변환 후 (pytest + factory_boy)
import pytest
from tests.factories import ProductFactory, CategoryFactory
@pytest.fixture
def category(db):
return CategoryFactory(name="Electronics", slug="electronics")
@pytest.fixture
def product(db, category):
return ProductFactory(
name="Laptop", price=1200000,
category=category, stock=50
)
def test_product_str(product):
assert str(product) == "Laptop"
def test_product_discount(product):
product.apply_discount(10)
assert product.price == 1080000
자동 생성된 Factory 예시
# tests/factories.py (Devin이 자동 생성)
import factory
from myapp.models import Product, Category
class CategoryFactory(factory.django.DjangoModelFactory):
class Meta:
model = Category
name = factory.Sequence(lambda n: f"Category {n}")
slug = factory.LazyAttribute(lambda o: o.name.lower().replace(" ", "-"))
class ProductFactory(factory.django.DjangoModelFactory):
class Meta:
model = Product
name = factory.Sequence(lambda n: f"Product {n}")
price = factory.Faker("pyint", min_value=10000, max_value=5000000)
category = factory.SubFactory(CategoryFactory)
stock = factory.Faker("pyint", min_value=0, max_value=1000)
4단계: CI 파이프라인 자동 업데이트
# .github/workflows/tests.yml (Devin이 자동 수정)
name: Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: test_db
POSTGRES_PASSWORD: postgres
ports: ["5432:5432"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -r requirements/test.txt
- run: pytest --cov=myapp --cov-report=xml -n auto
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
결과 및 성과 지표
지표 마이그레이션 전 마이그레이션 후 개선율 테스트 실행 시간 47분 12분 (pytest-xdist 병렬) 74% 단축 Fixture 코드 중복 ~3,200줄 중복 공유 fixture 420줄 87% 감소 CI 파이프라인 시간 58분 18분 69% 단축 테스트 통과율 100% (8,412개) 100% (8,412개) 무손실 전환 소요 기간 3개월 (예상) 10일 (실제) 89% 단축 투입 인력 시니어 3명 시니어 1명 (리뷰 전담) 67% 절감
## Pro Tips: Devin 활용 고급 팁
- **배치 분할 전략:** 1,200개 파일을 한 번에 지시하지 말고, 앱 모듈 단위(예: /tests/orders/, /tests/products/)로 세션을 분리하면 정확도가 향상됩니다.- **Knowledge Base 활용:** 프로젝트의 코딩 컨벤션 문서를 Devin Knowledge에 등록해두면 변환된 코드가 팀 스타일에 맞게 생성됩니다.- **점진적 검증:** 각 모듈 변환 후 pytest --tb=long -x로 첫 실패에서 중단하게 설정하여 Devin이 즉시 수정하도록 유도하세요.- **Snapshot 리뷰:** Devin의 각 PR을 작은 단위로 생성하게 하면 코드 리뷰 부담이 크게 줄어듭니다. 세션 프롬프트에 Create separate PRs per Django app module을 명시하세요.- **conftest.py 계층 구조:** Devin에게 공유 fixture는 루트 conftest에, 앱별 fixture는 각 앱 디렉토리 conftest에 배치하도록 명시하면 fixture 충돌을 방지할 수 있습니다.
## Troubleshooting: 자주 발생하는 문제와 해결법
문제 1: Django DB 접근 오류
# 오류 메시지
E Failed: Database access not allowed, use the "django_db" mark,
or the "db" fixture to enable it.
# 해결: Devin 프롬프트에 다음을 추가
"Ensure all test functions that access the database
use @pytest.mark.django_db or receive db fixture"
문제 2: self.client 참조 변환 누락
# 오류 메시지
NameError: name 'self' is not defined
# 해결: client fixture 사용 명시
@pytest.fixture
def api_client():
from django.test import Client
return Client()
def test_homepage(api_client):
response = api_client.get("/")
assert response.status_code == 200
문제 3: fixture 순환 의존성
# 오류 메시지
RECURSIONERROR: fixture 'order' and 'product' have circular dependency
# 해결: Devin에게 의존성 그래프를 먼저 분석하도록 지시
"Before converting, analyze fixture dependencies and
resolve circular references by extracting shared state
into independent base fixtures"
핵심 교훈
- 자율적 의존성 해결 능력: Devin은 테스트 파일 간 의존 관계를 자체 분석하여 변환 순서를 최적화했습니다.- 인간은 리뷰에 집중: 엔지니어가 변환 작업 대신 코드 리뷰에 집중하면서 코드 품질이 오히려 향상되었습니다.- CI 통합의 중요성: 테스트 코드 변환과 CI 파이프라인 업데이트를 동시에 처리하여 배포 중단 없이 전환했습니다.
자주 묻는 질문 (FAQ)
Q1: Devin이 변환한 코드의 정확도는 어느 정도인가요?
본 사례에서 Devin의 초기 변환 정확도는 약 94%였습니다. 나머지 6%는 복잡한 mock 체인이나 커스텀 TestRunner를 사용한 테스트로, Devin이 1~2회 추가 반복(iteration)을 통해 자체 수정했습니다. 최종적으로 8,412개 전체 테스트가 100% 통과했으며, 시니어 엔지니어 1명이 리뷰를 수행하여 품질을 보증했습니다.
Q2: 커스텀 테스트 헬퍼나 복잡한 mixin이 있는 경우에도 자동 변환이 가능한가요?
가능합니다. 다만 복잡한 mixin이나 다중 상속 패턴이 적용된 테스트 클래스의 경우, Devin 세션 시작 시 해당 mixin의 역할과 변환 방향을 명시적으로 설명하는 것이 좋습니다. 예를 들어 “Convert AuthenticatedTestMixin to a pytest fixture named authenticated_client that returns a logged-in client instance”와 같이 구체적으로 지시하면 변환 정확도가 크게 향상됩니다.
Q3: 마이그레이션 중 기존 CI가 중단되지 않도록 하려면 어떻게 해야 하나요?
점진적 마이그레이션 전략을 권장합니다. pytest는 unittest 기반 테스트도 실행할 수 있으므로, CI를 먼저 pytest runner로 전환한 후 테스트 파일을 모듈 단위로 변환합니다. Devin에게 “Maintain backward compatibility: ensure both unittest and pytest styles can coexist during migration”을 지시하면 변환 중에도 CI가 정상 동작합니다. 본 사례에서도 10일간 모든 CI 빌드가 green 상태를 유지했습니다.