N + 1 문제
언제 ??
주로 외래키(혹은 다대다)와 같은 관계를 갖는 데이터베이스 모델에서 발생한다고 합니다.
"N+1 문제"란?
연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우에
조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 된다. 이를 N+1 문제라고 한다.
예를 들어, Product 모델에서 Category 모델을 ForeignKey로 가지고 왔을 때
class Category(models.Model):
class Meta:
verbose_name = verbose_name_plural = "상품 분류"
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return f"{self.name}"
class Product(models.Model):
class Status(models.TextChoices):
ACTIVE = "active", "활성화"
SOLDOUT = "soldout", "품절"
OBSOLETE = "obsolete", "단종"
INACTIVE = "inactive", "비활성화"
category = models.ForeignKey(Category, on_delete=models.CASCADE, db_constraint=False) # db_constraint=False 는 외래키 제약조건을 걸지 않겠다는 의미
name = models.CharField(max_length=100, db_index=True)
description = models.TextField(blank=True)
price = models.PositiveIntegerField() # 0 이상의 정수
status = models.CharField(max_length=10, choices=Status.choices, default=Status.INACTIVE)
photo = models.ImageField(blank=True, upload_to="mall/product/photo/%Y/%m/%d")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
프론트에서 product.category.name 로 카테고리 이름을 가지고 오려면
Product 갯수만큼 Category를 참조해서 가져와야 합니다.
< 상품 갯수가 369개면, 카테고리 이름을 요청하는 것도 369개 >
실제로 백엔드 Views.py 에서 작성한 Product.objects.all() 를 사용한
def product_list(request): 함수를
프론트에서 urls와 함께 데이터를 가져오게 된다면
다음과 같이, SQL - 369개의 상품에 대해서 쿼리들이 발생했고, 시간은 7.34ms임음을 알 수 있습니다.
{% for product in product_list %}
<div class="col-sm-6 col-lg-4">
<div class="card">
<img src="{{ product.photo.url }}"
alt="상품 사진"
class="card-img-top object-fit-cover">
<div class="card-body">
<p>번호 : {{ product.id }}</p>
<p>상품 종류 : {{ product.category.name }}</p>
<div class="text-truncate">
<h5>{{ product.name }}</h5>
</div>
<div class="d-flex justify-content-between">
<div>{{ product.price|intcomma }}원</div>
<div>
<a href="#" class="btn btn-primary">장바구니</a>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
프론트는
장고 템플릿 문법에서 for in 문법을 통해
상품 모델 정보를 불러오고 있는데요
여기서 반복문 for .. in .. :을 통해
<p>상품 종류 : {{ product.category.name }}</p>
상품의 카테고리 이름을 불러옵니다
여기서 Product 를 주 엔터티(Dominant Entity, Independent Entity)
Category 를 종속 엔터티(Dependent Entity, Subordinate Entity)
라고 합니다
(https://dataprofessional.tistory.com/17)
장고에서는
select_related 혹은 prefetch_related
라는 장고의 기능을 사용해서,
추가적인 쿼리를 최소화하고 성능을 향상시킬 수 있습니다.
def product_list(request):
# products_qs = Product.objects.all()
products_qs = Product.objects.all().select_related("category")
# Product 에서 Category를 외래키로 참조해서 가져오는 것은, 데이터베이스에 쿼리를 두번 요청하는 것이므로 비효율적입니다. 이를 해결하기 위해 select_related() 메서드를 사용합니다.
return render(
request,
'mall/product_list.html',
{
"product_list": products_qs,
},
)
.select_related("category") 를 통해서 category 필드에 대해서
Join 하게 되었고
def select_related(self, *fields: Any) -> Self: ...
인자로 fields 필드들을 받는 것을 확인할 수 있습니다.
이는 불필요한 반복된 쿼리를 피하고 데이터를 효율적으로 로드하여
N+1 문제를 해결하는 데 도움이 됩니다.
select_related 은 ForeignKey와 OneToOneField와 같은 관계 필드에서 사용되고
prefetch_related은 ManyToManyField 및 GenericRelation과 같은 "다대다 및 역참조 "관계에서 사용됩니다.
prefetch_related의 사용도 다음과 같습니다.
# 예시
products = Product.objects.all().prefetch_related('categories')
사실 어떤 관계의 필드인지는 데이터 모델링 마다 다를 것이고,
프론트에서 어떤 필드를 가져올 것인지가 , 기획하는 것마다 다르기 때문에
두 기법을 함께 이용할 수도 있고,
따로 사용할 수도 있다고 생각합니다.
백엔드에서 , 상품 리스트를 들고오며, 카테고리 name 뿐만아니라
다양한 데이터를 들고와야 한다면
상품 id 에 대한 카테고리 모델에 대한 정보를
따로 요청하고, 가지고 오는게 데이터 효율적으론
괜찮을 수도 있을 것 같다는 생각이 듭니다.
def select_related(self, *fields: Any) -> Self: ...https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1
현재 자바 , 스프링을 공부하는 중에 있는데
공부하면서 JPA 에 대해서도 더 알아가보도록 하겠습니다.
'Coding > Python' 카테고리의 다른 글
[pyhton 기초] 동작원리? 동작 방식? (0) | 2023.04.11 |
---|