Django 모놀리스 최적화: 보고서 로딩 30초에서 142ms로 단축하기
레거시 Django 프로젝트에서 지점별 보고서가 28만 개의 주문 레코드를 가진 테이블에서 순차 스캔으로 인해 30초나 걸려 로딩되고 있었습니다. EXPLAIN ANALYZE 결과:
Seq Scan on orders (cost=0.00..18420.00 rows=2841 width=847)
Filter: (branch_id = 42)
Rows Removed by Filter: 284100
Execution Time: 28340 ms
ORM은 관련 객체에 대한 N+1 문제로 인해 2800개 이상의 쿼리를 생성하고 있었습니다. 컨트롤러에 배치된 비즈니스 로직이 부하를 가중시켰습니다.
원본 쿼리:
SELECT * FROM orders
JOIN order_items ON orders.id = order_items.order_id
JOIN products ON order_items.product_id = products.id
WHERE orders.branch_id = 42;
N+1 문제 해결
N+1은 지수적으로 부하를 증가시켰습니다. 개선 전:
orders = Order.objects.filter(branch_id=branch_id)
for order in orders:
items = order.order_items.all() # N+1
for item in items:
product = item.product # N+1
prefetch 적용 후:
orders = Order.objects.filter(
branch_id=branch_id
).select_related('customer').prefetch_related('order_items__product')
쿼리 수가 3개로 감소했습니다. select_related는 ForeignKey에 JOIN을 사용하고, prefetch_related는 ManyToMany 및 역관계에 IN 쿼리를 사용합니다.
테이블 인덱싱
N+1 해결 후에도 순차 스캔 문제가 남아 있었습니다. 다운타임 없이 인덱스를 추가했습니다:
CREATE INDEX CONCURRENTLY idx_orders_branch_created ON orders(branch_id, created_at DESC);
CREATE INDEX CONCURRENTLY idx_products_search ON products USING GIN(to_tsvector('english', name));
새로운 실행 계획:
Index Scan using idx_orders_branch_created on orders
Index Cond: (branch_id = 42)
Execution Time: 142 ms
실행 시간이 28초에서 142ms로 단축되었습니다. CONCURRENTLY는 프로덕션 환경에서 테이블 잠금을 방지합니다.
인덱싱 핵심 권장사항:
- (필터 + 정렬)에 대한 복합 인덱스
- 전체 텍스트 검색에는 GIN 인덱스
- 추가 전 항상 EXPLAIN ANALYZE 확인
DDD로의 리팩토링
컨트롤러에 배치된 비즈니스 로직은 기술 부채를 생성했습니다. 도메인과 유스케이스를 프레임워크와 독립적으로 추출했습니다.
유스케이스 예시:
class GetBranchReportUseCase:
def __init__(self, repo: OrderRepository):
self._repo = repo
def execute(self, branch_id, period) -> BranchReport:
orders = self._repo.get_by_branch_and_period(branch_id, period)
return BranchReport.from_orders(orders)
뷰는 얇아졌습니다:
class BranchReportView(APIView):
def get(self, request, branch_id):
use_case = GetBranchReportUseCase(DjangoOrderRepository())
report = use_case.execute(branch_id, DateRange.from_request(request))
return Response(BranchReportSerializer(report).data)
유스케이스는 Django와 데이터베이스 없이 모킹된 저장소로 테스트됩니다. 신규 기능의 출시 시간이 절반으로 단축되었습니다.
코드 품질 관리
추가된 항목:
- Mypy strict 모드:
[mypy]
strict = true
disallow_untyped_defs = true
warn_return_any = true
- Pytest with coverage ≥87%, GitLab CI에서 품질 게이트 차단.
- 조기 오류 감지로 MTTD 40% 감소.
개선 지표
| 지표 | 개선 전 | 개선 후 |
|--------|--------|-------|
| 보고서 시간 | 30초 | 1.5초 |
| DB CPU | 80% | 32% |
| 페이지당 쿼리 수 | 2800+ | 3 |
| 기능 출시 시간 | X | X/2 |
| MTTD | - | -40% |
핵심 요약
- 성능 저하 시 EXPLAIN ANALYZE가 첫 번째 단계
- N+1은 부하 시 치명적, prefetch_related/select_related 필수
- (branch_id, created_at) 복합 인덱스로 200배 속도 향상
- DDD는 비즈니스 로직을 격리하여 개발 및 테스트 가속화
- Mypy + pytest + CI 게이트로 오버헤드 없이 MTTD 감소
— Editorial Team
아직 댓글이 없습니다.