# Django 면접 질문: ORM, 미들웨어, DRF 심층 분석 > Django 면접에서 자주 출제되는 ORM 최적화(select_related와 prefetch_related), 미들웨어 아키텍처, Django REST Framework 시리얼라이저 성능, 권한 설정, 페이지네이션 패턴을 상세히 다룹니다. - Published: 2026-04-03 - Updated: 2026-04-03 - Author: SharpSkill - Tags: django, python, interview, orm, middleware, drf, rest-api - Reading time: 9 min --- Django 면접에서는 시니어 엔지니어와 그 외 지원자를 구분하는 세 가지 핵심 영역이 있습니다. ORM 최적화, 미들웨어 아키텍처, 그리고 Django REST Framework(DRF) 설계 패턴입니다. 이 글에서는 2026년 채용 면접에서 실제로 출제되는 질문들을 Django 5.2 LTS와 DRF 3.17의 프로덕션 수준 코드 예제와 함께 분석합니다. > **면접관이 실제로 평가하는 것** > > Django 면접에서 기본적인 CRUD를 묻는 경우는 거의 없습니다. 현재 면접의 초점은 QuerySet 최적화(select_related vs prefetch_related), 커스텀 미들웨어 설계, DRF 시리얼라이저 성능에 맞춰져 있습니다. N+1 쿼리 문제를 설명하고 효율적인 ViewSet을 작성할 수 있는 지원자가 제네릭 뷰만 아는 지원자보다 압도적으로 높은 평가를 받습니다. ## Django ORM 면접 질문: QuerySet 최적화 Django ORM 면접에서 가장 많이 출제되는 질문은 N+1 문제에 관한 것입니다. 외래 키 관계를 가진 모델에서 `select_related`와 `prefetch_related`를 어떻게 구분하여 사용하는지가 핵심입니다. ```python # models.py from django.db import models class Company(models.Model): name = models.CharField(max_length=200) founded_year = models.IntegerField() class Developer(models.Model): name = models.CharField(max_length=200) company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="developers") skills = models.ManyToManyField("Skill", related_name="developers") class Skill(models.Model): name = models.CharField(max_length=100) category = models.CharField(max_length=50) ``` 두 메서드의 차이점은 탐색하는 관계의 유형에 따라 결정됩니다. ```python # queries.py — Correct approach for ForeignKey (single object) developers = Developer.objects.select_related("company").all() # Generates ONE SQL query with JOIN # SELECT developer.*, company.* FROM developer INNER JOIN company ... # Correct approach for ManyToMany (multiple objects) developers = Developer.objects.prefetch_related("skills").all() # Generates TWO SQL queries: # 1. SELECT * FROM developer # 2. SELECT * FROM skill INNER JOIN developer_skills WHERE developer_id IN (...) ``` `select_related`는 SQL JOIN을 수행하며 ForeignKey와 OneToOneField에서 동작합니다. `prefetch_related`는 별도의 쿼리를 실행하며 ManyToManyField와 역방향 ForeignKey 관계에서 동작합니다. 이 둘을 혼동하면 대규모 데이터셋에서 불필요한 JOIN이 발생하거나, 심각한 N+1 패턴이 나타납니다. ## 고급 ORM: 커스텀 Manager와 QuerySet 체이닝 면접관은 비즈니스 로직을 캡슐화하는 커스텀 Manager 구현을 요구하는 경우가 많습니다. Django의 매니저 패턴을 이해하고 재사용 가능한 쿼리 인터페이스를 작성할 수 있는지가 평가 포인트입니다. ```python # managers.py from django.db import models from django.utils import timezone class ActiveDeveloperQuerySet(models.QuerySet): def active(self): """Filter developers who logged in within the last 30 days.""" cutoff = timezone.now() - timezone.timedelta(days=30) return self.filter(last_login__gte=cutoff) def senior(self): """Filter developers with 5+ years of experience.""" return self.filter(years_experience__gte=5) def by_skill(self, skill_name): """Filter developers by a specific skill.""" return self.filter(skills__name=skill_name) class ActiveDeveloperManager(models.Manager): def get_queryset(self): return ActiveDeveloperQuerySet(self.model, using=self._db) def active(self): return self.get_queryset().active() ``` 자주 나오는 후속 질문은 "Manager만 사용하지 않고 커스텀 QuerySet을 사용하는 이유는 무엇입니까?"입니다. 정답은 체이닝에 있습니다. 커스텀 QuerySet 메서드는 연쇄적으로 호출할 수 있지만, Manager 메서드는 첫 번째 호출 이후에 체이닝이 불가능합니다. ```python # usage.py — QuerySet chaining in action # This works because each method returns a QuerySet senior_python_devs = ( Developer.active_objects # custom manager .active() # ActiveDeveloperQuerySet method .senior() # chains another QuerySet method .by_skill("Python") # chains a third method .select_related("company") # standard QuerySet method still works ) ``` > **Django 5.2 LTS: 복합 기본 키** > > Django 5.2에서 CompositePrimaryKey가 도입되었습니다. 다중 컬럼 기본 키가 필요한 모델에서 오랫동안 기다려온 기능입니다. 이 기능에 관한 면접 질문이 증가하는 추세이며, 특히 레거시 데이터베이스나 데이터 웨어하우스 스키마를 다루는 지원자에게 자주 출제됩니다. ## Django 미들웨어 면접 질문: 요청-응답 파이프라인 미들웨어 관련 질문은 Django의 요청-응답 라이프사이클에 대한 이해도를 평가합니다. 대표적인 질문은 "미들웨어가 요청과 응답을 처리하는 순서를 설명하십시오"입니다. 정답은 명확한 패턴을 따릅니다. 요청 시에는 `MIDDLEWARE`에 정의된 순서대로 위에서 아래로 미들웨어 클래스가 실행됩니다. 응답 시에는 아래에서 위로 실행됩니다. 이 양파 레이어 아키텍처 덕분에 목록의 첫 번째 미들웨어가 모든 것을 감싸는 형태가 됩니다. ```python # middleware.py import time import logging from django.http import JsonResponse logger = logging.getLogger(__name__) class RequestTimingMiddleware: """Logs the time taken to process each request.""" def __init__(self, get_response): self.get_response = get_response # next middleware or view def __call__(self, request): start_time = time.monotonic() response = self.get_response(request) # passes to next layer duration_ms = (time.monotonic() - start_time) * 1000 logger.info( "method=%s path=%s status=%d duration=%.2fms", request.method, request.path, response.status_code, duration_ms, ) return response ``` 자주 나오는 후속 질문은 "미들웨어 체인을 쇼트서킷하려면 어떻게 합니까?"입니다. `__call__`에서 `self.get_response(request)`를 호출하기 전에 HttpResponse를 반환하면 체인 전체가 중단됩니다. 나머지 미들웨어와 뷰는 실행되지 않습니다. ```python # middleware.py — Rate limiting with short-circuit from django.core.cache import cache class RateLimitMiddleware: """Blocks requests exceeding 100 per minute per IP.""" def __init__(self, get_response): self.get_response = get_response def __call__(self, request): ip = request.META.get("REMOTE_ADDR") cache_key = f"rate_limit:{ip}" request_count = cache.get(cache_key, 0) if request_count >= 100: return JsonResponse( # short-circuits — view never executes {"error": "Rate limit exceeded. Try again in 60 seconds."}, status=429, ) cache.set(cache_key, request_count + 1, timeout=60) return self.get_response(request) ``` ## 미들웨어 훅: process_view, process_exception, process_template_response `__call__` 외에도 Django 미들웨어는 세 가지 선택적 훅 메서드를 지원합니다. 면접관은 이 훅들을 통해 지원자의 지식 깊이를 측정합니다. ```python # middleware.py — Full middleware with all hooks class AuditMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): return self.get_response(request) def process_view(self, request, view_func, view_args, view_kwargs): """Called after URL resolution, before the view executes.""" logger.info("Calling view: %s", view_func.__name__) return None # returning None continues normal processing def process_exception(self, request, exception): """Called only if the view raises an exception.""" logger.error("View exception: %s", exception, exc_info=True) return None # returning None lets Django's default handling proceed def process_template_response(self, request, response): """Called if the response has a render() method (TemplateResponse).""" response.context_data["audit_timestamp"] = time.time() return response # must return a response with render() ``` `process_view`는 URL 해석 후, 뷰 실행 전에 호출됩니다. `None`을 반환하면 실행이 계속되고, `HttpResponse`를 반환하면 쇼트서킷됩니다. `process_exception`은 처리되지 않은 예외가 발생한 경우에만 호출됩니다. `process_template_response`는 `TemplateResponse` 객체에 대해서만 호출되며, 일반 `HttpResponse`에서는 호출되지 않습니다. ## DRF 면접 질문: 시리얼라이저 성능 [Django REST Framework](https://www.django-rest-framework.org/) 시리얼라이저 관련 질문은 중첩된 직렬화와 그에 따른 성능 영향에 초점을 맞추고 있습니다. 가장 자주 출제되는 질문은 "N+1 쿼리를 발생시키지 않으면서 중첩된 관계를 어떻게 처리합니까?"입니다. ```python # serializers.py from rest_framework import serializers from .models import Developer, Company, Skill class SkillSerializer(serializers.ModelSerializer): class Meta: model = Skill fields = ["name", "category"] class CompanySerializer(serializers.ModelSerializer): class Meta: model = Company fields = ["name", "founded_year"] class DeveloperSerializer(serializers.ModelSerializer): company = CompanySerializer(read_only=True) # nested FK skills = SkillSerializer(many=True, read_only=True) # nested M2M class Meta: model = Developer fields = ["id", "name", "company", "skills"] ``` 시리얼라이저만으로는 성능 문제가 해결되지 않습니다. ViewSet에서 쿼리셋을 최적화해야 합니다. ```python # views.py from rest_framework import viewsets from .models import Developer from .serializers import DeveloperSerializer class DeveloperViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = DeveloperSerializer def get_queryset(self): return ( Developer.objects .select_related("company") # JOIN for FK .prefetch_related("skills") # separate query for M2M .order_by("-id") ) ``` ViewSet에서 `select_related`와 `prefetch_related`를 지정하지 않으면, 직렬화되는 각 developer마다 company와 skills에 대한 개별 쿼리가 발생합니다. 50명의 developer 목록에서는 3개의 쿼리로 충분한 것이 1 + 50 + 50 = 101개의 쿼리가 됩니다. ## DRF 커스텀 권한과 인증 DRF 면접에서 자주 출제되는 질문으로 "사용자의 역할과 객체의 소유자를 기반으로 접근을 제한하는 커스텀 권한을 작성하십시오"가 있습니다. ```python # permissions.py from rest_framework.permissions import BasePermission class IsOwnerOrAdmin(BasePermission): """ Object-level permission: - Admin users can access any object - Regular users can only access objects they own """ message = "Access restricted to the object owner or admin users." def has_object_permission(self, request, view, obj): if request.user.is_staff: return True # Assumes the model has an 'owner' field pointing to User return obj.owner == request.user ``` ```python # views.py — Applying custom permissions from rest_framework import viewsets, permissions from .permissions import IsOwnerOrAdmin class ProjectViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated, IsOwnerOrAdmin] def get_queryset(self): # Non-admin users only see their own projects if self.request.user.is_staff: return Project.objects.all() return Project.objects.filter(owner=self.request.user) def perform_create(self, serializer): serializer.save(owner=self.request.user) # auto-assign owner ``` 면접관이 주목하는 핵심은 다음과 같습니다. `has_permission`은 모든 요청에서 실행됩니다(목록 수준). 반면 `has_object_permission`은 `get_object()`가 호출될 때만 실행됩니다(상세 수준). 목록 뷰에서 `get_queryset`을 오버라이드하지 않으면, 사용자가 소유하지 않은 객체를 볼 수 있는 보안 취약점이 발생합니다. > **DRF에서 흔히 발생하는 보안 실수** > > has_object_permission에만 의존하고 쿼리셋 필터링을 하지 않으면 목록 엔드포인트가 보호되지 않는 상태가 됩니다. 목록 뷰와 상세 뷰 모두에서 접근 제어를 적용하려면 객체 수준 권한과 필터링된 get_queryset을 반드시 함께 사용해야 합니다. ## DRF 스로틀링과 페이지네이션 패턴 스로틀링과 페이지네이션은 표준적인 후속 질문입니다. 면접관은 지원자가 프로덕션 환경에 적합한 API 설정을 이해하고 있는지 확인하고자 합니다. ```python # settings.py — Production DRF configuration REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "anon": "20/minute", # unauthenticated users "user": "200/minute", # authenticated users }, "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.CursorPagination", "PAGE_SIZE": 25, } ``` ```python # pagination.py — Custom cursor pagination for consistent ordering from rest_framework.pagination import CursorPagination class CreatedAtCursorPagination(CursorPagination): page_size = 25 ordering = "-created_at" # must be a unique, sequential field cursor_query_param = "cursor" # ?cursor=abc123 ``` 커서 기반 페이지네이션은 대규모 데이터셋에서 오프셋 페이지네이션보다 우수한 성능을 보입니다. 전체 행 수를 세지 않아도 되기 때문입니다. 트레이드오프로, 클라이언트는 임의의 페이지로 이동할 수 없습니다. "페이지 번호 기반 페이지네이션 대신 커서 페이지네이션을 선택하는 이유는 무엇입니까?"라는 질문에 대한 기대 답변이 바로 이것입니다. ## 결론 Django 면접 준비의 핵심 포인트는 다음과 같습니다. - **ORM 최적화**: 쿼리 메서드를 관계 유형에 맞추는 것이 필수입니다. ForeignKey/OneToOne에는 `select_related`, ManyToMany와 역방향 FK에는 `prefetch_related`를 사용합니다. 커스텀 QuerySet은 체이닝 가능하고 재사용 가능한 비즈니스 로직을 구현합니다. - **미들웨어 아키텍처**: 요청은 위에서 아래로, 응답은 아래에서 위로 흐릅니다. `get_response()` 호출 전에 응답을 반환하는 쇼트서킷은 레이트 리미팅, 인증 검사, 요청 유효성 검증의 기본 패턴입니다. - **DRF 시리얼라이저 성능**: 중첩된 시리얼라이저는 ViewSet에서 명시적인 쿼리셋 최적화가 필요합니다. `select_related`/`prefetch_related` 없이는 직렬화가 대규모 환경에서 N+1 쿼리를 발생시킵니다. - **DRF 권한**: 목록 뷰와 상세 뷰 모두에서 접근 제어를 적용하려면 `has_object_permission`과 필터링된 `get_queryset`을 결합해야 합니다. 객체 수준 권한만으로는 목록 뷰가 보호되지 않습니다. - **스로틀링과 페이지네이션**: 커서 페이지네이션은 대규모 테이블에서 오프셋보다 확장성이 뛰어납니다. 익명 사용자와 인증된 사용자에 대해 별도의 스로틀 레이트를 설정합니다. SharpSkill의 [Django ORM 면접 질문](/technologies/django/interview-questions/django-orm-advanced)과 [Django 미들웨어 질문](/technologies/django/interview-questions/django-middleware)으로 연습하여 실전 면접에 대비하십시오. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/django/django-interview-orm-middleware-drf