1. N+1문제와 최적화(1)
N개의 추가 쿼리가 발생하며 데이터 개수에 따라 성능이 급감하는 문제를 말한다. 비효율적 접근을 살펴보자.
transactions = PaymentTransaction.objects.filter(user=request.user)
for txn in transactions:
print(txn.user.profile.name) # N+1 문제 발생
첫번째로 PaymentTransaction 테이블에서 request.user의 모든 거래내역을 가져온다.
그리고 루프를 실행하며 txn.user.profile.name에 접근할 때마다 user와 profile을 개별조회하는 SQL query가 실행되며 transaction에 N개의 결과가 있으면 추가로 N개의 쿼리가 실행된다.
select_related()와 only()로 최적화
transactions = PaymentTransaction.objects.select_related(
'user__profile'
).filter(user=request.user).only(
'amount', 'currency', 'user__profile__name'
)
먼저 select_related()를 통해 JOIN을 사용해 외래키로 연결된 테이블을 한번에 가져온다. 즉 추가적인 쿼리실행없이 user.profile 데이터를 미리 로딩한다. 그리고 only()를 사용해 필요한 특정 필드만 선택적으로 가져온다.
2. N+1문제와 최적화(2)
select_related()는 JOIN을 사용해 외래키 데이터를 한번에 가져오는 최적화를 한다고 했다.
하지만 ManyToManyField, 역참조(related_name)으로 연결된 데이터를 가져올때는 prefetch_related()를 사용해야한다.
transactions = PaymentTransaction.objects.filter(user=request.user)
for txn in transactions:
print(txn.tags.all()) # ManyToMany 관계에서 추가적인 N개의 SQL 실행됨 (N+1 문제)
tags.all()을 실행할때 각 트랜잭션마다 추가적인 SQL quert를 발생시킨다.
prefetch_related()로 최적화
transactions = PaymentTransaction.objects.prefetch_related('tags').filter(user=request.user)
for txn in transactions:
print(txn.tags.all())
prefetch는 두개의 별도쿼리를 실행하고, django 내부에서 데이터를 연결한다. JOIN사용하지 않고도 디비에서 미리 가져와서 문제를 해결한다. 결과적으로는 2개의 쿼리로 최적화된다.
defer()의 경우
only()는 선택한 필드만을 가져올때 사용한다고 했다. defer()는 선택한 필드만 제외하고 가져온다.
3. 캐싱전략 (cache_page())
자주 조회되는 데이터는 디비를 거치지 않고 캐싱을 활용하는게 낫다.
from django.views.decorators.cache import cache_page
@cache_page(60 * 5) # 5분 동안 캐싱
def user_list(request):
users = User.objects.all()
return JsonResponse({"users": list(users.values("id", "email"))})
매번 완벽히 실시간 상황이 반영된 정확한 데이터를 줄 필요 없고, 조회가 잦은 경우 이런 식의 캐싱 전략이 유효하다.
4. 인덱스 전략
class PaymentTransaction(models.Model):
transaction_id = models.UUIDField(primary_key=True, default=uuid.uuid4)
user = models.ForeignKey(User, on_delete=models.PROTECT, db_index=True)
amount = models.DecimalField(max_digits=12, decimal_places=2)
currency = models.CharField(max_length=3)
status = models.CharField(max_length=20, choices=TransactionStatus.choices)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
indexes = [
models.Index(fields=['user', '-created_at'], name='user_transaction_history_idx')
]
- user필드의 db_index=True는 해당 필드에 단일 인덱스를 생성한다. 모델에서 사용자의 결제내역을 조회할 일이 많은 경우라면 적절한 인덱스다. 만약 인덱스가 없다면 user_id를 검색할때 전체 테이블을 스캔해야하지만 인덱스를 통해 빠르게 user_id 기반 검색이 가능하다. (부분 인덱스)
- Meta 부분을 보면 (user, created_at) 복합 인덱스 설정을 확인할 수 있다. 인덱스가 정렬된 상태로 저장되므로 ORDER BY 연산이 빠르게 수행된다. 정렬조회를 위해 최적화된 결정이다.
'웹 프로그래밍 > DRF' 카테고리의 다른 글
효율적인 Django 사용을 위한 몇가지 (분산트랜잭션 처리와 ACID) (0) | 2025.02.26 |
---|---|
효율적인 Django 사용을 위한 몇가지(정규화 설계 적용편) (0) | 2025.02.26 |