∟ Framework/∟ DRF

ORM 최적화

최 수빈 2025. 3. 31. 03:29

 

기본 ORM 동작 이해

 

Article Comment 간의 관계가 설정된 모델

class Article(models.Model):
    title = models.CharField(max_length=120)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

class Comment(models.Model):
    article = models.ForeignKey(
        Article, on_delete=models.CASCADE, related_name="comments"
    )
    content = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

 

모든 댓글(Comment) 조회

Comment.objects.all()

 

각 댓글이 달린 글의 제목 출력

comments = Comment.objects.all()
for comment in comments:
    print(comment.article.title)

 

 

N+1 문제

 

 Comment article.title을 출력했을 때, Django의 동작

  1. 첫 번째 쿼리: Comment.objects.all() → 전체 댓글 조회
  2. 이후 comment.article.title 접근할 때마다 → 해당 article을 위한 추가 쿼리(N번) 발생

총 N+1개의 SQL 쿼리가 실행됨

 

connection.queries

# articles/urls.py
from django.urls import path
from . import views

app_name = "articles"
urlpatterns = [
    path("check_sql/", views.check_sql, name="check_sql"),
]
@api_view(["GET"])
def check_sql(request):
    from django.db import connection

    comments = Comment.objects.all()
    for comment in comments:
        print(comment.article.title)

    print("-" * 30)
    for query in connection.queries:
        print(query)

    return Response()

수많은 쿼리들

 

 

 

 

ORM의 지연로딩(Lazy Loading)

 

Django ORM은 기본적으로 지연 로딩을 사용함

  • queryset = Model.objects.all() → SQL 실행 안 함
  • 반복문을 돌거나 실제 속성에 접근할 때 → SQL 실행됨

→ 불필요한 데이터 접근을 줄이는 장점이 있지만, 관계 필드 접근 시 N+1 문제가 발생할 수 있음

comments = Comment.objects.all()         # 아직 쿼리 안 나감 → 쿼리 계획만 있음
for comment in comments:                 # 이 시점에 1번 쿼리 (전체 Comment) → 메모리에 데이터 채움
    print(comment.id)                    # 메모리에 있으니 OK → 쿼리 X
    print(comment.article.title)         # comment 개수만큼 추가 쿼리 발생 (N번)

 

 

 

즉시로딩(Eager Loading)

데이터를 로드할 때 필요하다고 판단되는 연관된 데이터 객체들을 한번에 가져옴

지연로딩에서 발생하는 N+1 문제를 해결할 수 있으나, 너무 많은 데이터를 가져오면 성능 문제를 야기할 수 있음

 

select_related (정참조 - ForeignKey, OneToOne 또는 OneToMany)

comments = Comment.objects.all().select_related("article")

→ JOIN 쿼리를 통해 article까지 한 번에 가져옴

@api_view(["GET"])
def check_sql(request):
    from django.db import connection

    comments = Comment.objects.all().select_related("article")
    for comment in comments:
        print(comment.article.title)

    print("-" * 30)
    for query in connection.queries:
        print(query)

    return Response()

SELECT FROM INNER JOIN ON

prefetch_related (역참조 - related_name/select_related)

articles = Article.objects.all().prefetch_related("comments")

→ 두 개의 쿼리로 분리되지만, Python이 내부에서 관계를 연결해서 N+1 문제를 방지

→ 첫번째 쿼리는 원래 객체를 조회, 두번째 쿼리는 연관된 객체를 가져옴

@api_view(["GET"])
def check_sql(request):
    from django.db import connection

    articles = Article.objects.all().prefetch_related("comments")
    for article in articles:
        for comment in article.comments.all():
            print(comment.id)

    for query in connection.queries:
        print(query)

    return Response()

articles_comment, articles_article

 

 

성능 확인 도구: Silk

요청별 실행된 SQL 쿼리 수, 속도 등 확인 가능

N+1 문제, 비효율적 join 등을 확인 가능

 

 

설치

pip install django-silk

 

settings.py 설정

MIDDLEWARE = [
    ...
    "silk.middleware.SilkyMiddleware", # 최대한 아래에 두어야 요청 + 응답을 다 받을 수 있는 확률이 높음
]

INSTALLED_APPS = [
    ...
    "silk",
]

 

urls.py 설정

from django.urls import path, include

urlpatterns += [path("silk/", include("silk.urls", namespace="silk"))]

 

 

 

서버 실행 후 /silk/ 페이지에 접속

silk

 

https://github.com/jazzband/django-silk

 

GitHub - jazzband/django-silk: Silky smooth profiling for Django

Silky smooth profiling for Django. Contribute to jazzband/django-silk development by creating an account on GitHub.

github.com

 

 

 

 

'∟ Framework > ∟ DRF' 카테고리의 다른 글

API 문서화(Documentation)  (0) 2025.03.31
Redis를 Django 캐시 백엔드로 설정하기  (1) 2025.03.31
Django ORM(Object Relational Mapping) 활용  (0) 2025.03.31
Token Auth with JWT  (0) 2025.03.30
DRF Serializer 활용  (1) 2025.03.30