홈으로 돌아가기

Django 최적화: 30초에서 142ms로

Django 모놀리스 최적화 사례: N+1 제거, 인덱스 추가 및 DDD 구현으로 보고서 시간 30초에서 142ms로 단축. DB CPU 60% 하락, 쿼리 — 2800에서 3으로. Use Cases 구조가 기능 개발 가속화.

Django 보고서 200배 속도 향상 방법: 사례
Advertisement 728x90

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개 이상의 쿼리를 생성하고 있었습니다. 컨트롤러에 배치된 비즈니스 로직이 부하를 가중시켰습니다.

원본 쿼리:

Google AdInline article slot
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 쿼리를 사용합니다.

Google AdInline article slot

테이블 인덱싱

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는 프로덕션 환경에서 테이블 잠금을 방지합니다.

Google AdInline article slot

인덱싱 핵심 권장사항:

  • (필터 + 정렬)에 대한 복합 인덱스
  • 전체 텍스트 검색에는 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

Advertisement 728x90

다음 읽기