본문 바로가기
TIL/Python | Django

2021.11.26 TIL : [Django] ORM lazy loading

by yeon_zoo 2021. 11. 27.

DRF를 이용한 페이지네이션을 공부하면서 갑자기 혼자 고민하기 시작했다. 다음은 페이지네이션 블로그 코드 중 일부를 중략한 코드인데, objects.all()을 이용해서 전체 데이터를 쿼리셋으로 불러오고 그 쿼리셋을 page size에 맞게 자르는 듯한 느낌을 받았기 때문이다.

def get(self, request):
        instance = Dataset.objects.all()
        page = self.paginate_queryset(instance)
        if page is not None:
            serializer = self.get_paginated_response(self.serializer_class(page,
 many=True).data)
        else:
            serializer = self.serializer_class(instance, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

특히 데이터의 양이 많을 때, 이렇게 전체 데이터를 한번에 불러오면 그 속도가 너무 오래걸리지 않나? 싶으면서 전체 데이터를 한번에 불러올 거면 뭐하러 페이지네이션을 구현하지? 싶은 마음이었다.

 

 

알고 보니 여기에 적용되는 개념은 lazy loading 이라는 기술로, Dataset.objects.all()이 실행되는 시점에 DB를 방문하는 게 아니었다. 그래서 lazy loading에 대한 개념을 조금 찾아봤다. 


lazt loading 이란?

페이지를 읽어들이는 시점에 중요하지 않은 리소스 로딩을 추후에 하는 기술이다. 대신, 이 때 불러오지 않은 (중요하지 않은) 리소스들은 필요할 때 로드가 된다. 따라서 내가 작성한 코드에서도 objects.all() 당시에는 당장 필요한 게 아니므로 잠시 미뤄뒀다가 page를 사용할 때, 10개씩 끊어서 불러올 수 있는 것이다. 

 

추가로 내 웹사이트에서 어떤 요소들이 Lazy loading 방식으로 구성되었는지에 따라서 SEO에도 영향을 미친다. 내 사이트에서 구성 요소를 느리게 로드하면 검색 엔진이 해당 구성 요소를 우회하고 넘어갈 수 있기 때문이다. 이 때, lazy load하는 컨텐츠의 링크를 제공하는 것으로 이 문제를 해결할 수 있다고 한다. 


ORM lazy loading

정리하자면 ORM에서는 정말 필요한 시점에만 SQL을 호출하는 특징이 있다. 위의 설명과 더불어서 아래의 예시를 보며 몇 번의 SQL문이 호출되었는지 생각해보자.

 

users = User.objects.all()
foods = Food.objects.all()
restaurants = Restaurants.objects.all()
list(users)

위의 코드에서는 1번의 SQL문이 실행되었다. 마지막 코드인 users 리스트를 만들 때이다. 

 

다음 코드도 한번 보자.

users = User.objects.all()
first_user = users[0]
user_list = list(users)

위의 코드에서 SQL문은 몇 번이나 실행되었을까? 2번이다. 2번째 줄과 3번째 줄에서 각각의 SQL문이 실행되었다.

 

보통은 변수에 무언가를 저장해두면 해당 변수에 저장된 값을 다시 계산하지 않아도 되므로, SQL문도 첫 번째 줄에서 한 번만 실행될 것이라고 생각한다. 하지만 ORM에서는 실제 해당 변수가 사용될 때마다 방문하기 때문에 여기서는 2번의 쿼리가 발생한다. 

 

그렇지만 내가 원하는 건 queryset을 두 번 부르는 게 아니다. 그렇다면 어떻게 해야 할까?

 


Caching(캐싱)

쿼리셋에서 SQL을 호출하면 그 데이터 결과를 가지고 있다. 이것을 QuerySet에서 Result Cache라고 부른다. 

 

만약 위의 코드에서 쿼리를 한 번만 호출하고 싶다면 QuerySet Caching을 재사용하면 된다.

users = User.objects.all()
user_list = list(user)
first_user = users[0]

2번 째 줄과 3번 째 줄의 순서를 바꿔주었다. 모든 유저들의 정보를 불러오는 쿼리가 2번 째 줄에서 실행이 되면서 실제로 users는 전체 유저 정보가 담긴 변수가 되었다. 따라서 그 이후부터는 users[0]만 해도 0번째 사용자를 이미 DB를 방문하여 얻어온 쿼리셋에서 불러올 수 있게 된다. 


QuerySet Caching 개념까지 알게 되어서 내가 작성한 코드들이 불필요하게 두 번씩 DB를 방문했던 것은 아닌지, 다시 한 번 점검해봐야 할 것 같다. 

댓글