GraphQL - Graphene: 7(Execution: Dataloader)

Jihun Kim·2022년 4월 30일
0

GraphQL

목록 보기
14/16
post-thumbnail

Dataloader

Dataloader를 이용하면 GraphQL에서 발생할 수 있는 N+1 problem을 해결할 수 있다.

  • Dataloader는 현재 사용하는 앱에서 데이터를 가져오는 데 사용할 수 있는 fetching layer로, 이를 이용하면 데이터베이스나 웹 서비스와 같은 데이터 소스로부터 batching 또는 caching을 통해 간결하고 일관된 API를 제공할 수 있다.
  • DataLoader는 batch data-loading(데이터 일괄 로딩)의 성능은 저하시키지 않으면서 애플리케이션의 불필요한 부분은 분리할 수 있도록 도와준다.
    - 이를 통해 애플리케이션은 데이터를 가져오기 위한 요구 사항을 만족시키면서 동시에 데이터를 가져오기 위한 최소한의 요청만 할 수 있다.

아래 예시는 graphene에서 제공한 예시로, Promise를 리턴한다.

예시

from promise import Promise
from promise.dataloader import DataLoader

class UserLoader(DataLoader):
    def batch_load_fn(self, keys):
        # Here we return a promise that will result on the
        # corresponding user for each key in keys
        return Promise.resolve([get_user(id=key) for key in keys])
  • batch loading function은 keys의 list를 인자로 받아 values의 리스트를 resolve 하는 Promise를 리턴한다.
  • DataLoader는 단일 실행 프레임 내에서 발생하는 모든 개별 load를 통합한 다음(wrapping promise가 resolve된 다음에 실행됨) 요청된 모든 키를 사용하여 배치 함수를 호출 한다.
          user_loader = UserLoader()

          user_loader.load(1).then(lambda user: user_loader.load(user.best_friend_id))

          user_loader.load(2).then(lambda user: user_loader.load(user.best_friend_id))
  • 일반 애플리케이션은 필요한 정보를 가져오기 위해 백엔드로 4번의 왕복을 할 것이다.
    - 그러나 DataLoader를 사용하면 이 애플리케이션은 최대 2번만 왕복해도 되기 때문에 훨씬 효율적이다.
    - 즉, 한 번의 요청으로 원하는 모든 데이터를 가져올 수 있다(N+1 problem 해결).
  • 주의할 점은 load 된 values는 keys와 일대일 대응 관계이며 같은 정렬 순서를 가져야 한다는 것이다.

Saleors에서 사용한 DataLoader 예시

아래는 saleor 오픈소스에서 사용한 예시이다.

dataloaders.py

from collections import defaultdict

from ..graphql.core.dataloaders import DataLoader
from .models import Payment

# DataLoader
class AddressByIdLoader(DataLoader):
    context_key = "address_by_id"

    def batch_load(self, keys):
        address_map = Address.objects.using(self.database_connection_name).in_bulk(keys)
        
        # 아직 Promise는 아니다.
        return [address_map.get(address_id) for address_id in keys]

# ObjectType
class Order(ModelObjectType):
    id = graphene.GlobalID(required=True)
    ...
    
    @staticmethod
    @traced_resolver
    def resolve_billing_address(root: models.Order, info):
        def _resolve_billing_address(data):
          	...
            
        	if root.user_id:
            	# user, address는 list이다.
            	user = UserByUserIdLoader(info.context).load(root.user_id)
            	address = AddressByIdLoader(info.context).load(root.billing_address_id)
                # Promise를 반환한다.
            	return Promise.all([user, address]).then(_resolve_billing_address)
  • 여기서는 self.database_connection_name를 read replica로 설정해 Address에 해당하는 데이터를 가져올 때 read replica를 사용하도록 했다.
  • DataLoader를 받는 dataloader를 정의한 뒤(AddressByIdLoader) batch_load 함수로 리스트를 리턴한다.
  • 실제로 resolve할 때는 AddressByIdLoader(info.context).load(root.billing_address_id)를 통해 데이터를 load한 뒤 이에 대한 Promise를 리턴한다.

GraphQL 요청 보내기

아래와 같은 GraphQL 요청을 보내야 한다고 가정해 보자.

{
  me {
    name
    bestFriend {
      name
    }
    friends(first: 5) {
      name
      bestFriend {
        name
      }
    }
  }
}
  • 가령 me 필드는 bestFriend, friends 필드를 가지고 있다.
    - 일반적인 경우 bestFriend에 대한 요청 따로, friends에 대한 요청 따로 백엔드로 보내야 한다(N+1 problem).
    - 이 경우 최대 13번까지 데이터베이스에 요청을 해야 되는 상황이 올 수 있다.
  • DataLoader를 사용하면 최대 4번의 데이터베이스 요청만으로 원하는 결과를 가져올 수 있다.
    - 위에서 정의했던 UserLoader 의 인스턴스를 user_loader로 받아 왔다.
    - 아래와 같이 사용하면 DataLoader는 UserLoader에 정의된 개별 load(best_friend_id, friend_ids)를 통합해 배치 함수를 1번 호출하게 된다.
      class User(graphene.ObjectType):
      	name = graphene.String()
      	best_friend = graphene.Field(lambda: User)
      	friends = graphene.List(lambda: User)

	    def resolve_best_friend(root, info):
    	      return user_loader.load(root.best_friend_id)

      	def resolve_friends(root, info):
        	  return user_loader.load_many(root.friend_ids)


참고
https://medium.com/open-graphql/solving-n-1-problem-with-dataloader-in-python-graphene-django-7a75d6c259ba

profile
쿄쿄

1개의 댓글

comment-user-thumbnail
2022년 8월 18일

안녕하세요. 쿄쿄님. 좋은 글 감사합니다.

답글 달기