N+1 문제란? django에서의 해결 방법은 무엇인가요?
카페 비유로 이해하기
카페에서 웨이터가 손님 10개 테이블의 주문을 주방에 전달해야 하는 상황을 가정해봅시다.
- 비효율적인 방법: 1번 테이블 주문 받기 → 주방에 전달 → 2번 테이블 주문 받기 → 주방에 전달 → … 주방을 10번 왕복
- 효율적인 방법: 10개 테이블 주문을 한 번에 다 받고 → 주방에 한 번만 전달
여기서 "주방 왕복"이 바로 데이터베이스 쿼리입니다. 쉽게 말해 데이터베이스에 요청을 보내는 것인데요, 요청을 보내는 자체가 시간이 걸리는 작업이기 때문에 100번 왔다갔다 하느냐 vs 1번에 끝내느냐는 성능에 큰 차이를 만듭니다.
N+1 문제는 이처럼 목록 조회 1번 + 각 항목마다 추가 조회 N번 = 총 N+1번 쿼리가 발생하는 현상입니다.
코드로 보는 N+1 문제
강의(Course)와 강사(Instructor)가 있는 플랫폼을 예로 들겠습니다.
# models.py
class Instructor(models.Model):
name = models.CharField(max_length=100)
class Course(models.Model):
title = models.CharField(max_length=200)
instructor = models.ForeignKey(Instructor, on_delete=models.CASCADE)
# views.py (문제 있는 코드)
courses = Course.objects.all() # 쿼리 1번: 강의 목록 조회
for course in courses:
print(course.instructor.name) # 쿼리 N번: 강의마다 강사 정보를 따로 조회
강의가 100개라면? 101번의 쿼리가 실행됩니다. 1000개라면 1001번이고요.
DRF에서도 Serializer 안에서 관계 필드를 접근할 때 정확히 같은 문제가 발생합니다.
class CourseSerializer(serializers.ModelSerializer):
instructor_name = serializers.CharField(source='instructor.name') # 여기서 N번 쿼리 발생
class Meta:
model = Course
fields = ['title', 'instructor_name']
해결 방법: select_related와 prefetch_related
Django ORM은 이 문제를 해결하는 두 가지 도구를 제공합니다.
select_related — JOIN으로 한 번에
ForeignKey, OneToOneField 관계에서 사용합니다. SQL의 JOIN을 사용해서 쿼리 1번으로 관련 데이터를 함께 가져옵니다.
# 쿼리 1번으로 강의 + 강사 정보를 함께 조회
courses = Course.objects.select_related('instructor').all()
for course in courses:
print(course.instructor.name) # 추가 쿼리 없음!
생성되는 SQL
SELECT course.*, instructor.*
FROM course
INNER JOIN instructor ON course.instructor_id = instructor.id;
prefetch_related — 별도 쿼리로 미리 가져오기
ManyToManyField, 역방향 ForeignKey에서 사용합니다. 쿼리를 2번 실행하되, Python에서 미리 연결해줍니다.
# 강사별로 담당 강의 목록을 가져올 때
instructors = Instructor.objects.prefetch_related('course_set').all()
for instructor in instructors:
for course in instructor.course_set.all(): # 추가 쿼리 없음!
print(course.title)
생성되는 SQL
-- 쿼리 1: 강사 목록
SELECT * FROM instructor;
-- 쿼리 2: 해당 강사들의 강의 목록
SELECT * FROM course WHERE instructor_id IN (1, 2, 3, ...);
DRF에서 적용하기
ViewSet의 get_queryset에서 처리하는 것이 일반적입니다.
class CourseViewSet(viewsets.ModelViewSet):
serializer_class = CourseSerializer
def get_queryset(self):
return (
Course.objects
.select_related('instructor') # ForeignKey
.prefetch_related('tags', 'students') # ManyToMany
)
언제 뭘 써야 하나요?
| 관계 유형 | 사용할 메서드 | 이유 |
|---|---|---|
ForeignKey, OneToOneField | select_related | JOIN이 효율적 |
ManyToManyField | prefetch_related | JOIN 시 행이 폭발적으로 증가 |
역방향 FK (_set) | prefetch_related | 동일 |
| 중첩 관계 | select_related('a__b') | 연쇄 JOIN |
마치며
N+1 문제는 코드만 보면 절대 눈에 띄지 않습니다. 실제로 발생하는 쿼리를 확인하려면 django-debug-toolbar나 Django의 connection.queries를 활용해보세요. 로컬에서는 데이터가 적어 빠르게 느껴지더라도, 실서비스에서 데이터가 쌓이면 즉각 체감하게 됩니다.
select_related와 prefetch_related를 습관적으로 챙기는 것만으로도 API 응답 속도가 눈에 띄게 달라집니다.

