Django 면접 질문: ORM, 미들웨어, DRF 심층 분석
Django 면접에서 자주 출제되는 ORM 최적화(select_related와 prefetch_related), 미들웨어 아키텍처, Django REST Framework 시리얼라이저 성능, 권한 설정, 페이지네이션 패턴을 상세히 다룹니다.

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를 어떻게 구분하여 사용하는지가 핵심입니다.
# 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)두 메서드의 차이점은 탐색하는 관계의 유형에 따라 결정됩니다.
# 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의 매니저 패턴을 이해하고 재사용 가능한 쿼리 인터페이스를 작성할 수 있는지가 평가 포인트입니다.
# 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 메서드는 첫 번째 호출 이후에 체이닝이 불가능합니다.
# 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에서 CompositePrimaryKey가 도입되었습니다. 다중 컬럼 기본 키가 필요한 모델에서 오랫동안 기다려온 기능입니다. 이 기능에 관한 면접 질문이 증가하는 추세이며, 특히 레거시 데이터베이스나 데이터 웨어하우스 스키마를 다루는 지원자에게 자주 출제됩니다.
Django 미들웨어 면접 질문: 요청-응답 파이프라인
미들웨어 관련 질문은 Django의 요청-응답 라이프사이클에 대한 이해도를 평가합니다. 대표적인 질문은 "미들웨어가 요청과 응답을 처리하는 순서를 설명하십시오"입니다.
정답은 명확한 패턴을 따릅니다. 요청 시에는 MIDDLEWARE에 정의된 순서대로 위에서 아래로 미들웨어 클래스가 실행됩니다. 응답 시에는 아래에서 위로 실행됩니다. 이 양파 레이어 아키텍처 덕분에 목록의 첫 번째 미들웨어가 모든 것을 감싸는 형태가 됩니다.
# 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를 반환하면 체인 전체가 중단됩니다. 나머지 미들웨어와 뷰는 실행되지 않습니다.
# 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 미들웨어는 세 가지 선택적 훅 메서드를 지원합니다. 면접관은 이 훅들을 통해 지원자의 지식 깊이를 측정합니다.
# 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에서는 호출되지 않습니다.
Django 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
DRF 면접 질문: 시리얼라이저 성능
Django REST Framework 시리얼라이저 관련 질문은 중첩된 직렬화와 그에 따른 성능 영향에 초점을 맞추고 있습니다. 가장 자주 출제되는 질문은 "N+1 쿼리를 발생시키지 않으면서 중첩된 관계를 어떻게 처리합니까?"입니다.
# 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에서 쿼리셋을 최적화해야 합니다.
# 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 면접에서 자주 출제되는 질문으로 "사용자의 역할과 객체의 소유자를 기반으로 접근을 제한하는 커스텀 권한을 작성하십시오"가 있습니다.
# 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# 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을 오버라이드하지 않으면, 사용자가 소유하지 않은 객체를 볼 수 있는 보안 취약점이 발생합니다.
has_object_permission에만 의존하고 쿼리셋 필터링을 하지 않으면 목록 엔드포인트가 보호되지 않는 상태가 됩니다. 목록 뷰와 상세 뷰 모두에서 접근 제어를 적용하려면 객체 수준 권한과 필터링된 get_queryset을 반드시 함께 사용해야 합니다.
DRF 스로틀링과 페이지네이션 패턴
스로틀링과 페이지네이션은 표준적인 후속 질문입니다. 면접관은 지원자가 프로덕션 환경에 적합한 API 설정을 이해하고 있는지 확인하고자 합니다.
# 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,
}# 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 면접 질문과 Django 미들웨어 질문으로 연습하여 실전 면접에 대비하십시오.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

Django 5.2 커스텀 미들웨어와 시그널 핸들링: 기술 면접 완벽 가이드
Django 5.2 커스텀 미들웨어 작성법과 시그널 핸들링 패턴을 기술 면접 관점에서 심층 분석합니다. 비동기 미들웨어, 커스텀 시그널 등 실무 예제를 포함합니다.

Django와 Celery: 비동기 태스크 처리 및 면접 질문 가이드 2026
Django와 Celery를 활용한 비동기 태스크 처리 구현 방법을 설명합니다. 태스크 라우팅, Celery Beat 스케줄링, 프로덕션 설정, 2026년 최신 기술 면접 질문을 다룹니다.

Django와 Python 면접 질문: 2026년 핵심 25선
Django와 Python 면접에서 가장 자주 출제되는 25가지 질문을 상세히 다룹니다. ORM, 뷰, 미들웨어, Django REST Framework, 시그널, 성능 최적화에 대한 상세한 답변과 코드 예제를 제공합니다.