Django ORM N+1 문제 해결하기

문제 상황

회사 프로젝트에서 Django REST Framework로 게시글 목록 API를 만들었는데, 데이터가 100건을 넘어가면서 응답 속도가 3초 이상 걸리기 시작했다. Django Debug Toolbar로 확인해보니 단일 요청에 200개 이상의 쿼리가 발생하고 있었다.

# 문제가 있던 코드
class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

class PostSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source='author.username')
    category_name = serializers.CharField(source='category.name')
    
    class Meta:
        model = Post
        fields = ['id', 'title', 'author_name', 'category_name']

게시글 100개를 조회할 때마다 각 게시글의 작성자와 카테고리를 가져오기 위해 추가 쿼리가 발생하는 전형적인 N+1 문제였다.

해결 방법

외래키 관계는 select_related, 역참조나 M2M 관계는 prefetch_related를 사용하면 된다.

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.select_related(
        'author', 'category'
    ).all()
    serializer_class = PostSerializer

이렇게 수정하니 200개의 쿼리가 3개로 줄었고, 응답 속도도 300ms 이하로 개선되었다.

추가 최적화

댓글 개수처럼 집계 데이터가 필요한 경우는 annotate를 사용했다.

from django.db.models import Count

queryset = Post.objects.select_related(
    'author', 'category'
).annotate(
    comment_count=Count('comments')
).all()

ORM은 편리하지만 생성되는 쿼리를 항상 확인하는 습관이 필요하다. Django Debug Toolbar는 개발 환경에서 필수 도구다.