Django ORM

개발 끄적끄적 .. ✍️·2022년 7월 24일
0

Django

Django는 파이썬 기반 웹프레임워크이며, 파이썬 백엔드 웹 프레임워크 생태계 중에서 중 가장 높은 점유율을 차지하고 있습니다. 기본적으로 admin 패널, auth, django template 등을 제공하며 Django와 연계된 다양한 서드파티 패키지들이 매우 많아 높은 확장성으로 빠르고 안정적으로 추가 기능을 개발할 수 있다는 장점이 있습니다. 또한 손에 꼽을 정도로 큰 큐모의 커뮤니티가 존재하며 상세한 공식 문서를 통해 필요한 정보를 빠르게 취득할 수 있습니다. 이외에도 여러가지 장점이 있지만 저는 Django의 가장 큰 장점은 ORM이라고 생각합니다.

ORM

ORM이란 Object Relational Mapping의 약자로 객체와 관계형 데이터 베이스의 데이터를 자동으로 연결해주는 것을 말합니다. ORM을 통해 개발자는 코드 상에서 객체를 통해 실제 데이터 베이스를 조작할 수 있습니다. 대부분의 프로그래밍 아케텍쳐는 낮은 결합도와 높은 응집도를 지향합니다. 모듈과 모듈간의 의존정도에 따라 유지보수와 운영의 관점에서 투입해야하는 리소스가 다르기 때문에 느슨하게 연결되어야 효율적이라고 볼 수 있습니다.

ORM을 통해 개발자는 서버 애플리케이션과 데이터베이스 사이의 낮은 결합도(느슨한 연결)을 지향할 수 있습니다. ORM은 서버 애플리케이션과 데이터베이스 사이에 들어간 추상화 계층이며 결합도를 낮추는 가장 직접적인 방법이며 입니다. 만약 ORM 없이 데이터베이스와 강하게 결합된 상황이라면 간단한 스키마 수정에도 광역적인 변경이 발생할 수 있습니다. 꼭 ORM이 아니더라도 추상화를 구현할 수 있지만 이미 많은 개발자들이 사용하며 검증된 ORM을 사용하지 않을 이유는 없습니다.

QuerySet

QuerySet 이란 한마디로 전달받은 객체의 목록이며, ORM을 통해 가져온 데이터베이스의 row를 말합니다. 이반적으로 리스트와 구조는 같지만 파이썬의 기본 자료구조가 아니기 때문에 파이썬에서 이를 이용하기 위해서는 자료형 변환을 해주어야 합니다.

Lazy Loading(지연 로딩)

지연 로딩이란 한 마디로 필요한 시점에만 쿼리를 날리는 개념입니다. 데이터가 필요하지 않다면 가져오지 않음을 말하며 이를 Django QuerySet의 개념을 통해 설명하면, QuerySet이 평가될 때 실제 SQL은 동작합니다. 예를 들어 아래와 같은 상황에서도 실질적인 SQL은 1번 밖에 발생하지 않습니다.

users = User.objects.all()
orders = Order.objects.all()
companies = Company.objects.all()
list(users)

Django 공식문서에도 QuerySet 평가와 관련된 내용이 설명되어 있는데, QuerySet 평가는 아래 이미지와 같이 7개의 상황에서만 평가가 진행됩니다. (반복 (Iteration), 슬라이싱 (Slicing), 피클링 / 캐싱 (Pickling/Caching), repr(), len(), list(), bool())

Lazy Loading은 크게 두 가지의 문제를 가지고 있는데 중복쿼리 가능성과 N+1 문제입니다.

Lazy Loading 단점 1. 비효율성

예를 들어 아래와 같은 Django ORM 코드가 있다면, 위에서 말한 두 가지 QuerySet 평가가 동작하도 2개의 SQL이 동작하게 됩니다.

users = User.objects.all()

first_user = users[0] # <- LIMIT 1 인 SQL 동작 (slicing)
user_list = list(users) # <- 전체 User 데이터를 가져오는 SQL 동작 (list)

위처럼 1번 째 유저 목록을 가져오기 위해 슬라이싱을 통한 쿼리셋 평가가 진행되면 LIMIT 1이 포함된 SQL이 호출되고, 전체 유저의 목록을 가져오기 위해 리스트를 통한 쿼리셋 평가가 진행되면 앞의 쿼리를 재사용하지 않고 불필요한 SQL이 호출되게 됩니다.

이러한 문제는 QuerySet 캐싱을 사용하며 보완할 수 있습니다. QuerySet을 캐싱해두고 필요한 정보를 가져올 때 앞에 선언한 변수를 확용하면 추가 쿼리를 호출하지 않습니다. 간단하게 순서만 변경하는 것으로도 Lazy Loading의 단점을 보완할 수 있습니다.

users = User.objects.all()

user_list = list(users) # <- user_list 변수에 유절 목록 캐싱
first_user = user_list[0] # <- 추가 쿼리 발생하지 않음

Lazy Loading 단점 2. N+1 문제

Lazy Loading은 쿼리를 날릴 때, 참조모델(fk, many-to-many)의 데이터는 가지고 오지 않습니다. 단순히 해당 모델이 가지고 있는 필드만을 가지고옵니다. 그렇기 때문에 현재의 모델에서 외래키 관계의 모델을 호출할 때마다 추가 쿼리가 발생하게 됩니다.

posts = Post.objects.all()

for post in posts:
    post.user.name # <-  추가 쿼리 발생 지점

따라서 이러한 문제를 개선하기위해 select_related, prefetch_related를 통한 Eager Loading 전략이 있습니다.

ORM Eager Loading(즉시로딩)

queryset은 기본 Lazy Loading을 하지만 한 번에 여러 개의 데이터를 가져오고 싶을 때는 select_related, prefetch_related를 통한 Eager Loading 전략을 통해 가져옵니다. Eager Loading를 통해 성능향상을 기대할 수 있습니다.

select_related란 원래 쿼리에 join을 통해 데이터를 즉시 로딩하는 방식입니다. one-to-one, foreign key인 경우에만 select_related 옵션을 줄 수 있습니다. 이는 Django의 제약사항입니다. SQL 단계에서 join이 발생합니다.

prefetch_related는 추가 쿼리를 수행하며 데이터를 즉시 로딩하는 방식입니다. many-to-many, one-to-many의 정참조이거나 역참조 Foreign Key일 때 사용합니다. 각 관계별로 DB 쿼리를 수행하고 파이썬 레벨에서 join이 발생합니다

정리

django를 사용한지 3개월 정도 사용하며 느낀점은 django orm을 통해 개발자는 쿼리에 대한 고민을 줄이고 비즈니스 로직에만 집중 할 수 있다는 점이 매우 크게 다가왔습니다. django orm은 매우 수준높게 구현되어 있으며 특히 다양한 모델별 orm이 존재하여 그 위상에 더 큰 감탄을 했습니다.(ex 공간 관련 모델의 orm 등등). 물론 위와 같이 가장 기본적인 내용 없이는 orm을 사용하는 이유를 찾기 힘들며, 실제 사용하더라도 더 큰 비효율을 발생시킬 수 있으니 사용할 때는 “왜 사용하며”, “어떻게 사용해야하는지” 한 번더 배우는 기회였습니다.

0개의 댓글