기본 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의 동작
- 첫 번째 쿼리: Comment.objects.all() → 전체 댓글 조회
- 이후 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()
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()
성능 확인 도구: 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/ 페이지에 접속
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 |