ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • DataLoader
    Graphql 2022. 10. 9. 13:50

     기본적으로 graphQL에 대해 어느정도 알고 있다고 생각하고 작성하겠다.

    DataLoader란

     graphQL에서 Resolver를 사용할 때 N+1 문제가 생긴다. N+1 문제란 요청은 한번만 했지만 Resolver에 의해 N번의 트랜젝션이 발생할 수 있는 것이다.

     예를 들면, user, purchase, stuff 이 3개의 table이 있다고 가정해보고 gql 타입을 아래와 같이 정의했다고 가정하자.

    type User {
      id: String!
      name: String!
      email: String!
      purchases: [Purchase] # purchase를 조회하기 위한 Resolver가 있음
    }
    
    type Purchase {
      id: String!
      total_price: Int!
      date: Time!
      stuffs: [Stuff] #Stuff를 조회하기 위한 Resolver가 있음
    }
    
    type Stuff {
      name: String!
      price: Int!
      link: String!
    }
    
    Query {
      userDetail(id: String!): User!
    }

    이 상황에서 userDetail query를 호출하면 User정보를 위해 DB 조회 1회, 여러 Purchase 조회를 위한 DB 조회 1회 Purchase 하나마다 Stuff조회를 따로해야하므로 Purchase의 갯수만큼 Stuff 조회를 위한 DB 조회 N 번이 필요하다.

     이는 N이 커지면 커질수록 DB에 과부화를 줄 여지가 높아진다. 따라서 이를 해결하기 위해 DataLoader가 개발됐다.

    위의 예시에서 dataLoader는 purchase의 id를 key 값으로 사용하고 이 id를 배열로 모아서 1번의 트랜잭션 Or 연산으로 stuff를 찾을 것이다

    DataLoader의 동작 방식은 아주 간단하고 아래와 같다:

    1. Batch func을 선언한다.
      • 이는 dataloader 모듈에서 관리하는데 대략 몇 ms안에 들어온 명령에 대해 그 Key값을 모았다가 한번에 요청하도록 도와준다.
      • 사용 방법은 어느정도 차이가 있지만 아래에 golang으로 예시를 작성하겠다.
    2. middleware에서 context에 해당 batch func을 삽입한다.
    3. 필요한 resolver에서 dataLoader의 값을 Load해온다.

    주의할 점

    • dataLoader에 요청한 키 값의 길이와 데이터를 조회한 결과값의 길이가 달라서는 안된다. 즉, 위에 예시에 이어서 만약에 조회 할 때 purchase에 해당하는 stuff가 하나도 존재하지 않아서 Null 값을 return할 수 있다. 이 때, 생략해서는 안된다.

    Golang 코드 예시

    graph/initialize.go

    package graph
    
    import (
    	"context"
    	"log"
    	"net/http"
    	"os"
    
    	"github.com/99designs/gqlgen/graphql/handler"
    	"github.com/99designs/gqlgen/graphql/handler/transport"
    	"github.com/99designs/gqlgen/graphql/playground"
    	"github.com/caveakorea/goback.git/graph/generated"
    	"github.com/caveakorea/goback.git/service"
    	"github.com/go-chi/chi"
    	"github.com/gorilla/websocket"
    	"github.com/graph-gophers/dataloader"
    	"github.com/rs/cors"
    )
    
    type ctxKey string
    
    const (
    	loadersKey = ctxKey("dataloaders")
    	uidKey     = ctxKey("uid")
    )
    
    var loader *Loaders
    
    // Loaders wrap your data loaders to inject via middleware
    type Loaders struct {
    	VenuesByEventLoader *dataloader.Loader
    }
    
    // NewLoaders instantiates data loaders for the middleware
    func NewLoaders() *Loaders {
    	// define the data loader
    	loaders := &Loaders{
    		VenuesByEventLoader: dataloader.NewBatchedLoader(service.GetVenuesByEvents),
    	}
    	return loaders
    }
    
    func initializeGraphQLServer() {
    	loader = NewLoaders()
    }
    
    func startGraphQLServer(port string) {
    	router := chi.NewRouter()
    	debugVar := true
    	if len(os.Args) > 1 {
    		debugVar = false
    	}
    	router.Use(cors.New(cors.Options{
    		AllowedOrigins:   []string{"*"},
    		AllowedHeaders:   []string{"*"},
    		AllowCredentials: true,
    		Debug:            debugVar,
    	}).Handler)
    	router.Use(Middleware())
    	srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &Resolver{}}))
    	srv.AddTransport(&transport.Websocket{
    		Upgrader: websocket.Upgrader{
    			CheckOrigin: func(r *http.Request) bool {
    				// Check against your desired domains here
    				log.Println(r.Host)
    				return r.Host == "studio.apollographql.com"
    			},
    			ReadBufferSize:  1024,
    			WriteBufferSize: 1024,
    		},
    	})
    	router.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
    	router.Handle("/query", srv)
    	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    	log.Fatal(http.ListenAndServe(":"+port, router))
    }
    
    func InitializeController() {
    	initializeGraphQLServer()
    }
    
    func StartController(graphQLPort string) {
    	startGraphQLServer(graphQLPort)
    }
    
    // Middleware decodes the share session cookie and packs the session into context
    func Middleware() func(http.Handler) http.Handler {
    	return func(next http.Handler) http.Handler {
    		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    			uid := r.Header.Get("Uid")
    			ctx := context.WithValue(r.Context(), "uid", uid)
    			r = r.WithContext(ctx)
    			ctx = context.WithValue(r.Context(), "dataloaders", loader)
    			r = r.WithContext(ctx)
    			next.ServeHTTP(w, r)
    		})
    	}
    }

    resolver.go

    func (r *eventResolver) Venue(ctx context.Context, obj *model.Event) (*model.VenueDetail, error) {
    	// 여기에 데이터로더 적용해보자
    	loaders := ctx.Value("dataloaders").(*Loaders)
    	thunk := loaders.VenuesByEventLoader.Load(ctx, dataloader.StringKey(strconv.FormatInt(obj.UID, 10)))
    	result, err := thunk()
    	if err != nil || result == nil {
    		return nil, err
    	}
    	return result.(*model.VenueDetail), nil
    }

    service/venue.go

    func GetVenuesByEvents(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
    	store := infrastructure.GetStore()
    	event_ids := make([]int64, len(keys))
    	for ix, key := range keys {
    		val, err := strconv.ParseInt(key.String(), 10, 64)
    		if err != nil {
    			event_ids[ix] = 0
    		} else {
    			event_ids[ix] = val
    		}
    	}
    	venues, err := store.GetVenueByEventIDs(ctx, event_ids)
    	if err != nil {
    		return []*dataloader.Result{}
    	}
    	// return users in the same order requested
    	output := []*dataloader.Result{}
    	venuesMap := map[string]int{}
    	for idx, venue := range venues {
    		venuesMap[strconv.FormatInt(venue.EveID, 10)] = idx
    	}
    	for _, key := range keys {
    		if _, ok := venuesMap[key.String()]; !ok { // 찾지 못하더라도 nil 넣어줘야함
    			output = append(output, &dataloader.Result{Data: nil, Error: nil})
    			continue
    		}
    		venue := venues[venuesMap[key.String()]]
    		rate, err := strconv.ParseFloat(venue.Rate, 64)
    		if err != nil {
    			log.Println("check DB Rate EVENT UID: ", venue.Uid)
    		}
    		output = append(output, &dataloader.Result{Data: &model.VenueDetail{
    			...
    		}, Error: nil})
    	}
    	return output
    }

    실제 코드를 붙여넣은거라 적용하려면 본인의 코드에 맞게 바꾸면 될 듯 하다.

    'Graphql' 카테고리의 다른 글

    GraphQL에 대하여 ( Resolver & Dataloader / Aliases & Fragment)  (1) 2023.06.03
    GraphQL에 대하여 (Interface & Union Type)  (0) 2023.05.07
    GraphQL (Schema & Type)  (0) 2023.04.23
    GraphQL이란  (0) 2023.04.16

    댓글

Designed by Tistory.