[Django] ORM 구조와 원리 (2)

WooJin Chung·2022년 7월 2일
0

Django

목록 보기
2/2

ORM의 무서운 점은 실제로 어떤 쿼리문이 발생되고 어떻게 동작하는지 알지 못하고 다룰 수 있는 점이라고 생각한다.
이번 포스팅에서는 이러한 ORM에 대한 의존 때문에 놓치기 쉬운 N+1 문제와 이를 해결하기 위해 Django에서 지원하는 메소드에 대해 다뤄보려고 한다.

N+1 Problem


N+1 문제란, 쿼리 1번으로 데이터를 가져왔음에도 불구하고 의도치 않게 쿼리를 데이터의 개수(N)만큼 추가로 발생시켜 데이터를 읽어오는 상황을 말한다.

Django DB를 다음과 같이 정의하고 간단한 예시를 통해 N+1 문제를 확인해 보겠다.

class User(models.Model):
	name = models.CharField(max_length=16)
    age = models.SmallIntegerField()
    detail = models.ForeignKey('UserDetail', on_delete=models.SET_NULL, null=True)
    
	class Meta:
    	db_table = "user"
        
class UserDetail(models.Model):
	email = models.CharField(max_length=32)
    
    class Meta:
    	db_table = "user_detail"

나이가 26 이상인 User들의 email을 가지고 오고 싶어서 함수를 다음과 같이 작성하였다.

def get_user_email():
	result = []

    user_list = User.objects.filter(age__gte=26)

    for user in user_list:
    	user_email = user.detail.email
        result.append(user_email)

    return result

ORM의 구조를 잘 알지 못한다면 위의 함수가 어떤 문제를 가지는지 파악하기 힘들 것이다.
하지만 함수의 로그를 확인한다면 처음 호출에서 N개의 모델을 가져오고 이후에 실제 데이터를 불러오는 과정에서 또 하나의 Row를 가져오는 쿼리가 실행되는 것을 확인할 수 있다.
ORM의 Lazy loading 특징 때문에 for문이 한 번 돌 때마다 1번씩 쿼리가 추가로 발생하는 N+1 문제가 생기는 것이다.


출처: Django QuerySet API reference

Select_related는 이전 포스팅에서 언급된 Django에서 지원하는 Eager loading의 메소드 중 하나이다.
Selected_related는 SQL의 JOIN을 사용하여 쿼리셋을 불러올 때 related objects까지 불러와 Cache에 저장하는 방식을 통해 N+1 문제를 해결한다.

Select_related의 사용법은 다음과 같다.

def get_user_email():
	result = []

    user_list = User.objects.select_related(age__gte=26)

    for user in user_list:
    	user_email = user.detail.email
        result.append(user_email)

    return result

언뜻 보면 위의 함수와 별 차이 없어 보이지만, select_related를 통해 처음 user_list를 쿼리할 때 SQL 상에서 테이블 JOIN이 일어나 외래키(Foreign Key)로 묶인 UserDetail 정보도 함께 쿼리된다.
이는 객체가 역참조하는 단일 객체(one-to-one or many-to-one)이거나, 또는 정참조하는 관계일 때 사용 가능하다.

데이터베이스에서 외래키(Foreign Key)가 있다면 양방향으로 JOIN이 가능하지만 select_related 이를 지원하지 않는다.
따라서 one-to-many, many-to-many 모델의 경우는 또 다른 메소드인 prefetch_related를 사용한다.


출처: Django QuerySet API reference

마찬가지로 Django에서 Eager loading을 위해 지원하는 메소드이다.
Django Document에서는 prefetch_related를 python 내에서 "JOINING"한다고 설명한다.
Select_related와 달리 데이터베이스 내에서 JOIN이 발생하지 않고, 2개의 테이블을 각각 불러들여 ORM에서 결합하는 방식을 사용한다는 뜻이다.

따라서 위에서 정의된 N+1 문제도 prefect_related를 사용하여 해결 가능하다.

def get_user_email():
	result = []

    user_list = User.objects.prefetch_related(age__gte=26)

    for user in user_list:
    	user_email = user.detail.email
        result.append(user_email)

    return result

하지만 이 경우에는 메인 쿼리가 실행된 후 모델에 대해 별도의 쿼리가 한번 더 실행되므로, 단일 쿼리로 실행되는 select_related보다 리소스를 더 많이 소모한다.
따라서 SQL 로그를 확인하면서 상황에 맞는 DB 접근을 설계하는 것이 중요할 것이다.



최대한 내용을 검토하면서 작성하지만 틀린 내용이 있을 수 있습니다. 댓글로 남겨주시면 감사하겠습니다.

profile
Student Studying medical A.I

0개의 댓글