Query 개선의 목적
백엔드 개발자의 채용 공고에 종종 쿼리 튜닝이 들어가 있다. 결국 한 API가 실행될 때 몇 개의 Query를 날리느냐 ( = DB hit를 몇 번 치느냐)는 성능 향상으로 직결되는 매우 중요한 문제인 것이다.
N+1 문제란?
오늘 살펴볼 N+1 문제는 Djnago의 ORM과 같은 ORM(Object-Relational Mapping)으로 작업할 때 발생할 수 있는 성능 문제다. 각 객체에 대해 검색하려고 할 때 관련된 다른 Table(=collection)까지 검색이 필요한 경우에 발생한다. 이 작업을 비효율적으로 수행하면 데이터베이스에 대해 N+1 Query를 실행하게 된다. 여기서 N은 해당 객체의 수이다. 이로 인한 성능 저하는 대규모 데이터를 세트로 작업하는 경우 애플리케이션의 출동까지도 야기할 수 있다.
Django ORM의 N+1 문제
Django의 ORM은 Lazy-Loading을 기본값으로 사용하고 있어 자체적으로 이를 최적화하여 최소한의 쿼리만 날린다. 하지만 DB 설계에따라 N번 쿼리를 더 날리는 N+1 문제가 종종 발생한다. 아래 예시로 확인해 보자. 아래 작가와 책이라는 두 테이블 있다.
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
아래 코드를 통해 실제 Query를 실행하는 시점을 알아보자. Lazy Loading 방법을 사용하기 때문에 실제 books를 사용하는 시점에 실행된다는 것을 염두에 두고 보면 좋다.
# Query 실행 전
books = Book.objects.all()
for book in books:
# books 호출 시점에서 쿼리를 날린다.
# 하지만 위에 작성된 쿼리로는 아래 작가의 정보까지 불러올 수 없어 book 개수만큼
# author 정보를 가져오기 위해 쿼리를 더 보낸다.
print(book.author)
DRF를 사용하여 N+1 문제가 생기는 경우는 아래와 같다. 책 정보 반환 시 작가 관련 정보도 같이 조회되게끔 serializer을 작성했다.
from rest_framework import serializers
from .models import Author, Book
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = ('id', 'name')
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer()
class Meta:
model = Book
fields = ('id', 'title', 'author')
책의 목록을 가져오는 API를 만들며 queryset을 아래와 같이 작성했다면 N+1 문제가 발생한다.
from rest_framework import generics
from .models import Book
from .serializers import BookSerializer
class BookList(generics.ListAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
이 코드의 문제점은 각 책의 저자를 검색하기 위해 별도의 쿼리가 또 실행된다는 것이다. 만약 100권의 책이 있는 경우 101개의 쿼리를 실행한다. 1번은 책 목록을 검색하고 나머지 100번은 연결된 저자를 검색한다.
N+1 문제 해결 방법
n+1 문제를 해결하기 위한 방법 중 하나는 Eager Loading을 사용하는 것이다. Eager Loading은 관련된 데이터를 모두 한 번에 가져오는 것을 말한다. 이 방법은 쿼리를 실행할 때 필요한 모든 데이터를 한 번에 가져옴으로써, 중복된 데이터를 제거하는 데 도움을 준다. 예를 들어, 위에서 언급한 예시에서는 책 정보와 함께 그 책을 작성한 작가를 한 번에 가져와서 중복된 책 정보의 요청을 제거할 수 있다.
Django ORM을 이용한 N+1 문제 해결 방법
Django REST framework (DRF)에서는 Eager Loading을 위해 select_related()와 prefetch_related() 함수를 제공한다. select_related() 함수는 ForeignKey와 OneToOneField를 기반으로 한 필드들을 미리 가져온다. prefetch_related() 함수는 ManyToManyField와 GenericRelation 필드를 기반으로 한 필드들을 미리 가져온다. 이를 통해 데이터베이스에서 중복된 데이터를 제거하고 성능을 향상할 수 있다.
사용방법은 다음과 같다. 먼저 Join 하여 가져올 저자를 select_releated 필드로 먼저 설정해 준다.
from rest_framework import generics
from .models import Book
from .serializers import BookSerializer
class BookList(generics.ListAPIView):
queryset = Book.objects.select_related('author').all()
serializer_class = BookSerializer
다음은 조회 시 가져오는 저자의 정보에 대해 read_only 처리를 해준다.
from rest_framework import serializers
from .models import Author, Book
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = ('id', 'name')
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer()
class Meta:
model = Book
fields = ('id', 'title', 'author')
def to_representation(self, instance):
self.fields['author'] = AuthorSerializer(read_only=True)
return super().to_representation(instance)
위의 코드처럼 select_related()를 사용하여 모든 책과 책의 저자 정보를 한 번에 가져옴으로써 N+1문제를 해결할 수 있다.