Django 로 백엔드를 개발하던 중, 연관관계가 복잡한 엔티티들의 조회를 구현해야하는 경우가 발생했다. 성능을 생각해서 raw query 로 작성을 하려다가 이번기회에 Django ORM 에서 안 써본 기능 들을 쓰고싶어서 ORM 으로 개발하기로 했다. 연관관계를 가지는 엔티티들을 조회할 때, 일반 방식으로 접근하면 lazy loading 으로 N+1 문제가 발생하게 된다. 이때문에 한번에 객체들을 조회할 수 있도록 eager loading 방식으로 구현해야 한다. Djnago ORM 에서는 eager loading 방식을 select_related() 와 prefetch_related() 두가지 기능으로 제공해주어 이에 대한 정리를 해보려한다.
1. eager loading (즉시 로딩)
ORM 에서 외래키 관계에 따라 데이터를 조회할 때 조회하는 방식에 따라서 lazy loading 과 eager loading 으로 나뉘어진다. lazy loading 은 연관관계에 있는 객체의 필드에 접근할 때 해당 모델에 대한 쿼리가 발생하면서 객체를 조회하는 방식이다. 반대로 eager loading 의 경우 특정 객체를 조회할 때 연관관계에 있는 객체를 한꺼번에 같이 조회해오는 방식이다. ORM 에서는 기본적으로 lazy loading 방식으로 객체를 조회한다. 연관관계에 있는 객체에 접근하지도 않는데 미리 조회해 올 필요가 없기 때문이다.
하지만 lazy loading 방식은 N+1 문제를 유발한다. one to many 관계를 가지고 있는 A 와 B 모델이 있다. lazy loading 으로 A 객체를 조회하면 A 객체를 조회하는 하나의 쿼리가 발생한다. 이후에 A 객체에서 참조하고 있는 N개의 B 객체에 접근하게 되면 B 객체 하나당 하나씩, 총 N 개의 쿼리가 발생하게 된다. 이때문에 연관관계를 가지는 1개의 객체 조회에 N+1개의 쿼리가 발생하는 상황이 된다.
이러한 N+1 문제는 성능에 심각한 영향을 주는데, 이 문제를 해결하기 위해 eager loading 으로 참조하고 있는 객체를 한번에 조회하여 추가 쿼리가 발생하지 않도록 한다. Django ORM 에서는 select_related() 와 prefetch_related() 로 이러한 문제를 해결하고자 한다.
2. select_related()
select_related() 는 정방향 (FK, 1:1) 관계에 최적화된 즉시 로딩 기능이다. 여기서 정방향이란 외래키를 가지고 모델에서 상대 모델을 참조하는 방향, 1:1 또는 N:1 관계를 말한다. 반대로 역방향 (1:N, N:N) 관계에서는 사용할 수 없다.
select_related() 는 연관관계에 있는 객체를 join 을 사용하여 하나의 쿼리로 조회한다.
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
위와 같은 참조 관계에서 Book 객체와 연관된 Author 객체를 모두 포함하여 조회할 때 select_related() 를 사용한다.
books = Book.objects.select_related("author").all()
for book in books:
print(book.title, book.author.name)
위 사용예제와 같이 select_related() 함수 안에 join 으로 조회할 필드의 이름, author 를 입력해주어 사용한다. 이렇게 되면 이미 하나의 쿼리를 통해서 author 가 조회됐기 때문에 반복문을 통해서 book.author 에 접근할때 추가 쿼리가 발생하지 않는다.
실제로 발생하는 쿼리는 아래와 같다.
SELECT "book"."id", "book"."title", "book"."author_id",
"author"."id", "author"."name"
FROM "book"
INNER JOIN "author" ON "book"."author_id" = "author"."id";
3. prefetch_related()
prefetch_related() 는 역방향 (1:N, N:N) 관계에 적합한 즉시 로딩 기능이다. 정방향도 가능하긴 하지만 추천하지 않으며, 역방향 참조, N:N, 또는 QuerySet 필터링이 필요한 경우에 사용한다.
prefetch_related() 는 조회시에 조건에 따라 여러개의 쿼리를 수행하고 그 결과 데이터를 합치는 식으로 동작한다.
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
select_related() 때와 동일한 상황에서 Author 에서 연관된 Book 을 한번에 조회하려고 할 때 prefetch_related() 를 사용한다.
authors = Author.objects.prefetch_related("book_set").all()
for author in authors:
for book in author.book_set.all():
print(author.name, book.title)
prefetch_related() 에 함께 조회하려는 역참조 관계 필드의 이름을 넣어주는데, 이때 Django 의 기본 설정은 '{모델 소문자}_set' 이다. 이때문에 book_set 이라는 이름으로 역참조 조회를 수행한다. 역참조 이름은 외래키 설정시에 related_name 값으로 다른 이름으로 지정이 가능한다.
-- 첫 번째 쿼리: Author
SELECT "author"."id", "author"."name"
FROM "author";
-- 두 번째 쿼리: Book
SELECT "book"."id", "book"."title", "book"."author_id"
FROM "book"
WHERE "book"."author_id" IN (1, 2, 3, ...);
실제 쿼리는 위와 같이 Author 에 대한 쿼리 하나, Book 에 대한 쿼리가 하나, 총 두개가 나가게 된다. 이렇게 조회한 결과를 합쳐서 사용하게 되고 따로 author 의 book 에 접근했을 때 쿼리가 발생하지는 않는다.
4. Prefetch
prefetch_related() 안에서 추가적인 필터 조건이 필요한 경우 Prefetch 를 사용하여 조회할 수 있다.
from django.db.models import Prefetch
authors = Author.objects.prefetch_related(
Prefetch(
"book_set",
queryset=recent_books,
to_attr="books"
)
)
for author in authors:
for book in author.books:
print(author.name, book.title)
이 방법을 사용하면 author 에 연관된 book 중에서도 Prefetch 의 queryset 을 통해 필터링 한 결과만 조회되게 된다. 그리고 조회된 book 의 리스트는 to_attr 에 지정된 book 라는 이름의 필드로 author 객체에 포함되어 author.books 를 통해 접근할 수 있다.
'프로그래밍언어 > Python' 카테고리의 다른 글
[Django] Django 배포 - wsgi vs asgi (1) | 2024.11.16 |
---|---|
[Python] Default Argument Value - mutable object (2) | 2023.12.02 |
[Python] GIL (Global Interpreter Lock) (0) | 2022.07.24 |
[Python] Awaitable (0) | 2022.04.19 |
[Python] Decorator (0) | 2022.04.01 |